天天看點

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

文章目錄

  • 一、環境
  • 二、方案對比
  • 三、思路概述
  • 四、關鍵技術點
    • 1.為什麼要用雙錐
      • 1.1.仰視俯視問題
      • 1.2.運動适應
    • 2.怎麼實作雨的層次感
    • 3.怎麼實作遮擋
  • 五、效果優化
    • 1.固定FOV
    • 2.突出近景
    • 3.交錯方向和風向
  • 六、性能優化
    • 1.降分辨率
    • 2.Low-Level Optimization
      • 2.1.unroll loop
      • 2.2.使用?:代替if-else
      • 2.3.clip還是step
    • 3.高低端機差異
  • 七、遇到的問題
    • 1.ClearColor對深度圖的意義
    • 2.自定義的非線性Z轉線性W(指Perspective Division前的W)
    • 3.近裁剪面後面是什麼
    • 4.正确地剔除
  • 八、參考

文章較長,建議跳到感興趣的部分閱讀,轉載請标明出處。

一、環境

Win/Android/iOS,Unity3D

二、方案對比

  使用粒子系統的實作方法1,效果是最自然的,可控程度也是最高的,但最大的問題是性能,基本要到10k個粒子才能表現出小雨,大雨就更不用說了。

  而用ATI Toy Shop Demo2的方法,本質上就是加了一個全屏貼圖滾動的後處理,他們用了4層不同Tiling的貼圖模拟雨的層次縱深,這種方法的好處就是性能與雨滴的數量無關,但缺點也比較明顯,一個是要做出好的效果比較麻煩且不直覺,另一點是後處理的效果永遠是“平行于”螢幕的,在有俯仰視視角的遊戲裡就會出現雨水準行于地面或天空的問題。

  本文實作方法是在ATI Toy Shop Demo的方法之上加以改進。主要思路參考天刀的文章3。

三、思路概述

  最主要其實就是解決兩個問題,雨是怎麼“下”的,雨的縱深感怎麼表現。簡而言之就是,使用一個雙錐(double cone)模型4,做貼圖滾動;用深度圖還原雨滴的位置做剔除。

以下代碼隻作參考

for (int k = 0; k < 3; ++k)
{
	depthLayer = depthLayers[k] * depthRange[k] + baseDepth; // 逐像素深度
	if ((viewDepth > depthLayer)) 
	{
		float3 worldPos = GetWorldPosFromViewZ(screenUV01, depthLayer);
		float4 projPos = GetVerticalColliderCamProjPos(worldPos);
		half collisionDepth = GetVerticalColliderCamPixel01Depth(projPos.xy); 
		clip(collisionDepth - projPos.z);
		baseDepth += depthLayer;
		alpha += colorLayer[k];
	}
	else return fixed4(_Color.rgb, alpha);
}
           

四、關鍵技術點

1.為什麼要用雙錐

1.1.仰視俯視問題

  正如上面說的,使用全屏後處理(full-screen quad)的方式會有視角問題,這個可以通過使用雙錐代替。使用雙錐,在俯視和仰視的時候就會有雨水向遠處收攏的效果,再配合調節頂點色(兩端暗中間亮)就可以實作雨水消失在滅點的感覺。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

【雙錐的UV展開】

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

【頂點镂空,獲得更好UV展開效果】

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

1.2.運動适應

  既然我們做天氣系統的目的是為了增強環境氣氛,那麼最好的肯定是能與玩家産生互動。為此,我實作了一個相對運動的效果。在飛行模拟器文章5中給出了一種依賴實際資料分析的貼圖Scrolling和Tiling的公式來模拟相對運動,但我認為,讓美術能更直覺地調節效果的參數應該會更有實際意義,便沒有采用。

  對這種方法,要模拟相對運動,将角色的運動分解為水準運動和垂直運動,水準相對運動展現在雙錐的傾斜上,垂直相對運動展現在貼圖的滾動速度上,雨水收尾速度一般是4~9m/s,是以可以用這個作為預設值,再加上角色的垂直相對運動速度即可。通過這種方法可以實作角色向前奔跑時,雨水迎面打來,角色跳躍時雨水下落加快,角色下落時和雨水相對靜止,讓玩家更能身臨其境。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

  在這裡有一點小技巧,一個是在一些高度變化頻繁的地形上移動時,如小石階,雨水會抖動;一個是一般第三人稱相機的運動一般會有一個平滑首尾,人物移動也會有一個加速過程,這時雙錐會緩慢傾斜至最終狀态,效果很突兀。這兩個問題都可以通過加入一個門檻值,隻有超過門檻值才應用這種相對運動的效果即可。

