首页
/ Three.js ComputeNode 全解析:TSL 计算着色器的节点封装、调度参数与底层执行机制

Three.js ComputeNode 全解析:TSL 计算着色器的节点封装、调度参数与底层执行机制

2026-09-06 15:45:55作者:农烁颖Land

ComputeNode 是 three.js 节点系统(TSL)中专用于 WebGPU 计算着色器的核心封装类,继承链为 EventDispatcher → Node → ComputeNode,位于 src/nodes/gpgpu/ComputeNode.js。掌握它意味着你可以用 TSL 声明式地编写 GPU 并行计算逻辑(粒子模拟、粒子排序、流体等),并通过 count / dispatchSize / workgroupSize 精确控制线程调度。本文基于官方 API 文档 docs/pages/ComputeNode.html.md 与仓库源码,完整覆盖其构造参数、属性、方法,并深入解析其在 Renderer 中的实际执行链路。

构造器与继承体系

ComputeNode 继承自 Node(进而继承 EventDispatcher),代表一个计算着色器节点("Represents a compute shader node")。

new ComputeNode( computeNode, workgroupSize )

参数 类型 说明
computeNode Node 定义计算着色器逻辑的节点
workgroupSize Array.<number> 定义计算着色器执行时 workgroup 的 X、Y、Z 维度

src/nodes/gpgpu/ComputeNode.js 的源码可以看到,构造函数内部首先调用 super( 'void' ) 将节点输出类型固定为 void——计算着色器不产出渲染结果,只产生副作用(写入存储缓冲、纹理等);随后初始化 isComputeNode = true 类型标志,并保存 workgroupSize。所有属性初始值均已在构造函数中显式声明:countdispatchSizecountNode 初始为 nullversion 初始为 1name 初始为 ''onInitFunction 初始为 null

属性详解

.computeNode : Node

定义计算着色器逻辑的节点。这是被包装的实际计算体。

.count : number | Array.

要执行的线程(invocation)总数。文档指出:当其为 number 类型时,会自动生成针对 instanceIndex 的边界检查(bounds checking)。这一行为在源码 setup()generate() 中得到印证(见后文"底层执行机制"一节)。

.countNode : UniformNode

一个持有 dispatch count 的 uniform 节点,用于边界检查。当 count 为 number 时由 setup( builder ) 自动创建:

// src/nodes/gpgpu/ComputeNode.js
if ( this.count !== null && this.countNode === null ) {
    this.countNode = uniform( this.count, 'uint' ).onObjectUpdate( () => this.count );
}

可以看到 countNode 绑定为 'uint' 类型,且通过 onObjectUpdate 在每次对象更新时同步 count 的最新值——这意味着运行期修改 computeNode.count 会动态改变着色器内的线程上限。

.dispatchSize : number | Array.

workgroup 在 X、Y、Z 轴上的 dispatch 尺寸。当 count 未提供时直接使用。

.isComputeNode : boolean (readonly)

用于类型测试的标志,默认为 true。这个标志是 Renderer 校验入参的依据:src/renderers/common/Renderer.jscompute() 方法中明确检查 computeList[ 0 ].isComputeNode !== true 时抛出 'THREE.Renderer: .compute() expects a ComputeNode.',异步预编译接口 compileComputeAsync() 也有同样的 isComputeNode !== true 校验——因此该标志并非装饰性的,而是调度层识别计算节点的唯一凭据。

.name : string

节点的名称或标签,默认为 ''。覆写自 Node#name

.onInitFunction : function

计算节点完成初始化时执行的回调。Renderer 在首次编译该节点的 pipeline 时(即 pipelines.has( computeNode ) === false 分支)调用它,并以 { renderer: this } 作为参数上下文执行 onInitFn.call( computeNode, { renderer: this } )——典型用途是在初始化回调中获取 renderer 后配置存储缓冲、纹理等资源。

.updateBeforeType : string

覆写自 Node#updateBeforeType,默认值为 'object'(即 NodeUpdateType.OBJECT)。文档说明:因为 updateBefore 默认每个对象执行一次,所以类型设为 OBJECT。这决定了 ComputeNode 的每帧更新时机绑定到场景对象的生命周期上。

.version : number

节点版本号,覆写自 Node#version

.workgroupSize : Array.

定义 workgroup 的 X、Y、Z 维度,默认值为 [ 64 ]

方法详解

.dispose()

触发本节点的 dispose 事件(this.dispatchEvent( { type: 'dispose' } ))。Renderer 在首次编译 pipeline 时会通过 computeNode.addEventListener( 'dispose', dispose ) 监听该事件,用于在节点销毁时清理 pipelines、bindings 与 nodes 缓存,因此 dispose() 是资源回收的入口。

.setName( name : string ) : ComputeNode

