three.js FXAAShader 源码解析与 EffectComposer 快速抗锯齿集成实战
FXAAShader 是 three.js 提供的一套可直接注入后处理管线的快速近似抗锯齿(Fast Approximate Anti-Aliasing)GLSL 着色器对象。本文以仓库中的 FXAAShader 官方文档 为骨架,结合 着色器实现、FXAAPass、ShaderPass 与官方示例 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 型纯数据对象(仅含 name、uniforms、vertexShader、fragmentShader 四个字段,不含任何 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 会出现边缘检测错位。这也是为什么官方推荐使用会覆写 setSize 的 FXAAPass(见下文集成章节)。
属性说明:.FXAAShader : ShaderMaterial~Shader (inner, constant)
官方文档将 FXAAShader 声明为模块内的私有常量成员(inner, constant),类型为 ShaderMaterial~Shader。这意味着:
- 它属于「常量着色器描述对象」而非
ShaderMaterial实例,自身没有dispose、render等方法; - 直接传给它不会自动创建材质,需经
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)
能量更大者即边缘走向(锯齿梯级沿着与颜色突变垂直的方向)。随后在边缘两侧取梯度更大的一侧作为搜索方向,pixelStep 由 texSize(即逆分辨率)换算为单个像素的 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 并生成 ShaderMaterial(ShaderPass.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 方案。
延伸阅读
- 官方文档:本文骨架来源 module-FXAAShader 页面,原始着色器实现位于 examples/jsm/shaders/FXAAShader.js
- 封装 Pass 及通用通道:FXAAPass、ShaderPass、ShaderPass 源码
- 可运行对照示例:webgl_postprocessing_fxaa.html(屏幕左右分栏对比无 FXAA 与启用 FXAA 的渲染差异),其运行截图见 examples/screenshots/webgl_postprocessing_fxaa.jpg
- WebGPU/TSL 版本:examples/jsm/tsl/display/FXAANode.js
- 算法原始出处(文档引用):NVIDIA 官方 FXAA 技术白皮书(FXAA WhitePaper);Jasper Flick 撰写的 Catlike Coding「Advanced Rendering / FXAA」系列教程(C# 参考实现来源)。若需深入比对算法细节,可查阅上述文献,本仓库仅承载其 GLSL 移植版本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00