Skip to content

WebGL - 旋转

旋转是用于在 3D 空间中调整对象方向的另一个基本变换。本章演示如何使用 WebGL 将一个简单的对象(三角形)绕某个轴旋转。

旋转涉及将对象绕特定轴(X、Y 或 Z 轴,或任意轴)按特定角度改变其方向。与缩放和平移一样,旋转在 WebGL 中也是通过矩阵乘法实现的。

对于绕 X、Y 和 Z 轴的旋转,存在单独的旋转矩阵。例如,绕 Z 轴旋转角度 θ 的矩阵表示为:

cos(θ) -sin(θ) 0 0
sin(θ) cos(θ) 0 0
0 0 1 0
0 0 0 1

X 轴和 Y 轴旋转的矩阵结构类似,都包含角度的正弦和余弦。这些矩阵在顶点着色器中与顶点位置向量相乘。

在典型的 3D 应用中,旋转是模型矩阵(Model matrix)的一部分,模型矩阵将顶点从对象的局部空间(Model Space)变换到场景的全局空间(World Space)。这个模型矩阵(通常包含缩放、旋转和平移)随后与视图矩阵(View matrix,代表摄像机的位置和方向)和投影矩阵(Projection matrix,定义摄像机的镜头属性,如视野和透视)结合。

顶点着色器中的最终顶点位置计算如下:gl_Position = ProjectionMatrix * ViewMatrix * ModelMatrix * VertexPosition;

  • 设置: 初始化 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 矩阵发送到着色器。
    • 绘制:清空 canvas,绑定缓冲区,并调用 gl.drawElements 或 gl.drawArrays。

此示例演示如何将一个白色三角形绕 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 轴重合)持续逆时针旋转。

  • 创建并应用 X 轴和 Y 轴旋转函数。
  • 通过在 Model 矩阵计算中,于旋转之前和之后应用平移来改变旋转轴。
  • 使用像 gl-matrix 这样的数学库来实现更健壮和高效的矩阵运算。
  • 旋转一个更复杂的对象,比如下一章中的立方体。