写在前面

我在9月份的时候对博客的主页换了个模板,一些童鞋可能会发现边栏多了个小雨伞的动画,再细心的同学可能会发现如果一直开着我的博客电脑耗电更快了……当然啦,也有可能你看到的是一团黑,这说明你该换更高级的浏览器啦!

前几天有人问我这个是怎么实现的,其实我一直都想写一下的。这个例子的灵感是我暑假的时候喝了一杯奶茶(我超爱喝奶茶……),然后觉得杯子上的图案非常可爱(有点类似于吉米的那种画风)。当然啦,用我相信用PS很多人都可以画出来,后来我就想可以不可以在shadertoy上把这个图画出来?于是就有了画卡通伞的想法。

事实上,我和很多人一样,一直惊叹于shadertoy上那些宏大的场景竟然没有使用任何传统模型,而是用代码写出来的。其中一种重要的方法就是使用distance field。而雨伞其实二维的distance field的一个简单应用。

P.S. 如果你很好奇ShaderToy上那些效果都是怎么写的,可以看两个创始人在Siggraph Asia 2014上的一个课程(而且是在中国深圳讲的呦)—— Learn to Create Everything in a Fragment Shader。

我的Umbrella:

再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);
}

上面的代码都很简单,就是利用了圆和椭圆的公式,线段就是斜截式的变种,在之前的文章中都看过很多次。只是需要注意,distance function要保证图元内部的点返回值小于0,外部则大于0,顺序不要搞反。

有了距离,我们就可以据此来画图了。render函数就是做这个用的:

    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);
}

render接受三个参数,第一个就是距离值,第二个是需要绘制的颜色,第三个描边的宽度。我首先在render函数里绘制绘制颜色层,并进行了抗锯齿处理(原理参见之前的文章:http://blog.csdn.net/candycat1992/article/details/44673819),然后判断需不需要描边。如果需要的话在下面首先绘制一层描边层,再把颜色层混合上去。

有了这些函数,我们就可以在屏幕上画一些基本的图元了,例如:

那么,伞在哪里呢?完全看不到嘛!别着急,我们还需要把这些基本的图元组合起来,这可以依赖基本的并集、交集和差集操作:

    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);
}

读者如果还记得初中数学的话应该对上面的概念并不陌生。交集就是取A和B共同的部分,并集就是取A和B加起来的部分,而差集就是取A-B得到的部分。应用到distance field,那么交集就是取距离值a和b中较大的一个,这样只有其中有大于0的(在区域外)结果也会大于0,并集则取两者中较小的,这样只要其只能有小于0的(在区域内)结果也会小于0。而差集首先对b取反,即取b表示区域的补集,再和a对应的区域取交集就可以了。

上面这些函数,就是我们用到的所有函数。现在,就可以在真正开始画伞啦!

等等,在动手写代码前,我们需要首先安排下伞的各个部分的绘制部分。我把整个伞分成了三个部分:伞柄,伞身,和伞上的条纹,它们的绘制顺序也是依次从前往后。我把这三个部分绘制在不同的层上面,最后再按顺序混合它们。

伞柄

那么,第一步就是画伞柄:

    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);

老规矩,首先要计算当前绘制的这点在屏幕上的uv值。我们以水平方向的为基准,即变换后水平方向的值域为[0, 1],而竖直方向的要取决于分辨率。同时,为了方便后面定位一些参数和位置,我提前计算了描边的宽度值stroke,和屏幕中心的位置center。

伞柄还需要进一步细化它的结构。我是从下往上画的。首先,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);

当然,里面的参数也是手调的,凭感觉。这里描边的时候没有用之前计算的宽度定值strock,就因为椭圆函数的距离值并不是线性的,所以使用定值会使得描边得到的宽度不一致。所以改用导数了。

绘制完这一步就可以得到下面的效果了。

条纹

实际上,条纹才是整个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);
}

sinuv是基础的波浪式变化的屏幕坐标,所有条纹都是由它延伸出来的。由于这些条纹是随着时间不短向下移动,所以它们的位置可以使用不同的时间点来绘制。首先,我把条纹的循环时间定义为了3秒,这个值决定了后面的许多计算,例如要安排多少个条纹、它们的间距等等。每个条纹都是由两个椭圆取差集得到的,并和之前的结果取交集来不断增加条纹。啊,这里面的椭圆参数我就不解释了,其实如果现在让我再重现之前的实现我也很难做到了……不过可以说一下基本思路。对于每个条纹,我需要确定它们的长短轴的值。由于要实现从上到下弧度逐渐增大的效果,所以长短轴应该随着时间增加而增大。这个增大的幅度是很多实验得到的结果,最后计算得到了r0和r1这两个值。两个椭圆的中心位置,x分量好说,都是0.5,y分量的比较难办,因为想要实现从上到下宽度依次增加的效果,所以也是需要和t有关,但是如果两个椭圆关于t的系数是完全相等的话就无法实现改变宽度的效果,因此最后实验得到了r2这个参数。

完成后,把这三层和背景层混合后就得到了下面这样的效果。

Gamma校正

最后,在输出前进行伽马校正,得到最终的效果。

写在最后

有没有觉得distance field很神奇?的确,数学的魅力就是这么强大,哇哈哈哈。实际上,ShaderToy上很多3D效果也是基于这样的想法。我们看到很多看似很复杂的形状,往往也是由一些非常基本的三维图元变换而来的,例如球、三角锥、圆柱、长方体等等。那些大牛的厉害之处,在于他们可以随手拈来一些数学公式,把平淡无奇的图元逐渐变化成各种不可思议的图像。当然啦,人家的基本功肯定练了很多年了。大家可以在ShaderToy上直接搜distancefield,一定有很多不错的shader可以学习!

