首页
/ three.js PassNode 深度指南:用 TSL 构建 WebGPU 后处理渲染通道(Beauty Pass 与 MRT)

three.js PassNode 深度指南:用 TSL 构建 WebGPU 后处理渲染通道(Beauty Pass 与 MRT)

2026-09-07 12:22:13作者:温玫谨Lighthearted

本文围绕 three.js 官方文档 docs/pages/PassNode.html.md 展开,并结合其源码实现(PassNode.js)与真实示例进行纵深讲解。你将掌握:PassNode 在 WebGPU 后处理管线中的作用与渲染时机、pass() / depthPass() 两种 TSL 工厂函数的用法、内部渲染目标与输出纹理的命名约定、MRT(多渲染目标)与双层纹理缓冲(前帧纹理)的工作机制,以及分辨率缩放、视口/裁剪等常用 API 的实战写法。

一、PassNode 是什么:后处理管线中的"渲染场景"节点

PassNode 位于 TSL(Three.js Shading Language)节点体系中,继承链为 EventDispatcher → Node → TempNode → PassNode。它表示一次场景渲染 pass(在后期处理语境下常被称作 beauty pass):接收一个场景与相机,把它们渲染进一个内部 RenderTarget,并把结果以纹理节点的形式暴露给后处理节点图继续加工。

它的使用位置在 WebGPU 后处理管线 RenderPipeline 的末端:

import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';

const renderPipeline = new THREE.RenderPipeline( renderer );

const scenePass = pass( scene, camera );
renderPipeline.outputNode = scenePass;

src/renderers/common/RenderPipeline.js 中,RenderPipeline 会把 outputNode 编译进全屏四边形(quad mesh)的片元节点并逐帧执行,即每次渲染都将当前帧号交给这段节点图计算最终像素。PassNode 作为最上游的"场景渲染源"节点,负责把 3D 场景离线渲染到离屏纹理,供下游的辉光、景深、SSR、调色等后处理消费。

围绕 PassNode,同一文件还定义了两种内部辅助节点:

  • PassTextureNode源码 L22-L75):TextureNode 的子类,绑定某个 Pass 及其输出纹理;
  • PassMultipleTextureNode源码 L83-L165):支持"当前帧 / 前帧"多纹理切换,是 getPreviousTextureNode() 等前帧(velocity/motion vector)相关 API 的基础,它的采样纹理由 Pass 节点自身托管而非外部传入。

这两个辅助类属于实现细节,对使用者透明;日常只需使用下述 TSL 工厂函数。

二、创建 PassNode:构造函数与 TSL 工厂函数

文档给出的直接构造签名是:

new PassNode( scope : 'color' | 'depth', scene : Scene, camera : Camera, options : Object )

但绝大多数场景推荐使用同一源码文件底部导出的两个 TSL 工厂函数(源码 L1073-L1106):

// 颜色 pass(beauty pass)
export const pass = ( scene, camera, options ) => new PassNode( PassNode.COLOR, scene, camera, options );

// 深度 pass
export const depthPass = ( scene, camera, options ) => new PassNode( PassNode.DEPTH, scene, camera, options );

对应关系为 pass() → scope 'color'depthPass() → scope 'depth'scope 决定节点的最终输出内容(见下文 setup() 逻辑)。options 会透传给内部 RenderTarget(详见下一节)。

examples/webgpu_mrt.html 中可以见到真实调用:

const scenePass = pass( scene, camera, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter } );
scenePass.setMRT( mrt( {
    output: output,
    normal: packNormalToRGB( normalView ),
    diffuse: diffuseColor,
    emissive: emissive
} ) );

三、内部渲染目标与 options 选项

构造函数内部(源码 L246-L260)会创建渲染目标:

const renderTarget = new RenderTarget( this._width, this._height, { type: HalfFloatType, ...options } );
renderTarget.texture.name = 'output';

let depthTexture = null;
if ( this.scope === PassNode.DEPTH || options.depthBuffer !== false ) {
    depthTexture = options.depthTexture || new DepthTexture();
    depthTexture.isRenderTargetTexture = true;
    depthTexture.name = 'depth';
    renderTarget.depthTexture = depthTexture;
}

