WebGL系列(4):开启三维世界

1,254 阅读4分钟

前面几个章节渲染的都是二维平面的图形,但是WebGL真正的作用在于3D图形,那么本文就开始介绍,如何使用WebGL绘制三维图形。

实际上,三维图形就是由二维图形组成的,说的更明白点,三维图形就是由多个三角形组成的。下面我们就来介绍三维图形相关的一些基础知识。

一、视点和视线

二维图形是平面的,所以,无论我们从哪个角度观察,看到都一样的图形。但是三维图形不同,我们所看到的三维物体的样子,与我们观察的位置相关。

我们将观察者所处的位置称为视点(eye point),从视点出发沿着观察方向的射线称作视线(view direction)

为了确定观察者的状态,我们需要获取以下信息:

  • 视点(eye point):观察者所在三维空间中的位置,视线的起点。即:(eyeX, eyeY, eyeZ)
  • 观察目标点(look-at point):被观察目标所在的点。即(atX, atY, atZ)。视线从视点出发,穿过观察目标点并继续延伸。
  • 上方向(up direction):最终绘制在屏幕上的影像中的向上的方向。即(upX, upY, upZ)。如果只确定了视点和观察目标点,观察者还是可能以视线为轴旋转的。所以为了将观察者固定住,需要指定上方向。

在 WebGL 中,观察者的默认状态应该如下:

  • 视点位于坐标系统原点(0,0,0)
  • 视线为Z轴负方向,观察点为(0,0,-1)
  • 上方向为Y轴负方向,即(0,1,0)

