three.js 后期处理中的 SMAA 抗锯齿:SMAAPass 完整使用与源码解析
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
SMAAPass 是 three.js 后期处理(post-processing)体系中的**亚像素形态学抗锯齿(Subpixel Morphological Antialiasing,SMAA)**通道,基于 WebGL 移植的 SMAA v2.8(1x Medium 预设、颜色边缘检测)实现。本文将围绕官方文档 SMAAPass 参考页,讲解其在 EffectComposer 中的接入方式、色彩空间约束(必须在 OutputPass 之前执行)、构造函数与三个核心方法,并结合 SMAAPass.js、SMAAShader.js 与 webgl_postprocessing_smaa 示例 源码,深入剖析其内部三阶段渲染管线。读完本文你将能够正确地把 SMAA 集成进自己的 three.js 渲染链,并理解它与 FXAA 的取舍、为何要保持线性色彩空间,以及三个 GPU 中间缓冲的作用。
SMAAPass 与 FXAAPass:在后期处理链中的定位差异
SMAA(Subpixel Morphological Antialiasing)是一种基于图像形态学分析的边缘抗锯齿算法,它不依赖额外的几何信息,而是通过检测图像中的边缘图案(pattern)计算混合权重,再对边缘像素做加权混合。相比常见的快速近似抗锯齿 FXAA,SMAA 在视觉上通常能保留更多细节、锯齿消除更干净,代价是更高的计算开销。
官方文档给出了一个关键区别:与 FXAAPass 不同,SMAAPass在linear-srgb(线性色彩空间)中工作,因此必须被放置在 OutputPass 之前执行。
要理解这条约束,需要先看 OutputPass.js 的类注释:OutputPass 负责在渲染链末尾加入色调映射(tone mapping)与色彩空间转换(sRGB transfer),它会从渲染器中提取renderer.toneMapping、renderer.toneMappingExposure与renderer.outputColorSpace,见 render 方法。因此:
- 链中靠前阶段都工作在线性 HDR 数据上;
- 一旦经过 OutputPass,数据被转换到 sRGB 显示空间;
- SMAAPass 的混合操作假定输入为线性数据,所以它必须加在 OutputPass之前;
- 而像 FXAA 这类以 sRGB 输入为前提的通道,官方注释则说明它需要加在 OutputPass之后(参见 OutputPass.js 中 "If a pass requires sRGB input (e.g. like FXAA)")。
典型且推荐的渲染链顺序如下:
const composer = new EffectComposer( renderer ); composer.addPass( new RenderPass( scene, camera ) ); composer.addPass( new SMAAPass() ); // SMAA 在 OutputPass 之前、工作在线性色彩空间 composer.addPass( new OutputPass() ); // 色调映射 + sRGB 转换,作为链的最后一环最小接入示例与 Addon 导入方式
SMAAPass 属于 examples/jsm 下的addon(附加模块),不会打包进核心 three.js 模块,必须显式导入,参见 官方安装文档中关于 Addons 的说明。在构建工具或 importmap 场景下,按官方命名空间three/addons/导入:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';导入后在 EffectComposer 中追加通道即可完成最基本接入:
const smaaPass = new SMAAPass(); composer.addPass( smaaPass );更完整的真实接入可以参考仓库内示例 webgl_postprocessing_smaa.html:它先添加RenderPass渲染线框立方体与砖墙纹理立方体组成的场景,再依次添加smaaPass与outputPass,并在动画循环中通过smaaPass.enabled开关实时对比 SMAA 开/关效果(见 第 141 行),窗口尺寸变化时则同步调用composer.setSize( width, height )通知各通道重设内部缓冲(见 第 111-122 行)。
构造函数:new SMAAPass()
构造签名与继承关系
new SMAAPass()构造函数不接收任何参数,创建一个新的 SMAA 后期处理通道。它的继承链为:
Pass(抽象基类) → SMAAPassPass.js 定义了所有后期处理通道共有的属性与方法骨架:enabled(是否被 composer 处理,默认true)、needsSwap(渲染后是否交换读写缓冲,默认true)、clear(渲染前是否清空缓冲,默认false)、renderToScreen(结果是否直接输出到屏幕,默认false,通道链的最后一环会自动上屏)、抽象方法setSize、render与dispose。SMAAPass 覆写了其中三个:render、setSize与dispose。
构造时分配的 GPU 资源
虽然构造函数参数为空,但内部一次性构建了大量资源,理解这些有助于掌握内存占用与dispose()的必要性。从 SMAAPass.js 构造函数源码 可以看到:
- 两个中间渲染目标
_edgesRT与_weightsRT:均为HalfFloatType(半精度浮点)、不附带深度缓冲(depthBuffer: false),初始尺寸 1×1,之后由setSize调整。它们分别保存"边缘检测图"与"混合权重图",对应纹理命名为'SMAAPass.edges'与'SMAAPass.weights'(见 第 36-46 行)。 - 两张查找纹理
_areaTexture(面积纹理,160×560 量级)与_searchTexture(搜索纹理,66×33 量级):它们的图像数据以 base64 PNG 形式内嵌在代码中,由_getAreaTexture()与_getSearchTexture()两个内部方法返回 data URL 并通过Image异步解码(见 第 51-82 行)。_areaTexture使用LinearFilter双线性过滤、flipY: false且不生成 mipmap;_searchTexture则强制NearestFilter点采样,以精确读取搜索步长。 - 三份 ShaderMaterial 与着色器:分别对应 SMAA 的三个子阶段——
_materialEdges、_materialWeights、_materialBlend(见 第 84-119 行),着色器源码来自 SMAAShader.js 中导出的SMAAEdgesShader、SMAAWeightsShader、SMAABlendShader。 - 一个复用的全屏四边形
_fsQuad = new FullScreenQuad( null ):因为三个子阶段渲染目标一致(全屏四边形),只需渲染时切换 material 即可复用一个四边形实例(见 第 121 行)。FullScreenQuad本身定义在 Pass.js 中,内部使用OrthographicCamera与一个覆盖视口的三角形网格执行一次无顶点缓冲的全屏绘制。
此外,构造时还会把_weightsRT.texture绑定到tDiffuse、_areaTexture绑定到tArea、_searchTexture绑定到tSearch(见 第 99-101 行)。对 WebGL2 或高 DPI 场景而言,这两张半浮点渲染目标会带来约2 × 宽 × 高 × 2 字节的额外显存占用,需要接入时有所预期。
三阶段渲染管线:render() 方法内部原理
方法签名
.render( renderer : WebGLRenderer, writeBuffer : WebGLRenderTarget, readBuffer : WebGLRenderTarget, deltaTime : number, maskActive : boolean )参数语义继承自 Pass.render 的抽象定义,由 EffectComposer 自动调用:
| 参数 | 类型 | 说明 |
|---|---|---|
renderer | WebGLRenderer | 渲染器实例 |
writeBuffer | WebGLRenderTarget | 写缓冲,本通道的渲染目标(输出位置) |
readBuffer | WebGLRenderTarget | 读缓冲,本通道从此处读取上一个通道的结果 |
deltaTime | number | 时间增量(秒)。注意当前实现并未使用它(见下文源码注释) |
maskActive | boolean | 通道掩码是否激活。当前实现同样未使用 |
内部执行流程
从 render 源码 看,SMAAPass在渲染链中实际只占一个Pass槽位,但其内部会连续执行三次全屏渲染(三阶段 SMAA):
- 第 1 阶段——边缘检测(Edges):把
readBuffer.texture绑到边缘着色器的tDiffuse,用_materialEdges渲染到内部_edgesRT。对应 GLSL 函数为SMAAColorEdgeDetectionPS,它逐像素比较当前点与其左/上/右/下及次近邻的颜色差(RGB 分量逐通道取最大),超过阈值SMAA_THRESHOLD(默认0.1,定义于 SMAAShader.js)则判定为边缘,还包含局部对比度自适应逻辑,见 SMAAShader.js 边缘着色器。 - 第 2 阶段——权重计算(Weights):把第 1 阶段产生的
_edgesRT.texture作为输入,连同两张查找纹理一起,用_materialWeights渲染到内部_weightsRT。对应SMAABlendingWeightCalculationPS,沿检测到的边缘方向做最多SMAA_MAX_SEARCH_STEPS(默认8步,见 第 139-144 行 defines)的搜索,配合SMAAArea面积查表与距离开方补偿,输出每个像素的混合权重(见 SMAAShader.js 权重着色器)。 - 第 3 阶段——邻域混合(Blend):把原始场景图
readBuffer.texture绑定到tColor,权重图作为tDiffuse,用_materialBlend输出。对应SMAANeighborhoodBlendingPS:若当前像素权重和趋近于 0(dot < 1e-5)则原样返回颜色,否则沿最大权重方向取反色像素并插值混合(见 SMAAShader.js 混合着色器)。
第 3 阶段是决定输出目标的一步(见 第 158-173 行):如果renderToScreen === true,直接把结果渲染到屏幕(renderer.setRenderTarget( null ));否则渲染进writeBuffer交给链条中下一个通道,同时若this.clear为真会先清空目标。
值得注意的两点实现细节:
- 前两个中间阶段各自判断
this.clear,用于清理内部_edgesRT/_weightsRT中上一帧残留内容。 - 该 pass 的 render 方法把参数注释为
render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive*/ )——即deltaTime与maskActive目前在实现中并未参与运算,这与其文档签名保留参数一致(见 第 136 行)。
SMAA 与色彩空间的历史细节
在 three.js 的 WebGL 移植中,混合阶段特意加入了一次Gamma 校正:混合前把颜色提升到 2.2 次幂、混合后再还原(代码注释为 "WebGL port note: Added gamma correction",见 SMAAShader.js 第 471-475 行)。这也是文档强调"本 pass 在 linear-srgb 中运算、须置于 OutputPass 之前"的实践原因——若传入已经过 sRGB 转换的数据,双重的 gamma 处理与后续颜色管理会让结果失真。
setSize():分辨率自适应
方法签名
.setSize( width : number, height : number )当渲染器或画布尺寸变化(含windowresize、composer.setSize)时,EffectComposer 会调用各通道的setSize。SMAAPass 的实现做了三件事(见 setSize 源码):
- 把内部两个中间渲染目标缩放到目标尺寸:
_edgesRT.setSize( width, height )、_weightsRT.setSize( width, height )——确保中间图与原图分辨率一致,这是边缘精确对齐的前提; - 为三个材质统一更新
resolution均匀量:new Vector2( 1 / width, 1 / height )。SMAA 各着色器全部用该像素倒数分辨率把采样坐标换算成 UV 偏移(例如边缘着色器顶点阶段根据resolution计算 ±1、±2 像素的邻域采样点,见 SMAAShader.js 第 39-60 行),因此分辨率错误会直接导致锯齿误检或混合错位; resolution的默认值在着色器中为( 1 / 1024, 1 / 512 )(见 第 35 行 等),仅作为构造初值,实际总会在运行时被setSize覆盖为真实画布尺寸的倒数。
dispose():释放 GPU 资源
方法签名
.dispose()当 SMAA 通道在应用中不再被使用时,应主动调用dispose()以释放其占用的 GPU 资源。从 dispose 源码 可以看出它依次释放了:
- 两个渲染目标
_edgesRT、_weightsRT(含其纹理); - 两张查找纹理
_areaTexture、_searchTexture; - 三个 ShaderMaterial
_materialEdges、_materialWeights、_materialBlend; - 全屏四边形
_fsQuad。
该方法覆写自 Pass#dispose(基类中为空实现,见 Pass.js)。典型场景是:动态切换抗锯齿方案(如从 SMAA 切回 FXAA/关闭 AA)、卸载场景或销毁整个 composer 时调用,随后把通道从 composer 中移除,避免泄漏。切勿与三个中间阶段共用的全屏四边形重复释放发生冲突——_fsQuad是 SMAAPass 自身创建的实例,其dispose()内部会释放共享三角形几何(见 Pass.js)。
总结:接入清单与注意事项
在 WebGL 渲染管线中启用 SMAA,你需要记住以下要点:
- 导入路径:使用
three/addons/postprocessing/SMAAPass.js显式导入 addon。 - 链内位置:SMAA 工作在线性 sRGB 数据上,务必放在
OutputPass之前;只有 FXAA 这类需要 sRGB 输入的通道才应放在OutputPass之后。 - 推荐顺序:
RenderPass → SMAAPass → OutputPass,完整代码可对照 webgl_postprocessing_smaa 示例。 - 三阶段代价:每次
render会内部执行边缘检测、权重计算、邻域混合共三次全屏 pass,并额外占用两张半浮点中间渲染目标与两张内嵌查找纹理;相对 FXAA 更费,但换来了更精细的边缘处理。 - 可调参数:阈值
SMAA_THRESHOLD、搜索步数SMAA_MAX_SEARCH_STEPS等 defines 定义在 SMAAShader.js(edges)与 第 139-144 行(weights),预设为 SMAA 1x Medium,可通过修改对应 ShaderMaterial 的 defines 调整,但会改变画质与性能权衡。 - 尺寸同步:窗口变化时应调用
composer.setSize( width, height ),否则内部中间缓冲分辨率失配会造成错误结果。 - 资源回收:通道不再使用时调用
smaaPass.dispose()释放渲染目标、查找纹理、材质与全屏四边形。
如果需要深入阅读,推荐直接查看本文引用的三份关键源码:SMAAPass.js(通道编排)、SMAAShader.js(SMAA 算法本体)、OutputPass.js(色彩空间约束的源头),以及完整的官方文档页 SMAAPass。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考