詳解Unity官方插件: Animation-Instancing

前言

本篇博客的主旨并不是介紹如何使用 Animaiton-Instancing 插件 (對(duì)這部分內(nèi)容有需求的同學(xué)可以很方便的參考網(wǎng)上其他同學(xué)的貢獻(xiàn)),而是通過(guò)解構(gòu)源碼梳理原理的方式弄清楚這個(gè)插件到底能為我們帶來(lái)什么,如何控制參數(shù)以便最大化運(yùn)行時(shí)收益,以及還有哪些需要改進(jìn)的地方。首先本文會(huì)從相對(duì)宏觀的角度來(lái)解讀該插件所解決的核心痛點(diǎn),以及其之所以能夠奏效的背后的核心原理。其次由于Animation-Instancing插件是按照面向數(shù)據(jù)和大規(guī)模運(yùn)算而設(shè)計(jì)的,我重點(diǎn)拆解并講解了其中的主要數(shù)據(jù)(包括運(yùn)行時(shí)和預(yù)處理階段生成的數(shù)據(jù)和結(jié)構(gòu))。在搞清楚運(yùn)作原理和數(shù)據(jù)結(jié)構(gòu)的前提下,我在文章的第三部分以表格形態(tài)簡(jiǎn)單羅列了一下該插件的API及重要全局變量,以作為對(duì)其他使用文檔的補(bǔ)充。最后的部分是一些其他相對(duì)重要的分支,比如attachment,root motion等功能的支持,再比如對(duì)重要shader代碼庫(kù)函數(shù)的注釋和說(shuō)明等。作為附錄,我也給出了一些自己在閱讀源碼過(guò)程中發(fā)現(xiàn)的bug(這些bug可能至今沒(méi)有在官方原始工程repo中得到修正),希望對(duì)后來(lái)使用者有所幫助。

Big Picture

傳統(tǒng)的骨骼動(dòng)畫(huà)是一項(xiàng)非常消耗CPU時(shí)鐘的活兒,大體上它需要在每一幀的時(shí)間內(nèi),為場(chǎng)景中的每一個(gè)動(dòng)畫(huà)角色解算出最新的動(dòng)作(Pose),每一次這樣的動(dòng)作計(jì)算又涉及到數(shù)以萬(wàn)計(jì)頂點(diǎn)位置的更新,更有甚者,這些更新本身需要數(shù)倍于頂點(diǎn)數(shù)目的矩陣運(yùn)算和線性插值。雖然我們可以利用CullingGroup剔除不被拍攝到的角色,可以硬著頭皮減少模型頂點(diǎn)數(shù)目,還可以利用引擎提供的部分基于CPU和(或)GPU的多線程處理能力并行的解算(矩陣運(yùn)算)角色的動(dòng)畫(huà)數(shù)據(jù),但是面對(duì)不斷擠進(jìn)屏幕的角色,這種主要依賴于昂貴CPU時(shí)間來(lái)處理海量矩陣運(yùn)算的方法顯然是不太明智的。Unity官方推出的 “Animation-Instancing” 插件給出了一套能夠最大化利用GPU強(qiáng)大并行算力的方案(區(qū)別于之前引入的基于compute shader的GPU輔助矩陣計(jì)算)。接下來(lái)我們來(lái)簡(jiǎn)單梳理下這套方案的Big Picture:

動(dòng)畫(huà)解算最消耗資源的地方在于對(duì)模型網(wǎng)格頂點(diǎn)位置的計(jì)算,播放中的動(dòng)畫(huà)要求蒙皮上的頂點(diǎn)跟隨關(guān)聯(lián)的骨骼運(yùn)動(dòng),而預(yù)先錄制好的骨骼動(dòng)畫(huà)記錄了骨骼在其父骨骼空間下的位置,由此可見(jiàn),我們需要在每一個(gè)動(dòng)畫(huà)幀中讀取骨骼根節(jié)點(diǎn)的信息,依次尋找到各級(jí)子節(jié)點(diǎn),利用已知的 bindpose 矩陣(相當(dāng)于記錄了默認(rèn)狀態(tài)下蒙皮頂點(diǎn)綁定到當(dāng)前骨骼的姿勢(shì)),求解出目標(biāo)頂點(diǎn)在其關(guān)聯(lián)的骨骼空間下的坐標(biāo)位置,最后一路使用骨骼父節(jié)點(diǎn)到子節(jié)點(diǎn)的逆變換矩陣,轉(zhuǎn)換到真正的模型空間(根骨骼所處的空間)。這個(gè)過(guò)程看似復(fù)雜,但是只要我們?cè)敢夥艞壒趋乐g的拓?fù)潢P(guān)系(本質(zhì)上是放棄了動(dòng)態(tài)操作骨骼鏈,支持實(shí)時(shí)物理反饋的可能性),那么只需要一次矩陣運(yùn)算,就能讓蒙皮頂點(diǎn)從默認(rèn)模型空間位置變換到目標(biāo)幀動(dòng)畫(huà)期望的模型空間位置!這個(gè)“關(guān)鍵矩陣”可以如此求解:

key_matrix[i] = root.worldToLocalMatrix * bonePose[i].localToWorldMatrix * bindPose[i];   // (1)
bindPose[i] = bonePose[i].worldToLocalMatrix * root.localToWorldMatrix;                   // (2)

變量名解釋

  • 兩式中所有的下標(biāo)i均用于索引骨骼,
  • 式(1)的bonePose[i]特指骨骼[i]在當(dāng)前動(dòng)畫(huà)幀時(shí)的transform,
  • 式(2)的bonePose[i]特指骨骼[i]在第0幀動(dòng)畫(huà)時(shí)的transform,
  • root是所有骨骼的根節(jié)點(diǎn),也可以認(rèn)為是模型對(duì)象的transform,它不隨動(dòng)畫(huà)變化,
  • bindPose可以直接從Unity的Mesh對(duì)象中獲取到,也可由式(2)手動(dòng)計(jì)算出來(lái)。

矩陣乘法解釋
將模型空間中點(diǎn)A轉(zhuǎn)換到骨骼[i]表示的空間后,骨骼[i]受動(dòng)畫(huà)驅(qū)動(dòng)產(chǎn)生了變化,用處于新位置的骨骼逆變換矩陣(從本地到世界),將點(diǎn)A變換到新的世界空間中,最后用模型根的變換矩陣,把點(diǎn)A從世界空間變回模型空間。可見(jiàn)經(jīng)過(guò)這樣一圈變化,我們的頂點(diǎn)從模型空間來(lái),回到了模型空間去,期間受所綁定骨骼節(jié)點(diǎn)最新位置和朝向的影響,從模型空間來(lái)看產(chǎn)生了一點(diǎn)或多或少的變化,當(dāng)然在骨骼空間觀察,頂點(diǎn)的位置依然是保持不變的,既模型的頂點(diǎn)被骨骼"帶著走"了。

至此Big Picture中最重要的一環(huán)已被我們斬獲,可以設(shè)想我們能夠在渲染管線的頂點(diǎn)階段(vertex stage),通過(guò)采樣預(yù)編碼的紋理,獲取到任意動(dòng)畫(huà)幀上任意骨骼的變換矩陣信息。那么只需要能準(zhǔn)確的知道當(dāng)前參與運(yùn)算的模型頂點(diǎn)到底被那個(gè)(哪些)骨骼影響,影響權(quán)重是多少,還有我們的動(dòng)畫(huà)模型目前播放到哪一個(gè)動(dòng)畫(huà)幀,只需知道這幾樣信息,就能采樣到符合預(yù)期的變換矩陣,計(jì)算并混合出這個(gè)頂點(diǎn)在目標(biāo)動(dòng)畫(huà)幀時(shí)刻所處的模型空間新坐標(biāo)了。這都不是什么難事,動(dòng)畫(huà)幀作為全模型統(tǒng)一的變量,可以直接通過(guò)材質(zhì)傳參實(shí)現(xiàn)賦值和更新,至于頂點(diǎn)與骨骼的綁定關(guān)系以及它們之間的影響權(quán)重等與頂點(diǎn)綁定的參數(shù),自然可以預(yù)先埋設(shè)到模型Mesh的頂點(diǎn)色(Mesh.color)和頂點(diǎn)uv(Mesh.uv)中去。

剩下的拼圖與上述核心渲染邏輯關(guān)系不大,主要是數(shù)據(jù)的收集、組織和派發(fā)在工程上的必要實(shí)現(xiàn),這里面涉及的模塊主要有:負(fù)責(zé)采樣動(dòng)畫(huà)信息并烘焙成紋理的預(yù)處理模塊;負(fù)責(zé)運(yùn)行時(shí)解碼數(shù)據(jù),組織層級(jí)結(jié)構(gòu),統(tǒng)一維護(hù)和管理所有待渲染實(shí)例的中心化模塊;還有負(fù)責(zé)追蹤角色數(shù)據(jù)更新,處理動(dòng)畫(huà)事件,同步幀動(dòng)畫(huà)數(shù)據(jù)的實(shí)例模塊。凡此種種,會(huì)在之后專門的單元中進(jìn)一步介紹,不過(guò)作為Big Picture的一部分,它們的功能總的來(lái)說(shuō)只是為了能在正確是的時(shí)候?yàn)閟hader提供正確的數(shù)據(jù)而已。