可以根据上述三个矢量创建一个视图矩阵(view matrix),然后将该矩阵传递给顶点着色器。

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标和颜色
    const verticesColors = new Float32Array([
        0.0, 0.5, -0.4, 0.4, 1.0, 0.4,
        -0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
        0.5, -0.5, -0.4, 1.0, 0.4, 0.4,

        0.5, 0.4, -0.2, 1.0, 0.4, 0.4,
        -0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
        0.0, -0.6, -0.2, 1.0, 1.0, 0.4,

        0.0, 0.5, 0.0, 0.4, 0.4, 1.0,
        -0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
        0.5, -0.5, 0.0, 1.0, 0.4, 0.4,
    ]);
    const n = 9;  // 点的个数
    
    // 创建坐标缓冲区对象
    const vertexColorBuffer  =gl.createBuffer();
    if (!vertexColorBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
    const FSIZE = verticesColors.BYTES_PER_ELEMENT;
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    // 获取 attribute 变量的存储位置a_Color
    const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    // 判断是否获取成功
    if (a_Color < 0) {
        console.log('获取 a_Color 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_PointSize 变量
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
    // 连接 a_PointSize 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Color);

    return n;

}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'uniform mat4 u_ViewMatrix;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = u_ViewMatrix * a_Position;\n' +   // 设置顶点坐标
    '  v_Color = a_Color;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    // 获取canvas元素
    const canvas = document.getElementById('gl');
    // 获取WebGL绘图上下文
    const gl = canvas.getContext('webgl');
    // 确认WebGL支持性
    if (!gl) {
        console.log('浏览器不支持WebGL');
        return;
    }
    // 初始化着色器
    if(!initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('初始化着色器失败!');
        return;
    }

    // 设置顶点位置
    const n = initVertexBuffer(gl);
    if (n < 0) {
        console.log('设置顶点位置失败!');
        return;
    } 

    // 获取 u_ViewMatrix变量的存储地址
    const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
    if (u_ViewMatrix < 0) {
        console.log('u_ViewMatrix变量的存储地址获取失败!');
        return;
    }
    // 设置视点、视线和上方向
    const viewMatrix = new Matrix4();
    viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
    // 将试图矩阵传递给u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT); 

    // 绘制一个点
    gl.drawArrays(gl.TRIANGLES , 0, n);
}

image.png

从着色器代码gl_Position = u_ViewMatrix * a_Position,改变观察者的状态其实也是通过视图矩阵乘以原有坐标实现的。记得前面我们曾学过,可以通过变换矩阵,对图形进行平移、旋转、缩放等变换。但实际上:

  • 根据自定义的观察者状态,绘制观察者看到的景象
  • 使用默认的观察状态,对三维对象进行平移、旋转等变换,再绘制观察者看到的景象

这两种行为是等价的。即“改变观察者的状态”和“对整个世界进行平移和旋转变换”本质上是一样的,都可以通过矩阵来描述。

二、可视范围

在 WebGL 中,只有在可视范围内的对象才会被绘制。绘制可视范围外的对象没有一样,即使绘制出来也不会在屏幕上显示。实际上,人类也只能看到眼前的东西,水平视角大约200度左右。WebGL就是以类似的方式,只绘制可视范围内的三维对象。

水平视角、垂直视角、可视深度,定义了可视空间(view volume)

常用的可视空间有如下两种:

  • 长方体可视空间,也称盒状空间,由正射投影(orthographic projection) 产生。
  • 四棱锥/金字塔可视空间,由透视投影(perspective projection) 产生。

2.1 可视空间(正射投影)

盒装可视空间由前后两个矩形表面确定,分别称为近裁剪面(near clipping plane)远裁剪面(far clipping plane)

  • 近裁剪面的四个顶点:(right, top, -near), (-left, top, -near), (-left, -bottom, -near), (right, -bottom, -near)
  • 远裁剪面的四个顶点:(right, top, far), (-left, top, far), (-left, -bottom, far), (right, -bottom, far)

820cec5c8cb4aba4de85332e9f9f02b.jpg

<canvas>上显示的是可视空间中物体在近裁剪面上的投影。近裁剪面和远裁剪面之间的盒状空间就是可视空间,只有在此空间内的物体会被显示出来。如果某个物体一部分在可视空间内,一部分在其外,那么只显示空间内的部分。

我们可以通过正射投影矩阵(orthographic projection matrix) 来定义盒状可视空间。数学公式定义如下:

[2rightleft00right+leftrightleft02topbottom0top+bottomtopbottom002farnearfar+nearfarnear0001]\begin{bmatrix} \frac{2}{right-left}& 0 & 0 & -\frac{right + left}{right - left}\\\\ 0 & \frac{2}{top-bottom} & 0 & -\frac{top + bottom}{top - bottom}\\\\ 0 & 0 & -\frac{2}{far - near} & -\frac{far + near}{far - near}\\\\ 0 & 0 & 0 & 1 \end{bmatrix}

如下程序:

let g_near = 0.0, g_far = 0.5 // 视点
function keydown(ev, gl, n, u_ProjMatrix, projMatrix) {
    switch(ev.keyCode) {
        case 39: g_near += 0.01; break;
        case 37: g_near -= 0.01; break;
        case 38: g_far += 0.01; break;
        case 40: g_far -= 0.01; break;
        default: return;
    }
    draw(gl, n, u_ProjMatrix, projMatrix);
}

function draw(gl, n, u_ProjMatrix, projMatrix) {
    projMatrix.setOrtho(-1, 1, -1, 1, g_near, g_far);
    // 将试图矩阵传递给u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT); 

    // 绘制一个点
    gl.drawArrays(gl.TRIANGLES , 0, n);
}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'uniform mat4 u_ProjMatrix;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = u_ProjMatrix * a_Position;\n' +   // 设置顶点坐标
    '  v_Color = a_Color;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    ...
    // 获取 u_ProjMatrix 变量的存储地址
    const u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
    if (u_ProjMatrix < 0) {
        console.log('u_ProjMatrix变量的存储地址获取失败!');
        return;
    }
    // 设置视点、视线和上方向
    const projMatrix = new Matrix4();

    document.onkeydown = (ev) => keydown(ev, gl, n, u_ProjMatrix, projMatrix);

    draw(gl, n, u_ProjMatrix, projMatrix);
    
}

image.png

如上图所示,这是初始观察到的图形,但随着near的增大或far的减小,可视空间逐渐减小,我们所观察到的图形也逐渐减小,最后什么也观察不到。

2.2 可视空间(透视投影)

透视投影的核心在于“近大远小”,具体如下图所示:

2f70a901d89dda01a80ba4be60e0d94.jpg

