0%

实时渲染学习笔记—FXAA(代码+逐步解读)

本系列主要为作者本人在学习实时渲染过程中,针对特定问题或技术做的研究笔记。其中所有涉及的点,都会实现在作者本人的学习向渲染引擎MyCGLab上,仓库地址如下,欢迎围观:https://github.com/WingerZeng/MyCGLab

先上效果图

FXAA OFF

FXAA ON

以上图片由MyCGLab渲染。

简介

FXAA全称为快速近似抗锯齿,是在屏幕空间上进行抗锯齿的技术。相比于传统的MSAA,FXAA仅需要单个图像作为输入,在一个单独的后处理Pass中就可以完成,因此可以非常有效地兼容延迟渲染,并且实现起来真的非常简单。

FXAA用的比较多的FXAA 3.11 Console和FXAA 3.11 Quality两个版本,前者是面向主机平台的,属于是后者的简化版本,本文主要讲解Quality版本,Console版本可以参考文献[1]。

核心思路

锯齿产生的原因是物理空间中连续的边界,被离散到了屏幕空间中的独立的像素上,抗锯齿的目的就是判断每一个片段落在原始边界内的比例。FXAA主要思路是认为每个锯齿都是由连续的水平或竖直的锯齿边缘组成的,可以通过着色片段在锯齿边缘中的位置,就可以近似求出其落在原始边界内的比例。

FXAA主要有以下几个关键步骤:

  1. 检测边缘,如果该像素不包含边缘,不进行FXAA
  2. 检测边缘的朝向(水平、垂直)
  3. 判断当前像素位于边的哪一侧
  4. 从当前像素出发,向边缘的两端遍历,进行端点查找
  5. 根据端点的位置计算最终纹理采样坐标
  6. 结合额外的低通滤波

代码讲解

下面将逐步分析[2]Open source FXAA code的FXAA shader代码,以对FXAA的原理有更深的理解。

边缘检测

首先采样当前pixel和上下左右的邻居。

1
2
3
4
5
6
7
8
9
vec3 colorCenter = texture(ldrTexture, texCoord).rgb;
// Luma at the current fragment
float lumaCenter = rgb2luma(colorCenter);

// Luma at the four direct neighbours of the current fragment.
float lumaDown = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 0,-1)).rgb);
float lumaUp = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 0, 1)).rgb);
float lumaLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1, 0)).rgb);
float lumaRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1, 0)).rgb);

根据最大值与最小值的差值来判断是否为边缘。此处的EDGE_THRESHOLD_MIN用于在低明度下直接跳过抗锯齿,因为暗处的锯齿往往不引人注意。

1
2
3
4
5
6
7
8
9
10
11
// Find the maximum and minimum luma around the current fragment.
float lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
float lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));

// Compute the delta.
float lumaRange = lumaMax - lumaMin;

// If the luma variation is lower that a threshold (or if we are in a really dark area), we are not on an edge, don't perform any AA.
if(lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)){
return colorCenter;
}

判断边缘的走向

额外采样对角线方向的四个邻居。

1
2
3
4
5
   // Query the 4 remaining corners lumas.
float lumaDownLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1,-1)).rgb);
float lumaUpRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1, 1)).rgb);
float lumaUpLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1, 1)).rgb);
float lumaDownRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1,-1)).rgb);

再求水平方向上和垂直方向上的“绝对梯度值”,来判断边缘的方向是水平还是垂直的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Combine the four edges lumas (using intermediary variables for future computations with the same values).
float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;

// Same for corners
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;

// Compute an estimation of the gradient along the horizontal and vertical axis.
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp ) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners);

// Is the local edge horizontal or vertical ?
bool isHorizontal = (edgeHorizontal >= edgeVertical);

这里的“绝对梯度值”求法可以这么理解,以垂直方向为例,可以理解为row2分别和row1,row3求差并取绝对值,结果相加,并且将三个分量1,2,1加权得到结果。可以参考下面的伪代码。

1
2
vec3 differ = abs(row2 - row1) + abs(row3 - row1);
float verticalGradient = differ[0] + 2*differ[1] + differ[2];

最后根据边的方向,计算单个采样步长。

1
2
   // Choose the step size (one pixel) accordingly.
float stepLength = isHorizontal ? inverseScreenSize.y : inverseScreenSize.x;

判断边位于中心点的哪侧

分别计算两边的梯度:

1
2
3
4
5
6
// Select the two neighboring texels lumas in the opposite direction to the local edge.
float luma1 = isHorizontal ? lumaDown : lumaLeft;
float luma2 = isHorizontal ? lumaUp : lumaRight;
// Compute gradients in this direction.
float gradient1 = luma1 - lumaCenter;
float gradient2 = luma2 - lumaCenter;

哪边梯度大,边缘就落在那一边呗

