写在前面
原文链接:http://blog.csdn.net/candycat1992/article/details/49389905
再P.S. 很兴奋的是,Iq给这个作品留言啦,被偶像说cute好开心呀,哇哈哈哈
再再P.S.小雨伞现已加入我的Github项目Shadertoy_Lab(https://github.com/candycat1992/Shadertoy_Lab)。
什么是Distance Field
Distance Field(中文翻译为距离场?)的含义很好理解,我们可以用它来判断一个点是否在一个区域内。我们往往用一个函数来表示某个需要绘制图形的distance function,然后把屏幕上某点的位置代入计算,如果得到的值为负,那么该点就在该图形内部,如果为正,就在图形外部。这种思想看似很简单,但实际上当使用一些复杂的distance function后,就可以得到非常复杂的场景,再配合使用一些光照、图形处理的技术,就可以得到非常出色的画面效果。Iq(Inigo Quilez,Shadertoy的创始人之一)在他的博客里概述过distance field的技术。感兴趣的一定要去拜读一下。
在Umbrellar的例子里,我实际上只是非常简单地应用了一下二维空间里的distance field。这些效果都是由简单的圆、椭圆、有宽度的线段变化而来的,配合使用了并集(Union)、交集(Intersection)、差集(Difference)的操作。这些变化大部分是使用了正弦函数和一些简单的线性方程(例如伞上的条纹),只是为了得到比较好的效果需要不断尝试各种参数。
我是怎么实现的
我一开始就计划伞大概可以用一些基本图元来表示,例如伞的主题可以用两个椭圆的交集画,伞柄可以用线段+圆的并集+差集,至于伞上面的条纹其实也是很多椭圆的交集+差集画出来的。看到这里有些人可能会觉得有些混乱,实际上你在脑海里想象一下这些图元的组合关系就可以明白了。
所以,我只需要对三个图元——圆、椭圆和线段定义它们的distance function就可以了(实际上椭圆是圆的超集,但为了方便我还是把圆和椭圆分开了):
float sdfCircle(vec2 center, float radius, vec2 coord) { vec2 offset = coord - center; return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius; } float sdfEllipse(vec2 center, float a, float b, vec2 coord) { float a2 = a * a; float b2 = b * b; return (b2 * (coord.x - center.x) * (coord.x - center.x) + a2 * (coord.y - center.y) * (coord.y - center.y) - a2 * b2)/(a2 * b2); } float sdfLine(vec2 p0, vec2 p1, float width, vec2 coord) { vec2 dir0 = p1 - p0; vec2 dir1 = coord - p0; float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0); return (length(dir1 - dir0 * h) - width * 0.5); }
vec4 render(float d, vec3 color, float stroke) { //stroke = fwidth(d) * 2.0; float anti = fwidth(d) * 1.0; vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d)); if (stroke < 0.000001) { return colorLayer; } vec4 strokeLayer = vec4(vec3(0.05, 0.05, 0.05), 1.0 - smoothstep(-anti, anti, d - stroke)); return vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a); }
有了这些函数,我们就可以在屏幕上画一些基本的图元了,例如:
float sdfUnion( const float a, const float b ) { return min(a, b); } float sdfDifference( const float a, const float b) { return max(a, -b); } float sdfIntersection( const float a, const float b ) { return max(a, b); }
上面这些函数,就是我们用到的所有函数。现在,就可以在真正开始画伞啦!
等等,在动手写代码前,我们需要首先安排下伞的各个部分的绘制部分。我把整个伞分成了三个部分:伞柄,伞身,和伞上的条纹,它们的绘制顺序也是依次从前往后。我把这三个部分绘制在不同的层上面,最后再按顺序混合它们。
伞柄
那么,第一步就是画伞柄:
vec4 main(vec2 fragCoord) { float size = min(iResolution.x, iResolution.y); float pixSize = 1.0 / size; vec2 uv = fragCoord.xy / iResolution.x; float stroke = pixSize * 1.5; vec2 center = vec2(0.5, 0.5 * iResolution.y/iResolution.x); // Draw the handle float bottom = 0.08; float handleWidth = 0.01; float handleRadius = 0.04; float d = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius, uv); float c = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius-handleWidth, uv); d = sdfDifference(d, c); c = uv.y - bottom; d = sdfIntersection(d, c); c = sdfLine(vec2(0.5, center.y*2.0-0.05), vec2(0.5, bottom), handleWidth, uv); d = sdfUnion(d, c); c = sdfCircle(vec2(0.5, center.y*2.0-0.05), 0.01, uv); d = sdfUnion(c, d); c = sdfCircle(vec2(0.5-handleRadius*2.0+handleWidth, bottom), handleWidth*0.5, uv); d = sdfUnion(c, d); vec4 layer0 = render(d, vec3(0.404, 0.298, 0.278), stroke);
伞柄还需要进一步细化它的结构。我是从下往上画的。首先,1)画一个落空的圆圈(对两个圆去差集),2)再去掉上半部分只留下半部分(交集),3)之后画一条表示主杆的线段(取并集)。4)伞头我想用一个更大的圆表示,所以又画了一个半径更大的圆(取并集)。5)最后,手握的那里有些突兀,所以又使用了一个圆(取并集)。得到最后的距离值后,使用棕色绘制出来,伞柄部分完成。这个过程可以用下面的图展示。当然啦,里面的位置和参数都是手调的,试了很多次,还要考虑屏幕分辨率的变化。
和伞柄相比,伞身就更加简单了。只需要用两个长短轴不同的椭圆,然后对它们取交集即可:
float a = sdfEllipse(vec2(0.5, center.y * 2.0 - 0.34), 0.25, 0.25, uv); float b = sdfEllipse(vec2(0.5, center.y * 2.0 + 0.03), 0.8, 0.35, uv); b = sdfIntersection(a, b); vec4 layer1 = render(b, vec3(0.32, 0.56, 0.53), fwidth(b) * 2.0);
绘制完这一步就可以得到下面的效果了。
实际上,条纹才是整个shader里最麻烦的部分。我一开始就想到使用正弦函数来模拟波浪的效果,但是为了让这些条纹有从上到下逐渐加宽、弧度逐渐增大的效果,还是调了很一会。
// Draw strips vec4 layer2 = layer1; float t, r0, r1, r2, e, f; vec2 sinuv = vec2(uv.x, (sin(uv.x * 40.0) * 0.02 + 1.0) * uv.y); for (float i = 0.0; i < 10.0; i++) { t = mod(iGlobalTime + 0.3 * i, 3.0) * 0.2; r0 = (t - 0.15) / 0.2 * 0.9 + 0.1; r1 = (t - 0.15) / 0.2 * 0.1 + 0.9; r2 = (t - 0.15) / 0.2 * 0.15 + 0.85; e = sdfEllipse(vec2(0.5, center.y * 2.0+0.37 - t * r2), 0.7 * r0, 0.35 * r1, sinuv); f = sdfEllipse(vec2(0.5, center.y * 2.0+0.41 - t), 0.7 * r0, 0.35 * r1, sinuv); f = sdfDifference(e, f); f = sdfIntersection(f, b); vec4 layer = render(f, vec3(1.0, 0.81, 0.27), 0.0); layer2 = mix(layer2, layer, layer.a); }
完成后,把这三层和背景层混合后就得到了下面这样的效果。
最后,在输出前进行伽马校正,得到最终的效果。
有没有觉得distance field很神奇?的确,数学的魅力就是这么强大,哇哈哈哈。实际上,ShaderToy上很多3D效果也是基于这样的想法。我们看到很多看似很复杂的形状,往往也是由一些非常基本的三维图元变换而来的,例如球、三角锥、圆柱、长方体等等。那些大牛的厉害之处,在于他们可以随手拈来一些数学公式,把平淡无奇的图元逐渐变化成各种不可思议的图像。当然啦,人家的基本功肯定练了很多年了。大家可以在ShaderToy上直接搜distancefield,一定有很多不错的shader可以学习!
总之,希望这篇文章可以对一些人有所帮助。以后我会写些三维的,不过最近比较忙,可能又要搁置一段时间了,没办法,感觉要学要做的好多好多好多好多……