图中相关参数解释如下:

  • fov:垂直视角,即可视空间顶面和底面间的夹角
  • sapect:近裁剪面的宽高比(宽度/高度)
  • near, far:近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界。(near 和 far 必须大于0,且 near 必须小于 far)

根据上述参数我们可以得到一个透视投影矩阵(perspective projection matrix) 来定义透视投影可视空间。具体公式如下:

[1aspecttan(fov2)00  001tan(fov2)0000far+nearfarnear2farnearfarnear0001]\begin{bmatrix} \frac{1}{aspect*tan(\frac{fov}{2})} & 0 & 0\ \ 0\\\\ 0 & \frac{1}{tan(\frac{fov}{2})} & 0 & 0\\\\ 0 & 0 & -\frac{far + near}{far - near} & -\frac{2*far*near}{far - near}\\\\ 0 & 0 & 0 & 1 \end{bmatrix}
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标和颜色
    const verticesColors = new Float32Array([
        0.75, 1.0, -4.0, 0.4, 1.0, 0.4,
        0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
        1.25, -1.0, -4.0, 1.0, 0.4, 0.4,

        0.75, 1.0, -2.0, 1.0, 1.0, 0.4,
        0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
        1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

        0.75, 1.0, 0.0, 0.4, 0.4, 1.0,
        0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, -4.0, 0.4, 1.0, 0.4,
        -1.25, -1.0, -4.0, 0.4, 1.0, 0.4,
        -0.25, -1.0, -4.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, -2.0, 1.0, 1.0, 0.4,
        -1.25, -1.0, -2.0, 1.0, 1.0, 0.4,
        -0.25, -1.0, -2.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, 0.0, 0.4, 0.4, 1.0,
        -1.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        -0.25, -1.0, 0.0, 1.0, 0.4, 0.4,
    ]);
    const n = 18;  // 点的个数
    
    ...
}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'uniform mat4 u_ViewMatrix;\n' +
    'uniform mat4 u_ProjMatrix;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n' +   // 设置顶点坐标
    '  v_Color = a_Color;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    ...
    // 获取 u_ViewMatrix变量的存储地址
    const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
    if (u_ViewMatrix < 0) {
        console.log('u_ViewMatrix变量的存储地址获取失败!');
        return;
    }
    // 获取 u_ProjMatrix 变量的存储地址
    const u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
    if (u_ProjMatrix < 0) {
        console.log('u_ProjMatrix变量的存储地址获取失败!');
        return;
    }

    const viewMatrix = new Matrix4();
    const projMatrix = new Matrix4();

    // 计算视图矩阵和投影矩阵
    viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 10, 0)
    projMatrix.setPerspective(30, canvas.clientWidth / canvas.clientHeight, 1, 100);
    // 将视图矩阵和投影矩阵传递给u_ViewMatrix和u_ProjMatrix变量
    gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
    gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT); 

    // 绘制一个点
    gl.drawArrays(gl.TRIANGLES , 0, n);
    
}

image.png

如上图所示,在透视投影视图下,远处的三角形会看起来较小一些,而且更加贴近视线。但其实这些三角形的大小都是相同的,透视投影矩阵对三角形进行了两次变换:

  • 根据三角形与视点的距离,按比例对三角形进行了缩小变换
  • 对三角形进行平移变换,使其贴近视线

三、对象的遮挡关系

在现实世界中,如果一个对象在另一个对象前面,那么这个对象势必会对后面的对象有所遮挡。如上述例子中,前面的三角形会遮挡后面的三角形。

但是在WebGl中,不能自动分析出三维对象的远近,并正确处理对象遮挡关系。默认情况下,WebGL 为了加速绘图操作,是按照缓冲区中的顺序来绘制图形。例如我对上述程序中的缓冲区数据做如下修改:

