Skip to main content

08. 实战:绘制第一个动态 Shader

让我们把学到的东西组合起来,写一个简单的“动态色彩波浪”效果。 本次实战我们将使用 原生 Three.jsRawShaderMaterial,体验最原汁原味的 Shader 编程。

目标

  1. 在屏幕上显示一个平面。
  2. 平面上的颜色随时间变化。
  3. 利用 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();

代码解析

  1. 手动搭建舞台

    • 没有了 <Canvas>,我们需要自己 new THREE.Scene()new THREE.Camera(),并手动编写 animate 循环。
    • 这让你清楚地看到每一帧 renderer.render() 是如何被调用的。
  2. 显式声明 (RawShaderMaterial)

    • 在 Shader 中显式声明 precisionattributeuniform
    • 这是脱离 React 封装后,真正的 WebGL 开发体验。
  3. 动画原理

    • 在 JS 端:animate 函数每帧运行,更新 uniforms.uTime.value
    • 在 GPU 端:顶点着色器读取最新的 uTime,重新计算 pos.z,产生波浪。

📝 编程作业

难度:⭐⭐ 中等 | 预计时间:60 分钟

任务要求

参考教程中的"动态色彩波浪"效果,实现并改进这个 Shader。

具体要求

  1. 基础版本:完全按照教程实现波浪效果
  2. 改进版本:修改频率、振幅、方向等参数
  3. 创意版本:添加你自己的创意元素

改进建议

  • 改变波浪的方向(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 波浪效果(在多个方向上波动)
  • 🌟 添加纹理采样,让波浪有材质感
  • 🌟 实现交互:鼠标位置影响波浪参数

参考资源