Three.js 参数节点深度指南:理解 TSL 中的 ParameterNode 及其着色器参数机制
Three.js 的节点式着色器系统(TSL,Three Shading Language)中,ParameterNode 是链接 TSL 抽象语法图与真实着色器代码之间的关键节点之一。本文以仓库文档 docs/pages/ParameterNode.html.md 为骨架,结合 src/nodes/core/ParameterNode.js 等源码,系统讲解 ParameterNode 的构造方式、类型标记、结构体成员解析机制、在 NodeBuilder 内部的自动装配流程,以及它与 PropertyNode、结构体类型等 TSL 核心概念的协作关系。
读完本文,你将掌握:ParameterNode 与普通 PropertyNode 的区别、如何在自定义 TSL 代码中通过 parameter() 工厂函数引用着色器参数、结构体类型参数的成员类型是如何被解析的,以及它在底层构建管线中的真实角色。
一、ParameterNode 是什么:从类继承链说起
三份文档与源码给出了完全一致的继承链:
EventDispatcher → Node → PropertyNode → ParameterNode
对应文档原文第一行 "Inheritance: EventDispatcher → Node → PropertyNode →"。在源码中:
- PropertyNode.js 定义了着色器属性(property),可"显式声明一个属性并为其赋值",如
property( 'float', 'threshold' ).assign( THRESHOLD ); - ParameterNode.js 继承
PropertyNode,源码注释与文档一致地描述其为 "Special version of PropertyNode which is used for parameters"。
从源码结构看,二者的分工可以这样理解:PropertyNode 偏"属性声明",用于声明并持有可变的着色器变量(引擎内部大量用它预置 DiffuseColor、Roughness、Metalness 等常见材质属性,见 PropertyNode.js);而 ParameterNode 则偏"参数引用",它的 generate() 实现直接返回参数名本身(return this.name,见 ParameterNode.js),说明它代表的是"一个由外部上下文(如函数签名、layout 布局)声明的参数",而非由它自己产出声明语句。
二、构造函数与 TSL 工厂函数 parameter()
文档给出了构造函数签名:
new ParameterNode( nodeType : string, name : string )
源码实现与其一致,并补充了 name 的默认值与类型:
constructor( nodeType, name = null ) {
super( nodeType, name );
this.isParameterNode = true;
}
两个参数的含义如下:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
nodeType |
string |
必填 | 节点的类型(如 'float'、'vec3'、'vec4',也可以是结构体类型名) |
name |
string(可空) |
null |
参数在着色器中的名字;不指定时由节点系统自动生成 |
关键点:参数的顺序是 (nodeType, name),类型在前、名字在后。这一点对使用下面的 TSL 工厂函数至关重要。
ParameterNode.js 文件末尾还导出了一个 TSL 级别的工厂函数,这是普通用户接触 ParameterNode 最直接的入口(ParameterNode.js):
export const parameter = ( type, name ) => new ParameterNode( type, name );
也就是说,在 TSL 代码中你可以这样创建一个参数节点:
import { parameter } from 'three/tsl';
// 引用一个名为 myParam 的 float 着色器参数
const myParam = parameter( 'float', 'myParam' );
// 引用结构体类型的参数,例如后续要对其成员做类型解析
const lightData = parameter( 'LightData', 'light' );
该 TSL 函数经由 TSL.js 的 export * from './core/ParameterNode.js' 被整体汇入 TSL 命名空间,并在 Three.TSL.js 中以 export const parameter = TSL.parameter; 显式导出,因此既可通过具名导入使用,也可通过 three/tsl 的统一命名空间访问。类本身也通过 Nodes.js 的 export { default as ParameterNode } from './core/ParameterNode.js'; 在节点集合中注册。
三、只读类型标记 isParameterNode
文档列出的唯一 Property 是:
.isParameterNode : boolean (readonly)
源码 ParameterNode.js 中该标记在构造时被硬编码为 true。它的用途和 TSL 中其他 isXxxNode 标记一致——运行时类型检测:
if ( node.isParameterNode === true ) {
// 该节点是参数节点,可安全地按 ParameterNode 处理
}
作为对照,基类 PropertyNode 同样提供 isPropertyNode 标记(PropertyNode.js),Node 基类及各具体节点类也各自维护类似标记。这种"鸭子类型"标记体系贯穿整个 TSL 节点系统,避免了大型继承树下的 instanceof 依赖。
由于 ParameterNode 继承自 PropertyNode,它还自动继承了 PropertyNode 上的一系列成员,包括:
.name:属性/参数在着色器中的名字(PropertyNode.js);.varying:是否为 varying(默认false,PropertyNode.js);.placeholderNode:未赋值时的占位节点(PropertyNode.js);.global:是否参与全局缓存,默认true(PropertyNode.js)。
这些属性在编写自定义着色器属性/参数管理逻辑时非常有用。
四、getMemberType:解析结构体类型参数的成员类型
文档列出的唯一方法为:
.getMemberType( builder : NodeBuilder, name : string ) : string
它在文档中被标注为对 PropertyNode#getMemberType 的覆写(从源码结构看,最底层的默认实现位于 Node.js,它不做任何解析,直接返回 'void')。而 ParameterNode 的覆写逻辑要具体得多(ParameterNode.js):
getMemberType( builder, name ) {
const type = this.getNodeType( builder );
const struct = builder.getStructTypeNode( type );
let memberType;
if ( struct !== null ) {
memberType = struct.getMemberType( builder, name );
} else {
error( `TSL: Member "${ name }" not found in struct "${ type }".`, new StackTrace() );
memberType = 'float';
}
return memberType;
}
逐行拆解其语义:
- 确定参数自身的类型:先通过
getNodeType( builder )拿到参数的类型字符串; - 在 builder 中查找已注册的结构体:调用
builder.getStructTypeNode( type )。该方法实现于 NodeBuilder.js,本质是在当前 shader stage(vertex/fragment/compute/any)的types表中按名字查找StructType,找不到就返回null; - 分发成员类型查询:若找到了对应结构体,就委托给结构体的
getMemberType( builder, name )。以StructTypeNode为例,其实现是线性查找membersLayout(StructTypeNode.js):
getMemberType( builder, name ) {
const member = this.membersLayout.find( m => m.name === name );
return member ? member.type : 'void';
}
- 兜底与报错:如果该类型名没有对应的已注册结构体,会通过
error()抛出带调用栈(StackTrace)的错误信息,并返回兜底类型'float'。
该方法在什么场景被真正调用?
从节点间的调用关系可以还原出它的用途:当 TSL 表达式对某个对象做成员访问(例如 param.position 这种 .property 形态)时,真正负责生成代码的是 MemberNode.js。它会先调用宿主对象的 hasMember / getMemberType 来判定成员是否存在并推导成员类型(MemberNode.js),随后才拼出 属性名 + '.' + 成员名 的着色器代码(MemberNode.js)。
因此,当一个结构体类型的参数(例如按名字引用的 LightData)参与成员访问时,ParameterNode.getMemberType 就负责回答"这个参数的某成员是什么类型"。这保证了即便参数的实体(结构体变量)由外部声明,TSL 依然能对该参数的成员进行类型正确的图分析、缓存与代码生成。
五、源码级洞察:NodeBuilder 如何用 ParameterNode 装配函数参数
ParameterNode 并不只是留给用户手动调用的抽象类,它在构建管线内部有一个确定性的使用点——NodeBuilder.flowShaderNode()。当某个带 layout 的 TSL 函数(Fn 声明、经由 ShaderNode.setLayout 记录输入的类型与名字,见 TSLCore.js)需要以"流"的方式被编译时,构建器会为布局中的每一个输入创建一个 ParameterNode(NodeBuilder.js):
for ( const input of layout.inputs ) {
inputs[ input.name ] = new ParameterNode( input.type, input.name );
}
随后这些输入节点被统一传给函数调用节点(shaderNode.call( inputs ))。这意味着:
- TSL 函数体中,布局里声明的具名输入在内部就是以
ParameterNode形式存在的参数,每个参数被绑定为(type, name)二元组; - 这也解释了构造函数为什么同时要求
nodeType与name:编译器需要类型用于图分析与类型推导,需要名字用于最终生成引用到正确变量的着色器代码; - 当布局中的某个输入是结构体类型时,函数体内部对该参数的成员访问就会走第四节的
getMemberType解析路径。
另外值得注意的两处覆写也印证了"参数 = 按名字引用的外部符号"这一语义:
getHash()返回String( this.id )(ParameterNode.js),确保每个参数节点实例都以自己唯一的节点 id 参与缓存键;generate()直接返回参数名this.name(ParameterNode.js),不产出任何声明、赋值或初始化代码,与PropertyNode.generate()中通过builder.getVarFromNode()生成变量声明的行为形成鲜明对比。
六、实战视角:parameter() 与 property() / uniform() 如何取舍
在 TSL 文档的"Variables"一节中,property( type, name = null ) 被描述为"声明一个属性但不赋初始值"(docs/TSL.md)。那么什么时候用 property(),什么时候用 parameter()?
从类注释与源码语义可以总结出如下判断口径:
property( type, name ):面向"由节点图自己声明、持有、可被赋值"的着色器变量。引擎内部大量以nodeImmutable( PropertyNode, type, name )形式预置材质相关变量(roughness、metalness、diffuseColor、emissive等,见 PropertyNode.js)。需要手动声明并.assign()初始值时可优先考虑它。parameter( type, name ):面向"作为参数被引用"的实体——典型代表就是函数 layout 里的输入,或在自定义着色器逻辑中需要直接引用某个已在 shader 上下文中以该名字存在的参数变量。它只负责"引用"这个名字并携带类型信息,不负责声明。- 若想暴露一个可被 JS 侧通过
.value动态更新的外部变量,则属于uniform()(UniformNode)的职责范畴——ReferenceNode内部即通过uniform暴露对象属性(见 ReferenceNode.js 的uniform引入),这与ParameterNode"纯参数引用"的定位是不同的。
一个直观的使用示意(在 TSL 材质着色器逻辑中绑定某个已有着色器参数并参与运算):
import { material, vec4, parameter, color } from 'three/tsl';
// 引用外部名为 baseColor 的 vec3 着色器参数
const baseColorParam = parameter( 'vec3', 'baseColor' );
material.colorNode = vec4( baseColorParam.mul( color( '#3f51b5' ) ), 1 );
七、总结与进一步阅读
ParameterNode 是 TSL 体系里体积小但定位精确的节点:
- 定位:
PropertyNode的"参数专用"子类,源码注释与 API 文档表述完全一致; - 构造:
new ParameterNode( nodeType, name = null ),或使用等价的 TSL 工厂parameter( type, name )(注意参数顺序); - 类型检测:只读标记
isParameterNode === true; - 成员解析:覆写
getMemberType(),把结构体参数的成员类型查询委托给builder.getStructTypeNode()返回的StructType,找不到注册结构体时报TSL: Member "..." not found in struct "..."错误并兜底返回'float'; - 底层角色:
NodeBuilder.flowShaderNode()将 TSL 函数 layout 的每个输入实例化为ParameterNode,函数体的参数按名字被引用、按 id 参与缓存,这是它与"自声明属性"型PropertyNode的本质差异。
如果想继续深入,推荐阅读仓库中的以下材料:
- 类的直接文档与实现:ParameterNode.html.md、src/nodes/core/ParameterNode.js;
- 父类属性声明与内置属性一览:PropertyNode.html.md、PropertyNode.js;
- 成员访问如何依赖类型解析:MemberNode.js、StructTypeNode.js;
- 构建器对结构体类型的注册与查找:NodeBuilder.js;
- TSL 语言层面的变量、函数与布局体系总览:docs/TSL.md。
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 StartedRust0627
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