const verticesColors = new Float32Array([
        0.75, 1.0, 0.0, 0.4, 0.4, 1.0,
        0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

        0.75, 1.0, -2.0, 1.0, 1.0, 0.4,
        0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
        1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

        0.75, 1.0, -4.0, 0.4, 1.0, 0.4,
        0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
        1.25, -1.0, -4.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, -4.0, 0.4, 1.0, 0.4,
        -1.25, -1.0, -4.0, 0.4, 1.0, 0.4,
        -0.25, -1.0, -4.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, -2.0, 1.0, 1.0, 0.4,
        -1.25, -1.0, -2.0, 1.0, 1.0, 0.4,
        -0.25, -1.0, -2.0, 1.0, 0.4, 0.4,

        -0.75, 1.0, 0.0, 0.4, 0.4, 1.0,
        -1.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        -0.25, -1.0, 0.0, 1.0, 0.4, 0.4,
    ]);

绘制的图形如下,可以看到本该出现在最远的图形却遮挡住近处的图形。

image.png

3.1 隐藏面消除

对此,WebGL提供了隐藏面消除(hidden surface removal) 功能,用于消除那些被遮挡的表面。

开启隐藏面消除功能,需要执行如下两步:

  • 开启隐藏面消除功能:gl.enable(gl.DEPTH_TEST)
  • 绘制之前,清除深度缓冲区:gl.clear(gl.DEPTH_BUFFER_BIT)
// 开启隐藏面消除
gl.enable(gl.DEPTH_TEST);
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 

3.2 深度冲突

当几何图形或物体的两个表面过于接近时,深度缓冲区有限的精度已经不能区分哪个在前,哪个在后。这种情况就是深度冲突。对于这个问题,WebGL提供了一种称为多边形偏移(polygon offset) 的机制来解决这个问题。

该机制将自动在Z值加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定。启动该机制需要以下两步:

  • 启动多边形偏移:gl.enable(gl.POLYGON_OFFSET_FILL)
  • 在绘制之前指定用来计算偏移量的参数:gl.polygonOffset(1.0, 1.0)

gl.polygonOffset(factor, units)
指定加到每个顶点绘制后 z 值上的偏移量,偏移量按照公式 m*factor + r*units计算,其中 m 表示顶点所在的表面相对于观察者的视线的角度,而 r 表示硬件能区分两个 z 值之差的最小值。

四、立方体

接下来我们绘制一个真正意义上的三维图形——立方体。我们知道,立方体有六面,也就意味着我们需要绘制六个矩形表面,而一个矩形又是由两个三角形组成,三角形有三个点,也就是说我们绘制一个立方体需要定义36个顶点的坐标。但是实际上,立方体的顶点只有8个,也就是说,需要定义的36个顶点中存在重复的点。

那么为了避免重复的点,WebGL 提供了一种新的绘制函数drawElements(),具体如下:

gl.drawElements(mode, count, type, offset)
按照 mode 指定的方式,根据绑定到 gl.ELEMENT_ARRAY_BUFFER的缓冲区中的顶点索引值绘制图形。

  • 参数
    • mode:指定绘制方式。即:gl.POINTSgl.LINESgl.LINE_STRIPgl.LINE_LOOPgl.TRIANGLESgl.TRIANGLE_STRIPgl.TRIANGLE_FAN
    • count:指定绘制顶点的个数
    • type:指定索引值数据的类型,gl.UNSIGNED_BYTEgl.UNSIGNED_SHORT
    • offset:指定索引数组中开始绘制的位置,以字节为单位
  • 返回值:无

