首页
/ three.js TSL BloomNode:基于 WebGPU 渲染管线的泛光(Bloom)后处理节点详解

three.js TSL BloomNode:基于 WebGPU 渲染管线的泛光(Bloom)后处理节点详解

2026-09-06 09:15:22作者:蔡丛锟

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.valuebloomPass.strength.valuebloomPass.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 ),即把 emissive TSL 节点(内置变量,暴露材质自发光)写入独立的附加纹理;
  • 对附加纹理设置 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 计算并重建全部渲染目标尺寸、更新各级模糊的 invSize uniform。此方法由 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 结果与场景色相加后还要经过色调映射,示例同时暴露了 toneMappingExposureMath.pow( value, 4.0 ) 指数化)以便配合调整整体曝光。

小结与延伸阅读

BloomNode 的设计体现了 TSL 后处理节点的一贯范式:setup() 只负责声明 TSL 代码与材质,updateBefore() 负责每帧命令式的多 Pass 渲染,结果经 PassTextureNode 回注节点图。使用它时记住三个关键控制面即可:

  1. threshold + smoothWidth 决定“哪些区域参与泛光”;
  2. radius + strength + bloomTintColors 决定“泛光长什么样”;
  3. setResolutionScale() 决定“花多少性能预算”。

结合 MRT 的 emissive 通道或自定义 bloomIntensity 通道,即可覆盖从全图泛光到逐对象选择性泛光的全部常见需求。更多相关实现可参考:BloomNode 源码基础泛光示例自发光选择性泛光示例逐对象选择性泛光示例Anamorphic 泛光示例

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