首页
/ three.js TSL 着色器多输出指南:OutputStructNode 实现原理与 MRT 实战

three.js TSL 着色器多输出指南:OutputStructNode 实现原理与 MRT 实战

2026-09-07 18:29:39作者:丁柯新Fawn

OutputStructNode 是 three.js TSL(Three Shading Language)节点系统中负责“在着色器程序中声明多个输出”的核心节点。它以结构体的形式把片元着色器的输出组织成一个整体,是 WebGPU 渲染器中 @fragment 入口返回多字段结构体,以及 MRT(多渲染目标)后处理链路的底层基础设施。阅读本文后,你将理解该节点的构造函数与属性、它在 src/nodes/core/OutputStructNode.js 中的完整代码生成流程,并能借助其 TSL 封装 outputStruct 及派生类 MRTNode 写出可复用的多输出着色器代码。

本文对应的官方 API 参考页位于 docs/pages/OutputStructNode.html,可对照阅读。

节点定位与继承关系

OutputStructNode 的继承链为:

EventDispatcher → Node → OutputStructNode

从类定义(src/nodes/core/OutputStructNode.js)可以看到它继承自 Node,并没有重写 setup 等阶段的默认行为,核心职责全部集中在代码生成阶段(generate)。它的静态 type 返回字符串 'OutputStructNode',用于节点系统的类型注册与序列化。

在 TSL 中它还有一个一等公民的工厂函数 outputStruct( ...members ),定义于源码同一文件的末尾(src/nodes/core/OutputStructNode.js):

export const outputStruct = /*@__PURE__*/ nodeProxy( OutputStructNode );

它通过 nodeProxy 生成,凡是引用 src/nodes/TSL.js 的模块都能直接以 outputStruct( a, b, c ) 的形式创建节点。官方的 TSL 速查表在 docs/TSL.md 中将它登记为 “Creates an output struct node for returning multiple values”,而 docs/pages/TSL.html.md 给出了函数签名 .outputStruct( …members : Node ) : OutputStructNode

构造函数与参数语义

new OutputStructNode( …members : Node )

构造函数接收任意数量的节点作为成员参数(src/nodes/core/OutputStructNode.js):

constructor( ...members ) {

    super();
    this.members = members;
    this.isOutputStructNode = true;

}
  • members:一个可变长的参数列表,每个元素都是产生某种类型值的 Node(颜色、法线、深度、自定义 float 等)。传入顺序即最终结构体成员的声明顺序。
  • 需要注意:构造时并不校验成员类型,真正的类型解析发生在着色器编译阶段,由 generate() 对每个成员调用 getNodeType( builder ) 完成。因此传入的成员应当是"可求值出具体类型"的节点。

与普通节点不同,OutputStructNode 的成员可以拥有异构类型,这正是"结构体输出"与"单值输出"的本质区别——它允许一次编译生成多个不同数据类型的着色器输出。

属性详解

.isOutputStructNode : boolean(只读)

类型测试标志,默认值为 truesrc/nodes/core/OutputStructNode.js)。渲染器与构建器在生成片元着色器代码时用它来判断"当前输出是否是多字段结构体输出",例如 WebGL 回退后端在片元着色器中据此决定是否写入 fragColor(详见下文"着色器后端的差异化处理")。

.members : Array.

定义输出内容的一组节点(src/nodes/core/OutputStructNode.js)。它直接引用了构造函数收到的成员参数数组。派生类 MRTNode 会在 setup() 阶段重建该数组(对齐到渲染目标纹理索引),因此这既是构造语义的一部分,也是可被后处理器改写的工作区。

代码生成原理:从成员列表到着色器结构体

OutputStructNode 之所以能输出"结构体",全部秘密都在 generate( builder ) 中(src/nodes/core/OutputStructNode.js)。其流程可分为三段:

第一段:构建成员布局并注册结构体类型(仅首次执行)

if ( nodeData.membersLayout === undefined ) {

    const membersLayout = [];

    for ( let i = 0; i < members.length; i ++ ) {
        const name = 'm' + i;                         // 成员名固定为 m0、m1、m2……
        const type = members[ i ].getNodeType( builder );
        membersLayout.push( { name, type, index: i } );
    }

    nodeData.membersLayout = membersLayout;
    nodeData.structType = builder.getOutputStructTypeFromNode( this, nodeData.membersLayout );
}

成员被自动命名为 m0m1……并按声明次序排列,每个成员的类型通过 Node#getNodeType 实时解析。随后调用构建器的 getOutputStructTypeFromNodesrc/nodes/core/NodeBuilder.js)生成结构体类型:

getOutputStructTypeFromNode( node, membersLayout ) {

    const structType = this.getStructTypeFromNode( node, membersLayout, 'OutputType', 'fragment' );
    structType.output = true;
    return structType;

}