由此可知以下关键默认行为

  • 内部纹理采用 HalfFloatType(半浮点)作为默认类型,除非 options 覆盖;在 setup() 中(源码 L798-L812)还会根据 renderer.getOutputBufferType() 与反向深度缓冲状态同步调整类型。
  • 颜色输出纹理默认命名为 'output',深度纹理默认命名为 'depth'
  • 只要 scope'depth',或者未显式传 options.depthBuffer = false,就会附加一份 DepthTexture
  • 渲染目标尺寸初始为 1×1,随后在每帧渲染前由 updateBefore() 依据渲染器实际绘图缓冲大小自动同步(见第五节)。

常用的 options 字段(来自渲染目标与 Pass 自身)

options 字段 类型 说明(依据 PassNode.jsRenderTarget
type 纹理类型常量 覆盖默认 HalfFloatType,例如 THREE.FloatTypeTHREE.UnsignedByteType
samples number MSAA 采样数;缺省沿用 renderer.samples
minFilter / magFilter 过滤常量 纹理过滤方式,如示例中的 NearestFilter
depthBuffer boolean 是否创建深度纹理,缺省 true(除非 scope 是 color 且显式传 false)
depthTexture DepthTexture 自定义深度纹理实例,替代内部新建的 DepthTexture
autoClear boolean 渲染本 pass 前是否自动清屏,缺省 true
autoClearColor / autoClearDepth / autoClearStencil boolean 分别控制颜色/深度/模板缓冲是否自动清除,缺省均 true

其中 autoClear 系列字段会在每帧 updateBefore() 中临时应用到渲染器(源码 L875-L883),渲染结束后再恢复,因此不会污染全局渲染状态。

四、属性(Properties)总览

文档逐条列出了以下属性,源码中均有对应声明:

  • .camera : Camera —— 渲染所用的相机引用。

  • .scene : Scene —— 渲染所用的场景引用。

  • .scope : 'color' | 'depth' —— 输出通道语义;决定 setup() 返回颜色纹理节点还是线性深度节点。

  • .options : Object —— 内部渲染目标选项(见第三节)。

  • .renderTarget : RenderTarget —— 由 Pass 内部持有并管理的渲染目标;dispose() 释放的正是它(源码 L1040-L1044)。

  • .contextNode : ContextNode | null —— 可选的全局渲染上下文节点;若非空,updateBefore() 会在渲染本 pass 期间把它与渲染器当前上下文合并后临时注入(源码 L890-L903),并在结束后还原。

  • .overrideMaterial : Material | null —— 可选的覆盖材质;非空时,渲染前写入 scene.overrideMaterial,渲染后还原,用于批量替换场景全部对象的材质(如法线/深度可视化)。

  • .transparent : boolean —— 本 pass 是否渲染透明对象,默认 false(注意:源码构造函数里初始化为 true,随后在渲染前会作为 renderer 的 transparent/opaque 开关被设置——依据 JSDoc 与文档口径,默认语义为 true/false,实际以 updateBefore() 中应用到渲染器为准)。

    说明:此处文档标注 transparent 默认 falseopaque 默认 true,而构造函数实际初始值与之略有出入(transparent = true)。updateBefore() 会把这两个布尔值直接写入 renderer.transparentrenderer.opaque,因此最终渲染行为取决于你如何设置它们。编写时建议显式赋值以避免歧义。

  • .global : boolean —— 全局缓存标志,默认 true(覆盖 TempNode 的同名属性),让相同输入的 Pass 在构建缓存中可被复用。

  • .isPassNode : boolean(只读) —— 类型测试标志,恒为 true

  • .updateBeforeType : string —— 帧更新频率,默认 'frame'NodeUpdateType.FRAME,见 src/nodes/core/constants.js)。PassNode 需要每帧在正式渲染前执行一次场景渲染。

  • .COLOR : 'color' / .DEPTH : 'depth' —— 两个静态只读常量,用于构造参数与内部比较(setup()this.scope === PassNode.COLOR)。

五、每帧渲染时机:updateBefore 生命周期

