billboard效果的實現以及延伸(空間變換、批處理)

billboard效果的實現以及延伸(空間變換、批處理)

來自專欄 Game Probe

最近有點空了,就寫一下日常工作中的一些心得,主要是為了方便自己記憶,如果有什麼錯誤的地方,懇切希望大家能夠多多指出,這是小弟進步的動力。之所以挑選billboard作為職業生涯的第一篇文章,是因為這其中的涉及的渲染管線、線性變換、批處理技術等一些重要而且基礎的知識點。

由於是第一次寫東西,裡面有很多表述不清楚的地方,歡迎大家多多留言指出。謝謝。


在遊戲中,經常會用到的billboard效果,有兩種方法可以實現:

一:掛腳本控制Transfrom組件,使面片的始終朝向攝像機。

二:在Shader中改變計算頂點位置,使其面向攝像機。

這兩種方法各自都有局限性。

一:實現方法:BillBoard.cs

void OnWillRenderObject() {transform.LookAt(transform.position + Camera.transform.forward, Camera.transform.up); switch (_mode) { case Mode.HorizontalBillboard: Vector3 angles = transform.eulerAngles; angles.x = 90; transform.eulerAngles = angles; break; case Mode.VerticalBillboard: Vector3 angles = transform.eulerAngles; angles.x = 0; transform.eulerAngles = angles; break; }}

腳本掛在物體上,直接改變Transform組件,非常簡單。但是當物體是勾選了bacthing static標記,Unity會對他們進行靜態合併,經過合併後的Mesh是不能變動的,所以會失去效果。所以我們可以在Shader裡面實現這個效果。

順便一提方法是在OnWillRenderObject中實現的,因為這個方法只會在物體參與渲染的時候調用,相對來說比Update要好。

查了下文檔,幾個函數的執行順序如下:

OnPreCull: Called before the camera culls the scene. Culling determines which objects are visible to the camera. OnPreCull is called just before culling takes place.

OnBecameVisible/OnBecameInvisible: Called when an object becomes visible/invisible to any camera.

OnWillRenderObject: Called once for each camera if the object is visible.

OnPreRender: Called before the camera starts rendering the scene.

OnRenderObject: Called after all regular scene rendering is done. You can use GL class or Graphics.DrawMeshNow to draw custom geometry at this point.

OnPostRender: Called after a camera finishes rendering the scene.

OnRenderImage: Called after scene rendering is complete to allow postprocessing of the image, see ImageEffects.

其中OnWillRenderObject()方法只會在具有Mesh組件的GameObject上的腳本中每幀調用。

OnPreCull、OnPreRender、OnPostRender、OnRenderImage只會在Camera上每幀調用。

OnRenderObject方法與OnPostRender相似,不過可以在任意GameObject上調用。

二:BillBoard.shader

先放個銜接:Cg Programming/Unity/Billboards

在vertex program中的關鍵代碼:

output.pos = mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0)) + float4(input.vertex.x, input.vertex.y, 0.0, 0.0));

思路:float4(0,0,0,1)是物體在ObjectSpace中的坐標原點,也就是billboard的中心點。先將該點轉到到ViewSpace中, mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0),得到ViewSpace中billboard的中心點坐標,也就是物體相對於攝像機的位置。

首先假設物體在ObjectSpace下就是一個平行於XY平面的面片,在ViewSpace中,用ViewSpace的x,y基軸來描述面片頂點位置。可以求出各頂點在ViewSpace下的坐標。得到面片就已經正對攝像機。如下:

mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0)) + float4(input.vertex.x, input.vertex.y, 0.0, 0.0)

但是上面這種方法得到的面片始終完全平行於攝像機的XY平面的。不能實現固定某軸旋轉的效果:例如只能沿水平方向旋轉。

於是看到ShadowGun項目裡面關於Billboard的shader代碼,發現有更好的寫法。我這裡先發把原代碼改一下,除了方便解析之外,還有一個原因,後面會說到。

