首页
/ three.js FXAAShader 源码解析与 EffectComposer 快速抗锯齿集成实战

three.js FXAAShader 源码解析与 EffectComposer 快速抗锯齿集成实战

2026-09-08 15:08:37作者:尤辰城Agatha

FXAAShader 是 three.js 提供的一套可直接注入后处理管线的快速近似抗锯齿(Fast Approximate Anti-Aliasing)GLSL 着色器对象。本文以仓库中的 FXAAShader 官方文档 为骨架,结合 着色器实现FXAAPassShaderPass 与官方示例 webgl_postprocessing_fxaa.html,完整讲解其导入方式、uniform 结构、逐段算法原理、EffectComposer 集成步骤及内部可调参数,使读者既能拿来即用,也能按需读懂底层实现。

FXAA 与 FXAAShader:先明确是什么

FXAA(Fast Approximate Anti-Aliasing)是一种基于屏幕空间的后期抗锯齿技术:它不依赖几何体的多重采样,而是对最终渲染图像做一遍全屏后处理,通过分析像素亮度梯度定位锯齿边缘并沿边缘方向平滑采样。相比依赖 GPU 硬件 MSAA 的方式,FXAA 直接作用于后处理缓冲,无需额外几何开销,实现与管线位置灵活,适合移动端或大规模场景作为低成本抗锯齿方案。

仓库中的 FXAAShader 是这一算法在 three.js 中的 GLSL 落地实现。按官方文档说明,其算法源自 NVIDIA 官方 FXAA 白皮书,C# 参考实现来自 Jasper Flick(Catlike Coding 的 FXAA 系列教程),GLSL 移植由 Dave Hoskins 完成。它是 addon(附加模块),不属于 three.js 核心包,使用时必须显式导入。

导入方式

FXAAShader 作为 addon 与核心库分离,需从 three/addons/ 命名空间显式导入(官方文档标注其来源文件为 examples/jsm/shaders/FXAAShader.js):

import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';

模块内通过具名导出暴露唯一成员 FXAAShader(见源码末尾 export { FXAAShader };),该导出同时在聚合入口 examples/jsm/Addons.js 中注册,因此 importmap 指向 three/addons/ 时即可按上述路径解析。

着色器对象结构总览

源码 可见,FXAAShader 是标准的 ShaderMaterial~Shader 型纯数据对象(仅含 nameuniformsvertexShaderfragmentShader 四个字段,不含任何 JavaScript 逻辑):

字段 内容
name 'FXAAShader'
uniforms.tDiffuse 输入场景纹理采样器,初始值为 null
uniforms.resolution vec2 类型的逆分辨率(1/宽, 1/高),初始值 new Vector2( 1 / 1024, 1 / 512 )
vertexShader 透传 uv 与顶点变换的全屏三角形/四边形顶点着色器
fragmentShader 核心 FXAA 算法片段着色器

两个 uniform 的语义

  • tDiffuse:由 ShaderPass 在渲染时自动注入的读取缓冲纹理(见下文 ShaderPass 说明),即上一 pass 输出的画面。
  • resolution:存放逆分辨率而非像素尺寸本身。源码中纹理尺寸 texSize 直接取 resolution.xy 传入算法,因此必须按实际渲染目标宽高 W、H 设置为 ( 1 / W, 1 / H )

需要特别留意:源码第 27 行的默认值 Vector2( 1 / 1024, 1 / 512 ) 只是针对 1024×512 缓冲的占位值。直接裸用 FXAAShader(如自行 new ShaderMaterial)而不更新该 uniform,其他分辨率下 FXAA 会出现边缘检测错位。这也是为什么官方推荐使用会覆写 setSizeFXAAPass(见下文集成章节)。

属性说明:.FXAAShader : ShaderMaterial~Shader (inner, constant)

官方文档将 FXAAShader 声明为模块内的私有常量成员(inner, constant),类型为 ShaderMaterial~Shader。这意味着:

  • 它属于「常量着色器描述对象」而非 ShaderMaterial 实例,自身没有 disposerender 等方法;
  • 直接传给它不会自动创建材质,需经 ShaderPass/FXAAPass 包装(内部通过 UniformsUtils.clone 克隆 uniforms 并构造 ShaderMaterial,见 ShaderPass.js)。

算法原理逐段解析

片段着色器将 FXAA 拆成若干可读的小函数。理解整条数据流有助于按需调整质量/性能平衡。

1. 亮度采样(Luminance Sampling)