2.怎麼實作雨的層次感

  在Toy Shop裡,用了4層不同深度位移的貼圖來模拟雨的縱深感。每一層中的每個雨滴都有不同的深度,本質上是通過遮擋關系來表現的,是以能控制雨滴的深度對效果的提升是巨大的。我的做法是,直接用雨水的albedo貼圖來做逐雨滴的深度貼圖,這是一種省功夫的做法,要實作更好的效果,最好還是用一張專門的深度圖。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

  這裡有一個問題,我用的是Remember Me中的貼圖(沒有美術功底,隻能用别人的。。),而貼圖是已經經過運動模糊處理的,是以如果直接用一個關于顔色的函數來作為雨水的深度,在水準深度剔除時會出現雨水的細節被剔除掉。所幸,一般雨的alpha值較小,不易察覺。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

3.怎麼實作遮擋

  這種雨的實作方法的精髓就在于如何實作水準遮擋和垂直遮擋了。用雙錐的方法本質上還是後處理,隻是将full-screen quad換成了double cone,他依然是全屏的,完全透明的。我們可以為每一層雨水指定一個深度範圍,通過逐雨滴深度圖和螢幕空間坐标,來還原雨水的世界坐标。通過主相機視角的深度圖來和還原出的世界坐标做深度剔除。深度圖做碰撞檢測,通過類似于平行光投影的方法,假定雨都是從角色上方某一個高度的平面下落的,計算雨滴在這個矩形裡的投影坐标,可以判斷是否被屋檐一類的物體遮擋。

五、效果優化

1.固定FOV

  我們的遊戲是可以在一定範圍内讓玩家調整FOV的,這會導緻一個問題,現實中的雨是沒辦法“靠近看”的,同一時刻你在目前位置和100米以外看到的雨滴的大小應該不會有太大差别的,而FOV改變的時候我們看到的雨滴的大小是會變化的。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

這個問題可以直接修改裁剪矩陣的一行一列和二行二列數值解決。

// vertex = UnityObjectToClipPos(i.vertex);
float4x4 projMat = UNITY_MATRIX_P;
float rcpAspect = projMat._m00 / projMat._m11;
projMat._m11 = (projMat._m11 > 0 ? 2.1445 : -2.1445);// 2.1445==cot25, 固定FOV, 避免貼圖縮放
projMat._m00 = projMat._m11 * rcpAspect;
vertex = mul(projMat, mul(unity_MatrixMV, float4(i.vertex.xyz, 1.0)));
           

2.突出近景

  在現實中,肉眼會用亮度來判斷雨的遠近,是以我在代碼裡做了一個小的改進,就是将最靠近相機那一層雨的alpha值取平方,讓近處的雨看起來更亮,這對效果的提升也是很明顯的

3.交錯方向和風向

  還有一種提升效果的思路,剛開始實作功能的時候,參考了Remember Me6的文章,也參考了明日之後7的實作,都發現兩者在UV變換上動了手腳。一開始兩者的做法都有點費解的。把效果實作一遍,再看一下遊戲裡的效果,最後按我的了解,他們或是為了讓每一層雨有單獨的方向,形成交錯的感覺;或是為了形成周期性擺動,模拟風吹。

·Remember Me裡的變換,UV繞中心周期性旋轉

// 實際上就是uv乘以一個二維旋轉矩陣
float2 SinT = sin(Time.xx * 2.0f * Pi / speed.xy) * scale.xy;
// rotate and scale UV
float4 Cosines = float4(cos(SinT), sin(SinT));
float2 CenteredUV = UV - float2(0.5f, 0.5f);
float4 RotatedUV = float4(dot(Cosines.xz*float2(1,-1), CenteredUV)
                         , dot(Cosines.zx, CenteredUV)
                         , dot(Cosines.yw*float2(1,-1), CenteredUV)
                         , dot(Cosines.wy, CenteredUV) ) + 0.5f);
float4 UVLayer12 = ScalesLayer12 * RotatedUV.xyzw;
           

·通過GPA抓幀明日之後,頂點着色器裡的UV變換

