three.js TSL:BilateralBlurNode 保边模糊后处理节点完全解析
2026/9/7 4:16:17 网站建设 项目流程

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 相互印证):

参数类型默认值说明
textureNodeTextureNode必填表示效果输入的纹理节点;经 TSL 函数传入时会先被convertToTexture转换
directionNodeNode.<(vec2\|float)>null定义模糊的方向和半径。为null时源码中使用vec2( 1 )作为单位方向(见 BilateralBlurNode.js#L237)
sigmanumber4控制空间核(spatial kernel)。值越大,模糊半径越宽
sigmaColornumber0.1控制强度核(intensity kernel)。值越大,允许颜色差异更大的像素被混合到一起;值越小,保边越严格

sigmasigmaColor的配合关系值得注意: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 的实现看,流程为:

  1. 通过RendererUtils.resetRendererState保存当前渲染器状态;
  2. 依据输入纹理的实际宽高调用setSize(),并同步中间纹理的type
  3. 水平 pass:将_passDirection设为(1, 0),渲染一个QuadMesh_horizontalRT
  4. 垂直 pass:临时把textureNode.value指向水平 pass 的输出,将_passDirection设为(0, 1),再渲染到_verticalRT
  5. 恢复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 后处理管线(示例使用WebGPURendererRenderPipeline),与 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询