前面几个章节渲染的都是二维平面的图形,但是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);
}
从着色器代码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)
<canvas>上显示的是可视空间中物体在近裁剪面上的投影。近裁剪面和远裁剪面之间的盒状空间就是可视空间,只有在此空间内的物体会被显示出来。如果某个物体一部分在可视空间内,一部分在其外,那么只显示空间内的部分。
我们可以通过正射投影矩阵(orthographic projection matrix) 来定义盒状可视空间。数学公式定义如下:
如下程序:
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);
}
如上图所示,这是初始观察到的图形,但随着near的增大或far的减小,可视空间逐渐减小,我们所观察到的图形也逐渐减小,最后什么也观察不到。
2.2 可视空间(透视投影)
透视投影的核心在于“近大远小”,具体如下图所示:
图中相关参数解释如下:
fov:垂直视角,即可视空间顶面和底面间的夹角sapect:近裁剪面的宽高比(宽度/高度)near, far:近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界。(near 和 far 必须大于0,且 near 必须小于 far)
根据上述参数我们可以得到一个透视投影矩阵(perspective projection matrix) 来定义透视投影可视空间。具体公式如下:
// 初始化顶点缓冲区
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);
}
如上图所示,在透视投影视图下,远处的三角形会看起来较小一些,而且更加贴近视线。但其实这些三角形的大小都是相同的,透视投影矩阵对三角形进行了两次变换:
- 根据三角形与视点的距离,按比例对三角形进行了缩小变换
- 对三角形进行平移变换,使其贴近视线
三、对象的遮挡关系
在现实世界中,如果一个对象在另一个对象前面,那么这个对象势必会对后面的对象有所遮挡。如上述例子中,前面的三角形会遮挡后面的三角形。
但是在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,
]);
绘制的图形如下,可以看到本该出现在最远的图形却遮挡住近处的图形。
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.POINTS、gl.LINES、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FANcount:指定绘制顶点的个数type:指定索引值数据的类型,gl.UNSIGNED_BYTE或gl.UNSIGNED_SHORToffset:指定索引数组中开始绘制的位置,以字节为单位
- 返回值:无
下面我们就用该函数绘制一个正方体:
// 初始化顶点缓冲区
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);
}
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**);**
当我们调用gl.drawElements()时:
- WebGL 首先绑定到
gl.ELEMENT_ARRAY_BUFFER的缓冲区中获取顶点的索引值 - 然后根据该索引值,从绑定到
gl.ARRAY_BUFFER的缓冲区中获取坐标、颜色等信息 - 然后传递给 attribute 变量并执行顶点着色器
这种方式通过索引来访问顶点数据,从而循环利用顶点信息,控制内存的开销。但代价就是需要通过索引间接访问顶点数据,从某种程度使程序复杂化了。所以gl.drawElements()也不是绝对优于gl.drawArrays()。
4.2 指定立方体表面颜色
上述示例中的立方体,每个面的颜色都是渐变的。如果我们想要立方体的每个面都是纯色,那就需要保证每个面的四个顶点都是同一个颜色的。因为WebGL内部会经过内插过程,自动计算每个片元的颜色。只有每个顶点的颜色相同,才能保证绘制的图形中每个片元颜色相同。
为了保证每个面四个顶点的颜色相同,那么此时我们就需要定义 4x6=24 个顶点的信息,如下图所示:
本文到这里就结束了!主要是个人在学习《WebGL 编程指南》的记录。本文中的图皆出自本书。
参考:
[1] 《WebGL 编程指南》