可以看到:生成的结构体类型名为 OutputType,作用于 fragment 阶段,并且被显式标记为 structType.output = true,从而让后端的着色器生成器把它识别为片元入口的返回结构体。

第二段:生成每个成员的赋值代码

const propertyName = builder.getOutputStructName();
const structPrefix = propertyName !== '' ? propertyName + '.' : '';

for ( let i = 0; i < members.length; i ++ ) {
    const snippet = members[ i ].build( builder, nodeData.membersLayout[ i ].type );
    builder.addLineFlowCode( `${ structPrefix }m${ i } = ${ snippet }`, this );
}

每个成员按布局中记录的期望类型完成自身构建(build),随后以 output.mN = <value>; 的形式写入流式代码。此处 propertyName 来自构建器的 getOutputStructName(),它决定了赋值时的前缀:

第三段:返回值

return propertyName;

generate() 最终返回结构体变量名(WGSL 下为 'output'),上层片段代码据此引用整个输出结构体。

着色器后端的差异化处理

OutputStructNode 这一抽象在 GLSL 与 WGSL 两种后端下表现不同,可从源码证实:

  • WGSL 构建器通过 getOutputStructName() 返回 'output',并把结构体注册进 fragment 阶段的 structs 列表,最终形成真正的"多字段结构体返回值",这正是 WebGPU 片元着色器入口的惯用写法。
  • GLSL 回退构建器在生成片元主流程时有专门判断(src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js):
} else if ( shaderStage === 'fragment' ) {

    if ( ! node.outputNode.isOutputStructNode ) {
        flow += 'fragColor = ';
        flow += `${ this.format( flowSlotData.result, ... ) };`;
    }
}

即:当主输出节点是一个 OutputStructNode(或其派生类 MRTNode)时,回退后端不再写默认的 fragColor,而是信任该结构体节点自身已展开的多输出赋值语句。这正是文档注释"该节点可被用来在着色器程序中定义多个输出"的落地实现。

与 MRTNode 的关系:多渲染目标的标准实现

OutputStructNode 最主要的实战派生类就是 MRTNodesrc/nodes/core/MRTNode.js),它继承 OutputStructNode,把"多字段结构体输出"升级为"多渲染目标"。其注释给出了最典型的用法:

const mrtNode = mrt( {
    output: output,
    normal: normalView
} );

MRTNode 的 setup()src/nodes/core/MRTNode.js)展示了它如何复用父类的结构体输出机制:

  1. 读取当前渲染器绑定的渲染目标及其纹理数组 mrt.textures
  2. 对字典 outputNodes 中的每个键,通过 getTextureIndex 找到目标纹理索引,未使用过的输出会被直接跳过;
  3. 按索引位置把输出节点转换为对应目标的输出类型,填入数组 members
  4. 最后调用 super.setup( builder ) 走 OutputStructNode 的统一流程。

从源码结构看,MRTNode.members 本质上就是 OutputStructNode 的 members——MRT 复用同一套"多输出结构体"生成管线,只是输入形态从参数列表变成了具名字典,并额外维护了每个输出的 blendModesclearColors

实战场景:后处理 Pass 中的多输出

在真实的后处理示例中,outputStruct/mrt 普遍承担着"一个 Pass 输出多张纹理"的角色。以下文件均可在 examples/jsm/tsl/display/ 下找到相应实现:

  • SSAONode.js 注释中演示了 scenePass.setMRT( mrt( { output, normal: normalView } ) ),即用一个场景 Pass 同时产出最终颜色与视图法线;
  • BloomNode.js 使用 MRT 在一次渲染中分离颜色输出;
  • GTAONode.jsOITPassNode.js 分别在 AO 计算与顺序无关透明(OIT)中管理多个输出目标;
  • PixelationPassNode.jsSSAAPassNode.js 在超采样相关链路中也通过 mrt( outputs ) 组织中间缓冲。

这些 Pass 一般配合 NodeMaterial/后处理节点的 setMRT() 使用——NodeMaterial 会把传入的 MRT 节点记录为主输出节点,着色器编译后即可一次性写入多个附件,最终由 WebGPU 渲染器的多个颜色附件(或 WebGL 回退的相应扩展)承接。对于需要 GBuffer、AO 法线、深度等多数据流的高阶渲染,这种模式是官方推荐且经过大量示例验证的写法。

小结

OutputStructNode 是 three.js TSL 中"多输出"语义的最小封装:构造函数用可变参数收集异构成员,membersisOutputStructNode 两个属性分别承担数据载体与类型判定的职责;编译期它会把成员按 m0、m1… 自动编排为名为 OutputType、标记 output = true 的片元结构体,并按后端差异生成带前缀(WGSL 的 output.)或无前缀(GLSL)的逐成员赋值代码。理解它的代码生成路径,是掌握 WebGPU 片元结构体输出、MRT 多渲染目标,以及阅读官方 TSL 后处理源码的起点。

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