设置 name 属性,返回自身引用以支持链式调用。

.label( name : string ) : ComputeNode(已废弃)

功能等同于 setName,但源码中已标记废弃:

label( name ) {
    warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() );
    return this.setName( name );
}

新代码应直接使用 setName()

.onInit( callback : function ) : ComputeNode

设置初始化期间运行的回调函数,等价于赋值 this.onInitFunction = callback,返回自身。

.updateBefore( frame : NodeFrame )

执行本节点的计算方法,覆写自 Node#updateBefore。实现极为简洁:

updateBefore( { renderer } ) {
    renderer.compute( this );
}

即把自身作为 ComputeNode 交给 Renderer 的 compute() 方法调度,这是它接入渲染循环的唯一钩子。

TSL 工厂函数:compute 与 computeKernel

除了直接使用构造器,TSL 更推荐使用两个工厂函数(均定义在 src/nodes/gpgpu/ComputeNode.js 文件底部):

computeKernel( node, workgroupSize = [ 64 ] )

创建计算内核节点的 TSL 函数,源码中包含两层严格校验:

  1. workgroupSize 长度必须为 1、2 或 3 个元素,否则报错 'TSL: compute() workgroupSize must have 1, 2, or 3 elements'
  2. 每个元素必须是正整数,否则报错 'TSL: compute() workgroupSize element at index [ n ] must be a positive integer'
  3. 校验通过后,不足 3 维的部分自动填充 1——源码注释说明这与 WGSL 对 @workgroup_size 的处理方式一致。

compute( node, count, workgroupSize )

computeKernel 基础上增加调度参数,关键分支逻辑为:

if ( typeof count === 'number' ) {
    computeNode.count = count;
} else {
    computeNode.dispatchSize = count;
}

即:传入数字走 count(自动获得 instanceIndex 边界检查);传入数组则视为 workgroup 的 dispatch 尺寸写入 dispatchSize。两个函数均通过 addMethodChaining 注册,因此也可作为节点方法链式调用。

底层执行机制:setup 与 generate 的双阶段行为

理解 ComputeNode 的关键在于它区分了两种 shader stage。源码 generate( builder, output ) 中:

  • compute stage(builder.shaderStage === 'compute':调用 this.computeNode.build( builder, 'void' ) 生成计算逻辑代码片段,若非空则通过 builder.addLineFlowCode( snippet, this ) 注入。随后若 count !== nullbuilder.allowEarlyReturns === true,会额外注入一段边界检查代码:

    builder.flow.code = `${ builder.tab }if ( ${ indexSnippet } >= ${ countSnippet } ) { return; }\n\n${ builder.flow.code }`;
    

    这正是文档中"自动生成针对 instanceIndex 的边界检查"的实现——把 instanceIndex >= count 的线程提前 return,保证 dispatch 尺寸非 workgroup 整数倍时不会有线程越界写入。

  • 非 compute stage:读取 setup() 阶段保存的 properties.outputComputeNode 并重建输出,实现计算节点向渲染阶段的结果透传。

Renderer 中的实际调度链路

src/renderers/common/Renderer.jscompute() 方法可以确认完整的调用链:

  1. updateBefore( frame ) 由节点更新系统按 updateBeforeType = 'object' 每对象触发一次,内部调用 renderer.compute( this )
  2. compute() 递增 info.compute.callsinfo.compute.frameCalls 统计计数(可通过 renderer.info 观测);
  3. 校验 isComputeNode 标志,不通过则抛错;
  4. 对每个尚未编译的节点(pipelines.has( computeNode ) === false):注册 dispose 监听以自动清理资源,调用 onInitFunction(即 .onInit() 设置的回调)完成初始化,再编译 pipeline;
  5. 已通过 backend.beginCompute( computeNodes ) 将计算分发到底层。

此外,Renderer 还提供异步预编译接口 compileComputeAsync( computeNodes, onProgress )(源码注释说明其用途是规避首次渲染时的"shader compilation stutter"),同样要求入参通过 isComputeNode 校验。

适用前提与限制

  • ComputeNode 属于 WebGPU 计算管线能力,依赖 src/renderers/common/Backend.js 一类的后端抽象(Backend 中通过 abstractRenderContext.isComputeNode 分支处理计算上下文),实际运行需要支持 WebGPU 的环境;
  • workgroupSize 各维度必须为正整数,长度 1–3,默认 [ 64 ]
  • 边界检查仅在 count 为 number 且构建上下文允许 early return(allowEarlyReturns === true)时生效;
  • label() 已废弃,请使用 setName()
  • 本文所有 API 描述以当前仓库 docs/pages/ComputeNode.html.md 文档与 src/nodes/gpgpu/ComputeNode.js 源码为准。
登录后查看全文
热门项目推荐
相关项目推荐