Unity Shader之萧萧暗雨打窗声

Unity Shader之萧萧暗雨打窗声

在知名Shader学习社区——ShadeToy中,有一个高赞作品:Heartfelt。该项目在不使用贴图的情况下,使用GLSL语言撰写了一个雨滴落在玻璃上的动态场景。


导读


Heartfelt 来自ShaderToy

许多Unity Shader爱好者便将这个作品移植到Unity平台上。本文选择了其中几个项目,以开源库中Unity Raindrops项目为主,介绍相关实现方法以及在移动端运行的性能。

开源库链接:https://lab.uwa4d.com/lab/5b5ca21ad7f10a201fe8b476


综述

雨滴和玻璃上附着的雾气都是通过模糊实现的。没有水滴的部分,雾气较大、模糊程度较高。有水滴的地方,模糊程度较小、清晰度略高。下滑的水滴划过留下的痕迹清晰度是渐变的。


水痕效果

在此基础上,对于有水痕的地方实现水滴形状,使场景更加逼真。


效果图


静态雨滴实现

所谓静态雨滴是体积较小,并不会滑落的水珠,随着时间流逝,体积慢慢变小直到消失。

首先,确定静态雨滴所在位置。项目中给出的算法如下:

#define S(a, b, t) smoothstep(a, b, t)

float Saw(float b, float t) {
  return S(0., b, t)*S(1., b, t);
}

float3 N13(float p) {
  float3 p3 = frac(float3(p, p, p) * float3(.1031, .11369, .13787));
  p3 += dot(p3, p3.yzx + 19.19);
  return frac(float3((p3.x + p3.y)*p3.z, (p3.x + p3.z)*p3.y, (p3.y + p3.z)*p3.x));
}

float N(float t) {
  return frac(sin(t*12345.564)*7658.76);
}

float StaticDrops(float2 uv, float t) {
  uv *= 40.;
  float2 id = floor(uv);
  uv = frac(uv) - .5;
  float3 n = N13(id.x*107.45 + id.y*3543.654);
  float2 p = (n.xy - .5)*.7;
  float d = length(uv - p);
  float fade = Saw(.025, frac(t + n.z));
  float c = S(.3, 0., d)*frac(n.z*10.)*fade;
  return c;
}

大致思路是将整个Mesh分为40*40块小区域。每个小区域确定一个水珠位置坐标(项目中使用的算法为N13),该区域其它位置到水珠坐标的距离d作为模糊程度的判断依据。例如,项目中S(.3,0., d),即为:d>0.3时,c值为0,后续进行模糊处理的时候为最大模糊程度。


使用算法N13的静态雨滴分布图

也可以使用其它的算法计算雨滴位置。例如,项目中给出的算法N:


使用算法N的静态雨滴分布图

雨滴数量过多时可进行进一步筛选。例如,项目中使用:frac(n.z*10.)删掉一些雨滴。


进行筛选

使用Unity Shader内置变量:_Time用于计时形成动画效果:


效果图

接着,实现雨滴特征:可以倒影出部分窗外的物体。

项目中采取的实现方式是:分别沿水平和垂直方向平移得到两个相同的雨滴,计算该区域的位置分别到这三个雨滴坐标距离的差。相关代码如下:

float2 c = Drops(uv, t, staticDrops, layer1, layer2);
float2 e = float2(.001, 0.);
float cx = Drops(uv + e, t, staticDrops, layer1, layer2).x;
float cy = Drops(uv + e.yx, t, staticDrops, layer1, layer2).x;
float2 n = float2(cx-c.x, cy-c.x);

根据上述内容,区域中其它位置到水珠坐标的距离d作为模糊程度的判断依据,越靠近值越大。即:圆心处值为1,圆上圆外值为0。如此根据代码可以得到一个UV值分布图:


UV分布图

如图,黄色轴为v轴,代表u=0的线,为AB线段的中垂线,其上的点到AB两点的距离相同,故差值为0,u=0;同理蓝色轴为u轴,代表v=0的线。这样可以得到相应位置的采样方式,得到效果如图:

在最终采样的时候加上原本的UV坐标即可:

float4 texCoord = float4(UV.x + n.x, UV.y + n.y, 0, focus);
float4 lod = tex2Dlod(iChannel0, texCoord);
float3 col = lod.rgb;

同样的,使用Lerp函数、smoothstep函数进行一些删减,得到效果如图:


效果图


动态雨滴实现

所谓动态雨滴是体积较大,会向下滑落,最终移出可视区域的水珠。

首先,雨滴形状与分布于上节描述大致相同(12*2个),代码如下:

                float2 UV = uv;
                uv.y += t *0.75;
                float2 a = float2(6., 1.);
                float2 grid = a*2.;
                float2 id = floor(uv*grid);
                float3 n = N13(id.x*35.2 + id.y*2376.1);
                float2 st = frac(uv*grid) - float2(.5, 0);
                float x = n.x - .5;
                float y = UV.y*20.;
                float wiggle = sin(y + sin(y));
                x += wiggle*(.5 - abs(x))*(n.z - .5);
                x *= .7;
                float ti = frac(n.z);
                y = (Saw(.85, ti) - .5)*.9 + .5;
                float2 p = float2(x, y);
                float d = length((st - p) *a.yx);
                float mainDrop = S(.4, .0, d);

区别在于雨滴位置的y值不再利用算法随机生成,而是一个固定值。引入时间变量t改变UV值,产生下落的动画效果。给雨滴位置的x值一个随着y值变化的偏移量wiggle,从而产生摆动效果。效果如下:


下落效果图

现实中的水珠下落,并不是这样匀速的。而是会停留等待一段时间,等到水珠变大后,加速下落一段距离然后再次停止。项目采用改变雨滴位置的y值来达到这样的效果,给雨滴位置的y值。加上一个偏移量,抵消掉下落距离(uv.y += t *0.75;),保持雨滴位置不变。代码如下:

float Saw(float b, float t) {
  return S(0., b, t)*S(1., b, t);
}

float ti = frac(t + n.z);
y = (Saw(.85, ti) - .5)*.9 + .5;
float2 p = float2(x, y);

这样的算法使得ti值为0~0.85时,雨滴位置上升,抵消下落距离,ti值为0.85~1时,雨滴位置下降,与下落距离相加,急速下落。效果如下:


分阶段下落效果图

接着实现滑落痕迹,思路与雨滴位置分布类似,根据距离体现出不同的模糊程度即可:

float r = sqrt(S(1., y, st.y));
float cd = abs(st.x - x);
float trail = S(.23*r, .15*r*r, cd);
trail *= trailFront*r*r;

最后为了使效果更加逼真,可以再下落的路径中随机产生一些静态雨滴,思路与上节描述类似,代码如下:

float trailFront = S(-.02, .02, st.y - y);
y = UV.y;
float trail2 = S(.2*r, .0, cd);
float droplets = max(0., (sin(y*(1. - y)*120.) -                   st.y))*trail2*trailFront*n.z;
y = frac(y*10.) + (st.y - .5);
float dd = length(st - float2(x, y));
droplets = S(.3, 0., dd);

产生效果如图:


动态雨滴效果图

其余部分,可添加一些屏幕后处理。例如,玻璃的变色特征、微调一些雨滴位置等。得到最终效果如图:


其它相似项目

还有其它项目也模拟了该效果。例如,该项目在此基础上拓展出两种效果。

其一是,不再使用背景贴图,使玻璃半透明:

其二是,为提高性能而采取的Unlit不接收光照版本:


性能测试

全屏运行该效果,使用UWA GOT Online在小米8和华为荣耀V20上进行测试,运行大约在28帧左右,GPU压力较大,耗时较高。可以考虑在一些特定剧情时运用,进一步优化,取消雨滴透视效果的实现,减少在片元着色器中采样次数等。


逻辑代码函数耗时列表


快用UWA Lab合辑Mark好项目!

请输入图片描述

今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......

请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。


【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。

更多精彩内容请关注:lab.uwa4d.com