主要數(shù)據(jù)結(jié)構(gòu)解析

這一部分主要圍繞數(shù)據(jù)的編碼和解碼過(guò)程,以及數(shù)據(jù)的分層管理方案展開(kāi):

(一)編碼流程與紋理中的數(shù)據(jù)結(jié)構(gòu)

對(duì)于重?cái)?shù)據(jù)處理的模塊,沒(méi)有什么比讀圖來(lái)得更加準(zhǔn)確和高效了,所以具體細(xì)節(jié)請(qǐng)參考下圖,這里文字部分只簡(jiǎn)單勾勒一下Codec的編碼邏輯以及最終輸出的數(shù)據(jù)結(jié)構(gòu):

(1)Animation-Instancing插件的編碼模塊(又叫預(yù)處理模塊或Codec),它的工作對(duì)象是一個(gè)特定的prefab資源,會(huì)通過(guò)讀取其上SkinnedMeshRenderer組件獲取所有骨骼的關(guān)聯(lián)信息,同時(shí)也會(huì)讀取第一個(gè)找到的animator組件,而后遞歸訪問(wèn)其中所有動(dòng)畫(huà)狀態(tài)(ChildAnimatorState)以獲取動(dòng)畫(huà)剪影(AnimationClip)資源和動(dòng)畫(huà)事件信息(AnimationEvent),但是動(dòng)畫(huà)狀態(tài)間的跳轉(zhuǎn)邏輯(Transition)并不會(huì)被收集。而后Codec開(kāi)啟烘焙,對(duì)每個(gè)動(dòng)畫(huà)按照預(yù)設(shè)的FPS進(jìn)行采樣,計(jì)算并錄入上文提及的頂點(diǎn)轉(zhuǎn)換矩陣(每個(gè)矩陣與一個(gè)骨骼綁定),與此同時(shí)修改Mesh的頂點(diǎn)色和UV2,錄入骨骼索引和權(quán)重(4通道,所以頂點(diǎn)最大支持被4塊骨頭影響)。

(2)編碼后的數(shù)據(jù)以二進(jìn)制byte[]形式緊密排列,詳細(xì)信息參考下圖,簡(jiǎn)單說(shuō)一共可以拆分為三個(gè)部分,第一部分是角色自身各個(gè)動(dòng)畫(huà)的參數(shù)信息(AnimationInfo),這里面最重要的是動(dòng)畫(huà)Index到紋理Offset的映射關(guān)系。第二部分是可選的,用于存儲(chǔ)各種待綁定的骨骼節(jié)點(diǎn)名稱以及變換矩陣(矩陣并未使用),系統(tǒng)會(huì)在加載attachment時(shí),利用編碼的骨骼信息,確定變換矩陣,一次性將頂點(diǎn)從附加物模型空間變換到角色模型空間中去,并且位于綁定骨骼的正確的相對(duì)位置。最后一部分是序列化為byte[]的紋理單元,系統(tǒng)支持多紋理存放海量的動(dòng)畫(huà)幀數(shù)據(jù),但是任何一個(gè)獨(dú)立動(dòng)畫(huà)不能拆分到不同紋理中,而且除了最后一張紋理可以小于1024以外,其他紋理都必須是1024大小的(1024x1024是系統(tǒng)支持的最大紋理尺寸),以確保竟可能少用紋理。

Encoded Data Structure

(二)解碼流程與數(shù)據(jù)結(jié)構(gòu)

解碼流程比較平淡,參考下圖中左上角的流程圖:每當(dāng)掛在角色身上的 Animation-Instancing 被啟動(dòng)(Start),系統(tǒng)會(huì)自動(dòng)收集其頂點(diǎn)和材質(zhì)數(shù)據(jù),整合到LodInfo結(jié)構(gòu)中。隨后角色腳本會(huì)找到全局唯一的Mgr,把自己注冊(cè)上去。緊接著會(huì)立馬發(fā)起數(shù)據(jù)加載request,到Unity工程的特定Path下尋找與自己的prefab同名的烘焙數(shù)據(jù),加載之。如果一切順利,那么系統(tǒng)會(huì)展開(kāi)數(shù)據(jù)并分別存放到 InstanceAnimationInfo結(jié)構(gòu)(對(duì)應(yīng)編碼紋理里的AnimationInfoAttachmentInfo)以及AnimationTexture結(jié)構(gòu)中去,這些解碼數(shù)據(jù)結(jié)構(gòu)均由全局單例對(duì)象管理。

