本文参考文章:【UE4】皮肤下雨效果复现 大体的思路就是使用UV坐标生成水滴遮罩以及法线。
1.原理简单阐述
首先建一个简单的Shader来输出UV坐标:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rg=i.uv;
return col;
}
为了方便计算,这里将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;
然后我们就可以根据,文章中的方法使用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);
...
然后给它算一下高光,这个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;
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;
第三步, 我们可以将这一步骤封装成一个函数,这里引入了time和scale是为了做移动和拉伸,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;
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:
这三个随机值其实怎么用都行,这里先用最简单的,首先用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;
然后根据开头讲的原理做水滴遮罩,但是我们不需要这么圆的水滴,所以可以给它加上一个缩放值:
float mask=-length(uv*float2(3.54,2.1))+0.5;
mask=smoothstep(0,0.3,mask);
col.rgb=mask;
接下来,可以用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;
接下来把所有函数都封装一下,原文为了有两个高光,计算了两层法线——一层是凸出来一层是凹进去的,所以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;
调节 _PointCut 对Mask的影响
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;
简单计算一下高光,已经稍微体现出体积感了,但需要调整的地方还有很多:
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;
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;
}
然后把静态水滴剩余部分的代入到其中,就可得到和静态水滴一样的结果,不同的是这次水滴是运动的:
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;
可以法线这里问题还是很多,接下来一个一个解决。
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的函数效果如图所示:
这里把第一层的的运动值设为0,就可以很清楚的看到水滴显示往下移动到1,再由最大值变回0:
再加上第一层的运动,水滴的效果就看起来是时而快时而慢的了
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;
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);
}
原文章使用了以下代码对拖尾宽度的进行限制:
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=0,fracUV.y是X轴,x越大,有一部分它们间距基本一样,而越后面间距越小:
如果我们这时直接输出lineWidth会得到以下效果:
这是因为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);
}
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);
最后的效果:
完整代码在Github——Alutemurat,以后写的效果都会放那了。
感觉自己写得有点乱,不知道能不能讲明白。