【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂於分享也博採眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
寫在前面
早前,我截幀分析了《Call of Dragons》,具體內容可以看《〈Call of Dragons〉渲染截幀分析與迷思》,在其中提到了《Call of Dragons》中使用了3D場景與部分2D渲染結合的方式來最佳化效能,而這種方式的一個核心思路就是“三轉二”,關於“三轉二”的具體內容也可以參考我更早早前寫的一篇《三渲二(三轉二)2D動畫方案調研》。而這種將3D物體轉換成2D的思想,同樣應用於一項渲染技術——Impostors,中譯為冒名頂替者(感覺怪怪的,所以後文還是說Impostors吧)。
Impostors簡單來說就是使用Billboard的2D片來表示複雜3D模型,可以說《Call of Dragons》中使用的三轉二算是Impostors中的一種簡化版。而《Call of Dragons》中攝像機不會旋轉,因此它處理的情況更簡單,甚至都用不到Billboard渲染,只需要將2D片平行於攝像機近平面擺放就行了。而廣泛意義上的Impostors會在各類遊戲中廣泛應用(通常用於表達遠景),它需要考慮攝像機觀察角度的變化、透視關係、渲染效果還原、過渡等眾多因素,因此Impostors的實踐絕不是一件簡單的事情。
在Unity Asset Store中存在著一個名為Amplify Impostors的工具,它提供了非常好的Impostors技術的實踐,因此本文的主要目的是從Amplify Impostors中詳細分析Impostors的實踐原理與使用到的技術,由此對Impostors渲染技術進行學習與分析。
在本文中,你可以瞭解到:
- Impostors概念。
- Amplify Impostors的簡單使用方法,包含介面引數介紹、配合LOD Group等。
- 官方手冊的要點提煉,包括支援、不支援的功能、不同Impostors型別、適合的使用場景等。
- Amplify Impostors中烘培Impostors的過程詳解,包括Billboard Mesh的建立、GBufferTextures的生成等。
- 對Amplify Impostors提供的Demo場景的簡要分析。
測試環境:Unity 2021.3.12f1、Amplify Impostors 0.9.9.3、URP 12.1.7
一、Impostors概念
參考《Real-Time Rendering, Fourth Edition》(感謝Morakito翻譯的中文版)中的13.6章節,介紹了廣告牌技術Billboarding,其原理是基於攝像機觀察方向來修改紋理矩形朝向,通常會讓矩形總是與螢幕平行。Billboard技術可以用於表示草、煙、能量盾、雲等(比如《原神》中的遠景雲使用了Billboard實現),原書中也舉例了一個用Billboard模擬的雲層。
而Impostors是Billboard技術的一種應用,透過將當前觀察點中的複雜物體渲染成紋理,並將改紋理對映到一個Billboard Mesh上,從而建立一個Impostors。它可以用於同一個物體的若干例項,或者在幾幀中重複使用,從而分攤它的建立成本。
在渲染物體存在的地方,Impostors紋理是不透明的,而在其他任何地方都是完全透明的,它可以將複雜的模型簡化成單個的紋理,因此Impostors對於快速渲染遠處的物體十分有用。說到渲染遠處的物體,當然還有一項耳熟能詳的技術——LOD,即對於遠處的物體使用更簡化的Mesh。然而這種簡化的Mesh往往會丟失物體的形狀資訊和顏色資訊。而Impostors則沒有這個缺點,因為可以使生成的紋理解析度與顯示器的解析度近似匹配。另一種使用Impostors的情況是,當觀察者只能看到這個物體的同一個側面的時候,可以使用Impostors(在《Call of Dragons》中就是這種使用場景)。
在渲染物體建立Impostors紋理的過程中,我們需要將Camera朝向物體包圍盒的中心,並將視錐體設定為物體投影包圍盒的最小矩形。Impostors紋理的Alpha值一開始會被清除為0,而在物體渲染的地方,Alpha值則會設定為1.0。然後將生成的紋理用作面向視點的Billboard Mesh的紋理貼圖。
參考UE4的Document——Render 3D Imposter Sprites,Impostors是使用翻頁書風格的Textures的Sprites,用於儲存一個靜態Mesh的每一個可能的觀察角度下的渲染結果,儘可能讓Sprite的渲染效果在材質和光照方面與原始Mesh匹配。但是,它也可能帶來幀之間的Popping,意味著它不一定適用於所有場景,當渲染大量不會靠近相機且移動緩慢的物體時,它非常有用。
翻頁書風格的Textures如下圖所示,透過將不同角度下材質屬性(Albedo、Normal等)儲存到Textures中,用於渲染Sprite時儘可能模擬原始Mesh,這些就是Impostors所用的GBufferTextures。
二、Amplify Impostors簡介
Amplify Impostors是一個強大的工具,透過使用經典的Billboard技術的現代版本來構建幾何簡化單體積準確的複雜模型表示。
拿官方的一張圖片來舉例,我們就可以知道Impostors做的事情,使用2D片代替3D Mesh來營造視覺欺騙,以此來最佳化渲染上的效能與表現。通常,Impostors可以應用於遠景的表達。
在官方提供的Demo中,有一個場景對比了Impostors和3D Mesh的渲染效果,如下圖所示(左下是3D Mesh,右上是Impostors)。從中可以看到,Amplify Impostors的效果可以做到幾乎與原始Mesh無法區分,在距離渲染物體較遠的情況下,這個效果完全可以以假亂真。
但在距離渲染物體近的情況下,可以看到Impostors會有比較明顯的穿幫,並且在視角轉動時會有Popping與不自然的過渡鬼影。
三、Amplify Impostors的Start Screen
匯入Amplify Impostors.unitypackage後,會自動彈出一個Amlify Impostors Start Screen,如下圖所示。
透過Manual按鈕,可以快速跳轉到官方提供的使用手冊。
同時,Start Screen中提供了3種渲染管線HDRP、URP和Built-In下的Demo用例,點選對應的按鈕即可匯入對應的Demo用例。原文中提到了不要手動解壓用例,否則不能保證匯入正確,請使用Start Screen匯入Demo用例。
官方提供了對Unity 2019.4 LTS以上、URP 10或HDRP 10(SRP 10)以上以及Built-in管線的一鍵匯入支援。注意,對於URP和SRP,官方只確保了對(10以上)原生URP和HDRP的支援,而在實際專案中,我們很可能會對URP或HDRP進行修改,可能會導致匯入用例失敗。在我一開始使用實際專案的URP管線時,匯入就失敗了,原因是丟失了Decal Feature和SSAO Feature,因為Decal被我們從URP中刪除了。
所以對於初步上手Amplify Impostors,建議先使用原生URP管線(HDRP也行,但我用的是URP,所以後文均會在URP的情況下實踐)。在瞭解Amplify Impostors後,可以按照需要移植到實際專案的URP管線中。
四、Manual提煉
1. Impostors型別
Amplify Impostors中提供了三種預烘培Impostors型別:
- Spherical:以經典的經緯度劃分烘培角度進行快照。這種情況的Impostors Shader非常簡單,因此效能很好,並且Impostors近距離看下來表現基本沒問題,但是在視角發生變化的多幀之間,抖動很明顯。可以透過犧牲渲染解析度以提高幀率來減少該問題(雖然我想應該不會有人這麼做)。
Spherical的圖示如下,左邊的猴子是真實3D模型,右邊的猴子是Impostors效果,灰色幾何體代表預烘培的所有角度(後兩種型別的圖示也是這種排布,後續不再贅述)。
- Octahedron:以Icosphere(短程線性多面體Geodesic Polyhedron)劃分烘培角度進行快照。這種方式烘培的好處是,給出攝像機的任意座標,都可以在多面體上找到最接近該位置的3個Frame,然後對3個Frame進行混合。這種方法很好地解決了視角移動時多個Frame之間的抖動。但是,Shader複雜度會更高,當近距離觀察Impostors時,混合會產生明顯的鬼影Artifacts。
- HemiOctahedron:Octahedron的變體,只從“Icosphere的上半球”進行相同數量的快照,從而有效地將混合精度提高了一倍。其缺點在於,如果不烘培下半球,從下面看時會產生不正確的結果。只有當我們已知永遠只會從上方觀察該物體時,該方法才適用。
2. 實時渲染Feature
- SRP(HD and LW)(v4.9.0+)
- 標準/Legacy的前向渲染和延遲渲染
- 動態光照和陰影
- 物體相交時的深度寫入
- 全域性光照
- 烘培的Lightmaps(透過自定義烘培)
- GPU Instancing
- 抖動穿插過渡
3. 其他支援的功能與不支援的功能
- 支援自定義打包貼圖:渲染最多4個貼圖用於Standard材質和自定義材質,也可以使用自定義烘培最多8個不同的貼圖。
支援自定義形狀編輯器:自動生成Impostors的自定義形狀,減少Impostors的透明區域以減少Overdraw,同時也支援手動編輯。 - 支援烘培預設:定義了烘培Impostors的預設,其中還包括各種匯入和匯出選項。
- 不支援Skinned Mesh的烘培,目前不支援Animated Skinned Meshes,比如角色。作者計劃提供一個實時烘培變體,允許動畫物體以指定速率渲染為Impostors。
- 如果想在近距離下改善質量,只能提高紋理解析度,因為近距離下渲染Impostors會產生Artifacts。注意,Impostors不適用於近距離渲染,它不能代替真實的Mesh,只能作為遠距離下渲染的效能最佳化。2K尺寸的紋理通常效果OK,對於移動端,甚至可以使用1K(感覺1K也很大啊)。
- 僅當Shader包含Deferred Pass時才支援標準烘培,比如Unity Standard Shaders。但是建立出的Impostors可以同時用於前向和延遲渲染兩種模式。如果原始的Mesh使用了自定義的前向Shader(比如卡通渲染),我們需要使用Custom Baking Shader來烘培它。
五、Impostors烘培介面
Impostors的烘培操作是透過在Inspector檢視中Amplify Impostors指令碼提供的介面下進行的,如下圖所示。
參考Manual,對幾個重要引數進行解釋:
- Impostors Asset:對烘培Impostors生成的資原始檔的引用,其中包含了該物件大部分的Impostors資訊。使用資原始檔形式的好處是如果我們有多個相同物件的Impostors,資原始檔可以共享。
- LOD Group:引用物件身上的LOD Group元件,作者似乎更建議Impostors要和LOD Group搭配使用。
- References:要烘培成Impostors的Renderer的引用。
- BakeType:烘培和渲染Impostors使用的技術,即上文提到的三種Impostors型別,Spherical、Octahedron和HemiOctahedron。
- Texture Size:最終烘培出的Image尺寸。更高的尺寸意味著在近距離下有更好的表現,但會帶來更高的記憶體佔用和執行時渲染消耗。
- Axis Frames:每個軸上的Frames數量。例如,16代表會對單個Impostors執行256(16x16)次快照。(這個軸指最終Impostors對應的2DTextures的橫軸縱軸)對於Spherical Impostors,每個軸上可以指定不同的Frame數量。
- Pixel Padding:對於每個快照的邊緣畫素的填充Padding,以避免由Mipmap引起的渲染偽影。
- Billboard Mesh:可用於編輯烘培的Billboard Mesh形狀,使用了Unity內建的Shape Editor來自動估計形狀,同時提供了手動編輯功能。
- Bake Preset:提供了Standard烘培預設以及可自定義的預設,在預設中,可以配置烘培用的Shader以及執行時渲染Impostors用的Shader(可自定義),也可以配置烘培輸出的Impostors物件的材質Textures(_AlbedoAlpha、_SpecularSmoothness等等)以及Textures的一些屬性。
六、Impostors的烘培過程
講完烘培Impostors的操作介面,接下來看下Impostors的烘培過程。首先,在前面提到過,僅當Shader包含Deferred Pass時才支援標準烘培,同時在Manual中提到過,預設烘培會使用原始物體的Shader中的Deferred Pass。
整個烘培過程可以簡單描述成3個步驟:
- 生成Billboard Mesh。
- 烘培GBufferTextures。
- 建立Impo Billboard。
1. 生成Billboard Mesh
因為最終Impostors是渲染在Billboard方式的Mesh上的,所以得先思考Mesh長什麼樣。可能我們會首先想到,直接用一個矩形Quad不就行了嗎?沒錯,矩形片完全OK可用,但實際渲染物件可能是不規則的形狀,矩形片上會有許多完全透明的區域,這意味著我們需要在Billboard Mesh使用的紋理上也空出這些透明的區域,因為UV座標是在整個Mesh的所有頂點之間插值的,由此造成了紋理空間的浪費以及紋理的實際精度下降。因此,對於Billboard Mesh的構建,使用最貼近原渲染物件邊緣的簡單不規則幾何片是最合理的,這樣能充分利用紋理,也能減少Overdraw。
接下來就是考慮如何生成最貼近原渲染物件邊緣的簡單不規則幾何片,主要思路就是找到所有Frame下的渲染物件邊界,然後使用其中的最大(小)值作為最終Billboard Mesh的邊界。在Amplify Impostors中,使用的方法可以簡單描述成以下兩個步驟(為了易於理解,我描述的步驟和程式碼實現會有略微不同,但原理完全是一致的):
- 建立一張預設大小為256x256的AlphaTexture,對於每個Frame,將渲染物件使用正交投影(並確定合適的VP矩陣,確保渲染物件儘量保持在畫面的正中心)渲染到AlphaTexture,渲染的Pass是按DEFERRED、Deferred、GBuffer的順序尋找到一個可用的延遲渲染Pass。簡單理解就是將所有Frame下渲染物件的Alpha值(相當於Mask)疊加渲染到一個Mask上,該Mask可以覆蓋到所有Frame下渲染物件的不透明區域。可以在程式碼中配置AlphaTexture的尺寸,但是太大沒有意義,這裡只是用來生成簡單多邊形。同時這裡我們也看到,在Amplify Impostors中烘培用的Shader必須包含延遲渲染Pass。
- 將上一步生成的AlphaTexture的Alpha通道值渲染(使用Blit)到一張同尺寸256x256的CombinedAlphaTexture上(Combined顧名思義)。使用的Pass程式碼如下,非常簡單。重新Blit的主要目的是過濾出AlphaTexture的Alpha通道值(因為上一步走的延遲渲染Pass還會讓AlphaTexture包含RGB值)。
- 根據CombinedAlphaTexture(相當於一個最終的Mask)使用一定演算法(只要能找到近似幾何邊緣的演算法就行)求出原渲染物件的近似幾何邊緣,由這個近似幾何邊緣的頂點生成最終的Billboard Mesh。在Amplify Impostors中使用的求邊緣方法為Unity Editor下的SpriteUtility的Generate Outline方法,Sprite本身提供了符合這個功能的方法,省的自己實現了。
接下來詳細介紹這三個步驟的具體實現,核心邏輯在於疊加渲染所有Frame下的3D物體來生成AlphaTexture,該邏輯和最終生成Impostors的GBufferTextures的邏輯大部分相同,很多邏輯相通(比如確定當前Frame下的CameraVP矩陣),因此瞭解了生成AlphaTexture的邏輯就必然能理解最終烘培GBufferTextures的邏輯。
1.1 生成AlphaTexure
生成AlphaTexure詳細過程如下:
- 首先透過AmlifyImpostor.cs引用的AmplifyImpostorAsset獲取烘培用到的所有資訊,包括Impostors型別、烘培用的shader、預設等等。
- 計算3D Mesh在所有快照角度下的Bound的最大值(對應CalculatePixelBounds函式),生成各個快照角度下的CameraView矩陣的旋轉部分,應用於mesh.bounds,得到每個角度旋轉後的meshBounds資訊,然後在所有角度中對xy方向Bound和Depth方向Bound找到最大值。
- 初始化建立要烘培的AlphaTexture(如果是烘培GBufferTextures建立的就是_AlbedoAlpha、_SpecularSmoothness這些GBufferTextures),以及一張trueDepth Texture(用於在烘培時執行深度測試),Texture的尺寸均為minAlphaResolution預設256值(如果是烘培GBufferTextures則建立的尺寸為配置的Texel Size,比如2048)。
- 開啟sRGB寫入。
- 記錄原Renderer的Transform資訊(Position、Rotation、LocalScale這些)並儲存到成員變數,並將其重置為預設值(位置歸零、縮放為1)。
- 開始烘培Impostors(對應RenderImpostors函式)。首先判斷Bake Shader是否為空:是,則啟用標準烘培;不是,則啟用自定義烘培。
- 建立一個CommandBuffer,SetRenderTarget為所有要烘培的AlphaTexture(如果是烘培GBufferTextures則設定為GBufferTextures陣列,利用MRT一次烘培多張Textures)和trueDepth(DepthRT,臨時資源,用於深度測試),進行一次ClearRenderTarget對所有Textures初始化清理。
- 篩選下要烘培的Mesh,去除所有為空的、Disable的、ShadowsOnly的Renderer。
- 遍歷所有快照角度,在每個Frame角度下進行快照,每次快照具體步驟如下。
- 對於每個Frame角度,首先計算CameraView矩陣的旋轉部分,計算旋轉後mesh.bounds作為FrameBounds(當前Frame的邊界資訊)。然後根據FrameBounds計算烘培需要使用的Camera的V矩陣和正交P矩陣。其中V矩陣包含了一些資訊,攝像機從哪看——從Bounds中心退後一定距離,以及要看向哪——FrameBounds的中心。P矩陣包含了一些資訊,投影模式為正交(必須是正交),根據Bounds以及第2步計算出的最大值計算視錐體各個平面(即near、far、left、right等),主要目的就是找到一個合適大小的視錐體。
- 得到當前Frame角度的V矩陣和P矩陣後,透過CommandBuffer設定對應Pipeline狀態(VP矩陣、視口),注意對於AlphaTexture的渲染,視口統一為(0, 0, 256, 256)。然後,選擇烘培用的材質:如果啟用了標準烘培,則在當前Renderer使用的材質中按DEFERRED、Deferred、GBuffer的順序尋找到一個可用的Pass,如果全部都沒找到,則Fallback到LightMode為Deferred的Pass,並且如果存在DepthOnly的PrePass,則記錄下該PrePass;如果啟用了自定義烘培,則將<當前Renderer的材質,自定義烘培使用的材質>的鍵值對關係儲存到bakeMats字典中,在自定義烘培的情況下,預設會使用Pass 0。
- 根據Renderer的屬性,設定Lightmap、全域性光照相關的關鍵字和值。關閉LightProbe關鍵字,因為LightProbe原本作用是影響動態物體,即使Renderer本身是動態的,但在烘培時也不需要讓Renderer包含LightProbe提供的的光照資訊,否則物體所處的位置、周圍環境的變化會影響烘培結果。雖然對於烘培AlphaTexture這一步是多餘的,因為我們只需要Alpha值,但是對於烘培GBufferTextures是必要的。
- 對Renderer進行烘培:在標準烘培的情況下,呼叫CommandBuffer.DrawRenderer,如果包含DepthOnly的PrePass則先渲染PrePass,再渲染GBUFFER,將GBUFFER渲染到AlphaTexture(如果是烘培GBufferTextures,則渲染到多張GBufferTextures)上;在自定義烘培的情況下,呼叫CommandBuffer.DrawRenderer,使用自定義的烘培材質的Pass 0將物體烘培到AlphaTexture(如果是烘培GBufferTextures,則渲染到多張GBufferTextures)上。
- 重複10~13步,烘培得到所有Frame下渲染物件Alpha值合併後的AlphaTexture。
- 清理所有臨時資源,包括CommandBuffer、烘培材質。
1.2 生成CombinedAlphaTexture
有了所有Frame下的AlphaTextures,生成CombinedAlphaTexture就很簡單了,即過濾出AlphaTexture的Alpha通道值。
- 開啟sRGB寫入。
- 載入取樣Alpha值用的Shader及材質(Shader名為ShaderPacker)。
- 建立一張RenderTexture,名為CombinedAlphaTexture,尺寸為minAlphaResolution預設256值。
- 將AlphaTexture使用上述步驟2的材質Blit到CombinedAlphaTexture上。
- 將型別為RenderTexture的CombinedAlphaTexture儲存到Texture2D型別的CombinedAlphaTexture中,方法很簡單,使用Texuture2D.ReadPixels函式,這裡不過多描述。
1.3 生成Billboard Mesh
傳入引數CombinedAlphaTexture以及一些必要的引數,直接呼叫SpriteUtility.GenerateOutline生成BillboardMesh。
如下圖所示,木桶(3D Mesh)最後生成的CombinedAlphaTexture(256x256)和Billboard Mesh長這樣。
2. 烘培GBufferTextures
烘培GBufferTextures可以簡單分為以下幾個步驟:
- 生成GBufferTextures。
- 對GBufferTextures執行一些可選操作。
2.1 生成GBufferTextures
接下來就是生成GBufferTextures實際執行時渲染Billboard Mesh要取樣的GBufferTextures了,在瞭解了AlphaTexture如何生成後,生成GBufferTextures的原理我們就幾乎完全知道了。
生成GBufferTextures的步驟和7.1.1節生成AlphaTexture幾乎完全相同,除了視口Viewport設定會有所區別。在渲染GBufferTextures時,每個Frame的渲染結果不會疊加在RenderTarget上,而是會在GBufferTextures上劃分出快照數量(通常是Frame x Frame)個Grid區域,渲染每個Frame時將視口指定為對應Frame的區域(有點像陰影級聯的邏輯)。邏輯在這裡就不贅述了,因為只有視口的區別以及一些細枝末節的區別。
如下圖即為一個木桶生成的16x16的GBufferTextures中的AlbedoAlpha紋理。
2.2 可選操作
根據烘培的預設、提供的Feature以及單次烘培的配置,可以有一些額外的步驟,不是很重要的點,因此略過詳細內容。
- (可選操作)在標準烘培下,對GBufferTextures的值做一定修正(Depth、Albedo等)。
- (可選操作)如果輸出GBufferTextures的圖片格式為TGA,則將通道佈局改為BGRA。
- (可選操作)若烘培預設中PixelPadding值大於0,則使用ImpostorDilate Shader對GBufferTextures進行邊緣擴張。
- (可選操作)若在烘培預設的基礎上Override了當前烘培的GBufferTextures尺寸,則對GBufferTextures進行Resize,(也就是說可以單獨修改單次烘培中個別GBufferTexture的尺寸)。
3. 生成Impostors Billboard
目前我們生成了Billboard Mesh和GBufferTextures,接下來就是組合成最後的Impostors Billboard(GameObject)。
- 配置Runtime下渲染用的Shader並建立對應材質儲存到AmlifyImpostorAsset中:如果在烘培預設中指定了Runtime Shader,則使用指定的Runtime Shader;如果沒指定,則根據Impostors Type使用Amplify Impostors提供的對應Runtime Shader。
- 將(RAM中的)GBufferTextures建立並儲存到Assets中對應資料夾(磁碟空間)中,生成貼圖資原始檔。
- 建立ImpostorsGameObject,為其建立並配置SharedMesh(Billboard Mesh)、LOD Group。
- 為Runtime下的材質設定正確的GBufferTextures Properties,根據Impostors型別開啟關閉對應關鍵字,將一些必要的Properties設定為正確值。
由此,最終的Impostors Billboard就生成完畢了。
如下圖所示,最後的AmplifyImpostorAsset的資源包括以下內容:Runtime材質、Billboard Mesh、GBufferTextures。這些資源也就是渲染一個Impostors需要的所有東西。
七、Runtime渲染
上一節完整介紹了Impostors Billboard的離線烘培生成,接下來我們來看向執行時如何渲染Impostors。
在這一節中,我們會重點關注Impostors的Frames選擇與插值,而不會關注Impostors Shader的前向渲染Pass和延遲渲染Pass,因為其不是本文的主要內容。接下來,會以效果最好的OctachedronImpostorsURP.shader為例,對Shader進行分析。
首先分析在Vertex Shader中,Billboard片的實現和如何確定使用哪些Frame以及對應頂點UV。
- 首先是Billboard片的實現,將CameraPosition從世界空間變換到模型空間objectCameraPosition,求出模型空間物體指向攝像機的單位向量objectCameraDirection。
- 使用簡單的叉乘計算,求出定義Billboard Mesh的(單位)正交向量,即計算Billboard Mesh朝向objectCameraDirection時Local的上向量(Billboard Local Y)與右向量(Billboard Local X)。如下圖所示,只考慮相機在Object Space的XY平面內的情況下,Billboard正交向量示意圖。
- 直接對Billboard Mesh的Vertex的X分量乘以右向量(Billboard Local X),Y分量同理。這樣做是正確的,因為Vertex的X分量確定了頂點水平方向到空間中心的距離,因此該距離等於Billboard顯示時頂點水平方向到Billboard空間中心的距離。因此該操作保證了Billboard顯示時從Camera看到的Mesh和模型空間下從Z軸負方向看向Mesh是一致的。以上三步即為Amplify Impostors中Billboard的實現,該方法利用了Billboard Mesh只關注頂點的XY分量(Z分量始終為0)的特性,從而避免了矩陣與向量的運算,而是直接使用標量乘以向量,簡化了計算。
- 接下來確定使用哪些Frame以及對應頂點UV,首先計算從Camera指向變換後的Billboard頂點的向量localDir,對於localDir使用八面體對映投影到二維平面上。八面體對映的邏輯可以參考《八面體引數化球面對映方式及實現》,該技術主要用於將球面引數化對映到平面,通常會用於CubeMap的紋理對映,這篇文章中對該方法介紹的很詳細,在此不做過多贅述。
- 將localDir對映到二維平面之後,就相當於將localDir對映到GBufferTextures上的UV座標OctaUV(範圍0到1),此時OctaUV必定落在某一個Frame的Grid內部,那麼容易得到floor(OctaUV x framesHorizontal)就是當前落到的Frame索引baseOctaFrame。
- 透過baseOctaFrame,很容易計算出該Frame對應的Grid在GBufferTextures上的UV範圍,比如對於2x2的Grid,左下角的Grid對應的UV範圍為[0, 0.5]。同時,我們也可以得到該Frame的起始UV點(即Grid的左下角),將起始UV點使用八面體對映的逆運算對映回三維空間下的向量,該向量即對應該Frame下烘培相機的近平面Plane的法線Normal。
- 接下來就是求當前頂點對應的UV座標。已知該Frame下烘培相機近平面Plane的法線Normal、objectCameraPosition、localDir,求出以objectCameraPosition為起點、localDir為方向的射線與法線為Normal的平面的交點p。
- 建立一個與Plane正交的本地切線空間座標系TBN,向量Tangent與Bitangent與Normal正交。
- 已知交點p、以Plane為XZ平面的切線空間的X、Z方向單位向量Tangent、Bitangent,求出p在X、Z方向上的分量frameX、frameZ,由此求得當前頂點對應的UV座標uvs=-(frameX, frameZ)(為什麼是負的?)。同時,我們計算區域性法線localNormal。
- 對UVS應用Scale和Offset。
- 由於OctachedronImpostors中,會對3個Frame進行取樣插值,所以接下來會找到與當前落到的Frame索引baseOctaFrame相鄰的2個Frame進行6~10步的計算,得到另2個UV座標,關於相鄰Frame的選擇在此不做贅述。
- 最終,得到Octachedron多面體上找到最接近該相機位置的3個Frame對應的3組頂點UV。
Fragment Shader就沒什麼好說的了,主要就是取樣三個Frame然後根據權重插值,其餘就是正常的延遲渲染Pass了。
八、【番外篇】Amplify Impostors Demo分析
終於,我們分析完了Amplify Impostors中所有核心邏輯。接下來是一些我在分析Amplify Impostors提供的Demo時的一些分析過程,就當作番外篇了,有興趣的同學可以閱讀,跳過也完全OK。
因為之前提到了我在使用自己的URP管線時匯入失敗,那麼就首先看下官方Demo用例中的URP Asset和Renderer都有些什麼特點吧。
Demo中提供了三種質量的URP Asset,分為高保真度High Fidelity、均衡模式Balanced和效能至上Performant。三種Asset使用的Renderer均為同一個AI Universal Render Pipeline Asset Renderer。
在這裡就拿Balanced分析吧(魯迅先生說過,中國人的性情總是喜歡調和和折中的)。
首先看AI Universal Render Pipeline Asset Balanced(AI顯然是Amplify Impostors的縮寫)。
雖然是個毫無特點而言的URP Asset(主要也是因為URP Asset本身玩不出什麼花樣了),但要注意到一點是,當前使用的Renderer並不支援OpenGLES3,這個對於手遊專案而言還是挺致命的,很多手機裝置都可能用的是OpenGLES3(快都給我去用Vulkan啦!),之後查一下Renderer不支援OpenGLES3的原因,埋坑ing~
在該URP Asset中,啟用了Depth Texture和Opaque Texture特性。Depth Texture特性會在渲染完Opaque Pass(取決於Renderer上的Depth Texture Mode,在這裡是After Opaques)之後Copy Camera深度圖到一張_CameraDepthTexture中,而Opaque Texture特性會在渲染完Opaque Pass之後使用Opaque Downsampling(可配置的降取樣模式)將ColorAttachment降取樣到一張_CameraOpaqueTexture中。
對於URP Asset,基本上沒別的好說的了。接下來看AI Universal Render Pipeline Renderer。
這裡看到Rendering Path為Deferred,即使用了延遲渲染。Depth Texture Mode為After Opaques,上文中提到過,會在Opaque Pass之後Copy Depth到一張Texture。
分析完了URP管線,接下來看一個Demo場景Barriels & Boulders(木桶和巨石)。
這個場景非常簡潔,放了5個木桶和3塊石頭。就拿一個木桶來分析吧。
對於一個木桶Game Object,其使用了LOD Group元件:LOD0是個正常的3D模型,有2106個三角面;LOD1是Impostors,只有6個三角面。也就是說,當攝像機距離木桶很遠時,木桶會切到Impostors顯示。
這樣是Impostors的一個很恰當的實踐方法——即近景使用3D模型,遠景使用Impostors。正如官方手冊中提到的,"The common purpose of using such technique is to be able to represent far distant objects with a very low polycount, for instance, trees, bushes, rocks, ruins, buildings, props, etc." Imposter適合用於代替3D模型表現遠景。雖然Impostors在近景表現也還算可以,但在一些情況(如視角轉動)下還是會穿幫,以及一些過渡的ArtiFacts,是否在近景使用Impostors取決於專案需求。
九、小結
好啦,目前我已經介紹了我對Impostors的全部認識,包括Impostors的概念、以及Amplify Impostors中對該技術的實現細節。相信在足夠了解之後,無論是自己去實踐Impostors,還是基於Amplify Impostors進行改造,都會變得不那麼遙不可及。至於Impostors的實際效能怎麼樣,這裡我尚且沒有關注,一是因為我目前也仍然在Impostors學習過程中,二是對於不同的場景、不同的Impostors使用,效能都是未知的,不能一概而言。本文也告一段落了,未來如果有對Impostors新的認知,後面會及時更新到本文中。
參考
- https://assetstore.unity.com/packages/tools/utilities/amplify-impostors-119877#description
- https://wiki.amplify.pt/index.php?title=Unity_Products:Amplify_Impostors/Manual
- https://en.wikipedia.org/wiki/Geodesic_polyhedron
- https://zhuanlan.zhihu.com/p/408898601
- https://zhuanlan.zhihu.com/p/667993188
- https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/RenderToTextureTools/3/
- 題圖來自畫師wlop
這是侑虎科技第1561篇文章,感謝作者歐幾里得範數供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯絡我們,一起探討。(QQ群:465082844)
作者主頁:https://www.zhihu.com/people/ou-ji-li-de-fan-shu-34
再次感謝歐幾里得範數的分享,如果您有任何獨到的見解或者發現也歡迎聯絡我們,一起探討。(QQ群:465082844)