three.js TSL FXAANode 详解:使用 fxaa() 在 WebGPU 渲染管线中实现快速近似抗锯齿
FXAANode 是 three.js 在 TSL(Three Shading Language)节点系统中提供的后处理节点,用于以极低成本实现 FXAA(Fast Approximate Anti-Aliasing,快速近似抗锯齿)。它主要服务于 WebGPU 渲染路径:配合 RenderPipeline 与 renderOutput() 使用,让开发者用一行 fxaa(pass) 把完整的 FXAA 算法接入渲染输出的末端。读完本文,你将掌握 FXAANode 的导入与构造方式、textureNode 与 updateBeforeType 等关键成员的行为、其每帧运行的底层实现原理,以及如何在真实示例场景中把它组装进渲染管线,并理清它与 WebGL 路径中 FXAAPass 的对应关系。
FXAA 属于"后处理型抗锯齿":它不依赖 GPU 的多重采样(MSAA),而是对已经渲染完成的整幅图像做边缘检测与像素混合,因此具有极低的性能开销、适合作为渲染管线的收尾步骤。FXAANode 把原本以 GLSL 编写的经典 FXAA 着色器改写为基于 TSL 的节点图实现,从而能无缝挂接在 TSL 的渲染输出链上。
FXAANode 的基本定位与继承关系
从 FXAANode 文档 与源码 examples/jsm/tsl/display/FXAANode.js 可以看到它的继承链为:
EventDispatcher → Node → TempNode → FXAANode
Node:TSL 节点体系的基类,承担节点图的编译与调度;TempNode:负责在最终着色器中生成临时求值结果;FXAANode:在二者基础上,将 FXAA 的完整算法封装成一个后处理节点。
该节点本身不做任何场景渲染,它的输入是一张已渲染好的纹理(以 TextureNode 形式表示),输出是完成抗锯齿后的颜色。需要特别强调的是其色彩空间前提:FXAA 需要 sRGB(gamma 空间)输入,因此色调映射(tone mapping)与颜色空间转换必须发生在抗锯齿之前。这一点是 FXAANode 正确使用的第一原则,稍后会在"渲染管线中的组装位置"一节具体演示。
导入方式
FXAANode 属于 addons(附加组件),不会被 three/webgpu 的核心包默认导出,必须显式导入:
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
文件路径对应的正是源码模块 examples/jsm/tsl/display/FXAANode.js(npm 包结构中映射为 three/addons/tsl/display/FXAANode.js)。该模块共导出两样东西:
FXAANode(默认导出)——类本身,可手动new;fxaa(命名导出)——推荐使用的 TSL 便捷工厂函数。
构造函数与 fxaa() 便捷函数
new FXAANode( textureNode )
const node = new FXAANode( textureNode );
参数 textureNode:类型为 TextureNode,代表 FXAA 效果的输入(即待抗锯齿的画面纹理节点)。
在实际使用中,直接向构造函数传入原始输出节点即可,因为工厂函数内部会做一次规范化。源码 examples/jsm/tsl/display/FXAANode.js 尾部给出了推荐用法:
export const fxaa = ( node ) => new FXAANode( convertToTexture( node ) );
这里通过 convertToTexture() 把任意"节点求值结果"统一转成可采样纹理,因此日常 TSL 编程中几乎不需要手动 new FXAANode,直接使用函数式写法:
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
const fxaaPass = fxaa( outputPass ); // outputPass 可以是任意后处理输出节点
fxaa() 返回一个 FXAANode 实例,其构造器内部执行 super( 'vec4' )(源码构造函数第 24–34 行),声明该节点的输出类型为四分量颜色。
属性成员
.textureNode : TextureNode
保存 FXAA 效果的输入纹理节点,与构造函数传入值一致。它是 FXAANode 在 updateBefore() 中读取画面尺寸、以及在 setup() 中执行采样与边缘处理的唯一数据来源。
.updateBeforeType : string
类型为字符串,取值恒为 NodeUpdateType.FRAME(即 'frame'),默认值即 'frame'。含义是:节点会在每一帧渲染前执行 updateBefore(),用于更新其内部 uniform。该常量定义于节点核心 src/nodes/core/constants.js,其中 FRAME 的注释明确说明"update 方法按帧执行"。FXAANode 选择按帧更新,是因为渲染目标(RenderTarget)尺寸可能随窗口/相机变化,必须每帧把最新的"逆分辨率"写入 uniform。
从源码结构还可以看到它持有一个私有 uniform:
this._invSize = uniform( new Vector2() ); // 逆分辨率:vec2(1/width, 1/height)
_invSize 正是把真实像素偏移(如 ±1 个像素、1.5 个像素)换算成 UV 坐标步长所需的缩放因子,整个 FXAA 的边缘搜索都依赖它。
重写自 TempNode 的说明
updateBeforeType 与下文的方法均覆盖自父类 TempNode,相关基础约定可参考 TempNode 文档。
方法:setup() 与 updateBefore()
.updateBefore( frame : NodeFrame )
每帧执行一次,用于同步 uniform。源码实现非常精简(examples/jsm/tsl/display/FXAANode.js):
updateBefore( /* frame */ ) {
const map = this.textureNode.value;
this._invSize.value.set( 1 / map.image.width, 1 / map.image.height );
}
也就是读取输入纹理图像的实际宽高,计算其倒数(逆分辨率)写入 _invSize。当后处理缓冲被 resize 时,这里保证下一帧的 FXAA 边缘搜索仍以正确的物理像素步长进行。
.setup( builder : NodeBuilder )
这是 FXAANode 的核心,负责把整个 FXAA 算法构造成 TSL 节点图,返回一个 ShaderCallNodeInternal。源码中约从第 73 行开始,包含约 270 行 TSL 代码,完整翻译了经典 FXAA 算法的每一个环节。其内部逻辑可以划分为以下几层。
1. 采样前处理与 UV 获取
const textureNode = this.textureNode.bias( - 100 );
const uvNode = textureNode.uvNode || uv();
- 对输入纹理施加一个极负的
bias(-100):从实现意图看,这是让纹理采样始终落在最高分辨率层级(等效于关闭 mipmap 模糊),保证后处理边缘检测读到的是一帧锐利的颜色值; - UV 优先取纹理节点自带的
uvNode,否则回退到当前片元默认uv()。
2. 算法常量(全部为 TSL float 硬编码)
| 常量 | 取值 | 作用 |
|---|---|---|
EDGE_STEP_COUNT |
6 | 沿边缘向两个方向搜索的迭代次数 |
EDGE_GUESS |
8.0 | 到达边缘后"外推猜步"的最大像素数 |
EDGE_STEPS |
[1.0, 1.5, 2.0, 2.0, 2.0, 4.0] |
每次迭代的像素步长数组(非均匀步长) |
_ContrastThreshold |
0.0312 | 对比度下界,低于它视为平坦区域直接跳过 |
_RelativeThreshold |
0.063 | 相对阈值,按最亮邻域亮度缩放跳过判定 |
_SubpixelBlending |
1.0 | 亚像素混合强度系数 |
其中 EDGE_STEP_COUNT = 6、EDGE_GUESS = 8.0 等常量与 WebGL 路径的 GLSL 版本 examples/jsm/shaders/FXAAShader.js 中 #define EDGE_STEP_COUNT 6、#define EDGE_GUESS 8.0 一一对应,说明两套实现(WebGL Shader 与 WebGPU TSL)遵循同一套算法参数。注意:这些阈值在节点内是写死的,TSL API 目前不暴露参数化入口,如需调节强度需直接修改常量(本仓库只读,可 fork 后自行调整)。
3. 亮度采样函数族
Sample( uv ):对输入纹理在指定 UV 采样;SampleLuminance( uv ):按人眼感知权重dot(rgb, vec3(0.3, 0.59, 0.11))把颜色折算成亮度;SampleLuminanceOffset( texSize, uv, uOffset, vOffset ):借助_invSize实现"当前 UV + N 个像素"的偏移采样。
4. 邻域采样与"是否跳过像素"判定
SampleLuminanceNeighborhood 会读取中心 m 以及上、下、左、右、四个对角线共 9 个位置的亮度,并计算:
highest = max(n,e,s,w,m)
lowest = min(n,e,s,w,m)
contrast = highest - lowest
ShouldSkipPixel 则判断:若 contrast < max(0.0312, 0.063 * highest),说明当前像素附近缺乏可感知的边缘,直接返回原始采样,这是 FXAA 高性能的关键——大多数平坦像素根本不进入昂贵的长程搜索。
5. 边缘方向检测(水平 vs 垂直)
DetermineEdge 分别累计水平方向与垂直方向的二阶差分绝对值,比较两者大小判断边缘朝向;随后取边缘两侧梯度较大的那一侧作为主方向,并换算出一个"每步移动量" pixelStep(按水平/垂直分别取 texSize.x 或 texSize.y)。
6. 沿边缘的迭代式端点搜索
DetermineEdgeBlendFactor 先从当前像素向边缘正、反两个方向出发:
- 以
EDGE_STEPS数组的步长做最多 6 次迭代,每次比较采样点亮度与"边缘平均亮度"的差值是否超过gradient * 0.25的阈值,找到边缘端点; - 若 6 步内仍未触及阈值(
pAtEnd.not()),则额外外推EDGE_GUESS = 8.0个像素; - 最后依据两侧最短距离与符号计算出介于 0~0.5 的
blendFactor。
7. 亚像素混合与最终输出
DeterminePixelBlendFactor 对 3×3 邻域做 12 点加权平均(2*(n+e+s+w) + (ne+nw+se+sw) 除以 12),得到该像素与邻域平均的差异,再经 smoothstep 平方得到亚像素混合量。FXAANode 最终取 max(pixelBlend, edgeBlend) 作为混合系数,把 finalUv 沿边缘垂直方向偏移 pixelStep * blendFactor 后重新采样输出——这正是 FXAA"把边缘颜色向过渡区中心拉拢"的核心手法。
整个流程被封装进一个名为 FxaaPixelShader 的 TSL 函数(vec4 FxaaPixelShader(vec2 uv, vec2 texSize)),最后由主函数 fxaa() 以 ApplyFXAA( uvNode, this._invSize ) 的方式调用。
真实示例:在 RenderPipeline 中启用 FXAA
仓库提供了可直接运行的官方示例 examples/webgpu_postprocessing_fxaa.html,其中完整展示了 FXAANode 的接入方式,核心片段如下:
import * as THREE from 'three/webgpu';
import { pass, renderOutput } from 'three/tsl';
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
// 后处理管线
renderPipeline = new THREE.RenderPipeline( renderer );
// 关闭渲染器默认的输出颜色变换(色调映射与输出色彩空间)
// 改由 renderOutput() 显式控制顺序
renderPipeline.outputColorTransform = false;
// 场景渲染通道
const scenePass = pass( scene, camera );
// 先做色调映射 + sRGB 色彩空间转换
const outputPass = renderOutput( scenePass );
// FXAA 必须在 sRGB 颜色空间计算(位于色调映射和色彩空间转换之后)
const fxaaPass = fxaa( outputPass );
renderPipeline.outputNode = fxaaPass;
这段代码值得逐行理解:
renderPipeline.outputColorTransform = false:把"色调映射 + 输出色彩空间"从渲染器默认收尾中剥离(相关机制可参考 RenderPipeline 文档);renderOutput( scenePass ):TSL 内建函数,在当前节点链中显式应用渲染器的输出设置,产出 sRGB 画面。fxaa在 TSL 函数参考 中被定义为 "Creates a FXAA anti-aliasing effect",与renderOutput等一同列在显示/后处理函数表中;fxaa( outputPass ):把已经完成色彩空间转换的图像接入 FXAANode——这正是"FXAA 需要 sRGB 输入"这一文档约束的实现方式;renderPipeline.outputNode = fxaaPass:把 FXAA 结果指定为管线最终输出。
示例中还提供了开关逻辑:需要关闭 FXAA 时,把 renderPipeline.outputNode 切回 outputPass 并置 renderPipeline.needsUpdate = true 即可,这展示了 TSL 后处理"节点可随时替换"的灵活性。
与 WebGL 路径 FXAAPass 的对应关系
若你此前使用的是 WebGL 渲染器,会发现 FXAA 存在"双轨"实现,二者算法同源但宿主不同:
| 维度 | WebGL 路径 | WebGPU / TSL 路径 |
|---|---|---|
| 载体 | EffectComposer + Pass |
RenderPipeline + 输出节点 |
| 实现模块 | examples/jsm/postprocessing/FXAAPass.js | examples/jsm/tsl/display/FXAANode.js |
| 着色器 | GLSL examples/jsm/shaders/FXAAShader.js | TSL 节点图(setup() 内联构建) |
| 分辨率处理 | setSize() 更新 material.uniforms['resolution'] |
updateBefore() 更新 _invSize uniform |
WebGL 的 FXAAPass 继承自 ShaderPass,其 setSize 同样写入逆分辨率 (1/width, 1/height),可见"以逆分辨率作为像素步长换算因子"是两套实现的共同设计。可对比官方示例 examples/webgl_postprocessing_fxaa.html 与 WebGPU 版本,体会从"EffectComposer.addPass"到"outputNode 赋值"的 API 演变。除 FXAA 外,仓库还提供了更高质量但开销更大的 SMAA 方案,可查阅 SMAANode 文档 作为横向对比。
使用注意事项小结
- 色彩空间顺序不可颠倒:FXAA 的亮度阈值针对感知(sRGB)亮度设计,必须在
renderOutput()完成色调映射与 sRGB 转换之后再接入,否则边缘检测结果会偏差甚至失效; - 输入纹理需真实存在尺寸:
updateBefore()依赖textureNode.value.image.width/height计算逆分辨率,因此请勿在纹理尚未加载/创建完成时启用该节点; - 均匀调参入口:节点内部阈值(对比度 0.0312、相对阈值 0.063、亚像素 1.0)与步长均为写死常量,若默认强度不适合你的项目画面,需基于源码自行派生或修改(本仓库只读,请复制到自有工程后调整);
- 与多采样方案的关系:FXAA 无法消除闪烁性锯齿(shimmer),它擅长的是平滑斜边阶梯与减少高对比边缘的"锯齿感",在性能预算紧张的场景(如 WebGPU 移动端)常作为 MSAA 之外的轻量补充方案。
扩展阅读
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