還是有一點(diǎn)可以提醒下大家,按照流程,如果我們需要展現(xiàn)1000個(gè)同源prefab單位的場(chǎng)景,那么就需要在場(chǎng)景中實(shí)例化創(chuàng)建1000個(gè)帶有AnimationInstancing的該角色,這些對(duì)象帶有完整的骨骼結(jié)構(gòu),掛載多個(gè)組件,以及處于失活狀態(tài)的MeshRenderMonoBeahvior.Start方法執(zhí)行期間會(huì)訪問(wèn)該組件及其中數(shù)據(jù))。我想在運(yùn)行時(shí)的內(nèi)存消耗肯定會(huì)比完全由腳本模擬的方式來(lái)得多(主要由 <常駐的一堆無(wú)用骨骼節(jié)點(diǎn)> + <“可能的”對(duì)Mesh資源的訪問(wèn)而引起的數(shù)據(jù)加載&創(chuàng)建> 所致)。

Decode work flow

(三)數(shù)據(jù)的分層管理

這是一個(gè)比較大的邏輯范疇,籠統(tǒng)的說(shuō),系統(tǒng)從四面八方收集來(lái)數(shù)據(jù),需要將它們分類、分層布置妥當(dāng),及時(shí)刷新,在每個(gè)渲染幀的最后,再以適當(dāng)?shù)姆绞教峤唤o管線執(zhí)行渲染。雖然我對(duì)系統(tǒng)使用的幾個(gè)關(guān)鍵數(shù)據(jù)結(jié)構(gòu)做了思維導(dǎo)圖,但是很快法線如果我們不清楚影藏在背后的許多技巧性質(zhì)的邏輯,數(shù)據(jù)結(jié)構(gòu)中的大量變量和子結(jié)構(gòu)根部無(wú)法通過(guò)名字準(zhǔn)確理解含義。因此我決定從數(shù)據(jù)接受方的需求入?yún)㈤_(kāi)始,反向展開(kāi)數(shù)據(jù),當(dāng)然這里特指由Graphics提供的渲染接口:DrawMeshInstance

參考如下,標(biāo)黑的數(shù)據(jù)結(jié)構(gòu)體量由前往后 (既序號(hào)由小到大)依次膨脹(被包含),最早羅列出的則是直接輸入到渲染API的結(jié)構(gòu)參數(shù)。

(1)InstancingPackage.subMesh
說(shuō)明:一個(gè)實(shí)例渲染的Mesh可能存在多個(gè)submesh,同時(shí)每個(gè)submesh也可以有其自身對(duì)應(yīng)的材質(zhì)Material,對(duì)DrawMeshInstance接口來(lái)說(shuō),每次調(diào)用傳入的是當(dāng)前Avatar對(duì)應(yīng)Mesh的一個(gè)subMesh。
因此這一層結(jié)構(gòu)的出現(xiàn)是為了 -> 區(qū)分不同submesh和material

(2)List<InstancingPackage>.at( packageIndex )
說(shuō)明:?jiǎn)蝹€(gè)DrawMeshInstance有最大合批上限,工程中設(shè)置為200,所有超過(guò)200個(gè)相同單位的渲染要求,會(huì)模式系統(tǒng)追加新的合批渲染(叫InstancePackage)。
因此這一層結(jié)構(gòu)的出現(xiàn)是為了 -> 能夠區(qū)分復(fù)數(shù)個(gè)InstancePackage

(3)List<InstancingPackage>[ boneTextureIndex ]
說(shuō)明:模型動(dòng)畫(huà)可能被保存在不同的boneTexture紋理下,一個(gè)材質(zhì)Mat只綁定了一張動(dòng)畫(huà)紋理,因而必須依據(jù)在播動(dòng)畫(huà)區(qū)分實(shí)例化合批,不同目標(biāo)動(dòng)畫(huà)的材質(zhì)不同,即便有一樣的Mesh和MeshRender,也不能合批渲染。
因此這一層結(jié)構(gòu)的出現(xiàn)是為了 -> 區(qū)分出擁有目標(biāo)動(dòng)畫(huà)紋理資源的資源集合。
一點(diǎn)優(yōu)化的迷思 -> 動(dòng)畫(huà)紋理以Textrue2DArray方式存放?

(4)VertexCache[ meshRenderIndex ]
說(shuō)明:下標(biāo)“meshRenderIndex”引導(dǎo)出的VertexCache對(duì)象主要用來(lái)對(duì)應(yīng)一個(gè)特定的Renderer,具體而言是SkinnedMeshRender或MeshRender中的一個(gè)。
因此這一層結(jié)構(gòu)的出現(xiàn)是為了 -> 確定待渲染的 Mesh 資源到底是那一個(gè)。