float SampleLuminance( sampler2D tex2D, vec2 uv ) {
	return dot( Sample( tex2D, uv ).rgb, vec3( 0.3, 0.59, 0.11 ) );
}

源码 L63-L74 用固定的亮度权重 vec3(0.3, 0.59, 0.11)(近似 BT.601 亮度系数)把 RGB 压缩为单通道亮度。整个边缘判定都在这条标量亮度信号上进行,将 3 次颜色运算降为 1 次,是 FXAA 保持低成本的关键。SampleLuminanceNeighborhood(L84-L103)随后以当前像素为中心,采集上下左右与四个对角共 8 个邻域亮度,并求出邻域最大/最小亮度及对比度 contrast = highest - lowest

2. 跳过平坦像素(ShouldSkipPixel)

float threshold = max( _ContrastThreshold, _RelativeThreshold * l.highest );
return l.contrast < threshold;

L105-L110 依据局部对比度判断是否处于边缘。阈值取「绝对阈值 _ContrastThreshold」与「相对阈值 _RelativeThreshold * highest」两者较大者:绝对阈值保证暗部低对比区域不会被误判,相对阈值则让阈值随局部亮度自适应。对比度不足的像素直接原样输出,避开平滑处理,这是 FXAA 不模糊纹理细节的根基。

3. 亚像素混合(DeterminePixelBlendFactor)

L112-L123 统计 3×3 邻域亮度与中心亮度的差异,估计该像素是否属于整条细线的亚像素部分(如远距离栏杆、高光),再经 smoothstep(0.0, 1.0, f) 的平方曲线放大后乘 _SubpixelBlending 得到像素级混合权重。这条路径弥补了仅沿边缘平滑时对「比一个像素还细」结构的漏检。

4. 判定边缘朝向(DetermineEdge)

L133-L168 计算两类二阶差分式响应:

  • 水平方向能量:abs(n + s - 2m)*2 + abs(ne + se - 2e) + abs(nw + sw - 2w)
  • 垂直方向能量:abs(e + w - 2m)*2 + abs(ne + nw - 2n) + abs(se + sw - 2s)

能量更大者即边缘走向(锯齿梯级沿着与颜色突变垂直的方向)。随后在边缘两侧取梯度更大的一侧作为搜索方向,pixelSteptexSize(即逆分辨率)换算为单个像素的 UV 步长,保证所有采样按像素对齐。

5. 沿边缘迭代搜索与混合权重(DetermineEdgeBlendFactor)

L170-L260 沿垂直于边缘的方向双向步进采样,寻找「边缘终点」:

#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
const float edgeSteps[EDGE_STEP_COUNT] = floatEDGE_STEP_COUNT;

起点先跨半步(pixelStep * 0.5),以边缘两侧平均亮度 edgeLuminance 为基准,只要某次采样亮度差绝对值大于 gradient * 0.25(梯度阈值)即视为到达边缘末端并停止;循环最多执行 6 次,若 6 次内未触边,则用大步长 EDGE_GUESS = 8.0 继续外推一次。正反两侧各取距当前像素较近的距离 shortestDistance,并结合符号一致性判断后得到:

return 0.5 - shortestDistance / ( pDistance + nDistance );

该值描述当前像素在整条边缘中相对位置,越靠近边缘中心平滑强度越高。

6. 合成输出(ApplyFXAA)

L262-L288 汇总前面各阶段:先由 ShouldSkipPixel 快速跳出;否则 finalBlend = max( pixelBlend, edgeBlend ) 取像素级与边缘级混合中的较大者,再沿垂直于边缘的方向把采样点偏移 pixelStep * finalBlend 后重采样一次,即可把锯齿边「拉」成平滑过渡。主函数 main(L290-L294)以 gl_FragColor 输出,配合透传 uv 的顶点着色器构成完整全屏 pass。

在 EffectComposer 后处理管线中集成

FXAAShader 的直接消费方是后处理管线。最省心的方式是用官方封装 FXAAPass,它继承自 ShaderPass

class FXAAPass extends ShaderPass {
	constructor() {
		super( FXAAShader );
	}
	setSize( width, height ) {
		this.material.uniforms[ 'resolution' ].value.set( 1 / width, 1 / height );
	}
}

constructor 内部以 super( FXAAShader ) 调用 ShaderPass,ShaderPass 会用 UniformsUtils.clone 复制 uniforms 并生成 ShaderMaterialShaderPass.js);渲染时 render()uniforms.tDiffuse.value 指向 readBuffer.texture(L95-L119),并用 FullScreenQuad 完成全屏绘制。FXAAPass 额外覆写 setSize,在合成器尺寸变化时自动把 resolution 更新为最新的逆分辨率——这是裸用 ShaderPass 时必须手动处理的部分(ShaderPass 未覆写 setSize)。

