爲了實現遊戲人物外形的定製,專門研究了Unity示例程序 。對程序中動態載入身體各部位模型並進行組合的代碼看了很久才明白。下面做一些備忘。
首先需要了解幾個基本對象的結構
一、 SkinedMeshRender:該對象負責網格繪製。主要數據成員包括
var bones : Transform[] 骨骼
var materials : Material[] 材質
var sharedMesh : Mesh 網格
其中Mesh的主要成員是
vertices : Vector3[] 頂點
boneWeights : BoneWeight[] 骨骼權重
boneWeights數組與vertices數組對應,表示對應下標的頂點運動受骨骼影響的權重。BoenWeight結構記錄了骨骼在SkinedMeshRender.bones數組中的索引。
二、網格和材質的對應關係
一張實際的網格只能施加一個材質。因此,當render所使用的mesh包含多個實際網格(sub mesh),它對每個sub mesh所施加的材質實際上是materials數組中對應下標的材質。
三、合併網格(CombineMeshes)函數的第二個參數是設置是否將多個子網格合併成一張實際的網格。正如前面所述,一個實際的網格只能施加一個材質,所以只有被合併的所有網格原來使用的就是同一個材質(即共享材質)時,將它們真正合並才能正確應用材質。否則,應該將該參數置爲false,表示不實際合併這些sub mesh,而是將它們作爲被合併後Mesh對象的sub mesh。
四、數組對應問題:網格頂點和骨骼、sub mesh和材質之間的對應都是通過數組下標進行的,所以操作時保證新生成的個數組下標對應關係正確是非常重要的。
這是例子中組合創建模型的主要函數,我將自己理解後的備註添加在裏面。
// Creates a character based on the currentConfiguration recycling a
// character base, this way the position and animation of the character
// are not changed.
// 這個函數實際上並沒有將各部分的子網格合併成一張網,而只是將他們合併到
// 同一個Mesh下作爲sub mesh。因爲一張網格只能用一個材質,只有所有子網格
// 都共享同一個材質時,合併成一張網才能保證材質應用正確。
public GameObject Generate(GameObject root)
{
// The SkinnedMeshRenderers that will make up a character will be
// combined into one SkinnedMeshRenderers using multiple materials.
// This will speed up rendering the resulting character.
List<CombineInstance> combineInstances = new List<CombineInstance>();
List<Material> materials = new List<Material>();
List<Transform> bones = new List<Transform>();
//獲得構成骨架的所有Transform
Transform[] transforms = root.GetComponentsInChildren<Transform>();
//一次處理構成身體的各部分
foreach (CharacterElement element in currentConfiguration.Values)
{
//GetSkinnedMeshRenderer()內部Instantiat了一個由該部分肢體Assets構成的
//GameObject,並返回Unity自動爲其創建SinkedMeshRender。
SkinnedMeshRenderer smr = element.GetSkinnedMeshRenderer();
//注意smr.materials中包含的材質數量和順序與下面的sub mesh是對應的
materials.AddRange(smr.materials);
for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)
{
CombineInstance ci = new CombineInstance();
ci.mesh = smr.sharedMesh;
ci.subMeshIndex = sub;
combineInstances.Add(ci);
}
// As the SkinnedMeshRenders are stored in assetbundles that do not
// contain their bones (those are stored in the characterbase assetbundles)
// we need to collect references to the bones we are using
// 網格點與骨骼的對應關係是通過Mesh數據結構中的BoneWeight數組來實現的。該數組
// 與網格頂點數組對應,記錄了每個網格點受骨骼(骨骼記錄在SinkedMeshRender的bones
// 數組中,按下標索引)影響的權重。
// 而此處,示例程序提供的肢體Assets並不包含骨骼,而是返回骨骼名稱。因此,推斷
// GetBoneNames()返回的骨骼名稱應該與實際骨骼數組的順序相同。
foreach (string bone in element.GetBoneNames())
{
foreach (Transform transform in transforms)
{
//通過名字找到實際的骨骼
if (transform.name != bone) continue;
bones.Add(transform);
break;
}
}
Object.Destroy(smr.gameObject);
}
// Obtain and configure the SkinnedMeshRenderer attached to
// the character base.
// 至此,combineInstances、bones和materials三個數組中的數據對應關係是正確的。
// 合併時,第二個參數是fals,表示保持子網格不變,只不過將它們統一到一個Mesh裏
// 來管理,這樣只需採用一個SkinedMeshRender繪製,效率較高。
SkinnedMeshRenderer r = root.GetComponent<SkinnedMeshRenderer>();
r.sharedMesh = new Mesh();
r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
r.bones = bones.ToArray();
r.materials = materials.ToArray();
return root;
}