float3 viewerLocal = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));float3 localDir = viewerLocal - float3(0,0,0);localDir.y = lerp(0, localDir.y, _VerticalBillboarding);localDir = normalize(localDir);float3 upLocal = abs(localDir.y) > 0.999f ? float3(0, 0, 1) : float3(0, 1, 0);float3 rightLocal = normalize(cross(localDir, upLocal));upLocal = cross(rightLocal, localDir);float3 BBLocalPos = rightLocal * v.vertex.x + upLocal * v.vertex.y;o.pos = mul(UNITY_MATRIX_MVP, float4(BBLocalPos, 1));

思路跟方法一裡面腳本實現的原理差不多,就是要計算出ObjectSpace下頂點的位置,使其朝向攝像機。

第一步現將攝像機原點位置,轉換到ObjectSpace坐標系中,然後計算出物體中心點到攝像機的方向向量localDir 。如下:

float3 viewerLocal = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));float3 localDir = viewerLocal - float3(0,0,0);

這裡定義一個變數_VerticalBillboarding來控制localDir在垂直方向上的偏移,localDir.y = 0,即localDir平行於XZ平面,從而限制了billboard只能左右轉動。

localDir.y = lerp(0, localDir.y, _VerticalBillboarding);

下面最關鍵的步驟,就是根據localDir,求出兩兩垂直的uplocal,rightlocal方向。這兩個方向決定了面片的朝向。Unity中採用的是左手坐標系,所以向量叉乘也服從左手定則。假定一個uplocal向量是(0,1,0),但如果localDir朝向在頭頂,uplocal也應該跟著旋轉一下。最終得到ObjectSpace下,面片朝向攝像機的三個「軸向量」。

用其中rightLocal和upLocal來表示描述變換後的頂點位置。這樣,在ObjectSpace下,完成了面片朝向攝像機的變換。下面這樣的乘法,如果不是很理解,可以複習一下線性代數的知識。

float3 BBLocalPos = rightLocal * v.vertex.x + upLocal * v.vertex.y;

放一個線性代數的講解鏈接:【官方雙語/合集】線性代數的本質 - 系列合集

線性代數在遊戲開發中的作用非常重要,


好了,其實上面就完成了Billboard的效果。這裡建議看者先試試上面兩個Billboard shader。但一旦你使用起來就會發現,如果重複copy了幾個面片,效果就會發生錯誤。這是因為Unity的優化手段Dynamic Batching造成的,如果將物體都設成batching static就沒問題。但這是為什麼呢?首先我們得了解一下批處理技術(Draw call batching)。

Batching是一種很重要的優化手段,能夠有效低減少DrawCall。更多描述看文檔。這裡主要說一下區別:

  • Static batching:運行時會將共用同一個材質球的網格合併,但不是真正意義上的合併。而是在世界空間中,創建VBO來記錄下這一批網格的頂點數據,等到每一個網格渲染的時候,從VBO中拿到屬於他們自己的頂點,然後進行渲染。所以說,經過Static bathcing後,每個網格渲染時,在vertex階段,頂點的數據還是自身ObjectSpace空間的數據。static batching其實從3D API的層面上來說,並沒有減少Draw的次數,但確實減少了cpu向gpu傳遞Batch(包含頂點數據和Drawcall)的次數,而恰恰這個操作是極其浪費性能的。(上面這段可能會引起爭議)。直接扔鏈接:Draw call batching

Internally, static batching works by transforming the static GameObjects into world space and building a big vertex and index buffer for them. Then, for visible GameObjects in the same batch, a series of simple draw calls are done, with almost no state changes in between. Technically it does not save 3D API draw calls, but it saves on state changes between them (which is the resource-intensive part).