(5)LodInfo[ lod ]
說(shuō)明:對(duì)應(yīng)一個(gè)Avatar不同的Lod層級(jí)資源,依據(jù)遠(yuǎn)近不同,模型網(wǎng)格和渲染材質(zhì)都會(huì)有不同的變化 。
因此這一層結(jié)構(gòu)的出現(xiàn)是為了 -> 確立目標(biāo)Lod級(jí)別的模型資源。

(6)Avatar_#N
說(shuō)明:對(duì)應(yīng)角色Avatar,AnimationInstance腳本,prefab等。這是數(shù)據(jù)管理層面的最上層。

然后再來(lái)看數(shù)據(jù)結(jié)構(gòu)的全貌,應(yīng)該會(huì)更加清晰,注意到上文中歸納的一線數(shù)據(jù)結(jié)構(gòu)脈絡(luò)能在下圖中比較明確得定位到,唯一的例外是VertexChache,似乎 InstancingPackage 的管理者是 MaterialBlock。

Data Overview

實(shí)際上如下圖所示,VertexCache 結(jié)構(gòu)主要存放著任何與頂點(diǎn)和材質(zhì)有關(guān)的數(shù)據(jù),比如模型的Mesh,還有與材質(zhì)對(duì)應(yīng)的MaterialBlock。且系統(tǒng)單例通過(guò)全局緩存池的方式管理新生成的VertexCache,這樣當(dāng)添加了同prefab的其他實(shí)例時(shí),該對(duì)象不會(huì)被反復(fù)構(gòu)造,而是訪問(wèn)同一個(gè)引用,從而也訪問(wèn)了相同的MaterialBlock結(jié)構(gòu)。

VertexCache

參考如下MaterialBlock結(jié)構(gòu),系統(tǒng)通過(guò)引導(dǎo)相同prefab的對(duì)象訪問(wèn)并更新同一份數(shù)據(jù),從而達(dá)到了歸類可合批渲染對(duì)象,同時(shí)又區(qū)分不同實(shí)例之間獨(dú)立的MaterialProperty。我們不妨考查一下 InstanceData 數(shù)據(jù)結(jié)構(gòu),系統(tǒng)在每次Render之前會(huì)更新其中全部數(shù)據(jù),它所存放的數(shù)據(jù)數(shù)組的三個(gè)維度依次是:不同動(dòng)畫(huà)紋理(boneTexture) -> 不同合批次(200個(gè)單位合批一次) -> 同一批次下的不同實(shí)例。因此這個(gè)數(shù)據(jù)結(jié)構(gòu)記錄的正是不同實(shí)例之間的差異,主要涉及世界空間的變換矩陣,還有動(dòng)畫(huà)幀相關(guān)的幾個(gè)參數(shù)。

MaterialBlock

API

AnimationInstancing -> API
AnimationInstancing -> Key Variant

其他技術(shù)細(xì)節(jié)

關(guān)于shader庫(kù)的解讀

參考如下源碼,shader很簡(jiǎn)單,只有1~2個(gè)方法,使用時(shí)直接包含工程提供的AnimationInstancingBase.cginc庫(kù),然后在vertex stage中,使用模型空間位置之前,調(diào)用一下skinning方法,用返回值替換原有模型空間坐標(biāo)即可。

