这次我们将运用之前所学的内容进行简单的水体编辑,先上效果图。
这次的水体其实非常的简单,主要是通过UV运动与顶点运动所实现的,为了节省美术资源,我们就用了两张图像资源,一张包含渐变、泡沫的Foam图,一张法线图。
我们所使用的资源就是这两个。首先我们先完成海水的渐变效果,在这里我们需要用到Foam的R通道,如果要完成更加自由的组合我们可以用photoshop来进行编辑或者设置两个颜色与一张灰度图来进行比较,这两种方法都会在日常的工作中运用得到。但这里我们只用Foam图的R通道进行实现渐变。
fixed degree=tex2D(_Foma,i.uv).r;
fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);
如上所写,我们将_Foam进行采样然后将他的R通道的值赋予degree,之后将albedo也就是反射率进行线性插值,便可以得到如下效果
与法线贴图配合就能实现上图的效果。这部分的代码比较重要,建议写的比较有条理些,毕竟框架蛮重要的。
Shader "Custom/SeaWater"
{
Properties
{
[Space(20)]
[Header(Color)]
_ShalowColor("Shalow Color",Color)=(1,1,1,1)
_DeepColor("Deep Color",Color)=(1,1,1,1)
[Space(20)]
[Header(Texture)]
_Foam("Foam",2D)="white"{}
_WaterNormal("Water Normal",2D)="Bump"{}
_NormalScale("Normal Scale",Float)=1.0
[Space(20)]
[Header(Specular)]
_SpecularColor("Specular Color",Color)=(1,1,1,1)
_SpecularScale("Specular Scale",Range(8,256))=20
}
SubShader
{
Pass{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
fixed4 _ShalowColor;
fixed4 _DeepColor;
fixed4 _SpecularColor;
float _SpecularScale;
sampler2D _Foam;
float4 _Foam_ST;
sampler2D _WaterNormal;
float4 _WaterNormal_ST;
float _NormalScale;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _Foam_ST.xy + _Foam_ST.zw;
o.uv.zw = v.texcoord.xy * _WaterNormal_ST.xy + _WaterNormal_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
float3 tangentLightDir=normalize(i.lightDir);
float3 tangentViewDir=normalize(i.viewDir);
fixed4 packedNormal = tex2D(_WaterNormal, i.uv.zw);
fixed3 tangentNormal=UnpackNormal(packedNormal);
tangentNormal.xy *=_NormalScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed degree=tex2D(_Foam,i.uv.xy).r;
fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _SpecularScale);
return fixed4(ambient+diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
我们先将框架搭完了之后就是传统的搭积木写代码而已。法线贴图的主要思路可以参考网上的代码,这部分资料有很多,我比较推荐冯乐乐的《Shader 入门精要》,里面阐述的比较详细。
接着我们进行最关键的一部分代码的书写,水面波纹的运动。毕竟我们的运动主要由法线动画所体现,核心代码如下
half2 panner1 = ( _Time.y * _WaveParams.xy + i.uv);
half2 panner2 = ( _Time.y * _WaveParams.zw + i.uv);
half3 worldNormal = BlendNormals(UnpackNormal(tex2D( _WaterNormal, panner1)) , UnpackNormal(tex2D(_WaterNormal, panner2)));
worldNormal = lerp(half3(0, 0, 1), worldNormal, _NormalScale);
我们在这里新定义了一个参数_WaveParams(x,y,z,w),这个参数我们UE4中经常用到是4位的向量,我们在这里可以定义一下,海浪参数(x:海浪范围,y:海浪偏移,z:海浪扰动,w:浪花泡沫扰动),这里我要啰嗦一句,在进行法线贴图的时候,法线其实是储存在切线空间下的,我当要使用法线在世界中的坐标的时候需要进行一部分转换。具体可以看这篇文章。关于为何要将法线存储在切线文章,可以参考法线贴图这篇文章。具体的关键代码如下:
v2f vert (appdata_full v)
{
...
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
o.TW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, o.worldPos.x);
o.TW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, o.worldPos.y);
o.TW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, o.worldPos.z);
...
return o;
}
fixed4 frag (v2f i) : SV_Target
{
...
worldNormal = normalize(fixed3(dot(i.TW0.xyz, worldNormal), dot(i.TW1.xyz, worldNormal), dot(i.TW2.xyz, worldNormal)));
...
}
制作高光的时候我这里选择的Blinn-Phong高光模型,关于Blinn-Phong模型我们这里不再赘述,网上有很多资料,本博客也有。
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _SpecularScale);
我们在打框架的时候已经将Blinn-Phong模型加了进去,效果图就不放了,接下来我们要加入Fresnel效果,关于Fresnel有一篇很著名的博文EveryThingHasFresnel大家如果有空可以去看一看,Fresnel有一个比较类似的公式
其中F0意味着Fresnel系数,由我们自己定义。其他的都是由我们在shader中获取。还有一个比较广泛的公式是fresnel Empricial如下
其中Blas、Scale、Power是可控项,这个公式具有更高的可控性,大家可以玩玩,挺有意思的。
但,我们这里一个都不用,主要是具体介绍下Fresnel公式,我们这里用一个trick来实现fresnel,就是用自发光来代替不同角度下的颜色。
fixed3 rim = pow(1-saturate(dot(tangentNormal,tangentViewDir)),_RimPower)*_LightColor0.rgb * _RimIntensity;
接着是我们另外一个难点,就是海浪的绘制,海浪的绘制有两个方法,一是用通道图画出边缘。优点是可控性强,性能消耗低。缺点是边缘质量很依赖贴图大小和精度,一旦场景改变需要重制贴图;二是使用深度图直接检测出交接边缘。优点是精度较高,无需美术反复修改贴图。缺点是性能消耗略大(有些低端移动设备不支持渲染深度图或者默认不开启渲染深度图)。这里采用深度图做法。深度图我们需要在Camera中开启深度渲染,这一步只需要再脚本中写入
Camera.main.depthTextureMode = DepthTextureMode.Depth;
在Shader中我们首先要先声明深度纹理
uniform sampler2D _CamerDepthTexture;
获取屏幕位置
o.screenPos = ComputeScreenPos(o.vertex);
计算边缘区域
half4 screenPos = float4( i.screenPos.xyz , i.screenPos.w);
half eyeDepth = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD( screenPos ))));
half eyeDepthSubScreenPos = abs( eyeDepth - screenPos.w );
half depthMask = 1-eyeDepthSubScreenPos + _FoamDepth;
计算边缘区域的逻辑其实非常简单,用摄像机距离地形的距离减去摄像机距离平面的距离,然后取得绝对值。该绝对值约接近为0则越可能是其边缘,添加FoamDepth是为了控制边缘区域的范围大小
他的绿色部分就是我们所需要的值,该值的求解就是黄色的线减去红色的线,可以非常容易理解通过深度图是如何进行取得接触边缘的的。
在这里需要强调一点的是当开启深度纹理的时候需要在shader的FallBack设置正确的”Diffuse”能够体现深度的设置。这点很重要,不然Shader不会报错,但依旧的不到正确的结果。
有了边缘区域接下来就是要让边缘显示为泡沫形状。最简单的做法就是使用泡沫通道乘以遮罩然后对水颜色和泡沫颜色进行插值。参考代码如下:
half3 foam = tex2D(_Foam,i.uv);
float temp_output = ( saturate( (foam.g * depthMask - _FoamFactor) );
diffuse = lerp( diffuse , _FoamColor, temp_output);
为了使边缘不规则,我们可以增加噪声图或者直接新型采样,depthMask *=water.g 这部分的water可以是
half3 water = tex2D(_Foam,i.uv.xy/_Foam_ST.xy);
精简下代码就是
depthMask *=tex2D(_Foam,i.uv.xy/_Foam_ST.xy).g;
此时我们就得到了初步的岸边的效果。
我们同样对水面进行UV的动画处理
half3 foam1 = tex2D(_Foam,i.uv + worldNormal.xy*_FoamOffset.w);
half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv + worldNormal.xy*_FoamOffset.w);
现在细节只是一层不动的颜色贴图,在这基础上添加扰动,让它更像海水的状态:
alf3 foam1 = tex2D(_Foam,i.uv + worldNormal.xy*_FoamOffset.w);
half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv + worldNormal.xy*_FoamOffset.w);
Foamoffset.w是扰动因子然后修改diffuse的颜色与细节颜色混合。
half4 detail = tex2D(_Foam,i.uv/_Foam_ST.xy).b * _DetailColor;
diffuse.rgb = fixed3(diffuse.rgb * (NdotV + detail.rgb) * 0.5);
现在细节只是一层不动的颜色贴图,在这基础上添加扰动,让它更像海水的状态:
half2 detailpanner = (i.uv/_Foam_ST.xy + worldNormal.xy*_WaterWave);
half4 detail = tex2D(_Foam,i.uv/_Foam_ST.xy).b * _DetailColor;
接下来我们需要进行顶点动画的制作,我们首先将原来的Plane替换成几万顶点的Mesh。顶点动画的代码之前的文章有过叙述。
float time = _Time.y * _Speed;
float waveValue = sin(time + v.vertex.x *_Frequency)* _Amplitude;
v.vertex.xyz = float3(v.vertex.x, v.vertex.y + waveValue, v.vertex.z);
speed是移动速度,Frequency是频率,Amplitude是幅度。
最后我们将标签改为Transparent进行透明度渲染。
half alpha = saturate(eyeDepthSubScreenPos-_AlphaWidth);
fixed4 col = fixed4( diffuse + specular + rim ,alpha);
整体代码如下:
Shader "Custom/SeaWater"
{
Properties
{
[Space(20)]
[Header(Color)]
_ShalowColor("Shalow Color",Color)=(1,1,1,1)
_DeepColor("Deep Color",Color)=(1,1,1,1)
_FoamColor("Foam Color",Color)=(1,1,1,1)
_DetailColor("Detail Color",Color)=(1,1,1,1)
[Space(20)]
[Header(Texture)]
_Foam("Foam",2D)="white"{}
_WaterNormal("Water Normal",2D)="Bump"{}
_NormalScale("Normal Scale",Float)=1.0
[Space(20)]
[Header(Specular)]
_SpecularColor("Specular Color",Color)=(1,1,1,1)
_SpecularScale("Specular Scale",Float)=1.0
_Gloss("Gloss",Float)=20
[Space(20)]
[Header(Animation)]
_WaveParams("Wave Params",Vector)=(0,0,0,0)
_FoamOffset("Foam Offset",Vector)=(0,0,0,0)
_WaterWave("Water Wave",Float)=1.0
_WaveXSpeed("Wave Horizontal Speed",Range(-0.1,0.1))=0.01
_WaveYSpeed("Wave Vertical Speed",Range(-0.1,0.1))=0.01
_Amplitude("Amplitude",Float)=1.0
_Frequency("Frequency",Float)=1.0
_Speed("Speed",Float)=1.0
[Space(20)]
[Header(Emission)]
_RimPower("Rim Power",Float)=1.0
_RimIntensity("RimIntensity",Float)=1.0
[Space(20)]
[Header(Foam)]
_FoamDepth("Foam Depth",Float)=1.0
_FoamFactor("Foam Factor",Float)=1.0
[Space(20)]
[Header(Alpha)]
_AlphaWidth("AlphaWidth",Float)=1.0
}
SubShader
{
Pass{
Tags { "LightMode"="ForwardBase" "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
fixed4 _ShalowColor;
fixed4 _DeepColor;
fixed4 _SpecularColor;
fixed4 _FoamColor;
fixed4 _DetailColor;
float _SpecularScale;
float _Gloss;
sampler2D _Foam;
float4 _Foam_ST;
sampler2D _WaterNormal;
float4 _WaterNormal_ST;
float _NormalScale;
float4 _WaveParams;
fixed4 _FoamOffset;
float _WaterWave;
fixed _WaveXSpeed;
fixed _WaveYSpeed;
float _Amplitude;
float _Frequency;
float _Speed;
float _AlphaWidth;
float _RimPower;
float _RimIntensity;
float _FoamDepth;
float _FoamFactor;
uniform sampler2D _CameraDepthTexture;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
float4 screenPos:TEXCOORD3;
};
v2f vert(a2v v){
v2f o;
float time=_Time.y*_Speed;
float waveValue=sin(time+v.vertex.x*_Frequency)*_Amplitude;
v.vertex.xyz=float3(v.vertex.x,v.vertex.y+waveValue,v.vertex.z);
o.pos=UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _Foam_ST.xy + _Foam_ST.zw;
o.uv.zw = v.texcoord.xy * _WaterNormal_ST.xy + _WaterNormal_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
o.screenPos = ComputeScreenPos(o.pos);
return o;
}
fixed4 frag(v2f i):SV_Target{
float3 tangentLightDir=normalize(i.lightDir);
float3 tangentViewDir=normalize(i.viewDir);
half2 panner1 = ( _Time.y * _WaveParams.xy + i.uv.zw);
half2 panner2 = ( _Time.y * _WaveParams.zw + i.uv.zw);
half3 tangentNormal = BlendNormals(UnpackNormal(tex2D( _WaterNormal, panner1)) , UnpackNormal(tex2D(_WaterNormal, panner2)));
tangentNormal = lerp(half3(0, 0, 1), tangentNormal, _NormalScale);
tangentNormal.xy *=_NormalScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
half4 screenPos = float4( i.screenPos.xyz , i.screenPos.w);
half eyeDepth = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD( screenPos ))));
half eyeDepthSubScreenPos = abs( eyeDepth - screenPos.w );
half depthMask = 1-eyeDepthSubScreenPos + _FoamDepth;
depthMask *=tex2D(_Foam,i.uv.xy/_Foam_ST.xy).g;
half3 foam1 = tex2D(_Foam,i.uv.xy+tangentNormal.xy*_FoamOffset.w);
half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv.xy+tangentNormal.xy*_FoamOffset.w);
float temp_output = ( saturate( (foam1.g + foam2.g ) * depthMask - _FoamFactor));
fixed degree=tex2D(_Foam,i.uv.xy).r;
fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);
half2 detailpanner=i.uv.xy/_Foam_ST.xy+tangentNormal.xy*_WaterWave;
half4 detail=tex2D(_Foam,i.uv.xy/detailpanner).b*_DetailColor;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 rim = pow(1-saturate(dot(tangentNormal,tangentViewDir)),_RimPower)*_LightColor0.rgb * _RimIntensity;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
diffuse = lerp( diffuse , _FoamColor, temp_output);
diffuse.rgb=fixed3(diffuse.rgb*(dot(tangentNormal,tangentViewDir)+detail.rgb)*0.5);
//half alpha = saturate(eyeDepthSubScreenPos-_AlphaWidth);
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed specularMask = tex2D(_WaterNormal, i.uv.zw).r * _SpecularScale;
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss)*specularMask;
return fixed4(ambient+diffuse+specular+rim, _AlphaWidth);
}
ENDCG
}
}
FallBack "Diffuse/Specular/Transparent"
}
整体的效果到这就结束了,Shader的思路就是搭积木,选好模型,然后不断添加细节。在这基础之上可以添加交互功能,具体的方法我是参考这篇Blgo。可以得到基础的效果。另外关于海面波光的表现形式有多种做法,这里用了将法线贴图作为遮罩进行遮罩高光来实现具体效果,还有一种方法我很喜欢的效果,再风之旅人的GDC上提过,他们用在了沙漠上,但在海水上也很有效果。风之旅人的沙漠实现方法。这篇文章的实现思路也很值得学习。