本文参考文章:【UE4】皮肤下雨效果复现 大体的思路就是使用UV坐标生成水滴遮罩以及法线。

1.原理简单阐述

首先建一个简单的Shader来输出UV坐标:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    col.rg=i.uv;
    return col;
}

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨


为了方便计算,这里将UV的原点移到中心。接下来第一步我们先取UV坐标的长度(Length);第二步,取负并加0.3(截取其 [0,0.3] 部分);第三步,对其进行 Smoothstep 便可获取到一个 [0,1] 的平滑遮罩:

float2 uv=i.uv-0.5;
                float len=-length(uv)+0.3;
                float mask=smoothstep(0.15,0.2,len);
                col.rgb=len;

unity unistorm 没有下雨 unity下雨效果_Unity Shader_02


然后我们就可以根据,文章中的方法使用UV坐标和Mask来计算切线空间的法线

float3 TUV(float2 uv)
	            {
	                float3 normal; 
	                normal.y=abs(uv.x)*abs(uv.x)+uv.y;
					normal.x=abs(uv.y)*abs(uv.y)+uv.x;
	                normal.z=0.2;
	
	                return normalize(normal);
	            }
	            
				...
				float3 up=float3(0,0,1);
                float3 normal=TUV(uv);
                normal=lerp(up,normal,mask);
                ...

unity unistorm 没有下雨 unity下雨效果_水滴_03


然后给它算一下高光,这个Shader原理的最基本的一个流程就走完了,需要注意的是计算出来的法线是切线空间的,所以要进行转换:

float3 TransfromTanToWorld(float3 normal,float3x3 tanToworld)
				{
					float3 worldNormal;
					worldNormal.x = dot(tanToworld[0], normal);
					worldNormal.y = dot(tanToworld[1], normal);
					worldNormal.z = dot(tanToworld[2], normal);
					return normalize(worldNormal);
				}
				...
				
				...
				float3x3 tanToWorld;
				tanToWorld[0]=i.TtoW0.xyz;
				tanToWorld[1]=i.TtoW1.xyz;
				tanToWorld[2]=i.TtoW2.xyz;

                float3 worldPos=float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
				float3 lightDir=normalize(UnityWorldSpaceLightDir(worldPos));
                float3 viewDir=normalize(UnityWorldSpaceViewDir(worldPos));

                fixed4 col = tex2D(_MainTex, i.uv);
                float2 uv=i.uv-0.5;
                float len=-length(uv)+0.3;
                float mask=smoothstep(0.15,0.2,len);

                float3 up=float3(0,0,1);
                float3 normal=TUV(uv);
                normal=lerp(up,normal,mask);
                normal=TransfromTanToWorld(normal,tanToWorld);
                
                fixed spec=saturate(dot(viewDir,normal));
                spec=smoothstep(0.9,1,spec)*mask;
                col.rgb+=spec;
                return col;

unity unistorm 没有下雨 unity下雨效果_随机数_04

2.静态水滴

1.UV分块及错位

第一步:是将UV分块,每一块UV计算一个水滴 (_PointAmount=15)

float2 uv=i.uv*_PointAmount;
                uv=frac(uv);
                col.rg=uv;

但是这样的UV块过于整齐,所以在第二步我们需要引入随机数对UV进行偏移。这里我们对Y轴进行偏移,所以可以用X轴(floor取整是保证UV块偏移一致——伪随机的好处,同一个值得到的是同一个随机值)做随机数的输入:

float2 uv=i.uv;
                //frac将偏移值控制在0~1
                float random=frac(sin(floor(_PointAmount*uv.x)*12345.580078)*7658.759766);
                uv.y+=random;
                uv=frac(uv*_PointAmount);
                col.rg=uv;

第三步, 我们可以将这一步骤封装成一个函数,这里引入了timescale是为了做移动和拉伸,Out.zw是为了下一步生成随机数:

// XY:frac  ZW:floor
			float4 UVConfigure(float2 uv,float time,float2 scale,float amount)
			{
				uv=uv*scale;
				uv.y+=time;
				float factor=frac(sin(floor(amount*uv.x)*12345.580078)*7658.759766);
				uv.y+=factor;
				uv*=amount;

				float4 Out=0;
				//floor:返回小于等于x的最大整数。
				Out.zw=floor(uv);

				//frac返回输入值的小数部分。
				//x[0,1]---->[-0.5,0.5]
				Out.xy=frac(uv)-float2(0.5,0);
				//Out.xy=frac(uv);
				return Out;
			}
			...
			
			...
			float4 configure=UVConfigure(uv,0,1, _PointAmount);
            col.rg=configure.xy;

