☰
three.js TSL 节点系统中的 UniformGroupNode:用 uniform 组管理着色器缓冲数据
2026/10/20 15:26:51 网站建设 项目流程

three.js TSL 节点系统中的 UniformGroupNode:用 uniform 组管理着色器缓冲数据

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

导读

UniformGroupNode 是 three.js 节点材质(Node Material / TSL)体系中负责"把多个 UniformNode 打包成一个 uniform buffer 分组"的核心节点类型。本文基于 docs/pages/UniformGroupNode.html.md 与仓库源码,完整讲解其构造函数、属性、方法、内置工厂函数与objectGroup/renderGroup/frameGroup三个预定义组的语义,并深入渲染器底层说明这些分组如何决定 uniform 的更新时机与更新粒度。读完本文,你将理解三类"共享 uniform 缓冲"各自适用的场景,并能在自己的 TSL 材质中正确使用setGroup()、uniformGroup()与sharedUniformGroup()编排着色器输入数据。

UniformGroupNode 在 TSL 体系中的定位

在 three.js 的节点材质中,UniformNode表示一个可被着色器读取的 uniform 值(标量、向量、矩阵、颜色或纹理等),其实现位于 src/nodes/core/UniformNode.js。当场景中有大量 uniform 时,逐对象逐个上传既不高效也不利于 GPU 缓冲管理,因此引擎提供了UniformGroupNode这样的"分组节点":将多个单例 UniformNode 归入同一组,并在底层以一组 uniform buffer 的形式统一管理。

该类直接继承自Node,完整继承链为EventDispatcher → Node → UniformGroupNode(参考 src/nodes/core/UniformGroupNode.js),因此它具备节点序列化、类型测试、缓存版本等全部 Node 基础能力。在 TSL 模块中它被导出为uniformGroup、sharedUniformGroup两个工厂函数以及三个全局预定义组,供外部直接导入使用。

UniformGroupNode与UniformNode通过后者的groupNode属性关联:每个UniformNode默认归属于objectGroup,也可以通过.setGroup( group )改挂到其他分组(见 UniformNode.js 与 setGroup 实现)。

三个预定义组:objectGroup / renderGroup / frameGroup

绝大多数情况下,不需要手动 new 一个 UniformGroupNode,直接使用引擎预定义的三组对象即可。原文档用一句话概括了三者的本质区别,下面结合源码逐一展开:

预定义组语义初始化调用(源码)更新类型
objectGroup每个被渲染对象各持有一份 uniform bufferuniformGroup( 'object', 1, NodeUpdateType.OBJECT )OBJECT
renderGroup共享 uniform buffer,每次 render 调用更新一次sharedUniformGroup( 'render', 0, NodeUpdateType.RENDER )RENDER
frameGroup共享 uniform buffer,每帧更新一次sharedUniformGroup( 'frame', 0, NodeUpdateType.FRAME )FRAME

这三个对象在 src/nodes/core/UniformGroupNode.js 中被定义为模块级单例(/*@__PURE__*/),并在 src/Three.TSL.js 中被重新导出到three/tsl命名空间。注意frameGroup和renderGroup是shared = true的组,而objectGroup是shared = false的普通组。

由于一个frame内部可能发起多次render()(例如多视口、阴影 pass、后处理等),RENDER 相比 FRAME 拥有更细的更新粒度,NodeUpdateType常量在 src/nodes/core/constants.js 中有明确注释:"RENDER 的更新发生在每次渲染调用;一帧可能由多次渲染调用产生,因此 RENDER 允许比 FRAME 更精细的更新"。实践上:

  • 只与摄像机相关、同一次渲染内不变的矩阵与视口信息放在renderGroup;
  • 按时间流逝、需要每帧刷新的数据放在frameGroup;
  • 每个对象私有、对象间互不共享的数据保留在默认的objectGroup。

构造函数与参数详解

new UniformGroupNode( name : string, shared : boolean, order : number, updateType : string | null )

对应实现见 UniformGroupNode.js#L33-L79,构造时以'string'为节点数据类型调用super(),随后依次写入四个成员。

