美文网首页
WebGL-学习笔记(二)

WebGL-学习笔记(二)

作者: Patrick浩 | 来源:发表于2018-03-19 00:11 被阅读0次
    WebGL学习笔记(二).png

    构成三维模型的基本图形是三角形,所以接下来就从如何绘制一个三角形开始,之后涉及到图形的变换和动画。

    1. 图形绘制

    先回顾以下绘制单个点的方式:通过gl.getAttribLocati on()获得了GLSL中的Vertex着色器的属性值,并利用gl.vertexArrib[1234]f()方法簇给着色器属性赋值,并将值传递给GLSL的内置变量gl_Position,之后调用gl.drawArrays(gl.POINT, 0, 1)的方法绘制点。
    如果要绘制多个点怎么办?当然,我们可以设置多次调用gl.vertexAttrib[1234]fgl.drawArrays(gl.POINT, 0, 1)的方式来实现绘制多个点,例如:

    html:
      <canvas id="glCanvas" width="640" height="480"></canvas>
    Javascript:
    const canvas = document.querySelector('#glCanvas');
    const gl = canvas.getContext('webgl');
    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        void main() {
            gl_Position = a_Position;
            gl_PointSize = a_PointSize;
        }
        `;
    const FSHADER_SOURCE = `
        precision mediump float;
        uniform vec4 u_FragColor;
        void main() {
            gl_FragColor = u_FragColor;
        }
        `
    let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
    // 获取顶点位置的属性
    let a_Position = gl.getAttribLocation(program, 'a_Position');
    if (a_Position < 0) {
        console.log('Cant find the position');
        return;
    }
    // 设置顶点点的尺寸
    let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
        if (a_PointSize < 0) {
            console.log('Cant find the pointsize');
            return;
        }
    gl.vertexAttrib1f(a_PointSize, 10.0);
    // 设置顶点颜色
    let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
    gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    // 设置并绘制多个顶点
    drawPoint();
    // 绘制多个点的方法
    function drawPoint(gl, a_Position) {
        gl.vertexAttrib3f(a_Position, -0.5, -0.5, 0.0);
        gl.drawArrays(gl.POINT, 0, 1);
        gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);
        gl.drawArrays(gl.POINT, 0, 1);
        gl.vertexAttrib3f(a_Position, 0.5, -0.5, 0.0);
        gl.drawArrays(gl.POINT, 0, 1);
    }
    

    这样反复调用绘制的方案明显效率低下(反复重绘整个画布),另外如果我们想绘制其他图形,建立点之间点连接关系,似乎就没有办法了(因为每次绘制的点都是独立的)。为了解决着两个问题,就需要用到WebGL提供的缓冲区来一次性存储多种信息(不仅仅存储顶点坐标,还可以一次性存入顶点颜色等信息,后面再讨论)

    1.1 利用缓冲区绘制多个点

    WebGL中要使用缓冲区,主要有以下五个步骤:

    1. 利用gl.createBuffer()创建缓冲区对象
    2. 利用gl.bindBuffer()将创建的缓冲区对象和WebGL中的内置对象gl.ARRAY_BUFFER进行绑定
    3. 利用gl.bufferData()gl.ARRAY_BUFFER内置对象传递数据(数据不能直接传递给缓冲区对象,要通过gl.ARRAY_BUFFER来进行)
    4. 利用gl.vertexAttribPointer()将缓冲区数据分配给GLSL中的变量
    5. 最后利用gl.enableVertexAttribArray()开启缓冲区对变量的使用,开启后gl.vertexAttrib[1234]f()方法簇的赋值将失效

    完成上述操作后,调用gl.drawArrays()来就可以一次绘制缓冲区数据了,这时只要将第三个参数变为所需要绘制点的数目就可以了。根据前一个例子,这里将drawPoint()进行调整为使用缓冲区

    function initPoint(gl, a_Position) {
        let pointData = new Float32Array([
            -0.5, -0.5,
            0, 0.5,
            0.5, -0.5
        ]);
        // 创建缓冲区对象
        let buffer = gl.createBuffer();
        // 绑定缓冲区对象
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        // 将数据传递到缓冲区
        gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
        // 将缓冲区数据传递给顶点着色器attribute属性
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
        // 激活顶点和缓冲区到连接
        gl.enableVertexAttribArray(a_Position);
        // 一次性绘制多个点
        gl.drawArrays(gl.POINT, 0, 3);
    }
    

    PS:代码中使用了Float32Array来创建点的数据对象,它是Javascript提供的一种类型化数组,目的是为了说明数组中的所有数据都是同一种数据类型的特殊数组,使处理数组效率更快(可能不用做类型判断和转化了),其中类型化数组提供了很多方法和属性,尤其BYTES_PER_ELEMENT属性在之后会有很大用处的。

    1.2 利用mode控制图形绘制

    在使用缓冲区的基础上,绘制图形就很简单了,只需要改变gl.drawArrays()中的第一个参数就可以了
    WebGL的第一个参数mode,提供了7种值:

    • gl.POINT: 绘制点
    • gl.LINES: 绘制线段
    • gl.LINE_STRIP:绘制连续线段,例如传入[A0, A1, A2, A3]四个坐标信息,那么绘制结果为[A0, A1], [A1, A2], [A2, A3]
    • gl.LINE_LOOP:首位两个点会连接起来
    • gl.TRIANGLES:绘制三角形
    • gl.TRIANGLE_STRIP:绘制一系列三角形,例如传入[A0, A1, A2, A3, A4, A5]五个坐标信息,那么绘制结果为[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中绘制是按照逆时针方式进行绘制的)
    • gl.TRIANGLE_FAN:以第一个点为所有三角形顶点,绘制三角扇

    2. 图形变换

    我们经常会将绘制的图形进行平移,旋转,缩放的操作,这些操作统一被称为仿射变化。

    2.1 基本变换

    2.1.1 平移

    平移要实现的其实是方向坐标上的位移:

    x' = x + ux;
    y' = y + uy;
    

    因为GLSL中矢量可以直接进行加减运算,所以修改顶点着色器程序:

    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        void main() {
            // 矢量可以直接进行运算
            gl_Position = a_Position + vec4(0.1, 0.1, 0.0, 0.0); 
            gl_PointSize = a_PointSize;
        }
    `;
    

    如果想要自定义平移距离,那么可以在顶点着色器程序中新增一个uniform变量(使用uniform是因为变量本身与顶点无关)

    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        unifrom vec4 u_Translate;
        void main() {
            // 矢量可以直接进行运算
            gl_Position = a_Position + u_Translate; 
            gl_PointSize = a_PointSize;
        }
    `;
    // 设置平移距离
    let u_Translate = gl.getUniformLocation(program, 'u_Translate');
    gl.uniform4f(u_Translate, 0.1, 0.1, 0.0, 0.0);
    

    PS:要注意因为是使用齐次坐标,所以最后一个变量值要传递为0.0(因为默认齐次坐标的第四个变量为1.0,矢量计算后最后一个变量仍然应该为1.0)

    2.1.2 缩放

    缩放要实现的是方向坐标上的比例变化

    x' = ux;
    y' = uy;
    

    实现缩放的关键是要获取坐标在某些方向上的分量,可以通过a_Position.xa_Position.y来分别获取xy方向上的分量,修改着色器程序:

    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        void main() {
            // 分别设置四个方向上的分量信息
            gl_Position.x = a_Position.x * 0.5; 
            gl_Position.y = a_Position.y * 0.5; 
            gl_Position.z = a_Position.z;
            gl_Position.w = 1.0;
            gl_PointSize = a_PointSize;
        }
    `;
    
    2.1.3 旋转

    旋转相比起来就复杂的多了,旋转必须指明三个要素,旋转轴,旋转方向和旋转角度。
    WebGL中旋转正方向是逆时针方向,遵循右手旋转法则,也就是大拇指朝向轴的正方向,四指方向就是旋转正方向。
    以仅绕z轴旋转为例,那么如果坐标系中的点的原角度为a(与x轴正方向角度),转动角度为b,我们可以利用三角函数得到

    x = r*cos(a);
    y = r*sin(a);
    // 利用三角函数和角公式进行变化
    x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y* sin(b);
    y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b)
    

    于是同样通过获取分量的方式,将顶点着色器程序进行修改

    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        uniform float u_Cosb, u_Sinb;
        void main() {
            // 分别设置四个方向上的分量信息
            gl_Position.x = a_Position.x * u_Cosb - a_Position.y * u_Sinb
            gl_Position.y = a_Position.x * * u_Sinb + a_Position.y * u_Cosb; 
            gl_Position.z = a_Position.z;
            gl_Position.w = 1.0;
            gl_PointSize = a_PointSize;
        }
    `;
    // 转动30度
    let rad = 90 / 180 * Math.PI;
    let u_Sinb = gl.getUniformLocation(program, 'u_Sinb');
    gl.uniform1f(u_Sinb, Math.sin(rad));
    let u_Cosb = gl.getUniformLocation(program, 'u_Cosb');
    gl.uniform1f(u_Cosb, Math.cos(rad));
    

    2.2 矩阵变换

    所有的变换其实都是平移,旋转,缩放的叠加,但是按照之前的方式来进行那么在变换叠加的时候,计算过程就变得麻烦了,例如,我们要先旋转后平移再缩放。。。由于WebGL支持矩阵运算,所以变换可以使用矩阵变化来处理。
    具体矩阵运算可以参见矩阵(其实也就是将方程转换为了矩阵的方式)
    在WebGL中,要使用变换矩阵主要要进行四步骤:

    1. 顶点着色器程序中增加矩阵变量uniform mat4 u_Matrix
    2. 顶点着色器程序中修改gl_Position = u_Matrix * a_Position(注意矩阵计算时的顺序)
    3. 创建矩阵变换的数组new Float32Array()
    4. 使用gl.unifromMatrix[1234]fv的方法设置矩阵变换uniform变量的值

    以旋转变化为例,那么:

      // 着色器程序
      const VSHADER_SOURCE = `
          attribute vec4 a_Position;
          attribute float a_PointSize;
          uniform mat4 u_Matrix;
          void main() {
              gl_Position = u_Matrix * a_Position;
              gl_PointSize = a_PointSize;
          }
      `;
    let rad = 90 / 180 * Math.PI;
    let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
    let matrix = new Float32Array([
        Math.cos(rad), Math.sin(rad), 0.0, 0.0,
        -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    ])
    gl.uniformMatrix4fv(u_Matrix, false, matrix);
    

    PS:特别注意,数组中存储二维数组有两种顺序,按列主序和按行主序,WebGL和OpenGL中都是按列主序,所以,注意数组中的数据和真是矩阵中数据位置存在转置(gl.uniformMatrix4fv的第二个参数可以实现矩阵转置,但是WebGL中并没有实现转置操作所以始终默认为false

    3. 动画

    3.1 利用requestAnimationFrame进行动画

    动画基本上就是基于图形的各种变换的持续执行过程,于是在webGL中要实现动画,实际上就是在图形变换的基础上,调用了动画函数进行循环调用,不断重新绘制图形。可以使用setInterval函数也可以使用requestAnimationFrame来实现动画的循环调用,不过建议使用后者,因为后者只有在浏览器tab页激活的时候才会执行,而前者会一直执行。还是以旋转为例,下面是持续旋转的例子:

        // 着色器程序
        const VSHADER_SOURCE = `
            attribute vec4 a_Position;
            attribute float a_PointSize;
            uniform mat4 u_Matrix;
            void main() {
                gl_Position = u_Matrix * a_Position;
                gl_PointSize = a_PointSize;
            }
        `;
        let start = Date.now();
        let rad = 0;
        animation();
        // 旋转动画
        function animation() {
            let now = Date.now();
            let offsetTime = now - start;
            start = now;
            // 假设每秒钟转动30度
            rad += offsetTime * 30 / 360 / 1000 * Math.PI;
            let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
            let matrix = new Float32Array([
                Math.cos(rad), Math.sin(rad), 0.0, 0.0,
                -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
                0.0, 0.0, 1.0, 0.0,
                0.0, 0.0, 0.0, 1.0
            ])
            gl.uniformMatrix4fv(u_Matrix, false, matrix);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.LINE_LOOP, 0, 3);
            requestAnimationFrame(animation);
        }
    }
    
    1. 总结
      一开始我总觉得自己在旋转的过程中图形形状发生了变化,很奇怪,最后花了很多事件才发现原来demo中的canvas不是正方形。。。所以在进行旋转操作的时候,x轴和y轴并不是相同的单位比例。
    2. 参考
      《WebGL编程指南》

    相关文章

      网友评论

          本文标题:WebGL-学习笔记(二)

          本文链接:https://www.haomeiwen.com/subject/pcddfftx.html