1
2
   // Which direction is the steepest ?
bool is1Steepest = abs(gradient1) >= abs(gradient2);

再将最大梯度进行一个缩放,该值在之后沿边遍历时作为阈值来判断是否为边的端点。这里的0.25是一个经验系数。

1
2
// Gradient in the corresponding direction, normalized.
float gradientScaled = 0.25*max(abs(gradient1),abs(gradient2));

再计算边上的平均明度值

1
2
3
4
5
6
7
8
9
// Average luma in the correct direction.
float lumaLocalAverage = 0.0;
if(is1Steepest){
// Switch the direction
stepLength = - stepLength;
lumaLocalAverage = 0.5*(luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5*(luma2 + lumaCenter);
}

迭代查找边的端点

迭代前,先要把起始点偏移0.5个步长,移到边上。

1
2
3
4
5
6
7
// Shift UV in the correct direction by half a pixel.
vec2 currentUv = texCoord;
if(isHorizontal){
currentUv.y += stepLength * 0.5;
} else {
currentUv.x += stepLength * 0.5;
}

并初始化一些变量,其中uv1、uv2分别是两侧迭代的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
// Compute offset (for each iteration step) in the right direction.
vec2 offset = isHorizontal ? vec2(inverseScreenSize.x,0.0) : vec2(0.0,inverseScreenSize.y);
// Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to step faster.
vec2 uv1 = currentUv - offset * QUALITY(0);
vec2 uv2 = currentUv + offset * QUALITY(0);

float lumaEnd1;
float lumaEnd2;

// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
bool reached1 = false;
bool reached2 = false;
bool reachedBoth = false;

此处用到的QUALITY函数定义如下,我觉得实际上就是根据迭代次数返回自适应的步长,不知道为什么要命名为QUALITY,挺奇怪的。

1
#define QUALITY(q) ((q) < 5 ? 1.0 : ((q) > 5 ? ((q) < 10 ? 2.0 : ((q) < 11 ? 4.0 : 8.0)) : 1.5))

之后就开始迭代了,主要思路就是沿着边缘逐步向两边走,直到迭代点的明度和起始区域的明度之差超过阈值,该阈值也就是之前计算的gradientScaled。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// If both sides have not been reached, continue to explore.
for(int i = 1; i < ITERATIONS; i++){
// If needed, read luma in 1st direction, compute delta.
if(!reached1){
lumaEnd1 = rgb2luma(textureLod(ldrTexture, uv1, 0.0).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached1 = abs(lumaEnd1) >= gradientScaled;
}
// If needed, read luma in opposite direction, compute delta.
if(!reached2){
lumaEnd2 = rgb2luma(textureLod(ldrTexture, uv2, 0.0).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached2 = abs(lumaEnd2) >= gradientScaled;
}
reachedBoth = reached1 && reached2;

// If the side is not reached, we continue to explore in this direction, with a variable quality.
if(!reached1){
uv1 -= offset * QUALITY(i);
}
if(!reached2){
uv2 += offset * QUALITY(i);
}

// If both sides have been reached, stop the exploration.
if(reachedBoth){ break;}
}

根据端点位置偏移采样点

首先,我们选择更近的端点。

1
2
3
4
5
6
7
   // Compute the distances to each side edge of the edge (!).
float distance1 = isHorizontal ? (texCoord.x - uv1.x) : (texCoord.y - uv1.y);
float distance2 = isHorizontal ? (uv2.x - texCoord.x) : (uv2.y - texCoord.y);

// In which direction is the side of the edge closer ?
bool isDirection1 = distance1 < distance2;
float distanceFinal = min(distance1, distance2);

接下来,我们要根据端点位置对着色像素进行抗锯齿,其中有两个关键点:

  1. 对于下图中的绿色像素,其完全落在原始边界内,无需抗锯齿;从端点上判断,就是如果近端点和着色点颜色相近,就无需抗锯齿

  2. 对于需要抗锯齿的像素(蓝色、黄色),离近端点越远,其需要混合的比例也就越小。

实现1的代码如下,可以理解为,如果着色点的明度小于周围平均明度,并且近端点明度也小于平均明度,则认为着色点和端点近色,无需抗锯齿。

1
2
3
4
5
6
7
8
9
// Is the luma at center smaller than the local average ?
bool isLumaCenterSmaller = lumaCenter < lumaLocalAverage;

// If the luma at center is smaller than at its neighbour, the delta luma at each end should be positive (same variation).
bool correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller;
bool correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller;

// Only keep the result in the direction of the closer side of the edge.
bool correctVariation = isDirection1 ? correctVariation1 : correctVariation2;

实现2的代码如下,基本上就是如果端点离得越近,采样点就越靠近边缘,也就是混合程度越高。

1
2
3
4
5
6
7
8
// Length of the edge.
float edgeLength = (distance1 + distance2);

// UV offset: read in the direction of the closest side of the edge.
float pixelOffset = - distanceFinal / edgeLength + 0.5;

// If the luma variation is incorrect, do not offset.
float finalOffset = correctVariation ? pixelOffset : 0.0;

结合额外的低通滤波

上述算法基于像素边缘和原始边界的对应关系,因此非常适用于长边的场合。但对于某些高频细节,原始边界可能就覆盖了屏幕空间中的一两个像素,这种算法自然无法解决了,因此FXAA中结合了额外的低通滤波,来补充对于高频细节的抗锯齿。

这里的低通滤波其实可以自我发挥想象,以任何方式加入FXAA。本文中的实现是通过着色像素周围的相对差值,并通过经验化的插值,得到低通滤波下的采样偏移,并与之前基于边缘抗锯齿得到的采样偏移取大值即可。

1
2
3
4
5
6
7
8
9
10
11
// Sub-pixel shifting
// Full weighted average of the luma over the 3x3 neighborhood.
float lumaAverage = (1.0/12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
// Ratio of the delta between the global average and the center luma, over the luma range in the 3x3 neighborhood.
float subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter)/lumaRange,0.0,1.0);
float subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
// Compute a sub-pixel offset based on this delta.
float subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;

// Pick the biggest of the two offsets.
finalOffset = max(finalOffset,subPixelOffsetFinal);

最后,我们得到了唯一的一个采样偏移,那么就将采样点向锯齿边缘偏移,通过一次纹理采样,来为FXAA谢幕吧。

1
2
3
4
5
6
7
8
9
10
11
// Compute the final UV coordinates.
vec2 finalUv = texCoord;
if(isHorizontal){
finalUv.y += finalOffset * stepLength;
} else {
finalUv.x += finalOffset * stepLength;
}

// Read the color at the new UV coordinates, and use it.
vec3 finalColor = textureLod(ldrTexture, finalUv, 0.0).rgb;
return finalColor;

结果分析

下图中分别展示了三种情况下的输出图像,我们发现FXAA中基于边缘的算法,可以有效解决一些低频处的锯齿问题,但会保留高频部分的细节(树枝处)。通过低通滤波的引入,可以进一步模糊化这些高频细节,但这种模糊化是否是所希望的。可能还得根据实际需求,仁者见仁,智者见智。

注意点

需要在LDR sRBG空间内进行FXAA,因为FXAA是基于感知而非基于物理的,在物理空间中进行FXAA会导致色调映射后仍然会走样。

参考资料

  1. FXAA 3.11 in 15 slides
  2. Open source FXAA code
  3. Nvidia FXAA White Paper

完整代码

完整的FXAA Fragment Shader代码奉上!(主要借鉴了Open source FXAA code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#version 430 core
out vec4 FragColor;
in vec2 texCoord;
uniform sampler2D ldrTexture;
uniform vec2 viewportSize;
const vec2 inverseScreenSize = 1/viewportSize;

// Settings for FXAA.
#define EDGE_THRESHOLD_MIN 0.0312
#define EDGE_THRESHOLD_MAX 0.125
#define QUALITY(q) ((q) < 5 ? 1.0 : ((q) > 5 ? ((q) < 10 ? 2.0 : ((q) < 11 ? 4.0 : 8.0)) : 1.5))
#define ITERATIONS 12
#define SUBPIXEL_QUALITY 0.75

float rgb2luma(vec3 rgb){
return dot(rgb, vec3(0.299, 0.587, 0.114));
}

vec3 fxaa(){
vec3 colorCenter = texture(ldrTexture, texCoord).rgb;
// Luma at the current fragment
float lumaCenter = rgb2luma(colorCenter);

// Luma at the four direct neighbours of the current fragment.
float lumaDown = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 0,-1)).rgb);
float lumaUp = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 0, 1)).rgb);
float lumaLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1, 0)).rgb);
float lumaRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1, 0)).rgb);

