最终效果
实现思路
网格
首先使用一个Unlitshader,为了限制雨滴的移动范围,需要先定义出网格,首先将采样得到的纹理坐标乘一个数,然后取小数部分,就可以得到重复的网格:
1 2 3 4 5 6 7 8 9 10 11 12 13
| fixed4 frag (v2f i) : SV_Target { float4 col = 0;
float2 aspect = float2(2,1); float2 uv = i.uv * _GridSize * aspect; float2 grid_uv = frac(uv) - 0.5; if(grid_uv.x > 0.48 || grid_uv.y > 0.49) col = float4(1, 0, 0, 1);
return col; }
|
雨滴
使用smoothstep,通过计算到网格中心的距离来画出雨滴:
1 2
| float drop = smoothstep(0.05, 0.03, length(grid_uv / aspect)); col += drop;
|
移动逻辑
额外定义两个变量来控制雨滴偏移的量,通过改变这个两个变量来实现雨滴的移动,直接对网格坐标做加减即可:
1 2 3 4 5 6 7
| float x = 0; float y = 0;
float2 dropPos = (grid_uv - float2(x, y)) / aspect;
float drop = smoothstep(0.05, 0.03, length(dropPos)); col += drop;
|
然后就是最重要的,正常情况下雨滴落在窗户上,会先缓慢下滑,然后快速下滑,先使用一个函数模拟这个过程:
为了避免雨滴出现上滑,所以再同时把网格的y往下移,就可以做出这样的视觉效果:不断有雨滴从缓慢下滑到快速下滑:
1 2 3 4 5
| float2 uv = i.uv * _GridSize * aspect; uv.y += t * 0.25;
float y = -sin(t + sin(t + sin(t) * .5)) * 0.45;
|
雨滴轨迹
思路就是同样在生成drop的同时生成几个比它小的drop:
1 2 3 4 5 6 7
| float2 trailPos = (grid_uv - float2(x, t * 0.25)) / aspect; trailPos.y = (frac(trailPos.y * 8) - 0.5) / 8; float trail = smoothstep(0.03, 0.01, length(trailPos));
trail *= dropPos.y > 0 ? 1 : 0;
|
在这个基础上再去做颜色的渐变:
1
| trail *= smoothstep(0.5, y, grid_uv.y);
|
然后就是雨滴滑落的过程中的左右摇晃运动,描述函数:
1 2
| float w = i.uv.y * 10; float x = sin(3 * w) * pow(sin(w), 6) * 0.45;
|
随机化
为了让每个雨滴的轨迹,下落时间不一样,还需要分离每一个格子,让每个格子都有一个独有的id号,然后就可以针对不同的格子去做随机:
1
| float2 grid_id = floor(uv);
|
格子有了不同的颜色,即有了不同的id。然后根据这个float2的id去生成一个随机数:
1 2 3 4 5 6 7 8
| float GetRandom(float2 p) { p = frac(p * float2(123.33, 233.233)); p += dot(p, p + 34.33); return frac(p.x * p.y); }
col += GetRandom(grid_id);
|
成功给每个格子生成了一个随机数:
然后就能用每个格子生成的随机数去设置这个格子里水滴的移动(左右和上下):
1 2 3 4 5 6 7 8
| float gridValue = GetRandom(grid_id); t += gridValue * 6.28;
float w = i.uv.y * 10; float x = (gridValue - 0.5) * 0.8; x += (0.4 - abs(x)) * sin(3 * w) * pow(sin(w), 6) * 0.45; float y = -sin(t + sin(t + sin(t) * .5)) * 0.45; y -= (grid_uv.x - x) * (grid_uv.x - x);
|
这样就已经有了水滴随机下滑的效果。
雾气效果
1 2 3 4
| float fogTrail = dropPos.y > 0 ? 1 : 0; fogTrail *= smoothstep(0.5, y, grid_uv.y); trail *= fogTrail; fogTrail *= smoothstep(0.05, 0.04, abs(dropPos.x));
|
采样
然后就是拿这个图去对窗户后面的景色进行采样:
1 2 3
| float2 offsets = drop * dropPos + trail * trailPos;
col = tex2D(_MainTex, i.uv + offsets * _Distortion);
|
然后就已经可以得到很棒的效果了:
为了做到模糊的效果,使用tex2Dlod进行采样:
1 2 3 4 5
| float2 offsets = drop * dropPos + trail * trailPos;
float blur = _Blur * 7 * (1 - fogTrail);
col = tex2Dlod(_MainTex, float4(i.uv + offsets * _Distortion,0,blur));
|
雨滴层级
使用一个for循环去控制多层级的雨滴,数值越大,雨滴越多:
1 2 3 4 5
| float3 drops = DrawDrop(i.uv, t); for(int k = 1; k <= _RainLayers; k++) { drops += DrawDrop(i.uv * (1.13 + 0.012 * k) + 0.98 * k, t); }
|
窗户效果
要真正有着透过窗户看到外面的效果,可以使用grabpass实现。为了正确得到镜子后面的效果,必须让它最后渲染,所以把它的队列为Transparent,在不透明物体渲染完之后再渲染它。计算出抓取texture的坐标后用tex2Dproj进行采样,就可以得到正确的透视效果:
1 2 3 4 5 6 7 8 9
| struct v2f { float2 uv : TEXCOORD0; float4 grab_uv : TEXCOORD1; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
col = tex2Dproj(_GrabTexture, i.grab_uv);
|
模糊效果
模糊效果的思路和做高斯模糊相似,直接在采样的时候对坐标进行偏移,然后采样后做均值就行:
1 2 3 4 5 6 7 8 9 10 11
| proj_uv += drops.xy * _Distortion;
float a = GetRandom(i.uv) * 6.283; for(float j = 0; j < _SampleNum; j++) { float2 sampleOffset = float2(sin(a),cos(a)) * blur * 0.01; float d = sqrt(frac(sin((j + 1) * 546.) * 5489.)); col += tex2D(_GrabTexture, proj_uv + sampleOffset * d); a++; } col /= _SampleNum;
|
现在已经是挺不错的效果了。
Mipmap Level
现在有一个比较明显的问题是当距离比较远的时候,水滴看起来会很像闪烁着的点:
这是因为当相机离目标物体比较远时,目标物体只占屏幕很小一块区域(pixels),但我们还是用了正常尺寸的texture对目标物体进行渲染,此时一个pixel对应多个texel,采样就会不足。所以要基于距离的远近去做细节的程度,于是使用fwidth函数,会返回这个值在当前像素和它的下一个相邻像素之间的差值(与X和Y方向上的下一个像素上该值差的绝对值和)。
我们可以把这个差值放大,然后把它限制在0~1之间,把它作为一个系数去控制雨滴的细节程度:
1 2 3 4 5 6
| float fade = 1 - saturate(fwidth(i.uv) * 50); float blur = _Blur * 7 * (1 - drops.z * fade);
float2 proj_uv = i.grab_uv.xy / i.grab_uv.w; proj_uv += drops.xy * _Distortion * fade;
|
这样就不会出现很远的距离还能看见一个一个的雨滴了:
OVER!