08. 实战:绘制第一个动态 Shader
让我们把学到的东西组合起来,写一个简单的“动态色彩波浪”效果。
本次实战我们将使用 原生 Three.js 和 RawShaderMaterial,体验最原汁原味的 Shader 编程。
目标
- 在屏幕上显示一个平面。
- 平面上的颜色随时间变化。
- 利用 UV 坐标制造图案。
代码 (Vanilla JS)
你可以创建一个 .html 文件,引入 Three.js (通过 CDN 或构建工具) 运行此代码。
import * as THREE from "three";
// --- 0. 基础场景设置 ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 2; // 相机后退一点
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// --- 1. 编写 Shader ---
// 顶点着色器:处理位置和 UV
const vertexShader = `
precision mediump float;
precision mediump int;
// 接收矩阵 (Three.js 传入)
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform float uTime;
// 接收属性 (几何体数据)
attribute vec3 position;
attribute vec2 uv;
// 传递给片段着色器
varying vec2 vUv;
varying float vElevation;
void main() {
vUv = uv;
vec3 pos = position;
// 顶点动画:Z轴随正弦波波动
// pos.x * 5.0 控制频率
// uTime 控制相位(流动)
pos.z += sin(pos.x * 5.0 + uTime) * 0.2;
vElevation = pos.z;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
// 片段着色器:处理颜色
const fragmentShader = `
precision mediump float;
varying vec2 vUv;
varying float vElevation;
void main() {
// 基础颜色:根据 UV 变化
vec3 color = vec3(vUv.x, vUv.y, 1.0);
// 混合颜色:根据高度 vElevation 调整亮度
// (vElevation * 2.0) 让波峰更亮
color += vElevation * 2.0;
gl_FragColor = vec4(color, 1.0);
}
`;
// --- 2. 创建物体 ---
// 增加分段数 (32x32),否则顶点太少看不出波浪
const geometry = new THREE.PlaneGeometry(2, 2, 32, 32);
// 定义 Uniforms
const uniforms = {
uTime: { value: 0 },
};
const material = new THREE.RawShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: uniforms,
side: THREE.DoubleSide, // 双面渲染
});
const mesh = new THREE.Mesh(geometry, material);
// 旋转一下平面,让它稍微躺下一点,更有立体感
mesh.rotation.x = -Math.PI / 4;
scene.add(mesh);
// --- 3. 渲染循环 ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// 更新时间
const elapsedTime = clock.getElapsedTime();
uniforms.uTime.value = elapsedTime;
renderer.render(scene, camera);
}
// 处理窗口大小调整
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
代码解析
手动搭建舞台:
- 没有了
<Canvas>,我们需要自己new THREE.Scene(),new THREE.Camera(),并手动编写animate循环。 - 这让你清楚地看到每一帧
renderer.render()是如何被调用的。
- 没有了
显式声明 (RawShaderMaterial):
- 在 Shader 中显式声明
precision,attribute,uniform。 - 这是脱离 React 封装后,真正的 WebGL 开发体验。
- 在 Shader 中显式声明
动画原理:
- 在 JS 端:
animate函数每帧运行,更新uniforms.uTime.value。 - 在 GPU 端:顶点着色器读取最新的
uTime,重新计算pos.z,产生波浪。
- 在 JS 端:
📝 编程作业
难度:⭐⭐ 中等 | 预计时间:60 分钟
任务要求
参考教程中的"动态色彩波浪"效果,实现并改进这个 Shader。
具体要求
- 基础版本:完全按照教程实现波浪效果
- 改进版本:修改频率、振幅、方向等参数
- 创意版本:添加你自己的创意元素
改进建议
- 改变波浪的方向(X、Y、对角线)
- 添加多个频率的叠加(如
sin(x * 5.0) + sin(x * 10.0) * 0.5) - 改变颜色计算方式(如根据高度使用不同颜色)
- 添加交互控制(鼠标位置影响效果)
代码框架
// 顶点着色器 - 基础版本
void main() {
vUv = uv;
vec3 pos = position;
// TODO: 修改这里,创建不同方向的波浪
pos.z += sin(pos.x * 5.0 + uTime) * 0.2;
vElevation = pos.z;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// 片段着色器 - 创意版本
void main() {
vec3 color = vec3(vUv.x, vUv.y, 1.0);
// TODO: 改进颜色计算
// 提示:可以使用 vElevation、vUv、uTime 等
gl_FragColor = vec4(color, 1.0);
}
检查清单
- 基础版本可以正常运行
- 至少实现了 2 种改进
- 添加了自己的创意元素
- 代码有清晰的注释
扩展挑战
- 🌟 实现 3D 波浪效果(在多个方向上波动)
- 🌟 添加纹理采样,让波浪有材质感
- 🌟 实现交互:鼠标位置影响波浪参数
参考资源
- 完整作业清单:编程作业清单.md