// Find the maximum and minimum luma around the current fragment.
float lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
float lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));

// Compute the delta.
float lumaRange = lumaMax - lumaMin;

// If the luma variation is lower that a threshold (or if we are in a really dark area), we are not on an edge, don't perform any AA.
if(lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)){
return colorCenter;
}

// Query the 4 remaining corners lumas.
float lumaDownLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1,-1)).rgb);
float lumaUpRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1, 1)).rgb);
float lumaUpLeft = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2(-1, 1)).rgb);
float lumaDownRight = rgb2luma(textureLodOffset(ldrTexture, texCoord, 0.0,ivec2( 1,-1)).rgb);

// Combine the four edges lumas (using intermediary variables for future computations with the same values).
float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;

// Same for corners
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;

// Compute an estimation of the gradient along the horizontal and vertical axis.
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp ) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners);

// Is the local edge horizontal or vertical ?
bool isHorizontal = (edgeHorizontal >= edgeVertical);

// Choose the step size (one pixel) accordingly.
float stepLength = isHorizontal ? inverseScreenSize.y : inverseScreenSize.x;

// Select the two neighboring texels lumas in the opposite direction to the local edge.
float luma1 = isHorizontal ? lumaDown : lumaLeft;
float luma2 = isHorizontal ? lumaUp : lumaRight;
// Compute gradients in this direction.
float gradient1 = luma1 - lumaCenter;
float gradient2 = luma2 - lumaCenter;