完整可运行的集成示例

参考官方示例 examples/webgl_postprocessing_fxaa.html,最小集成如下:

import * as THREE from 'three';
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';

// ...创建 renderer / scene / camera / 场景对象...

const composer = new EffectComposer( renderer );

composer.addPass( new RenderPass( scene, camera ) );
// ...中间可插入 Bloom、SSAO 等其它效果 pass...
composer.addPass( new OutputPass() );   // 色调映射 + sRGB 输出转换
composer.addPass( new FXAAPass() );     // FXAA 放在管线末端

// 窗口尺寸变化时同步合成器
window.addEventListener( 'resize', () => {
	composer.setSize( window.innerWidth, window.innerHeight );
} );

关于 Pass 顺序的官方建议

官方示例在 webgl_postprocessing_fxaa.html 中明确注释:

FXAA is engineered to be applied towards the end of engine post processing after conversion to low dynamic range and conversion to the sRGB color space for display.

即 FXAA 面向低动态范围(LDR)且已转换为 sRGB 显示空间的最终画面设计,应放在后处理链末端、位于色调映射与 sRGB 输出转换之后。示例中 composer2 的 Pass 顺序为 RenderPass → OutputPass → FXAAPass,正是这一工程经验的直接体现。

管线装配方式(EffectComposer)

FXAA Pass 依赖 EffectComposer 的双缓冲机制:RenderPass 将场景渲染到读取缓冲,OutputPass 负责色彩空间转换,FXAAPass 从读取缓冲采样并输出到写入缓冲(renderToScreen 为真时直接输出到屏幕)。每个 pass 的 setSize 由 EffectComposer 在初始化及尺寸变化时统一调用,因此 FXAAPass.resolution 始终与实际目标尺寸保持一致。切换回普通渲染(无后处理)或移除此 pass 时,应调用 pass.dispose() 释放材质与全屏四边形资源(见 ShaderPass.js)。

内部参数表与调优注意事项

FXAA 的质量/灵敏度由片段着色器内部的局部常量(float 变量及 #define)控制,并未暴露为 uniform,详见 源码 L48-L55

常量 默认值 作用与影响
_ContrastThreshold 0.0312 绝对对比度下限,防止暗部/平坦区域被误判为边缘;调高则平滑更保守、图像更锐
_RelativeThreshold 0.063 相对对比度系数,阈值随局部最高亮度线性放大;调节对高光、亮区边缘的敏感度
_SubpixelBlending 1.0 亚像素混合强度(0~1);设为 0 可完全关闭亚像素平滑
EDGE_STEP_COUNT 6 沿边缘搜索的最大步数,影响长边缘的覆盖范围与性能
EDGE_STEPS 1.0, 1.5, 2.0, 2.0, 2.0, 4.0 非均匀步长序列,近处密集、远处稀疏,兼顾定位精度与搜索距离
EDGE_GUESS 8.0 未触边时的外推步长,决定边缘末端判定范围

由于这些常量编译期写死在片段着色器中,想调节只能修改 examples/jsm/shaders/FXAAShader.js 源码后重新构建/拷贝使用,无法在运行时通过 uniform 注入。

相关实现:ShaderPass 与 WebGPU/TSL 版本

理解 FXAAShader 还需了解它的两个近邻:

  • ShaderPass:通用「裸着色器」后处理通道,new ShaderPass( FXAAShader ) 是官方文档给出的替代用法,但如上文所述,它会丢失 resolution 的自动更新能力,动态分辨率场景需自行管理。若使用 ShaderMaterial 直接创建材质,则传入 ShaderPass 时不会克隆其 uniforms(见 ShaderPass.js)。
  • WebGPU/TSL 等价物:在 WebGPU 渲染路径下,仓库提供同算法的 TSL 节点封装 examples/jsm/tsl/display/FXAANode.js,通过 fxaa() 函数创建节点(内部同样使用 EDGE_STEPS = uniformArray( [1.0, 1.5, 2.0, 2.0, 2.0, 4.0] ) 与逐帧更新的逆分辨率 uniform),并在节点注释中注明其同样要求 sRGB 输入。这为 TSL 后处理链提供了与 WebGL 端行为一致的 FXAA 方案。

延伸阅读

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391