half4 skinning(inout appdata_full v)
{
    //骨骼權(quán)重
    fixed4 w = v.color;
    //骨骼索引,這些骨骼會(huì)影響當(dāng)前頂點(diǎn) 
    half4 bone = half4(v.texcoord2.x, v.texcoord2.y, v.texcoord2.z, v.texcoord2.w);

    //獲取Instance數(shù)據(jù)
    float curFrame = UNITY_ACCESS_INSTANCED_PROP(frameIndex_arr, frameIndex); //當(dāng)前動(dòng)畫(huà)幀,浮點(diǎn)數(shù),可帶小數(shù)點(diǎn)后尾數(shù),用以表示前后動(dòng)畫(huà)幀之間的轉(zhuǎn)換進(jìn)度
    float preAniFrame = UNITY_ACCESS_INSTANCED_PROP(preFrameIndex_arr,  preFrameIndex);         //切換到當(dāng)前動(dòng)畫(huà)前,上一段動(dòng)畫(huà)停留在的幀 
    float progress = UNITY_ACCESS_INSTANCED_PROP(transitionProgress_arr,  transitionProgress);  //前后兩端動(dòng)畫(huà)的轉(zhuǎn)換進(jìn)度 

    //當(dāng)前幀,整數(shù),向下取整
    int preFrame = curFrame;
    //下一幀,整數(shù)
    int nextFrame = curFrame + 1.0f;

    //采樣4次紋理,獲取當(dāng)前幀下,4個(gè)不同骨骼節(jié)點(diǎn)對(duì)自身的變換矩陣
    //注意一次紋理采樣返回的half4只能覆蓋4X4矩陣中的一行,因此需要至少3次采樣 
    half4x4 localToWorldMatrixPre = loadMatFromTexture(preFrame, bone.x) * w.x;   
    localToWorldMatrixPre += loadMatFromTexture(preFrame, bone.y) * max(0, w.y);    //矩陣乘法分配律
    localToWorldMatrixPre += loadMatFromTexture(preFrame, bone.z) * max(0, w.z);    
    localToWorldMatrixPre += loadMatFromTexture(preFrame, bone.w) * max(0, w.w);

    //采樣4次紋理,獲取下一幀,4個(gè)不同骨骼節(jié)點(diǎn)對(duì)自身的變換矩陣
    half4x4 localToWorldMatrixNext = loadMatFromTexture(nextFrame, bone.x) * w.x;
    localToWorldMatrixNext += loadMatFromTexture(nextFrame, bone.y) * max(0, w.y);
    localToWorldMatrixNext += loadMatFromTexture(nextFrame, bone.z) * max(0, w.z);
    localToWorldMatrixNext += loadMatFromTexture(nextFrame, bone.w) * max(0, w.w);

    //計(jì)算當(dāng)前幀位置(模型空間內(nèi))
    half4 localPosPre = mul(v.vertex, localToWorldMatrixPre);
    //計(jì)算下一幀位置(模型空間內(nèi))
    half4 localPosNext = mul(v.vertex, localToWorldMatrixNext);
    //按播放進(jìn)度插值,獲取準(zhǔn)確的當(dāng)前時(shí)間點(diǎn)的位置 
    half4 localPos = lerp(localPosPre, localPosNext, curFrame - preFrame);

    //求解Normal和Tangent在當(dāng)前時(shí)刻的位置
    ...

    //采樣獲取上一段動(dòng)畫(huà)的頂點(diǎn)變換矩陣,為了性能考量,這里默認(rèn)只受到一個(gè)骨骼影響,權(quán)重為100% 
    half4x4 localToWorldMatrixPreAni = loadMatFromTexture(preAniFrame, bone.x);
    half4 localPosPreAni = mul(v.vertex, localToWorldMatrixPreAni);

    //如果設(shè)置了前動(dòng)畫(huà)幀,那么就按照給定額progress比例,返回前后動(dòng)畫(huà)的插值,
    //如果沒(méi)有設(shè)置前動(dòng)畫(huà)幀,直接返回前后兩幀插值的結(jié)果
    localPos = lerp(localPos, localPosPreAni, (1.0f - progress) * (preAniFrame >  0.0f));

    //最后總結(jié)一下,一共花費(fèi)了27次紋理采樣 
    return localPos;
}

root motion

實(shí)現(xiàn)root motion需要通過(guò)掛載在角色身上的AnimationInstatnce.cs腳本在每個(gè)游戲幀中,使用烘焙好的線速度和角速度等參數(shù),輔助計(jì)算出當(dāng)前待播放的動(dòng)畫(huà)幀進(jìn)度,進(jìn)而推送到GPU端參與動(dòng)畫(huà)運(yùn)算而得。因?yàn)椴皇侵攸c(diǎn),這里不做深入解讀。

attachment

參考前文,特別是編碼圖示對(duì)attachment的描述,所謂的“掛載附屬物”本質(zhì)上是另一個(gè)獨(dú)立的模型prefab,但是與主模型(帶有骨骼)的某個(gè)骨骼掛點(diǎn)相互關(guān)聯(lián),播放時(shí)該模型融入粘在了角色模型的給的掛點(diǎn)上。為了達(dá)成這個(gè)效果,系統(tǒng)會(huì)在CPU運(yùn)行加載attachment的階段,將attachment上的某個(gè)掛點(diǎn)(比如說(shuō)slot_A)與角色身上的某個(gè)同名骨骼掛點(diǎn)(slot_A)互相綁定和重疊起來(lái)。具體而言,我們視“掛載附屬物”的默認(rèn)模型空間等價(jià)于角色身上的(slot_A)骨骼空間,然后用骨骼空間bindPose矩陣的逆矩陣(inverse)將attachment從自己的模型空間變換到角色的模型空間,同時(shí)保留了與slot_A的相對(duì)位置關(guān)系。最后交給GPU渲染時(shí),使用的是變換后的模型頂點(diǎn),動(dòng)畫(huà)關(guān)聯(lián)骨骼使用slot_A指定的骨骼,權(quán)重100%。