PassNode.updateBeforeType = NodeUpdateType.FRAME 意味着:只要 Pass 节点参与了某帧的节点图求值,其 updateBefore() 就会在帧渲染前被调用一次,完成整场场景渲染。该方法是理解 PassNode 的核心(源码 L814-L927),其主要步骤可概括为:

  1. 解析相机与尺寸:若是 XR 渲染(renderer 的当前输出是 XRRenderTarget),改用 renderer.xr.getCamera() 并更新 XR 相机;否则使用 .camera,并用 renderer.getDrawingBufferSize() 获取实际绘图缓冲尺寸(已含 pixelRatio)。
  2. 尺寸同步:调用 setSize() 让内部渲染目标与绘制缓冲保持一致(含分辨率缩放系数)。
  3. 状态保存与覆盖:备份渲染器的渲染目标、MRT、autoClear 系列、transparent/opaque、光照、contextNode 以及相机的 layers.mask;然后逐一应用本 Pass 配置(MRT、autoClear、overrideMaterial、layers、contextNode 等)。
  4. 前帧纹理轮换:遍历 _previousTextures 中的每个输出名执行 toggleTexture(),实现当前帧/前帧双缓冲(运动矢量等时间型算法依赖此机制)。
  5. 渲染:设置相机近远平面 uniform,然后调用 renderer.render( scene, camera ) 将场景渲染进 this.renderTarget
  6. 状态还原:恢复所有渲染器状态与相机 layers.mask、场景名与 overrideMaterial,确保 Pass 之间互不干扰。

其中 .cameranear/far 会写入两个内部 uniform(_cameraNear/_cameraFar),供深度换算节点使用(见第七节)。

六、渲染结果读取:setup 返回什么

setup()源码 L798-L812)决定了 Pass 节点在图求值中的"值":

return this.scope === PassNode.COLOR ? this.getTextureNode() : this.getLinearDepthNode();
  • scope 为 'color':返回颜色输出纹理节点(默认名 'output'),可直接作为 RenderPipeline.outputNode 或喂给下游后处理节点;
  • scope 为 'depth':返回线性深度节点(由深度纹理推导而来),因此 depthPass() 天然适合做深度相关的自定义后处理输入。

七、方法详解:从分辨率缩放到深度节点

7.1 分辨率控制

scenePass.setResolutionScale( 0.5 );   // 半分辨率渲染
const s = scenePass.getResolutionScale(); // 0.5,1 表示全分辨率

setResolutionScale() / getResolutionScale()源码 L497-L514)是分辨率控制的首选 API:缩放系数会乘以渲染器宽高,并在 setSize() 中向下取整到渲染目标(源码 L935-L943)。降低分辨率是后处理中常见的性能优化手段。

⚠️ 注意setResolution() / getResolution()r181 起已废弃源码 L524-L544),调用时会打印弃用警告并转发到 setResolutionScale()/getResolutionScale()。新代码请直接使用后者。

setSize( width, height )源码 L935-L966)由渲染系统每帧自动调用以保证尺寸同步(honors pixel ratio),一般无需手动调用。

7.2 视口与裁剪(Viewport / Scissor)

// 方式一:四个参数(逻辑像素,以左下角为原点)
scenePass.setViewport( 0, 0, width / 2, height );
scenePass.setScissor( 0, 0, width / 2, height );

// 方式二:单个 Vector4
scenePass.setViewport( new THREE.Vector4( 0, 0, width / 2, height ) );

// 传 null 恢复为跟随 Pass 尺寸的自动行为
scenePass.setViewport( null );
scenePass.setScissor( null );
  • setViewport()源码 L1013-L1035)用于定义视口矩形,例如分屏对比渲染(左右眼/新旧算法各占半屏)。
  • setScissor()源码 L979-L1001)用于定义裁剪矩形,只绘制矩形内像素。
  • 默认两者都"与 Pass 尺寸保持同步";一旦手动设置则启用自定义矩形,并在 setSize() 中乘以分辨率缩放系数(multiplyScalar( _resolutionScale ).floor())。传 null 可回到自动模式。
  • 参数均以逻辑像素为单位,数值同时接受 x, y, width, height 四参数或单个 Vector4(通过 x.isVector4 判断)。

7.3 MRT 多渲染目标