参数类型默认值说明
namestring无(必填)uniform group 的名字,会出现在生成的着色器标识符中(例如 WebGPU 后端用组名.成员名定位 uniform)。
sharedbooleanfalse该分组是否共享。true表示组内数据由多个渲染对象共享,只在需要时重新上传。
ordernumber1组间排序键,数值越小越靠前,影响 uniform 组在缓冲中的排列顺序。共享组通常使用更小的 order,从而排在普通逐对象组之前。
updateTypestring | nullnull组的更新类型,取值为 NodeUpdateType 中的'none'/'frame'/'render'/'object',默认null。

从源码注释(UniformGroupNode.js#L52-L60)可确认order的排序语义:order 较低的组会优先排列,共享组通常用较小的 order 以排在普通逐对象组之前。这一点也反映在三个预定义组的取值上——两个共享组 order 均为0,而普通组objectGroup的 order 为1。

属性速查表

属性类型默认值说明
.isUniformGroupboolean(只读)true类型测试标志,用于快速判断一个节点是否为 uniform 分组。
.namestring—分组名称(覆盖Node#name),构造时必填。
.ordernumber1影响内部排序。按源码语义,order 越小排位越靠前;共享组建议取小值。
.sharedbooleanfalse是否为共享分组,直接决定该组是否进入渲染器"仅更新共享缓冲"的路径。
.updateTypestring | nullnull更新类型(覆盖Node#updateType),取值见上述 NodeUpdateType 常量。

其中.shared是决定性能特征的关键开关:渲染器在 src/renderers/common/Bindings.js#L165 中仅遍历binding.groupNode.shared === true的NodeUniformsGroup/NodeUniformBuffer,通过updateSharedForRender()在渲染对象之间共享同一份缓冲更新。非共享(shared = false)的组则走逐对象绑定路径,每个使用该材质的 Object3D 都有独立副本——这正是objectGroup的模式。

方法详解

.update()

update()

将组标记为需要更新:把this.needsUpdate = true(见 UniformGroupNode.js#L85-L89),覆盖了Node#update。它会在渲染流程中触发与该组相关的必要刷新,配合 Node 基类的needsUpdate/version缓存机制(src/nodes/core/Node.js)工作。

.serialize( data : Object )

将节点序列化为 JSON 对象,覆盖Node#serialize。实现(UniformGroupNode.js#L96-L104)在调用基类序列化的基础上额外写入三个字段:

data.name = this.name; data.version = this.version; data.shared = this.shared;

.deserialize( data : Object )

从 JSON 对象反序列化,覆盖Node#deserialize。对应实现(UniformGroupNode.js#L111-L119)将data.name、data.version、data.shared依次回填到实例。

注意序列化/反序列化往返并不显式保存order与updateType,这两者依赖构造或工厂函数的默认约定(自定义组建议通过工厂函数明确指定 order)。

工厂函数:创建自己的 uniform group

除三个预定义组外,TSL 还暴露了两个创建自定义分组的工厂函数(UniformGroupNode.js#L133-L144):

// 非共享分组:name 必填,order 默认 1,updateType 默认 null export const uniformGroup = ( name, order = 1, updateType = null ) => new UniformGroupNode( name, false, order, updateType ); // 共享分组:name 必填,order 默认 0,updateType 默认 null export const sharedUniformGroup = ( name, order = 0, updateType = null ) => new UniformGroupNode( name, true, order, updateType );

工厂函数的默认值与原文档中构造参数默认值保持一致:普通分组order默认为1,共享分组默认order = 0,这一点印证了"共享组排在普通组之前"的设计。

需要创建自定义组时,典型做法是从three/tsl导入工厂并配合UniformNode#setGroup()使用,例如定义一个按渲染调用刷新的时间 uniform:

import { uniformGroup, uniform, renderer, object } from 'three/tsl'; // 自定义非共享分组(注意 import 来源为 'three/tsl') const myGroup = uniformGroup( 'myGroup', 1 ); // 自定义共享分组:渲染级更新 + 每渲染调用刷新回调 const myTime = uniform( 0 ).setName( 'myTime' ) .setGroup( sharedUniformGroup( 'time', 0 ) ) .onRenderUpdate( ( { camera } ) => performance.now() / 1000 );

如何在真实材质代码中生效

将 UniformNode 挂到某个组后,节点构建器会依据组的 name 决定其在着色器中的标识与缓冲归属:

  • WebGPU / WGSL 后端中 uniform 以组名.成员名的形式定位,相关拼接逻辑位于 src/renderers/webgpu/nodes/WGSLNodeBuilder.js#L1192,例如默认renderGroup(name 为render)下的摄像机矩阵会呈现为render.cameraViewMatrix之类的名称;
  • WebGL fallback 后端的 GLSL 生成同样读取uniform.groupNode.name作为分组前缀(见 src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js#L947)。

仓库中大量 TSL 内置节点都以这种"uniform + setGroup"的方式组织输入数据。最具代表性的是摄像机矩阵模块 src/nodes/accessors/Camera.js,其中视图矩阵、投影矩阵等全部显式挂到renderGroup并通过onRenderUpdate在每渲染调用时回填最新值:

// src/nodes/accessors/Camera.js(结构示意) const _cameraViewMatrixBase = uniform( camera.matrixWorldInverse ) .setName( 'cameraViewMatrix' ) .setGroup( renderGroup ) .onRenderUpdate( ( { camera } ) => camera.matrixWorldInverse );

同理,灯光系统(如 src/nodes/lighting/AnalyticLightNode.js、src/nodes/lighting/PointLightNode.js)、阴影参数(src/nodes/lighting/ShadowNode.js)、裁剪平面(src/nodes/accessors/ClippingNode.js)以及场景背景属性(src/nodes/accessors/SceneProperties.js)都通过.setGroup( renderGroup )归入"渲染级共享缓冲"。而在 examples/jsm/tsl/lighting/data/AmbientLightDataNode.js、examples/jsm/tsl/lighting/data/DirectionalLightDataNode.js 等例子中,聚簇光照(Clustered Lights)数据甚至用uniformArray(...).setGroup( renderGroup )一次提交整批灯光数组。想深入了解 uniform 与组的关系,可对照 UniformNode 参考页 的groupNode一节(该属性默认指向objectGroup,见 UniformNode.js#L55)。

底层:分组如何驱动更新时机

将 uniform 归组之后,three.js 的通用渲染管线(Common Renderer)通过两组机制把"分组"翻译成实际的缓冲刷新策略:

1. 共享组的按需更新(updateSharedForRender)在 src/renderers/common/Bindings.js#L157-L187 中,每次渲染前只对shared === true的绑定组调用NodeManager.updateGroup()做更新判定,命中后才执行binding.update()与backend.updateBinding(),从而避免对共享缓冲做无意义的重复上传。

2. 版本号驱动的更新判定(NodeManager.updateGroup)src/renderers/common/nodes/NodeManager.js#L114-L143 展示了核心判定逻辑:

  • 若groupNode.updateType === NodeUpdateType.OBJECT,说明是逐对象组,每次都直接返回需要更新(return true),不额外判断;
  • 其余类型(FRAME / RENDER / NONE)则把组与绑定缓存进ChainMap,只有当groupNode.version相对上次记录发生变化时才返回true并刷新版本。

也就是说,updateType = 'object'的组无条件更新,而'frame'/'render'的共享组借助 Node 的version计数器做去重,组数据未变时同一帧多次渲染不会重复上传。此外 src/renderers/common/nodes/NodeBuilderState.js#L133 有一项一致性约束:同一绑定组内的所有绑定必须共享同一个groupNode,混用分组会导致状态缓存错乱,自定义分组时应保证组与成员一一对应。

使用建议与局限

  • 优先使用预定义组:摄像机、场景、灯光等全局数据的语义与renderGroup天然匹配,自己 new 分组前先确认是否真的需要独立生命周期。
  • 善用order让共享组在前:若同时使用共享与非共享组,参照源码约定让共享组取更小的order(如0),普通组用默认1,可以保持缓冲布局的可预期性。
  • 更新频率决定 updateType:需要比"每渲染一次"更频繁的数据用 FRAME;相同帧内多次渲染共享且每次都要最新值的数据用 RENDER;对象级私有数据保持 OBJECT(这也是UniformNode的默认行为)。
  • 序列化边界:serialize/deserialize只往返name、version、shared三个字段(见 UniformGroupNode.js#L96-L119),order与updateType需要依赖工厂默认值或在派生类中另行处理。

如需继续深入,可在仓库中按以下路径追踪实现细节:UniformGroupNode 源码、UniformNode 源码、Node 基类、更新类型常量、共享绑定刷新逻辑、分组更新判定,以及大量.setGroup( renderGroup )的真实调用案例(从 src/nodes/accessors/Camera.js 与 examples/jsm/tsl/lighting/data/AmbientLightDataNode.js 入手最为直观)。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询