Three.js TSL CodeNode:在节点系统中嵌入原生着色器代码的机制与用法
本文基于 three.js 官方 API 文档 CodeNode 页面,系统讲解 TSL(Three Shading Language)中 CodeNode 的构造参数、属性、方法及其默认值,并结合 源码实现 剖析它如何被 NodeBuilder 编译进最终着色器,以及它与 FunctionNode 等子类的继承关系。读完本文,你将掌握在 three.js 节点材质中安全嵌入 WGSL/GLSL 原生代码片段的完整方式。
CodeNode 的定位:节点系统到原生着色器的桥梁
CodeNode 表示一段"原生代码段"(native code sections)。它的继承链为 EventDispatcher → Node → CodeNode,是那些需要直接书写原生着色器语言(GLSL 或 WGSL)的节点模块的基类——最典型的子类就是 FunctionNode,它允许用原生着色器语言实现函数。
这一角色可以从源码中得到印证:CodeNode.js 中 class CodeNode extends Node,其构造函数第一行即调用 super('code'),将节点类型注册为 'code'。在 src/nodes/code/ 目录下,CodeNode 与 FunctionNode、FunctionCallNode、ExpressionNode 共同构成了 TSL 与原生着色器代码交互的基础设施。
构造函数与 TSL 快捷函数
文档定义的构造函数签名为:
new CodeNode( code = '', includes = [], language = '' )
三个参数均为可选,语义与默认值如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
code |
string |
'' |
原生代码文本(着色器代码片段) |
includes |
Array<Node> |
[] |
需要随代码一同注入的节点数组,通常是被引用到的其他 CodeNode/FunctionNode |
language |
'js' | 'wgsl' | 'glsl' |
'' |
代码使用的语言 |
除了直接 new CodeNode(...),CodeNode.js 底部还导出了四个 TSL 函数,这是实际使用中最常见的入口:
// TSL 函数:创建通用 code node,参数 1~3 个
export const code = nodeProxy( CodeNode ).setParameterLength( 1, 3 );
// 三个语言快捷包装
export const js = ( src, includes ) => code( src, includes, 'js' );
export const wgsl = ( src, includes ) => code( src, includes, 'wgsl' );
export const glsl = ( src, includes ) => code( src, includes, 'glsl' );
可以看到 wgsl/glsl/js 只是 code 的薄封装,差异仅在第三个 language 参数被固定。code 通过 nodeProxy 生成,因此既支持 code('...') 这样的函数式调用,也支持 new CodeNode(...) 的构造式调用。
属性详解:.code、.global、.includes、.isCodeNode 与 .language
文档列出了 5 个属性,结合源码逐条说明:
.code : string
原生代码本体,默认 ''。在 构造函数 中赋值给 this.code。
.global : boolean
该标志用于"全局缓存"(global cache),默认 true。注意这里覆盖了 Node 基类的行为:在 Node.js 中,普通节点 this.global 默认是 false,而 CodeNode 在构造时将其置为 true。
为什么要默认全局?从 NodeBuilder 的源码结构看,节点数据存放在两级缓存中:node.isGlobal( this ) ? this.globalCache : this.cache(见 NodeBuilder.js 与 NodeBuilder.js)。全局缓存跨 shader stage 共享,而普通缓存按 stage 隔离。着色器函数这类声明性代码在 vertex/fragment 各阶段只需输出一次、位置全局可见,天然适合放入 globalCache——这正是文档中 "Overrides: Node#global" 一行的工程含义。
.includes : Array.<Node>
includes 数组,默认 []。它声明了"这段原生代码依赖哪些前置声明",是 FunctionNode 链式调用能跨节点工作的关键(见下文"与 FunctionNode 的关系")。
.isCodeNode : boolean (readonly)
只读类型测试标志,默认 true,可用于 instanceof 之外的轻量类型判断。
.language : 'js' | 'wgsl' | 'glsl'
代码语言标识,默认 ''。它会被写入序列化数据(见下文),是区分同一 CodeNode 在不同后端(WebGL 走 GLSL、WebGPU 走 WGSL)下代码语义的依据之一。
方法:.getIncludes( builder ) 与 .setIncludes( includes )
文档定义了两个方法:
.getIncludes( builder : NodeBuilder ) : Array.<Node>——返回当前 code node 的 includes;.setIncludes( includes : Array.<Node> ) : CodeNode——设置 includes,返回this(可链式调用)。
源码中 getIncludes 虽然接收 builder 参数但并不使用它(参数被注释掉),直接返回 this.includes。保留 builder 形参是为了与节点系统的统一接口对齐,也给子类留出自定义空间——子类可以依据当前构建上下文动态决定注入哪些 include。setIncludes 则是标准的可链式 setter。
编译生成原理:generate() 如何把代码放进着色器
CodeNode 真正"生效"发生在 generate( builder ) 阶段,其实现(CodeNode.js)可以概括为三步:
generate( builder ) {
const includes = this.getIncludes( builder );
for ( const include of includes ) {
include.build( builder, 'void' ); // 1. 先把依赖的前置声明构建进全局
}
const nodeCode = builder.getCodeFromNode( this, this.getNodeType( builder ) );
nodeCode.code = this.code; // 2. 以该 code node 为 key 取(或创建)NodeCode
return nodeCode.code; // 3. 返回代码文本
}
- 先构建 includes:以
'void'输出模式逐个build依赖节点,保证被引用函数在调用点之前完成声明; - 经
builder.getCodeFromNode取缓存槽位:查看 NodeBuilder.js 可知,getCodeFromNode以shaderStage为维度维护codes数组——同一 shader stage 内同一个 code node 只会分配一个NodeCode(命名形如nodeCode0、nodeCode1),后续引用复用同一槽位,避免重复注入; - 写入代码:把
this.code直接赋给该槽位,最终由后端(WGSLNodeBuilder 或 GLSLNodeBuilder 等)把NodeCode汇入对应 shader stage 的全局声明区。
generate 不在 API 文档中列出(属于内部机制),但它是理解 "includes 顺序为何重要"、"代码为何只出现一次" 的必要补充。
与 FunctionNode 的关系:一个可用的完整示例
FunctionNode 继承自 CodeNode(class FunctionNode extends CodeNode,见 FunctionNode.js),在构造时直接复用父类的 code/includes/language 三参数,另外的价值在于:它会把原生代码解析为 NodeFunction,自动完成函数声明注册、参数类型推导与调用包装(wgslFn/glslFn 返回的即是可直接调用的函数代理)。
FunctionNode.js 的 JSDoc 给出了一个展示 includes 用法的标准示例——第二个函数依赖第一个函数声明,通过 includes 数组声明依赖:
const desaturateWGSLFn = wgslFn( `
fn desaturate( color:vec3<f32> ) -> vec3<f32> {
let lum = vec3<f32>( 0.299, 0.587, 0.114 );
return vec3<f32>( dot( lum, color ) );
}`
);
const someWGSLFn = wgslFn( `
fn someFn( color:vec3<f32> ) -> vec3<f32> {
return desaturate( color );
}
`, [ desaturateWGSLFn ] ); // 通过 includes 声明对 desaturate 的依赖
material.colorNode = someWGSLFn( { color: texture( map ) } );
结合前文对 generate() 的分析可以确认:[ desaturateWGSLFn ] 中的节点会在 someWGSLFn 生成前以 void 模式先被构建,从而确保 desaturate 的函数声明出现在调用之前。这正是 CodeNode 的 includes 机制在设计上的用途。
同目录下的 FunctionCallNode.js 与 ExpressionNode.js 分别负责原生函数调用与 return/continue/discard 这类语句级表达式,可与 CodeNode 配合使用。
序列化支持:serialize 与 deserialize
CodeNode 实现了 serialize/deserialize,在调用父类序列化之后额外持久化两个字段:
serialize( data ) {
super.serialize( data );
data.code = this.code;
data.language = this.language;
}
即 code 与 language 会随节点图一起序列化/反序列化,这使得包含原生代码段的节点图可以被完整保存与还原——对需要持久化场景(例如节点编辑器的场景存档)是必要的能力。
相关文档与源码索引
- 文档:CodeNode.html.md、FunctionNode.html.md、Node.html.md
- TSL 总览:docs/TSL.md
- 源码:src/nodes/code/CodeNode.js、src/nodes/code/FunctionNode.js
- 构建机制:src/nodes/core/NodeBuilder.js(
isGlobal/globalCache与getCodeFromNode)、src/nodes/core/Node.js(global标志定义)
适用前提:本文基于当前仓库版本的 TSL 实现,language 支持 'js' | 'wgsl' | 'glsl' 三种取值;CodeNode 属于 TSL(Three.TSL.js 导出链)的一部分,使用时需引入 TSL 模块。由于 CodeNode 直接透传原生代码字符串,嵌入的代码需自行保证与目标语言(GLSL/WGSL)语法一致,库本身不做代码合法性校验。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00