// 實際上就是做了一個 uv.x’ = Auv.x + B * uv.y + C的線性變換
float _local_2 = dot(_local_1, _uv_scale_offset[0].xyz);
float _local_3 = dot(_local_1, _uv_scale_offset[1].xyz);
float2 _local_4 = vec2_ctor(_local_2, _local_3);
(_v_texture0 = _local_4);
           

六、性能優化

  因為本文介紹的天氣是要應用在移動端的是以性能優化尤其重要。目前的做法不可謂不昂貴,增加一個全屏Overdraw,需要兩張深度圖,采樣9次。。。在沒有優化之前,在低端機GPUBound條件下實測單幀耗時由33ms上升到57ms,掉了十幾幀。而經優化和機型效果劃分後,在低端機(Snapdragon430,Adreno505)單幀耗時34ms,高端機(Snapdragon660,Adreno512)單幀耗時37ms。

采用的優化手段如下:

1.降分辨率

  這是最簡單暴力的優化手段,可以将下雨效果渲染到一張分辨率較低的RenderTexture上,然後再升采樣Blit到Default FBO/主相機上即可。降采樣主要帶來的代價是雨滴會變模糊,但其實本來我們用的貼圖就是運動模糊後的效果,是以影響其實并不明顯,而且也可以通過提高貼圖尺寸改善。

  通過降低分辨率(總像素降到1280*720的40%)和配合調整Tiling,耗時得到極大的減少。

2.Low-Level Optimization

應用以下的優化後耗時可以減少2~4ms。

2.1.unroll loop

  和CPU代碼優化類似,一個常用的優化手段是手動展開循環,一般來說,展開較小的循環體會帶來性能提升,編譯器會自動幫你展開循環體較小的循環。但至于這樣做是否真的帶來性能提升,最好還是實際測一下資料比較好。

// Unroll Loop性能較好
					bool notcull = true;
					int k = 0;
					{
						// if-else改?:性能更換
						depthLayer = (1 - depthLayers[k]) * depthRange[k] + baseDepth; // 逐像素深度
						srcAlpha = colorLayer[k];
						{
							float3 worldPos = GetWorldPosFromViewZ(screenUV01, depthLayer); // 世界坐标
							float4 projPos = GetRainColliderCamProjPos(worldPos); // 垂直投影坐标
							half collisionDepth = GetRainColliderCamPixel01Depth(projPos.xy); // 垂直深度
							fixed clipped = ClipCond(collisionDepth, projPos.z); // 垂直遮擋
							output = AlphaBlend(alpha, srcAlpha * (1 - clipped)); // 正片疊加
						}
						bool choose = (viewDepth > depthLayer);
						alpha = choose ? output * output : alpha;
						baseDepth += choose ? depthLayer : 0;
						notcull = choose ? true : false;
					}
					k = 1;
					{
						depthLayer = (1 - depthLayers[k]) * depthRange[k] + baseDepth;
						srcAlpha = colorLayer[k];
						{
							float3 worldPos = GetWorldPosFromViewZ(screenUV01, depthLayer); 
							float4 projPos = GetRainColliderCamProjPos(worldPos);
							half collisionDepth = GetRainColliderCamPixel01Depth(projPos.xy);
							fixed clipped = ClipCond(collisionDepth, projPos.z);
							output = AlphaBlend(alpha, srcAlpha * (1 - clipped));
						}
						bool choose = (viewDepth > depthLayer) && notcull;
						alpha = (choose) ? output : alpha;
						baseDepth += (choose) ? depthLayer : 0;
						notcull = (choose) ? true : false;
					}
					k = 2;
					{
						depthLayer = (1 - depthLayers[k]) * depthRange[k] + baseDepth;
						srcAlpha = colorLayer[k];
						{
							float3 worldPos = GetWorldPosFromViewZ(screenUV01, depthLayer);
							float4 projPos = GetRainColliderCamProjPos(worldPos);
							half collisionDepth = GetRainColliderCamPixel01Depth(projPos.xy); 
							fixed clipped = ClipCond(collisionDepth, projPos.z);
							output = AlphaBlend(alpha, srcAlpha * (1 - clipped));
						}
						alpha = ((viewDepth > depthLayer) && notcull) ? output : alpha;
					}
					return fixed4(_Color.rgb, alpha * _Color.a * i.color.r * i.color.r); // 頂點色衰減
           

