首页
/ three.js 后处理全屏抗锯齿 FXAAPass 使用指南:继承体系、setSize 与 FXAA 着色器原理

three.js 后处理全屏抗锯齿 FXAAPass 使用指南:继承体系、setSize 与 FXAA 着色器原理

2026-09-06 18:49:22作者:舒璇辛Bertina

FXAAPass 是 three.js 官方提供的一种快速近似抗锯齿(Fast Approximate Anti-Aliasing,FXAA)后处理通道,用于消除画面边缘的锯齿伪影。它适合作为 EffectComposer 后期处理链的最后一环,以极低的性能开销平滑 3D 渲染输出的几何边缘。读完本文,你将掌握 FXAAPass 的导入方式、与 EffectComposer 的组合用法、其 setSize 覆写行为,以及其底层 FXAAShader 的亮度取样、对比度判定与边缘混合等核心实现原理。

一、FXAAPass 在通道继承体系中的定位

FXAAPass 是一个无需任何构造参数的成品后处理通道,其完整的继承链为:

Pass → ShaderPass → FXAAPass
  • 基类 Passexamples/jsm/postprocessing/Pass.js)定义了所有后处理通道共有的接口,如 enabledneedsSwapclearrenderToScreen 标志位,以及抽象的 setSizerenderdispose 方法。
  • 中间层 ShaderPass 负责把「着色器对象」包装成一个后处理通道:它会克隆 shader 的 uniforms、构建 ShaderMaterial、并通过 FullScreenQuad 渲染全屏四边形。因此任何以 GLSL 着色器定义的效果(如 FXAA)都可以复用这一套管线。
  • FXAAPass 则是在 ShaderPass 之上,直接使用内置的 FXAA 着色器实例化出一个开箱即用的抗锯齿通道。

FXAAPass 源码 可以看到其实现非常精简——构造函数只做了一件事:把 FXAAShader 交给父类 ShaderPass,其余渲染逻辑全部由父类承担:

class FXAAPass extends ShaderPass {

	constructor() {

		super( FXAAShader );

	}

	// 仅覆写了 setSize,用于同步着色器中的分辨率 uniform
	setSize( width, height ) {

		this.material.uniforms[ 'resolution' ].value.set( 1 / width, 1 / height );

	}

}

二、快速上手:导入并加入后期处理链

与 three.js 核心 API 不同,FXAAPass 属于 addon(附加模块),必须显式导入。官方文档给出的模块路径为 three/addons/postprocessing/FXAAPass.js

import { FXAAPass } from 'three/addons/postprocessing/FXAAPass.js';

导入后只需两行即可接入渲染流程:

const fxaaPass = new FXAAPass();
composer.addPass( fxaaPass );

其中 composer 是一个 EffectComposer 实例。一个完整、可运行的最小链路通常包含 RenderPass(把场景渲染进离屏缓冲)、OutputPass(色调映射与色彩空间输出)以及位于末端的 FXAAPass

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { FXAAPass } from 'three/addons/postprocessing/FXAAPass.js';

const composer = new EffectComposer( renderer );
composer.addPass( new RenderPass( scene, camera ) );
composer.addPass( new OutputPass() );

const fxaaPass = new FXAAPass();
composer.addPass( fxaaPass );

// 渲染循环内
composer.render();

在经由 importmap 配置的裸模块环境中,示例 HTML 通常写成:

<script type="importmap">
	{
		"imports": {
			"three": "../build/three.module.js",
			"three/addons/": "./jsm/"
		}
	}
</script>
<script type="module">
	import { FXAAPass } from 'three/addons/postprocessing/FXAAPass.js';
	// ...
</script>

也可以从 three/addons/ 根入口 examples/jsm/Addons.js 统一引入(该文件已对 FXAAPass 做了再导出)。在支持 ES Module 的构建工具环境中,仓库本身还提供了 src/Three.js 等核心入口,但后处理 addon 始终以显式模块路径单独引用。

官方对比示例参考

仓库中的示例页 examples/webgl_postprocessing_fxaa.html 展示了 FXAA 的官方用法与效果验证方式:它在同一画布内通过 scissor 测试分屏渲染了两个 EffectComposer——左侧 composer1 仅含 RenderPass + OutputPass(无抗锯齿),右侧 composer2 在此基础上追加 fxaaPass,方便直接肉眼对比边缘差异。其中关键代码片段为:

const renderPass = new RenderPass( scene, camera );
renderPass.clearAlpha = 0;

fxaaPass = new FXAAPass();

const outputPass = new OutputPass();

composer1 = new EffectComposer( renderer );
composer1.addPass( renderPass );
composer1.addPass( outputPass );

composer2 = new EffectComposer( renderer );
composer2.addPass( renderPass );
composer2.addPass( outputPass );

// FXAA 应在色调映射并转换到 sRGB 显示色彩空间之后、接近链路末端时应用
composer2.addPass( fxaaPass );

该示例在动画循环内通过 renderer.setScissorTest( true ) 配合 renderer.setScissor( ... ) 让两个 composer 分别渲染到画布的左右两半(见 examples/webgl_postprocessing_fxaa.htmlanimate() 的实现)。场景本身由 100 个带平面着色的红色四面体 InstancedMesh 组成,红色几何体在白色背景上的清晰轮廓非常适合暴露边缘锯齿,便于检验 FXAA 效果。若想本地查看,可在仓库根目录使用 python -m http.server 之类静态服务器托管后打开该 HTML(需 GPU 与 WebGL 支持)。

三、构造函数

new FXAAPass()

创建一个新的 FXAA 后处理通道。该构造函数不接受任何参数,也无配置项;它只是把内置着色器对象 FXAAShader 传入父类 ShaderPass 完成初始化。因此实例创建后即可直接交给 EffectComposer

const fxaaPass = new FXAAPass();

ShaderPass 构造函数在执行 super( FXAAShader ) 时会发生以下动作(见 ShaderPass.js):

  1. shader 视为普通着色器对象(而非已存在的 ShaderMaterial),用 UniformsUtils.clone( shader.uniforms ) 克隆其 uniforms;
  2. 新建一个名为 FXAAShader(取自 shader.name)的 ShaderMaterial
  3. 记录默认采样纹理 uniform 名为 textureID = 'tDiffuse'
  4. 用该材质实例化一个内部 FullScreenQuad,作为全屏渲染的载体。

四、方法:setSize( width, height )

setSize( width : number, height : number ) : void

设置通道(pass)的尺寸,widthheight 为要设置的宽高(像素值)。文档将其标注为对 ShaderPass#setSize 的覆写(ShaderPass 本身继承自 Pass,基类中该方法是空实现),而 FXAAPass 的重写逻辑只有一行:

this.material.uniforms[ 'resolution' ].value.set( 1 / width, 1 / height );

其含义是:FXAA 算法需要在片元着色器中把「像素偏移量」换算成 UV 坐标增量,因此必须知道每个像素在整张纹理中占多大比例。resolution uniform 存的就是「单个像素的宽高(1/width, 1/height)」,着色器后续用它乘以采样偏移即可获得纹理坐标步长。

在实际调用中,该方法由 EffectComposer 自动驱动:当调用 composer.setSize( width, height ) 时,EffectComposer.js 会计算出考虑了像素比的真实缓冲尺寸 effectiveWidth = width * _pixelRatio,并依次调用链路上每个通道的 pass.setSize( effectiveWidth, effectiveHeight )。因此:

  • 若使用了 renderer.setPixelRatio( dpr )composer.setPixelRatio( dpr ),FXAAPass 得到的将是物理像素尺寸,resolution 也会随之正确更新;
  • 窗口 resize 后务必同步调用 composer.setSize( container.offsetWidth, container.offsetHeight ),否则抗锯齿采样步长与画面尺寸失配,FXAA 会失效或产生模糊。

由于 FXAAPass 自身只维护一个 uniforms 引用,且每次渲染都会由 ShaderPass.render()readBuffer 的纹理重新赋给 tDiffuse(见 ShaderPass.js),所以即使全屏四边形被多个通道复用,效果依旧正确。

五、底层支撑:FXAAShader 与关键 uniform

FXAAPass 的实现完全依托 examples/jsm/shaders/FXAAShader.js 中的着色器对象 FXAAShader。它继承了 NVIDIA FXAA 算法,代码注释中标注了参考来源为 NVIDIA 的 FXAA 白皮书与 Jasper Flick 的 C# 实现、Dave Hoskins 的 GLSL 移植版。其结构如下:

