现在让我们给之前的正方形添加五个面从而可以创建一个三维的立方体。最简单的方式就是通过调用方法 gl.drawElements()
使用顶点数组列表来替换之前的通过方法gl.drawArrays()
直接使用顶点数组。而顶点数组列表里保存着将会被引用到一个个独立的顶点。
其实现在会存在这样一个问题:每个面需要 4 个顶点,而每个顶点会被 3 个面共享。我们会创建一个包含 24 个顶点的数组列表,通过使用数组下标来索引顶点,然后把这些用于索引的下标传递给渲染程序而不是直接把整个顶点数据传递过去,这样来减少数据传递。那么也许你就会问:那么使用 8 个顶点就好了,为什么要使用 24 个顶点呢?这是因为每个顶点虽然被 3 个面共享但是它在每个面上需要使用不同的颜色信息。24 个顶点中的每一个都会有独立的颜色信息,这就会造成每个顶点位置都会有 3 份副本。
定义立方体顶点位置
首先,更新 initBuffers()
函数中代码来创建立方体的顶点位置缓存区。现在的代码看起来和渲染正方形时的代码很相似,只是比之前的代码更长因为现在有了 24 个顶点(每个面使用 4 个顶点)。
在“init-buffers.js”文件 initPositionBuffer()
函数中,用下面代码替换 positions:
const positions = [// Front face-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,// Back face-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,// Top face-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,// Bottom face-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,// Right face1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,// Left face-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
];
由于我们给顶点添加了 Z 分量,因此我们需要将 vertexPosition
属性的 numComponents
更新为 3。
定义顶点颜色
然后我们还要为每个顶点定义颜色。下面的代码首先为每个面定义颜色,然后用一个循环语句为每个顶点定义颜色信息。
在“init-buffers.js”文件 initColorBuffer()
函数中,用下面代码替换 colors
定义:
const faceColors = [[1.0, 1.0, 1.0, 1.0], // Front face: white[1.0, 0.0, 0.0, 1.0], // Back face: red[0.0, 1.0, 0.0, 1.0], // Top face: green[0.0, 0.0, 1.0, 1.0], // Bottom face: blue[1.0, 1.0, 0.0, 1.0], // Right face: yellow[1.0, 0.0, 1.0, 1.0], // Left face: purple
];// Convert the array of colors into a table for all the vertices.var colors = [];for (var j = 0; j < faceColors.length; ++j) {const c = faceColors[j];// Repeat each color four times for the four vertices of the facecolors = colors.concat(c, c, c, c);
}
定义元素(三角形)数组
既然已经创建好了顶点数组,接下来就要创建元素(三角形)数组了。
function initIndexBuffer(gl) {const indexBuffer = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);// This array defines each face as two triangles, using the// indices into the vertex array to specify each triangle's// position.const indices = [0,1,2,0,2,3, // front4,5,6,4,6,7, // back8,9,10,8,10,11, // top12,13,14,12,14,15, // bottom16,17,18,16,18,19, // right20,21,22,20,22,23, // left];// 将索引数据传递给缓冲区对象gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(indices),gl.STATIC_DRAW,);return indexBuffer;
}
indices
数组声明每一个面都使用两个三角形来渲染。通过立方体顶点数组的索引指定每个三角形的顶点。那么这个立方体就是由 12 个三角形组成的了。
渲染立方体
接下来就需要在 drawScene()
函数里添加代码使用立方体顶点索引数据来渲染这个立方体了。代码里添加了对 gl.bindBuffer()
和 gl.drawElements()
的调用:
{const vertexCount = 36;const type = gl.UNSIGNED_SHORT;const offset = 0;gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
立方体的每个面都由 2 个三角形组成,那就是每个面需要 6 个顶点,或者说总共 36 个顶点,尽管有许多重复的。然而,因为索引数组的每个元素都是简单的整数类型,所以每一帧动画需要传递给渲染程序的数据也不是很多。
最后,让我们把变量 squareRotation
替换成 cubeRotation
并添加 X 轴的第二个旋转。
在“webgl-demo.js”文件的头部,把变量 squareRotation
替换成 cubeRotation
:
let cubeRotation = 0.0;
在 drawScene() 函数中,用下面代码替换之前的 mat4.rotate 函数:
mat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation, // amount to rotate in radians[0, 0, 1],
); // axis to rotate around (Z)
mat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation * 0.7, // amount to rotate in radians[0, 1, 0],
); // axis to rotate around (Y)
mat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation * 0.3, // amount to rotate in radians[1, 0, 0],
); // axis to rotate around (X)
在 main() 函数中,替换 drawScene() 函数调用参数中的 squareRotation 为 cubeRotation:
drawScene(gl, programInfo, buffers, cubeRotation);
cubeRotation += deltaTime;
完整源码如下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><title>WebGL Demo</title><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"integrity="sha512-zhHQR0/H5SEBL3Wn6yYSaTTZej12z0hVZKOv3TwCUXT1z5qeqGcXJLLrbERYRScEDDpYIJhPC1fk31gqR783iQ=="crossorigin="anonymous"defer></script><script src="webgl-demo.js" type="module"></script></head><body><canvas id="glcanvas" width="640" height="480"></canvas></body>
</html>
// webgl-demo.js
import { initBuffers } from "./init-buffers.js";
import { drawScene } from "./draw-scene.js";let cubeRotation = 0.0;
let deltaTime = 0;main();function main() {const canvas = document.querySelector("#glcanvas");// Initialize the GL contextconst gl = canvas.getContext("webgl");// Only continue if WebGL is available and workingif (gl === null) {alert("Unable to initialize WebGL. Your browser or machine may not support it.");return;}// Set clear color to black, fully opaquegl.clearColor(0.0, 0.0, 0.0, 1.0);// Clear the color buffer with specified clear colorgl.clear(gl.COLOR_BUFFER_BIT);// Vertex shader programconst vsSource = `attribute vec4 aVertexPosition;attribute vec4 aVertexColor;uniform mat4 uModelViewMatrix;uniform mat4 uProjectionMatrix;varying lowp vec4 vColor;void main(void) {gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;vColor = aVertexColor;}`;// Fragment shader programconst fsSource = `varying lowp vec4 vColor;void main(void) {gl_FragColor = vColor;}`;// Initialize a shader program; this is where all the lighting// for the vertices and so forth is established.const shaderProgram = initShaderProgram(gl, vsSource, fsSource);// Collect all the info needed to use the shader program.// Look up which attributes our shader program is using// for aVertexPosition, aVertexColor and also// look up uniform locations.const programInfo = {program: shaderProgram,attribLocations: {vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),vertexColor: gl.getAttribLocation(shaderProgram, "aVertexColor"),},uniformLocations: {projectionMatrix: gl.getUniformLocation(shaderProgram,"uProjectionMatrix"),modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),},};// Here's where we call the routine that builds all the// objects we'll be drawing.const buffers = initBuffers(gl);let then = 0;// 添加时间function render(now) {now *= 0.001; // convert to secondsdeltaTime = now - then;then = now;drawScene(gl, programInfo, buffers, cubeRotation);cubeRotation += deltaTime;requestAnimationFrame(render);}requestAnimationFrame(render);
}//
// Initialize a shader program, so WebGL knows how to draw our data
//
function initShaderProgram(gl, vsSource, fsSource) {const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);// Create the shader programconst shaderProgram = gl.createProgram();gl.attachShader(shaderProgram, vertexShader);gl.attachShader(shaderProgram, fragmentShader);gl.linkProgram(shaderProgram);// If creating the shader program failed, alertif (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {alert(`Unable to initialize the shader program: ${gl.getProgramInfoLog(shaderProgram)}`);return null;}return shaderProgram;
}//
// creates a shader of the given type, uploads the source and
// compiles it.
//
function loadShader(gl, type, source) {const shader = gl.createShader(type);// Send the source to the shader objectgl.shaderSource(shader, source);// Compile the shader programgl.compileShader(shader);// See if it compiled successfullyif (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {alert(`An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`);gl.deleteShader(shader);return null;}return shader;
}
// init-buffers.js
function initBuffers(gl) {const positionBuffer = initPositionBuffer(gl);const colorBuffer = initColorBuffer(gl);// 新增立方体所有顶点索引const indexBuffer = initIndexBuffer(gl);return {position: positionBuffer,color: colorBuffer,indices: indexBuffer,};
}
// 设置立方体的点位置
function initPositionBuffer(gl) {// Create a buffer for the square's positions.const positionBuffer = gl.createBuffer();// Select the positionBuffer as the one to apply buffer// operations to from here out.gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// 定义了一个包含顶点位置信息的 JavaScript 数组。每组三个数值表示一个顶点的 x、y、z 坐标。这里包含了立方体的各个面的顶点坐标// 想要每个面有不同的颜色,那么你需要为每个面定义独立的顶点,因为颜色是顶点属性。这意味着你不能让顶点被多个面共享,因为一个顶点只能有一个颜色const positions = [// Front face-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,// Back face-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,// Top face-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,// Bottom face-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,// Right face1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,// Left face-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,];// Now pass the list of positions into WebGL to build the// shape. We do this by creating a Float32Array from the// JavaScript array, then use it to fill the current buffer.gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);return positionBuffer;
}
// 初始化每个顶点颜色并设置缓冲区
function initColorBuffer(gl) {// 定义了一个包含各个面颜色的数组。每个元素是一个包含 RGBA 值的数组,表示一个面的颜色。这里列出了立方体的各个面的颜色const faceColors = [[1.0, 1.0, 1.0, 1.0], // Front face: white[1.0, 0.0, 0.0, 1.0], // Back face: red[0.0, 1.0, 0.0, 1.0], // Top face: green[0.0, 0.0, 1.0, 1.0], // Bottom face: blue[1.0, 1.0, 0.0, 1.0], // Right face: yellow[1.0, 0.0, 1.0, 1.0], // Left face: purple];// Convert the array of colors into a table for all the vertices.var colors = [];// 将当前面的颜色值重复四次,因为每个面有四个顶点,所以需要将颜色值复制四次,以便为每个顶点指定颜色for (var j = 0; j < faceColors.length; ++j) {const c = faceColors[j];// Repeat each color four times for the four vertices of the facecolors = colors.concat(c, c, c, c);}const colorBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);return colorBuffer;
}
// 初始化索引缓冲区
function initIndexBuffer(gl) {const indexBuffer = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);// 定义了一个包含每个面两个三角形的索引数据数组。每六个索引值定义了两个三角形,这些索引值指定了顶点数组中哪些顶点组成了一个面的两个三角形// 为每个面定义独立的颜色,因为每个面的顶点是独立的,所以可以为每个顶点定义不同的颜色const indices = [0,1,2,0,2,3, // front4,5,6,4,6,7, // back8,9,10,8,10,11, // top12,13,14,12,14,15, // bottom16,17,18,16,18,19, // right20,21,22,20,22,23, // left];// Now send the element array to GLgl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(indices),gl.STATIC_DRAW);return indexBuffer;
}export { initBuffers };
// draw-scene.js
function drawScene(gl, programInfo, buffers, cubeRotation) {gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaquegl.clearDepth(1.0); // Clear everythinggl.enable(gl.DEPTH_TEST); // Enable depth testinggl.depthFunc(gl.LEQUAL); // Near things obscure far things// Clear the canvas before we start drawing on it.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);const fieldOfView = (45 * Math.PI) / 180; // in radiansconst aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;const zNear = 0.1;const zFar = 100.0;const projectionMatrix = mat4.create();// note: glmatrix.js always has the first argument// as the destination to receive the result.mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);// Set the drawing position to the "identity" point, which is// the center of the scene.const modelViewMatrix = mat4.create();// Now move the drawing position a bit to where we want to// start drawing the square.mat4.translate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to translate[-0.0, 0.0, -6.0]); // amount to translatemat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation, // amount to rotate in radians[0, 0, 1]);// 旋转y轴mat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation * 0.7, // amount to rotate in radians[0, 1, 0]); // 旋转x轴mat4.rotate(modelViewMatrix, // destination matrixmodelViewMatrix, // matrix to rotatecubeRotation * 0.3, // amount to rotate in radians[1, 0, 0]); // Tell WebGL how to pull out the positions from the position// buffer into the vertexPosition attribute.setPositionAttribute(gl, buffers, programInfo);setColorAttribute(gl, buffers, programInfo);// buffers.indices 的索引缓冲区对象绑定到 WebGL 的 ELEMENT_ARRAY_BUFFER 目标上,以便在渲染时使用这个索引缓冲区对象gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);// Tell WebGL to use our program when drawinggl.useProgram(programInfo.program);// Set the shader uniformsgl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix,false,projectionMatrix);gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix,false,modelViewMatrix);{// 定义了要绘制的顶点数量const vertexCount = 36;const type = gl.UNSIGNED_SHORT;// 从索引缓冲区中的哪个位置开始绘制。在这个例子中,从索引缓冲区的第一个索引开始绘制const offset = 0;gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);}
}// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute.
function setPositionAttribute(gl, buffers, programInfo) {const numComponents = 3;const type = gl.FLOAT; // the data in the buffer is 32bit floatsconst normalize = false; // don't normalizeconst stride = 0; // how many bytes to get from one set of values to the next// 0 = use type and numComponents aboveconst offset = 0; // how many bytes inside the buffer to start fromgl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition,numComponents,type,normalize,stride,offset);gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}// Tell WebGL how to pull out the colors from the color buffer
// into the vertexColor attribute.
function setColorAttribute(gl, buffers, programInfo) {const numComponents = 4;const type = gl.FLOAT;const normalize = false;const stride = 0;const offset = 0;gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);gl.vertexAttribPointer(programInfo.attribLocations.vertexColor,numComponents,type,normalize,stride,offset);gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}export { drawScene };
所以要想画立方体首先确定有多少个面,每个面由两个三角形构成,所以一个面需要四个顶点。由于每个面有四个顶点,并且每个面颜色不同,所以顶点也不能共享,同一个面的顶点颜色是一样的。索引值也因为每个面的颜色不一样导致不同面的索引值不能共享,同一个面的可以共享,共4*6个。
索引缓冲区(Index Buffer)
索引缓冲区是一种优化图形渲染的方式,它允许我们重复使用顶点数据来绘制多个面。如果不使用索引缓冲区,我们需要为每个面的每个顶点都提供完整的顶点数据,包括位置、颜色、纹理坐标等。这在有大量重复顶点的情况下,比如在绘制一个立方体时,可能会导致大量的数据冗余。
如果不使用索引,你将需要为每个三角形定义所有的顶点数据。例如,一个立方体由12个三角形组成,每个三角形有3个顶点,所以你需要定义36个顶点。这就需要更多的内存,因为你需要为每个顶点定义所有的属性(比如位置、颜色、纹理坐标等)。而且,GPU需要处理更多的数据,这可能会降低性能。
使用索引,你可以定义立方体的8个顶点,然后通过索引来定义立方体的12个三角形。这样,你只需要定义8个顶点的属性,然后使用索引来重复使用这些顶点。这可以减少内存使用,并提高性能,因为GPU需要处理的数据更少。
然而,使用索引也有一些限制。比如,如果你想让立方体的每个面有不同的颜色,你可能需要为每个面定义独立的顶点,因为颜色是顶点属性。这就减少了使用索引的优势,因为你不能重复使用顶点。
总的来说,是否使用索引取决于你的具体需求。在一些情况下,使用索引可以提高性能和减少内存使用。在其他情况下,可能需要为每个面定义独立的顶点。