首页
/ three.js EffectComposer 完全指南:从零搭建后处理管线与自定义 Pass

three.js EffectComposer 完全指南:从零搭建后处理管线与自定义 Pass

2026-09-06 18:32:42作者:幸俭卉

导读

EffectComposer 是 three.js 中实现后期处理(post-processing)的核心类,它把渲染结果拆分成一条按序执行的 Pass 链,用离屏纹理完成"渲染 → 逐级滤镜 → 输出到屏幕"的完整流水线。本文以 docs/pages/EffectComposer.html.md 为主线,结合仓库中 EffectComposer 源码webgl_postprocessing.html 官方示例,系统讲解其双缓冲原理、构造参数、全部属性与方法的实际语义,并给出可运行的最小管线、自定义 ShaderPass 与常见坑位(像素比、颜色空间、释放资源)的实战方案。读完你既能熟练使用内置 Pass 编排后处理链,也能照源码逻辑写出自己的 Pass 类。

适用前提:EffectComposer 及其所属 Pass 体系只支持 WebGLRenderer,不支持 WebGPURenderer(后者对应的后处理方案在 examples 中另有实现)。

three.js 后处理官方示例(点阵 + 色彩偏移等滤镜组合的最终输出)

一、核心思想:一条有序的 Pass 链 + 双缓冲乒乓

官方文档给出的定义即其设计本质:"EffectComposer 管理一条后处理 Pass 链来产出最终画面;Pass 按加入/插入的顺序依次执行;最后一个 Pass 会被自动渲染到屏幕。"

把它拆开对应源码,EffectComposer.js 的 render() 中可以看到这条流水线的真实执行逻辑:

  1. 用内部 Timer 计算 deltaTime(若调用方未传入)。
  2. 记录当前渲染目标,遍历 this.passes跳过 enabled === false 的 Pass
  3. 对每个 Pass,若它是链中最后一个启用状态的 Pass 且全局 renderToScreen === true,则该 Pass 的 renderToScreen 会被临时置为 true,最终画面直接写入默认帧缓冲。
  4. 执行 pass.render( renderer, writeBuffer, readBuffer, deltaTime, maskActive )
  5. 若该 Pass 的 needsSwap === true,则调用 swapBuffers() 交换读写缓冲,使"上一次的输出"成为"下一次的输入"——这就是双缓冲乒乓(ping-pong)渲染。

这条流程解释了为什么绝大多数后处理效果(模糊、泛光、描边、色彩偏移)必须放在场景渲染之后、最终输出之前。

二、最小可运行管线:三层 Pass 的结构范本

文档开头的代码示例是后处理使用的最小骨架,webgl_postprocessing.html 中给出了它被实际使用的完整版本:

// 1. 场景与相机按常规方式创建
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

// 2. 创建 EffectComposer,接管渲染
const composer = new EffectComposer( renderer );

// 3. 第一条 Pass:把 scene+camera 渲染到离屏缓冲(beauty pass)
composer.addPass( new RenderPass( scene, camera ) );

// 4. 中间 N 条滤镜 Pass,例如点阵滤镜 + 色彩偏移
const effect1 = new ShaderPass( DotScreenShader );
effect1.uniforms[ 'scale' ].value = 4;
composer.addPass( effect1 );

const effect2 = new ShaderPass( RGBShiftShader );
effect2.uniforms[ 'amount' ].value = 0.0015;
composer.addPass( effect2 );

// 5. 收尾 Pass:负责色调映射与颜色空间转换
const outputPass = new OutputPass();
composer.addPass( outputPass );

// 6. 动画循环中不再调用 renderer.render(),改用 composer.render()
function animate() {
	composer.render();
}

结构上通常固定为三层:开头一个 RenderPass 负责把真实场景画进缓冲;中间多个滤镜类 Pass(如 GlitchPassShaderPass、[UnrealBloomPass]);结尾一个 OutputPass 处理色彩输出。文档中 GlitchPass 示例链正是这一范式的直接体现。

OutputPass 并非可有可无:当渲染链经过中间缓冲后,three.js 默认的直接渲染路径会被绕过,色调映射(tone mapping)与 sRGB 颜色空间转换都需要由它显式完成。从 OutputPass 源码 可以看到,它每次渲染会从 renderer 读取 toneMappingExposureoutputColorSpacetoneMapping 并按需重建着色器宏定义(SRGB_TRANSFER、ACES_FILMIC_TONE_MAPPING、AGX_TONE_MAPPING 等)。如果你的 Pass 链需要 sRGB 输入(例如 FXAA 这类依赖 gamma 空间的算法),它还提供"OutputPass 之后可以再接其他 Pass"的灵活性。

