首页
/ three.js UniformNode 深度解析:用 TSL `uniform()` 构建可动态更新的着色器参数

three.js UniformNode 深度解析:用 TSL `uniform()` 构建可动态更新的着色器参数

2026-09-08 11:19:07作者:昌雅子Ethen

本文聚焦 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)以及基于值自动推导节点类型的能力。InputNodegenerateNodeType() 在未显式指定 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) 会被推导为 colornew 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 === truebinding.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)的代码片段,流程如下:

  1. 取得节点类型与共享节点,确定 uniform 在 builder 中登记的输入类型;
  2. this.name || builder.context.nodeName 作为 uniform 名调用 getUniformFromNode(),随后清理 builder.context.nodeName
  3. 若节点类型为 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 片段

把以上要点串起来,一个同时体现 setNamesetGroup 与运行更新机制的完整形态如下(与官方示例中的用法保持一致):

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() 工厂、getSharedNodegenerate 等完整实现)
  • 父类:src/nodes/core/InputNode.jsvalueprecision、序列化与类型推导)
  • 分组机制:src/nodes/core/UniformGroupNode.jsobjectGroup / renderGroup / frameGroup 的定义)
  • Builder 侧 uniform 登记:src/nodes/core/NodeBuilder.js#L2084-L2106
  • 官方 TSL 指南中的 uniform 章节:docs/TSL.md(包含共享、on*Update 事件表与更多示例)
  • 实际应用:webgpu_compute_birds.htmlsetName 批量命名)、webgpu_tsl_editor.html(uniform + 纹理节点交互)、webgpu_custom_fog.htmlonFrameUpdate 驱动时间)、examples/jsm/csm/CSMShadowNode.jsrenderGroup + 共享分组实践)
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391