首页
/ Three.js TSL CodeNode:在节点系统中嵌入原生着色器代码的机制与用法

Three.js TSL CodeNode:在节点系统中嵌入原生着色器代码的机制与用法

2026-09-06 14:27:06作者:董斯意

本文基于 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.jsclass CodeNode extends Node,其构造函数第一行即调用 super('code'),将节点类型注册为 'code'。在 src/nodes/code/ 目录下,CodeNodeFunctionNodeFunctionCallNodeExpressionNode 共同构成了 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.jsNodeBuilder.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. 返回代码文本

}
  1. 先构建 includes:以 'void' 输出模式逐个 build 依赖节点,保证被引用函数在调用点之前完成声明;
  2. builder.getCodeFromNode 取缓存槽位:查看 NodeBuilder.js 可知,getCodeFromNodeshaderStage 为维度维护 codes 数组——同一 shader stage 内同一个 code node 只会分配一个 NodeCode(命名形如 nodeCode0nodeCode1),后续引用复用同一槽位,避免重复注入;
  3. 写入代码:把 this.code 直接赋给该槽位,最终由后端(WGSLNodeBuilderGLSLNodeBuilder 等)把 NodeCode 汇入对应 shader stage 的全局声明区。

generate 不在 API 文档中列出(属于内部机制),但它是理解 "includes 顺序为何重要"、"代码为何只出现一次" 的必要补充。

与 FunctionNode 的关系:一个可用的完整示例

FunctionNode 继承自 CodeNodeclass 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 的函数声明出现在调用之前。这正是 CodeNodeincludes 机制在设计上的用途。

同目录下的 FunctionCallNode.jsExpressionNode.js 分别负责原生函数调用与 return/continue/discard 这类语句级表达式,可与 CodeNode 配合使用。

序列化支持:serialize 与 deserialize

CodeNode 实现了 serialize/deserialize,在调用父类序列化之后额外持久化两个字段:

serialize( data ) {

	super.serialize( data );
	data.code = this.code;
	data.language = this.language;

}

codelanguage 会随节点图一起序列化/反序列化,这使得包含原生代码段的节点图可以被完整保存与还原——对需要持久化场景(例如节点编辑器的场景存档)是必要的能力。

相关文档与源码索引

适用前提:本文基于当前仓库版本的 TSL 实现,language 支持 'js' | 'wgsl' | 'glsl' 三种取值;CodeNode 属于 TSL(Three.TSL.js 导出链)的一部分,使用时需引入 TSL 模块。由于 CodeNode 直接透传原生代码字符串,嵌入的代码需自行保证与目标语言(GLSL/WGSL)语法一致,库本身不做代码合法性校验。

登录后查看全文
热门项目推荐
相关项目推荐