Skip to main content

第 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>

关键点

  1. 获取 Uniform 位置gl.getUniformLocation(program, "uTime")
  2. 设置 Uniform 值gl.uniform1f(timeLoc, value)
  3. 渲染循环:使用 requestAnimationFrame 实现动画
  4. 时间计算:使用 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 的优势

  1. 自动管理:Uniform 位置自动获取
  2. 类型推断:根据 value 类型自动选择 API
  3. Clock 类:提供便捷的时间管理
  4. 自动更新: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);
}

📝 编程作业

基础作业(⭐)

任务:实现一个颜色呼吸动画

要求

  1. 使用 uniform 变量传递时间
  2. 实现颜色的平滑变化(使用 sin 函数)
  3. 使用原生 WebGL 和 Three.js 两种方式实现

检查清单

  • 颜色可以平滑变化
  • 理解 uniform 变量的作用
  • 理解渲染循环的实现

进阶作业(⭐⭐)

任务:实现三角形旋转动画

要求

  1. 在顶点着色器中使用 uniform 传递旋转角度
  2. 使用矩阵旋转顶点位置
  3. 实现平滑的旋转动画

提示

// 旋转矩阵(绕 Z 轴)
mat2 rotation = mat2(
cos(angle), -sin(angle),
sin(angle), cos(angle)
);
vec2 rotated = rotation * position.xy;

检查清单

  • 三角形可以旋转
  • 理解矩阵变换
  • 理解 uniform 在顶点着色器中的使用

挑战作业(⭐⭐⭐)

任务:实现交互式动画控制

要求

  1. 添加鼠标/键盘控制
  2. 可以调整动画速度、方向、颜色等参数
  3. 实时显示当前参数值

扩展功能

  • 添加 GUI 控制面板(如 dat.GUI)
  • 支持保存/加载参数配置
  • 支持动画录制和回放

🎓 本节总结

关键概念

  1. Uniform 变量:全局共享的只读变量
  2. 渲染循环:使用 requestAnimationFrame 实现动画
  3. 时间管理:使用时间驱动动画变化
  4. Uniform APIuniform1funiform3funiformMatrix4fv

原生 WebGL vs Three.js

方面原生 WebGLThree.js
Uniform 管理手动获取位置自动管理
时间管理手动计算Clock 类
代码量较多较少
灵活性完全控制受限于 API

最佳实践

  1. 使用 requestAnimationFrame:保证流畅的动画
  2. 统一时间单位:使用秒作为时间单位
  3. 避免频繁更新:只在需要时更新 Uniform
  4. 使用 Clock:Three.js 中优先使用 Clock 类

下一步

完成作业后,进入下一节:第 5 节:矩阵变换(MVP)


📚 参考资源