Three.js 参数节点深度指南:理解 TSL 中的 ParameterNode 及其着色器参数机制
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
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。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考