three.js EffectComposer 完全指南:从零搭建后处理管线与自定义 Pass
导读
EffectComposer 是 three.js 中实现后期处理(post-processing)的核心类,它把渲染结果拆分成一条按序执行的 Pass 链,用离屏纹理完成"渲染 → 逐级滤镜 → 输出到屏幕"的完整流水线。本文以 docs/pages/EffectComposer.html.md 为主线,结合仓库中 EffectComposer 源码 与 webgl_postprocessing.html 官方示例,系统讲解其双缓冲原理、构造参数、全部属性与方法的实际语义,并给出可运行的最小管线、自定义 ShaderPass 与常见坑位(像素比、颜色空间、释放资源)的实战方案。读完你既能熟练使用内置 Pass 编排后处理链,也能照源码逻辑写出自己的 Pass 类。
适用前提:
EffectComposer及其所属 Pass 体系只支持 WebGLRenderer,不支持 WebGPURenderer(后者对应的后处理方案在 examples 中另有实现)。
一、核心思想:一条有序的 Pass 链 + 双缓冲乒乓
官方文档给出的定义即其设计本质:"EffectComposer 管理一条后处理 Pass 链来产出最终画面;Pass 按加入/插入的顺序依次执行;最后一个 Pass 会被自动渲染到屏幕。"
把它拆开对应源码,EffectComposer.js 的 render() 中可以看到这条流水线的真实执行逻辑:
- 用内部
Timer计算 deltaTime(若调用方未传入)。 - 记录当前渲染目标,遍历
this.passes,跳过enabled === false的 Pass。 - 对每个 Pass,若它是链中最后一个启用状态的 Pass 且全局
renderToScreen === true,则该 Pass 的renderToScreen会被临时置为true,最终画面直接写入默认帧缓冲。 - 执行
pass.render( renderer, writeBuffer, readBuffer, deltaTime, maskActive )。 - 若该 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(如 GlitchPass、ShaderPass、[UnrealBloomPass]);结尾一个 OutputPass 处理色彩输出。文档中 GlitchPass 示例链正是这一范式的直接体现。
OutputPass 并非可有可无:当渲染链经过中间缓冲后,three.js 默认的直接渲染路径会被绕过,色调映射(tone mapping)与 sRGB 颜色空间转换都需要由它显式完成。从 OutputPass 源码 可以看到,它每次渲染会从 renderer 读取 toneMappingExposure、outputColorSpace、toneMapping 并按需重建着色器宏定义(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 偏移滤镜后)如下:
上图是泛光(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指向rt1,readBuffer指向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默认true、needsSwap默认false(它渲染真实场景而非读写缓冲,无需交换,见 RenderPass 构造器),并会自动暂存/还原renderer.autoClear、清屏色、scene.overrideMaterial(render 实现)。
七、进阶编排技巧与资源出处
动态启用/禁用效果:修改 Pass 的 enabled 属性即可在运行时开关滤镜,render() 会自动跳过 disabled Pass 并把"最后一个启用 Pass"正确切换到上一张图,无需重建链:
const glitchPass = new GlitchPass();
composer.addPass( glitchPass );
// ... 需要时
glitchPass.enabled = false; // 下一帧起该效果不再生效
遮罩协同:链中若插入 MaskPass / ClearMaskPass,render() 会维护 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.js、RenderPass.js、OutputPass.js 与官方文档页 RenderPass 说明、ShaderPass 说明、OutputPass 说明、Pass 说明、WebGLRenderTarget 说明,即可把整个后处理体系的每一环都落到具体实现上。
结语
掌握 EffectComposer 的核心在于记住三件事:Pass 是有序链(先 RenderPass 渲染场景,中间任意滤镜,最后 OutputPass 收尾上屏);双缓冲是乒乓的(读写缓冲随每次 needsSwap 交换,链上每个 Pass 的输入都是上一个 Pass 的输出);尺寸与像素比要同步维护(composer.setSize / composer.setPixelRatio 必须与渲染器保持一致,dispose 记得清理 GPU 资源)。基于这套模型,你既可以组合 examples/jsm/postprocessing 里十余个现成 Pass 快速搭建风格化输出,也能通过继承 Pass 写出任意自定义的全屏特效。
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

