实现思路
今天来实现一个简单的水面效果。水面的话主要是以下几个效果叠加
1.水面的动态波浪效果,我们这里使用最简单的正弦波加上法线贴图来模拟。如果想要更精确的波动效果可以考虑使用Gerstner波
2.不同水深颜色不同的效果,使用该顶点深度和顶点坐标的差值来计算深度,然后映射到颜色
3.浪涌效果,还是根据顶点深度和顶点坐标的差值来计算深度,然后叠加上一层浪涌
4.折射效果,用grabpass,采样之后进行偏移,最后叠加上去。
代码实现
首先是顶点位移函数,使用正弦函数代入顶点的x坐标和z坐标来算,因为这里写的水面效果是一个平面效果而不是体积水的效果。所以不用计算y轴。_WaveSpeed和_WaveGap 用来控制波浪速度和间隔。最后对每个顶点朝顶点的法线方向进行偏移。
void vertexDataFunc(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
v.vertex.xyz += v.normal * height;
}
水面深度效果,首先在输入中声明screenPos变量获取范围在[0,w]之间的坐标值,xyz除以w获得[0,1]的uv坐标值。
使用该uv坐标值采样_CameraDepthTexture深度纹理获得该点的深度值,使用LinearEyeDepth转换成线性深度值。接着再使用LinearEyeDepth(IN.screenPos.z)获得顶点的实际深度。LinearEyeDepth其实就是把[0,1]之间的非线性深度值转换为线性深度值。
有一个疑惑点是为什么这两个深度值不同,这是因为透明物体是不会写入到深度纹理里面去的,所以读取某个屏幕坐标的深度值,其实是找到第一个不透明的点获取他的深度值,而如果这个点是透明的,那么深度纹理中读取的就不是他的深度值。但是无论如何直接使用LinearEyeDepth(IN.screenPos.z)获取的就是该顶点的实际线性深度值。
所以这两个深度的差值其实就是透过一个透明点后的第一个非透明点和这个透明点之间距离的差值。
IN.screenPos.xyz /= IN.screenPos.w;
//深度纹理中的深度
float depth1 = LinearEyeDepth(tex2D(_CameraDepthTexture, IN.screenPos.xy).r);
//当前顶点的深度
float depth2 = LinearEyeDepth(IN.screenPos.z);
float distance = abs(depth1 - depth2);
//深度插值颜色
fixed4 waterColor = lerp(_EdgeColor, _Color, saturate(distance * _EdgeThreshold));
设置tag ,“RenderType” = "Transparent"是为了不写入深度值,“Queue” = "Transparent"是为了使该shader在所有不透明物体渲染结束之后再渲染,是为了grabpass获取到正确图像,两个Transparent设置的目的是不同的
Tags
{
"RenderType" = "Transparent" "Queue" = "Transparent"
}
浪涌效果的话还是根据这个深度的差值来算,判断深度差值小于_FoamThreshold这个阈值的话就采样浪涌贴图叠加上一层效果。
//form
float4 foamColor = tex2D(_FoamTex, (IN.uv_FoamTex + fixed2(1, 1) * _Time.x * _FoamSpeed) * 20);
fixed formDegree = clamp(distance, 0, _FoamThreshold);
//把0~_FoamThreshold 缩放到0~1
formDegree = formDegree / _FoamThreshold;
fixed4 formColor = lerp(foamColor,fixed4(0, 0, 0, 0), formDegree) * _FoamColorScale;
要实现水面波光粼粼的效果还需要法线贴图的帮助。这边采样两次法线贴图,采样的时候分别按时间往不同的方向对uv做偏移,最后叠加两个法线,就会有一种波光粼粼的效果。嗯。。但是我也不知道原理是什么,反正写shader看上去是对的就可以了,看了几个水的shader也都有这样的处理。
//法线
float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
fixed3 bump = normalize(bump1 + bump2);
bump.xy *= _NormalScale;
bump = normalize(bump);
o.Normal = bump;
折射的话就是采样之前grabpass声明的_RefractionTex,然后再根据法线来做偏移就可以得到折射效果
//折射
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
fixed3 refrCol = tex2D(_RefractionTex, IN.screenPos.xy + offset).rgb;
最后对算出来的各种颜色进行叠加
o.Albedo = waterColor.rgb * refrCol + formColor.rgb;
o.Alpha = waterColor.a;
最终效果。
gif有点糊
参数面板
完整代码
Shader "LX/simpleWater"
{
Properties
{
_Color ("Color", Color) = (0,0,1,1)
_EdgeColor ("EdgeColor", Color) = (0,1,1,1)
_EdgeThreshold ("EdgeThreshold", float) =0.1
_WaveHeight ("WaveHeight", float) = 1
_WaveSpeed ("WaveSpeed", float) = 1
_WaveGap ("WaveGap", float) = 1
_FoamTex ("FoamTex", 2D) = "white"
_FoamThreshold ("FoamThreshold", float) =0.1
_FoamColorScale ("FoamColorScale", float) =0.2
_FoamSpeed ("FoamSpeed", float) =0.1
_NormalTex ("NormalTex", 2D) = "white"
_NormalScale ("NormalScale", float) = 1
_NormalSpeed ("NormalSpeed", float) = 1
_Distortion("Distortion",float) = 1
}
SubShader
{
Tags
{
"RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"
}
LOD 200
GrabPass
{
"_RefractionTex"
}
CGPROGRAM
#pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_FoamTex;
float4 screenPos;
};
sampler2D _CameraDepthTexture;
fixed4 _Color;
fixed4 _EdgeColor;
float _WaveHeight;
float _WaveSpeed;
float _WaveGap;
float _EdgeThreshold;
float _FoamThreshold;
sampler2D _FoamTex;
float _FoamColorScale;
sampler2D _NormalTex;
float _NormalScale;
float _NormalSpeed;
float _FoamSpeed;
float _Distortion;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
void vertexDataFunc(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
v.vertex.xyz += v.normal * height;
}
void surf(Input IN, inout SurfaceOutputStandard o)
{
IN.screenPos.xyz /= IN.screenPos.w;
//深度纹理中的深度
float depth1 = LinearEyeDepth(tex2D(_CameraDepthTexture, IN.screenPos.xy).r);
//当前顶点的深度
float depth2 = LinearEyeDepth(IN.screenPos.z);
float distance = abs(depth1 - depth2);
//深度插值颜色
fixed4 waterColor = lerp(_EdgeColor, _Color, saturate(distance * _EdgeThreshold));
//form
float4 foamColor = tex2D(_FoamTex, (IN.uv_FoamTex + fixed2(1, 1) * _Time.x * _FoamSpeed) * 20);
fixed formDegree = clamp(distance, 0, _FoamThreshold);
//把0~_FoamThreshold 缩放到0~1
formDegree = formDegree / _FoamThreshold;
fixed4 formColor = lerp(foamColor,fixed4(0, 0, 0, 0), formDegree) * _FoamColorScale;
//法线
float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
fixed3 bump = normalize(bump1 + bump2);
bump.xy *= _NormalScale;
bump = normalize(bump);
o.Normal = bump;
//折射
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
fixed3 refrCol = tex2D(_RefractionTex, IN.screenPos.xy + offset).rgb;
o.Albedo = waterColor.rgb * refrCol + formColor.rgb;
o.Alpha = waterColor.a;
}
ENDCG
}
FallBack "Diffuse"
}
另外代码也传到github仓库里了,大家也可以关注一下哦~
我的github