three.js Transpiler 完全指南:将 GLSL 着色器一键转换为 TSL 节点代码
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
Transpiler 是 three.js 提供的一个代码转译器工具,能够在不同着色器语言之间进行转换,目前官方支持的场景是将 GLSL 着色器代码转换为 TSL(Three Shading Language,节点式着色语言),其核心目标是帮助开发者在迁移自研材质时,把旧版 shader 平滑过渡到新的节点化材质系统(Node Material System)。阅读本文后,你将掌握 Transpiler 的构造方式、三大内部组件(decoder / linker / encoder)的职责划分,并能直接使用它(配合GLSLDecoder、TSLEncoder、ShaderToyDecoder等配套模块)把一段自定义 GLSL 源码实时转译为可在MeshBasicNodeMaterial等新材质中运行的 TSL 代码。
Transpiler 是什么
Transpiler是一个"语言到语言"的着色器代码转换器。从当前仓库的实现看(examples/jsm/transpiler/Transpiler.js),其类注释对它的定位做了清晰描述:
A class that transpiles shader code from one language into another.
目前它只能用于把 GLSL 转成 TSL(Transpiler can only be used to convert GLSL into TSL right now)。这一限制并非架构上的先天约束,而是当前官方验证并支持的组合路径。仓库中同时提供了WGSLEncoder等编码器,交互式示例 webgpu_tsl_transpiler.html 中甚至在编码器下拉框里同时允许选择 TSL 与 WGSL 两种输出,但文档明确标注的成熟用法是 GLSL → TSL。
它的典型应用场景是:材质迁移。当你的项目拥有大量基于RawShaderMaterial/ShaderMaterial编写的自定义着色器,而你想迁移到 WebGPU 与 TSL 的节点化材质系统时,可以借助 Transpiler 完成机械性的语法转换,省去逐行手写 TSL 节点代码的工作量。
安装与导入
Transpiler 属于**插件(addon)**而非核心库的一部分,不会被打包进three主模块,必须显式导入。
import Transpiler from 'three/addons/transpiler/Transpiler.js';在仓库中,其源码位于 examples/jsm/transpiler/Transpiler.js,与它同目录的配套模块还包括:
| 模块 | 文件 | 职责 |
|---|---|---|
GLSLDecoder | examples/jsm/transpiler/GLSLDecoder.js | GLSL 解码器,负责词法/语法解析 |
ShaderToyDecoder | examples/jsm/transpiler/ShaderToyDecoder.js | 面向 ShaderToy 的 GLSL 解码器 |
TSLEncoder | examples/jsm/transpiler/TSLEncoder.js | 把 AST 输出为 TSL 代码 |
WGSLEncoder | examples/jsm/transpiler/WGSLEncoder.js | 把 AST 输出为 WGSL 代码 |
Linker | examples/jsm/transpiler/Linker.js | 链接器,解析变量与函数引用 |
AST | examples/jsm/transpiler/AST.js | 语言无关的 AST 节点定义 |
TranspilerUtils | examples/jsm/transpiler/TranspilerUtils.js | 内置类型判断等公共工具 |
按 three.js 的 addon 惯例,three/addons/前缀在构建后的环境中指向examples/jsm/目录;在仓库示例里对应的 import map 写法为"three/addons/": "./jsm/"(见 webgpu_tsl_transpiler.html)。
构造函数:组合一个转译器
new Transpiler( decoder : GLSLDecoder, encoder : TSLEncoder )
Transpiler的构造函数接收两个关键协作对象,并在内部自动创建一个Linker(注意:linker 不需要、也不能由外部传入,它由构造函数自动实例化,见源码第 44 行this.linker = new Linker())。
| 参数 | 类型 | 说明 |
|---|---|---|
decoder | GLSLDecoder | 解码器,负责解析 GLSL 源码 |
encoder | TSLEncoder | 编码器,负责把中间结果输出为 TSL |
一个最小可用组合如下:
import Transpiler from 'three/addons/transpiler/Transpiler.js'; import GLSLDecoder from 'three/addons/transpiler/GLSLDecoder.js'; import TSLEncoder from 'three/addons/transpiler/TSLEncoder.js'; const decoder = new GLSLDecoder(); const encoder = new TSLEncoder(); const transpiler = new Transpiler( decoder, encoder );如果是从 ShaderToy 迁移片元着色器,则把GLSLDecoder换成ShaderToyDecoder(详见下文"迁移 ShaderToy 着色器"一节)。
三个公开属性:decoder、encoder 与 linker
Transpiler 把整条转换链路拆成三个职责单一的可替换组件,并全部以实例属性形式暴露,方便调试与替换。
.decoder : GLSLDecoder
GLSL 解码器。这一阶段负责把 GLSL 文本"读懂":它对源码做分词(tokenize)与语法解析,最终产出一棵与具体语言无关的 AST(抽象语法树),供后续进一步处理。
在 GLSLDecoder.js 内部可以印证这一点——它定义了一套完整的操作符表(一元操作符+ - ~ ! ++ --、算术操作符、优先级分层precedenceOperators、右结合性列表等),以及对数字、字符串、注释、标识符的正则识别规则;解析得到的节点类型(如FunctionDeclaration、VariableDeclaration、For、Switch、Ternary、Accessor等)全部来自 AST.js。这套 AST 是"语言无关"的,因此同一个 AST 理论上可以被不同语言的编码器消费。
.encoder : TSLEncoder
TSL 编码器。它接收 AST 并输出 TSL 代码字符串,是"读 AST → 生成文本"的最终出口。
在 TSLEncoder.js 中可以看到编码阶段的语义映射逻辑:
- 操作符映射表
opLib:把+ - * / % < > <= >= == != && || ^ & | << >>以及各类复合赋值操作符,映射为add / sub / mul / div / remainder / lessThan / equal / and / bitAnd / shiftLeft / addAssign等 TSL 语义函数; - 一元操作符映射表
unaryLib:- → negate、! → not、~ → bitNot、++ → increment、-- → decrement; - 纹理查找函数清单
textureLookupFunctions:覆盖texture / texture2D / texture3D / textureCube / textureLod / texelFetch / textureGrad等; - GLSL 内建函数名转换表
glslToTSL,例如inversesqrt → inverseSqrt; - 内置采样器类型清单(
sampler2D、samplerCube、sampler3D及其 isampler/usampler 变体等)。
这解释了为什么生成结果可以直接喂给节点化材质:编码器输出的是符合three/tsl语义的表达式链。
.linker : Linker
链接器,由构造函数自动创建。它处理 AST 并解析变量与函数引用,确保所有依赖都被正确链接——类似于编译器中"符号解析"阶段。在 Transpiler 的parse()流程中,linker 在 decoder 之后、encoder 之前介入处理。
核心方法:parse()
.parse( source : string ) : string
解析给定的 GLSL 源码,并返回 TSL 语法字符串。
| 项 | 说明 |
|---|---|
参数source | 待转换的 GLSL 源码字符串 |
| 返回值 | 转换得到的 TSL 代码字符串 |
结合 Transpiler.js 的实现,parse()内部严格按三阶段流水线执行:
parse( source ) { const ast = this.decoder.parse( source ); // 1. GLSL → AST // 2. 处理 AST:解析变量与函数引用、做优化 this.linker.process( ast ); return this.encoder.emit( ast ); // 3. AST → TSL }这条"decoder.parse → linker.process → encoder.emit"的调用链就是整个转译器的核心工作流。理解这一点后,你可以自由替换链路中的任意环节:换不同的 decoder(如ShaderToyDecoder),或换不同的 encoder(如WGSLEncoder),拼接出你自己的语言转换管线。
实战一:把一段 GLSL 噪声函数转成 TSL
下面给出一个完整、可运行的示例,演示如何把仓库 webgpu_tsl_transpiler.html 中内置的 Perlin 噪声 GLSL 源码转换为 TSL。
import Transpiler from 'three/addons/transpiler/Transpiler.js'; import GLSLDecoder from 'three/addons/transpiler/GLSLDecoder.js'; import TSLEncoder from 'three/addons/transpiler/TSLEncoder.js'; const glsl = ` const float PI = 3.141592653589793; float rand(vec2 c){ return fract(sin(dot(c.xy ,vec2(12.9898,78.233))) * 43758.5453); } float noise(vec2 p, float freq ){ float unit = 1./freq; vec2 ij = floor(p/unit); vec2 xy = mod(p,unit)/unit; xy = .5*(1.-cos(PI*xy)); float a = rand((ij+vec2(0.,0.))); float b = rand((ij+vec2(1.,0.))); float c = rand((ij+vec2(0.,1.))); float d = rand((ij+vec2(1.,1.))); float x1 = mix(a, b, xy.x); float x2 = mix(c, d, xy.x); return mix(x1, x2, xy.y); } `; const decoder = new GLSLDecoder(); const encoder = new TSLEncoder(); const transpiler = new Transpiler( decoder, encoder ); const tsl = transpiler.parse( glsl ); // 得到 TSL 代码字符串 console.log( tsl );注意到这段 GLSL 中出现了常量定义(const float PI)、自定义函数(rand、noise)、内建函数调用(fract、sin、dot、mix、floor、mod、cos)以及向量构造(vec2(...))。解码器需要把这些全部解析为 AST,随后编码器再翻译成 TSL 的链式节点表达。从源码看,上述 GLSL 运算符与内建函数均位于 GLSLDecoder.js 与 TSLEncoder.js 已支持的操作符与查找函数清单内,属于典型的可转换代码。
实战二:实时转译交互界面(官方示例)
仓库自带的 webgpu_tsl_transpiler.html 是一个实时转译编辑器,界面左侧为 Monaco 编辑器,右侧为只读输出面板。它把上述流程封装成了可交互的演示:
- 左侧下拉框选择解码语言(当前仅
GLSL); - 右侧下拉框选择编码语言(
TSL或WGSL); - 每次修改左侧 GLSL 源码,经约 1 秒防抖后自动触发
build(); - 内部流程与手写代码完全一致(见该文件第 200-235 行):
const decoder = new GLSLDecoder(); const transpiler = new Transpiler( decoder, encoder ); const tsl = transpiler.parse( glsl ); showCode( tsl );其中encoder依据选择动态实例化:
let encoder; if ( options.encoder === 'TSL' ) { encoder = new TSLEncoder(); } else if ( options.encoder === 'WGSL' ) { encoder = new WGSLEncoder(); }任何解析或转换异常都会被try/catch捕获并显示为Error: ...,方便定位不支持的语法。
实战三:把 ShaderToy 片元着色器接入节点化材质
Transpiler的另一个典型用法是迁移 ShaderToy 风格着色器。ShaderToy 片元着色器以mainImage( out vec4 fragColor, in vec2 fragCoord )为入口,并依赖iResolution、iTime等内建输入。仓库为此提供了专用的ShaderToyDecoder,配套示例是 webgpu_shadertoy.html。
在 ShaderToy 场景中,组合变为:
import Transpiler from 'three/addons/transpiler/Transpiler.js'; import ShaderToyDecoder from 'three/addons/transpiler/ShaderToyDecoder.js'; import TSLEncoder from 'three/addons/transpiler/TSLEncoder.js'; const decoder = new ShaderToyDecoder(); const encoder = new TSLEncoder(); const jsCode = new Transpiler( decoder, encoder ).parse( glsl );官方示例 webgpu_shadertoy.html 定义了一个ShaderToyNode extends THREE.Node的节点类,把转译结果封装成一个可直接被 TSL 图消费的节点:
transpile( glsl, iife = false ) { const decoder = new ShaderToyDecoder(); const encoder = new TSLEncoder(); encoder.iife = iife; // 是否包裹为立即执行函数 const jsCode = new Transpiler( decoder, encoder ).parse( glsl ); return jsCode; } parse( glsl ) { const jsCode = this.transpile( glsl, true ); const { mainImage } = eval( jsCode )( TSL ); // 导出 mainImage 函数 this.mainImage = mainImage; }这里用到了TSLEncoder的一个重要可配置属性.iife。从 TSLEncoder.js 源码可见其默认值为false;当iife = true时,编码器输出的 TSL 代码会被包裹成 IIFE(立即执行函数),便于通过eval/ 动态import直接取回导出的mainImage等符号。示例中还提供了异步方案parseAsync()——把转译出的 JS 字符串经data:text/javascriptURL 动态导入执行。
生成自定义节点后,即可作为 TSL 表达式接入新材质系统。示例最终将其挂在平面网格的MeshBasicNodeMaterial上,与时间节点混合做场景切换:
const material = new THREE.MeshBasicNodeMaterial(); material.colorNode = TSL.oscSine( TSL.time.mul( .3 ) ).mix( shaderToy1Node, shaderToy2Node );其中THREE.MeshBasicNodeMaterial与TSL空间分别来自three/webgpu与three/tsl(见示例 import 部分)。这条链路完整展示了 Transpiler 在"旧着色器资产 → 新节点系统"迁移中的实际价值:着色器代码不用重写,只需转换后包一层节点即可复用。
迁移时值得注意的语法边界
Transpiler 的定位是"机械性语法迁移",并非所有 GLSL 写法都能无差别转换。根据文档描述与源码结构,可以总结出如下适用边界(编写源着色器时尽量规避):
- 支持的语法以解码器的分词/解析规则为上限:运算符、字面量、注释、函数/结构体/分支循环的解析规则都硬编码在解码器中,超出清单的语法(如某些编译器扩展指令)会产生解析错误,可在交互示例中得到
Error反馈; - 函数与变量命名遵循 TSL 语义映射:例如 GLSL 的
inversesqrt会被重命名为inverseSqrt(glslToTSL表),运算符会被展开为add、mix对应的节点调用; - 入口函数的差异:普通 GLSL 顶点/片元着色器与 ShaderToy 的
mainImage入口形态不同,需选用对应的 decoder(GLSLDecoder或ShaderToyDecoder); - 转换产物需配合节点材质使用:转译输出的 TSL 字符串本质是供
MeshBasicNodeMaterial、MeshPhysicalNodeMaterial等节点化材质或自定义THREE.Node使用的表达式代码,而非传统ShaderMaterial的着色器文本。
小结与延伸阅读
综上所述,Transpiler以"解码(decoder)→ 链接(linker)→ 编码(encoder)"三段式流水线完成 GLSL → TSL 的转换,代码规模小、组件可替换、接口极简(仅一个parse()方法)。无论你是想批量迁移旧 GLSL 材质,还是想把 ShaderToy 作品搬进 WebGPU 渲染管线,都可以把它作为自动化转换的基础设施。
进一步深入时可参考以下仓库内资料:
- 转译器核心实现:examples/jsm/transpiler/Transpiler.js
- 语言无关 AST 节点定义:examples/jsm/transpiler/AST.js
- 可交互的实时转译示例:examples/webgpu_tsl_transpiler.html
- ShaderToy 转译节点示例:examples/webgpu_shadertoy.html
- GLSL 解码器源码:examples/jsm/transpiler/GLSLDecoder.js
- TSL 编码器源码(含
opLib/unaryLib/.iife):examples/jsm/transpiler/TSLEncoder.js - ShaderToy 专用解码器:examples/jsm/transpiler/ShaderToyDecoder.js
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考