three.js PassNode 深度指南:用 TSL 构建 WebGPU 后处理渲染通道(Beauty Pass 与 MRT)
本文围绕 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.js 与 RenderTarget) |
|---|---|---|
type |
纹理类型常量 | 覆盖默认 HalfFloatType,例如 THREE.FloatType、THREE.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默认false、opaque默认true,而构造函数实际初始值与之略有出入(transparent = true)。updateBefore()会把这两个布尔值直接写入renderer.transparent与renderer.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),其主要步骤可概括为:
- 解析相机与尺寸:若是 XR 渲染(
renderer的当前输出是XRRenderTarget),改用renderer.xr.getCamera()并更新 XR 相机;否则使用.camera,并用renderer.getDrawingBufferSize()获取实际绘图缓冲尺寸(已含 pixelRatio)。 - 尺寸同步:调用
setSize()让内部渲染目标与绘制缓冲保持一致(含分辨率缩放系数)。 - 状态保存与覆盖:备份渲染器的渲染目标、MRT、autoClear 系列、transparent/opaque、光照、contextNode 以及相机的
layers.mask;然后逐一应用本 Pass 配置(MRT、autoClear、overrideMaterial、layers、contextNode 等)。 - 前帧纹理轮换:遍历
_previousTextures中的每个输出名执行toggleTexture(),实现当前帧/前帧双缓冲(运动矢量等时间型算法依赖此机制)。 - 渲染:设置相机近远平面 uniform,然后调用
renderer.render( scene, camera )将场景渲染进this.renderTarget。 - 状态还原:恢复所有渲染器状态与相机
layers.mask、场景名与 overrideMaterial,确保 Pass 之间互不干扰。
其中 .camera 的 near/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.html 与 src/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.js、GTAONode.js、GodraysNode.js、SSAONode.js、SSRNode.js、OITPassNode.js、StereoPassNode.js、SSAAPassNode.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(深度换算)。
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 StartedRust0629
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