three.js TSL 节点系统剖析:FunctionCallNode 与 wgslFn/glslFn 函数调用机制
FunctionCallNode 是 three.js 节点材质系统(TSL)中代表「一次着色器函数调用」的底层节点类:它把 FunctionNode(用 WGSL/GLSL 原生代码描述的函数声明)与一组实参绑定,在着色器生成阶段输出形如 funcName( arg0, arg1 ) 的调用代码。多数开发者并不会直接实例化它,因为 TSL 提供的 wgslFn 与 glslFn 语法糖(以及 .call() 方法链)内部已经完全封装了此类;阅读本文你将掌握 FunctionCallNode 的构造与属性、generate 时的实参绑定/错误处理细节、call 工厂函数的工作方式,从而理解 TSL 中「声明函数 → 传参调用 → 接入材质输出」这条完整链路。
一、节点体系中的定位与职责
FunctionCallNode 的类继承链为:
EventDispatcher → Node → TempNode → FunctionCallNode
文档头部即标注了该继承关系。它在概念上并不持有函数实现本身,而只是「某函数节点的一个具体调用现场」:真正承载原生着色器代码(WGSL/GLSL 源码、include 依赖与语言类型)的是 FunctionNode,它是 CodeNode 的子类;FunctionCallNode 则持有对目标 FunctionNode 的引用以及本次调用要传入的实参映射,职责非常单一。
两者定义于同一目录 src/nodes/code/FunctionCallNode.js 与 src/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) 做了两件事:
- 内部构造一个
FunctionNode,语言分别固定为'wgsl'与'glsl'(对应构造函数签名new FunctionNode( code : string, includes : Array.<Node>, language : 'js'|'wgsl'|'glsl' )); - 返回一个可调用函数
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 ] 对应 FunctionNode 的 includes:它会把被依赖的函数声明一并注册进最终着色器,确保 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.js)setParameters 返回 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 的 generate 在 output === 'property' 时返回属性名,见 FunctionNode.js),并把已按序排列的实参用逗号拼接,最终生成一行合法的着色器函数调用。
五、call 工厂函数与方法链注册
FunctionCallNode.js 在类定义之外还导出了一个便捷工厂函数 call(FunctionCallNode.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.js 中 wgslFn/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.js、
src/Three.WebGPU.Nodes.js均导出了该模块;wgslFn/glslFn的实际 GLSL/WGSL 生成还依赖 src/renderers/webgpu/nodes/WGSLNodeBuilder.js 中builder.parser.parseFunction对原生函数签名的解析。
七、把一次函数调用接入材质输出的完整链路
综合上述机制,一次完整的「TSL 原生函数调用」经历了如下阶段:
- 声明:
wgslFn(code, includes)创建FunctionNode,函数体源码被缓存,等待解析; - 规整:调用返回函数时,实参经
nodeObjects/nodeArray规整为节点对象表或数组; - 绑定:
call工厂创建new FunctionCallNode( func, params ),记录函数节点与实参; - 解析:构建期
functionNode.getInputs( builder )通过语法解析器还原形参签名,generate按位置/键名完成实参匹配(缺失补float(0)、pointer 加&、数目超限报错); - 产出:
functionNode.build( builder, 'property' )返回函数名,拼接成fnName( arg0, arg1 )着色器代码,同时被 include 的依赖函数声明也一并注册进最终 shader; - 消费:将返回的调用节点赋给材质节点属性(如
material.colorNode = someWGSLFn( { color: texture( map ) } )),最终由 NodeMaterial 编译输出。
这条链路把「用原生着色器语言写函数」与「声明式 TSL 节点图」无缝衔接起来:你在材质的 color、position、normal 等任意节点槽位都能安全地插入一个 wgslFn/glslFn 调用节点,而类型安全、依赖注入与参数校验全部由 FunctionCallNode + FunctionNode 这一对底层节点透明处理。
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 StartedRust0629
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