浏览器端模块化加载

EffectComposer 属于 addon,必须显式导入。借助 import map 可以将裸模块名映射到本地文件:

<script type="importmap">
	{
		"imports": {
			"three": "../build/three.module.js",
			"three/addons/": "./jsm/"
		}
	}
</script>
<script type="module">
	import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
	import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
	import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
	import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
</script>

示例实际运行效果截图(同一场景叠加点阵与 RGB 偏移滤镜后)如下:

叠加 DotScreen 与 RGBShift 后的线条化输出效果

上图是泛光(UnrealBloom)这类"中间滤镜"链路的典型结果,说明在 RenderPass 之后插入不同效果的 Pass,最终画风会截然不同。

三、构造器与双缓冲初始化

new EffectComposer( renderer, renderTarget )
参数 类型 含义 可省略
renderer WebGLRenderer 渲染器实例,Composer 的所有绘制都委托给它 必填
renderTarget WebGLRenderTarget 与它的一个克隆体共同作为内部读写缓冲;缺省时自动创建 可选

对照 构造器源码 可以看到几处关键行为:

  • 像素比对齐this._pixelRatio = renderer.getPixelRatio(),自动继承渲染器当前的 device pixel ratio。
  • 缓冲自动创建:当未传入 renderTarget 时,用 renderer.getSize() 拿到逻辑尺寸,创建 WebGLRenderTarget( width * pixelRatio, height * pixelRatio, { type: HalfFloatType } )注意它默认使用 HalfFloatType(半精度浮点纹理),这是为了在 HDR 中间缓冲中保留颜色精度,避免多次滤镜迭代后的精度损失;自定义 renderTarget 时也建议选用该类型。
  • 双缓冲命名renderTarget1(名为 EffectComposer.rt1)与 renderTarget2(克隆自前者,名为 EffectComposer.rt2)。其中 writeBuffer 指向 rt1readBuffer 指向 rt2
  • 内部 CopyPass:Composer 自带一个 new ShaderPass( CopyShader ) 拷贝 Pass,并设置 material.blending = NoBlending源码位置),供遮罩(mask)状态下拷贝结果使用。
  • 内部计时器:内置 Timer,用于渲染循环中自行推算 deltaTime(源码位置)。

多 Composer 与自定义缓冲的典型场景参见 webgl_postprocessing_advanced.html,其中用 new EffectComposer( renderer, new THREE.WebGLRenderTarget( rtWidth, rtHeight, rtParameters ) ) 同时创建了多个独立的后处理链。

四、属性详解

.passes : Array<Pass>

以数组形式保存有序的 Pass 链。EffectComposer 遍历该数组执行渲染,因此数组顺序即视觉效果的作用顺序。推荐通过 addPass / insertPass / removePass 维护,避免直接操作数组导致内部尺寸失配。

.readBuffer : WebGLRenderTarget

只读缓冲引用。链中的 Pass 通常把"上一个 Pass 的渲染结果"作为输入纹理从这里读取(例如 ShaderPass 会执行 this.uniforms[ 'tDiffuse' ].value = readBuffer.texture)。在内部每轮渲染结束后会被 swapBuffers() 切换。

.writeBuffer : WebGLRenderTarget

写入缓冲引用,Pass 渲染时的目标(renderer.setRenderTarget( writeBuffer ))。它与 readBuffer 构成乒乓双缓冲。

.renderToScreen : boolean

是否把最终 Pass 渲染到屏幕(默认帧缓冲),默认值 true。由 render() 内部判断:仅当全局 renderToScreen 为真、且某 Pass 是最后一个启用 Pass 时,该 Pass 的 renderToScreen 才生效(对应 render() 实现)。

.renderer : WebGLRenderer

构造时传入的渲染器引用,所有实际绘制工作都通过它完成。

补充阅读:Pass 基类定义了每个 Pass 共享的四个状态位——enabled(是否参与渲染,默认 true)、needsSwap(渲染后是否交换缓冲,默认 true)、clear(渲染前是否清屏,默认 false)、renderToScreen(是否直接上屏,默认 false)。其中 Composer 会自动把最后一个启用 Pass 的 renderToScreen 置 true,无需手动设置。这两个文件配合阅读即可理解后处理链的全部状态模型。

五、方法全解