unity unistorm 没有下雨 unity下雨效果_水滴_05

2.随机偏移和消失

还记得前一步我们在函数里输出了floor,这其实是给每一小块UV赋予了一个单独的id。为了让每个UV块都有一个偏移,这里再引入一个随机数N13,输入一个UV,输出一个 [0,1]float3

float3 N13(float2 uv)
			{
				float p=uv.x*35.2+uv.y*2376.1;
				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));
			}
			
			
			float2 uv=i.uv;
            float4 configure=UVConfigure(uv,0,1, _PointAmount);
            col.rgb=N13(configure.zw);

这里把前一步的UV做输入值,每一个分区都有一个单独的float3

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_06


这三个随机值其实怎么用都行,这里先用最简单的,首先用xy做偏移(偏移过大的问题将在动态水滴的部分给出解决办法),并输出长度小于0.1的部分为白色:

float3 N=N13(configure.zw);

                half2 offsetUV=0;
				offsetUV.x=N.x-0.5;
				offsetUV.y=N.y;
				uv=configure.xy-offsetUV;

                col.rg=uv;
                if(length(uv)<0.1)
                {
                    return 1;
                }
                return col;

unity unistorm 没有下雨 unity下雨效果_Mask_07


然后根据开头讲的原理做水滴遮罩,但是我们不需要这么圆的水滴,所以可以给它加上一个缩放值:

float mask=-length(uv*float2(3.54,2.1))+0.5;
                mask=smoothstep(0,0.3,mask);
                col.rgb=mask;

unity unistorm 没有下雨 unity下雨效果_Mask_08


接下来,可以用z给水滴遮罩做由大到小的消失效果了:

float fade=smoothstep(0.2,1,1-frac(t*0.05+N.z));
                float mask=-length(uv*float2(3.54,2.1))+0.5*fade;
                mask=smoothstep(0,0.3,mask);
                col.rgb=mask;

unity unistorm 没有下雨 unity下雨效果_随机数_09


接下来把所有函数都封装一下,原文为了有两个高光,计算了两层法线——一层是凸出来一层是凹进去的,所以Mask也要有两个,凹的小一点,凸的大一点。还添加了一个Cut值,该值范围为 [0,1] ,1时显示所有水滴,0时全不显示:

float2 RainDropAndPointUV(float3 N,float2 fracUV,float Cut,out float cut)
            {
                cut=Cut;
				cut=floor(cut+N.y);

                half2 offsetUV=0;
				offsetUV.x=N.x-0.5;
				offsetUV.y=N.y;
				float2 uv=(fracUV-offsetUV);
                return uv;
            }


            float2 Mask(float2 uv,float3 scaleAndDir,float4 smoothRange,float2 cutAndBase)
			{
				//取负突出暗部,再和噪声相加获取渐变效果
				float maskBase=length(uv*scaleAndDir.xy)*scaleAndDir.z+cutAndBase.y;
				float2 mask=1;
				//内圈 凹
				mask.x=smoothstep(smoothRange.x,smoothRange.y,maskBase)*cutAndBase.x;
				//外圈 凸
				mask.y=smoothstep(smoothRange.z,smoothRange.w,maskBase)*cutAndBase.x;
				return mask;
			}
			
			float cut;
			uv=RainDropAndPointUV(N,configure.xy,_PointCut,cut);

            float fade=smoothstep(0.2,1,1-frac(t*0.05+N.z));
            float2 mask=Mask(uv,float3(3.54,2.1,-1),float4(0.3,0.44,0.1,0.45),float2(cut,fade));
            col.rgb=mask.r*cut;

调节 _PointCutMask的影响

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_10

3.法线和高光

由于需要计算两层法线,所以TUV函数也要修改,PosiNegaNormal用来决定法线的朝向,最后根据Mask对法线进行插值:

void TUV(float2 uv,float2 PosiNegaNormal,out float3 normal,out float3 causticNormal)
			{
				normal=0;
				causticNormal=0;

				normal.y=abs(uv.x)*abs(uv.x)+uv.y;
				normal.x=abs(uv.y)*abs(uv.y)+uv.x;
				causticNormal.xy=-normal.xy;

				normal.z=PosiNegaNormal.x;
				normal=normalize(normal);

				causticNormal.z=PosiNegaNormal.y;
				causticNormal=normalize(causticNormal);
			}
			.....
			
			float3 normal,causticNormal;
			//_PointPositiveNormal=0.235,_PointNegativeNormal=0.14
			float2 factor=float2(_PointPositiveNormal,_PointNegativeNormal); 
			TUV(uv,factor,normal,causticNormal);
			float3 up=float3(0,0,1);
			normal=lerp(up,normal,mask.y);
			causticNormal=lerp(up,causticNormal,mask.x);
               
            col.rgb=causticNormal;

