Skip to main content

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 不是免费的。它引入了:

  1. 抽象开销:TSL → WGSL 的 IR 层让最终代码比手写 WGSL 多 5%~10%(实测:Three.js 官方 benchmark)。极高性能场景下能感知到。
  2. 学习成本:要学 FnvaryingPropertyFnCompute 等新概念。
  3. 新 API 仍在演化:TSL 早期版本(r160 之前)API 大改过两次,生产代码要锁定版本。
  4. 调试边界模糊:写错 TSL 节点时,错误指向 WGSL 输出而不是 JS 源,定位成本高。

但对绝大多数 Web 3D 项目,这些代价远低于它在跨后端、维护性、复用上的收益。

7. 类比:TSL 在生态里的位置

为了让你看清楚 TSL 不是孤例,它的设计哲学在很多领域都有对应物:

领域用户写后端编译到
Three.jsTSL 节点WGSL / GLSL
LLVMC++ / Rustx86 / ARM / RISC-V / WASM
TensorFlowPython / KerasXLA → GPU Kernel / CPU
ReactJSXDOM API
SwiftSwift 代码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. 延伸阅读

思考与练习

  1. 解释为什么 Three.js 团队选择“AST 中间表示”而不是“字符串宏替换”作为 WebGPU 过渡方案。至少列出 3 个反方观点并反驳
  2. 调研 Three.js 仓库的 src/renderers/webgpu/nodes/ 目录,找出 3 个节点(如 SinNodeMixNodePositionNode)并说明它们如何对应到 GLSL 和 WGSL 后端。
  3. 阅读 React 的 JSX → createElement → DOM 流程,对比 TSL → AST → WGSL 的设计哲学相似点。
  4. 调研题:Three.js 在 r150~r170 之间,TSL API 有过哪些 breaking change?对你项目代码的迁移成本如何评估