第 4 节:Uniform 变量与动画
🎯 学习目标
- 理解 Uniform 变量的作用和使用
- 掌握渲染循环的实现
- 实现时间驱动的动画
- 对比原生 WebGL 和 Three.js 的动画系统
📖 理论:Uniform 变量
Uniform 变量是 Shader 中的全局变量,所有顶点/片段共享同一个值。
Uniform 的特点
- 全局性:所有顶点/片段共享
- 只读性:Shader 中不能修改
- 外部设置:由 JavaScript 设置值
Uniform 的类型
uniform float uTime; // 时间
uniform vec3 uColor; // 颜色
uniform mat4 uMatrix; // 矩阵
uniform sampler2D uTexture; // 纹理
设置 Uniform 的 API
gl.uniform1f(location, value); // float
gl.uniform3f(location, x, y, z); // vec3
gl.uniformMatrix4fv(location, transpose, value); // mat4
💻 原生 WebGL 实现
实现一个随时间变化的颜色动画:
<!DOCTYPE html>
<html>
<head>
<title>Uniform 变量与动画</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<script>
const canvas = document.getElementById("glcanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gl = canvas.getContext("webgl");
if (!gl) {
alert("无法初始化 WebGL");
}
// 复用之前的编译函数
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("编译错误:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("链接错误:", gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
// 顶点着色器
const vertexShaderSource = `
attribute vec4 aVertexPosition;
void main() {
gl_Position = aVertexPosition;
}
`;
// 片段着色器 - 使用 uniform 变量
const fragmentShaderSource = `
precision mediump float;
uniform float uTime; // 时间变量
uniform vec3 uColor; // 基础颜色
void main() {
// 使用 sin 函数创建平滑的颜色变化
float r = uColor.r + sin(uTime) * 0.5;
float g = uColor.g + sin(uTime + 2.0) * 0.5;
float b = uColor.b + sin(uTime + 4.0) * 0.5;
gl_FragColor = vec4(r, g, b, 1.0);
}
`;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
// 准备顶点数据
const vertices = new Float32Array([
-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0,
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLoc = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
// 获取 Uniform 位置
const timeLoc = gl.getUniformLocation(program, "uTime");
const colorLoc = gl.getUniformLocation(program, "uColor");
gl.viewport(0, 0, canvas.width, canvas.height);
// 渲染循环
let startTime = Date.now();
function animate() {
requestAnimationFrame(animate);
// 计算经过的时间(秒)
const elapsed = (Date.now() - startTime) / 1000.0;
// 设置 Uniform 变量
gl.uniform1f(timeLoc, elapsed);
gl.uniform3f(colorLoc, 0.5, 0.5, 0.5); // 基础颜色
// 清空并绘制
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
animate();
</script>
</body>
</html>
关键点
- 获取 Uniform 位置:
gl.getUniformLocation(program, "uTime") - 设置 Uniform 值:
gl.uniform1f(timeLoc, value) - 渲染循环:使用
requestAnimationFrame实现动画 - 时间计算:使用
Date.now()或performance.now()计算时间
🎨 Three.js 对比实现
Three.js 使用 uniforms 对象管理 Uniform 变量:
<!DOCTYPE html>
<html>
<head>
<title>Three.js Uniform 与动画</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("canvas"),
});
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array([
-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0,
]);
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// 使用 ShaderMaterial 自定义 Shader
const material = new THREE.ShaderMaterial({
vertexShader: `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
void main() {
float r = uColor.r + sin(uTime) * 0.5;
float g = uColor.g + sin(uTime + 2.0) * 0.5;
float b = uColor.b + sin(uTime + 4.0) * 0.5;
gl_FragColor = vec4(r, g, b, 1.0);
}
`,
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Vector3(0.5, 0.5, 0.5) },
},
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 渲染循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// 更新 Uniform
material.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Three.js 的优势
- 自动管理:Uniform 位置自动获取
- 类型推断:根据 value 类型自动选择 API
- Clock 类:提供便捷的时间管理
- 自动更新:Material 自动处理 Uniform 更新
🔍 原理对比分析
1. Uniform 管理
原生 WebGL:
// 需要手动获取位置
const timeLoc = gl.getUniformLocation(program, "uTime");
// 需要手动设置值
gl.uniform1f(timeLoc, elapsed);
Three.js:
// 自动管理
uniforms: {
uTime: { value: 0 }
}
// 直接更新值
material.uniforms.uTime.value = elapsed;
2. 时间管理
原生 WebGL:
// 需要手动计算时间
let startTime = Date.now();
const elapsed = (Date.now() - startTime) / 1000.0;
Three.js:
// 使用 Clock 类
const clock = new THREE.Clock();
const elapsed = clock.getElapsedTime();
3. 渲染循环
原生 WebGL:
// 需要手动调用所有步骤
function animate() {
requestAnimationFrame(animate);
gl.uniform1f(timeLoc, elapsed);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
Three.js:
// 自动处理大部分步骤
function animate() {
requestAnimationFrame(animate);
material.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
📝 编程作业
基础作业(⭐)
任务:实现一个颜色呼吸动画
要求:
- 使用 uniform 变量传递时间
- 实现颜色的平滑变化(使用 sin 函数)
- 使用原生 WebGL 和 Three.js 两种方式实现
检查清单:
- 颜色可以平滑变化
- 理解 uniform 变量的作用
- 理解渲染循环的实现
进阶作业(⭐⭐)
任务:实现三角形旋转动画
要求:
- 在顶点着色器中使用 uniform 传递旋转角度
- 使用矩阵旋转顶点位置
- 实现平滑的旋转动画
提示:
// 旋转矩阵(绕 Z 轴)
mat2 rotation = mat2(
cos(angle), -sin(angle),
sin(angle), cos(angle)
);
vec2 rotated = rotation * position.xy;
检查清单:
- 三角形可以旋转
- 理解矩阵变换
- 理解 uniform 在顶点着色器中的使用
挑战作业(⭐⭐⭐)
任务:实现交互式动画控制
要求:
- 添加鼠标/键盘控制
- 可以调整动画速度、方向、颜色等参数
- 实时显示当前参数值
扩展功能:
- 添加 GUI 控制面板(如 dat.GUI)
- 支持保存/加载参数配置
- 支持动画录制和回放
🎓 本节总结
关键概念
- Uniform 变量:全局共享的只读变量
- 渲染循环:使用
requestAnimationFrame实现动画 - 时间管理:使用时间驱动动画变化
- Uniform API:
uniform1f、uniform3f、uniformMatrix4fv等
原生 WebGL vs Three.js
| 方面 | 原生 WebGL | Three.js |
|---|---|---|
| Uniform 管理 | 手动获取位置 | 自动管理 |
| 时间管理 | 手动计算 | Clock 类 |
| 代码量 | 较多 | 较少 |
| 灵活性 | 完全控制 | 受限于 API |
最佳实践
- 使用 requestAnimationFrame:保证流畅的动画
- 统一时间单位:使用秒作为时间单位
- 避免频繁更新:只在需要时更新 Uniform
- 使用 Clock:Three.js 中优先使用 Clock 类
下一步
完成作业后,进入下一节:第 5 节:矩阵变换(MVP)