2.2.使用?:代替if-else

  我們知道,if這一類條件分支在GPU執行時大部分情況不會帶來性能提升的(但大佬說有特例),相反是會嚴重影響性能的。GPU實際上的做法是分出一半的線程來執行if為true的運算,另一半執行if為false的運算(沒有則等待true的運算完畢),也就是說,就這if這段代碼而言,性能下降最多可達50%。一個可行的方法是用條件運算符?:來代替if-else。

2.3.clip還是step

考慮到clip對early-test開啟的影響,我們可以用step代替clip

3.高低端機差異

  一些效果,無論再怎麼優化總會有個極限。為了讓低端機也有一個可以接受的效果,我再高低端基使用了不同的Shader LOD和配套政策。低端機削減了兩個Layer,另外減少參與深度圖渲染的物件,甚至是不再渲染深度圖。當雨覆寫的距離不大的時候,其實水準遮擋不會很明顯。而垂直遮擋則可以用射線檢測的方法來代替。高端機也削減了一個Layer。

七、遇到的問題

1.ClearColor對深度圖的意義

  在實作這個下雨效果時,要經常跟深度圖打交道,而深度圖的計算和相容問題也是一直被大佬們認為是dirty work。踩的坑,假設你是自己渲染深度圖的話,如果希望深度圖預設值表示“最遠”的地方,Unity的Standalone(D3D)下數值是0,OGL下數值是1,是以要麼保證ClearColor在兩個平台下是不一樣的,要麼在寫深度的Shader裡先做轉化,統一用0表示最近,1表示最遠。

2.自定義的非線性Z轉線性W(指Perspective Division前的W)

  一般主相機的遠裁剪面會設定得比較遠,而用于渲染主相機視角深度圖,出于性能考慮,精度較小(R8),這時就需要另一個遠近裁剪面不一樣的相機做渲染以提高深度圖精度使用率。在計算過程中需要用到觀察空間的線性深度,這時由于遠近裁剪面不一樣,不能用Unity提供的LineaEyeDepth,可以仿照它寫一個自定義遠近裁剪面的函數。(這裡隻讨論D3D11及之後的版本)

inline float CustomPerspectiveLinearEyeDepth(float near, float far, float z) // z ∈[0,1], 深度圖采樣結果
{
#if !defined(UNITY_REVERSED_Z)
	float halfNearInv = 0.5 / near;
	float halfFarIn = 0.5 / far;
	return 1 / max(0.00001, (halfNearInv + halfFarIn - (z * 2 - 1) * (halfNearInv - halfFarIn)));
#else
	float x = -1 + far / near;
	float y = 1;
	float rcpFar = 1 / far;
	float4 zBufferParams = float4(x, y, x * rcpFar, y * rcpFar);
	return 1.0 / (zBufferParams.z * z + zBufferParams.w);
#endif
}
           

3.近裁剪面後面是什麼

  正如上面所說,兩個相機的近裁剪面距離是不一樣的,在深度圖渲染裡近裁剪面後面的部分隻能是預設顔色,它要麼表示最近要麼表示最遠。如果預設是最近,那這部分當然就沒問題了,但如果預設最遠的話,這裡的雨就不會被遮擋。可以通過将深度相機近裁剪面距離改成小于等于主相機的近裁剪面即可,也可以在穿幫不明顯的前提下不做修改。

4.正确地剔除

  這篇文章3中列出的代碼,還有一個問題,就是它的遮擋邏輯實際是不完善的。

移動端天氣系統--【下雨】效果之【雨滴】的實作和分析一、環境二、方案對比三、思路概述四、關鍵技術點五、效果優化六、性能優化七、遇到的問題八、參考

這個if是實作水準方向上的遮擋的,譬如不遠處有堵牆,那就應該看不到更遠處的雨。

這個clip是實作垂直方向上的遮擋的。在這個基礎上,我們想要實作的效果是,如果頭頂有屋檐,那麼近處的雨會被擋住,但依然可以看到遠處的雨。但按照文章中的代碼,如果第1層雨在水準方向上被遮擋了,第2層雨依然有可能被渲染,這明顯是不符合現實的,效果上會出現分層現象。

正确的邏輯應該是,如果上一層雨沒有通過if判斷,之後的雨都應該不再被渲染。

八、參考

  1. Nvidia D3D10 Samples, Rain ↩︎
  2. ATI Toy Shop Demo ↩︎
  3. 天涯明月刀端遊 ↩︎ ↩︎
  4. Neon ↩︎
  5. Microsoft Flight Simulator ↩︎
  6. Remember Me ↩︎
  7. 明日之後手遊 ↩︎