unity unistorm 没有下雨 unity下雨效果_Mask_11


简单计算一下高光,已经稍微体现出体积感了,但需要调整的地方还有很多:

normal=TransfromTanToWorld(normal,tanToWorld);
                causticNormal=TransfromTanToWorld(causticNormal,tanToWorld);

                fixed spec1=smoothstep(0.9,1.2,dot(viewDir,normal))*mask.r*2;
                fixed spec2=smoothstep(0.5,1.5,dot(viewDir,causticNormal))*mask.g;
                
                col.rgb+=spec1+spec2;

unity unistorm 没有下雨 unity下雨效果_Mask_12

3.动态水滴

1.让UV动起来

静态水滴的UVConfigure函数可以接着用,但参数需要改一下,这里讲V方向拉长了五倍,因为静态水滴需要有水痕,照例把两个结果输出一下:

float2 uv=i.uv;
				float4 configureDrop=UVConfigure(uv,0.02*t,float2(5,1), _RainDropAmount);
                //col.rg=configureDrop.xy;
                col.rgb=N13(configureDrop.zw);
                if(length(configureDrop.xy)<0.1)
                {
                    return 1;
                }

unity unistorm 没有下雨 unity下雨效果_随机数_13


unity unistorm 没有下雨 unity下雨效果_Mask_14


然后把静态水滴剩余部分的代入到其中,就可得到和静态水滴一样的结果,不同的是这次水滴是运动的:

float2 uv=i.uv;
                float cut;

                float4 configureDrop=UVConfigure(uv,0.02*t,float2(5,1), _RainDropAmount);
                float3 N=N13(configureDrop.zw);
                //因为之前对uv做了拉伸,所以这里要逆回来,不然水滴会拉长
                float2 rainDrop=RainDropAndPointUV(N,configureDrop.xy*float2(1,5),_RainDropAmount,cut);
                float2 rianDropMask=Mask(rainDrop,float3(3.54,2.1,-1),float4(0.3,0.44,0.1,0.45),float2(cut,1));

                float3 dropNormal,dropCausticNormal;
				float2 factor=float2(_RainDropPositiveNormal,_RainDropNegativeNormal);
				TUV(rainDrop,factor,dropNormal,dropCausticNormal);
				dropNormal*=rianDropMask.y;
				dropCausticNormal*=rianDropMask.x;

                dropNormal=TransfromTanToWorld(dropNormal,tanToWorld);
                dropCausticNormal=TransfromTanToWorld(dropCausticNormal,tanToWorld);

                fixed spec1=smoothstep(0.9,1.2,dot(viewDir,dropNormal))*rianDropMask.r*2;
                fixed spec2=smoothstep(0.5,1.5,dot(viewDir,dropCausticNormal))*rianDropMask.g;
                
                col.rgb+=spec1+spec2;

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_15


可以法线这里问题还是很多,接下来一个一个解决。

2.运动和UV限制

这里的水滴是匀速的,看起来比较怪,所以我们修改一下RainDropAndPointUV函数,这里同时对UV的偏移做了限制:

float2 RainDropAndPointUV(float3 N,float2 fracUV,float time,float4 tillingOffset,float Cut,out float cut)
            {
                cut=Cut;
				cut=floor(cut+N.y);

                half2 offsetUV=0;
				offsetUV.x=(N.x-0.5)*0.5;

                float v=frac(N.y+time);
				v=smoothstep(0,0.85,v)*smoothstep(1,0.85,v)-0.5;
				offsetUV.y=v*0.5+0.5;

				float2 uv=(fracUV-offsetUV)*tillingOffset.xy;
                //float2 uv=fracUV*2;
                return uv;
            }

			float4 configureDrop=UVConfigure(uv,0,float2(5,1), _RainDropAmount);
            float3 N=N13(configureDrop.zw);
            float2 rainDrop=RainDropAndPointUV(N,configureDrop.xy,t*0.5,float4(1,5,0,0),_RainDropAmount,cut);

两个smoothstep的函数效果如图所示:

unity unistorm 没有下雨 unity下雨效果_随机数_16


这里把第一层的的运动值设为0,就可以很清楚的看到水滴显示往下移动到1,再由最大值变回0:

unity unistorm 没有下雨 unity下雨效果_随机数_17


再加上第一层的运动,水滴的效果就看起来是时而快时而慢的了

float4 configureDrop=UVConfigure(uv,0.02*t,float2(5,1), _RainDropAmount);
                float3 N=N13(configureDrop.zw);
                float2 rainDrop=RainDropAndPointUV(N,configureDrop.xy,t*0.05,float4(1,5,0,0),_RainDropAmount,cut);
				col.rg=rianDropMask.rg;

unity unistorm 没有下雨 unity下雨效果_Unity Shader_18

3.水滴拖尾

接下来可以制作水滴的拖尾效果了,为此我们需要两个变量,一个是未经偏移的UV坐标、另一个是偏移值,所以我们需要RainDropAndPointUV返回偏移UV值:

float4 RainDropAndPointUV(float3 N,float2 fracUV,float time,float4 tillingOffset,float Cut,out float cut)
            {
                cut=Cut;
				cut=floor(cut+N.y);

                half2 offsetUV=0;
				offsetUV.x=(N.x-0.5)*0.5;

                float v=frac(N.y+time);
				v=smoothstep(0,0.85,v)*smoothstep(1,0.85,v)-0.5;
				offsetUV.y=v*0.5+0.5;

				float2 uv=(fracUV-offsetUV)*tillingOffset.xy;
                return float4(offsetUV,uv);
            }

			float4 rainDrop=RainDropAndPointUV(N,configureDrop.xy,t*0.05,float4(1,5,0,0),_RainDropCut,cut);
			//rainDrop.xy:偏移值,configureDrop.xy:原始坐标
			float rainDropTrace=RainTrace(rainDrop.xy,configureDrop.xy)*cut;

首先需要确定拖尾的长度,其次是水滴的宽度

float RainTrace(float2 uv,float2 fracUV)
			{
				//横向X轴以水滴为中心向两边的渐变
                float lineWidth=abs(fracUV.x-uv.x);
            	//纵向Y轴,以Y轴最大边缘到水滴位置的0~1的渐变
                float lineHeight=smoothstep(1,uv.y,fracUV.y);
			}

unity unistorm 没有下雨 unity下雨效果_随机数_19


原文章使用了以下代码对拖尾宽度的进行限制:

float lineWidth=abs(fracUV.x-uv.x);
                float lineHeight=smoothstep(1,uv.y,fracUV.y);

				float base=sqrt(lineHeight);
				float widthMax=base*0.23;
				float widthMin=base*0.15*base;
                lineWidth=smoothstep(widthMax,widthMin,lineWidth);

uv.y=0fracUV.yX轴,x越大,有一部分它们间距基本一样,而越后面间距越小:

unity unistorm 没有下雨 unity下雨效果_水滴_20


如果我们这时直接输出lineWidth会得到以下效果:

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_21


这是因为lineWidth在fracUV.y小于uv.y后都等于1,所以我们需要进行限制,并使拖尾有一个渐变,最后的函数为:

float RainTrace(float2 uv,float2 fracUV)
			{
				float lineWidth=abs(fracUV.x-uv.x);
                float lineHeight=smoothstep(1,uv.y,fracUV.y);

                float base=sqrt(lineHeight);
				float widthMax=base*0.23;
				float widthMin=base*0.15*base;
                lineWidth=smoothstep(widthMax,widthMin,lineWidth);

                float trace=smoothstep(-0.02,0.02,fracUV.y-uv.y)*lineHeight;
                trace*=lineWidth;
				return max(0,trace);
			}

unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_22

3.阴影和结束

最后一部分也没什么好写的,大都是水滴效果上的东西,值得说到的是他用两层Mask——一层大、一层小,然后用一个正上方的光照去照亮他,就得到了水滴下的一小圈阴影:

float3 yDir=float3(0,1,0);
		fixed downMask=mask.x+dropMask.x;
    	fixed riseMask=mask.y+dropMask.y;
    
    	float RdotZ=smoothstep(-1.3,0.3,dot(yDir,riseNormal));
    	fixed fullMask=saturate(lerp(1,RdotZ,riseMask)+downMask);

unity unistorm 没有下雨 unity下雨效果_随机数_23


最后的效果:

unity unistorm 没有下雨 unity下雨效果_随机数_24


unity unistorm 没有下雨 unity下雨效果_unity unistorm 没有下雨_25


完整代码在Github——Alutemurat,以后写的效果都会放那了。

感觉自己写得有点乱,不知道能不能讲明白。