首页
/ three.js TSL 节点系统剖析:FunctionCallNode 与 wgslFn/glslFn 函数调用机制

three.js TSL 节点系统剖析:FunctionCallNode 与 wgslFn/glslFn 函数调用机制

2026-09-06 19:15:00作者:何将鹤

FunctionCallNode 是 three.js 节点材质系统(TSL)中代表「一次着色器函数调用」的底层节点类:它把 FunctionNode(用 WGSL/GLSL 原生代码描述的函数声明)与一组实参绑定,在着色器生成阶段输出形如 funcName( arg0, arg1 ) 的调用代码。多数开发者并不会直接实例化它,因为 TSL 提供的 wgslFnglslFn 语法糖(以及 .call() 方法链)内部已经完全封装了此类;阅读本文你将掌握 FunctionCallNode 的构造与属性、generate 时的实参绑定/错误处理细节、call 工厂函数的工作方式,从而理解 TSL 中「声明函数 → 传参调用 → 接入材质输出」这条完整链路。

一、节点体系中的定位与职责

FunctionCallNode 的类继承链为:

EventDispatcher → Node → TempNode → FunctionCallNode

文档头部即标注了该继承关系。它在概念上并不持有函数实现本身,而只是「某函数节点的一个具体调用现场」:真正承载原生着色器代码(WGSL/GLSL 源码、include 依赖与语言类型)的是 FunctionNode,它是 CodeNode 的子类;FunctionCallNode 则持有对目标 FunctionNode 的引用以及本次调用要传入的实参映射,职责非常单一。

两者定义于同一目录 src/nodes/code/FunctionCallNode.jssrc/nodes/code/FunctionNode.js。这种「声明与调用分离」的设计让同一个 FunctionNode 可以在材质中被多次以不同实参调用,而无需重复注入源码。关于函数声明节点(构造参数、getInputs/getNodeFunction 等)的完整说明,可对照阅读 FunctionNode 文档

FunctionCallNode 继承自 TempNode(见 src/nodes/core/TempNode.js),因此同样拥有临时节点基于类型推断生成中间变量等能力。

二、为什么通常感知不到它的存在:wgslFn / glslFn 语法糖

在 TSL 中,定义一个「可调用的原生着色器函数」最常用的方式是 wgslFn(WebGPU 后端)与 glslFn(WebGL 后端),它们出自 FunctionNode.js

const nativeFn = ( code, includes = [], language = '' ) => {

	const functionNode = new FunctionNode( code, includes, language );

	const fn = ( ...params ) => functionNode.call( ...params );

	return nodeProxyConstructor( fn, functionNode );

};

export const glslFn = ( code, includes ) => nativeFn( code, includes, 'glsl' );
export const wgslFn = ( code, includes ) => nativeFn( code, includes, 'wgsl' );

可以看到,wgslFn(code, includes) 做了两件事:

  1. 内部构造一个 FunctionNode,语言分别固定为 'wgsl''glsl'(对应构造函数签名 new FunctionNode( code : string, includes : Array.<Node>, language : 'js'|'wgsl'|'glsl' ));
  2. 返回一个可调用函数 fn,其内部执行 functionNode.call( ...params )——这里的 .call(...) 正是 FunctionCallNode 暴露的工厂入口。

因此当你写下 someWGSLFn( { color: texture( map ) } ) 时,实际上就触发了一次 FunctionCallNode 的创建。也就是说:每一次调用 wgslFn/glslFn 返回的函数,背后都等价于 new 一个 FunctionCallNode 并把实参传给它

一个包含 include 依赖的最小示例

下面示例来自 FunctionNode 文档,用于演示两个 WGSL 函数之间的依赖注入:

// 1) 声明底层辅助函数(无依赖)
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 ) );
	}`
);

// 2) 声明上层函数,并把 desaturateWGSLFn 作为 include 传入
const someWGSLFn = wgslFn( `
	fn someFn( color:vec3<f32> ) -> vec3<f32> {
		return desaturate( color );
	}
`, [ desaturateWGSLFn ] );

// 3) 以对象形式传参调用,得到一次函数调用节点,直接赋给材质
material.colorNode = someWGSLFn( { color: texture( map ) } );

步骤 2 中第二个数组参数 [ desaturateWGSLFn ] 对应 FunctionNodeincludes:它会把被依赖的函数声明一并注册进最终着色器,确保 someFn 能引用 desaturate;步骤 3 中的 { color: texture( map ) } 则是 FunctionCallNode 中的 parameters

仓库实际案例可参考 examples/jsm/materials/WoodNodeMaterial.js:其中用 TSL.wgslFn(...) 完整声明了一个 27 行的 voronoi3d 木纹噪声函数,随后通过 TSL 调用接口接入 mapRange 等 TSL 节点完成参数映射,最终驱动材质的 color 输出——这是 FunctionCallNode 机制在高真实感程序化材质中的真实落地。

三、构造函数与属性

new FunctionCallNode( functionNode, parameters )

签名如下(见 FunctionCallNode.js):

new FunctionCallNode( functionNode : FunctionNode, parameters : Object.<string, Node> )
参数 类型 默认值 说明
functionNode FunctionNode null 被调用的函数节点,即一次调用所指向的函数声明
parameters Object.<string, Node> {} 本次函数调用的实参集合,键名需与函数输入名对应

两个参数在构造函数中分别赋给同名实例属性:

  • .functionNode : FunctionNode(默认 null):要调用的函数节点。构造时传入 null 在源码中是允许的(构造器做了 functionNode = null 的默认值处理),但在真正 generate 之前必须被正确赋值。
  • .parameters : Object.<string, Node>(默认 {}):函数调用的参数表,每个值都应是 TSL 节点(如 texture(map)float(0.5)),而非原始 JS 数值。

四、方法与核心生成逻辑

.generateNodeType( builder : NodeBuilder ) : string

返回该调用节点的类型。它直接委托给所调用的函数节点:

generateNodeType( builder ) {
	return this.functionNode.getNodeType( builder );
}

(见 FunctionCallNode.js)即「调用的类型 = 函数声明的返回类型」,因此调用方的类型推导与被调函数严格一致。该方法覆写的是 TempNode#generateNodeType

.getMemberType( builder : NodeBuilder, name : string ) : string

用于查询函数返回结构体(struct)中某个成员的字段类型,同样委托给函数节点:

getMemberType( builder, name ) {
	return this.functionNode.getMemberType( builder, name );
}

(见 FunctionCallNode.js)它覆写 TempNode#getMemberType。结合 FunctionNode.js 的实现可知:当被调函数返回 struct 类型节点时,该调用节点会经由 builder.getStructTypeNode( type ) 查到结构体定义,再返回成员 name 的类型——这让 someFn({...}).member 这类链式取成员写法能够正确参与类型推导。

.getParameters() / .setParameters()

setParameters( parameters ) { this.parameters = parameters; return this; }
getParameters() { return this.parameters; }

(见 FunctionCallNode.jssetParameters 返回 this,支持链式调用,便于复用同一个调用节点、在渲染循环中动态更新实参。

.generate( builder )——实参绑定与错误处理的核心

generate 是真正产出着色器代码片段的入口(FunctionCallNode.js)。它内部有几个值得注意的设计:

1) 先取得函数声明的输入形参列表

const inputs = functionNode.getInputs( builder );  // Array<NodeFunctionInput>
const parameters = this.parameters;

getInputs 来自 FunctionNode,内部会通过 builder.parser.parseFunction( this.code ) 解析原生源码得到形参(名称、类型等)。

2) 区分「数组实参」与「对象实参」两种绑定方式

  • parameters数组:按位置与 inputs 一一对应。个数过多/过少时都会通过 error( 'TSL: ...' ) 输出错误:
    • 实参多于形参:截断多余的参数(parameters.length = inputs.length);
    • 实参少于形参:报错后以 float( 0 ) 补足缺位(while ( parameters.length < inputs.length ) parameters.push( float( 0 ) );)。
  • parameters对象:按形参名 inputNode.name 去对象中查找;找不到对应实参时报错并以 float( 0 ) 兜底:
error( `TSL: Input '${ inputNode.name }' not found in 'Fn()'.` );
params.push( generateInput( float( 0 ), inputNode ) );

注意错误文案中出现的是 'Fn()' 而不是 wgslFn/glslFn——因为同一套参数校验逻辑也被 Fn()(JS 函数式 TSL 节点,见 TSLCore.js)复用。

3) 特殊处理 pointer 类型形参

const type = inputNode.type;
const pointer = type === 'pointer';
if ( pointer ) output = '&' + node.build( builder );   // WGSL 取地址
else output = node.build( builder, type );

当形参类型是 'pointer'(WGSL 的指针参数)时,会在实参构建结果前加上 & 取址符,其余类型则按形参类型强制构建。

4) 输出最终调用代码

const functionName = functionNode.build( builder, 'property' );  // 只取函数名,不重复生成声明
return `${ functionName }( ${ params.join( ', ' ) } )`;

这里以 'property' 作为输出模式请求函数名(FunctionNode 的 generateoutput === 'property' 时返回属性名,见 FunctionNode.js),并把已按序排列的实参用逗号拼接,最终生成一行合法的着色器函数调用。

五、call 工厂函数与方法链注册

FunctionCallNode.js 在类定义之外还导出了一个便捷工厂函数 callFunctionCallNode.js):

export const call = ( func, ...params ) => {

	params = params.length > 1 || ( params[ 0 ] && params[ 0 ].isNode === true )
		? nodeArray( params )      // 把可变长参数序列规整为节点数组
		: nodeObjects( params[ 0 ] ); // 否则把唯一参数对象规整为节点对象表

	return new FunctionCallNode( nodeObject( func ), params );

};

addMethodChaining( 'call', call );

要点解读:

  • 它根据实参形态自动选择包装方式:传多个参数或一个 TSL 节点时,用 nodeArray 把参数序列包装为「位置实参」数组;只传一个普通对象时,用 nodeObjects 包装为「键名实参」对象表——这正对应 generate 里那两种绑定分支。
  • nodeObject( func ) 确保 func 是规范化的节点对象(ShaderNodeObject)。
  • 最后一行 addMethodChaining( 'call', call ) 将该函数注册为全局方法链,因此任何 TSL 节点都能以 .call(...) 形式触发,例如 functionNode.call( ...params )。TSL 基类索引 src/nodes/tsl/TSLBase.js 也专门注释 export * from '../code/FunctionCallNode.js'; // .call(),表明该模块是 .call() 链方法的事实来源。

同时 FunctionNode.jswgslFn/glslFn 返回的函数体 ( ...params ) => functionNode.call( ...params ) 走的正是这条 call 工厂路径;Fn()(JS 版函数节点)的调用逻辑 this.shaderNode.call( params ) 亦与此同源。从源码结构看,FunctionCallNode 是 TSL 中所有「原生/JS 函数被调用」场景的统一汇聚点。

六、继承关系与相关节点速查

  • 继承链EventDispatcher → Node → TempNode → FunctionCallNode,完整的节点基类能力(事件分发、引用计数、类型缓存等)来自 EventDispatcher 文档Node 文档TempNode 文档
  • 关联核心节点:函数声明端为 FunctionNode(继承 CodeNode),实参/类型规整工具为 TSLCore.js 中的 nodeObject/nodeObjects/nodeArray/nodeProxyConstructor 等。
  • 模块入口:三个后端构建入口 src/Three.TSL.jssrc/Three.WebGPU.Nodes.js 均导出了该模块;wgslFn/glslFn 的实际 GLSL/WGSL 生成还依赖 src/renderers/webgpu/nodes/WGSLNodeBuilder.jsbuilder.parser.parseFunction 对原生函数签名的解析。

七、把一次函数调用接入材质输出的完整链路

综合上述机制,一次完整的「TSL 原生函数调用」经历了如下阶段:

  1. 声明wgslFn(code, includes) 创建 FunctionNode,函数体源码被缓存,等待解析;
  2. 规整:调用返回函数时,实参经 nodeObjects/nodeArray 规整为节点对象表或数组;
  3. 绑定call 工厂创建 new FunctionCallNode( func, params ),记录函数节点与实参;
  4. 解析:构建期 functionNode.getInputs( builder ) 通过语法解析器还原形参签名,generate 按位置/键名完成实参匹配(缺失补 float(0)、pointer 加 &、数目超限报错);
  5. 产出functionNode.build( builder, 'property' ) 返回函数名,拼接成 fnName( arg0, arg1 ) 着色器代码,同时被 include 的依赖函数声明也一并注册进最终 shader;
  6. 消费:将返回的调用节点赋给材质节点属性(如 material.colorNode = someWGSLFn( { color: texture( map ) } )),最终由 NodeMaterial 编译输出。

这条链路把「用原生着色器语言写函数」与「声明式 TSL 节点图」无缝衔接起来:你在材质的 color、position、normal 等任意节点槽位都能安全地插入一个 wgslFn/glslFn 调用节点,而类型安全、依赖注入与参数校验全部由 FunctionCallNode + FunctionNode 这一对底层节点透明处理。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388