总之,希望这篇文章可以对一些人有所帮助。以后我会写些三维的,不过最近比较忙,可能又要搁置一段时间了,没办法,感觉要学要做的好多好多好多好多……

【ShaderToy】边栏的小雨伞的更多相关文章

  1. 微信小程序~TabBar底部导航切换栏

    底部导航栏这个功能是非常常见的一个功能,基本上一个完成的app,都会存在一个导航栏,那么微信小程序的导航栏该怎么实现呢?经过无数的踩坑,终于实现了,好了,先看看效果图. 对于底部导航栏,小程序上给出的 ...

  2. 微信小程序开发学习资料

    作者:初雪链接:https://www.zhihu.com/question/50907897/answer/128494332来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...

  3. Word分栏

    情景描述 Word分栏在小论文的撰写过程中是很常用的技术.但是,我们经常会遇到很难过的情况: 一段文字本来是连续分布的,可是当选择了分两栏         之后,开始部分在左边一栏,中间在右边一栏. ...

  4. 微信小程序小结02-- 完整的demo

    小程序确实方便,在移动端方便小个体宣传,不需要服务器和域名,还有客服功能.按朋友的意思,做了一次调整,分成了首页.预约和我的三个页面. 下面说下遇到的几个问题. 01.客服功能 不得不说这个一条龙服务 ...

  5. Confluence 6 注册单一小工具

    如果你不能订阅一个应用的小工具,你需要将小工具一个一个的添加进来.针对网站不支持小工具订阅和你的应用和你的 Confluence 不能建立信任连接的情况,你就只能这样添加了. 首先你需要获得小工具的 ...

  6. python 全栈开发,Day49(超链接导航栏案例,background,定位,z-index,iconfont使用)

    昨日内容回顾 浮动:是css中布局最多的一个属性 有浮动,一定要清除浮动 浮动不是一个元素单独浮动,要浮动一起浮动 清除浮动四种方式: 1.给父盒子添加高度,一般导航栏 2.给浮动元素后面加一个空的块 ...

  7. {03--CSS布局设置} 盒模型 二 padding bode margin 标准文档流 块级元素和行内元素 浮动 margin的用法 文本属性和字体属性 超链接导航栏 background 定位 z-index

    03--CSS布局设置 本节目录 一 盒模型 二 padding(内边距) 三 boder(边框) 四 简单认识一下margin(外边距) 五 标准文档流 六 块级元素和行内元素 七 浮动 八 mar ...

  8. 微信小程序踩坑集合

    1:官方工具:https://mp.weixin.qq.com/debug/w ... tml?t=1476434678461 2:简易教程:https://mp.weixin.qq.com/debu ...

  9. 微信小程序底部导航Tabbar

    1,底部导航栏这个功能是非常常见的一个功能,基本上一个完成的app,都会存在一个导航栏,那么微信小程序的导航栏该怎么实现呢?经过无数的踩坑,终于实现了,好了,先看看效果图. 2,对于底部导航栏,小程序 ...

随机推荐

  1. [HAOI2008]糖果传递

    题目描述 有n个小朋友坐成一圈,每人有ai个糖果.每人只能给左右两人传递糖果.每人每次传递一个糖果代价为1. 输入输出格式 输入格式: 小朋友个数n 下面n行 ai 输出格式: 求使所有人获得均等糖果 ...

  2. ●HDU 4787 GRE Words Revenge

    题链: http://acm.hdu.edu.cn/showproblem.php?pid=4787 题解: AC自动机(强制在线构造) 题目大意: 有两种操作, 一种为:+S,表示增加模式串S, 另 ...

  3. ●BZOJ 4556 [Tjoi2016&Heoi2016]字符串

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=4556 题解: 巨恶心...但是题很好呀,可以练习好几个比较麻烦的算法~ 1).预处理 首先用 ...

  4. 【SYZOI Round1】滑稽的树

    Description zzsyz实验楼里面种了一棵滑稽树,只有滑稽之力达到大乘期的oier才能看到.虽然我们看不到,但是还是知道一些信息: 这真的是一棵树,由n个节点,n-1条边联通.一号滑稽果同时 ...

  5. IPQ4028开启I2C功能

    0 概述 IPQ4028是一款集约式4核心ARM7 SOC芯片,内嵌独立双频WiFi子系统,offload式,支持MU-MIMO,最高支持1.2Gbps.标准的官方Demo方案中,IPQ4019开启了 ...

  6. C语言程序设计第二次作业—————顺序结构

    (一)改错题 1.输出带框文字:在屏幕上输出以下3行信息. ************* Welcome ************* 源程序 include int mian() { printf(&q ...

  7. php+xdebug+dbgp远程调试(多人)

    目录 创建 DBGP 服务 配置 调试 创建 DBGP 服务 到 下载页面,下载 python 版本的 dbgp 到服务器上. 解压后执行 pydbgpproxy. 如果提示找不到 dbgp 模块,则 ...

  8. c# 操作数据库

    查询 string strConnection = "Data Source=(local);Initial Catalog=zpractice;Integrated Security=SS ...

  9. 152. Maximum Product Subarray(中等, 神奇的 swap)

    Find the contiguous subarray within an array (containing at least one number) which has the largest ...

  10. No package tomcatX available. 解决办法

    当一个新的linux系统到手时,就要开始部署相关软件等等,有时候可能遇到无法安装的情况. 例如yum install tomcat7,在centos下无法安装,因为tomcat不再yum里面,怎么办呢 ...