WebGL - 旋转
WebGL - 旋转
Section titled “WebGL - 旋转”旋转是用于在 3D 空间中调整对象方向的另一个基本变换。本章演示如何使用 WebGL 将一个简单的对象(三角形)绕某个轴旋转。
旋转涉及将对象绕特定轴(X、Y 或 Z 轴,或任意轴)按特定角度改变其方向。与缩放和平移一样,旋转在 WebGL 中也是通过矩阵乘法实现的。
对于绕 X、Y 和 Z 轴的旋转,存在单独的旋转矩阵。例如,绕 Z 轴旋转角度 θ 的矩阵表示为:
cos(θ) -sin(θ) 0 0sin(θ) cos(θ) 0 0 0 0 1 0 0 0 0 1X 轴和 Y 轴旋转的矩阵结构类似,都包含角度的正弦和余弦。这些矩阵在顶点着色器中与顶点位置向量相乘。
模型-视图-投影 (MVP) 矩阵
Section titled “模型-视图-投影 (MVP) 矩阵”在典型的 3D 应用中,旋转是模型矩阵(Model matrix)的一部分,模型矩阵将顶点从对象的局部空间(Model Space)变换到场景的全局空间(World Space)。这个模型矩阵(通常包含缩放、旋转和平移)随后与视图矩阵(View matrix,代表摄像机的位置和方向)和投影矩阵(Projection matrix,定义摄像机的镜头属性,如视野和透视)结合。
顶点着色器中的最终顶点位置计算如下:gl_Position = ProjectionMatrix * ViewMatrix * ModelMatrix * VertexPosition;
旋转的核心步骤
Section titled “旋转的核心步骤”- 设置: 初始化 WebGL,定义几何体(顶点、索引,可能还有颜色),创建缓冲区。
- 着色器:
-
- 顶点着色器: 定义位置(以及颜色等)的
attribute属性。定义用于 Model、View 和 Projection 矩阵的uniform mat4变量。计算gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * vec4(a_position, 1.0);。通过varying将颜色(如果使用)传递给片段着色器。
- 顶点着色器: 定义位置(以及颜色等)的
-
- 片段着色器: 接收插值后的颜色(如果使用),并设置
gl_FragColor。
- 片段着色器: 接收插值后的颜色(如果使用),并设置
- 属性/Uniform 位置: 获取属性和 MVP 矩阵
uniform的位置。 - 矩阵创建 (JS): 定义 Projection 矩阵(例如透视矩阵)。定义 View 矩阵(摄像机位置/目标)。定义 Model 矩阵,初始通常是一个单位矩阵。创建修改 Model 矩阵的旋转函数(例如
rotateZ(matrix, angle))。 - 动画循环: 使用
requestAnimationFrame实现平滑动画。 -
- 在循环内部:计算时间差 (
dt)。根据dt更新旋转角度。
- 在循环内部:计算时间差 (
-
- 使用你的旋转函数将旋转应用到 Model 矩阵。
-
- 更新 Uniforms:使用
gl.uniformMatrix4fv将更新后的 Model、View 和 Projection 矩阵发送到着色器。
- 更新 Uniforms:使用
-
- 绘制:清空 canvas,绑定缓冲区,并调用
gl.drawElements或gl.drawArrays。
- 绘制:清空 canvas,绑定缓冲区,并调用
示例:旋转一个三角形
Section titled “示例:旋转一个三角形”此示例演示如何将一个白色三角形绕 Z 轴旋转。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>WebGL Rotation Example</title> <style> body { margin: 0; overflow: hidden; background-color: #111; } canvas { display: block; } </style></head><body> <canvas id="webgl-canvas" width="500" height="500"></canvas>
<script> // --- 着色器源代码 --- const vsSource = ` attribute vec3 a_position; attribute vec3 a_color; // 添加颜色属性
uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix;
varying lowp vec4 v_color; // 将颜色传递给片段着色器
void main() { gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * vec4(a_position, 1.0); v_color = vec4(a_color, 1.0); } `;
const fsSource = ` precision mediump float; varying lowp vec4 v_color; // 接收颜色 void main() { gl_FragColor = v_color; } `;
// --- 工具函数:createShader, createProgram (假定已定义) --- function createShader(gl, type, source) { /* ... */ const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(`Shader compilation error (${type === gl.VERTEX_SHADER ? 'Vertex' : 'Fragment'}):`, gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } function createProgram(gl, vsSource, fsSource) { /* ... */ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); if (!vertexShader || !fragmentShader) return null; const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Program linking error:', gl.getProgramInfoLog(program)); gl.deleteProgram(program); return null; } return program; }
// --- 矩阵工具函数 (简化版 - 生产环境请使用 gl-matrix) --- function createIdentityMatrix() { return new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); }
function createPerspectiveMatrix(fieldOfViewRadians, aspect, near, far) { const f = 1.0 / Math.tan(fieldOfViewRadians / 2); const rangeInv = 1 / (near - far); return new Float32Array([ f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (near + far) * rangeInv, -1, 0, 0, near * far * rangeInv * 2, 0 ]); }
function translateMatrix(matrix, tx, ty, tz) { // 简单平移 - 为此示例创建一个*新*矩阵 // 在库中,原地修改更常见 const result = matrix.slice(); // 复制 result[12] = matrix[0] * tx + matrix[4] * ty + matrix[8] * tz + matrix[12]; result[13] = matrix[1] * tx + matrix[5] * ty + matrix[9] * tz + matrix[13]; result[14] = matrix[2] * tx + matrix[6] * ty + matrix[10] * tz + matrix[14]; result[15] = matrix[3] * tx + matrix[7] * ty + matrix[11] * tz + matrix[15]; return result; }
function rotateZMatrix(matrix, angleRadians) { // 简单 Z 轴旋转 - 创建一个*新*矩阵 const result = matrix.slice(); // 复制 const c = Math.cos(angleRadians); const s = Math.sin(angleRadians); const m00 = result[0], m01 = result[1], m04 = result[4], m05 = result[5]; result[0] = m00 * c + m04 * s; result[1] = m01 * c + m05 * s; result[4] = m00 * -s + m04 * c; result[5] = m01 * -s + m05 * c; return result; }
// --- 主 WebGL 代码 --- const canvas = document.getElementById('webgl-canvas'); const gl = canvas.getContext('webgl'); if (!gl) { alert('WebGL not supported.'); throw new Error('WebGL not supported'); }
const shaderProgram = createProgram(gl, vsSource, fsSource); if (!shaderProgram) throw new Error('Failed program creation.');
// --- 几何体和颜色数据 --- const triangleVertices = new Float32Array([ 0.0, 0.6, 0.0, // 顶部顶点 -0.5, -0.4, 0.0, // 左下角 0.5, -0.4, 0.0 // 右下角 ]); const triangleColors = new Float32Array([ 1.0, 1.0, 1.0, // 白色 1.0, 1.0, 1.0, // 白色 1.0, 1.0, 1.0 // 白色 ]); const triangleIndices = new Uint16Array([0, 1, 2]);
// --- 缓冲区 --- const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, triangleColors, gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, triangleIndices, gl.STATIC_DRAW);
// --- 属性设置 --- const positionAttribLocation = gl.getAttribLocation(shaderProgram, 'a_position'); const colorAttribLocation = gl.getAttribLocation(shaderProgram, 'a_color'); if (positionAttribLocation < 0 || colorAttribLocation < 0) console.error("Attribute not found");
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(positionAttribLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.vertexAttribPointer(colorAttribLocation, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(colorAttribLocation);
// --- Uniform 位置 --- const projMatrixLocation = gl.getUniformLocation(shaderProgram, 'u_projectionMatrix'); const viewMatrixLocation = gl.getUniformLocation(shaderProgram, 'u_viewMatrix'); const modelMatrixLocation = gl.getUniformLocation(shaderProgram, 'u_modelMatrix'); if (!projMatrixLocation || !viewMatrixLocation || !modelMatrixLocation) console.error("Uniform not found");
// --- 矩阵 --- const fieldOfView = 45 * Math.PI / 180; // 弧度制下的 45 度 const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; const zNear = 0.1; const zFar = 100.0; const projectionMatrix = createPerspectiveMatrix(fieldOfView, aspect, zNear, zFar);
let viewMatrix = createIdentityMatrix(); viewMatrix = translateMatrix(viewMatrix, 0.0, 0.0, -3.0); // 将摄像机向后移动
let modelMatrix = createIdentityMatrix();
// --- 动画循环 --- let lastTime = 0; let rotationAngle = 0;
function render(currentTime) { currentTime *= 0.001; // 将时间转换为秒 const deltaTime = currentTime - lastTime; lastTime = currentTime;
// 更新旋转 rotationAngle += deltaTime * 1.0; // 每秒旋转 1 弧度
// 重新计算模型矩阵(为了简单起见,每帧都从单位矩阵开始) modelMatrix = createIdentityMatrix(); modelMatrix = rotateZMatrix(modelMatrix, rotationAngle);
// --- 绘制 --- gl.clearColor(0.05, 0.05, 0.1, 1.0); // 深蓝色背景 gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(shaderProgram); // 使用程序
// 设置矩阵 gl.uniformMatrix4fv(projMatrixLocation, false, projectionMatrix); gl.uniformMatrix4fv(viewMatrixLocation, false, viewMatrix); gl.uniformMatrix4fv(modelMatrixLocation, false, modelMatrix);
// 绑定索引缓冲区并绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render); // 启动循环 }
requestAnimationFrame(render);
</script></body></html>预期输出:
代码将显示一个位于深蓝色背景中央的白色三角形。该三角形将绕其中心(在此设置中与 Z 轴重合)持续逆时针旋转。
进一步探索:
Section titled “进一步探索:”- 创建并应用 X 轴和 Y 轴旋转函数。
- 通过在 Model 矩阵计算中,于旋转之前和之后应用平移来改变旋转轴。
- 使用像
gl-matrix这样的数学库来实现更健壮和高效的矩阵运算。 - 旋转一个更复杂的对象,比如下一章中的立方体。