scenePass.setMRT( mrt( { output, normal, diffuse, emissive } ) );
const current = scenePass.getMRT(); // 返回 MRTNode 或 null
  • setMRT( mrt : MRTNode )源码 L577-L583)为该 Pass 挂载 MRT 配置,使一次场景渲染同时输出颜色、法线、漫反射、自发光等多张纹理,减少重绘开销。示例见 examples/webgpu_mrt.html
  • getMRT() 返回当前 MRT 节点(默认 null)。
  • 每帧 updateBefore() 会把该 MRT 设置到渲染器,渲染完还原。

7.4 层配置(Layers)

scenePass.setLayers( new THREE.Layers() ); // 例如只渲染特定层的对象
const layers = scenePass.getLayers();

setLayers() / getLayers()源码 L552-L569)控制渲染时相机使用哪一组层遮罩:非空时,updateBefore() 会用 this._layers.mask 覆盖相机的 layers.mask,渲染后恢复原始遮罩。

7.5 纹理与纹理节点访问

Pass 内部以字典维护多路输出(默认键为 'output''depth'),通过以下 API 读取:

API 说明
getTexture( name ) 返回指定输出名的 Texture;若不存在则克隆主输出纹理并注册进 renderTarget.textures。请求 'depth' 但该 Pass 无深度纹理时抛出 THREE.PassNode: Depth texture is not available for this pass.源码 L602-L627
getTextureNode( name = 'output' ) 返回可在节点图中采样该输出的 PassMultipleTextureNode源码 L683-L697
getPreviousTexture( name ) 返回持有上一帧数据的纹理(首次调用按需克隆),用于速度/运动矢量计算(源码 L635-L649
getPreviousTextureNode( name = 'output' ) 前帧纹理对应的节点版本,供节点图直接采样上一帧(源码 L705-L721
toggleTexture( name ) 交换当前纹理与前帧纹理的引用,并同步刷新对应纹理节点的采样目标,实现双缓冲轮换(源码 L656-L675

examples/webgpu_mrt.html 中可以看到它们在实践中的搭配:先 scenePass.setMRT(...) 声明多路输出,再用 getTexture('normal') 等取出并优化纹理类型(如改为 UnsignedByteType),最后在 RenderPipeline.outputNode 的节点函数里通过 getTextureNode('output') 等采样各通道做分屏展示。

7.6 深度相关节点

深度信息在后处理(景深、雾、SSAO、接触阴影等)中经常被反算为线性深度或 view-space Z:

const linearDepth = scenePass.getLinearDepthNode();     // 默认针对 'depth' 输出
const viewZ = scenePass.getViewZNode();                 // 默认 'depth'
  • getViewZNode( name = 'depth' )源码 L729-L744)借助相机近远平面 uniform,通过 perspectiveDepthToViewZ 把透视深度转成 view-space Z(线性)。
  • getLinearDepthNode( name = 'depth' )源码 L752-L770)在其上继续用 viewZToOrthographicDepth 归一化到 0–1 线性深度。

两者都在内部做缓存并按输出名区分;name 参数为自定义深度输出预留(绝大多数情况使用默认 'depth' 即可)。上述换算函数来自 src/nodes/display/ViewportDepthNode.js,这正是 depthPass()setup() 中返回 getLinearDepthNode() 的原因——深度 Pass 的输出天然是可直接消费的线性深度。

7.7 预编译与释放

await scenePass.compileAsync( renderer ); // 预编译,返回 Promise
// ...资源不再使用时
scenePass.dispose();
  • compileAsync( renderer )源码 L783-L796)将 Pass 的渲染目标与 MRT 临时挂到渲染器,调用 renderer.compileAsync( this.scene, this.camera ) 完成管线预编译,随后恢复现场。文档特别强调:必须在 Pass 配置完成之后调用,即 setMRT()getTextureNode() 等应在预编译之前执行,否则新增输出不会进入编译内容。
  • dispose()源码 L1040-L1044)释放内部 RenderTarget(包括其纹理与深度纹理),适合在节点不再使用时调用以避免 GPU 资源泄漏。

八、实战组合示例:Beauty Pass + MRT 后处理

