three.js TSL 深入解析:Object3DNode 如何在 Shader 节点中访问 3D 对象的变换数据
【免费下载链接】three.js JavaScript 3D Library. 项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本篇基于 three.js 仓库中的 API 文档 Object3DNode 与对应实现 src/nodes/accessors/Object3DNode.js,系统讲解 TSL(Three Shading Language)节点系统中的 Object3DNode:它如何把 Object3D 的位置、朝向、缩放与世界矩阵等变换数据封装为 shader uniform,不同 scope 对应何种 GPU 数据类型与更新逻辑,以及 objectPosition、objectWorldMatrix 等 TSL 便捷函数在示例项目中的真实用法。读完本文,你可以熟练地在 TSL 着色器代码中引用任意场景对象的变换信息,并理解其按对象(per-object)更新的底层机制。
Object3DNode 的定位:把场景对象“注入”着色器
Object3DNode 的继承链为 EventDispatcher → Node,它是 TSL 访问器(accessor)节点家族的一员,位于 src/nodes/accessors/Object3DNode.js。其设计目标是:访问 3D 对象与变换相关的度量值——按选定的 scope,将不同度量以 uniform 的形式表示在 shader 中。
文档定义的 scope 及其含义如下:
| Scope 常量 | 字符串值 | 表示的度量 |
|---|---|---|
POSITION | 'position' | 对象在世界空间(world space)中的位置 |
VIEW_POSITION | 'viewPosition' | 对象在视图/相机空间(view/camera space)中的位置 |
DIRECTION | 'direction' | 对象在世界空间中的朝向 |
SCALE | 'scale' | 对象在世界空间中的缩放 |
WORLD_MATRIX | 'worldMatrix' | 对象在世界空间中的矩阵 |
这些字符串常量在源码中以静态属性形式固定,见 Object3DNode.js#L201-L206:
Object3DNode.WORLD_MATRIX = 'worldMatrix';
Object3DNode.POSITION = 'position';
Object3DNode.SCALE = 'scale';
Object3DNode.VIEW_POSITION = 'viewPosition';
Object3DNode.DIRECTION = 'direction';
Object3DNode.RADIUS = 'radius';
值得注意的是,从源码结构看,当前实现中还多出一个文档页面未列出的 RADIUS('radius')scope,表示对象包围球在世界空间变换后的半径(float 类型),详见下文更新逻辑与 TSL 函数 objectRadius。
构造函数与核心属性
构造函数
new Object3DNode( scope, object3d = null )
- scope:节点按 scope 表示不同类型的变换,取值为
'position' | 'viewPosition' | 'direction' | 'scale' | 'worldMatrix'(源码中还支持'radius')。 - object3d:目标 3D 对象,默认值为
null。
对应源码见 Object3DNode.js#L37-L71。
属性一览
.object3d : Object3D — 目标 3D 对象,默认 null。
.scope — 节点按 scope 上报不同类型的变换。
.uniformNode : UniformNode — 持有节点的值,其类型为 UniformNode,是真正被生成到 shader 代码中的 uniform。
.updateType : string — 覆盖自 Node#updateType(Node#updateType),默认值为 'object'。这一点非常关键:它决定节点更新的时机。
为什么 updateType 必须是 'object'
NodeUpdateType 的完整定义位于 src/nodes/core/constants.js#L20-L25:
| 常量 | 值 | 含义 |
|---|---|---|
NONE | 'none' | 不执行 update 方法 |
FRAME | 'frame' | 每帧执行一次 |
RENDER | 'render' | 每次渲染调用执行(一帧可能由多次 render 产生) |
OBJECT | 'object' | 对使用节点进行渲染的每个 Object3D 分别执行 |
Object3DNode 的取值是对象自身的世界变换(矩阵、位置、朝向),这些量随场景图中对象数量变化,因此必须“按对象”刷新:同一节点被多个对象复用(例如实例化渲染、批量对象)时,update(frame) 会在每个对象的上下文中被调用,从而把正确的 uniform 值写入对应的渲染批次。这就是 Object3DNode.js#L62 中 this.updateType = NodeUpdateType.OBJECT 的由来。
Scope 到 GPU 数据类型的映射
generateNodeType()(见 Object3DNode.js#L78-L96)根据 scope 推断节点类型,generate(builder)(见 Object3DNode.js#L161-L181)会把同一映射写入 uniformNode.nodeType 并返回 this.uniformNode.build(builder),完成 shader 代码片段的生成:
| scope | 节点类型 |
|---|---|
worldMatrix | mat4 |
position / viewPosition / direction / scale | vec3 |
radius(源码扩展) | float |
这个映射保证了下游节点的类型安全:objectPosition(obj) 可以参与 vec3 运算,objectWorldMatrix(obj).mul(...) 按 mat4 运算,而 objectRadius(obj) 直接得到标量。
按 Scope 更新的底层逻辑:update(frame)
update(frame)(见 Object3DNode.js#L103-L152)按 scope 计算 uniform 值。逐分支解读:
worldMatrix:uniformNode.value = object.matrixWorld,直接引用对象的matrixWorld,无拷贝开销。position:懒初始化一个Vector3(uniformNode.value || new Vector3()),然后setFromMatrixPosition(object.matrixWorld),即取世界矩阵的平移分量。scale:同样懒初始化Vector3,setFromMatrixScale(object.matrixWorld),从矩阵的三列向量长度还原世界缩放。direction:调用object.getWorldDirection(uniformNode.value),得到对象“正前方”(-Z 轴方向)在世界空间的单位向量。viewPosition:先取世界坐标setFromMatrixPosition(object.matrixWorld),再applyMatrix4(camera.matrixWorldInverse)——这里用到frame.camera,把世界位置变换到当前相机的视图空间。radius(源码扩展):取frame.object.geometry的包围球(若boundingSphere === null则先computeBoundingSphere()),用模块级缓存对象_sphere复制后applyMatrix4(object.matrixWorld),最终uniformNode.value = _sphere.radius。从源码结构看,该分支复用了全局唯一的临时Sphere实例以避免每帧分配。
可以看出,update() 的输入只有 frame(提供相机与当前渲染对象)和节点持有的 object3d,所有向量对象都采用“首次创建、后续复用”的模式,避免逐帧 GC 压力——这与 TSL 节点系统面向每帧高频调用的设计一致。
TSL 便捷函数:objectPosition、objectWorldMatrix 等
Object3DNode.js 文件末尾(L210-L268)为每个 scope 导出了一组 TSL 函数:
export const objectDirection = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.DIRECTION ).setParameterLength( 1 );
export const objectWorldMatrix = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.WORLD_MATRIX ).setParameterLength( 1 );
export const objectPosition = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.POSITION ).setParameterLength( 1 );
export const objectScale = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.SCALE ).setParameterLength( 1 );
export const objectViewPosition = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.VIEW_POSITION ).setParameterLength( 1 );
export const objectRadius = /*@__PURE__*/ nodeProxy( Object3DNode, Object3DNode.RADIUS ).setParameterLength( 1 );
其机制是 src/nodes/tsl/TSLCore.js#L978 中的 nodeProxy(NodeClass, scope, ...):创建一个 ShaderNodeProxy,调用时自动 new Object3DNode(scope, ...args) 并继承完整节点 API。setParameterLength(1) 将参数上限设为 1,即只接受一个可选的 object3d 参数。返回类型也由 JSDoc 标注清楚:Object3DNode<vec3>(position/viewPosition/direction/scale)、Object3DNode<mat4>(worldMatrix)、Object3DNode<float>(radius)。
因此日常编写 TSL 代码时无需手动构造节点,直接写:
import { objectPosition, objectWorldMatrix } from 'three/tsl';
const myPos = objectPosition( someObject ); // vec3,世界空间位置
const myMat = objectWorldMatrix( someObject ); // mat4,世界矩阵
仓库中的真实用法
1. 后处理中按相机位置生成水面遮罩 — examples/webgpu_backdrop_water.html#L241:
import { objectPosition, screenUV, pass, ... } from 'three/tsl';
// 以相机的世界坐标高度为界,判断屏幕 UV 是否处于"水面以下"
const waterMask = objectPosition( camera ).y
.greaterThan( screenUV.y.sub( .5 ).mul( camera.near ) );
这里 objectPosition(camera) 把相机自身当作 Object3D 使用,取世界空间 Y 坐标参与屏幕空间的比较,是 vec3 结果链式参与 TSL 运算的典型写法。
2. Compute 着色器中做世界空间指针距离判定 — examples/webgpu_compute_geometry.html#L106:
import { objectWorldMatrix, ... } from 'three/tsl';
// 把本地坐标乘以世界矩阵,得到世界空间位置后与指针比较
const worldPosition = objectWorldMatrix( object ).mul( currentPosition );
const dist = worldPosition.distance( pointerPosition.xyz );
const direction = pointerPosition.xyz.sub( worldPosition ).normalize();
此例中 mat4 类型的 objectWorldMatrix(object) 与向量相乘(.mul)完成本地到世界空间的变换,随后链式做距离与方向计算,正好体现了 scope 决定类型、类型决定可用运算的设计闭环。
序列化与其他要点
Object3DNode 重写了 serialize/deserialize(Object3DNode.js#L183-L197),在基类数据之外只额外持久化 scope 字段——object3d 与 uniformNode 属于运行时状态,不进入序列化数据。这意味着节点图(例如配合编辑器/检视器)保存与恢复时,scope 信息不会丢失。
使用前提与限制总结:
- 运行于 TSL / WebGPU 节点管线(
three/tsl、three/webgpu入口),updateType: 'object'的更新调度由节点系统在渲染阶段驱动,开发者一般不需要手动调用update; - scope 必须在构造时确定(
scope无 getter 级联约束,但类型映射在generate/generateNodeType中按固定分支处理); VIEW_POSITION依赖frame.camera,RADIUS依赖frame.object.geometry的包围球,二者取值与渲染时的上下文对象强相关;- 相关实现与文档可进一步参考:src/nodes/accessors/Object3DNode.js、src/nodes/core/constants.js、src/nodes/tsl/TSLCore.js、API 文档 Object3DNode 与基类 Node。
【免费下载链接】three.js JavaScript 3D Library. 项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