.addPass( pass : Pass )

把 Pass 追加到链尾。源码中除了 this.passes.push( pass ),还会立即用当前有效尺寸(逻辑尺寸 × 像素比)调用 pass.setSize()实现)。因此在 addPass 之后再调用 setPixelRatio/setSize 时,所有 Pass 都会被统一通知尺寸变化

.insertPass( pass : Pass, index : number )

在指定下标处插入 Pass:this.passes.splice( index, 0, pass ) 后同样立即 setSize实现)。index 是 Pass 链中的位置下标,0 表示链首。适合在"已经渲染完场景"的固定位置动态插入某个临时滤镜(例如命中高亮、受伤闪红)。

.removePass( pass : Pass )

按引用从链中移除指定 Pass;未找到(index 为 -1)时静默不做任何操作(实现)。

.isLastEnabledPass( passIndex : number ) : boolean

判断给定下标对应的 Pass 是否为链中最后一个 enabled 为 true 的 Pass(实现)。由于渲染循环会跳过 disabled Pass,"是否最后一个"必须按启用状态而非数组位置判断,该方法正是为此设计。一般由 Composer 内部调用,无需手动干预。

.render( deltaTime : number )

执行全部启用 Pass,产出最终一帧。deltaTime 单位是秒,可省略——省略时 Composer 用内置 Timer 自动计算(this.timer.getDelta())。渲染循环里每个动画帧调用一次即可;传 deltaTime 的场景通常是效果依赖时间(如 Glitch、Film 噪点),且你希望外部统一管理时钟。

.reset( renderTarget : WebGLRenderTarget )

重置内部状态,可传新的 renderTarget 重建双缓冲(实现)。调用时先 dispose() 旧的两个 renderTarget,再按新目标重建 rt1/rt2 与 read/writeBuffer。缺省参数时会从 renderer 读取当前尺寸与像素比、克隆 rt1 重建。适合画布尺寸结构性变化或复用 Composer 对象时的场景。

.setPixelRatio( pixelRatio : number )

设置设备像素比,通常用于 HiDPI(高分屏)设备防止输出模糊。设置后会以新的像素比自动调用 setSize( this._width, this._height ) 缩放内部缓冲与所有 Pass(实现)。请注意这是 Composer 自身维护的一份像素比(构造时取自 renderer.getPixelRatio()),渲染器像素比变化时需要同步调用。

.setSize( width : number, height : number )

逻辑像素为单位调整内部读写缓冲与所有 Pass 的尺寸,行为类似 WebGLRenderer#setSize——即会乘以当前像素比得到实际纹理尺寸:effectiveWidth = width * _pixelRatio实现)。窗口 resize 时必须同时调用 renderer.setSize 与 composer.setSize,官方示例的写法是:

function onWindowResize() {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize( window.innerWidth, window.innerHeight );
	composer.setSize( window.innerWidth, window.innerHeight );
}

如果漏掉 composer.setSize,画面内容会持续渲染到旧尺寸的缓冲上,出现拉伸或裁剪。

.swapBuffers()

交换 read/write 缓冲引用(实现),本质只是互换两个指针,开销极小。通常由渲染循环在 needsSwap 的 Pass 之后自动调用。

.dispose()

释放 GPU 相关资源:renderTarget1.dispose()renderTarget2.dispose() 以及内部 copyPass.dispose()实现)。官方文档特别强调:当 Composer 不再使用时必须调用,否则纹理等 GPU 对象不会被正确回收。页面销毁、切换场景或重建 Composer 前都应调用。

六、自定义效果:从 ShaderPass 到自有 Pass

方式一:ShaderPass + 现成 Shader(最快路径)

若要快速套用一个效果,直接用 ShaderPass 包装已有的着色器对象即可(上面最小管线的 DotScreen/RGBShift 就是这种用法)。ShaderPass 源码 显示它接受一个含 uniforms/vertexShader/fragmentShader/defines 的 shader 对象(或直接传 ShaderMaterial),并把上一 Pass 的结果自动绑定到名为 tDiffuse 的纹理 uniform 上;渲染到屏幕时 setRenderTarget( null ),否则 setRenderTarget( writeBuffer )实现)。

常用 uniform 参数调整示例:

const effect = new ShaderPass( RGBShiftShader );
effect.uniforms[ 'amount' ].value = 0.0015;  // 偏移量越大,红/蓝分色越明显
composer.addPass( effect );

方式二:继承 Pass 编写自有类(完全可控)

