three.js UniformNode 深度解析:用 TSL `uniform()` 构建可动态更新的着色器参数
本文聚焦 three.js 节点材质体系(Node Material / TSL)中最基础也最常用的输入节点 —— UniformNode。它以 API 参考页 docs/pages/UniformNode.html 为核心骨架,结合仓库中的源码实现与真实示例,讲解 UniformNode 的构造、属性、分组机制、共享/哈希复用逻辑以及代码生成原理。读完本文,你将掌握用 uniform() 与 setName()/setGroup()/onUpdate() 编写可在运行时无缝更新颜色、位移、时间等动态参数的 TSL 着色器,并理解这些参数在 WebGL/WebGPU 后端中如何被组织成 uniform buffer。
UniformNode 在节点体系中的定位
在 three.js 的节点体系中,UniformNode 的继承链为:
EventDispatcher → Node → InputNode → UniformNode
它继承自 InputNode,因此天然具备输入节点的基础能力:持有 value 值、可选的 precision('low' | 'medium' | 'high',默认 null)以及基于值自动推导节点类型的能力。InputNode 中 generateNodeType() 在未显式指定 nodeType 时会调用 getValueType( this.value ) 根据值推断类型;UniformNode 在此基础上进一步实现了「uniform 声明、分组与共享」。
源码中其类定义为(src/nodes/core/UniformNode.js):
class UniformNode extends InputNode {
static get type() {
return 'UniformNode';
}
// ...
}
说明:uniform 是着色器运行期间由 CPU 侧每帧/每次渲染更新的“全局变量”,是 GPU 视角下真正的变量。用它来更新颜色、光照、变换等值,无需重新编译着色器程序。
构造函数:值 + 类型
new UniformNode( value, nodeType = null )
| 参数 | 类型 | 说明 |
|---|---|---|
value |
any |
节点承载的值。通常是一个 JS 原生值(boolean / number)或 three.js 对象(Vector、Matrix、Color、Texture 等) |
nodeType |
string |
节点类型。若未显式传入,节点会尝试从 value 中推导类型 |
构造函数内部(UniformNode.js#L27-L57)依次完成三个默认值的初始化:
constructor( value, nodeType = null ) {
super( value, nodeType );
this.isUniformNode = true; // 类型测试标志
this.name = ''; // 名称/标签,默认空字符串
this.groupNode = objectGroup; // 默认分组:按对象管理
}
其中 nodeType 的默认值并非字段中显式存储,而是当为 null 时延迟到构建期由 InputNode.generateNodeType() 调用 getValueType() 依据 value 实时推断,例如 new THREE.Color(0x0066ff) 会被推导为 color,new THREE.Vector3() 推导为 vec3,数字推导为 float。
uniform() 工厂函数:推荐的创建方式
日常 TSL 编程并不直接 new UniformNode,而是使用模块底部导出的 TSL 工厂函数(UniformNode.js#L241-L272):
export const uniform = ( value, type ) => {
const nodeType = getConstNodeType( type || value );
if ( nodeType === value ) {
// 当传入的是类型字符串而非带值的实体时,用该类型的零值初始化
value = getValueFromType( nodeType );
}
// 若传入的是其他节点,则取出其 value(并沿图遍历取出其中的 ConstNode 值)
if ( value && value.isNode === true ) { /* ...提取 value.value... */ }
return new UniformNode( value, nodeType );
};
uniform() 支持的值如下(摘自 docs/TSL.md):
uniform( boolean | number | Color | Vector2 | Vector3 | Vector4 | Matrix3 | Matrix4, type = null )
由于 getConstNodeType 的存在,uniform( 'float' )、uniform( 'int' )、uniform( 'uint' ) 这种“只给类型不给值”的写法是合法的——此时自动用 getValueFromType 填充零值。例如官方体积火示例中:
const steps = uniform( 'int' ).onRenderUpdate( ... );
仓库中大量示例都在使用这一工厂,例如 examples/webgpu_compute_particles.html:
const gravity = uniform( -0.00098 );
const bounce = uniform( 0.8 );
const size = uniform( 0.12 );
const clickPosition = uniform( new THREE.Vector3() ); // 运行时写入鼠标坐标
属性逐一说明
.groupNode : UniformGroupNode(默认 objectGroup)
uniform 所属的分组。默认情况下 uniform 按“对象”(object)维度管理;也可以显式归属到按“帧 / 渲染调用”共享更新的分组,相关机制见下一节。文档原文表述为:“By default, uniforms are managed per object but they might belong to a shared group which is updated per frame or render call.”
.name : string(默认 '')
uniform 的名称或标签。它会被用作最终着色器源码中的 uniform 标识符,便于在调试器 / Shader 代码 / 帧调试中辨认变量。类型测试标记 isUniformNode(只读,恒为 true)用于运行时区分节点种类。
Uniform 分组:控制作用域与更新频率
这是理解 UniformNode 的关键。每个 uniform 都归属一个 UniformGroupNode,three.js 依据该分组把若干 uniform 打包成一个 uniform buffer,并按分组声明的频率回传数据。分组类的定义在 UniformGroupNode.js,构造函数签名为:
constructor( name, shared = false, order = 1, updateType = null )
name:分组名;shared:是否共享。共享分组的 uniform buffer 在多个对象间复用;order:影响分组在 buffer 间的内部排序,共享分组通常取更小的order(0)以排在普通对象分组之前;updateType:分组对应的更新类型。
模块底部预置了三个全局分组对象(同文件 UniformGroupNode.js#L152-L168),它们正是 UniformNode.groupNode 的三种候选值:
| 预置对象 | 语义 | shared | order | updateType |
|---|---|---|---|---|
objectGroup |
每个对象一份 uniform buffer,随对象更新 | false |
1 |
OBJECT |
renderGroup |
共享 uniform buffer,每次渲染调用更新一次 | true |
0 |
RENDER |
frameGroup |
共享 uniform buffer,每帧只更新一次 | true |
0 |
FRAME |
这三个常量定义在 UniformGroupNode.js 底部:
export const frameGroup = /*@__PURE__*/ sharedUniformGroup( 'frame', 0, NodeUpdateType.FRAME );
export const renderGroup = /*@__PURE__*/ sharedUniformGroup( 'render', 0, NodeUpdateType.RENDER );
export const objectGroup = /*@__PURE__*/ uniformGroup( 'object', 1, NodeUpdateType.OBJECT );
还可以用导出的 uniformGroup( name, order, updateType ) 与 sharedUniformGroup( name, order = 0, updateType ) 创建自定义分组;分组还提供 update() 方法,内部把 needsUpdate 置为 true 以触发渲染流程中的更新。
渲染器如何消费 groupNode:在 src/renderers/common/Bindings.js#L165 中,绑定对象判断 binding.isNodeUniformsGroup === true || binding.isNodeUniformBuffer === true 且 binding.groupNode.shared === true 时走共享更新路径;而 src/renderers/common/nodes/NodeManager.js#L116-L135 依据 groupNode.updateType 决定分组是否需要在当前阶段刷新,并通过 groupNode.version 对比判断数据是否过期。从源码结构可以推断:objectGroup 的 uniform buffer 绑定在渲染对象上随对象切换上传,而 renderGroup / frameGroup 分别按 render 与 frame 节奏更新,极大减少重复上传。
一个真实的生产级用法来自 CSM 阴影模块 examples/jsm/csm/CSMShadowNode.js#L379-L382:
const cameraNear = reference( 'camera.near', 'float', this ).setGroup( renderGroup );
const cascades = reference( '_cascades', 'vec2', this ).setGroup( renderGroup ).setName( 'cascades' );
又如聚簇光源模块 examples/jsm/tsl/lighting/ClusteredLightsNode.js#L69-L72,把相机参数统一放进 renderGroup,保证一次渲染只上传一份:
this._cameraNear = uniform( 0 ).setName( 'clusteredCameraNear' ).setGroup( renderGroup );
this._cameraFar = uniform( 0 ).setName( 'clusteredCameraFar' ).setGroup( renderGroup );
核心方法逐个击破
.setName( name ) : UniformNode
设置 name 属性并返回 this,支持链式调用:
const separation = uniform( 15.0 ).setName( 'separation' );
examples/webgpu_compute_birds.html#L220-L227 中,一组计算着色器参数均以 setName() 命名以便辨识;examples/webgpu_tsl_editor.html#L136-L138 也给出了 uniform( new THREE.Color( 0x0066ff ) ).setName( 'myColor' ) 的写法,并注释 “.setName() is optional”。
.label( name ) : UniformNode(已废弃)
label() 是 setName() 的旧名称。当前实现会打印一条弃用警告并委托给 setName()(UniformNode.js#L80-L86):
label( name ) {
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() ); // @deprecated r179
return this.setName( name );
}
从源码注释可知其弃用始于 r179,新代码请一律使用 setName()。
.setGroup( group : UniformGroupNode ) : UniformNode 与 .getGroup() : UniformGroupNode
成对的分组读写方法,均返回节点本身以支持链式调用(getGroup() 无链式返回值):
const myUniform = uniform( 0 ).setGroup( frameGroup );
myUniform.getGroup(); // frameGroup
.getUniformHash( builder : NodeBuilder ) : string
默认实现返回 Node#getHash( builder ) 的结果。子类可覆写以改变哈希参与方式(例如与对象/参数关联的更细粒度哈希)。哈希是 uniform 自动去重/共享的键,见下一节。
哈希去重与跨材质自动共享
同一份 uniform 可以在多个材质、多个后处理链中复用而不产生重复声明,这是 UniformNode 的共享机制在起作用。
核心是 getSharedNode()(UniformNode.js#L133-L149):
getSharedNode( builder ) {
const hash = this.getUniformHash( builder );
let sharedNode = builder.getNodeFromHash( hash );
if ( sharedNode === undefined ) {
builder.setHashNode( this, hash ); // 第一个以该哈希注册的节点成为共享代表
sharedNode = this;
}
return sharedNode;
}
文档对它的描述是:“Uniform nodes with the same hash share a single uniform. This method returns the node the shared uniform refers to which is the first node registered for the hash.” 即:哈希相同的多个 UniformNode 在最终着色器中只生成一个 uniform,由第一个注册的节点代表;这也呼应 docs/TSL.md 中提到的自动优化 “Automatic reuse of uniforms and attributes”。
随后 generate() 通过 NodeBuilder.getUniformFromNode() 把共享节点登记为一个 NodeUniform:builder 按 shader stage 缓存到 nodeData.uniform,未命中时分配自增序号并命名 nodeUniform + index,避免同一 uniform 在多个阶段重复建绑定。
TSL 文档给出了跨材质共享同一份 uniform 的典型写法(docs/TSL.md#L217-L225):
const sharedColor = uniform( new THREE.Color() );
materialA.colorNode = sharedColor.div( 2 );
materialB.colorNode = sharedColor.mul( .5 );
materialC.colorNode = sharedColor.add( .5 );
运行时只需 sharedColor.value = newColor,三个材质将同步变化,且底层只维护一份 uniform 数据。
更新机制:onUpdate 与 TSL 快捷方法
UniformNode 覆写了输入节点的 onUpdate(callback, updateType)(UniformNode.js#L151-L167):先把回调 bind 到节点实例上,再调用父类实现;回调返回非 undefined 的值时自动写入 this.value:
onUpdate( callback, updateType ) {
callback = callback.bind( this );
return super.onUpdate( ( frame ) => {
const value = callback( frame, this );
if ( value !== undefined ) this.value = value;
}, updateType );
}
TSL 文档(docs/TSL.md#L457-L461)为常用更新频率提供了三个开箱即用的事件方法:
| 方法 | 触发时机 |
|---|---|
.onObjectUpdate( function ) |
每当携带该节点的 Material 渲染一个对象(如 Mesh)时更新 |
.onRenderUpdate( function ) |
每次渲染调用更新一次,适合常见与共享材质、雾、色调映射等 |
.onFrameUpdate( function ) |
每帧只更新一次,推荐用于每帧恒定值的场景(如时间 time) |
TSL 文档中的标准示例:
const posY = uniform( 0 ); // 也可以写 uniform( 'float' )
// 方式一:自动事件回调,{ object } 是当前被渲染的对象
posY.onObjectUpdate( ( { object } ) => object.position.y );
// 方式二:手动直接改 .value
posY.value = object.position.y;
material.colorNode = posY;
真实用例一(随时间推进的雾/时间 uniform,examples/webgpu_custom_fog.html#L116):
const time = uniform( 0 ).onFrameUpdate( ( frame ) => frame.time );
真实用例二(根据材质参数动态取值的 int uniform,examples/webgpu_volume_fire.html#L665):
const steps = uniform( 'int' ).onRenderUpdate(
( { material, object } ) => material.steps
|| ( object && object.material && object.material.steps )
|| volumetricMaterial.steps
);
进入着色器:generate() 与布尔特殊处理
UniformNode.generate()(UniformNode.js#L183-L226)负责把节点翻译成目标后端(WebGL/WebGPU/WGSL/GLSL)的代码片段,流程如下:
- 取得节点类型与共享节点,确定 uniform 在 builder 中登记的输入类型;
- 以
this.name || builder.context.nodeName作为 uniform 名调用getUniformFromNode(),随后清理builder.context.nodeName; - 若节点类型为
bool,额外缓存到一个局部bool变量中(getVarFromNode+addLineFlowCode),因为普通数值型 uniform buffer 无法直接承载真正的布尔类型。
与此配套的是 getInputType() 的重写(UniformNode.js#L169-L181):当推导出的输入类型为 bool 时统一映射为 uint:
getInputType( builder ) {
let type = super.getInputType( builder );
if ( type === 'bool' ) type = 'uint';
return type;
}
从后端实现推断,这是为了让布尔值能以 0/1 形式存入统一的 uniform buffer(UBO / storage buffer),避免平台相关的 bool 存储差异。整条链路可概括为:
uniform() → UniformNode
→ getSharedNode() (按 getUniformHash 去重,共享同一 uniform)
→ getInputType() (bool → uint)
→ builder.getUniformFromNode()(登记 NodeUniform 并按 shaderStage 缓存)
→ builder.getPropertyName() (解析成后端符号名)
→ generate() 返回格式化后的代码片段
综合实战:一段可运行的 TSL uniform 片段
把以上要点串起来,一个同时体现 setName、setGroup 与运行更新机制的完整形态如下(与官方示例中的用法保持一致):
import * as THREE from 'three';
import { uniform, vec3, time } from 'three/tsl';
import { uniformGroup } from 'three/tsl';
// 1) 基本 uniform:显式命名,便于调试着色器输出
const emissiveIntensity = uniform( 1.0 ).setName( 'emissiveIntensity' );
// 2) 需要跨材质共享的场景:只更新一个 .value 即可全局生效
const sharedTint = uniform( new THREE.Color( 0x0066ff ) );
// 3) 放入每帧更新的共享分组,避免每对象重复上传
const myFrameUniform = uniform( 0.0 ).setGroup( frameGroup ).onFrameUpdate( ( frame ) => frame.time );
// 4) 自定义分组:名字随意,updateType 决定刷新节奏
const myGroup = uniformGroup( 'MyGroup', 1, 'render' );
const sunDir = uniform( new THREE.Vector3( 1, 0, 0 ) ).setGroup( myGroup );
// 5) 手动更新依旧可行:uniform.value 在任何时候赋值都会被下一次上传拾取
emissiveIntensity.value = 2.5;
// 接入节点材质(示意)
material.emissiveNode = sharedTint.mul( emissiveIntensity );
material.colorNode = myFrameUniform.lessThan( 0.5 ).select( vec3( 1 ), vec3( 0 ) );
注意事项小结:
name只影响代码可读性与调试体验,不影响运行逻辑,setName可选;- 默认分组是
objectGroup(按对象管理);需要全局共享、避免每对象重复上传时改用renderGroup/frameGroup; - 旧 API
label()自 r179 起废弃,统一使用setName(); uniform( 'float' )/uniform( 'int' )/uniform( 'uint' )允许只传类型字符串,节点会自动补零值;- 布尔型 uniform 在生成阶段按
uint处理并缓存为局部变量,请勿依赖 GLSL/WGSL 对 bool 的存储布局。
延伸阅读
- 源码主文件:src/nodes/core/UniformNode.js(含
uniform()工厂、getSharedNode、generate等完整实现) - 父类:src/nodes/core/InputNode.js(
value、precision、序列化与类型推导) - 分组机制:src/nodes/core/UniformGroupNode.js(
objectGroup/renderGroup/frameGroup的定义) - Builder 侧 uniform 登记:src/nodes/core/NodeBuilder.js#L2084-L2106
- 官方 TSL 指南中的 uniform 章节:docs/TSL.md(包含共享、
on*Update事件表与更多示例) - 实际应用:
webgpu_compute_birds.html(setName批量命名)、webgpu_tsl_editor.html(uniform + 纹理节点交互)、webgpu_custom_fog.html(onFrameUpdate驱动时间)、examples/jsm/csm/CSMShadowNode.js(renderGroup+ 共享分组实践)
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