// Which direction is the steepest ?
bool is1Steepest = abs(gradient1) >= abs(gradient2);

// Gradient in the corresponding direction, normalized.
float gradientScaled = 0.25*max(abs(gradient1),abs(gradient2));

// Average luma in the correct direction.
float lumaLocalAverage = 0.0;
if(is1Steepest){
// Switch the direction
stepLength = - stepLength;
lumaLocalAverage = 0.5*(luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5*(luma2 + lumaCenter);
}

// Shift UV in the correct direction by half a pixel.
vec2 currentUv = texCoord;
if(isHorizontal){
currentUv.y += stepLength * 0.5;
} else {
currentUv.x += stepLength * 0.5;
}

// Compute offset (for each iteration step) in the right direction.
vec2 offset = isHorizontal ? vec2(inverseScreenSize.x,0.0) : vec2(0.0,inverseScreenSize.y);
// Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to step faster.
vec2 uv1 = currentUv - offset * QUALITY(0);
vec2 uv2 = currentUv + offset * QUALITY(0);

float lumaEnd1;
float lumaEnd2;

// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
bool reached1 = false;
bool reached2 = false;
bool reachedBoth = false;

// If both sides have not been reached, continue to explore.
for(int i = 1; i < ITERATIONS; i++){
// If needed, read luma in 1st direction, compute delta.
if(!reached1){
lumaEnd1 = rgb2luma(textureLod(ldrTexture, uv1, 0.0).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached1 = abs(lumaEnd1) >= gradientScaled;
}
// If needed, read luma in opposite direction, compute delta.
if(!reached2){
lumaEnd2 = rgb2luma(textureLod(ldrTexture, uv2, 0.0).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached2 = abs(lumaEnd2) >= gradientScaled;
}
reachedBoth = reached1 && reached2;

// If the side is not reached, we continue to explore in this direction, with a variable quality.
if(!reached1){
uv1 -= offset * QUALITY(i);
}
if(!reached2){
uv2 += offset * QUALITY(i);
}

// If both sides have been reached, stop the exploration.
if(reachedBoth){ break;}
}

// Compute the distances to each side edge of the edge (!).
float distance1 = isHorizontal ? (texCoord.x - uv1.x) : (texCoord.y - uv1.y);
float distance2 = isHorizontal ? (uv2.x - texCoord.x) : (uv2.y - texCoord.y);

// In which direction is the side of the edge closer ?
bool isDirection1 = distance1 < distance2;
float distanceFinal = min(distance1, distance2);

// Is the luma at center smaller than the local average ?
bool isLumaCenterSmaller = lumaCenter < lumaLocalAverage;

// If the luma at center is smaller than at its neighbour, the delta luma at each end should be positive (same variation).
bool correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller;
bool correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller;

// Only keep the result in the direction of the closer side of the edge.
bool correctVariation = isDirection1 ? correctVariation1 : correctVariation2;

// Length of the edge.
float edgeLength = (distance1 + distance2);

// UV offset: read in the direction of the closest side of the edge.
float pixelOffset = - distanceFinal / edgeLength + 0.5;

// If the luma variation is incorrect, do not offset.
float finalOffset = correctVariation ? pixelOffset : 0.0;

// Sub-pixel shifting
// Full weighted average of the luma over the 3x3 neighborhood.
float lumaAverage = (1.0/12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
// Ratio of the delta between the global average and the center luma, over the luma range in the 3x3 neighborhood.
float subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter)/lumaRange,0.0,1.0);
float subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
// Compute a sub-pixel offset based on this delta.
float subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;

// Pick the biggest of the two offsets.
finalOffset = max(finalOffset,subPixelOffsetFinal);

// Compute the final UV coordinates.
vec2 finalUv = texCoord;
if(isHorizontal){
finalUv.y += finalOffset * stepLength;
} else {
finalUv.x += finalOffset * stepLength;
}

// Read the color at the new UV coordinates, and use it.
vec3 finalColor = textureLod(ldrTexture, finalUv, 0.0).rgb;
return finalColor;
}

void main(){
FragColor = vec4(fxaa(),1);
}