首页
/ Three.js 参数节点深度指南:理解 TSL 中的 ParameterNode 及其着色器参数机制

Three.js 参数节点深度指南:理解 TSL 中的 ParameterNode 及其着色器参数机制

2026-09-07 15:03:18作者:秋泉律Samson

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 偏"属性声明",用于声明并持有可变的着色器变量(引擎内部大量用它预置 DiffuseColorRoughnessMetalness 等常见材质属性,见 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.jsexport * from './core/ParameterNode.js' 被整体汇入 TSL 命名空间,并在 Three.TSL.js 中以 export const parameter = TSL.parameter; 显式导出,因此既可通过具名导入使用,也可通过 three/tsl 的统一命名空间访问。类本身也通过 Nodes.jsexport { 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 上的一系列成员,包括:

这些属性在编写自定义着色器属性/参数管理逻辑时非常有用。

四、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;

}

逐行拆解其语义:

  1. 确定参数自身的类型:先通过 getNodeType( builder ) 拿到参数的类型字符串;
  2. 在 builder 中查找已注册的结构体:调用 builder.getStructTypeNode( type )。该方法实现于 NodeBuilder.js,本质是在当前 shader stage(vertex/fragment/compute/any)的 types 表中按名字查找 StructType,找不到就返回 null
  3. 分发成员类型查询:若找到了对应结构体,就委托给结构体的 getMemberType( builder, name )。以 StructTypeNode 为例,其实现是线性查找 membersLayoutStructTypeNode.js):
getMemberType( builder, name ) {

	const member = this.membersLayout.find( m => m.name === name );

	return member ? member.type : 'void';

}
  1. 兜底与报错:如果该类型名没有对应的已注册结构体,会通过 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)需要以"流"的方式被编译时,构建器会为布局中的每一个输入创建一个 ParameterNodeNodeBuilder.js):

for ( const input of layout.inputs ) {

	inputs[ input.name ] = new ParameterNode( input.type, input.name );

}

随后这些输入节点被统一传给函数调用节点(shaderNode.call( inputs ))。这意味着:

  • TSL 函数体中,布局里声明的具名输入在内部就是以 ParameterNode 形式存在的参数,每个参数被绑定为 (type, name) 二元组;
  • 这也解释了构造函数为什么同时要求 nodeTypename:编译器需要类型用于图分析与类型推导,需要名字用于最终生成引用到正确变量的着色器代码;
  • 当布局中的某个输入是结构体类型时,函数体内部对该参数的成员访问就会走第四节的 getMemberType 解析路径。

另外值得注意的两处覆写也印证了"参数 = 按名字引用的外部符号"这一语义:

  • getHash() 返回 String( this.id )ParameterNode.js),确保每个参数节点实例都以自己唯一的节点 id 参与缓存键;
  • generate() 直接返回参数名 this.nameParameterNode.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 ) 形式预置材质相关变量(roughnessmetalnessdiffuseColoremissive 等,见 PropertyNode.js)。需要手动声明并 .assign() 初始值时可优先考虑它。
  • parameter( type, name ):面向"作为参数被引用"的实体——典型代表就是函数 layout 里的输入,或在自定义着色器逻辑中需要直接引用某个已在 shader 上下文中以该名字存在的参数变量。它只负责"引用"这个名字并携带类型信息,不负责声明。
  • 若想暴露一个可被 JS 侧通过 .value 动态更新的外部变量,则属于 uniform()(UniformNode)的职责范畴——ReferenceNode 内部即通过 uniform 暴露对象属性(见 ReferenceNode.jsuniform 引入),这与 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 的本质差异。

如果想继续深入,推荐阅读仓库中的以下材料:

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