卡通风格的水面效果—ToonWater
今天我们来实现一个卡通风格的水面效果,我们游戏是上帝视角的,玩家只能平移不能旋转,所以水面的Shader计算会有很多简化的地方,毕竟是要用在移动端上面的。先看看最终的效果视频:
卡通风格的水面效果—ToonWaterhttps://www.zhihu.com/video/1240653770318274560我们先来拆分一下要实现的效果:
1.浅滩到深水区的颜色渐变
2.水波纹往岸边推进
3.水底纹理的扭曲
4.白色浪花泡沫往岸边推进消失
大概的实现思路:浅滩到深水区的颜色渐变以及水底想要显示的内容,全部绘制在贴图上。通过UV动画采样一张法线贴图,来实现水波的推进。再用一张Noise来扭曲主纹理,以及浪花泡沫的效果。
首先我们来制作水面的模型,岸边的形状是弯曲不规则的,而我们需要模型的UV又一个正规的矩形,这样看起来水波纹往岸边推进才会显示正确。
接下来我们用一张Noise扭曲来模拟下水底部折射的效果
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x);
return col;
}
_WaveControl.w是控制扭曲的速度,_WaveControl.z是控制扭曲强度的
https://www.zhihu.com/video/1240688003850924032现在再来给水面加上水波纹的效果,这里需要用到法线贴图,计算也是在切线空间下的
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25)));
float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
return vdn;
}
_WaveControl.x是控制波纹移动的速度
先看下法线计算出来的运动效果
https://www.zhihu.com/video/1240692532767232000是不是觉得怪怪的不好看,我们在采样的时候再加上之前的Noise扭曲来看下效果
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
return vdn;
}
这样看起来是不是就有点意思了
注意观察下,我们水面实际上要用的效果,其实是把这个计算得到的法线结果反向一下,作为水面的高光部分
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x) * (2 - vdn);
return col;
}
_WaveControl.y是控制法线高光的强度
https://www.zhihu.com/video/1240732368681717760最后我们来实现这个白色的浪花泡沫效果,刚开始的时候也不好想这个效果要怎么实现出来。后来就是把它当做一个简单的溶解消失来做,然后再反向思考下,只要得到一个动态的白色遮罩区域就可以了。这里我是把这个区域写在了模型的顶点颜色里面,所以我们先来看下怎么写入顶点色。
R通道写入的是半透明区域,也就是岸边的一个过度。
G通道写入的是白色浪花出现的区域
在Shader中用G通道的区域个和一个移动的Noise,计算可以得到动态的消散遮罩区域
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
o.color = v.color;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + samplerUV + samplerNoise).r;
fixed waveMask = saturate((i.color.g - distortNoise) * 30);
return fixed4(waveMask, waveMask, waveMask, 1);
}
然后再用采样的浪花贴图和得到的遮罩区域做一个乘法就可以得到最终的浪花效果了
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
o.color = v.color;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + samplerUV + samplerNoise).r;
fixed waveMask = saturate((i.color.g - distortNoise) * 30);
fixed waveTex = tex2D(_WaveTex, i.uv * _WaveTex_ST.xy + samplerUV + samplerNoise).r * waveMask;
return fixed4(waveTex , waveTex , waveTex , 1);
}
最后把上面的效果都合起来,用顶点颜色的R通道作为水面的透明度,完整代码如下:
Shader "Custom/LX_ToonWater"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_NormalTex ("Normal", 2D) = "bump" {}
_SamplerNoise ("SamplerNoise", 2D) = "white" {}
//x:WaveSpeed y:Reflection z:NoiseStrength w:DistortSpeed
_WaveControl ("WaveControl", vector) = (0,0,0,0)
_WaveTex ("WaveTexture", 2D) = "black" {}
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"IgnoreProjector" = "True"
}
LOD 100
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float4 color : TEXCOORD2;
};
sampler2D _MainTex;
sampler2D _NormalTex; float4 _NormalTex_ST;;
sampler2D _SamplerNoise; float4 _SamplerNoise_ST;
sampler2D _WaveTex; float4 _WaveTex_ST;
fixed4 _WaveControl;;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.color = v.color;
TANGENT_SPACE_ROTATION;
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.zw + samplerUV + samplerNoise).r;
fixed waveMask = saturate((i.color.g - distortNoise) * 30);
fixed waveTex = tex2D(_WaveTex, i.uv * _WaveTex_ST.xy + samplerUV + samplerNoise).r * waveMask;
fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x) * (2 - vdn);
col.rgb += waveTex;
col.a = i.color.r;
return col;
}
ENDCG
}
}
}
写在最后,无论是之前做影视还是现在做游戏,水的效果都是最难搞的,动态的物理模拟和渲染都非常复杂的。这个效果是因为游戏应用场景比较简单,所以很多地方都简化了,没有那些复杂的计算。每种效果都要根据实际项目的应用场景,来制定实现效果的解决方案,没有那么多直接拿来就能用的。其实最后就是想说,别在后台私信问我要Demo工程什么的,甚至是连贴图都要的,已经写的很详细了吧,要是连合适的贴图都不去找,不愿意直接动手做一下的话,也太那个啥了吧......(伸手党!)