three.js TSL BloomNode:基于 WebGPU 渲染管线的泛光(Bloom)后处理节点详解
BloomNode 是 three.js TSL(Three Shading Language)生态中用于在 WebGPU 渲染管线上实现泛光效果的后处理节点,继承自 EventDispatcher → Node → TempNode 这条类型链。它通过“亮度提取 + 多级可分离高斯模糊 + 复合叠加”三阶段流水线,将场景中的高亮区域扩散为柔和光晕。读完本文,你将掌握 bloom() TSL 函数的完整参数体系、选择性泛光的 MRT 实现方式,以及从源码层面理解其模糊核、Mip 链与分辨率缩放机制,并能直接在 webgpu_postprocessing_bloom.html 等官方示例上动手复现。
导入与基本用法
BloomNode 是一个 addon 模块,需要显式导入(见仓库 BloomNode.js 文件头部的 @three_import 注解):
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
最简用法是把场景 Pass 的输出纹理节点交给 bloom(),再把原色与泛光结果相加作为 RenderPipeline 的输出节点:
const renderPipeline = new THREE.RenderPipeline( renderer );
const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode( 'output' );
const bloomPass = bloom( scenePassColor );
renderPipeline.outputNode = scenePassColor.add( bloomPass );
bloom() 是一个 TSL 工厂函数,内部执行 new BloomNode( nodeObject( node ), strength, radius, threshold )(见 BloomNode.js#L595),因此第一个参数既可以是普通节点,也可以先经过 nodeObject() 包装。
官方示例 webgpu_postprocessing_bloom.html 在此基础之上做了完整的工程化演示:为场景 Pass 配置了 MSAA 解析优化(resolveColorBuffer: true,不存储多重采样缓冲),加载 GLTF 动画模型,并通过 Inspector 参数面板实时调节 bloomPass.threshold.value、bloomPass.strength.value、bloomPass.radius.value 三个 Uniform 值——这正好印证了这三个属性在运行时是可直接改写的 UniformNode<float>。
选择性泛光:基于 MRT 的 emissive 通道方案
默认情况下 BloomNode 作用于整帧图像。若要只让部分对象产生泛光,官方推荐的方案是利用 emissive 材质属性 + 多渲染目标(MRT)单独提取自发光通道:
const renderPipeline = new THREE.RenderPipeline( renderer );
const scenePass = pass( scene, camera );
scenePass.setMRT( mrt( {
output,
emissive
} ) );
const scenePassColor = scenePass.getTextureNode( 'output' );
const emissivePass = scenePass.getTextureNode( 'emissive' );
const bloomPass = bloom( emissivePass );
renderPipeline.outputNode = scenePassColor.add( bloomPass );
示例 webgpu_postprocessing_bloom_emissive.html 展示了这套方案的完整形态,其中有几个值得注意的细节:
- MRT 的第二通道写作
emissive: vec4( emissive, output.a ),即把emissiveTSL 节点(内置变量,暴露材质自发光)写入独立的附加纹理; - 对附加纹理设置
emissiveTexture.type = THREE.UnsignedByteType以“优化带宽”——自发光通道不需要浮点精度,降为字节型可减少显存带宽; - 泛光参数取
bloom( emissivePass, 2.5, .5 ),即 strength 2.5、radius 0.5。
除了 emissive 方案,示例 webgpu_postprocessing_bloom_selective.html 还演示了逐对象控制:通过 material.mrtNode = mrt( { bloomIntensity: uniform( bloomIntensity ) } ) 为每个网格写入一个 0/1 的强度通道,点击物体时切换该 uniform,最终用 bloom( outputPass.mul( bloomIntensityPass ) ) 实现“点谁谁发光”的交互效果。
构造函数参数
new BloomNode( inputNode, strength, radius, threshold )(见 BloomNode.js#L70-L100):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| inputNode | Node<vec4> | 必填 | 效果输入节点,通常是场景 Pass 的纹理节点 |
| strength | number | Node<float> | 1 |
泛光强度,最后乘在复合结果上 |
| radius | number | Node<float> | 0 |
泛光半径,必须在 [0,1] 范围内 |
| threshold | number | Node<float> | 0 |
亮度阈值,限制哪些高亮区域参与泛光 |
构造函数中 strength.isNode ? strength : uniform( strength ) 这一写法意味着三个参数既可以传普通数值(自动包装为 uniform),也可以直接传 Node,便于做程序化驱动。此外,构造函数还会创建:
smoothWidth:默认uniform( 0.01 ),用于调节亮度提取的平滑过渡宽度;bloomTintColors:长度为 5 的Vector3数组(每级 Mip 一个着色,默认全白),文档注释明确说明可将其改为暖色或用于“anamorphic 风格”的着色;_resolutionScale:内部私有属性,默认0.5,见下文。
内部渲染流水线(源码剖析)
BloomNode 的核心逻辑在 updateBefore(frame) 中,每帧执行一次完整的渲染序列(见 BloomNode.js#L348-L401),分三步:
1. 亮度提取(High Pass)
_renderTargetBright(HalfFloat 类型、无深度缓冲)作为目标,渲染一个以 highPassFn 为片元节点的 NodeMaterial 全屏四边形。默认的高通滤波函数为:
const luminosityHighPass = Fn( ( { input, threshold, smoothWidth } ) => {
const v = luminance( input.rgb );
const alpha = smoothstep( threshold, threshold.add( smoothWidth ), v );
return mix( vec4( 0 ), input, alpha );
} );
即:计算输入亮度 v,用 smoothstep(threshold, threshold + smoothWidth, v) 生成 0~1 的掩膜 alpha,再线性混合出“只保留高亮区域”的纹理。threshold 决定哪些区域被提取,smoothWidth 决定过渡的柔和程度。
2. 多级可分离高斯模糊
源码维护 _nMips = 5 组水平/垂直 RenderTarget,每级模糊核尺寸为 kernelSizeArray = [ 6, 10, 14, 18, 22 ](见 BloomNode.js#L425,注释指出这些尺寸是为避免块状伪影而调整过的系数)。每一级先沿 X 方向(_BlurDirectionX = (1,0))再沿 Y 方向((0,1))各渲染一次,输出链式传递为下一级的输入。模糊核系数按高斯分布 0.39894 * exp(-0.5 * i²/σ²) / σ(σ = kernelRadius / 3)生成,并且源码将相邻采样点合并为单次双线性采样(bilinear fetch),显著减少纹理读取次数。setSize() 中每级尺寸逐次减半(resx = Math.floor( resx / 2 )),形成完整的 Mip 链。
3. 复合叠加(Composite)
五路模糊结果按固定权重 bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ] 加权求和,最后乘以 strength:
const lerpBloomFactor = Fn( ( { factor, radius } ) => {
const mirrorFactor = float( 1.2 ).sub( factor );
return mix( factor, mirrorFactor, radius );
}, { factor: 'float', radius: 'float', return: 'float' } );
lerpBloomFactor 的妙处在于:radius = 0 时各 Mip 权重就是 bloomFactors 本身(细节保留更多);radius = 1 时权重变为 1.2 - factor,即 [ 0.2, 0.4, 0.6, 0.8, 1.0 ]——低分辨率(大范围)模糊的权重被放大,视觉上泛光扩散得更远更柔。每一路还会乘上对应的 bloomTintColors 颜色,这就是按 Mip 级染色的实现点。最终合成结果写回 _renderTargetsHorizontal[0],并通过 passTexture() 暴露为 _textureOutput。
关键属性与方法
以下属性与方法均可在 BloomNode.js 源码中一一对应:
属性
.inputNode : Node<vec4>— 效果输入节点;.strength : UniformNode<float>— 泛光强度,运行时可改.value;.radius : UniformNode<float>— 泛光半径,取值范围[0,1],作用见上文lerpBloomFactor;.threshold : UniformNode<float>— 亮度阈值;.smoothWidth : UniformNode<float>— 默认0.01,调节从场景中提取亮度的平滑度;.highPassFn : function— 可注入自定义高通滤波器的入口,这是做特殊泛光形态(如水平拉伸的变形宽银幕泛光)的官方扩展点;.updateBeforeType : string— 默认'frame'(NodeUpdateType.FRAME),覆盖自 TempNode,表明效果在updateBefore()中每帧渲染一次;.bloomTintColors : Array<Vector3>— 每级 Mip 的染色(源码中的补充属性,文档 API 未单列)。
方法
.getTextureNode() : PassTextureNode— 返回_textureOutput,即复合后的泛光纹理节点,用于接入outputNode表达式;.setResolutionScale( resolutionScale ) : BloomNode— 设置分辨率缩放因子,该值会乘上渲染器的宽高。1表示全分辨率,源码默认值为0.5,意味着模糊金字塔以半分辨率起步,这是控制性能开销的主要旋钮;.getResolutionScale() : number— 读取当前分辨率缩放;.setSize( width, height )— 按_resolutionScale计算并重建全部渲染目标尺寸、更新各级模糊的invSizeuniform。此方法由updateBefore()每帧根据renderer.getDrawingBufferSize()自动调用,窗口尺寸变化时无需手动处理;.setup( builder ) : PassTextureNode— 被 NodeBuilder 调用,构建高通、五组可分离模糊与复合三个NodeMaterial的 TSL 代码;.updateBefore( frame )— 每帧执行“提取 → 模糊 → 复合”三步渲染,并负责保存/恢复渲染器状态(RendererUtils.resetRendererState/restoreRendererState);.dispose()— 释放 11 个渲染目标(1 个 bright + 5 组 H/V)及全部NodeMaterial,效果不再需要时必须调用。
实战技巧:自定义 highPassFn 与分辨率调优
仓库示例 webgpu_postprocessing_anamorphic.html 完整演示了两个高级技巧的组合:
const bloomPass = bloom( scenePass.getTextureNode(), intensity, radius, threshold );
bloomPass.setResolutionScale( 0.25 );
// 自定义高通滤波器实现 anamorphic 效果
bloomPass.highPassFn = Fn( ( { input, threshold, smoothWidth } ) => {
const v = luminance( input.rgb );
const alpha = smoothstep( threshold, threshold.add( smoothWidth ), v );
const brightPass = rtt( mix( vec4( 0 ), input, alpha ), null, null, {
wrapS: THREE.MirroredRepeatWrapping,
wrapT: THREE.MirroredRepeatWrapping
} );
// 在水平方向做 80 次偏移采样并衰减加权,得到横向拉伸的光条
// ...
return total.div( samples.div( 3.0 ) );
} );
要点有二:一是 setResolutionScale( 0.25 ) 把整条模糊金字塔压缩到 1/4 分辨率,配合后续合成可显著降低开销;二是 highPassFn 返回的仍是一个 TSL 节点(示例里内部还嵌了一层 rtt() 渲染目标纹理 + 水平偏移采样循环),说明这个钩子的自由度很高——你可以在提取阶段做任何与 UV、时间、方向相关的处理,而不必改动 BloomNode 本身的模糊与合成逻辑。
调参方面,官方示例给出的面板范围可作为经验参考(见 webgpu_postprocessing_bloom.html 的 Inspector 参数配置):threshold 0~1、strength 0~3、radius 0~1。由于 bloom 结果与场景色相加后还要经过色调映射,示例同时暴露了 toneMappingExposure(Math.pow( value, 4.0 ) 指数化)以便配合调整整体曝光。
小结与延伸阅读
BloomNode 的设计体现了 TSL 后处理节点的一贯范式:setup() 只负责声明 TSL 代码与材质,updateBefore() 负责每帧命令式的多 Pass 渲染,结果经 PassTextureNode 回注节点图。使用它时记住三个关键控制面即可:
- threshold + smoothWidth 决定“哪些区域参与泛光”;
- radius + strength + bloomTintColors 决定“泛光长什么样”;
- setResolutionScale() 决定“花多少性能预算”。
结合 MRT 的 emissive 通道或自定义 bloomIntensity 通道,即可覆盖从全图泛光到逐对象选择性泛光的全部常见需求。更多相关实现可参考:BloomNode 源码、基础泛光示例、自发光选择性泛光示例、逐对象选择性泛光示例、Anamorphic 泛光示例。
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 StartedRust0627
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