【ShaderToy】基础篇之再谈抗锯齿(antialiasing,AA)

复杂的滤波操作大都只依赖于,“我们应该对图像滤波多少(也可以理解成模糊多大的范围)”。很容易理解,例如在如果相邻像素的UV值是一样的,那么我们就不需要采样啦,也就是说,这时可以不进行滤波。而现代很多GPU提供了偏导数函数给我们,在GLSL中,这个函数就是fwidth。当调用这个函数时,我们相当于问GPU:“嘿伙计,我想知道,参数这个值在屏幕的横纵方向的像素之间变化了多少?”fwidth函数返回的是X和Y方向偏导数的绝对值的和,而单方向的偏导可以通过ddx和ddy函数得到,这里不涉及了。当然,这些函数由于和像素有关,因此只能在Fragment Shader中访问。

例如,当我们写下fwidth(myVar)时,GPU将会返回myVar这个值在当前像素和它的下一个相邻像素之间的差值(与X和Y方向上的下一个像素上该值差的绝对值和)。也就是说,这个值其实就是直线的线性差值。一旦我们知道了在当前像素上这个值的变化程度,我们就可以进行合适程度的滤波操作。因此,对于一张纹理才说,当我们给定它的UV坐标后,更恰当的方法是不仅仅用这个UV坐标直接采样,还应该考虑其周围方形区域内纹理采样的结果,而这个区域就是ddx和ddy给定的区域。幸运的是,当我们调用tex2D这样的函数时,系统背后已经为我们完成了这个操作。而在一些高级的profiles中,还会允许我们自定义滤波窗口的大小。我们以Unity中的代码为例:

float4 fragColor = tex2D(_MainTex, i.uv);上述代码等价于:float4 fragColor = tex2D(_MainTex, i.uv, ddx(i.uv), ddy(i.uv));我们可以自定义滤波大小:float4 fragColor = tex2D(_MainTex, i.uv, float2(0), float2(0));上述代码意味着,我们强制滤波大小为0,也就是说,总是使用最近邻(Nearest-Neighbor)方法进行滤波,不考虑API状态和mipmaps。下面的图分别表示了滤波大小为*1,*5和*0的效果,注意场景视图中mipmap的使用情况,,在大小为0是没有使用任何mipmap。

对于不支持这些函数的硬件,可以使用其他方法代替,有兴趣的可以看这篇文章:

Point Style Sampling

上面的方法在point style sampling时同样可以抗锯齿,可以参见我在ShaderToy中写的一个合并版:https://www.shadertoy.com/view/XtB3zw。实现细节也请移步去那里看。这里只讲关键部分。下面表示了不同放缩程度下的效果:

这里我只分析下里面用到的不同的采样方法(依次对应从左到右的顺序)。代码如下:

vec4 AntiAlias_None(vec2 uv, vec2 texsize) {return texture2D(iChannel0, uv / texsize, -99999.0);}vec4 AntiAliasPointSampleTexture_None(vec2 uv, vec2 texsize) {return texture2D(iChannel0, (floor(uv+0.5)+0.5) / texsize, -99999.0);}vec4 AntiAliasPointSampleTexture_Smoothstep(vec2 uv, vec2 texsize) {vec2 w=fwidth(uv);return texture2D(iChannel0, (floor(uv)+0.5+smoothstep(0.5-w,0.5+w,fract(uv))) / texsize, -99999.0);}vec4 AntiAliasPointSampleTexture_Linear(vec2 uv, vec2 texsize) {vec2 w=fwidth(uv);return texture2D(iChannel0, (floor(uv)+0.5+clamp((fract(uv)-0.5+w)/w,0.,1.)) / texsize, -99999.0);}vec4 AntiAliasPointSampleTexture_ModifiedFractal(vec2 uv, vec2 texsize) {uv.xy -= 0.5;vec2 w=fwidth(uv);return texture2D(iChannel0, (floor(uv)+0.5+min(fract(uv)/min(w,1.0),1.0)) / texsize, -99999.0);}

总结一下,我们通过点采样的多种方法来演示如何使用fwidth+smoothstep+clamp进行抗锯齿,而这三者的组合也是非常常见的。

过程纹理的抗锯齿

对于使用过程纹理(Procedure Texture)的shaders来说,知道上面的知识是有助于我们理解如何进行抗锯齿的。

过程纹理往往都是通过一些函数来得到的,像ShaderToy中那样,因此抗锯齿往往就需要在像素间评估函数的值来实现。最简单的情况就是,我们可以在边界处取平均值,像多重采样那样。

考虑我们之前圆和直线的例子,其实就是只考虑了边界。当我们判断该像素在边界时,就将圆的颜色和其他颜色进行混合。但是,如一开始所说,这种抗锯齿方法仅适用于根据欧氏距离判断边界的情况,对于那些不能用欧式距离表示的距离函数来说,这种方法在边界处还是会产生锯齿。

我们还是来画画直线和圆好啦。只是这次我们要画一个由线段和圆组成的正四面体:线段组成了四面体的六条边,每个顶点用圆来表示。为了更具有一般性,我们改变一下之前计算直线和圆的函数:

离开你的那一天开始,左心房渐渐停止跳动…

【ShaderToy】基础篇之再谈抗锯齿(antialiasing,AA)

相关文章:

你感兴趣的文章:

标签云: