http://www.gameres.com/671850.html
Unity的資源讀取相信已經有很多分析的文章,本文將深入資源格式及引擎源碼分析unity的資源讀取,並嘗試給assetbundle添加一個接口AssetBundle.LoadDependentResource(name)加載一個AssetBundle中依賴的但又沒有顯式路徑的資源。
目的
對於特大規模的場景來說,我們往往希望能夠釋放掉一些不常用或遠離的被依賴的資源來減少內存。假如我們有一個名爲A的fbx角色模型依賴B、C兩個Texture和D、E兩個AnimChip,當這個A模型離鏡頭比較遠時,並正在播放D這個AnimClip,我們可能希望1)暫時卸載掉Texture B和C中的minmip的精細幾層,只保留粗糙的幾層,2)卸載掉AnimClip
E,並在A重新臨近鏡頭時,3)重新加載紋理B和C中精細的幾層minmip和4)重新加載AnimClip E,5)以及在一些特殊需求下直接需要加載資源包中的某個被依賴的資源。很多同學會想到調用Resouces.UnloadUnusedAssets來卸載不用的資源,但是並沒有直接的方法能夠卸載和重新加載正在被引用的資源中的一部分,如本文需求中的1)~5)。
Unity引擎的AssetBundle已經爲我們帶有了Load(path)和LoadAll等接口,其中:
Load (name : string, type : Type) : Object需要指定一個name/path,即處於包中的資源名字/相對路徑,而這名字則是打包的時候往往通過
BuildPipeline.BuildAssetBundle,
BuildPipeline.BuildStreamedSceneAssetBundle
BuildPipeline.BuildAssetBundleExplicitAssetNames
等函數顯示/非顯示打包的資源名字,對於依賴的資源通常會通過BuildAssetBundleOptions.CollectDependencies等標記一併打包進AssetBundle,但是我們在加載的時候卻無法直接通過Load指定名字來加載,除非把這個依賴資源也通過BuildPipeline.BuildAssetBundleExplicitAssetNames顯式添加名字到assetNames集合。本文的主要目的就是讓程序實現能夠直接加載沒有明確顯式指定的卻又因被依賴關係打包進AssetBundle的資源。
AssetBundle格式
AssetFile就是我們導出exe後bin/xxx_Data目錄下的各個擴展名爲.assets的文件,AssetBundle就是這些.assets文件的集合打包成一個可壓縮的.unity3d擴展名的文件。因此我們對Unity源碼進行修改,在原有AssetBundle.CreateFromFile基礎上也添加AssetBundle.CreateFromFolder允許從磁盤目錄上加載一堆.assets文件作爲一個映射到AssetBundle進行統一管理。每個AssetBundle包括一個頭BlockAssetBundleHeader,
AssetBundleHeader由signature,streamVersion, unityVersion等字段組成。讀完assetbundle的頭信息之後後面就是lzma壓縮數據包括可能的AssetFile集合,把數據解壓縮到一個MemoryStream後,在這個MemoryStream裏首先是每一個AssetFile在AssetBundle裏的條目信息:
指出了每一個AssetFile在解壓縮後的MemoryStream字節流中的偏移和長度。接下來就可以提取回每個AssetFile出來。
AssetFile格式
每個AssetFile由頭信息Header、類型樹TypeTree、資源對象信息ObjectInfo、外部結構externalsStruct、對象數據ObjectData等Block組成。頭信息Header Block如下:
包含了metadataSize,fileSize,versionInfo,dataOffset等字段以及表示編碼順序的endianness字段。緊隨其後的TypeTree Block保存了各種序列化的類型節點,也包括了嵌入的類型信息節點。ObjectInfo包括了AssetFile裏面每個Asset對象的元數據(條目信息,FileID指示了對象處於AssetFile的相對序號,offset指示字節偏移,length指示Asset對象長度,typeID指示類型類型,classID類型id,isDestroyed是否刪掉)如下:
跟隨而來後面的ObjectData Block就是每個對象的具體字節流。每個對象的名字,序列化的變量,屬性等,紋理的像素數據等都是存在對象的字節流裏。對於打包進AssetBundle的AssetFile的第一個對象往往是一個xxx<AssetBundle>對象,而獨立存在於目錄上的AssetFile的第一個對象往往是一個xxx<ResourceManager>對象,分別用於反序列化這兩種資源管理類。接下來就是個資源對象Asset了,比如xxx<Texture2D>, yyy<AnimClip>, zzz<Material>等。以紋理資源爲例,xxx<Texture>這樣一個名字爲xxx的紋理資源,首先有一堆原來在磁盤上.meta文件裏記錄的字段,接下來就是像素的字節流。下面給出了一些常用的類型的字段例子:
A3563f73<Texture2D>
229bed14<Material>
Box001<Mesh>
AlphaTest-VertexList<Shader>
unknown<AssetBundle>
UVMesh<MonoScript>
常見的一些資源類型及其屬性字段
Unity在加載這些資源的時候就是先反序列化這些字段到對象實例對應的變量上。
每個Asset對象的元數據只包含了Asset資源的引用對{FileID,localID},Asset對象自身的屬性又沒有相對路徑,那麼unity的Resouces.Load等究竟又是如何通過相對路徑映射到這各個對象呢?這個相對路徑到{FileID,localID}的映射表存在那?我們再次閱讀unity源碼,發現Unity初始化AssetBundle類實例會首先反序列化m_Container,m_MainAsset,m_PreloadTable等變量,而該類實例正是對應AssetFile中的第一個沒名字的<AssetBundle>資源對象。對於目錄上而非打包的AssetFile則是對應其中的第一個沒名字的<ResourceManager> 資源對象。m_Container變量是一個key-value表保存了所有打包時顯式資源從相對路徑path到Asset{FileID,localID}的映射。我們想直接加載的依賴資源並沒有在m_Container的映射表中,爲了能直接加載非顯式資源,我們另外建立一個映射表來實現從依賴的隱式對象名字name到引用對{FileID,localID}的映射表。
實現
AssetBundle導出給C#的接口定義在Runtime/Export/AssetBundleBindings.txt
具體實現在runtime/misc/AssetBundleUnity.cpp,AssetBundle.CreateFromFile 調用 ExtractAssetBundle加載AssetBundle到內存,具體加載過程是讀入文件頭後把AssetBundle壓縮數據解開,得到多個AssetFile的頭(一個AssetBundle包含多個AssetFile),名字可能包括”CAB”以識別,得到AssetFile頭後按普通AssetFile調用持久化管理器PersistentManager.LoadExternalStream把AssetFile中的資源對象映射入內存。
AssetBundle.Load加載對象具體在AssetBundleUtility:oadNamedObjectFromAssetBundle實現,首先調用ResourceManager::GetPathRange獲得AssetBundleBindings.txt增加LoadDependent
AssetBundleUnity.cpp中添加
在bundle.GetLocalID 裏從自定義的另外一個映射表根據名字查找資源引用對LocalSerializedObjectIdentifier {FileID,localID},得到後傳入重載的另外一個以LocalSerializedObjectIdentifier爲參數ProcessAssetBundleEntries逐個加載具體的Asset對象即可。