首页 / Three.js 入门教程 / TSL 着色语言:用 JavaScript 写着色器

Three.js 入门教程

TSL 着色语言:用 JavaScript 写着色器

本教程共 40 篇 · 第 35 篇 · 更新于 2026-08-14 · 约 9 分钟阅读

Three.jsTSL着色语言NodeMaterialWebGPU着色器

本节目标:理解 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 的几个核心卖点:

  1. 写的是 JavaScript,有类型、有报错、可以拆分复用;
  2. 自动优化:同一个节点多处使用,只计算一次;
  3. 渲染器无关:同一份代码,WebGL2 编译成 GLSL,WebGPU 编译成 WGSL;
  4. 官方主推: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 材质当成”普通材质 + 可编程插槽”,迁移成本很低。

Note

r185 的 WebGLRenderer 渲染 Node 材质,需要设置节点处理器:renderer.setNodesHandler(new WebGLNodesHandler())。官方所有 WebGL + TSL 示例都这么写,忘了它材质会渲染异常。

常用节点速查

TSL 提供了和 GLSL 变量一一对应的节点函数,从 ‘three/tsl’ 导入:

节点对应 GLSL说明
positionLocalattribute vec3 position顶点局部坐标
uv()attribute vec2 uv纹理坐标
normalLocalattribute vec3 normal法线
color(r, g, b)vec3(…)颜色常量
float(x) / vec2() / vec3()类型转换创建常量或转换类型
texture(tex)texture2D采样纹理
timeuniform float uTime自动更新的时间
cameraPositionuniform 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>

拆解一下两个节点:

  1. 颜色节点:uv().x 是 01 的横向坐标,乘 2π 加时间后取 sin,得到 -11 的波动,再映射回 0~1。mix 按这个系数在蓝、橙之间插值,形成流动的渐变;
  2. 位置节点:把 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

给个实用建议:

  1. 新项目、新效果:优先 TSL,官方主推,维护成本低;
  2. 学习着色器原理:先学 GLSL(第 33、34 章),理解 GPU 到底在算什么;
  3. 移植网上老代码:那些还是 GLSL 字符串,读得懂才搬得动。 还有一个判断标准:看效果将来要不要上 WebGPU。如果项目明确走 WebGPU(第 38 章会讲),TSL 是唯一不用写两遍代码的方案;如果只在 WebGL 上跑,两种写法都能用,选你读得懂、改得动的那种。

TSL 没有让 GLSL 消失,而是把它藏进了编译器。懂 GLSL 的人学 TSL 很快,反过来也一样——两边是同一套数学。