所有内置 Pass(RenderPass、ShaderPass、OutputPass、MaskPass 等)都继承自 examples/jsm/postprocessing/Pass.js 中的抽象基类 Pass。自定义 Pass 只需实现两个方法:

import { Pass, FullScreenQuad } from 'three/addons/postprocessing/Pass.js';

class MyEffectPass extends Pass {

	constructor() {
		super();
		this._fsQuad = new FullScreenQuad( myMaterial ); // 全屏四边形网格
	}

	setSize( width, height ) { /* 更新分辨率相关 uniform(可选实现) */ }

	render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
		if ( this.uniforms && this.uniforms.tDiffuse ) {
			this.uniforms.tDiffuse.value = readBuffer.texture; // 读取上一 Pass 结果
		}
		if ( this.renderToScreen ) {
			renderer.setRenderTarget( null );   // 最后一帧输出到屏幕
		} else {
			renderer.setRenderTarget( writeBuffer ); // 中间帧写入缓冲
		}
		this._fsQuad.render( renderer );
	}

	dispose() {
		this.material.dispose();
		this._fsQuad.dispose();
	}
}

实现原理:绝大多数后处理效果本质都是"用一张全屏三角形渲染当前纹理"。Pass.js 中的 FullScreenQuad 内置了共享的 FullscreenTriangleGeometry(一个覆盖视口的超大三角形,见 Pass.js#L110-L121)与正交相机,只需不断替换它的 material 即可完成一次全屏滤镜,这是官方多个 Pass 复用同一几何体、内存开销极小的关键设计。

方式三:结合 RenderPass 的进阶参数

RenderPass 额外支持四个可选参数:

new RenderPass( scene, camera, overrideMaterial, clearColor, clearAlpha )
  • overrideMaterial:设置后场景中所有物体强制使用该材质(常用于色块化/白模渲染)。
  • clearColor / clearAlpha:覆盖渲染器的清屏颜色与透明度。
  • 其内部 clear 默认 trueneedsSwap 默认 false(它渲染真实场景而非读写缓冲,无需交换,见 RenderPass 构造器),并会自动暂存/还原 renderer.autoClear、清屏色、scene.overrideMaterialrender 实现)。

七、进阶编排技巧与资源出处

动态启用/禁用效果:修改 Pass 的 enabled 属性即可在运行时开关滤镜,render() 会自动跳过 disabled Pass 并把"最后一个启用 Pass"正确切换到上一张图,无需重建链:

const glitchPass = new GlitchPass();
composer.addPass( glitchPass );
// ... 需要时
glitchPass.enabled = false; // 下一帧起该效果不再生效

遮罩协同:链中若插入 MaskPass / ClearMaskPassrender() 会维护 maskActive 状态,并在需要交换缓冲时用内部 CopyPass 配合模板测试(stencil)完成带遮罩的拷贝,实现"只对屏幕局部区域生效"的滤镜(渲染循环中遮罩逻辑)。

更多官方用例:仓库 examples 下的 webgl_postprocessing_*.html 系列 提供了 20+ 个可直接对照阅读的完整管线,覆盖泛光(unreal_bloom)、景深(dof)、抗锯齿(smaa/fxaa)、光晕(godrays)、遮罩(masking)、SSAO/SSR 等效果,是理解"什么效果应该放在链的哪个位置"的最佳实操素材。完整 Pass 清单见 examples/jsm/postprocessing 目录

查看源码:Composer 本体约 360 行,直接阅读 examples/jsm/postprocessing/EffectComposer.js 全文件,配合本仓库 Pass.jsRenderPass.jsOutputPass.js 与官方文档页 RenderPass 说明ShaderPass 说明OutputPass 说明Pass 说明WebGLRenderTarget 说明,即可把整个后处理体系的每一环都落到具体实现上。

结语

掌握 EffectComposer 的核心在于记住三件事:Pass 是有序链(先 RenderPass 渲染场景,中间任意滤镜,最后 OutputPass 收尾上屏);双缓冲是乒乓的(读写缓冲随每次 needsSwap 交换,链上每个 Pass 的输入都是上一个 Pass 的输出);尺寸与像素比要同步维护composer.setSize / composer.setPixelRatio 必须与渲染器保持一致,dispose 记得清理 GPU 资源)。基于这套模型,你既可以组合 examples/jsm/postprocessing 里十余个现成 Pass 快速搭建风格化输出,也能通过继承 Pass 写出任意自定义的全屏特效。

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