const FXAAShader = {

	name: 'FXAAShader',

	uniforms: {
		'tDiffuse':  { value: null },
		'resolution': { value: new Vector2( 1 / 1024, 1 / 512 ) }
	},

	vertexShader: /* glsl */`...`,
	fragmentShader: /* glsl */`...`

};

uniforms 一览:

uniform 类型 默认值 作用
tDiffuse sampler2D null 输入纹理,即上一个后处理通道的 readBuffer.texture,由 ShaderPass.render() 每帧注入
resolution Vector2 ( 1/1024, 1/512 ) 单个像素在 UV 空间中的宽高(1/width, 1/height),由 FXAAPass.setSize() 维护

默认值 ( 1 / 1024, 1 / 512 ) 只是一个占位初值——只要走 EffectComposer 的正常流程,setSize 会立刻用真实尺寸覆盖它。因此不要试图在构造 FXAAPass 后手动修改 resolution,应通过 composer.setSize() 或窗口尺寸来驱动。

片元着色器内部的可调参数

FXAA 的核心逻辑全部位于 fragmentShader 中。该着色器在文件开头定义了两组算法常量:

#define EDGE_STEP_COUNT 6
#define EDGE_GUESS 8.0
#define EDGE_STEPS 1.0, 1.5, 2.0, 2.0, 2.0, 4.0

float _ContrastThreshold = 0.0312;   // 像素对比度阈值(绝对值)
float _RelativeThreshold = 0.063;    // 相对阈值(乘以上下文中最高亮度)
float _SubpixelBlending  = 1.0;      // 子像素混合强度
  • _ContrastThreshold_RelativeThreshold 共同决定哪些像素需要处理:只有当邻域亮度对比 l.contrast 同时超过「绝对阈值」和「相对阈值 × 邻域最高亮度」(见 ShouldSkipPixel 函数)时才认定该像素位于边缘,否则直接返回原色。阈值越大,抗锯齿作用越保守、画面越锐利;越小则边缘覆盖越广、但也可能让文本等细节变糊。
  • _SubpixelBlending 控制「子像素混合」的强度,用于对跨越多个像素的细线(如 45° 斜线)做补偿平滑,取值 1.0 表示完整启用。

这些变量虽然以字面量内嵌在 GLSL 中(FXAAPass 没有把它们暴露为可配置参数),但理解它们有助于你直接改写 FXAAShader 或调整画质/性能取向。

六、FXAA 算法在着色器中的执行流程

