FSR技术分析
2. EASU源码分析
2.3 FsrEasuF分析
1️⃣首先,就参数而言,主要是:
void FsrEasuF(
out AF3 pix,
AU2 ip, // 整数像素位置
AU4 con0, // FsrEasuCon中设置的四个const vector
AU4 con1,
AU4 con2,
AU4 con3)
然后,参考下图的12 tap
窗口的编号:
首先,我们获得f
的位置,得到fp
:
// Get position of 'f'.
AF2 pp = AF2(ip) * AF2_AU2(con0.xy) + AF2_AU2(con0.zw);
AF2 fp = floor(pp);
pp -= fp;
然后,根据输入的const向量,我们可以得到四个tap
的中心:
// Allowing dead-code removal to remove the 'z's.
// 以F为原点,定位到(0)
AF2 p0 = fp * AF2_AU2(con1.xy) + AF2_AU2(con1.zw);
// These are from p0 to avoid pulling two constants on pre-Navi hardware.
// 以(0)为原点。定位(1) (2) (3)
AF2 p1 = p0 + AF2_AU2(con2.xy);
AF2 p2 = p0 + AF2_AU2(con2.zw);
AF2 p3 = p0 + AF2_AU2(con3.xy);
2️⃣然后是以下的一大串调用:
AF4 bczzR = FsrEasuRF(p0);
AF4 bczzG = FsrEasuGF(p0);
AF4 bczzB = FsrEasuBF(p0);
AF4 ijfeR = FsrEasuRF(p1);
AF4 ijfeG = FsrEasuGF(p1);
AF4 ijfeB = FsrEasuBF(p1);
AF4 klhgR = FsrEasuRF(p2);
AF4 klhgG = FsrEasuGF(p2);
AF4 klhgB = FsrEasuBF(p2);
AF4 zzonR = FsrEasuRF(p3);
AF4 zzonG = FsrEasuGF(p3);
AF4 zzonB = FsrEasuBF(p3);
这些FsrEasuXF
函数是我们用户自定义的,在PostProcessFFX_FSR.usf
中可以找到其定义:
AF4 FsrEasuRF(AF2 p) { AF4 res = InputTexture.GatherRed (samLinearClamp, p, ASU2(0, 0)); return res; }
AF4 FsrEasuGF(AF2 p) { AF4 res = InputTexture.GatherGreen(samLinearClamp, p, ASU2(0, 0)); return res; }
AF4 FsrEasuBF(AF2 p) { AF4 res = InputTexture.GatherBlue (samLinearClamp, p, ASU2(0, 0)); return res; }
对于
GatherRed
类函数,可以参考https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/sm5-object-texture2d-gatherred。
所以,上面就是读取四个tap
的值,不过和一般的读取不同,我们是读取一个
2
×
2
2\times2
2×2区域的指定通道。所以我们可以很快的转换成luma
:
// Simplest multi-channel approximate luma possible (luma times 2, in 2 FMA/MAD).
AF4 bczzL = bczzB * AF4_(0.5) + (bczzR * AF4_(0.5) + bczzG);
AF4 ijfeL = ijfeB * AF4_(0.5) + (ijfeR * AF4_(0.5) + ijfeG);
AF4 klhgL = klhgB * AF4_(0.5) + (klhgR * AF4_(0.5) + klhgG);
AF4 zzonL = zzonB * AF4_(0.5) + (zzonR * AF4_(0.5) + zzonG);
虽然我们读取了16个vec3
数据,但是实际上正如之前的理论探讨所说,我们只需要12
个vec3
数据,接下来我们直接把所需的数据单独提取出来:
// Rename.
AF1 bL = bczzL.x;
AF1 cL = bczzL.y;
AF1 iL = ijfeL.x;
AF1 jL = ijfeL.y;
AF1 fL = ijfeL.z;
AF1 eL = ijfeL.w;
AF1 kL = klhgL.x;
AF1 lL = klhgL.y;
AF1 hL = klhgL.z;
AF1 gL = klhgL.w;
AF1 oL = zzonL.z;
AF1 nL = zzonL.w;
3️⃣接下里,就是对4
个taps
窗口进行分析,来计算得到direction
和length
:
// Accumulate for bilinear interpolation.
AF2 dir = AF2_(0.0);
AF1 len = AF1_(0.0);
FsrEasuSetF(dir,len,pp,true, false,false,false,bL,eL,fL,gL,jL);
FsrEasuSetF(dir,len,pp,false,true ,false,false,cL,fL,gL,hL,kL);
FsrEasuSetF(dir,len,pp,false,false,true ,false,fL,iL,jL,kL,nL);
FsrEasuSetF(dir,len,pp,false,false,false,true ,gL,jL,kL,lL,oL);
FsrEasuSetF分析
1️⃣首先,我们很快可以知道:这个函数的4
个bool
参数是用来区分tap
的位置,从而计算双线性插值的权重:
AF1 w = AF1_(0.0);
if(biS) w = (AF1_(1.0) - pp.x) * (AF1_(1.0) - pp.y);
if(biT) w = pp.x * (AF1_(1.0) - pp.y);
if(biU) w = (AF1_(1.0) - pp.x) * pp.y ;
if(biV) w = pp.x * pp.y ;
上面的权重计算怎么来的?首先,我们需要知道,
12 tap
窗口默认是套在f
编号上的(下图Q
点),也就是我们应该按照下图计算双线性插值:
然后,我们给tap
窗口进行编号,如下所示:
先以x
轴方向为例,计算direction
很简单,就是梯度,然后乘上双线性插值权重:
AF1 dirX = lD - lB;
dir.x += dirX * w;
2️⃣length
(边缘特征)的计算则要复杂很多:
// ASatF1 : saturate
AF1 dc = lD - lC;
AF1 cb = lC - lB;
AF1 lenX = max(abs(dc),abs(cb));
// 快速倒数 1/lenX
lenX = APrxLoRcpF1(lenX);
// 分子结合分母,然后clamp
lenX = ASatF1(abs(dirX) * lenX);
lenX *= lenX;
// 乘上双线性插值权重
len += lenX * w;
根据大佬博客和直接推导,可以很容易得到:EASU
定义的边缘特征的计算公式为:
以上代码就是 F X 2 FX^2 FX2的计算。
3️⃣之后就是在Y
轴重复一遍代码:
AF1 ec = lE-lC;
AF1 ca = lC-lA;
AF1 lenY = max(abs(ec),abs(ca));
lenY = APrxLoRcpF1(lenY);
AF1 dirY = lE-lA;
dir.y += dirY*w;
lenY = ASatF1(abs(dirY)*lenY);
lenY *= lenY;
len += lenY*w;
注意:len
越大,说明这个特征越是Large Feature
正如前文所说,算法会忽略thin Feature
——所以结果上,thin Feature
和平坦区域的计算结果都会一致的:
l
e
n
=
0
len=0
len=0
回到主函数1
1️⃣回到主函数,我们已经得到了双线性插值完毕的direction
和length
(边缘特征)。首先,我们对direction
进行归一化,由于要考虑除零问题,所以代码稍显复杂,但其实就是归一化:
AF2 dir2 = dir * dir;
AF1 dirR = dir2.x + dir2.y;
AP1 zro = dirR < AF1_(1.0 / 32768.0);
dirR = APrxLoRsqF1(dirR);
dirR = zro ? AF1_(1.0) : dirR;
dir.x = zro ? AF1_(1.0) : dir.x;
dir *= AF2_(dirR);
然后,根据以下公式,由length
计算出实际的Feature
(
F
/
2
F/2
F/2 是了从
[
0
,
2
]
[0,2]
[0,2]映射回
[
0
,
1
]
[0,1]
[0,1]):
// Transform from {0 to 2} to {0 to 1} range, and shape with square.
len = len * AF1_(0.5);
len *= len;
2️⃣我们然后计算stretch
变量,用于拉伸过滤核:对其轴线时,这个值为1
,对其对角线时,这个值为
2
\sqrt{2}
2。具体代码如下:
// Stretch kernel {1.0 vert|horz, to sqrt(2.0) on diagonal}.
// 因为归一化了,分子不就是1吗
AF1 stretch = (dir.x * dir.x + dir.y * dir.y) * APrxLoRcpF1(max(abs(dir.x), abs(dir.y)));
3️⃣然后计算len2
:
// Anisotropic length after rotation,
// x := 1.0 lerp to 'stretch' on edges
// y := 1.0 lerp to 2x on edges
AF2 len2 = AF2(AF1_(1.0) + (stretch - AF1_(1.0)) * len,AF1_(1.0) + AF1_(-0.5) * len);
数学上的形式如下:
大佬指出:为了减少锯齿,
EASU
还提出可以根据梯度和边缘信息进行缩放,EASU定义的缩放比例如上。
回顾第一大节,我们也说了:“长度用于在X
和Y
轴上对旋转后的内核进行缩放”。所以,我们得到的len2
是为了缩放过滤核的size
。这里就是我们要计算Feature
的两大目的之一了。
这里,回顾下缩放的解释。对于缩放,更加准确的算法解释是,此外,针对上诉公式,我对缩放区间进行了修改(为什么不一致呢?):
X
轴:缩放区间是 [ 2 2 , 1 ] [\frac{\sqrt{2}}{2},1] [22,1],对应的是从轴对齐到对角线。这意味着:对角线情况下使用更大的内核,来避免带状伪影(band
)。Y
轴:缩放区间是 [ 0.5 , 1 ] [0.5,1] [0.5,1],对应的是从small feature
到large feature
。这意味着:对小的特征使用无比例,这样就得到了一个小的对称核,它不会在特征本身之外采样。而当特征变大时,使用一个较长的核,这样我们可以更好地还原边缘。
为什么中写的是上诉呢?考虑一下,我们把输入值进行缩小,不就相当于放大过滤核嘛。所以实际上我们做的,确实是放大操作。然后来思考下缩放输入值(放大内核)的作用:我后续观察lanczos核,它的特点很明显: [ 0 , 1 ] [0,1] [0,1]区间是正权重, [ 1 , 1 w ] [1,\frac{1}{\sqrt{w}}] [1,w1]是负权重。而我们传入该核的输入值 x x x,实际上是该像素到目标像素的距离,而且这个距离还是在屏幕空间算的!——距离都会大于1!感觉讲不清楚,直接上图:
4️⃣然后计算:
// Based on the amount of 'edge',
// the window shifts from +/-{sqrt(2.0) to slightly beyond 2.0}.
AF1 lob = AF1_(0.5) + AF1_((1.0 / 4.0 - 0.04) - 0.5) * len;
// Set distance^2 clipping point to the end of the adjustable window.
AF1 clp = APrxLoRcpF1(lob);
这里又是干什么的,别忘了,我们使用的过滤核是lanczos
核,所以我们还一个值没有确定,那就是用于控制其形状的基数w
,而这个就是通过Feature
(也就是length
)获得的,这里直接给出映射公式:
w
=
1
2
−
1
4
F
e
a
t
u
r
e
w=\frac{1}{2}-\frac{1}{4}Feature
w=21−41Feature
那么上诉lob
的计算其实就是这个公式,也就是
l
o
b
=
w
lob=w
lob=w。那第二行呢,这个其实是为了裁剪——至于为什么,以及怎么得出裁剪值是
1
w
\frac{1}{\sqrt{w}}
w1,具体见:https://zhuanlan.zhihu.com/p/401030221。但作用实际就是:超过这个区间的像素值的权重都是0
。
5️⃣计算得到12 tap
窗口的最大值和最小值:
AF3 min4 = min(AMin3F3(AF3(ijfeR.z,ijfeG.z,ijfeB.z),AF3(klhgR.w,klhgG.w,klhgB.w),AF3(ijfeR.y,ijfeG.y,ijfeB.y)),
AF3(klhgR.x,klhgG.x,klhgB.x));
AF3 max4 = max(AMax3F3(AF3(ijfeR.z,ijfeG.z,ijfeB.z),AF3(klhgR.w,klhgG.w,klhgB.w),AF3(ijfeR.y,ijfeG.y,ijfeB.y)),
AF3(klhgR.x,klhgG.x,klhgB.x));
6️⃣然后,就是调用FsrEasuTapF
12次,累加aC
(颜色)和aW
(权重):
AF3 aC = AF3_(0.0);
AF1 aW = AF1_(0.0);
FsrEasuTapF(aC,aW,AF2( 0.0,-1.0)-pp,dir,len2,lob,clp,AF3(bczzR.x,bczzG.x,bczzB.x)); // b
FsrEasuTapF(aC,aW,AF2( 1.0,-1.0)-pp,dir,len2,lob,clp,AF3(bczzR.y,bczzG.y,bczzB.y)); // c
FsrEasuTapF(aC,aW,AF2(-1.0, 1.0)-pp,dir,len2,lob,clp,AF3(ijfeR.x,ijfeG.x,ijfeB.x)); // i
FsrEasuTapF(aC,aW,AF2( 0.0, 1.0)-pp,dir,len2,lob,clp,AF3(ijfeR.y,ijfeG.y,ijfeB.y)); // j
FsrEasuTapF(aC,aW,AF2( 0.0, 0.0)-pp,dir,len2,lob,clp,AF3(ijfeR.z,ijfeG.z,ijfeB.z)); // f
FsrEasuTapF(aC,aW,AF2(-1.0, 0.0)-pp,dir,len2,lob,clp,AF3(ijfeR.w,ijfeG.w,ijfeB.w)); // e
FsrEasuTapF(aC,aW,AF2( 1.0, 1.0)-pp,dir,len2,lob,clp,AF3(klhgR.x,klhgG.x,klhgB.x)); // k
FsrEasuTapF(aC,aW,AF2( 2.0, 1.0)-pp,dir,len2,lob,clp,AF3(klhgR.y,klhgG.y,klhgB.y)); // l
FsrEasuTapF(aC,aW,AF2( 2.0, 0.0)-pp,dir,len2,lob,clp,AF3(klhgR.z,klhgG.z,klhgB.z)); // h
FsrEasuTapF(aC,aW,AF2( 1.0, 0.0)-pp,dir,len2,lob,clp,AF3(klhgR.w,klhgG.w,klhgB.w)); // g
FsrEasuTapF(aC,aW,AF2( 1.0, 2.0)-pp,dir,len2,lob,clp,AF3(zzonR.z,zzonG.z,zzonB.z)); // o
FsrEasuTapF(aC,aW,AF2( 0.0, 2.0)-pp,dir,len2,lob,clp,AF3(zzonR.w,zzonG.w,zzonB.w)); // n
FsrEasuTapF分析
1️⃣首先,参数分析:
// Filtering for a given tap for the scalar.
void FsrEasuTapF(
inout AF3 aC, // 累积的颜色, with negative lobe.
inout AF1 aW, // 累积的权重.
AF2 off, // Pixel offset from resolve position to tap. 偏移
AF2 dir, // Gradient direction. 旋转过滤核
AF2 len, // Length. 缩放过滤核
AF1 lob, // Negative lobe strength. 控制基数w
AF1 clp, // Clipping point. 裁剪点
AF3 c){ // Tap color. tap的颜色
2️⃣使用方向offset
(本质就是旋转过滤核):
// Rotate offset by direction.
AF2 v;
v.x = (off.x * ( dir.x)) + (off.y * dir.y);
v.y = (off.x * (-dir.y)) + (off.y * dir.x);
关于旋转,和之前讨论的缩放区间问题是一致的,我们原理是要旋转过滤核,但操作上更方便的,还是旋转输入值,但也要注意:旋转输入值,是要反方向旋转,这才对应的正确的过滤核旋转!
然后,继续缩放:
// Anisotropy.
v *= len;
3️⃣计算当前的位置离resolve point
的距离,然后进行裁剪:
// Compute distance^2.
AF1 d2 = v.x * v.x + v.y * v.y;
// Limit to the window as at corner, 2 taps can easily be outside.
d2 = min(d2,clp);
4️⃣以下一大串都是,以w
和d2
(也就是x
)作为输入,来应用lanczos
核,也就是计算当前tap
的权重:
[
25
16
(
2
5
x
2
−
1
)
2
−
(
25
16
−
1
)
]
(
w
x
2
−
1
)
2
[\frac{25}{16}(\frac{2}{5}x^2-1)^2-(\frac{25}{16}-1)](wx^2-1)^2
[1625(52x2−1)2−(1625−1)](wx2−1)2
AF1 wB = AF1_(2.0 / 5.0) * d2 + AF1_(-1.0);
AF1 wA = lob * d2 + AF1_(-1.0);
wB *= wB;
wA *= wA;
wB = AF1_(25.0/16.0)*wB+AF1_(-(25.0/16.0-1.0));
AF1 w = wB * wA;
5️⃣最后,累加颜色和权重:
aC += c * w;
aW += w;
回到主函数2
C = 1 ∑ i = 0 n w i ∑ i = 1 n w i C i C=\frac{1}{\sum_{i=0}^{n}w_i}\sum_{i=1}^{n}w_iC_i C=∑i=0nwi1i=1∑nwiCi
最后,进行基操:累加的颜色,除以累加权重。然后使用之前计算好的局部最大值和最小值,对结果进行clamp
:
pix = min(max4, max(min4, aC * AF3_(ARcpF1(aW))));
Tip : 之后
最后就是RACS源码分析, 就是最后的 AMD FSR 1.0源码分析(三) 。
可能还会有FSR 2.0技术的分享,反正文件夹是建好了,嘿嘿。