three.js TSL 深入解析:Object3DNode 如何在 Shader 节点中访问 3D 对象的变换数据

three.js TSL 深入解析:Object3DNode 如何在 Shader 节点中访问 3D 对象的变换数据

【免费下载链接】three.js JavaScript 3D Library. 【免费下载链接】three.js 项目地址: 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节点类型
worldMatrixmat4
position / viewPosition / direction / scalevec3
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. 【免费下载链接】three.js 项目地址: https://gitcode.com/GitHub_Trending/th/three.js

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

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值