具體到工程應(yīng)用中,我們要確保角色模型和attachment模型上都有同名的骨骼節(jié)點(diǎn)作為掛點(diǎn),系統(tǒng)依賴一些相同的名字進(jìn)行關(guān)聯(lián)和綁定。

[后記和參考] 一些發(fā)現(xiàn)的坑

其中前四個(gè)不會(huì)造成計(jì)算錯(cuò)誤,但是多少會(huì)有些性能影響;后三個(gè)在特定條件下會(huì)照成計(jì)算錯(cuò)誤,直接影響表現(xiàn)效果。

  1. [創(chuàng)建但未使用] AnimationInstancingMgr.cs腳本中定義在同名class中的instanceDataPool變量 -> 有初始化,但是沒(méi)有任何地方引用,具體可參考CreateVertexCache方法內(nèi)創(chuàng)建InstanceData并入池的操作,但是搜索工程并沒(méi)有法線對(duì)池中對(duì)象的取用操作。

  2. [不可進(jìn)入分支] AnimationInstancingMgr.cs腳本中 SetupVertexCache方法內(nèi),當(dāng)法線render.bones的數(shù)量與合并過(guò)后的全部骨骼數(shù)量不對(duì)等時(shí),會(huì)進(jìn)入分支創(chuàng)建boneIndex數(shù)組 (通過(guò)new int[render.bone.Length]創(chuàng)建),里面有如下代碼分支永遠(yuǎn)無(wú)法進(jìn)入:

if (boneIndex.Length == 0)
{
    boneIndex = null;
}
  1. [重復(fù)創(chuàng)建和覆蓋] AnimationInstancingMgr.cs腳本中的SetupVertexCache方法主要是設(shè)置VertexCache結(jié)構(gòu)對(duì)象中的骨骼權(quán)重和材質(zhì)引用,但是方法末尾會(huì)遍歷packageList,為買一張boneTexture創(chuàng)建一個(gè) InstancingPackage結(jié)構(gòu)。這個(gè)的問(wèn)題在于該結(jié)構(gòu)重復(fù)創(chuàng)建了,更早之前在調(diào)用SetupVertexCache方法的AddMeshVertex方法中,已經(jīng)在所調(diào)用的CreateBlock方法內(nèi)完成了目標(biāo)InstancingPackage結(jié)構(gòu)的創(chuàng)建。

  2. [退出未徹底清理] AnimationInstancingMgr.cs腳本中 vertexCachePool 隊(duì)列在 RemoveInstance() 操作后并未去除掉歷史數(shù)據(jù),可能導(dǎo)致Render過(guò)程中執(zhí)行更多次無(wú)意義的空循環(huán)。

  3. [錯(cuò)誤估算了烘焙紋理的尺寸和張數(shù)] AnimationGenerator.cs腳本中的CalculateTextureSize方法內(nèi),當(dāng)待處理動(dòng)畫(huà)數(shù)據(jù)無(wú)法完整打包進(jìn)一張(或多張)“最大尺寸”紋理時(shí),會(huì)嘗試將余下數(shù)據(jù)打包進(jìn)一張能完整容納且尺寸最小的紋理,但該方法所揭示的算法邏輯實(shí)際上是在尋找能完整放入“下一段”動(dòng)畫(huà)的紋理,而不是完整放入余下全部動(dòng)畫(huà)的紋理。這會(huì)導(dǎo)致最后一張紋理尺寸偏小。

[The size and number of baked textures were incorrectly estimated]
The problem code locates right at method "CalculateTextureSize" in script file "AnimationGenerator.cs". According to the project context, the method is trying to pack all animation data as tight as possible, which means it will try packing all data into ONE(or more) texture with largest size(1024) at first, if still remains some, it'll find out the best suited texture (probably with size smaller then 1024) to fit all rest data. However, when dealing with those remaining data, what the code really does is to find out a texture with certain size that can just hold data from "One Animation Clip", not from "Rest All Clips" , which may lead to a releatively smaller size of estimation for the last texture.

https://github.com/Unity-Technologies/Animation-Instancing/issues/116

  1. [創(chuàng)建了錯(cuò)誤尺寸的烘焙紋理] AnimationGenerator.cs腳本中的PrepareBoneTexture方法內(nèi),第二個(gè)for循環(huán)用于創(chuàng)建烘焙紋理,其中涉及判斷紋理width的邏輯有錯(cuò)誤,導(dǎo)致width變量在count > 1時(shí),只能取到size=1024尺寸的紋理 -> 導(dǎo)致最后一張紋理的大小被放大 -> 也恰巧解決了(5)導(dǎo)致的問(wèn)題。
