标签:shader
在之前的基础篇中,我们讲到了在绘制点线时如何处理边缘的锯齿,也就是使用smoothstep函数。而模糊参数是一些定值,或者是跟屏幕分辨率相关的数值,例如分辨率宽度的5%等等。但这种方法其实是有一种问题的。这需要我们从绘制的图像说起。
ShaderToy中绘制的很多图像可以说是一种Procedure Texture,过程纹理,即是计算机生成的纹理。拿之前画的圆和线来说,这些圆和线的绘制过程,是我们计算每个fragment到“期望图像”的距离,然后根据距离来判断使用哪种颜色。如果这个距离是在欧式空间,即圆和线的例子中那样,那么直接使用定制或者和屏幕系数相关的值作为模糊参数是没有问题的。但一旦这个距离无法用欧氏距离来表示(有很多这样的情况,例如判断到一条正弦曲线的距离,就无法使用点到直线的距离这样简单的公式了),使用这种参数是无法得到很好的模糊效果的。在ShaderToy中,很多处理方法其实就是在进行图片处理工作。
这种时候,我们就可以利用另一个函数fwidth,和smoothstep等函数合作来达到抗锯齿的目的,而这种方法实际上更加通用。这里还将介绍一种新的函数clamp。
本篇涉及到的原ShaderToy中的例子主要有:
https://www.shadertoy.com/view/4ssSRl
https://www.shadertoy.com/view/ldsSRX
https://www.shadertoy.com/view/ldlSzS
https://www.shadertoy.com/view/MsjSzy
我整理的效果如下:
https://www.shadertoy.com/view/XtB3zw
https://www.shadertoy.com/view/4tB3zm
以及GPU Gems的一篇文章:
http://http.developer.nvidia.com/GPUGems/gpugems_ch24.html
我们一定听说过抗锯齿这件事情,而不出意外的话,我们肯定也听说过多重采样这个抗锯齿途径。多重采样简单来说就是像素点的颜色是和其相邻的一些像素值有关的。我们把多重采样再一般化,实际上是是一个对图像求卷积的过程。输入图像A,经过某个定义的kernel进行卷积处理后,得到B,我们希望B中的锯齿尽可能少。很多GPU已经提供了一些快速的滤波操作,例如线性滤波等等。多重采样使用的kernel往往是一个长方形,每个点对应的权重是一样的,然后对原图像进行卷积,就可以输出抗锯齿后的图像B了。但是如果我们想要进行更复杂的滤波,就需要自定义kernel。当然,kernel的用处不仅仅是抗锯齿,还可以进行边界检测等等,我后面的文章可能会讲到。这里,我们仅讨论用于抗锯齿的kernel。
举个例子,我们要渲染一个有纹理的长方形,当知道了该像素对应的纹理UV坐标后,如果我们直接利用texture2D访问纹理,那么最终在长方形的边界就会出现明显锯齿。而如果我们利用多重采样的思想,就会在每次计算时,不仅仅对该像素对应的UV进行采样,还会调用多次texture2D函数,最后将结果相加取平均值。
常见的抗锯齿kernel有Box、Cubic等等。更多内容可以参考上面的GPU Gems中的文章。
复杂的滤波操作大都只依赖于,“我们应该对图像滤波多少(也可以理解成模糊多大的范围)”。很容易理解,例如在如果相邻像素的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));
对于不支持这些函数的硬件,可以使用其他方法代替,有兴趣的可以看这篇文章:
http://http.developer.nvidia.com/GPUGems/gpugems_ch25.html
上面的方法在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中那样,因此抗锯齿往往就需要在像素间评估函数的值来实现。最简单的情况就是,我们可以在边界处取平均值,像多重采样那样。
考虑我们之前圆和直线的例子,其实就是只考虑了边界。当我们判断该像素在边界时,就将圆的颜色和其他颜色进行混合。但是,如一开始所说,这种抗锯齿方法仅适用于根据欧氏距离判断边界的情况,对于那些不能用欧式距离表示的距离函数来说,这种方法在边界处还是会产生锯齿。
我们还是来画画直线和圆好啦。只是这次我们要画一个由线段和圆组成的正四面体:线段组成了四面体的六条边,每个顶点用圆来表示。为了更具有一般性,我们改变一下之前计算直线和圆的函数:
float line(vec2 pos, vec2 point1, vec2 point2, float width) { vec2 dir0 = point2 - point1; vec2 dir1 = pos - point1; float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0); return (length(dir1 - dir0 * h) - width * 0.5); } float circle(vec2 pos, vec2 center, float radius) { float d = length(pos - center) - radius; return d; }
vec2 originalPos = (2.0 * fragCoord - iResolution.xy)/iResolution.yy; vec2 pos = originalPos; // Twist // pos.x += 0.5 * sin(5.0 * pos.y); // Background vec3 col = _BackgroundColor.rgb * (1.0-0.2*length(originalPos)); float speed = 0.2; float l = 0.8; vec3 p0 = vec3(l, 0, pi * 0.5); vec3 p1 = vec3(l, pi * 0.5, pi); vec3 p2 = vec3(l, pi * 0.5, pi + pi * 0.66); vec3 p3 = vec3(l, pi * 0.5, pi + pi * 1.33); vec2 point0 = vec2(cos(p0.z), sin(p0.z)) * sin(p0.y) * p0.x; vec2 point1 = vec2(cos(p1.z), sin(p1.z)) * sin(p1.y) * p1.x; vec2 point2 = vec2(cos(p2.z), sin(p2.z)) * sin(p2.y) * p2.x; vec2 point3 = vec2(cos(p3.z), sin(p3.z)) * sin(p3.y) * p3.x; float d = line(pos, point0, point1, _LineWidth); d = min(d, line(pos, point1, point2, _LineWidth)); d = min(d, line(pos, point2, point3, _LineWidth)); d = min(d, line(pos, point0, point2, _LineWidth)); d = min(d, line(pos, point0, point3, _LineWidth)); d = min(d, line(pos, point1, point3, _LineWidth)); d = min(d, circle(pos, point0, _CircleRadius)); d = min(d, circle(pos, point1, _CircleRadius)); d = min(d, circle(pos, point2, _CircleRadius)); d = min(d, circle(pos, point3, _CircleRadius));
接下来就是抗锯齿的部分:
if (originalPos.x < split.x) { col = mix(_OutlineColor.rgb, col, step(0, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, step(0, d)); } else if (originalPos.y > split.y) { float w = _Antialias; col = mix(_OutlineColor.rgb, col, smoothstep(-w, w, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, smoothstep(-w, w, d)); } else { float w = fwidth(0.5*d) * 2.0; col = mix(_OutlineColor.rgb, col, smoothstep(-w, w, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, smoothstep(-w, w, d)); } // Draw split lines col = mix(vec3(0), col, smoothstep(0.005, 0.007, abs(originalPos.x - split.x))); col = mix(col, vec3(0), (1 - smoothstep(0.005, 0.007, abs(originalPos.y - split.y))) * step(split.x, originalPos.x));
保存后,我们可以得到下面的效果:
为了更好的效果,我们可以添加时间参数让它旋转起来,并且接受鼠标事件来移动分割线的位置,鼠标事件的使用可以参见更新后的开篇。完整的代码如下:
Shader "shadertoy/AA Line" { Properties{ _CircleRadius ("Circle Radius", Range(0, 0.1)) = 0.05 _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.01 _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1) _LineWidth ("Line Width", Range(0, 0.1)) = 0.01 _LineColor ("Line Color", Color) = (1, 1, 1, 1) _Antialias ("Antialias Factor", Range(0, 0.05)) = 0.01 _BackgroundColor ("Background Color", Color) = (1, 1, 1, 1) iMouse ("Mouse Pos", Vector) = (100,100,0,0) iChannel0("iChannel0", 2D) = "white" {} iChannelResolution0 ("iChannelResolution0", Vector) = (100,100,0,0) } CGINCLUDE #include "UnityCG.cginc" #pragma target 3.0 #pragma glsl #define vec2 float2 #define vec3 float3 #define vec4 float4 #define mat2 float2x2 #define iGlobalTime _Time.y #define mod fmod #define mix lerp #define atan atan2 #define fract frac #define texture2D tex2D // 屏幕的尺寸 #define iResolution _ScreenParams // 屏幕中的坐标,以pixel为单位 #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) #define PI2 6.28318530718 #define pi 3.14159265358979 #define halfpi (pi * 0.5) #define oneoverpi (1.0 / pi) float _CircleRadius; float _OutlineWidth; float4 _OutlineColor; float _LineWidth; float4 _LineColor; float _Antialias; float4 _BackgroundColor; fixed4 iMouse; sampler2D iChannel0; fixed4 iChannelResolution0; struct v2f { float4 pos : SV_POSITION; float4 srcPos : TEXCOORD0; }; // precision highp float; v2f vert(appdata_base v) { v2f o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); o.srcPos = ComputeScreenPos(o.pos); return o; } vec4 main(vec2 fragCoord); fixed4 frag(v2f _iParam) : COLOR0 { vec2 fragCoord = gl_FragCoord; return main(gl_FragCoord); } float line(vec2 pos, vec2 point1, vec2 point2, float width) { vec2 dir0 = point2 - point1; vec2 dir1 = pos - point1; float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0); return (length(dir1 - dir0 * h) - width * 0.5); } float circle(vec2 pos, vec2 center, float radius) { float d = length(pos - center) - radius; return d; } vec4 main(vec2 fragCoord) { vec2 originalPos = (2.0 * fragCoord - iResolution.xy)/iResolution.yy; vec2 pos = originalPos; // Twist // pos.x += 0.5 * sin(5.0 * pos.y); vec2 split = vec2(0, 0); if (iMouse.z > 0.0) { split = (-iResolution.xy + 2.0 * iMouse.xy) / iResolution.yy; } // Background vec3 col = _BackgroundColor.rgb * (1.0-0.2*length(originalPos)); float speed = 0.2; float l = 0.8; vec3 p0 = vec3(l, 0 + speed * iGlobalTime, pi * 0.5 + speed * iGlobalTime); vec3 p1 = vec3(l, pi * 0.5 +speed * iGlobalTime, pi +speed * iGlobalTime); vec3 p2 = vec3(l, pi * 0.5 +speed * iGlobalTime, pi + pi * 0.66 + speed * iGlobalTime); vec3 p3 = vec3(l, pi * 0.5 +speed * iGlobalTime, pi + pi * 1.33 + speed * iGlobalTime); vec2 point0 = vec2(cos(p0.z), sin(p0.z)) * sin(p0.y) * p0.x; vec2 point1 = vec2(cos(p1.z), sin(p1.z)) * sin(p1.y) * p1.x; vec2 point2 = vec2(cos(p2.z), sin(p2.z)) * sin(p2.y) * p2.x; vec2 point3 = vec2(cos(p3.z), sin(p3.z)) * sin(p3.y) * p3.x; float d = line(pos, point0, point1, _LineWidth); d = min(d, line(pos, point1, point2, _LineWidth)); d = min(d, line(pos, point2, point3, _LineWidth)); d = min(d, line(pos, point0, point2, _LineWidth)); d = min(d, line(pos, point0, point3, _LineWidth)); d = min(d, line(pos, point1, point3, _LineWidth)); d = min(d, circle(pos, point0, _CircleRadius)); d = min(d, circle(pos, point1, _CircleRadius)); d = min(d, circle(pos, point2, _CircleRadius)); d = min(d, circle(pos, point3, _CircleRadius)); if (originalPos.x < split.x) { col = mix(_OutlineColor.rgb, col, step(0, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, step(0, d)); } else if (originalPos.y > split.y) { float w = _Antialias; col = mix(_OutlineColor.rgb, col, smoothstep(-w, w, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, smoothstep(-w, w, d)); } else { float w = fwidth(0.5*d) * 2.0; col = mix(_OutlineColor.rgb, col, smoothstep(-w, w, d - _OutlineWidth)); col = mix(_LineColor.rgb, col, smoothstep(-w, w, d)); } // Draw split lines col = mix(vec3(0), col, smoothstep(0.005, 0.007, abs(originalPos.x - split.x))); col = mix(col, vec3(0), (1 - smoothstep(0.005, 0.007, abs(originalPos.y - split.y))) * step(split.x, originalPos.x)); return vec4(col, 1.0); } ENDCG SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest ENDCG } } FallBack Off }
上面使用的距离其实还是欧式距离,因此使用定值或是fwidth看起来似乎没什么差别。但是,如果我们对它做少许改变,例如取消下面的注释:
// Twist // pos.x += 0.5 * sin(5.0 * pos.y);
右下角使用fwidth的区域仍然可以保持良好的抗锯齿状态,而右上角已经开始出现一定量的锯齿了。虽然我们可以通过调大定值_Antialias的方法来增大模糊范围,但这样会造成有些区域过于模糊,而且这种根据情况来调整定值的方法会很麻烦。而使用fwidth的话,我们就不需要考虑这些因素,因为它是直接得到和临近像素之间的差值,不依赖与欧氏距离。
这篇文章老实说来有点杂,我尽我可能想把问题阐述清楚了。。。总结一下,这篇主要想要介绍fwidth这个函数在抗锯齿方面的应用,除此之外还介绍了clamp,当然我个人还是比较喜欢用smoothstep。。。但一个是线性插值的,一个的三次插值的,性能上有少许差别。效果上也有一点思维差别,这要根据项目需要有所选择。
好啦,下一篇我想写下各种插值曲线的细节,包括贝塞尔曲线,Catmull-Rom样条等等。我自己也总是忘。。。写一篇备忘一下。
【ShaderToy】基础篇之再谈抗锯齿(antialiasing,AA)
标签:shader
原文地址:http://blog.csdn.net/candycat1992/article/details/44673819