左→右:Point \ Bi-Linear \ B-Spline \ Catmull-Rom 9 Samples \ Catmull-Rom 5 Samples
本文是对Bicubic Filtering in Fewer Taps一文的说明、补充。
常见贴图采样的Filter有Point、Bi-Linear、Tri-Linear等,其中Bi-Linear又可以当作双轴,每轴两次(数量)采样的一次(指数)滤波,这里的一次指的是采样权重函数里x的最大指数。
//采样权重函数
y = 1-x
w0 = 1-x;
w1 = x;
//考虑单轴向时
S = S0*w0 + S1*w1
以此类推,Bi-Cubic就是双轴,每轴四次(数量)采样的三次(指数)滤波。
//采样权重函数
y = ax^3 + bx^2 + cx + d;
float2 w0 = //...
float2 w1 = //...
float2 w2 = //...
float2 w3 = //...
//考虑单轴向时
S = S0*w0 + S1*w1 + S2*w2 + S3*w3;
本文将介绍两种三次函数B-Spline和Catmull-Rom,这里先忽略函数本身,专注在函数的采样过程上。
现在先考虑单轴,上图的绿色点为采样点,以采样点为中心展开权重函数。上图白色的格子是贴图的纹素,红色点为纹素中心。 这里把我的采样点'下落'到最近的纹素中心。下面代码中括号里的减0.5非常重要,这保证了f的范围落于0-1之间,整个采样点的范围活动在第二、三纹素之间(上图第二个绿色方格)。
float2 invTexSize = 1./texSize;
float2 iTc = uv*texSize;
//纹素的中心
float2 tc = floor( iTc - 0.5 ) + 0.5;
//f范围从0-1,落于上图第二个绿色方格中
float2 f = iTc - tc;
float2 f2 = f * f;
float2 f3 = f2 * f;
//用指定的曲线计算权重
float2 w0 = //...
float2 w1 = //...
float2 w2 = //...
float2 w3 = //...
根据上述的采样方法,可以得到x,y轴各4个的采样权重。组合成4x4共16个采样点的权重,继而对这些点进行采样配权得到最后的采样结果。
//get our texture coordinates
float2 tc0 = tc - 1;
float2 tc1 = tc;
float2 tc2 = tc + 1;
float2 tc3 = tc + 2;
//convert them to normalized coordinates
tc0 *= invTexSize;
tc1 *= invTexSize;
tc2 *= invTexSize;
tc3 *= invTexSize;
return
Texture2DSample( Tex2D, float2( tc0.x, tc0.y ) ) * w0.x * w0.y
+ Texture2DSample( Tex2D, float2( tc1.x, tc0.y ) ) * w1.x * w0.y
+ Texture2DSample( Tex2D, float2( tc2.x, tc0.y ) ) * w2.x * w0.y
+ Texture2DSample( Tex2D, float2( tc3.x, tc0.y ) ) * w3.x * w0.y
+ Texture2DSample( Tex2D, float2( tc0.x, tc1.y ) ) * w0.x * w1.y
+ Texture2DSample( Tex2D, float2( tc1.x, tc1.y ) ) * w1.x * w1.y
+ Texture2DSample( Tex2D, float2( tc2.x, tc1.y ) ) * w2.x * w1.y
+ Texture2DSample( Tex2D, float2( tc3.x, tc1.y ) ) * w3.x * w1.y
+ Texture2DSample( Tex2D, float2( tc0.x, tc2.y ) ) * w0.x * w2.y
+ Texture2DSample( Tex2D, float2( tc1.x, tc2.y ) ) * w1.x * w2.y
+ Texture2DSample( Tex2D, float2( tc2.x, tc2.y ) ) * w2.x * w2.y
+ Texture2DSample( Tex2D, float2( tc3.x, tc2.y ) ) * w3.x * w2.y
+ Texture2DSample( Tex2D, float2( tc0.x, tc3.y ) ) * w0.x * w3.y
+ Texture2DSample( Tex2D, float2( tc1.x, tc3.y ) ) * w1.x * w3.y
+ Texture2DSample( Tex2D, float2( tc2.x, tc3.y ) ) * w2.x * w3.y
+ Texture2DSample( Tex2D, float2( tc3.x, tc3.y ) ) * w3.x * w3.y;
这样就得到了一个Bi-Cubic Filter的采样结果。
上面的过程一共需要手动采样16次,借助硬件的Bi-Linear Filter,最理想的情况下可以把采样数降到4次。
首先,我们先专注在单轴相邻的两个纹素之间,从上诉代码中可以看到每行Sample都可以写成Texel * Weight的形式,因此我们可以把相邻纹素的和写成:
而我们对两个像素做线性插值时,插值的结果可以写成:
因为w1加w2并不一定会等于1,我们假设有个值s,使得s(w1+w2) = 1
将最上面的等式乘s,再将上式代入
这里可以发现式子成了做线性插值的形式,令t=sW2代入上式,也就是将我们的采样点放在sW2的位置上:
最后s被替换掉,采样权重和变成了权重和乘以线性插值的结果,这可能是近期见过最优美的算法了。
替换之后,原本16次的采样(红色点)变成了坐落在绿色区域里的4次采样(绿色点)。
下面开始介绍第一种曲线B-Spline,是一个零点对称的多段函数。
结合上面的采样图可以找到4个采样点的位置:
因为之前我们保证了f属于0-1之间,因此这里的4个采样点都确保落在了某个区间的函数上,直接代入即可得到权重,继而进行上面的合并优化计算。
Catmull-Rom的分段信息和B-Spline曲线一致,曲线走向如下:
因此采样点区间也可以确定使用的函数
但是有一个重要的区别就是:x在1-2区间里有负数。采样的位置t = sW2,也就是W2/(W1+W2),在W1或者W2中有一个值小于0时,都会导致t的值大于1。而t大于1意味着我们没办法借助硬件的Bi-Linear Filter来进行采样了。
这样对于轴向上包含负数区域的相邻点,就无法进行合并,只能单独采样。重新构建我们的采样点分布如下:
中间区域的4个点用上面的优化合并。4条边的中间两个点在作为函数负数区间的轴上直接舍弃,相当于在此轴上Point采样,而另一个轴向上正处于中间两个采样点的位置权重为正数,这样就规避了上面的问题。对于角上的4个点直接进行Point采样。这样一共需要1+4+4共9次采样。
//TextureSampling.ush l:164
// Reweight after removing the corners
float CornerWeights;
CornerWeights = Samples.Weight[0];
CornerWeights += Samples.Weight[1];
CornerWeights += Samples.Weight[2];
CornerWeights += Samples.Weight[3];
CornerWeights += Samples.Weight[4];
Samples.FinalMultiplier = 1 / CornerWeights;
最后,在虚幻引擎中很多用到Capmull-Rom算法的地方都去掉了角上的4个点(上图绿色叉点),对其他点加权后重新分配权重,这样只剩下5个采样,也是一种优化的方式。
看下曲线在1-2最小值,之所以能忽略掉这4个点,是因为即使在极限值情况下4个点权重和也只有0.074*0.074*4 = 0.021904,占整体权重的比例非常小。