美文网首页
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-学习笔记(二)

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

  • WebGL-学习笔记(一)

    反正不管你信不信,我觉得WebGL是接下来一个时代的流量入口啦,谁不喜欢酷炫的东西,所以本着跟着时代步伐的精神,终...

  • WebGL-学习笔记(四)

    最近学习构建三维图形的时候,深感几何功底不够,一个视图变化矩阵看了几天也没想过来,只勉强理解原理,细节部分自己还需...

  • WebGL-学习笔记(五)

    1. 光照和反射 要知道看到的物体的颜色实际上是物体反射的光的颜色,物体吸收了部分频率的光,将不能吸收的光进行了反...

  • WebGL-学习笔记(三)

    在利用缓冲区并在学会利用 mode绘制图形动画以后,继续研究二维图形的颜色渲染以及纹理操作 1. 颜色渲染 1.1...

  • JavaScript学习笔记二

    JavaScript学习笔记二 个人学习笔记参考阮一峰的JavaScript教学学习笔记二是对学习笔记一的补充 J...

  • MySQL 学习实践笔记(四)

    MySQL 学习实践系列 MySQL 学习实践笔记(一) MySQL 学习实践笔记(二) MySQL 学习实践笔记...

  • MySQL 学习实践笔记(三)

    MySQL 学习实践系列 MySQL 学习实践笔记(一) MySQL 学习实践笔记(二) MySQL 学习实践笔记...

  • MySQL 学习实践笔记(二)

    MySQL 学习实践系列 MySQL 学习实践笔记(一) MySQL 学习实践笔记(二) MySQL 学习实践笔记...

  • MySQL 学习实践笔记(一)

    MySQL 学习实践系列 MySQL 学习实践笔记(一) MySQL 学习实践笔记(二) MySQL 学习实践笔记...

网友评论

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

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