先丢一个演示视频:https://www.bilibili.com/video/BV1E5411R7az。
整体来说,是根据 https://b23.tv/ZapyRQy 这个教程学习的。Shader的雨点是纯数学运算的,目前没有特别理解其中的奥妙,只是觉得太牛了。源Shader来自于ShaderToy。
触摸擦除雨点,是记录了触摸的轨迹,然后Tick的时候,维护轨迹的生命周期,并对点附近的像素提供颜色权值贡献,之后将这张贴图传递给Shader,Shader通过颜色任意一个通道的值,在mipmap的不同层次采样即可。
声控闪电则是采集声音音量大小,以一定权重乘在最后frag返回的颜色值上,让屏幕变亮即可。
现在TODO的有两个,一个是如何优化触摸擦除雨点相关逻辑,在手机端比较卡顿;另外是要研究一下这个雨点Shader究竟是如何用数学计算画出雨点的。
----------------------------------------------------------分割线----------------------------------------------------------------
后来研究了一下shader源码,静态雨滴的能看懂,落下的雨滴数学部分不是很看的懂。
大致原理的话,就是根据分格子,然后每个像素点根据和中心的相对坐标位置,计算每个像素点的清晰程度即可;比如静态雨滴的就是雨滴中心是比较清晰的,也就是blur程度较低的,边缘是模糊的,然后根据时间变化即可。之后有个细节每个点计算出法线以后,向着法线偏移一点,就能做出类似于雨滴折射的效果。
这边贴一下我写了部分注释的shader代码吧。
1 // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' 2 3 Shader "Shadertoy/Template" { 4 Properties{ 5 iMouse ("Mouse Pos", Vector) = (100, 100, 0, 0) 6 iChannel0("iChannel0", 2D) = "white" {} 7 iChannelResolution0 ("iChannelResolution0", Vector) = (100, 100, 0, 0) 8 touchTex("touchTex", 2D) = "white" {} 9 } 10 11 CGINCLUDE 12 #include "UnityCG.cginc" 13 #pragma target 3.0 14 15 #define vec2 float2 16 #define vec3 float3 17 #define vec4 float4 18 #define mat2 float2x2 19 #define mat3 float3x3 20 #define mat4 float4x4 21 #define fract frac 22 #define textureLod tex2Dlod 23 #define iTime _Time.y 24 #define iGlobalTime _Time.y 25 #define mod fmod 26 #define mix lerp 27 #define fract frac 28 #define texture2D tex2D 29 #define iResolution _ScreenParams 30 #define gl_FragCoord ((_iParam.scrPos.xy/_iParam.scrPos.w) * _ScreenParams.xy) // 前半部分是[0, 1],整体是屏幕像素的坐标 31 32 #define PI2 6.28318530718 33 #define pi 3.14159265358979 34 #define halfpi (pi * 0.5) 35 #define oneoverpi (1.0 / pi) 36 37 fixed4 iMouse; 38 sampler2D iChannel0; 39 fixed4 iChannelResolution0; 40 sampler2D touchTex; 41 int downSample; 42 float volumeVal; 43 44 struct v2f { 45 float4 pos : SV_POSITION; 46 float4 scrPos : TEXCOORD0; 47 }; 48 49 v2f vert(appdata_base v) { 50 v2f o; 51 o.pos = UnityObjectToClipPos (v.vertex); 52 o.scrPos = ComputeScreenPos(o.pos); 53 return o; 54 } 55 56 vec4 main(vec2 fragCoord); 57 58 fixed4 frag(v2f _iParam) : COLOR0 { 59 vec2 fragCoord = gl_FragCoord; 60 return main(gl_FragCoord); 61 } 62 63 // shader toy start 64 65 // zzy 66 float debug_float; 67 68 #define S(a, b, t) smoothstep(a, b, t) 69 //#define CHEAP_NORMALS 70 #define HAS_HEART 71 #define USE_POST_PROCESSING 72 73 vec3 N13(float p) { 74 // from DAVE HOSKINS 75 vec3 p3 = fract(vec3(p, p, p) * vec3(.1031,.11369,.13787)); 76 p3 += dot(p3, p3.yzx + 19.19); 77 return fract(vec3((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y, (p3.y+p3.z)*p3.x)); 78 } 79 80 vec4 N14(float t) { 81 return fract(sin(t*vec4(123., 1024., 1456., 264.))*vec4(6547., 345., 8799., 1564.)); 82 } 83 float N(float t) { 84 return fract(sin(t*12345.564)*7658.76); 85 } 86 87 // S是smoothstep的,就会变成波峰随着b变化的一个函数 88 // 另外,smoothstep参照这个:https://zhuanlan.zhihu.com/p/157758600 89 float Saw(float b, float t) { 90 return S(0., b, t)*S(1., b, t); 91 } 92 93 94 vec2 DropLayer2(vec2 uv, float t) { 95 vec2 UV = uv; 96 97 uv.y += t*0.75; // 所有雨滴跟着画布下落 // zzy 98 vec2 a = vec2(6., 1.); 99 vec2 grid = a*2.; 100 // grid = float2(1, 2); // zzy 101 vec2 id = floor(uv*grid); 102 103 float colShift = N(id.x); 104 uv.y += colShift; // y位置随机偏移 // zzy 105 106 id = floor(uv*grid); 107 vec3 n = N13(id.x*35.2+id.y*2376.1); 108 vec2 st = fract(uv*grid)-vec2(.5, 0); // 把x映射到[-0.5, 0.5],然后按格子loop 109 110 float x = n.x-.5; // random [-0.5, 0.5] 111 112 float y = UV.y*20.; 113 float wiggle = sin(y+sin(y)); // [-1, 1] 114 x += wiggle*(.5-abs(x))*(n.z-.5); 115 x *= .7; 116 float ti = fract(t+n.z); 117 // ti = debug_float; // zzy 118 // 下面的y值控制了雨滴在y轴的长度,y越接近1,越短,超过1就没了 119 // 前半部分在0.85的时候达到最大值;整体的值域在[0, 1]之间,0.9是为了让前半部分变小,这样值变小,这样整体值域就在[0.05, 0.95],不那么接近边缘效果会更好一些 120 // 同时根据Saw函数可知,在0.85的时候y值最大,雨滴最短,也就是说,雨滴是先变短,到85%的时候最短,然后迅速变到变长 121 // 可以想象成0.85的时候是开始,雨滴迅速下坠,然后停一会,然后慢慢的收缩到最小(配合画布移动,就达到了雨滴下坠的效果) 122 y = (Saw(.85, ti)-.5)*.9+.5; 123 vec2 p = vec2(x, y); 124 125 float d = length((st-p)*a.yx); 126 127 // 这里可以知道,p离st越近,mainDrop越大,最后的结果越清晰 128 float mainDrop = S(.4, .0, d); 129 130 // mainDrop = d; // zzy 131 132 float r = sqrt(S(1., y, st.y)); 133 float cd = abs(st.x-x); 134 float trail = S(.23*r, .15*r*r, cd); 135 float trailFront = S(-.02, .02, st.y-y); 136 trail *= trailFront*r*r; 137 138 y = UV.y; 139 float trail2 = S(.2*r, .0, cd); 140 float droplets = max(0., (sin(y*(1.-y)*120.)-st.y))*trail2*trailFront*n.z; 141 y = fract(y*10.)+(st.y-.5); 142 float dd = length(st-vec2(x, y)); 143 droplets = S(.3, 0., dd); 144 // r = 0; // zzy 145 float m = mainDrop+droplets*r*trailFront; 146 147 //m += st.x>a.y*.45 || st.y>a.x*.165 ? 1.2 : 0.; 148 return vec2(m, trail); 149 } 150 151 // 返回的值越小,越接近maxBlur,就越糊;值越大,越接近minBlur 152 float StaticDrops(vec2 uv, float t) { 153 uv *= 40.; 154 155 // 格子化雨点 156 vec2 id = floor(uv); 157 uv = fract(uv)-.5; // [-0.5, 0.5] 158 vec3 n = N13(id.x*107.45+id.y*3543.654); // random [0, 1] 159 vec2 p = (n.xy-.5)*.7; 160 // 让每个格子的雨点位置随机产生偏差 161 float d = length(uv-p); 162 163 // fade随着时间,从1变化到0 164 // fade越小,所有值会趋向于maxBlur,fade较大时,就是清晰的样子, 165 // 所以随时间从1变化到0,表现为blur块晕开 166 // 另外,由于加上了位置随机值 n.z,所有的起始时间被错开了 167 float fade = Saw(.025, fract(t+n.z)); 168 // S(.3, 0., d),d越接近雨滴中心,越趋向于1,离雨滴越远,越接近0; 169 // 所以这里其实反过来了,会变成一个blur值低(清晰)的圆,随后乘上fade(从1到0变化),中间的返回值越来越小,越来越接近maxBlur, 170 // 于是就会看到一个清晰的圆越来越小,模拟的就是雨滴打到玻璃上,然后慢慢消失的情形 171 // fract(n.z*10.) 是个根据位置的随机值,让每个圆的半径不一样 172 float c = S(.3, 0., d)*fract(n.z*10.)*fade; 173 return c; 174 } 175 176 vec2 Drops(vec2 uv, float t, float l0, float l1, float l2) { 177 float s = StaticDrops(uv, t)*l0; 178 vec2 m1 = DropLayer2(uv, t)*l1; 179 vec2 m2 = DropLayer2(uv*1.85, t)*l2; 180 181 // zzy 182 // m1 = float2(0,0); 183 // m2 = float2(0, 0); 184 185 float c = s+m1.x+m2.x; 186 c = S(.3, 1., c); 187 188 return vec2(c, max(m1.y*l0, m2.y*l1)); 189 } 190 191 vec4 main( /*out vec4 fragColor,*/ vec2 fragCoord ) 192 { 193 vec4 fragColor; 194 195 // 映射到[-0.5, 0.5],需要注意,不是标准的这个范围,因为分母是y不是xy 196 vec2 uv = (fragCoord.xy-.5*iResolution.xy) / iResolution.y; 197 // [0, 1] 198 vec2 UV = fragCoord.xy/iResolution.xy; 199 vec3 M = iMouse.xyz/iResolution.xyz; 200 float T = iTime+M.x*2.; 201 202 // 采样Touch 203 vec4 touchColor = tex2D(touchTex, UV); 204 205 // #ifdef HAS_HEART 206 // T = mod(iTime, 102.); 207 // T = mix(T, M.x*102., M.z>0.?1.:0.); 208 // #endif 209 210 211 float t = T*.2; 212 213 float rainAmount = iMouse.z>0. ? M.y : sin(T*.05)*.3+.7; 214 // float rainAmount = 1; // zzy 215 216 float maxBlur = mix(3., 6., rainAmount); 217 float minBlur = 2.; 218 219 float story = 0.; 220 float heart = 0.; 221 222 // #ifdef HAS_HEART 223 // story = S(0., 70., T); 224 225 // t = min(1., T/70.); // remap drop time so it goes slower when it freezes 226 // t = 1.-t; 227 // t = (1.-t*t)*70.; 228 229 // float zoom= mix(.3, 1.2, story); // slowly zoom out 230 // uv *=zoom; 231 // minBlur = 4.+S(.5, 1., story)*3.; // more opaque glass towards the end 232 // maxBlur = 6.+S(.5, 1., story)*1.5; 233 234 // vec2 hv = uv-vec2(.0, -.1); // build heart 235 // hv.x *= .5; 236 // float s = S(110., 70., T); // heart gets smaller and fades towards the end 237 // hv.y-=sqrt(abs(hv.x))*.5*s; 238 // heart = length(hv); 239 // heart = S(.4*s, .2*s, heart)*s; 240 // rainAmount = heart; // the rain is where the heart is 241 242 // maxBlur-=heart; // inside the heart slighly less foggy 243 // uv *= 1.5; // zoom out a bit more 244 // t *= .25; 245 // #else 246 // float zoom = -cos(T*.2); 247 // uv *= .7+zoom*.3; 248 // #endif 249 // UV = (UV-.5)*(.9+zoom*.1)+.5; 250 251 float staticDrops = S(-.5, 1., rainAmount)*2.; 252 float layer1 = S(.25, .75, rainAmount); 253 float layer2 = S(.0, .5, rainAmount); 254 255 256 vec2 c = Drops(uv, t, staticDrops, layer1, layer2); 257 // 下面的法线是为了采样有一些偏差,让雨滴看起来有折射一般的效果 258 #ifdef CHEAP_NORMALS 259 vec2 n = vec2(dFdx(c.x), dFdy(c.x));// cheap normals (3x cheaper, but 2 times shittier ;)) 260 #else 261 vec2 e = vec2(.001, 0.); 262 float cx = Drops(uv+e, t, staticDrops, layer1, layer2).x; 263 float cy = Drops(uv+e.yx, t, staticDrops, layer1, layer2).x; 264 vec2 n = vec2(cx-c.x, cy-c.x); // expensive normals 265 #endif 266 267 // zzy 268 // n = 0; 269 270 271 // #ifdef HAS_HEART 272 // n *= 1.-S(60., 85., T); 273 // c.y *= 1.-S(80., 100., T)*.8; 274 // #endif 275 276 // 按照雨滴c的返回值x,越小越糊;同时,y值越大,整体越清晰一些(这里不太能理解原因,貌似是竖着落下来的雨有用到,和trail有关系) 277 float focus = mix(maxBlur-c.y, minBlur, S(0.1, 0.2, c.x)); 278 focus = lerp(focus, 1, touchColor.r); 279 // focus = 1; // zzy 280 // focus *= (1 - touchColor.r * 0.8); 281 vec3 col = textureLod(iChannel0, float4(UV+n, 0, focus)).rgb; 282 283 #ifdef USE_POST_PROCESSING 284 t = (T+3.)*.5; // make time sync with first lightnoing 285 float colFade = sin(t*.2)*.5+.5+story; 286 col *= mix(vec3(1., 1., 1.), vec3(.8, .9, 1.3), colFade); // subtle color shift 287 float fade = S(0., 10., T); // fade in at the start 288 // float lightning = sin(t*sin(t*10.)); // lighting flicker 289 // lightning *= pow(max(0., sin(t+sin(t))), 10.); // lightning flash 290 float lightning = volumeVal / 15; 291 col *= 1.+lightning*fade*mix(1., .1, story*story); // composite lightning 292 col *= 1.-dot(UV-=.5, UV); // vignette 293 294 // #ifdef HAS_HEART 295 // col = mix(pow(col, vec3(1.2, 1.2, 1.2)), col, heart); 296 // fade *= S(102., 97., T); 297 // #endif 298 299 // col *= fade; // composite start and end fade 300 #endif 301 302 // col = vec3(heart); 303 fragColor = vec4(col, 1.); 304 return fragColor; 305 } 306 307 // shader toy end 308 309 ENDCG 310 311 SubShader { 312 ZTest Always 313 Pass { 314 CGPROGRAM 315 316 #pragma vertex vert 317 #pragma fragment frag 318 #pragma fragmentoption ARB_precision_hint_fastest 319 320 ENDCG 321 } 322 } 323 FallBack Off 324 }
----------------------------------------------------------分割线----------------------------------------------------------------
后来在网络上找到的一个类似的教程。Unity Shader 窗前雨滴效果 - 灰信网(软件开发博客聚合) (freesion.com)