1. 程式人生 > >【Unity Shader】unity海邊波浪效果的實現

【Unity Shader】unity海邊波浪效果的實現

效果圖如下(GIF因為為了把圖壓小所以刪掉了一些幀導致後面速度突然很快,實際效果並不是這樣~_~)

之前在玩很多遊戲的時候,注意到裡面的海水和陸地相交接的地方會產生海浪,比如《海島奇兵》,以及水面會出現一個透明漸隱的過度,而不會在水面和陸地的交界處產生硬切邊。其中海浪的效果考慮到可以使用單獨的面片來製作,不過最近在試著通過深度比較的方式直接計算出水面和陸地相交接的位置來製作海邊的浪花,這種方式很多效果都會用到,其中比如unity粒子shader的軟粒子計算部分(當使用軟粒子時粒子的面片和其它物體交叉時不會出現明顯的切邊,而是有一個過度),還有之前在網上看到的別人的熱扭曲效果。

以下例子只講如何實現浪花,水的反射和折射有時間另外寫。另外我的水shader中從潛水處到深水處的顏色漸變也是通過深度比較來完成的。

首先來講解一下深度比較的原理,其實很簡單,就是比較水面的z深度和已寫入快取的陸地的深度(準確的說是渲染到深度圖的深度值,也就是_CameraDepthTexture,若要在shader中能讀到有效的深度圖,攝像機的depthTextureMode要設定為DepthTextureMode.Depth),計算一個深度差,很明顯,水面和陸地部分交接的深度差是0,(因為都已經交叉在一起了),而水越深的地方深度差顯然越大,如下圖


unity內建粒子shader中使用深度差實現軟粒子的程式碼如下:

#ifdef SOFTPARTICLES_ON
float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
float partZ = i.projPos.z;
float fade = saturate (_InvFade * (sceneZ-partZ));
i.color.a *= fade;
#endif

其中LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)))表示對深度圖_CameraDepthTexture進行紋理投射,SAMPLE_DEPTH_TEXTURE_PROJ是定義在UnityCG.cginc檔案中的,相當於tex2Dproj,事實上只是多了對當前的圖形api做了判斷而已。

i.projPos.z的值來自頂點函式,程式碼如下:

v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
#ifdef SOFTPARTICLES_ON
o.projPos = ComputeScreenPos (o.vertex);
COMPUTE_EYEDEPTH(o.projPos.z);
#endif
而這一段就是軟粒子中計算深度差的部分:
float fade = saturate (_InvFade * (sceneZ-partZ));

我想讀到這裡你一定會產生一個疑問,既然深度差是計算水面深度和深度圖也就是_CameraDepthTexture中的深度的差,那麼為什麼水的平面不會寫入_CameraDepthTexture中呢,因為如果水的深度寫入了_CameraDepthTexture,必然會出現水的深度和_CameraDepthTexture中自己的深度比較,此時任何一個畫素位置的深度差都是0。這裡我要講一下Unity中深度圖的渲染方式,如果有接觸過unity中的材質替代渲染,應該知道材質替代渲染是替換掉場景中某個標籤的shader,而unity中深度圖的渲染方式實際上就是這樣,而它替帶渲染的標籤就是"RenderType",其中"RenderType"="Opaque"的物體會被渲染到深度圖中,而"RenderType"="Transparent"的shader則不會被渲染,而我這邊的水就使用了這個標籤,因此它是不會被渲染到深度圖的。

接下來由於在頂點函式中我們使用了

COMPUTE_EYEDEPTH
來計算深度,因此深度圖的深度也需要轉換到視空間,即使用LinearEyeDepth。

此時如果直接輸出深度差應該會得到這樣的效果:


注意水面和陸地交叉的地方因為深度差最小所以最暗,而水較深的地方則表現出白色。

此時我們實際上只需要在shader中輸入兩個值,對應潛水處的深度差和較深處的深度差,並使用插值,就能產生水邊海浪泡沫的效果了:


