Three.js ComputeNode 全解析:TSL 计算着色器的节点封装、调度参数与底层执行机制
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。所有属性初始值均已在构造函数中显式声明:count、dispatchSize、countNode 初始为 null,version 初始为 1,name 初始为 '',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.js 的 compute() 方法中明确检查 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 函数,源码中包含两层严格校验:
workgroupSize长度必须为 1、2 或 3 个元素,否则报错'TSL: compute() workgroupSize must have 1, 2, or 3 elements';- 每个元素必须是正整数,否则报错
'TSL: compute() workgroupSize element at index [ n ] must be a positive integer'; - 校验通过后,不足 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 !== null且builder.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.js 的 compute() 方法可以确认完整的调用链:
updateBefore( frame )由节点更新系统按updateBeforeType = 'object'每对象触发一次,内部调用renderer.compute( this );compute()递增info.compute.calls与info.compute.frameCalls统计计数(可通过 renderer.info 观测);- 校验
isComputeNode标志,不通过则抛错; - 对每个尚未编译的节点(
pipelines.has( computeNode ) === false):注册dispose监听以自动清理资源,调用onInitFunction(即.onInit()设置的回调)完成初始化,再编译 pipeline; - 已通过
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 源码为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00