结合 examples/webgpu_mrt.htmlsrc/renderers/common/RenderPipeline.js 的实现,一个完整的"场景渲染 → 多路输出 → 后处理合成"骨架如下:

import * as THREE from 'three/webgpu';
import {
    output, normalView, pass, step, diffuseColor, emissive,
    packNormalToRGB, screenUV, mix, mrt, Fn
} from 'three/tsl';

// 1) 场景 beauty pass,并声明 MRT 多路输出
const scenePass = pass( scene, camera, {
    minFilter: THREE.NearestFilter,
    magFilter: THREE.NearestFilter
} );
scenePass.setMRT( mrt( {
    output: output,               // 最终颜色
    normal: packNormalToRGB( normalView ),
    diffuse: diffuseColor,
    emissive: emissive
} ) );

// 2) 可按输出名取原始纹理做后续 CPU 侧处理/类型优化
const normalTexture = scenePass.getTexture( 'normal' );
normalTexture.type = THREE.UnsignedByteType;

// 3) 挂到后处理管线末端
const renderPipeline = new THREE.RenderPipeline( renderer );
renderPipeline.outputNode = Fn( () => {
    const out  = scenePass.getTextureNode( 'output' );
    const normal = scenePass.getTextureNode( 'normal' );
    // ……下游任意 TSL 合成逻辑
    return out;
} )();

该管线之所以成立,是因为 RenderPipeline 每帧把 outputNode(这里会触发 scenePass.updateBefore())编译进全屏四边形材质并渲染(RenderPipeline.js#L190-L206),从而构成"场景离屏渲染 → 后期处理 → 上屏"的完整链路。

九、与其它节点的组合及源码佐证

在仓库中,许多高级效果节点都以 PassNode 为内部基础,可进一步佐证其通用性,例如 examples/jsm/tsl/display/ 下的 BloomNode.jsGTAONode.jsGodraysNode.jsSSAONode.jsSSRNode.jsOITPassNode.jsStereoPassNode.jsSSAAPassNode.js 等,以及 src/nodes/display/ToonOutlinePassNode.js,它们普遍通过 pass()/depthPass() 抓取场景帧再叠加各自效果。若自定义效果需要"场景颜色 + 场景深度 + 上一帧画面"三种输入,可组合为:

const scenePass = pass( scene, camera );
// 场景颜色采样:scenePass.getTextureNode()          —— 当前帧
// 上一帧采样:  scenePass.getPreviousTextureNode()   —— 时间型效果(如 TAA/运动模糊)
// 深度采样:    scenePass.getLinearDepthNode()       —— 空间型效果(如景深/SSAO)

值得注意的是不同 Pass 若共享同一场景与相机,global = true 会令它们在节点构建缓存中按需复用,避免重复渲染同一场景。

十、要点回顾与最佳实践

  • 需要"把场景渲成一张离屏纹理"时,用 pass( scene, camera, options )(颜色)或 depthPass( scene, camera, options )(线性深度),而不是手动管理 RenderTarget 与渲染循环。
  • 输出命名约定:颜色 'output'、深度 'depth';MRT 额外输出名由 mrt( {...} ) 的键决定,并通过 getTexture( name ) / getTextureNode( name ) 访问。
  • 半分辨率渲染用 setResolutionScale(),不要用 r181 已废弃的 setResolution()
  • 所有"配置类"调用(setMRT()getTextureNode()setLayers()setResolutionScale() 等)应在 compileAsync() 之前完成。
  • 手动设置 setViewport()/setScissor() 后,如需恢复自动跟随 Pass 尺寸,请传入 null
  • 时间型后处理(运动模糊、TAA 等)依赖 getPreviousTexture()/getPreviousTextureNode() 与每帧自动执行的 toggleTexture() 双缓冲机制,无需自建纹理交换。
  • 使用完毕调用 dispose() 释放内部渲染目标;Pass 每帧渲染发生在 updateBefore(),场景状态(overrideMaterial、MRT、layers、autoClear 等)会被完整保存与还原,因此多个 Pass 串联时互不污染。

延伸阅读:PassNode 官方文档核心实现RenderPipeline 实现MRT 完整示例ViewportDepthNode(深度换算)

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388