下面我们就用该函数绘制一个正方体:

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标和颜色
    const verticesColors = new Float32Array([
        1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
        -1.0, 1.0, 1.0, 1.0, 0.0, 1.0,
        -1.0, -1.0, 1.0, 1.0, 0.0, 0.0,
        1.0, -1.0, 1.0, 0.5, 0.0, 0.0,
        1.0, -1.0, -1.0, 1.0, 0.5, 0.0,
        1.0, 1.0, -1.0, 0.2, 0.5, 0.0,
        -1.0, 1.0, -1.0, 1.0, 0.5, 0.5,
        -1.0, -1.0, -1.0, 0.5, 0.5, 0.0
    ]);
    // 顶点索引
    const indices = new Uint8Array([
        0, 1, 2, 0, 2, 3,  // 前
        0, 3, 4, 0, 4, 5,  // 右
        0, 5, 6, 0, 6, 1,  // 上
        1, 6, 7, 1, 7, 2,  // 左
        7, 4, 3, 7, 3, 2,  // 下
        4, 7, 6, 4, 6, 5   // 后
    ])
    
    // 创建坐标缓冲区对象
    const vertexColorBuffer  =gl.createBuffer();
    if (!vertexColorBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 创建索引缓冲区
    const indexBuffer  =gl.createBuffer();
    if (!indexBuffer) {
        console.log('创建索引缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
    const FSIZE = verticesColors.BYTES_PER_ELEMENT;
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    // 获取 attribute 变量的存储位置a_Color
    const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    // 判断是否获取成功
    if (a_Color < 0) {
        console.log('获取 a_Color 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_PointSize 变量
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
    // 连接 a_PointSize 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Color);

    // 将顶点索引数据写入缓冲区对象
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
    return indices.length;
}


// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'uniform mat4 u_MvpMatrix;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = u_MvpMatrix * a_Position;\n' +   // 设置顶点坐标
    '  v_Color = a_Color;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    // 获取canvas元素
    const canvas = document.getElementById('gl');
    // 获取WebGL绘图上下文
    const gl = canvas.getContext('webgl');
    // 确认WebGL支持性
    if (!gl) {
        console.log('浏览器不支持WebGL');
        return;
    }
    // 初始化着色器
    if(!initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('初始化着色器失败!');
        return;
    }

    // 设置顶点位置
    const n = initVertexBuffer(gl);
    if (n < 0) {
        console.log('设置顶点位置失败!');
        return;
    } 

    // 获取 u_MvpMatrix 变量的存储地址
    const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
    if (u_MvpMatrix < 0) {
        console.log('u_MvpMatrix 变量的存储地址获取失败!');
        return;
    }
    // 设置视点、视线和上方向
    const mvpMatrix = new Matrix4();
    mvpMatrix.setPerspective(30, 1, 1, 100)
    mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
    // 将试图矩阵传递给 u_MvpMatrix 变量
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 

    // 绘制一个点
    gl.drawElements(gl.TRIANGLES , n, gl.UNSIGNED_BYTE, 0);
    
}

image.png

4.1 索引访问顶点信息

上述绘制立方体代码中的initBuffer()函数比以往函数新增了如下代码:

// 顶点索引
const indices = new Uint8Array([
    0, 1, 2, 0, 2, 3,  // 前
    0, 3, 4, 0, 4, 5,  // 右
    0, 5, 6, 0, 6, 1,  // 上
    1, 6, 7, 1, 7, 2,  // 左
    7, 4, 3, 7, 3, 2,  // 下
    4, 7, 6, 4, 6, 5   // 后
])
// 创建索引缓冲区
const indexBuffer  =gl.createBuffer();
if (!indexBuffer) {
    console.log('创建索引缓冲区对象失败!');
    return -1;
}
// 将顶点索引数据写入缓冲区对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW**);**

a971bdc6fae306999d165a8204fadca.jpg

当我们调用gl.drawElements()时:

  • WebGL 首先绑定到 gl.ELEMENT_ARRAY_BUFFER的缓冲区中获取顶点的索引值
  • 然后根据该索引值,从绑定到gl.ARRAY_BUFFER的缓冲区中获取坐标、颜色等信息
  • 然后传递给 attribute 变量并执行顶点着色器

这种方式通过索引来访问顶点数据,从而循环利用顶点信息,控制内存的开销。但代价就是需要通过索引间接访问顶点数据,从某种程度使程序复杂化了。所以gl.drawElements()也不是绝对优于gl.drawArrays()

4.2 指定立方体表面颜色

上述示例中的立方体,每个面的颜色都是渐变的。如果我们想要立方体的每个面都是纯色,那就需要保证每个面的四个顶点都是同一个颜色的。因为WebGL内部会经过内插过程,自动计算每个片元的颜色。只有每个顶点的颜色相同,才能保证绘制的图形中每个片元颜色相同。

为了保证每个面四个顶点的颜色相同,那么此时我们就需要定义 4x6=24 个顶点的信息,如下图所示:

ea76d5ec63232aae7c4b562c5e94548.jpg

本文到这里就结束了!主要是个人在学习《WebGL 编程指南》的记录。本文中的图皆出自本书。

参考:

[1] 《WebGL 编程指南》