结合 FXAAShader.js 的片元着色器,可以梳理出 FXAA 对每个像素执行的完整流水线,这也是理解 FXAAPass「为什么能做到单次全屏采样即完成抗锯齿」的关键:

  1. 亮度取样(SampleLuminanceNeighborhood:对当前像素 m 及其 8 邻域(上 n、下 s、左 w、右 e 与 4 个对角)采样,并按 dot( rgb, vec3( 0.3, 0.59, 0.11 ) ) 的权重计算亮度(该权重对应人眼对 R/G/B 通道的感知灵敏度)。随后求出邻域最高亮度、最低亮度与对比度。
  2. 跳过判定(ShouldSkipPixel:若对比度不足(低于前文阈值公式),判定该像素不在边缘上,直接返回原始颜色——这正是 FXAA 比逐像素 MSAA 廉价得多的原因:绝大多数内部像素被提前跳过。
  3. 子像素混合因子(DeterminePixelBlendFactor:基于 3×3 邻域亮度的均值偏离程度,估算该像素落在「次像素级细线」上的概率,得到 pixelBlend
  4. 边缘方向判定(DetermineEdge:分别累加水平方向(n + s − 2m 项)与垂直方向的二阶梯度能量,较大的方向被认定为边缘走向,并据此决定沿哪个轴向搜索、以哪个邻域为「对侧亮度」。
  5. 边缘端点到端点距离搜索(DetermineEdgeBlendFactor:以半像素为起点,沿边缘走向两侧按 EDGE_STEPS(1.0、1.5、2.0、2.0、2.0、4.0 倍步长)逐步推进,最多走 EDGE_STEP_COUNT = 6 步,直到找到亮度越过 gradientThreshold 的端点;若始终未越界则再外推 EDGE_GUESS = 8.0 步作为假设端点。随后用两端点距离推导出沿边缘的偏移比例 edgeBlend
  6. 混合输出(ApplyFXAA:取 max( pixelBlend, edgeBlend ) 作为最终混合强度,让采样中心沿着边缘垂直方向移动该比例对应的像素步长,再重新采样一次颜色作为输出。这样相当于把边缘两侧的颜色按几何比例做了平均,从而在轮廓上产生平滑过渡。

整个算法只依赖 tDiffuseresolution 两个 uniform、对每个像素做有限次数的纹理采样,不生成或依赖几何体的多重采样信息,因此对任意渲染内容(含后期特效叠加后的画面)都能直接生效。

七、继承自 ShaderPass 的默认行为

因为 FXAAPass 没有重写父类的渲染逻辑,它在通道链中的表现由 ShaderPass 决定,使用时值得注意以下几点:

  • 纹理采样契约ShaderPass 默认把名为 tDiffuse 的 uniform 绑定到 readBuffer.texture。FXAAShader 恰好遵循该约定,所以 FXAAPass 无需额外配置即可取得上一个通道的输出。
  • 渲染到屏幕Pass.renderToScreen 默认为 falseEffectComposer 会自动把链路上最后一个通道设为输出到屏幕(其内部机制见 EffectComposer.render() 对末位 pass 的 renderToScreen 处理)。若把 FXAAPass 放在链路中间(而非最后),它只会写入 writeBuffer,供后续通道继续消费。
  • 释放资源:继承自 ShaderPass.dispose() 的方法会同时释放 material 与内部 FullScreenQuad 的 GPU 资源(ShaderPass.js)。当不再需要 FXAAPass 时应调用 fxaaPass.dispose()
  • 全屏四边形:所有基于 ShaderPass 的通道共享由 Pass.js 提供的单一三角形面片几何体(由 FullscreenTriangleGeometry 生成、随 FullScreenQuad 复用),内存开销极低。

八、FXAAPass 与着色器/TSL 生态中的其他 FXAA 实现

在 three.js 仓库中,FXAA 还存在多个层级的具体实现:

  • FXAAPass + FXAAShader(本文主角):面向传统 WebGL 渲染器与 EffectComposer 后处理链的 addon 实现,位于 examples/jsm/postprocessing/FXAAPass.jsexamples/jsm/shaders/FXAAShader.js
  • SMAAPass:同一后处理目录下另提供基于 SMAA 算法的通道 examples/jsm/postprocessing/SMAAPass.js。相比单次全屏处理的 FXAA,SMAA 需要额外的边缘检测与混合纹理等步骤,链路与资源占用更复杂;FXAA 的优势在于实现与依赖最少、执行开销低。
  • TSL 节点实现:文档目录中还包含面向节点/TSL 风格的 FXAA 节点文档 docs/pages/FXAANode.html.md,服务于新版 WebGPU/TSL 渲染体系,与本文基于 EffectComposer 的通道式用法属于不同管线,可按渲染后端选择。

选型时的实用参考:若目标是 WebGL + EffectComposer、希望在链路末尾对任意场景低成本去锯齿,FXAAPass 是最直接的答案;官方示例将它与 OutputPass 搭配并置于其后,正是为了在低动态范围与 sRGB 显示空间中对画面边缘做最终平滑(见 webgl_postprocessing_fxaa.html 中的注释说明)。

九、总结

FXAAPass 是一个「小而全」的官方后处理通道:对外只需 new FXAAPass() + composer.addPass() 两步即可启用;对内它通过覆写 setSize 维护着色器所需的像素分辨率、复用 ShaderPass 的材质与全屏四边形管线,并把 NVIDIA FXAA 的亮度对比度判定、子像素混合、边缘方向检测与端点搜索等算法完整封装在 FXAAShader 中。理解这套「通道对象 — 着色器对象 — uniform 契约」的分层关系,不仅能让你正确使用 FXAAPass,也为自定义后处理通道(继承 ShaderPass 并覆写 setSize 同步自身 uniform)提供了可直接照搬的模板。

相关资源索引

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