Day 116 · 自定义着色器入门 — 打开 3D 特效的最后一道门
前三周你一直在 Three.js 的"预设材质"里工作:
MeshStandardMaterial调参数、贴图、光照——效果好,但有天花板。今天打开最后一道门:自定义着色器(ShaderMaterial)。着色器是"直接给 GPU 写代码"——你想让物体怎么显示,就怎么算颜色。这是本周的灵魂课,也是"手写 WebGL vs Three.js"对照表的最终章:Three.js 帮你搭好了管线,但着色器里的一切是你自己的。今天只学"够用"的 GLSL 基础,写第一个会发光的自定义平面。
目录
一、为什么需要自定义着色器
1.1 内置材质的天花板
MeshStandardMaterial:调参数能到"真实"
但你要的效果(工业专属)内置材质做不了:
① 扫描脉冲(光带沿设备表面扫过)
② 能量流(流动的光线沿管道走)
③ 数据可视化专属形态(半透明能量柱、波面)
着色器 = 自己定义"每个像素的颜色怎么算"。从"调参"到"写算法",能力跃升。
1.2 定位:不是图形学专家,但要"够用"
红线第 3 条:会写"够用"的着色器即可——扫描线、发光、流动光效这类工业特效,用 GLSL 基础就能写。深入(PBR 公式、阴影算法)是另一个学习曲线,不在本周。
二、GPU 的流水线:顶点着色器 vs 片元着色器
2.1 两个阶段的职责
顶点着色器(Vertex Shader):
每个顶点跑一次
职责:决定顶点在哪(处理 position/矩阵变换)
输出:varying(传给片元着色器的中间量)
片元着色器(Fragment Shader):
每个像素跑一次(几十万次/帧!)
职责:决定这个像素什么颜色(最终输出的 gl_FragColor)
2.2 一个通俗比喻
顶点着色器 = 布置画布上的"锚点"(每个顶点一个位置)
片元着色器 = 给锚点之间的"每一小块颜色"上色(每像素一次)
记忆钩子:顶点管"形状",片元管"颜色"。想做形状动画 → 顶点着色器;想做颜色/光效 → 片元着色器。今天的主角是片元(工业特效大多是"给表面做颜色")。
三、GLSL 基础:够用的语法
3.1 核心语法(足够写工业特效)
// 1. 数据类型
float a = 1.0; // 浮点(注意:必须有小数点!)
vec2 p = vec2(0.0, 1.0); // 2 维向量
vec3 color = vec3(0.0, 0.8, 1.0); // 3 维向量(RGB)
vec4 rgba = vec4(1.0, 0.0, 0.0, 0.5); // 4 维(RGBA)
// 2. 分量访问(xyz / rgb 两套名字)
color.rgb // 相当于 color.xyz
color.x // 第一个分量
// 3. 内置数学函数
sin(uTime) // 正弦(周期动画的万能工具)
mix(a, b, t) // 线性插值:a 与 b 之间按 t 混合
smoothstep(a, b, x) // 平滑阶跃:0~1 平滑过渡(做光带)
clamp(x, 0.0, 1.0) // 夹取范围
3.2 必会的三个函数(工业特效三件套)
四、ShaderMaterial 的结构
import * as THREE from "three";
// ShaderMaterial:用字符串写 GLSL,替代内置材质
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 }, // 时间(循环里更新)
uColor: { value: new THREE.Color(0x00c6ff) }, // 主色
},
vertexShader: `...`, // 顶点着色器(GLSL 字符串)
fragmentShader: `...`, // 片元着色器(GLSL 字符串)
transparent: true, // 按需
});
注意:ShaderMaterial 不参与光照、没有 PBR——它完全由你写的着色器决定。这是"自由"也是"责任"(忘了自己算颜色,物体就是白的)。
五、uniforms:把数据喂给 GPU
5.1 三种传参方式
5.2 更新 uniform(循环里的标准姿势)
// 循环里:每帧更新 uniform(Day 100 时间纪律的着色器版)
material.uniforms.uTime.value = clock.getElapsedTime(); // 时间推进动画
uniform 是"CPU 和 GPU 的通信口":想让动画动,就把时间/状态写进 uniform。今天起你有了"给 GPU 发指令"的能力。
六、第一个自定义材质:发光平面
写一个"会呼吸的发光平面"——把 GLSL 基础全部串起来:
// src/core/three/create-glow-plane.ts
import * as THREE from "three";
/**
* 自定义着色器材质:工业发光平面(可做地面/墙面光带)
* - 用 sin(uTime) 做"呼吸"强度变化
* - 用 mix 混合主色与暗色
* 这是 ShaderMaterial 的最小可运行示例
*/
export function createGlowPlane(): THREE.Mesh {
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x00c6ff) },
},
transparent: true,
depthWrite: false,
// —— 顶点着色器:形状不动,原样输出 ——
vertexShader: `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
// —— 片元着色器:每个像素算颜色(今天是主角)——
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
void main() {
// 呼吸:0.3 ~ 1.0 之间往复(sin 周期)
float pulse = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * 2.0));
// 输出:主色 × 呼吸强度,半透明
gl_FragColor = vec4(uColor * pulse, 0.4);
}
`,
});
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(6, 6), material);
mesh.rotation.x = -Math.PI / 2; // 躺平当"发光地面"
mesh.position.y = 0.01;
return mesh;
}
// 循环里更新:
// const glow = createGlowPlane(); scene.add(glow);
// 每帧:glow.material.uniforms.uTime.value = clock.getElapsedTime();
读这个片元着色器:
pulse用sin(uTime)在 0.35~1.0 间呼吸,uColor * pulse让发光平面"一亮一暗"——这就是工业"呼吸灯/脉冲"的着色器版。Day 117 会在此基础上做扫描光带等。
七、对照表第 7-9 行:着色器
结论(最终章):Three.js 把"管线接线"全包了(矩阵、缓冲区、纹理绑定),但着色器里的代码永远是自己的——这就是"手写 WebGL vs Three.js"对照表的终点:你不是零基础手写,但你要会写那个"最核心的一段"。
八、常见坑点
坑 1:写着色器没反应(全白/全黑)
排查:① 顶点着色器的 gl_Position 那行在吗(没它物体不显示)② fragmentShader 里 gl_FragColor 赋值了吗 ③ uniform 名与 JS 端一致吗(拼写!)。
坑 2:忘记 projectionMatrix * modelViewMatrix
顶点着色器漏了矩阵变换 → 物体不在正确位置/看不见。修法:这行是顶点着色器的"标准模板",先抄对再改(第六节)。
坑 3:uniform 不更新(动画不动)
原因:改了 JS 端 uniform 但循环里没赋值,或名字不一致。修法:每帧 material.uniforms.uTime.value = ...(第五节)。
坑 4:GLSL 语法错(变量类型/分号)
GLSL 严格:float 必须有小数点(1.0 不是 1)、语句有分号、函数有返回。修法:报错信息里找第几行,逐行查语法。
坑 5:ShaderMaterial 物体"不发光/无光照"
原因:ShaderMaterial 不吃光——它全由你的片元着色器决定。修法:记得在片元里自己算颜色(含"假光照")。
九、自测挑战
T1 · 发光平面(50 分钟)
完成第六节:发光平面呼吸效果。调 sin(uTime * 2.0) 的系数看快慢——亲手理解"周期=动画节奏"。
T2 · GLSL 三件套演练(40 分钟)
在片元里分别用 sin/mix/smoothstep 做三个小效果:闪烁 / 双色渐变 / 平滑光带。每个都是 Day 117 特效的积木。
T3 · 逐行读着色器(20 分钟)
把第六节的 vertex/fragment 逐行加注释,讲给自己听(每行干什么)。讲不清楚的地方就是没懂的地方——回去查 GLSL 基础。
T4 · 对照表收尾(20 分钟)
把第 7-9 行补进"手写 WebGL vs Three.js"对照表,通读整张表。这张表是你 3D 能力的"知识地图"——面试前翻它。
十、总结
你写下了第一段 GPU 代码——发光平面在呼吸。明天把这些积木拼成工业特效。
明日预告:Day 117 着色器实战——扫描脉冲(smoothstep 光带)、设备能量发光(uTime 驱动)、顶点动画(形状变形)——用 Day 116 的 GLSL 基础写出真正的工业特效。