如上。

當然這一部分還只是產生還邊海浪泡沫的部分,接下來需要講的是能像真正的海浪一樣不斷沖刷海灘的浪花。

一般想到浪花,其實應該很容易想到使用類似正餘弦函式來模擬的方式,(當然頂點水波只靠普通的正餘弦還是達不到很好的效果的,不過這裡不討論)這是一種很簡單的方式,而我的浪花就使用了正弦函式。我使用了這樣一張貼圖:


用這張貼圖來模擬反覆沖刷的海浪,並通過sin函式和_Time來偏移uv來實現,那麼現在的問題是如何知道海浪的方向。

其實可以回顧上面講的,我們知道海水和陸地的交叉處深度差是0,而深水處的深度差則比較大,不妨把深度差繪製成一條軸:


那麼以深度差和時間作為sin函式的引數既可以計算出海浪紋理的uv偏移量了

大致為

deltaDepth+sin(_Time.x*_WaveSpeed)

當然直接這樣產生的效果,各部分海浪的相位都一致,會導致海浪效果像平滑的光圈一樣放大縮小,所以我用了一張躁波貼圖來擾亂海浪的相位

最後效果如下:


大致就是這樣,最後附上完整程式碼和資源

Shader "Water/SeaWave" {
	Properties {
		_WaterTex ("WaterTex", 2D) = "black" {} 
		_WaveTex ("WaveTex", 2D) = "black" {} //海浪
		_BumpTex ("BumpTex", 2D) = "bump" {} 
		_GTex ("Gradient", 2D) = "white" {} //海水漸變
		_NoiseTex ("Noise", 2D) = "white" {} //海浪躁波
		_WaterSpeed ("WaterSpeed", float) = 0.74  //海水速度
		_WaveSpeed ("WaveSpeed", float) = -12.64 //海浪速度
		_WaveRange ("WaveRange", float) = 0.3 
		_NoiseRange ("NoiseRange", float) = 6.43
		_WaveDelta ("WaveDelta", float) = 2.43
		_Refract ("Refract", float) = 0.07
		_Specular ("Specular", float) = 1.86
		_Gloss ("Gloss", float) = 0.71
		_SpecColor ("SpecColor", color) = (1, 1, 1, 1)
		_Range ("Range", vector) = (0.13, 1.53, 0.37, 0.78)
	}
	CGINCLUDE 
	fixed4 LightingWaterLight(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) {
		half3 halfVector = normalize(lightDir + viewDir);
		float diffFactor = max(0, dot(lightDir, s.Normal)) * 0.8 + 0.2;
		float nh = max(0, dot(halfVector, s.Normal));
		float spec = pow(nh, s.Specular * 128.0) * s.Gloss;
		fixed4 c;
		c.rgb = (s.Albedo * _LightColor0.rgb * diffFactor + _SpecColor.rgb * spec * _LightColor0.rgb) * (atten * 2);
		c.a = s.Alpha + spec * _SpecColor.a;
		return c;
	}
	ENDCG
	SubShader {
		Tags { "RenderType"="Transparent" "Queue"="Transparent"}
		LOD 200

		GrabPass{}
		zwrite off
		
		CGPROGRAM
		#pragma surface surf WaterLight vertex:vert alpha
		#pragma target 3.0

		sampler2D _GTex;

		sampler2D _WaterTex;
		sampler2D _BumpTex;
		sampler2D _CameraDepthTexture;
		sampler2D _GrabTexture;
		half4 _GrabTexture_TexelSize;
		
		sampler2D _NoiseTex;
		sampler2D _WaveTex;

		float4 _Range;

		half _WaterSpeed;
		
		half _WaveSpeed;
		fixed _WaveDelta;
		half _WaveRange;
		fixed _Refract;
		half _Specular;
		fixed _Gloss;

		half _NoiseRange;

		struct Input {
			float2 uv_WaterTex;
			float2 uv_NoiseTex;
			float4 proj;
			float3 viewDir;
		};

		void vert (inout appdata_full v, out Input i) {
			UNITY_INITIALIZE_OUTPUT(Input, i);

			i.proj = ComputeScreenPos(mul(UNITY_MATRIX_MVP, v.vertex));
			COMPUTE_EYEDEPTH(i.proj.z);
		}

		void surf (Input IN, inout SurfaceOutput o) {
			float2 uv = IN.proj.xy/IN.proj.w;
			#if UNITY_UV_STARTS_AT_TOP
			uv.y = 1 - uv.y;
			#endif
			fixed4 water = (tex2D(_WaterTex, IN.uv_WaterTex + float2(_WaterSpeed*_Time.x,0))+tex2D(_WaterTex, float2(1-IN.uv_WaterTex.y,IN.uv_WaterTex.x) + float2(_WaterSpeed*_Time.x,0)))/2;
			float4 offsetColor = (tex2D(_BumpTex, IN.uv_WaterTex + float2(_WaterSpeed*_Time.x,0))+tex2D(_BumpTex, float2(1-IN.uv_WaterTex.y,IN.uv_WaterTex.x) + float2(_WaterSpeed*_Time.x,0)))/2;
			half2 offset = UnpackNormal(offsetColor).xy * _Refract;//用於折射的uv偏移量
			half m_depth = LinearEyeDepth(tex2Dproj (_CameraDepthTexture, IN.proj).r);
			half deltaDepth = m_depth - IN.proj.z;//計算深度差

			fixed4 noiseColor = tex2D(_NoiseTex, IN.uv_NoiseTex);

			half4 bott = tex2D(_GrabTexture, uv+offset);
			fixed4 waterColor = tex2D(_GTex, float2(min(_Range.y, deltaDepth)/_Range.y,1));
			
			fixed4 waveColor = tex2D(_WaveTex, float2(1-min(_Range.z, deltaDepth)/_Range.z+_WaveRange*sin(_Time.x*_WaveSpeed+noiseColor.r*_NoiseRange),1)+offset);
			waveColor.rgb *= (1-(sin(_Time.x*_WaveSpeed+noiseColor.r*_NoiseRange)+1)/2)*noiseColor.r;
			fixed4 waveColor2 = tex2D(_WaveTex, float2(1-min(_Range.z, deltaDepth)/_Range.z+_WaveRange*sin(_Time.x*_WaveSpeed+_WaveDelta+noiseColor.r*_NoiseRange),1)+offset);//這裡計算了兩個海浪,其中第二個海浪和第一個海浪存在相位差
			waveColor2.rgb *= (1-(sin(_Time.x*_WaveSpeed+_WaveDelta+noiseColor.r*_NoiseRange)+1)/2)*noiseColor.r;
			
			half water_A = 1-min(_Range.z, deltaDepth)/_Range.z;
			half water_B = min(_Range.w, deltaDepth)/_Range.w;
			float4 bumpColor = (tex2D(_BumpTex, IN.uv_WaterTex+offset + float2(_WaterSpeed*_Time.x,0))+tex2D(_BumpTex, float2(1-IN.uv_WaterTex.y,IN.uv_WaterTex.x)+offset + float2(_WaterSpeed*_Time.x,0)))/2;

			o.Normal = UnpackNormal(bumpColor).xyz;
			
			o.Specular = _Specular;
			o.Gloss = _Gloss;
			o.Albedo = bott.rgb * (1 - water_B) + waterColor.rgb * water_B;
			o.Albedo = o.Albedo * (1 - water.a*water_A) + water.rgb * water.a*water_A;
			o.Albedo += (waveColor.rgb+waveColor2.rgb) * water_A; 
			
			o.Alpha = min(_Range.x, deltaDepth)/_Range.x;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}