three.js TSL:BilateralBlurNode 保边模糊后处理节点完全解析
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
BilateralBlurNode 是 three.js TSL(Three Shading Language)体系中的一个后处理节点,用于在平滑图像的同时保留锐利边缘。本文基于仓库中 BilateralBlurNode 文档 与 完整实现源码,覆盖其导入方式、构造参数、属性与方法的完整 API 说明,并深入源码剖析其"两遍分离 + 空间/颜色双权重"的滤波原理,最后给出在 Godrays 场景中的真实接线示例,帮助你在 WebGPU 后处理管线中直接落地保边模糊。
核心概念:双边模糊为什么能保边
BilateralBlurNode 实现的是双边滤波(Bilateral Filter)思想。与标准高斯模糊"对所有像素一视同仁"不同,双边模糊在采样邻域像素时会分析其强度/颜色与中心像素的差异:如果邻近像素与中心像素差异过大(说明此处存在边缘),该像素就会被排除或大幅降权,从而在平滑噪声的同时保住边缘轮廓。
从源码的 TSL 着色代码(BilateralBlurNode.js 中的setup()方法)可以看到完整的双权重模型:
- 空间权重(spatial weight):按采样距离衰减的高斯系数,由
sigma控制; - 颜色权重(color weight):按亮度差(luminance difference)计算的高斯函数
exp( -0.5 * diff² / sigmaColor² ),由sigmaColor控制; - 最终权重= 空间权重 × 颜色权重,所有加权采样按总权重归一化后输出。
这种"空间 + 颜色"双核设计是双边模糊区别于普通高斯模糊的根本所在,也是该节点在 Godrays(光晕射线步进)等效果中用于抑制噪声伪影的关键。
导入方式
BilateralBlurNode 是一个 addon(附加模块),不在核心包默认导出中,必须显式导入。package.json 中通过"./addons": "./examples/jsm/Addons.js"将three/addons别名映射到examples/jsm目录:
import { bilateralBlur } from 'three/addons/tsl/display/BilateralBlurNode.js';注意导出的是 TSL 函数bilateralBlur,而非类本身。从源码末尾可以看到该函数的定义(BilateralBlurNode.js#L375):
export const bilateralBlur = ( node, directionNode, sigma, sigmaColor ) => new BilateralBlurNode( convertToTexture( node ), directionNode, sigma, sigmaColor );一个容易忽略的细节:TSL 函数入口会用convertToTexture( node )把传入的任意节点先转换为纹理节点。这意味着你可以直接把一个计算节点(如另一个后处理 pass 的输出、或任意vec4类型的 TSL 表达式)作为输入,而不必先手动渲染到渲染目标——这是 TSL 后处理组合能力的体现。
构造函数与参数
const blurPass = new BilateralBlurNode( textureNode, directionNode, sigma, sigmaColor );由于通常通过 TSL 函数构造,更常见的写法是:
const blurPass = bilateralBlur( inputNode ); // 使用全部默认参数各参数说明如下(与文档 BilateralBlurNode.html.md 一致,默认值与源码构造函数 BilateralBlurNode.js#L38 相互印证):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
textureNode | TextureNode | 必填 | 表示效果输入的纹理节点;经 TSL 函数传入时会先被convertToTexture转换 |
directionNode | Node.<(vec2\|float)> | null | 定义模糊的方向和半径。为null时源码中使用vec2( 1 )作为单位方向(见 BilateralBlurNode.js#L237) |
sigma | number | 4 | 控制空间核(spatial kernel)。值越大,模糊半径越宽 |
sigmaColor | number | 0.1 | 控制强度核(intensity kernel)。值越大,允许颜色差异更大的像素被混合到一起;值越小,保边越严格 |
sigma与sigmaColor的配合关系值得注意:sigma决定"看多远",sigmaColor决定"多大的颜色差还算同一侧"。想强模糊但严格保边,可增大sigma同时保持较小的sigmaColor;反之则整体混合更平滑。
属性详解
BilateralBlurNode 继承链为EventDispatcher → Node → TempNode → BilateralBlurNode,其类声明见 BilateralBlurNode.js#L22。可读写属性如下:
.textureNode : TextureNode
效果的输入纹理节点。
.directionNode : Node.<(vec2|float)>
定义模糊的方向和半径。
.sigma : number / .sigmaColor : number
同构造参数,运行期可修改以实时调节模糊强度与保边程度。
.resolutionScale : number
分辨率缩放比例,默认1。它决定了中间渲染目标的实际尺寸:setSize()中会以Math.round( width * resolutionScale )计算宽度/高度(BilateralBlurNode.js#L148-L157)。将其设为0.5之类的值可以在视觉上几乎无损地降低模糊 pass 的开销,是后处理中常见的性能优化手段。
.updateBeforeType : string
默认'frame',即NodeUpdateType.FRAME。含义是:该节点在每帧的主渲染之前执行一次updateBefore()完成模糊渲染,而不是在 TSL 编译期静态生成。源码中显式赋值(BilateralBlurNode.js#L130),覆盖了 TempNode 的默认行为——这与文档页 TempNode 文档 中.updateBeforeType的说明对应。
方法与实现机制
.updateBefore( frame ) —— 每帧的两遍模糊
这是整个效果的核心执行入口。从 BilateralBlurNode.js#L164-L211 的实现看,流程为:
- 通过
RendererUtils.resetRendererState保存当前渲染器状态; - 依据输入纹理的实际宽高调用
setSize(),并同步中间纹理的type; - 水平 pass:将
_passDirection设为(1, 0),渲染一个QuadMesh到_horizontalRT; - 垂直 pass:临时把
textureNode.value指向水平 pass 的输出,将_passDirection设为(0, 1),再渲染到_verticalRT; - 恢复
textureNode.value与渲染器状态。
即一次双边模糊被拆解为水平、垂直两个一维 pass(separable 分解)。这与高斯模糊的分隔化技巧一致:将二维卷积分解为两个一维卷积,把O(k²)的采样量降到O(2k)。源码注释也明确写道"Bilateral blur is applied in two passes (horizontal, vertical)"(BilateralBlurNode.js#L80-L86)。
需要注意的是:严格意义上,颜色权重(基于亮度差的高斯)不可完全分隔化,两遍分离是一种工程近似——它保留了大部分保边效果,同时获得了分隔化模糊的性能收益。
空间系数的计算
_getSpatialCoefficients()(BilateralBlurNode.js#L342-L355)按给定半径生成高斯系数:
const kernelRadius = this.sigma * 2 + 3; // 核半径由 sigma 决定 const sigma = kernelRadius / 3; // 内部 sigma 取半径的 1/3 coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( sigma * sigma ) ) / sigma );注意区分两个 sigma:构造函数参数sigma(默认 4)用于推导核半径sigma * 2 + 3,而系数公式中的内部 sigma 是"半径 / 3"。这意味着默认参数下核半径为 11,即每次采样沿方向各偏移 1~10 个像素。系数在 CPU 端预计算后以float()常量形式进入 TSL 循环。
.setup( builder ) —— TSL 代码生成
setup()被 NodeBuilder 调用,负责把模糊逻辑翻译成 TSL 代码并创建渲染QuadMesh所用的NodeMaterial(BilateralBlurNode.js#L230-L318)。其内部blur()函数的关键片段:
const kernelSize = this.sigma * 2 + 3; const spatialCoefficients = this._getSpatialCoefficients( kernelSize ); const centerColor = sampleTexture( uvNode ); const centerLuminance = luminance( centerColor.rgb ); const colorSigmaFactor = float( -0.5 ).div( float( this.sigmaColor * this.sigmaColor ) ); for ( let i = 1; i < kernelSize; i ++ ) { const uvOffset = vec2( direction.mul( invSize.mul( i ) ) ); // 沿 +/− 两个方向对称采样 const sample1 = sampleTexture( uvNode.add( uvOffset ) ); const sample2 = sampleTexture( uvNode.sub( uvOffset ) ); // 颜色权重:亮度差的高斯 const colorWeight = exp( diff.mul( diff ).mul( colorSigmaFactor ) ); // 双边权重 = 空间权重 × 颜色权重 colorSum.addAssign( sample.mul( spatialWeight.mul( colorWeight ) ) ); weightSum.addAssign( spatialWeight.mul( colorWeight ) ); } return colorSum.div( max( weightSum, 0.0001 ) ); // 权重归一化其中invSize(1/分辨率)由setSize()维护,用于把"像素偏移"换算成 UV 偏移。归一化时以max( weightSum, 0.0001 )兜底,避免边缘处权重和接近零导致的除零问题。
.getTextureNode() / .setSize() / .dispose()
getTextureNode()返回一个PassTextureNode,它绑定_verticalRT.texture(即垂直 pass 的最终输出),这就是接入后续 TSL 链路的"出口"。源码中还有一行this._textureNode.uvNode = textureNode.uvNode(BilateralBlurNode.js#L113),让输出纹理继承输入节点的 UV 变换,保证后续组合时采样坐标一致。setSize( width, height )按resolutionScale缩放后重建两个中间RenderTarget并更新_invSize。注意它在updateBefore()中每帧都会根据输入纹理实际尺寸被调用一次,因此通常无需手动调用。dispose()释放两个中间渲染目标与模糊NodeMaterial(BilateralBlurNode.js#L325-L332)。效果不再使用时应调用,避免 GPU 资源泄漏。
关于 TempNode 继承
BilateralBlurNode 继承自 TempNode。从 TempNode 源码结构看,其build()会在"生成"阶段检测该节点是否被多处引用(hasDependencies()),若是则生成临时变量缓存中间结果,防止同一效果被重复求值。因此bilateralBlur()返回的节点在 TSL 图中被多次使用时不会导致模糊计算重复执行。
实战示例:Godrays 场景中的双边模糊
仓库中的官方示例 webgpu_postprocessing_godrays.html 展示了bilateralBlur的典型用途:GodraysNode 文档页(GodraysNode.html.md)明确建议"计算完 godrays 后,对结果施加 Bilateral Blur 以缓解射线步进与噪声伪影"。示例中的接线方式(webgpu_postprocessing_godrays.html#L138-L161):
import { bilateralBlur } from 'three/addons/tsl/display/BilateralBlurNode.js'; // beauty pass const scenePass = pass( scene, camera ); const scenePassColor = scenePass.getTextureNode( 'output' ); const scenePassDepth = scenePass.getTextureNode( 'depth' ); // godrays const godraysPass = godrays( scenePassDepth, camera, pointLight ); const godraysPassColor = godraysPass.getTextureNode(); // blur:对 godrays 结果做保边模糊,使用全部默认参数 const blurPass = bilateralBlur( godraysPassColor ); const blurPassColor = blurPass.getTextureNode(); // composite:用 depthAwareBlend 把模糊后的光晕合成回场景 const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } ); renderPipeline.outputNode = outputBlurred;这个组合体现了 TSL 后处理的链式特征:每个节点通过getTextureNode()输出纹理节点,直接作为下一个节点的输入;bilateralBlur只需一行即可完成"输入 → 两遍模糊 → 输出纹理"的全过程。示例中还通过 GUI 开关在outputBlurred与未模糊的outputRaw之间切换,方便直观对比保边模糊对光晕噪声的抑制效果。
适用前提:该节点走 WebGPU 后处理管线(示例使用WebGPURenderer与RenderPipeline),与 TSL 文档页 中bilateralBlur的函数签名条目一致。
API 速查
| API | 签名 | 说明 |
|---|---|---|
| TSL 函数 | bilateralBlur( node, directionNode, sigma, sigmaColor ) | 创建节点,输入节点自动convertToTexture |
| 构造函数 | new BilateralBlurNode( textureNode, directionNode = null, sigma = 4, sigmaColor = 0.1 ) | 直接以纹理节点构造 |
.getTextureNode() | : PassTextureNode | 获取效果输出纹理节点 |
.setSize( width, height ) | — | 按resolutionScale设置中间渲染目标尺寸 |
.updateBefore( frame ) | — | 每帧执行水平/垂直两遍模糊 |
.setup( builder ) | : PassTextureNode | 由 NodeBuilder 调用,生成 TSL 模糊代码 |
.dispose() | — | 释放中间 RenderTarget 与材质,不再使用时应调用 |
相关文档与源码:BilateralBlurNode 文档页、实现源码、TempNode 基类、TSL 函数索引、Godrays 实战示例。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考