首页
/ three.js TSL 中 ArrayElementNode 深度解析:数组元素访问节点的实现原理与子类体系

three.js TSL 中 ArrayElementNode 深度解析:数组元素访问节点的实现原理与子类体系

2026-09-04 12:46:26作者:何将鹤

ArrayElementNode 是 three.js 节点着色语言(TSL,Three Shading Language)中用于表达"数组元素访问"的基类。它在 TSL 的节点图编译流程中负责把形如 array(0) 的节点表达式翻译成着色器中的 buffer[ index ] 取值代码,并通过类型推断机制保证访问结果的类型正确。本文以 docs/pages/ArrayElementNode.html.md 这份官方 API 文档为骨架,结合 src/nodes/utils/ArrayElementNode.js 的完整源码实现,完整讲解其构造参数、属性、类型推断方法、底层代码生成逻辑,以及仓库中四个具体子类(存储缓冲、Uniform 数组、引用元素、Compute 工作组缓冲)的分工与适用场景。

一、定位:TSL 节点继承链中的位置

文档给出的继承关系是:

EventDispatcher → Node → ArrayElementNode

对应源码 src/nodes/utils/ArrayElementNode.js#L9

class ArrayElementNode extends Node { // @TODO: If extending from TempNode it breaks webgpu_compute

几个值得注意的设计细节:

  • 它位于 src/nodes/utils/ 目录,说明这是一个工具型基类:本身不是给最终用户直接创建的节点,而是为各种"数组类"数据节点提供统一的元素访问抽象。
  • 从源码注释可以看到,它刻意继承 Node 而非 TempNode——源文件中的 @TODO 备注指出"如果继承 TempNode 会破坏 webgpu_compute 示例"(见 src/nodes/utils/ArrayElementNode.js#L9)。这是因为元素访问通常是"取值表达式",不需要独立的临时存储槽,直接内联为 xxx[ i ] 片段即可。
  • 类上定义了静态 type 属性,返回字符串 'ArrayElementNode'src/nodes/utils/ArrayElementNode.js#L11-L15),供 TSL 的节点识别体系使用。

官方文档中的定义是:"Base class for representing element access on an array-like node data structures"(用于表示对类数组节点数据结构进行元素访问的基类)。

二、构造函数与属性

new ArrayElementNode( node, indexNode )

文档定义的构造签名为:

new ArrayElementNode( node, indexNode )

参数说明

参数 类型 含义
node Node 被访问的类数组节点(array-like node)
indexNode Node 定义元素访问位置的索引节点

对应源码实现(src/nodes/utils/ArrayElementNode.js#L23-L50):

constructor( node, indexNode ) {

    super();

    // 类数组节点
    this.node = node;

    // 定义元素访问位置的索引节点
    this.indexNode = indexNode;

    // 可用于类型测试的标志位,默认 true
    this.isArrayElementNode = true;

}

属性一览

官方文档列出的三个属性与源码一一对应:

属性 类型 说明
.node Node 被访问的类数组节点
.indexNode Node 定义元素访问位置的索引节点
.isArrayElementNode boolean(只读) 类型测试标志位,默认 true

three.js 的 TSL 体系普遍采用这种"布尔标志位 + 静态 type 字符串"的双重类型标记方式,方便在节点编译期用 node.isArrayElementNode === true 之类的判断对节点做特化处理。子类还会在此之上追加各自的标志位,例如 isStorageArrayElementNodeisArrayBufferElementNode 等。

三、类型推断:generateNodeType 与 getMemberType

文档中列出的两个方法 generateNodeTypegetMemberTypeArrayElementNode 的核心,它们的共同特点是把类型问题"转发"给被访问的数组节点——即"访问一个数组的第 i 个元素,结果的类型就等于数组元素类型"。

.generateNodeType( builder ) : string

源码(src/nodes/utils/ArrayElementNode.js#L58-L62):

generateNodeType( builder ) {

    return this.node.getElementType( builder );

}

它覆盖了基类 Node#generateNodeType 的默认行为,直接返回 this.node.getElementType( builder )。也就是说,元素节点的输出类型完全由被访问的类数组节点自行回答"我的元素是什么类型"。

作为对照,普通节点的 getElementType 默认实现位于 src/nodes/core/Node.js#L542-L548

getElementType( builder ) {

    const type = this.getNodeType( builder );
    const elementType = builder.getElementType( type );

    return elementType;

}

其注释解释道:某些类型由多个元素组成(例如 vec3 由三个 float 组成),该方法返回这些元素的类型。可见 getElementType 与"标量成分类型"是同一个语义入口,各数组类节点会按自身数据结构重写它。

.getMemberType( builder, name ) : string

源码(src/nodes/utils/ArrayElementNode.js#L71-L75):

getMemberType( builder, name ) {

    return this.node.getMemberType( builder, name );

}

它同样转发给类数组节点:当你进一步访问"元素的成员"(例如取某个结构体元素的 .x)时,成员类型依然由数组节点的结构定义决定。

四、代码生成:generate 方法与索引类型处理

官方 API 文档(docs/pages/ArrayElementNode.html.md)没有列出 generate 方法,但源码中存在该方法的实现(src/nodes/utils/ArrayElementNode.js#L77-L86),这是理解该基类如何真正产出着色器代码的关键:

generate( builder ) {

    const indexType = this.indexNode.getNodeType( builder );

    const nodeSnippet = this.node.build( builder );
    const indexSnippet = this.indexNode.build(
        builder,
        ! builder.isVector( indexType ) && builder.isInteger( indexType )
            ? indexType
            : 'uint'
    );

    return `${ nodeSnippet }[ ${ indexSnippet } ]`;

}

可以拆解为三步:

  1. 解析索引类型:先取 indexNode 的着色器类型 indexType
  2. 构建索引片段:索引被构建为 uint 或原始整型。规则是——如果索引既不是向量、又是整型,则沿用其自身类型;否则统一转成 uint。这保证了 arr[ floatIndex ] 这类写法在生成 WGSL 片段前完成必要的类型转换,避免着色器编译期类型不匹配。
  3. 拼接取值表达式:最终输出形如 ${ nodeSnippet }[ ${ indexSnippet } ] 的字符串,即标准的 数组名[ 下标 ] 语法。

这里的 builderNodeBuilder)是 TSL 编译期上下文,负责为每个节点生成唯一的着色器变量/片段名、处理类型转换与格式化(builder.format)。

五、配套数组节点:ArrayNode 与 array() 函数

文档没有展开"被访问的数组从哪来",仓库中与之配套的核心是 src/nodes/core/ArrayNode.jsArrayNode 继承自 TempNode,代表一组节点值,通常由 TSL 的 array() 函数创建(src/nodes/core/ArrayNode.js#L4-L17):

const colors = array( [
    vec3( 1, 0, 0 ),
    vec3( 0, 1, 0 ),
    vec3( 0, 0, 1 )
] );

const redColor = colors.element( 0 );

array() 工厂函数支持两种调用形态(src/nodes/core/ArrayNode.js#L151-L172):

  • array( [ value0, value1, ... ] ):传入节点数组,元素类型自动推断(nodeTypenull 时取 values[ 0 ] 的类型),数量取数组长度;
  • array( 'vec3', count ):显式指定元素类型与数量,元素默认值留空。

此外 TSL 还提供方法链 node.toArray( count )src/nodes/core/ArrayNode.js#L174),把同一节点重复填充成数组。

ArrayNodegenerate() 最终委托给 builder.generateArray( type, count, values )src/nodes/core/ArrayNode.js#L129-L135)。后者在 src/nodes/core/NodeBuilder.js#L1343-L1369 中生成 WGSL 风格的 array( ... ) 字面量构造:

generateArray( type, count, values = null ) {

    let snippet = this.generateArrayDeclaration( type, count ) + '( ';

    for ( let i = 0; i < count; i ++ ) {
        // values[i] 存在则构建其片段,否则生成该类型的默认常量
        ...
    }

    snippet += ' )';

    return snippet;

}

也就是说,array() 创建的数组会被实例化为着色器内的一次性常量结构,而通过 element( i ) 对其索引访问时,生成的正是 ArrayElementNode 家族输出的 xxx[ i ] 表达式。

六、TSL 使用方式:element() 函数与方法链

在用户代码中,通常不直接 new ArrayElementNode( ... ),而是使用 TSL 提供的两个等价入口(src/nodes/tsl/TSLCore.js#L1260-L1264):

export const element = /*@__PURE__*/ nodeProxy( ArrayElementNode ).setParameterLength( 2 );
...
addMethodChaining( 'element', element );
  • 函数式element( node, indexNode ),内部经由 nodeProxy 包装 ArrayElementNode,并自动把传入的普通对象转换为节点;
  • 方法链式:任何支持元素访问的数组类节点都可以通过 .element( indexNode ) 调用,indexNode 可以是数字字面量、int 节点或其他任何可构建为索引的节点。

典型用法(与 docs/pages/ArrayNode.html.md 中的示例一致):

import { array, vec3, int } from 'three/tsl';

const colors = array( [ vec3( 1, 0, 0 ), vec3( 0, 1, 0 ), vec3( 0, 0, 1 ) ] );

// 常量索引
const redColor = colors.element( 0 );

// 动态索引:由场景数据或计算得来
const index = int( uColorIndex );
const pickedColor = colors.element( index );

七、仓库中的四个子类:不同数据源下的元素访问

ArrayElementNode 本身只解决"语法与类型转发",真正对接不同数据源的细节由子类完成。通过检索 extends ArrayElementNode,仓库中共有四个子类:

1. StorageArrayElementNode —— GPU 存储缓冲元素访问

位置:src/nodes/utils/StorageArrayElementNode.js

用于对 StorageBufferNode 的元素访问,通常经由 storageBuffer.element( index ) 间接使用,官方示例:

const position = positionStorage.element( instanceIndex );

它的特殊之处在于后处理与回退逻辑src/nodes/utils/StorageArrayElementNode.js#L92-L128):

  • 当运行时不支持 storageBuffer 特性时,若缓冲是 PBO(isPBO === true)且不在赋值上下文中,走 builder.generatePBO( this ) 的 WebGL 回退路径;
  • 否则按标准 super.generate( builder ) 输出 buffer[ i ],再经 builder.format( snippet, type, output ) 做类型适配。

这解释了为什么 StorageArrayElementNode 可以在 WebGPU 不可用的 WebGL 后端上部分工作。

2. UniformArrayElementNode —— Uniform 数组元素访问

位置:src/nodes/accessors/UniformArrayNode.js#L12-L51

配套节点 UniformArrayNodethree.js 原生对象(ColorVector3Matrix4 等)数组上传为 uniform buffer,并自动处理 GPU uniform 布局对齐(paddedType)。其元素节点在 generate() 中用填充后的类型做格式转换(src/nodes/accessors/UniformArrayNode.js#L41-L49):

generate( builder ) {

    const snippet = super.generate( builder );
    const type = this.getNodeType( builder );
    const paddedType = this.node.getPaddedType();

    return builder.format( snippet, paddedType, type );

}

例如 vec3 元素在 uniform 布局中实际占 vec4 空间(见 getPaddedType()src/nodes/accessors/UniformArrayNode.js#L161-L187),builder.format 会完成 vec4 → vec3 的正确截取,用户拿到的依然是语义正确的 vec3。典型用法:

const tintColors = uniformArray( [
    new Color( 1, 0, 0 ),
    new Color( 0, 1, 0 ),
    new Color( 0, 0, 1 )
], 'color' );

const redColor = tintColors.element( 0 );

3. ReferenceElementNode —— 引用型属性中的数组元素

位置:src/nodes/accessors/ReferenceElementNode.js

当通过 reference() 引用的属性本身是数组型数据时,ReferenceElementNode 允许用索引指向该数据结构中的具体元素。它重写 generateNodeType() 直接返回 this.referenceNode.uniformTypesrc/nodes/accessors/ReferenceElementNode.js#L54-L58),并在 generate() 中对 super.generate( builder ) 的结果按"数组类型 → 元素类型"做格式化。

4. WorkgroupInfoElementNode —— Compute 工作组作用域缓冲元素

位置:src/nodes/gpgpu/WorkgroupInfoNode.js#L11-L55

对应 workgroupArray( type, count ) 创建的 workgroup 作用域共享内存(仅 WebGPU/Compute 可用),其 element( indexNode ) 返回 WorkgroupInfoElementNode。子类在生成时额外处理赋值上下文判断与非赋值场景下的类型格式化,保证对局部共享缓冲的读写符合 WGSL 作用域规则。

从源码结构看,四个子类共同遵循同一套模式:保留基类的 node[ index ] 语法骨架与类型转发,仅重写"索引来源的布局细节、后格式化与回退策略",这正是把该基类设计为抽象基类的价值所在。

八、小结

  • ArrayElementNode 是 TSL 中"数组元素访问"的统一抽象:构造时接收 node(类数组节点)与 indexNode(索引节点),以 xxx[ i ] 形式生成着色器片段,并把节点类型、成员类型推断转发给被访问数组(generateNodeType / getMemberType)。
  • 索引类型有明确处理规则:非向量整型索引保留原类型,其余转换为 uintsrc/nodes/utils/ArrayElementNode.js#L77-L86)。
  • 用户侧入口是 TSL 的 array()element() 函数与 .element( index ) 方法链(src/nodes/tsl/TSLCore.js#L1260-L1264src/nodes/core/ArrayNode.js#L151-L174);底层数组字面量由 NodeBuilder.generateArray() 产出 WGSL 构造代码。
  • 针对真实数据源,仓库提供了 StorageArrayElementNode(存储缓冲 + PBO 回退)、UniformArrayElementNode(uniform 布局对齐)、ReferenceElementNode(引用属性)、WorkgroupInfoElementNode(Compute 共享内存)四个子类,分别覆盖 WebGL/WebGPU 下的不同缓冲场景。

如需进一步研究,可继续阅读 docs/pages/ArrayNode.html.md(数组节点 API)、docs/TSL.md(TSL 总览)以及 src/nodes/ 目录下上述子类的完整实现。

登录后查看全文
热门项目推荐
相关项目推荐