three.js BloomPass 详解:基础泛光(Bloom)后处理通道的参数、原理与实战
BloomPass 是 three.js 提供的一个基础泛光(辉光)后处理通道,通过对场景做一次高斯卷积模糊再与原画面做加法叠加,让高亮区域产生柔和的"光晕溢出"效果。本文以 BloomPass 官方文档 为主线,完整讲解其构造函数参数、属性与方法,并结合 BloomPass.js 源码 剖析两次卷积分离模糊、核权重生成等底层实现,最终给出可复制到 EffectComposer 管线中的实战用法。读完后你将掌握:如何正确导入并配置 BloomPass、strength/kernelSize/sigma 三个参数各自控制什么、以及它与 UnrealBloomPass 的选型区别。
快速上手:在 EffectComposer 中使用 BloomPass
BloomPass 属于 three.js 的 addon(附加模块),需要从 three/addons 路径显式导入,不能从 three 主包直接引入:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
最简用法(与文档示例一致):
const composer = new EffectComposer( renderer );
composer.addPass( new RenderPass( scene, camera ) );
const effectBloom = new BloomPass( 0.75 );
composer.addPass( effectBloom );
composer.addPass( new OutputPass() );
仓库中 webgl_points_dynamic.html 给出了一个完整的管线编排示例,BloomPass 被放在 RenderPass 之后、FilmPass 之前:
const renderModel = new RenderPass( scene, camera );
const effectBloom = new BloomPass( 0.75 );
const effectFilm = new FilmPass();
const outputPass = new OutputPass();
composer = new EffectComposer( renderer );
composer.addPass( renderModel );
composer.addPass( effectBloom );
composer.addPass( effectFilm );
composer.addPass( outputPass );
另一个参考是 webgl_materials_video.html,其中使用了更强调的 new BloomPass( 1.3 ),可对比不同 strength 取值对辉光强度的影响。
构造函数:strength、kernelSize 与 sigma
new BloomPass( strength : number, kernelSize : number, sigma : number )
文档定义的构造签名与默认值如下(对应源码 constructor):
| 参数 | 含义 | 默认值 | 源码作用 |
|---|---|---|---|
strength |
泛光强度 | 1 |
作为 CombineShader 的 strength uniform,决定最终叠加辉光的亮度倍率 |
kernelSize |
卷积核大小(采样点数) | 25 |
写入 shader define KERNEL_SIZE_INT / KERNEL_SIZE_FLOAT,决定每次卷积的采样循环次数 |
sigma |
高斯函数标准差 | 4 |
传给 buildKernel(),控制模糊的"扩散范围";sigma 越大,核尾部权重越分散,模糊越柔和 |
三个参数都是可选的,因此 new BloomPass()、new BloomPass( 0.75 ) 都是合法写法。
源码中的参数落地方式
strength 的赋值在构造函数中直接写入 combine 材质的 uniform:
this.combineUniforms = UniformsUtils.clone( CombineShader.uniforms );
this.combineUniforms[ 'strength' ].value = strength;
kernelSize 则通过 shader 编译期宏传递,注意浮点与整型各写了一份(BloomPass.js#L90-L93):
defines: {
'KERNEL_SIZE_FLOAT': kernelSize.toFixed( 1 ),
'KERNEL_SIZE_INT': kernelSize.toFixed( 0 )
}
sigma 不参与 shader,而是在 CPU 端由 buildKernel() 生成归一化的一维高斯核:
function buildKernel( sigma ) {
// We loop off the sqrt(2 * pi) * sigma term, since we're going to normalize anyway.
const kMaxKernelSize = 25;
let kernelSize = 2 * Math.ceil( sigma * 3.0 ) + 1;
if ( kernelSize > kMaxKernelSize ) kernelSize = kMaxKernelSize;
const halfWidth = ( kernelSize - 1 ) * 0.5;
const values = new Array( kernelSize );
let sum = 0.0;
for ( let i = 0; i < kernelSize; ++ i ) {
values[ i ] = gauss( i - halfWidth, sigma );
sum += values[ i ];
}
// normalize the kernel
for ( let i = 0; i < kernelSize; ++ i ) values[ i ] /= sum;
return values;
}
从源码结构看有两个值得注意的细节:
- 核长度按 sigma 自适应:
2 * ceil( sigma * 3.0 ) + 1意味着 sigma=4 时理论核长为 25,恰好等于上限kMaxKernelSize = 25;若传入更大的 sigma,核长会被钳制在 25 以内。也就是说,单纯调大 sigma 超过 4 并不能显著加宽模糊范围。 - 构造函数的 kernelSize 决定采样次数,sigma 决定核权重形状:
cKerneluniform 存放的是按 sigma 计算好的权重数组(BloomPass.js#L77),而 shader 循环上限由 kernelSize 宏决定(见下文 ConvolutionShader 的实现)。若 kernelSize 小于 buildKernel 生成的数组长度,多余的权重不会参与采样。
内部结构:两块 shader 与两块纹理
.materialConvolution : ShaderMaterial
卷积模糊通道的材质,基于 ConvolutionShader。其片元着色器执行 1D 卷积:
vec2 imageCoord = vUv;
vec4 sum = vec4( 0.0, 0.0, 0.0, 0.0 );
for( int i = 0; i < KERNEL_SIZE_INT; i ++ ) {
sum += texture2D( tDiffuse, imageCoord ) * cKernel[ i ];
imageCoord += uImageIncrement;
}
gl_FragColor = sum;
每帧渲染时 cKernel 固定为 sigma 生成的高斯权重,而采样步进方向 uImageIncrement 在 X/Y 两遍之间切换。BloomPass 在类静态属性中预定义了两个步进向量(BloomPass.js#L236-L237):
BloomPass.blurX = new Vector2( 0.001953125, 0.0 );
BloomPass.blurY = new Vector2( 0.0, 0.001953125 );
0.001953125 即 1/512,是卷积类后处理中常用的 UV 步进常量。从源码结构看,这种"两次 1D 卷积代替一次 2D 卷积"的写法(先横向后纵向)把 25×25=625 次采样降为 25+25=50 次,是该通道保持低成本的关键。
.materialCombine : ShaderMaterial
合成通道的材质,使用文件内定义的 CombineShader,片元逻辑非常简单——把模糊结果乘上 strength 后以加法混合叠加回画面:
uniform float strength;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = strength * texel;
}
材质以 AdditiveBlending 且 transparent: true 创建(BloomPass.js#L54-L63),因此最终效果是"原图 + strength × 模糊图",这也是 BloomPass 只适合做整体辉光而非选择性泛光的原因——它没有亮度阈值(luminance threshold),所有像素都参与辉光叠加。
.combineUniforms / .convolutionUniforms : Object
两个材质对应的 uniform 集合,均为 UniformsUtils.clone() 的克隆结果,可以在运行时动态修改。例如想让辉光随时间脉冲,可以直接改 bloomPass.combineUniforms[ 'strength' ].value,无需重建材质。
内部渲染目标:HalfFloatType 双缓冲
构造函数还创建了两个内部 render target(BloomPass.js#L107-L110):
this._renderTargetX = new WebGLRenderTarget( 1, 1, { type: HalfFloatType, depthBuffer: false } );
this._renderTargetX.texture.name = 'BloomPass.x';
this._renderTargetY = new WebGLRenderTarget( 1, 1, { type: HalfFloatType, depthBuffer: false } );
this._renderTargetY.texture.name = 'BloomPass.y';
_renderTargetX 与 _renderTargetY 分别承接 X/Y 两遍卷积的结果,采用 HalfFloatType 半浮点格式以保留高亮区细节,并且关闭了 depthBuffer(全屏 quad 不需要深度)。初始尺寸为 1×1,由 setSize() 在实际使用时扩展。
.needsSwap : boolean —— 覆写为 false 的含义
文档中 .needsSwap 被标记为"Overwritten to disable the swap",默认值为 false。Pass 基类中 needsSwap 默认为 true,即 EffectComposer 在每帧执行完一个 pass 后会交换 read/write 缓冲:
// EffectComposer.js
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
if ( pass.needsSwap ) { /* 交换读写缓冲 */ }
而 BloomPass 在 render() 末尾是直接写回 readBuffer 的:
// Render original scene with superimposed blur to texture
this._fsQuad.material = this.materialCombine;
this.combineUniforms[ 'tDiffuse' ].value = this._renderTargetY.texture;
renderer.setRenderTarget( readBuffer );
if ( this.clear ) renderer.clear();
this._fsQuad.render( renderer );
也就是说,该通道以"就地加法叠加"的方式把模糊结果加到上一通道的输出上,输出直接落在 readBuffer,若再执行 swap 反而会把结果写进错误的缓冲。needsSwap = false 正是保证"叠加在原画之上"这一语义正确的前提。这个写法也让 BloomPass 天然适合插在任意 RenderPass 之后,不影响后续 pass(如 FilmPass、FocusShader)读取当前帧画面。
.render() 的完整执行流程
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) 的签名与 Pass#render 保持一致,文档参数说明如下:
- renderer:渲染器;
- writeBuffer:通道的写缓冲,通常作为本通道的渲染目标——但如前所述,BloomPass 实际上不用它,直接写 readBuffer;
- readBuffer:上一通道(如 RenderPass)的输出,BloomPass 从中读取原始场景;
- deltaTime:距上帧的秒数,本通道未使用该参数;
- maskActive:是否有 MaskPass 蒙版处于激活状态。
完整流程分三步(BloomPass.js#L127-L164):
- X 向卷积:取
readBuffer.texture作为输入,uImageIncrement = BloomPass.blurX,输出到_renderTargetX; - Y 向卷积:输入换成
_renderTargetX.texture,uImageIncrement = BloomPass.blurY,输出到_renderTargetY; - 加法合成:切换为
materialCombine,把_renderTargetY的模糊结果乘以 strength,以加法混合叠加回readBuffer。
此外还有一处与蒙版系统的配合细节:若 maskActive 为 true,render 开头会 renderer.state.buffers.stencil.setTest( false ) 关闭模板测试,合成前再恢复(BloomPass.js#L129-L158),保证两遍卷积在全屏范围进行,而最终的加法叠加仍受蒙版约束。
.setSize() 与 .dispose()
.setSize( width : number, height : number )
设置通道尺寸,实现非常简单——只是同步两块内部 render target(BloomPass.js#L172-L177):
setSize( width, height ) {
this._renderTargetX.setSize( width, height );
this._renderTargetY.setSize( width, height );
}
通常不需要手动调用:EffectComposer.setSize() 会遍历所有 pass 并传入乘以 pixelRatio 后的有效尺寸(EffectComposer.js#L317-L330)。在 resize 事件中只需调用 composer.setSize( window.innerWidth, window.innerHeight ) 即可。
.dispose()
释放本实例分配的 GPU 资源,文档建议在通道不再使用时调用。实现上会依次 dispose 两块 render target、两个材质以及内部 FullScreenQuad(BloomPass.js#L183-L193):
dispose() {
this._renderTargetX.dispose();
this._renderTargetY.dispose();
this.materialCombine.dispose();
this.materialConvolution.dispose();
this._fsQuad.dispose();
}
选型建议:BloomPass vs UnrealBloomPass
文档开宗明义:UnrealBloomPass 能产生更进阶的泛光,但代价更高。结合源码可以给出一个清晰的对比:
| 维度 | BloomPass | UnrealBloomPass |
|---|---|---|
| 模糊策略 | 两次 1D 高斯卷积(固定 25 点核) | mip map 链 + 多级不同半径模糊加权合成 |
| 亮度阈值 | 无,全像素参与叠加 | 支持 threshold,只有亮部产生辉光 |
| 参数 | strength / kernelSize / sigma |
resolution / strength / radius / threshold |
| 成本 | 低,两次全屏卷积 | 较高,需多张 mip 纹理 |
| 依赖 | 独立 | 需开启 tone mapping 才能正确表现 |
从实现成本看,BloomPass 每帧只做 3 次全屏 quad 绘制(X 卷积、Y 卷积、加法合成),且不产生 mip 链,因此适合对帧率敏感或只需"轻微柔光"的场景;而需要"霓虹灯、阳光过曝"这类阈值型、多级辉光的视觉时,应选用 UnrealBloomPass.js。
小结
- BloomPass 是一个"低成本泛光"通道:sigma 生成高斯核 → X/Y 两次 1D 卷积 → 按 strength 加法叠加回原图;
- 三个构造参数中,
strength控制辉光亮度、sigma控制模糊柔和度(且受 25 点核长钳制)、kernelSize控制卷积采样次数; needsSwap = false+ 直接写回 readBuffer 的设计,使它成为可安全插入管线中段的叠加型通道;- 运行
composer.setSize()会自动同步其内部缓冲尺寸;通道弃用时记得调用dispose()释放 GPU 资源。
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 StartedRust0623
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