three.js 节点系统(TSL)探秘:NodeFunctionInput 输入描述器与 GLSL/WGSL 函数解析机制
在 three.js 的节点材质/TSL(Three Shading Language)体系中,NodeFunction 描述一段“原生着色器函数”,而 NodeFunctionInput 则是该函数每个形参(输入参数)的元数据描述器——记录参数的类型、名字、数组长度以及 GLSL 特有的限定符信息。本篇以 docs/pages/NodeFunctionInput.html.md 文档为骨架,结合 src/nodes/core/NodeFunctionInput.js 及其解析器、调用端源码,深入讲解该类的构造参数、全部属性,以及它是如何被 GLSL/WGSL 解析器生成、又被 FunctionNode/FunctionCallNode 消费并最终拼装成真实着色器代码的。读完你将理解 glslFn/wgslFn 背后参数绑定机制的底层原理,并能准确排查自定义着色器函数在接入节点材质时遇到的类型与入参问题。
一、NodeFunctionInput 在节点体系中的定位
NodeFunctionInput 是 three.js 节点系统里的一个轻量数据类(不继承 Node),它本身不参与渲染计算,只在“函数解析 / 着色器代码生成”阶段作为数据结构传递。官方对其的一句话定义是:
Describes the input of a NodeFunction(描述一个 NodeFunction 的输入)。
它与相关组件的关系可以概括为:
| 组件 | 源码位置 | 角色 |
|---|---|---|
NodeFunction |
src/nodes/core/NodeFunction.js | 表示一段原生着色器函数的抽象基类,持有返回类型 type、inputs(即 NodeFunctionInput[])、函数名与精度限定符 |
NodeFunctionInput |
src/nodes/core/NodeFunctionInput.js | 描述 NodeFunction 的每一个输入参数 |
GLSLNodeFunction / WGSLNodeFunction |
src/nodes/parsers/GLSLNodeFunction.js / src/renderers/webgpu/nodes/WGSLNodeFunction.js | 两种原生语言解析器,解析着色器签名并产出 NodeFunctionInput[] |
NodeParser |
src/nodes/core/NodeParser.js | 解析器基类,parseFunction( source ) 返回一个 NodeFunction |
从 NodeFunction 的源码注释可以看到,这类模块“只在构建(building)过程中相关,用户层代码不会直接使用”,NodeFunctionInput 同样如此——普通用户通常经由 TSL 的 glslFn、wgslFn 间接与它打交道。
二、构造函数与参数详解
依据 NodeFunctionInput.js 的实现,构造函数签名与文档一致:
constructor( type, name, count = null, qualifier = '', isConst = false )
各参数说明如下:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
type |
string |
必填 | 输入参数的类型(见下文类型说明) |
name |
string |
必填 | 输入参数的名称,是后续函数调用时按名绑定参数的键 |
count |
?number |
null |
当参数是数组时,表示数组长度;非数组参数为 null |
qualifier |
'in' | 'out' | 'inout' |
'' |
参数限定符,仅 GLSL 相关 |
isConst |
boolean |
false |
参数是否带 const 限定符,仅 GLSL 相关 |
type:遵循 three.js 节点类型命名
这里的 type 并不是 GLSL/WGSL 原始拼写,而是 three.js 节点系统内部的类型字符串(与 NodeBuilder 认识的类型一致)。例如在 WGSLNodeFunction.js 的 wgslTypeLib 中定义了一整套映射:
- 标量:
f32 → float、i32 → int、u32 → uint、bool → bool; - 向量:
vec3<f32> → vec3、vec4<i32> → ivec4、bvec2/bvec3/bvec4等; - 矩阵:
mat2x2<f32> → mat2、mat3x3<f32> → mat3、mat4x4<f32> → mat4; - 资源类型:
sampler → sampler、texture_2d → texture、texture_cube → cubeTexture、texture_3d → texture3D、各类texture_storage_* → storageTexture等。
而 GLSL 本身命名与节点类型基本一致,因此在 GLSLNodeFunction.js 中直接取标识符作为 type 即可(如 float、vec3、sampler2D 视实际情况而定)。
qualifier / isConst:为什么“仅对 GLSL 相关”
GLSL 支持 in、out、inout 及 const 等参数限定符,用于声明参数的传递方向与只读性;而 WGSL 没有这套语法,取而代之的是 fn 参数默认按值传递、需要写回时使用 ptr 指针类型。所以源码中把这两个字段明确标注为“only relevant for GLSL”。默认值 '' 与 false 恰好对应“普通按值传入、不带任何限定符”的常见情形。
三、属性一览
该类实例化后暴露 5 个实例属性,全部来自构造参数:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
.type |
string |
— | 输入参数的类型 |
.name |
string |
— | 输入参数的名称 |
.count |
?number |
null |
数组输入的长度;非数组为 null |
.qualifier |
'in' | 'out' | 'inout' |
'' |
参数限定符(仅 GLSL 相关) |
.isConst |
boolean |
false |
是否带 const 限定符(仅 GLSL 相关) |
类定义末尾还有一个静态标记(NodeFunctionInput.js):
NodeFunctionInput.isNodeFunctionInput = true;
这一约定与 Node.isNode、NodeFunction.isNodeFunction 等一脉相承,用于在构建链路中做廉价、可靠的类型判别。
四、源码实现速览
NodeFunctionInput.js 全部实现不足 60 行,构造函数仅做字段赋值,逻辑非常干净:
constructor( type, name, count = null, qualifier = '', isConst = false ) {
this.type = type;
this.name = name;
this.count = count;
this.qualifier = qualifier;
this.isConst = isConst;
}
它没有方法、不参与计算,是典型的“描述性结构体”。也正因如此,理解它的关键在于弄清两个问题:它由谁创建、它被谁消费。
五、它从哪里来:GLSL / WGSL 解析器如何构造输入描述
NodeFunctionInput 实例是在解析原生着色器签名时被批量创建的。
5.1 GLSL 路径
在 WebGL 后端,GLSLNodeBuilder 会注入 new GLSLNodeParser(),最终调用 GLSLNodeFunction.js 中的 parse() 完成签名 token 化与解析。其解析循环(L38-L73)依次识别:
- 前置
const关键字 → 置isConst = true; in/out/inout限定符 → 记录qualifier,否则为空串;- 类型标识符 → 作为
type; - 可选的数字 token → 作为数组长度
count(例如float data[4]中的4),parseInt失败则为null; - 参数名 → 作为
name。
最后在 L71 组装:
inputs.push( new NodeFunctionInput( type, name, count, qualifier, isConst ) );
可以看出,GLSL 解析器是唯一会为 qualifier、isConst、count 填充非默认值的路径。
5.2 WGSL 路径
在 WebGPU 后端,WGSLNodeBuilder 注入 WGSLNodeParser,走 WGSLNodeFunction.js。它用正则从 fn name( ... ) -> type 中提取形参片段,逐项解析出名字与类型,做 WGSL→节点类型的查表映射后构造实例(L121):
inputs.push( new NodeFunctionInput( resolvedType, name ) );
注意此处只传 type 与 name,其余参数保持默认值——因为 WGSL 没有 const/in/out/inout 语法;对于指针类参数(ptr 开头)会被解析为类型 pointer,用于调用端生成取址符 &。
5.3 一个参数描述器对应一条真实形参
综上,无论哪种后端,最终一个 NodeFunctionInput 都忠实对应原始着色器函数中的一条形参。例如下面的 GLSL 函数:
vec3 brighten( const vec3 color, in float amount, out vec3 result, float weights[4] )
会被解析为 4 个 NodeFunctionInput:
| name | type | count | qualifier | isConst |
|---|---|---|---|---|
color |
vec3 |
null |
'' |
true |
amount |
float |
null |
'in' |
false |
result |
vec3 |
null |
'out' |
false |
weights |
float |
4 |
'' |
false |
六、它到哪里去:FunctionNode 与 FunctionCallNode 的参数绑定
解析产出的 NodeFunctionInput[] 会被上层的 FunctionNode.js 与 FunctionCallNode.js 消费,完成“TSL 参数 → 原生着色器调用”的转换。
6.1 惰性解析与输入列表暴露
FunctionNode.getNodeFunction( builder )(L98-L113)通过 builder.parser.parseFunction( this.code ) 解析当前代码并缓存在节点数据中;getInputs( builder )(L86-L90)直接返回 nodeFunction.inputs。解析器本身由 NodeBuilder 持有,从而保证代码生成时使用与当前渲染后端一致的语言解析规则。
6.2 按名与按位置传参
真正的参数绑定发生在 FunctionCallNode.generate()(L98-L172)中,它遍历 inputs(即 NodeFunctionInput[]):
- 对象形式调用(TSL 常用方式)时,用
parameters[ inputNode.name ]按参数名查找传入的节点,找不到会报TSL: Input 'xxx' not found in 'Fn()'.错误; - 数组形式调用时按位置匹配,个数不足会自动补
float(0),个数超限会截断并告警; - 每个实参通过
generateInput按inputNode.type构建,即node.build( builder, type ),把节点结果规整为函数形参声明的类型;当形参类型为pointer时则生成'&' + node.build( builder )(WGSL 引用传递)。
这正是 NodeFunctionInput.type 与 name 两个字段在实际着色器代码生成中最关键的用途——它决定了调用参数如何被强制转型、如何被命名寻址。
七、典型使用场景:以 glslFn 接入自定义函数
普通用户接触这套机制的最常见入口是 TSL 提供的 glslFn 与 wgslFn(定义见 FunctionNode.js)。例如:
import { glslFn, texture } from 'three/tsl';
// 原生 GLSL 函数:对颜色做亮度缩放
const scaleColor = glslFn( `
vec3 scaleColor( vec3 color, float amount ) {
return color * amount;
}
` );
// 使用:实参以对象形式按形参名绑定
material.colorNode = scaleColor( {
color: texture( mapNode ),
amount: 0.5
} );
内部流程即:glslFn 构建 FunctionNode → 构建期 GLSLNodeParser 把 vec3 color, float amount 解析成两个 NodeFunctionInput → 调用 scaleColor({...}) 时 FunctionCallNode 依据 inputNode.name(color/amount)把 TSL 节点实参逐一绑定、按 inputNode.type(vec3/float)格式化后拼出 scaleColor( tColor, tAmount ) 的调用字符串,并在着色器中登记函数声明。
八、注意事项与边界
NodeFunctionInput与NodeFunction一样属于“构建期内部结构”,除非你正在编写渲染器、节点解析器或深度定制 TSL,一般无需直接实例化它;它已通过 src/nodes/Nodes.js 统一导出,作为节点系统的公共 API 面供上层引用。- GLSL 数组形参需要带长度字面量,解析器才能正确填出
count;若长度是宏或表达式则无法识别(正则只匹配数字 token),count会退化为null。 - 对于
in/out/inout/const,GLSL 解析器只有在限定符出现在类型之前时才识别;书写时请保持const vec3 color、inout vec3 result这类标准顺序。 - 如果调用自定义函数时报
TSL: Input 'xxx' not found in 'Fn()'.,说明对象形式的实参键名与解析出的形参名不一致,可先核对 GLSL/WGSL 源码中的形参拼写。
九、延伸阅读
- 上游抽象基类:NodeFunction(函数返回类型与输入列表的容器) 与 src/nodes/core/NodeFunction.js
- 源码本体:src/nodes/core/NodeFunctionInput.js
- 两种解析器实现:src/nodes/parsers/GLSLNodeFunction.js、src/renderers/webgpu/nodes/WGSLNodeFunction.js
- 消费端代码生成:src/nodes/code/FunctionNode.js、src/nodes/code/FunctionCallNode.js
- 解析器在构建器中的注入位置:src/nodes/core/NodeBuilder.js、src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js、src/renderers/webgpu/nodes/WGSLNodeBuilder.js
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
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00