TSL 着色语言:用 JavaScript 写着色器
本教程共 40 篇 · 第 35 篇 · 更新于 2026-08-14 · 约 9 分钟阅读
本节目标:理解 TSL 是什么、为什么出现,学会用 NodeMaterial 和常用节点写自定义材质,能比较 TSL 与手写 GLSL 的差异,知道它与 WebGPU 的关系。
第 33、34 章写着色器靠字符串拼接。字符串没有类型检查,改一处要小心全局,出错只有编译日志。three.js 官方也清楚这个痛点,于是有了 TSL。
TSL 是什么
TSL(Three.js Shading Language)是 three.js 的着色语言,本质是”用 JavaScript 对象描述着色器”。每个值、每个运算都是一个节点(Node),节点连节点组成一张计算图,最后由 three.js 编译成真正的 GPU 代码。
import { uv, color, mix } from 'three/tsl';
// 用 uv.x 在红蓝之间插值
material.colorNode = mix(color(0xff0000), color(0x0000ff), uv().x);
这一行描述的就是:颜色 = 红色和蓝色的混合,混合系数是纹理坐标 x。读起来像拼积木,不用管它最终编译成 GLSL 还是 WGSL。
TSL 的几个核心卖点:
- 写的是 JavaScript,有类型、有报错、可以拆分复用;
- 自动优化:同一个节点多处使用,只计算一次;
- 渲染器无关:同一份代码,WebGL2 编译成 GLSL,WebGPU 编译成 WGSL;
- 官方主推:r185 的示例和文档大量使用 TSL,这是 three.js 的未来方向。 TSL 的学习曲线比想象中平缓:节点名字几乎就是 GLSL 函数名,第 34 章学的 mix、smoothstep、fract 全都能直接找到对应。区别只在写法——GLSL 是”在函数里写公式”,TSL 是”把公式接成链条”。
与手写 GLSL 的对比
同一个”细节贴图”效果,两种写法。手写 GLSL 要用 onBeforeCompile 往内置材质源码里做字符串替换:
material.onBeforeCompile = (shader) => {
shader.uniforms.detailMap = { value: detailMap };
let token = '#define STANDARD';
let insert = 'uniform sampler2D detailMap;';
shader.fragmentShader = shader.fragmentShader.replace(token, token + insert);
// 还要继续替换第二处 token,代码越长越难维护
};
TSL 写法:
import { texture, uv } from 'three/tsl';
const detail = texture(detailMap, uv().mul(10)); // 细节贴图,uv 放大 10 倍
material.colorNode = texture(colorMap).mul(detail);
字符串替换 vs 节点组合,可维护性一目了然。TSL 不需要关心代码插入顺序,节点系统会自己处理声明和去重。同一个节点被多处引用,也只会计算一次。
Node 材质:可以插节点的材质
TSL 的载体是 Node 材质。它和普通材质用法一样,只是多了 colorNode、positionNode、roughnessNode 这类”插槽”,把节点装进去就生效:
colorNode:漫反射颜色;positionNode:顶点位置,做顶点动画;roughnessNode/metalnessNode:PBR 粗糙度、金属度;opacityNode:透明度。
常用 Node 材质有 MeshStandardNodeMaterial(标准 PBR)、MeshBasicNodeMaterial(基础)、MeshPhongNodeMaterial(Phong)、PointsNodeMaterial(粒子)、SpriteNodeMaterial(精灵)。它们对应普通材质的标准、基础、Phong、粒子、精灵,多出来的就是这些节点插槽。 Node 材质保留了普通材质的所有属性:color、transparent、opacity、side 照常设置,灯光、阴影、雾照常生效。节点插槽是”覆盖”而不是”取代”:不设置 colorNode,它就按普通 color 属性渲染;设置了,节点算出的值优先。所以可以把 Node 材质当成”普通材质 + 可编程插槽”,迁移成本很低。
Noter185 的 WebGLRenderer 渲染 Node 材质,需要设置节点处理器:
renderer.setNodesHandler(new WebGLNodesHandler())。官方所有 WebGL + TSL 示例都这么写,忘了它材质会渲染异常。
常用节点速查
TSL 提供了和 GLSL 变量一一对应的节点函数,从 ‘three/tsl’ 导入:
| 节点 | 对应 GLSL | 说明 |
|---|---|---|
| positionLocal | attribute vec3 position | 顶点局部坐标 |
| uv() | attribute vec2 uv | 纹理坐标 |
| normalLocal | attribute vec3 normal | 法线 |
| color(r, g, b) | vec3(…) | 颜色常量 |
| float(x) / vec2() / vec3() | 类型转换 | 创建常量或转换类型 |
| texture(tex) | texture2D | 采样纹理 |
| time | uniform float uTime | 自动更新的时间 |
| cameraPosition | uniform vec3 cameraPosition | 相机位置 |
数学函数几乎与 GLSL 同名:sin、cos、mix、smoothstep、fract、clamp、dot、normalize、length。用法换成方法链:
// GLSL: 0.5 + 0.5 * sin(uTime)
// TSL:
time.sin().mul(0.5).add(0.5)
// GLSL: mix(colorA, colorB, t)
// TSL:
mix(colorA, colorB, t)
算术运算符也变成了方法:.add() 加法、.sub() 减法、.mul() 乘法、.div() 除法、.mod() 取余。习惯之后,读代码就像读算式。
除了这些”读”数据的节点,还有”写”数据的 uniform()。它创建一个可以随时更新的值,把 JS 侧的参数传进材质:
import { uniform } from 'three/tsl';
const uSpeed = uniform(1.0); // 创建 uniform 节点
// 材质里直接用 uSpeed
material.colorNode = ...;
uSpeed.value = 2.0; // 运行中改值,下一帧生效
想封装一段可复用的逻辑,用 Fn() 包成函数:
const oscSine = Fn(() => {
return time.mul(2.0).sin().mul(0.5).add(0.5); // 0~1 呼吸波
});
material.opacityNode = oscSine(); // 透明度跟着呼吸
TSL 还自带一批实用节点:oscSine 正弦振荡、hash 随机数、rotateUV 旋转纹理坐标、saturate 截断到 0~1、positionWorld / normalWorld 世界空间坐标。写效果前先翻一遍 three/tsl 的导出列表,很多轮子官方已经造好了。
完整示例:会呼吸的波浪平面
把节点拼成一个可运行的示例:平面顶点随正弦波起伏,颜色在蓝橙之间渐变呼吸。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>35 TSL 着色语言</title>
<style>body { margin: 0; overflow: hidden; }</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.tsl.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three/webgpu';
import { WebGLRenderer } from 'three';
import { WebGLNodesHandler } from 'three/addons/tsl/WebGLNodesHandler.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { uv, time, sin, mix, color, positionLocal, vec3 } from 'three/tsl';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 3, 6);
const renderer = new WebGLRenderer();
renderer.setNodesHandler(new WebGLNodesHandler());
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// 灯光:标准材质需要光
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(3, 5, 4);
scene.add(light);
// TSL 节点材质
const material = new THREE.MeshStandardNodeMaterial();
// 颜色:随时间在蓝橙之间呼吸
const t = sin(uv().x.mul(6.28318).add(time)).mul(0.5).add(0.5);
material.colorNode = mix(color(0x1e3a8a), color(0xf97316), t);
// 位置:顶点沿 z 方向做正弦波浪
material.positionNode = positionLocal.add(
vec3(0, 0, sin(positionLocal.x.mul(2).add(time)).mul(0.4))
);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(8, 8, 64, 64), material);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
</script>
</body>
</html>
拆解一下两个节点:
- 颜色节点:
uv().x是 01 的横向坐标,乘 2π 加时间后取 sin,得到 -11 的波动,再映射回 0~1。mix 按这个系数在蓝、橙之间插值,形成流动的渐变; - 位置节点:把
positionLocal加上一个 z 方向的偏移,偏移量随顶点的 x 坐标和时间波动,平面就变成了起伏的波浪。细分数 64×64 让波浪足够平滑。
Note
import * as THREE from 'three/webgpu'是为了拿到 MeshStandardNodeMaterial 等 Node 材质,渲染器仍然是 WebGLRenderer。TSL 的好处就在这:代码不区分渲染器,未来换 WebGPURenderer 只需改渲染器实例化方式。
TSL 与 WebGPU 的关系
很多人把 TSL 和 WebGPU 画等号,其实不对。TSL 是着色语言抽象,WebGPU 是底层图形 API。当前状态:
- WebGLRenderer:TSL 编译成 GLSL,r185 可用,需要 setNodesHandler;
- WebGPURenderer:TSL 编译成 WGSL,是 WebGPU 的官方搭配;
- 同一份 TSL 代码,两种渲染器都能跑。
WebGPU 提供了 WebGL 没有的能力:计算着色器、存储缓冲、更现代的管线。这些能力在 WebGL 上用不了,所以部分 TSL 功能(计算相关)只有 WebGPU 才完整。第 38 章会详细讲 WebGPU,这里记住结论:TSL 是面向未来的写法,现在学,WebGL 和 WebGPU 都受益。
什么时候用 TSL
给个实用建议:
- 新项目、新效果:优先 TSL,官方主推,维护成本低;
- 学习着色器原理:先学 GLSL(第 33、34 章),理解 GPU 到底在算什么;
- 移植网上老代码:那些还是 GLSL 字符串,读得懂才搬得动。 还有一个判断标准:看效果将来要不要上 WebGPU。如果项目明确走 WebGPU(第 38 章会讲),TSL 是唯一不用写两遍代码的方案;如果只在 WebGL 上跑,两种写法都能用,选你读得懂、改得动的那种。
TSL 没有让 GLSL 消失,而是把它藏进了编译器。懂 GLSL 的人学 TSL 很快,反过来也一样——两边是同一套数学。