第 4 周:渲染管线深入
📅 学习计划
本周目标: 掌握 GLSL 语法,理解 Uniform 和 Attribute 的区别,尝试修改 Shader。
时间安排: 每天 1-2 小时,共 5 天
第 1 天:GLSL 语言基础
📖 理论:GLSL 语法
GLSL (OpenGL Shading Language) 是一种类 C 语言。
基本类型:
float: 浮点数(GLSL 中非常常用,整数很少用)。int: 整数。bool: 布尔值。vec2,vec3,vec4: 2/3/4 维向量(核心类型)。mat3,mat4: 3x3, 4x4 矩阵。sampler2D: 2D 纹理。
向量操作(超级方便):
vec3 v = vec3(1.0, 2.0, 3.0);
v.xy; // 取前两个分量 (1.0, 2.0) -> vec2
v.z; // 取第三个分量
v.rgb; // 等同于 xyz
v * 2.0; // 所有分量乘 2 -> (2.0, 4.0, 6.0)
v1 + v2; // 向量加法
内置函数:
sin,cos,pow,sqrtdot,cross,normalizemix(线性插值),step,smoothstep(平滑阶梯)
💻 实践:项目中的 GLSL
查看代码: line_shader/index.ts:25
// mix 函数的使用
// mix(x, y, a) = x * (1-a) + y * a
// 在两种颜色间混合
vec3 ccc = mix(diffuse, cColor, mask);
查看代码: line_shader/index.ts:32
// step 函数的使用
// step(edge, x) -> 如果 x < edge 返回 0.0,否则返回 1.0
float mask = step(0.5, select);
🎯 练习 4.1:玩转 GLSL
访问 The Book of Shaders 或者在 Three.js Editor 中尝试。
试着写一个简单的颜色混合:
vec3 colorA = vec3(1.0, 0.0, 0.0);
vec3 colorB = vec3(0.0, 0.0, 1.0);
float t = abs(sin(time)); // 让 t 在 0 到 1 之间变化
vec3 finalColor = mix(colorA, colorB, t);
gl_FragColor = vec4(finalColor, 1.0);
📝 今日任务
- 熟悉 GLSL 的向量写法 (
vec3,swizzling) - 理解
mix和step函数
第 2 天:Uniform, Attribute, Varying
📖 理论:三大变量类型
这三个词是 Shader 编程的核心。
Attribute (属性)
- 来源:从 JavaScript (BufferGeometry) 传给 Vertex Shader。
- 频率:每个顶点不同。
- 例子:顶点位置(
position)、法线(normal)、UV坐标(uv)。 - 项目例子:
lineNormal,lineMiter。 - 补充理解:这些 Attribute 本质上都来自 GPU 上的 顶点缓冲(VBO)(也就是 BufferGeometry 里那些
setAttribute(...)上传后的数据)。
Uniform (统一变量)
- 来源:从 JavaScript (Material) 传给 Vertex & Fragment Shader。
- 频率:整个 Draw Call(一次绘制)保持不变。
- 例子:全局颜色、时间、变换矩阵、纹理。
- 项目例子:
thickness,opacity,diffuse。
Varying (易变变量)
- 来源:Vertex Shader 输出,传给 Fragment Shader。
- 特性:会自动插值。
- 项目例子:如果想让线有渐变色,需要在 VS 中算出颜色,传给 varying,FS 接收。
💻 实践:项目中的数据传递
JavaScript 端 (LineRender.ts):
const material = newCustomizedShader({
thickness: width, // 这是一个 uniform
// ...
});
// Attribute 是通过 BufferGeometry 设置的
const geometry = newCustomizedVertexLine(...);
// 里面会调用 geometry.setAttribute(...)
Shader 端 (line_shader/index.ts):
// 定义
uniform float thickness;
attribute vec2 lineNormal;
varying vec4 vColor; // 用于传递颜色
void main() {
// 使用
vec3 offset = vec3(lineNormal * thickness, 0.0);
}
🎯 练习 4.2:添加一个 Uniform
- 在
CustomShaderMaterial的uniforms中添加time。 - 在 JS 的
animate循环中更新material.uniforms.time.value。 - 在 Shader 中声明
uniform float time;。 - 用
time让线动起来(比如改变宽度或颜色)。
📝 今日任务
- 能区分 Attribute (顶点独有) 和 Uniform (全局)
- 理解 Varying 的插值作用
第 3-5 天:综合项目实战 - Shader 分析
🎯 项目实战:实线与虚线 Shader 对比
任务: 对比 SolidShader 和 DashedShader。
SolidShader (实线):
- 逻辑很简单,直接输出颜色。
- 只是处理了选中状态 (
select)。
DashedShader (虚线):
- 位于
line_shader/index.ts:44。 - 多了一个 Attribute:
lineDistance(当前点离起点的距离)。 - 多了一个 Varying:
lineU(把距离传给 FS)。
虚线原理 (Fragment Shader):
// mod 取模,让距离循环
// dashSteps + dashDistance 是一个周期长度
float lineUMod = mod(lineU, dashSteps + dashDistance);
// step 判断:如果在实线段内返回 0,空隙段返回 1(或者反过来)
float dash = 1.0 - step(dashDistance, lineUMod);
// 如果 dash 是 0,乘以颜色就变成透明/黑色了
vec3 ccc = diffuse * vec3(dash);
gl_FragColor = vec4(ccc.xyz, opacity * dash);
🎯 思考题
- 为什么
lineDistance必须是 Attribute?- 因为每个点离起点的距离不一样,必须预先算好存起来。
- 如果想让虚线动起来(流动效果)怎么办?
- 加一个
uniform float time。 mod(lineU + time, ...)。
- 加一个
📝 本周总结
- 我能看懂项目里的 Shader 代码了。
- 我明白了虚线是通过 Shader 里的数学计算模拟出来的,而不是真的断开了几何体。
- 我掌握了 GLSL 的基本语法。
(选修)第 6 天:Three.js TSL 与 WebGPU 方向
这一节是给“想跟上 Three.js 新版和 WebGPU 趋势”的你准备的,不是必须掌握,但非常推荐扫一遍,建立心智模型。
📖 理论:TSL 是什么?
- TSL(Three.js Shader Language) 是 Three.js 在新版里引入的一套 “JS/TS 友好的着色语言层”:
- 不是完全抛弃 GLSL,而是在 GLSL 之上加了一层更高级的表达方式。
- 你用 JS/TS 写出“节点/表达式”的组合,TSL 再把它编译成真正跑在 GPU 上的 Shader 代码(WebGL 用 GLSL,WebGPU 用 WGSL)。
- 可以粗暴地类比为:
- TSL = TypeScript 世界里的 Shader DSL
- 有类型检查、自动拼接、可组合的“节点”,最后统一输出底层 Shader。
✅ 和直接写 GLSL 相比,有什么优势?
站在你刚学完 GLSL 的视角,可以这样对比:
1)类型更安全,和 TS 工具链更友好
- 传统 GLSL:字符串拼接,很容易写错名字、类型,错误在运行时才在控制台爆红。
- TSL:用函数/对象/链式写法,很多错误在编译期就能被 TS/IDE 提示出来。
2)更易组合和复用
- GLSL 要复用一段逻辑,通常靠
#include或复制粘贴,管理起来比较散。 - TSL 鼓励你写成节点函数(比如一个 BRDF、一个噪声函数),像搭积木一样组合,Three.js 帮你把依赖关系、注入顺序都处理好。
- GLSL 要复用一段逻辑,通常靠
3)跨后端更容易(为 WebGPU 做准备)
- 直接写 GLSL:强绑定 WebGL,未来要上 WebGPU 时,要翻译成 WGSL 或重写。
- 写 TSL:上层逻辑不变,由 Three.js 在编译阶段决定输出 GLSL 还是 WGSL。
- 这就是官方一直强调的:TSL 是“跨 WebGL / WebGPU 的一层公共着色抽象”。
4)更接近“材质节点编辑器”的思路
- 如果你用过 Blender / UE 的材质节点,就会很熟悉这种“节点拼接出材质”的思路。
- TSL 把这种节点式表达搬到了代码世界,更适合复杂材质/效果的工程化维护。
简单感受一下对比(伪代码,只看味道):
// 传统 GLSL 片段着色器(字符串)
void main() {
vec3 baseColor = texture(uMap, vUv).rgb;
float nDotL = max(dot(normalize(vNormal), normalize(uLightDir)), 0.0);
gl_FragColor = vec4(baseColor * nDotL, 1.0);
}
// TSL 思路(伪代码):用节点和函数组合同样的逻辑
const baseColor = texture( mapTSL, uvTSL ).rgb;
const n = normalize( normalTSL );
const l = normalize( lightDirTSL );
const nDotL = max( dot( n, l ), 0.0 );
material.colorNode = baseColor.mul( nDotL );
你写的是一棵“表达式树”,Three.js 帮你把它编译成真正跑在 GPU 上的 Shader。
再看几个直观一点的“小对比”:
示例 1:纯色 + 时间闪烁
// GLSL
uniform float uTime;
void main() {
float t = abs(sin(uTime));
gl_FragColor = vec4(t, 0.0, 1.0 - t, 1.0);
}
// TSL(伪代码)
const t = abs( sin( timeTSL ) );
material.colorNode = vec3( t, 0.0, one.sub( t ) );
示例 2:使用一张遮罩纹理控制透明度
// GLSL
uniform sampler2D uMask;
varying vec2 vUv;
void main() {
float m = texture2D(uMask, vUv).r;
gl_FragColor = vec4(1.0, 1.0, 1.0, m);
}
// TSL(伪代码)
const m = texture( maskTSL, uvTSL ).r;
material.colorNode = vec4( one3, m );
示例 3:顶点位移(简单波浪)
// GLSL 顶点着色器
uniform float uTime;
attribute vec3 position;
attribute vec2 uv;
void main() {
vec3 pos = position;
pos.z += sin(pos.x * 4.0 + uTime) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// TSL(伪代码)
const pos = positionTSL.xyz;
const wave = sin( pos.x.mul( 4.0 ).add( timeTSL ) ).mul( 0.1 );
const displaced = vec3( pos.x, pos.y, pos.z.add( wave ) );
material.positionNode = displaced;
可以看到:
- 左边 GLSL:你在字符串里写完整的 Shader 源码。
- 右边 TSL:你在 TS 代码里用“节点 + 表达式”描述同样的计算,Three.js 把这些节点拼成最终 Shader。
🔗 TSL 和 WebGPU 的关系
WebGPU 使用 WGSL,而不是 GLSL:
- WebGPU 的官方着色语言是 WGSL(WebGPU Shading Language),语法和 GLSL 有明显差异。
- 这意味着:如果你项目未来要跑在 WebGPU 上,只会 GLSL 不够用。
TSL 的目标之一:让同一套材质逻辑可以跑在 WebGL + WebGPU 上
- 上层你写的还是 TSL 节点;
- 底层:
- 在 WebGL Renderer 下输出 GLSL;
- 在 WebGPU Renderer 下输出 WGSL。
- 对你来说,迁移成本更像是“换一个 Renderer + 开启部分特性”,而不是“重写所有 Shader”。
所以你可以把 TSL 看成 Three.js 在为未来的 WebGPU 渲染管线“铺的路”:
今天用 TSL 写材质,未来换成 WebGPU 时,更大概率是“换后端”,而不是“推翻重写”。
📝 今日任务(选修)
- 了解 Three.js 官方文档中关于 TSL / NodeMaterial 的介绍。
- 用 TSL 写一个最简单的“颜色 = 纹理 * 漫反射光照”的材质,和你之前的 GLSL 版本对比。
- 思考一下:如果未来要把项目迁到 WebGPU,上层逻辑/材质如何尽量写在 TSL 层,让底层切换成本最小。
下一步: 第 5-6 周:实战项目应用