第 1 课:Hello Shader
从一个三角形开始
日常开发中,材质可以决定物体“看起来是什么样”。MeshBasicMaterial 已能画纯色,但当颜色需要按时间、鼠标或位置逐像素变化时,我们才需要直接写 Shader。本课从纯红三角形走到动画和交互。先运行纯红三角形;它是后面六题的起点。
浏览器打开 exercises/ 目录中的 index.html 不能使用 file://:ES module 和 fetch 读取 .vert / .frag 文件需要 HTTP。进入 doc/shader-tutorial/exercises/ 后执行 http-server -p 1000,然后打开 http://localhost:1000/01-hello-shader/。本教程本地提供 Three.js 模块,无需 CDN。
GPU 为什么分两个阶段
一个三角形有三个顶点。顶点着色器分别处理每个顶点,输出 gl_Position,即裁剪空间中的四维位置。光栅化阶段根据三角形覆盖的屏幕像素生成片元,并对顶点输出的值做插值。片元着色器对每个片元输出颜色。一个 100 万像素的三角形仍只有三个顶点着色器调用,却可能有约 100 万次片元着色器调用。因此放在片元阶段的昂贵计算会被重复执行很多次。
本课的最短路径是:
JavaScript 建立 BufferGeometry(position, uv)
→ Three.js 将 attribute 与 ShaderMaterial 上传到 GPU
→ 顶点着色器把 position 变换成 gl_Position,并传出 vUv
→ 光栅化为三角形内部的每个片元插值 vUv
→ 片元着色器写 gl_FragColor
→ Three.js 进行输出颜色空间转换,显示到画布
position 和 uv 是 attribute:每个顶点有一个值。uTime、uColor、uMouse 是 uniform:一次 draw call 内所有顶点和片元共享同一个值。vUv 是 varying:顶点传出三个值,GPU 在三角形内部插值,片元收到的是当前所在位置的值。第 1 题虽然暂时不使用 vUv,仍保留传递代码,便于第 2–3 课接着使用。
第一段可运行 Shader
main.js 创建三角形,并把两段源码交给 ShaderMaterial。顶点着色器的核心如下:
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
position 是模型局部坐标。modelViewMatrix 先把它移到观察空间;projectionMatrix 再映射到裁剪空间。GPU 最后做透视除法,决定它出现在画布哪里。片元着色器的最小颜色语句是:
vec3 color = vec3(1.0, 0.0, 0.0);
gl_FragColor = vec4(color, 1.0);
三个通道分别是 R、G、B,范围通常为 0–1;第四个数是 alpha。这里输出线性颜色,示例尾部的 tonemapping_fragment 和 colorspace_fragment 由 Three.js 处理输出。vec3(1,0,0) 与 vec3(1.0,0.0,0.0) 在语法和类型上不等价;GLSL 中应明确写浮点常量。
uniform、时间和交互
第 2 题在 JS 中写 uColor: { value: new THREE.Vector3(0, 1, 0) },片元着色器写 uniform vec3 uColor;,再用 vec3 color = uColor;。改颜色只改 JS 数值,Shader 不动。它适合主题色、用户选色和状态色。若只需整块物体纯色而无需自定义算法,内建材质更简单,也更容易获得 Three.js 的光照等功能。
第 3 题的混合权重是 0.5 + 0.5 * sin(uTime * 2.0)。sin 原本在 -1 到 1 之间,乘 0.5 再加 0.5 后变为 0 到 1,正好供 mix(red, blue, weight) 使用。uTime 在 JS 中按秒累加;乘数 2.0 是角速度,单位约为弧度每秒,增大它会加快循环。第 4 题让 RGB 三通道的相位错开 2π/3,得到连续的彩色循环。
第 5 题把指针位置转换成归一化画布坐标。DOM 的 Y 轴向下,而 Shader 中的演示坐标约定 Y 轴向上,所以 JS 用 1 - (...) 翻转。指针在左下角时 uMouse≈(0,0),右上角时 uMouse≈(1,1)。这也是把触摸、点击传入 Shader 的常用步骤;不要直接把以 CSS 像素为单位的 clientX 当作 0–1 的 uniform。
第 6 题在顶点着色器中旋转 position.xy。二维旋转公式为 x' = x cosθ - y sinθ,y' = x sinθ + y cosθ。在局部坐标中旋转,三角形绕自身原点转;若把原点移走,旋转中心也会变。第 7 题把同一个 uTime 波形用于顶点缩放和片元亮度:这两个阶段不能互相调用,但能读取相同的 uniform,因此尺寸与颜色同步“呼吸”。
常规实践和故障排查
- 渲染循环只更新时间值与已有 uniform,不要每帧新建材质、几何体或
Vector。本课的公共运行时负责缩放、停止循环和释放仍在场景中的资源;页面监听器应显式移除。 - ShaderMaterial 适合教学和自定义视觉算法。需要标准 PBR 光照、阴影或完整材质工作流时,优先用
MeshStandardMaterial或在其基础上扩展。 - 全黑先检查浏览器控制台中的 GLSL 编译错误,再检查
.vert/.frag的 Network 状态、uniform 名称和取值。动画静止则检查uTime是否每帧更新,以及浏览器标签页是否暂停。 - 鼠标偏移时,确认用了画布的
getBoundingClientRect(),而非整个窗口宽高。窄屏页面和滚动位置都会改变画布在窗口中的位置。
练习
| 难度 | 题目 | 可见验收 |
|---|---|---|
| ⭐ | 纯红三角形 | 深色背景中央出现红色三角形 |
| ⭐ | 用 uniform 传入绿色 | 只改 JS 数值即可变色 |
| ⭐⭐ | 红蓝之间平滑循环 | 两色之间连续往返 |
| ⭐⭐ | RGB 三通道正弦循环 | 三通道错峰变化 |
| ⭐⭐⭐ | 鼠标控制红绿通道 | 向右变红、向上变绿 |
| ⭐⭐⭐ | 顶点着色器旋转三角形 | 绕 Z 轴连续旋转 |
| ⭐⭐⭐⭐ | 颜色与缩放同步呼吸 | 放大时变亮,缩小时变暗 |