for (int i = 0; i != count; ++i)
{   //如果有多張紋理,既count > 1,理論上最后一張紋理的size使用textureWidth標(biāo)記的值(一般會(huì)小于1024),而前面所有紋理size=1024
    //參考如下邏輯,判斷條件應(yīng)當(dāng)為 count > 1 && i < count -1 ? ...
    int width = count > 1 && i < count ? stardardTextureSize[stardardTextureSize.Length - 1] : textureWidth;
    bakedBoneTexture[i] = new Texture2D(width, width, format, false);
    bakedBoneTexture[i].filterMode = FilterMode.Point;
}

[Wrong size of baking texture was created]
The problem code locates right at method "PrepareBoneTexture" in script file "AnimationGenerator.cs". According to the code, one can not generate any texture with the size equal to variant "textureWidth" which describes the size of last texture (which is miss-estimated as well).

https://github.com/Unity-Technologies/Animation-Instancing/issues/117

  1. [切換動(dòng)畫(huà)的插值采樣問(wèn)題] 動(dòng)畫(huà)切換涉及到動(dòng)作融合,需要采樣上一段動(dòng)畫(huà)指定幀數(shù)的編碼數(shù)據(jù),在shader中重構(gòu)出那個(gè)狀態(tài)的頂點(diǎn)坐標(biāo),然后再拿目標(biāo)狀態(tài)的頂點(diǎn)坐標(biāo)去做Lerp。問(wèn)題在于作者使用的材質(zhì)只支持一張boneTexture紋理,如果上一段動(dòng)畫(huà)并沒(méi)有編碼在當(dāng)前紋理中,那么重構(gòu)動(dòng)畫(huà)的工作將會(huì)基于完全錯(cuò)誤的數(shù)據(jù)執(zhí)行。

[Sampling problem for interpolating animations]
Switching between different anim-clips involves in animation interpolation, there will be sampling requests among different animation-info blocks during shader's vertex stage. By reconstructing pre-anim and cur-anim frame state and appling interpolation based on transition progress, we get animation transition properly. However this could be true only if the two anim-infos were packed within the same boneTexture, otherwise we lost our pre-anim info or even worse(sampling on a totally wrong area). To solve this issue, maybe you could bind a texture2DArray to the instancing material and introduce one more shader variable as the extra index of boneTexture :)

https://github.com/Unity-Technologies/Animation-Instancing/issues/118

  1. [烘焙attachment問(wèn)題] 在AniamtionGenerator.cs腳本的BakeWithAnimator方法中,有如下處理附加物體骨骼和綁定的邏輯:
List<Transform> listExtra = new List<Transform>();
Transform[] trans = generatedFbx.GetComponentsInChildren<Transform>();          //附加物對(duì)象(如手持的劍)上的全部transform
Transform[] bakedTrans = generatedObject.GetComponentsInChildren<Transform>();  //角色身上的全部transform 
foreach (var obj in selectExtraBone)
{
    if (!obj.Value)
        continue;

    for (int i = 0; i != trans.Length; ++i)
    {
        Transform tran = trans[i] as Transform;
        if (tran.name == obj.Key)
        {
            bindPose.Add(tran.localToWorldMatrix);
            //以下為修改后代碼
            for (int j = 0; j < bakedTrans.Length; ++j)
            {
                if (bakedTrans[j].name == obj.Key)
                {
                    listExtra.Add(bakedTrans[j]);
                    break;
                }
            }
            //以下為原始代碼
            //listExtra.Add(bakedTrans[i]);
        }
    }
}

代碼內(nèi)存for循環(huán)的行為邏輯是由問(wèn)題的,源碼中bakedTrans隊(duì)列與實(shí)際參與循環(huán)的trans隊(duì)列長(zhǎng)度和內(nèi)容屬性都不一致,直接套用trans隊(duì)列關(guān)聯(lián)的索引值“i”取用bakedTrans中的東西,在邏輯上找不到合理性。事實(shí)上按照上式中修改后的代碼對(duì)attachment進(jìn)行烘焙是成功的。

[Malfunction when baking prefab with attachment]
The problem code locates right at method "BakeWithAnimator" in script file "AnimationGenerator.cs". For more details plz refer to the following code(and comments).

https://github.com/Unity-Technologies/Animation-Instancing/issues/119

參考

TODO

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容