17. TSL 为何是 WebGPU 时代的语言无关层
16 篇 讲清了 WGSL 和 GLSL 的差异。一个很自然的问题:Three.js 团队为什么不让用户直接写 WGSL,而是搞个 TSL 出来?
如果 WebGPU 是未来,TSL 是不是过渡品?这一篇把这个问题拆开。
目标:理解 TSL 在 Three.js 整体架构里的位置,不是学 TSL 怎么用(那是 threejs_advanced/09_TSL入门与节点式着色.md 的事)。
1. 背景:WebGL 时代 Three.js 怎么处理着色器
Three.js 在 WebGL 时代是这样工作的:
用户写 .glsl 文件或字符串
│
▼
ShaderMaterial / RawShaderMaterial
│
▼
Three.js 拼接 ShaderChunk 模板
│
▼
GLSL 字符串
│
▼
gl.compileShader / gl.linkProgram
ShaderChunk 是 GLSL 的 #include 片段库。onBeforeCompile 让你能在这个流程里做字符串替换。
问题 1:字符串拼接的脆弱性(05 篇详述)
问题 2:跨后端的不可能性——onBeforeCompile 注入的是 GLSL 字符串,WebGPU 完全不认。如果 Three.js 要支持 WebGPU,所有内置 Shader 必须重写。
2. 假如"让用户直接写 WGSL"会怎样
最直觉的方案是:
// 假想 API
const material = new THREE.MeshStandardMaterial({
wgsl: {
vertex: wgslString1,
fragment: wgslString2,
},
});
但这条路走不通。理由有四:
2.1 维护成本爆炸
Three.js 内部约 60+ 套内置 Shader(MeshStandard、MeshPhysical、Shadow、PostProcessing、BatchedMesh、InstancedMesh...)。如果每套都要为 WebGPU 维护一份 WGSL 版,再为 WebGL2 维护一份 GLSL 版,维护量翻倍。
2.2 共享逻辑无法复用
PBR 光照、阴影计算、雾效、IBL 这些逻辑在 GLSL 和 WGSL 之间不能直接复用。两份代码意味着两套 bug。
2.3 跨后端运行时切换失败
// 假想代码
if (navigator.gpu) {
material.useWebGPU(wgslString); // 一份代码
} else {
material.useWebGL(glslString); // 另一份代码
}
用户要为同一特效维护两份等价代码,TSL 抽象的核心动机就是为了消除这种重复。
2.4 Shader 编译期错误难以定位
WGSL 编译错误信息是"按 WGSL 源码行列号"报错的。如果用户写了 WGSL 字符串,错误直接指向用户写的字符串,没问题。但对 Three.js 自动生成的部分,用户根本看不到那部分代码,错误信息没有意义。
3. TSL 的方案:抽象语法树(AST)作为中间表示
TSL 的核心抽象:JS 端构造 AST,由 Three.js 决定如何把 AST 编译到 GLSL 或 WGSL。
┌─────────────────────┐
│ TSL 节点(JS 端) │
│ Fn, uniform, mix │
│ positionLocal, ... │
└──────────┬──────────┘
│ AST
┌──────────────┴──────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ WebGPU 后端 │ │ WebGL2 后端 │
│ AST → WGSL │ │ AST → GLSL │
└──────────────────┘ └──────────────────┘
这与编译器领域的 LLVM IR 是同一种思路:IR 不直接跑,而是被翻译到不同硬件指令。TSL 就是 Three.js Shader 领域的 IR。
4. TSL 实际是怎么编译的
看一段 TSL 代码:
import { Fn, positionLocal, time, sin, vec3 } from "three/tsl";
material.positionNode = Fn(() => {
const h = sin(time.add(positionLocal.x.mul(2.0))).mul(0.5);
return positionLocal.add(vec3(0, h, 0));
})();
JS 端不执行这段代码,只构造 AST:
FnCall {
fn: "sin",
args: [
Add {
left: Uniform { name: "time" },
right: Mul {
left: Attribute { name: "positionLocal", swizzle: "x" },
right: Constant(2.0)
}
}
]
}
Mul { left: prev, right: Constant(0.5) }
Add { left: positionLocal, right: Vec3(0, prev, 0) }
关键点:AST 是后端无关的。Three.js 内置两套 CodeGen:
WebGPUBackend:把 AST 翻译成 WGSL 字符串WebGLBackend:把 AST 翻译成 GLSL 字符串
用户只写一次,后端自动适配。
5. TSL 解决了 16 篇提到的所有痛点
| 痛点 | TSL 的解法 |
|---|---|
onBeforeCompile 字符串 hack | 直接赋值 AST |
| 类型缺失 | TypeScript 强类型 + 编译期校验 |
| customDepthMaterial 手动同步 | 节点系统自动同步 |
| WebGPU/WebGL2 双跑 | AST 一份,两个后端各编译一次 |
| 60+ 内置 Shader 重写 | 内置材质全部走 TSL,自动跨后端 |
其中第 5 点是最关键的价值:Three.js 整个内置材质库(MeshStandard、MeshPhysical、ShadowMaterial、NodeMaterial、LineMaterial...)全部从 GLSL 迁移到 TSL。也就是说,未来 WebGPU 普及后,所有内置材质的 PBR、光照、阴影完全不需要重写。
6. TSL 自身的代价
TSL 不是免费的。它引入了:
- 抽象开销:TSL → WGSL 的 IR 层让最终代码比手写 WGSL 多 5%~10%(实测:Three.js 官方 benchmark)。极高性能场景下能感知到。
- 学习成本:要学
Fn、varyingProperty、FnCompute等新概念。 - 新 API 仍在演化:TSL 早期版本(r160 之前)API 大改过两次,生产代码要锁定版本。
- 调试边界模糊:写错 TSL 节点时,错误指向 WGSL 输出而不是 JS 源,定位成本高。
但对绝大多数 Web 3D 项目,这些代价远低于它在跨后端、维护性、复用上的收益。
7. 类比:TSL 在生态里的位置
为了让你看清楚 TSL 不是孤例,它的设计哲学在很多领域都有对应物:
| 领域 | 用户写 | 后端编译到 |
|---|---|---|
| Three.js | TSL 节点 | WGSL / GLSL |
| LLVM | C++ / Rust | x86 / ARM / RISC-V / WASM |
| TensorFlow | Python / Keras | XLA → GPU Kernel / CPU |
| React | JSX | DOM API |
| Swift | Swift 代码 | LLVM IR → 机器码 |
TSL 就是 GPU Shader 领域的“JSX”——一套声明式 API,编译器决定如何落到目标平台。
8. TSL 适合谁,不适合谁
8.1 适合
- 跨后端项目:今天用 WebGL2,明天可能迁 WebGPU
- 长期维护的代码库:节点系统是声明式的,半年后回看依然可读
- 需要 GPGPU(Compute):TSL 的
FnCompute+instancedArray是 2026 年最简洁的 GPGPU 写法
8.2 不适合
- 极致性能场景(1000 万顶点、海量粒子):手写 GLSL/WGSL 仍有 5%~10% 优势
- 教学 / 演示:讲 WebGL 原理时直接看 GLSL/WGSL 源码更清晰
- 不打算升级 WebGPU:纯 WebGL2 项目直接
onBeforeCompile更快
9. 一句话总结
TSL 不是“WebGPU 时代的新玩具”,它是 Three.js 团队为 60+ 内置 Shader 同时跑在 GLSL/WGSL 两个后端所选择的“中间表示”。用 TSL 写自定义材质,等于买了一份面向未来的跨后端保险。
10. 延伸阅读
- threejs_advanced/09_TSL 入门与节点式着色:TSL 完整实战教程
- 16. WGSL vs GLSL 思维模型差异:如果想理解 TSL 编译出的 WGSL 长什么样
- Three.js 仓库
src/nodes/目录:TSL 节点实现,可以反推“哪些节点在哪些后端可用” - WebGPU Spec:webgpu.spec.whatwg.org
- WGSL Spec:wgsl.spec.whatwg.org
思考与练习
- 解释为什么 Three.js 团队选择“AST 中间表示”而不是“字符串宏替换”作为 WebGPU 过渡方案。至少列出 3 个反方观点并反驳。
- 调研 Three.js 仓库的
src/renderers/webgpu/nodes/目录,找出 3 个节点(如SinNode、MixNode、PositionNode)并说明它们如何对应到 GLSL 和 WGSL 后端。 - 阅读 React 的
JSX → createElement → DOM流程,对比 TSL → AST → WGSL 的设计哲学相似点。 - 调研题:Three.js 在 r150~r170 之间,TSL API 有过哪些 breaking change?对你项目代码的迁移成本如何评估?