中秋将至,教你使用shader画月亮,完结(第一次写教程,不足之处多多见谅)

工程文件: moon.zip (814.7 KB)
最终期望效果(shadertoy实现):


cocos完成效果(为了方便理解将一些步骤简化,以及少了纹理实现,效果差了好多哎):

shadertoy页面查看效果:moon
说明:

  1. 本人也是初学者,所以难免一些地方表达不清,还望多多包涵。
  2. 本文并不针对完全的初学者,对一些基础函数不会去深入讲解,例如smoothstep等,初学者跟着学的时候希望能自己多多去查询相关api这些才能明白原理,这样才能完全摸透知识点
    3.为了赶在中秋前完成,在公司加班到了11点多,如果此篇对你有用还希望点个赞,如果觉得不足的地方,也请指出我以后好纠正

准备工作: 本人cocos creator 2.4.3版本
如何创建材质和shader 参考王师傅的帖子画一道彩虹 ,本文不在累赘

step1: 画星空
思路:对一个黑底上的像素点进行随机,随机1的为星星,随机到0的,则无,然后对这些星星利用时间函数进行透明度的调整。 有了思路咱们就开干吧
首先我们得确定一个黑夜的底颜色,这决定了一个黑夜的整体色调,我从网上找了一张星空图对其的底色进行萃取后,大致的颜色值为0.1,0.15,0.2,那么在像素着色器的main函数里将颜色改成这个,代码如下 :image
我们将得到接近于纯黑色的黑夜。
然后可以往上画星星了, gpu最难的一部分就在于随机了,随机算法都得亲自实现,首先我这里提供一个hash到0到1的算法
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * MOD3);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.x + p3.y) * p3.z);
}
这个hash函数会返回一个0到1的伪随机数,此时我们可以在main里面对每个点进行一下hash,main代码修改后如下: image
此时我们将会得到如下的一张雪花的图片image 那么雪花图片怎么变成星星呢? 这里的思路是将每个点进行一个幂操作,这样接近0的值会更接近于0,而越接近于1的还会比较接近于1,此时main函数中的颜色加上hash的代码改为
col += pow(hash(v_uv010000.),1000.);
我们进行1000次幂后发现已经挺像星空了,效果图如下:image
但我们要明白gpu擅长的是多线处理,却并不擅长计算,1000次的幂是不是太浪费gpu的效率了,我们该如何提高效率呢? 我采用的是利用两个随机的hash幂相乘,具体原因希望小伙伴们自己思考思考,此代码改为了
col += pow(hash(v_uv0
10000.),20.)pow(hash(v_uv011111.),20.),
效果图如下: image
接下来我们就要开始让星光开始闪烁了
step2: 星光闪烁
Gpu是不知道时间概念的,所以我们需要从cocos那边喂一个unifrom的时间变量进来,至于怎么喂变量这里也不提,不懂的可以自行搜索。 js 代码:


此时shader就可以用tt的时间变量来做闪烁了, 闪烁的透明度在0和1之间规律的变化,什么函数可以有此效果呢? 聪明的你是不是已经想到了三角函数 sin(x),函数曲线如下:
我们只要稍加改动就可以让他在0和1之间变化,如图:
最后的代码变成如下

fs main函数代码
void main () {
vec3 col = vec3(.1,.15,.2);
//draw star
float alpha = pow(hash(v_uv0*10000.),20.)*pow(hash(v_uv011111.),20.);
float twinkle = (1.+sin(tt*2.5+(hash(v_uv0*35785.)*hash(v_uv0*24586.))*314.23))/2.;
col += alpha*twinkle;
// gl_FragColor是最终决定每个像素的颜色的值,
gl_FragColor = vec4(col,1.);
}
几个数值说明: tt
2.5 ,2.5可以改变周期时间,需要增大或者减小闪烁速度,可以更改这个, 随机数值是为了让每个星星不是同时发亮和变暗,至于为啥我用了两个,只是因为一个看着感觉还是很随机,所以一个不够,那么就再来一个,314.23的作用是随机值的范围更大, 星星的就告一段落了,接下去就是月亮
step3:画一个月亮, 先发代码再讲解里面的含义

vec2 uv = v_uv0;
uv.x = 960. / 540.;
uv.x -= 1.2;
uv.y -= .3;
float d = length(uv);
vec3 moon = vec3(0.98,0.97,0.95);
col += 0.7
moonexp(-4.0d)*vec3(1.1,1.0,0.8);
//moon = 0.85 + 0.15 hash2(v_uv0);
col = mix( col, moon, 1.0-smoothstep(0.1,0.110,d) );

目前的uv坐标都是0到1之间,所以如果uv.x = 0.1,uv.y = 0.1的时候,他们真实的长宽比和材质的相同,这样不方便后续计算,举个栗子,如果材质长宽分别是960,540,那么vec2(.1,.1)的位置其实为96,54,对于画圆这种及其不方便,所以先修改uv的坐标值范围, uv.x = 960. / 540.;(因为我的长宽是960,540,更好的做法是获取到长宽,不过cocos下我暂时还不知道怎么获取),这样x的坐标范围变成了0到96/54, 我希望将月亮的位置放在右上角,大致位于0.3,1.2的位置,此时将uv的坐标圆心移动到此位置,之后设置一个接近于纯白的月亮的颜色vec3 moon = vec3(0.98,0.97,0.95); 之后就是画一个光圈,光圈会随着距离越远而衰减,代码: col += 0.7moonexp(-4.0d)*vec3(1.1,1.0,0.8);以e对底的指数级别衰减效果, 最后根据距离 float d = length(uv); col = mix( col, moon, 1.0-smoothstep(0.1,0.110,d) );画出一个半径为0.1的月亮,okay 此时一个基础的月亮已经大公告成,效果如下: image ,
setp4: 增加月亮表面纹理
原理为利用噪点图片生成纹理,但我cocos没做成功,哎,实力还是不够。 本文就此结束

如果你能坚持看到这里,不妨自己来做个作业吧。
作业: 实现狼牙月, 月牙部分高亮,其他部分变暗,这样可以避免目前的月亮过白而不够真实的问题,本人的解决方案放再下次帖子更新后发布。

另外分享下在大神作品上的改动效果 原作品链接:Shader - Shadertoy BETA 本人修改后效果(会慢慢由圆形变成爱心):

下期预告, 国庆降至,大家一起用shader画一幅国旗吧,效果如下:

7赞

https://www.shadertoy.com/view/7sy3Wh

顶起来,mark