這裡的「3D API draw calls」是為了跟Draw Call進行區分。而我上下文所說的「Draw」都是指3D API draw calls,而非Draw Call。

  • 手動合併網格:手動合併網格簡單粗暴地講網格合併成同一個物體,所以只需要Draw一次。但是,經過合併後的網格,在vertex計算階段,ObjectSpace中就是整個合併後的網格。對於billboard的計算來說,你再也找不到面片中心點了。
  • Dynamic batching:Dynamic batching的實現方法和手動合併網格是類似的,在世界空間中進行,相當於就是動態地合併網格。所以在vertex計算階段,與上面手動合併網格造成的問題一樣。這就是多個billboard存在時,效果不正確的原因。

所以,得出一個很重要的結論就是, Dynamic batching會破壞BillBoard的效果。其實不僅僅是Billboard,Dynamic batching會導致所有的頂點動畫計算錯誤。

因此解決辦法就是,勾選bacthing static進行批處理就好啦!完美解決!

還有一點就是關於手動合併網格和Static batching兩者優化。官方文檔也給出解析,建議開發者不要使用「手動合併網格」來替代static batching。因為static batching是在攝像機「軟裁剪」操作後發生的,而手動合併網格會影響軟裁剪的進行。

關於軟裁剪,我這樣說是為了區分渲染管線中光柵化階段的視錐體剔除。軟裁剪是發生在應用程序階段,攝像機視錐體和場景物體進行一個包圍盒相交檢測,完全在視錐體外的物體將會被剔除,在視錐體內的物體才會允許進入渲染管線,而對於相交的物體而言,取決於引擎的處理方式,Unity對這種情況,只要物體跟視錐體有一點點相交,都是完全允許整個物體進入管線的,可能是因為分割「物體」的計算方法會影響效率的緣故吧。

下面的延伸有興趣可以看看


ShadowGun的為了解決Dynamic batching會破壞頂點動畫的問題。想到的方法就是:在建模階段,預先將面片各個點偏離中心點的值記錄在頂點顏色第二套UV中,然後再在計算的時候還原其中心點,知道中心點,可以通過上面說方法就能計算出billboard效果。

這是一種很好的思路,我對ShadowGun的技術還是非常respect的。

但是!

但是!

但是!

我不知道ShadowGun做的時候,static batch功能還不完善還是怎麼的,對於現在的Unity新版本來說,完全不需要這樣做。有不同意見的歡迎留言謝謝。

鑒於之前踩過的坑,還是要把這種方法的流程說一下:

shader中假定float(0.5,0.5)的位置是中心點。

所以建立一個面片模型的時候,將模型的軸心拉到面片的底邊中心的位置。面片不一定需要正方形,但是你要清楚知道面片的長和寬。

先給頂點刷顏色,因為顏色範圍是0~1,所以ShadowGun只是利用顏色(rg)和中心點

(0.5,0.5),這樣就能計算出vertex到中心點的偏移方向向量。至於距離,可以用UV2來儲存面片的長和寬,這樣就能求出偏移的距離了。U、V分別對應面片的寬和高。

下面放出源碼:

float3 centerOffs = float3(float(0.5).xx - v.color.rg,0) *v.texcoord1.xyy; float3 centerLocal = v.vertex.xyz + centerOffs.xyz; float3 viewerLocal = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)); float3 localDir = viewerLocal - centerLocal; localDir[1] = lerp(0,localDir[1],_VerticalBillboarding); float localDirLength=length(localDir); float3 rightLocal; float3 upLocal; float3 BBLocalPos = centerLocal - (rightLocal * centerOffs.x + upLocal * centerOffs.y) ; o.pos = mul(UNITY_MATRIX_MVP, float4(BBLocalPos,1));

推薦閱讀:

圖靈宇宙漫遊指南
[StrangeLoop 2017] How to Hack a Painting
GPU Gems 基於正弦波之和的水面渲染 (1)
Unity3D如何將圖片以正確的像素顯示在屏幕上
UE4中的基於物理的著色(二)

TAG:計算機圖形學 |