淺談角色換裝功能--Unity簡單例子實現


  在前置篇中,基本上梳理了一下換裝功能背后涉及到的美術工作流。但程序員嘛,功能終歸是要落到代碼上的。本文中會結合Unity提供的API及之前提到的內容來實現一個簡單的換裝功能。效果如下:
  

           (圖1:最終效果展示)


 資源導出規則


 所有的換裝實現都是和導出規則相對應的。先說一下我這個小例子的導出規則。

1.角色的主干部分,包括頭,胳膊,大腿。整體導出作為一個基礎蒙皮。

2.其他部分的蒙皮,手套,下裝,衣服,頭發。每一種樣式都一個個單獨導出。

3.從MAX中導出FBX資源時,要注意導出蒙皮時候,骨骼也要選上,否則導出的就是普通mesh,而不是蒙皮了。

                

          (圖2:角色基礎部分的導出內容,左側為主干部分,右側為一個頭發部件.都要帶上骨骼)            


基本流程


 如圖3,將max導出的所有fbx放入Unity后,每個部件都是單獨的,我們要做的就是把這些分散的部件攢在一起,讓他們正確的顯示並響應動畫。

        

(圖3:Unity中顯示的所有導出身體部件,Girl為主干模型)

  寫具體代碼之前,我們先說一下幾個關鍵的Unity組件,Animator,SkinnedMeshRenderer.Animator會讀取動畫信息,我們在前置篇提到,max只制作動畫的關鍵幀,而游戲渲染是一幀一幀的,關鍵幀之間的動畫如何過渡,就是引擎自己負責的,也就是Animator做的事,Animator計算好當前幀的骨骼姿態后。會根據結果去改變Animator組件所在節點下的骨骼結構節點,只有我們在max里將骨骼正確導出,才會出現這些節點。SkinnedMeshRenderer則負責蒙皮計算,在每一幀中根據Animator計算出來后的骨骼位置,找到自己關聯了哪些骨骼及權重,然后進行變換和插值,計算出mesh頂點的正確位置。再將這些頂點信息傳入對應的材質球中進行渲染。


 實現代碼


下面是一個簡單的實現代碼,我會對一些關鍵代碼進行說明。這個腳本是掛在角色主干部分的Prefab上。

  1 public class SkinTest : MonoBehaviour 
  2 {
  3 
  4     public GameObject[] Hairs;
  5     public GameObject[] Clothes;
  6     public GameObject[] Gloves;
  7     public GameObject[] Unders;
  8     
  9     private int hairIndex = 0;
 10     private int clothesIndex = 0;
 11     private int glovesIndex = 0;
 12     private int underIndex = 0;
 13 
 14     private List<Transform> bones;
 15     private GameObject rootBone;
 16     void Start () 
 17     {
 18         rootBone = gameObject.transform.FindChild("Bip001").gameObject;
 19         bones = new List<Transform>();
 20 
 21         BuildPlayer();
 22     }
 23     
 24     public void BuildPlayer()
 25     {
 26         bones.Clear();
 27         List<CombineInstance> combineInstances = new List<CombineInstance>();
 28         List<Material> materials = new List<Material>();
 29         List<SkinnedMeshRenderer> smrList = new List<SkinnedMeshRenderer>();
 30         Transform[] transforms = rootBone.GetComponentsInChildren<Transform>(true);
 31 
 32         if(Hairs!=null && Hairs.Length > hairIndex && Hairs[hairIndex]!=null)
 33         {
 34             SkinnedMeshRenderer smr = Hairs[hairIndex].GetComponentInChildren<SkinnedMeshRenderer>();
 35             if (smr != null)
 36             {
 37                 smrList.Add(smr);
 38             }
 39         }
 40 
 41         if (Clothes != null && Clothes.Length > clothesIndex && Clothes[clothesIndex] != null)
 42         {
 43             SkinnedMeshRenderer smr = Clothes[clothesIndex].GetComponentInChildren<SkinnedMeshRenderer>();
 44             if (smr != null)
 45             {
 46                 smrList.Add(smr);
 47             }
 48         }
 49 
 50         if (Gloves != null && Gloves.Length > glovesIndex && Gloves[glovesIndex] != null)
 51         {
 52             SkinnedMeshRenderer smr = Gloves[glovesIndex].GetComponentInChildren<SkinnedMeshRenderer>();
 53             if (smr != null)
 54             {
 55                 smrList.Add(smr);
 56             }
 57         }
 58 
 59         if (Unders != null && Unders.Length > underIndex && Unders[underIndex] != null)
 60         {
 61             SkinnedMeshRenderer smr = Unders[underIndex].GetComponentInChildren<SkinnedMeshRenderer>();
 62             if (smr != null)
 63             {
 64                 smrList.Add(smr);
 65             }
 66         }
 67 
 68         for(int i =0;i<smrList.Count;i++)
 69         {
 70             SkinnedMeshRenderer smr = smrList[i];
 71             if(smr)
 72             {
 73                 for(int sub =0;sub<smr.sharedMesh.subMeshCount;sub++)
 74                 {
 75                     for (int j = 0; j < smr.bones.Length; j++)
 76                     {
 77                         for (int index = 0; index < transforms.Length; index++)
 78                         {
 79                             if (smr.bones[j].name.Equals(transforms[index].name))
 80                             {
 81                                 bones.Add(transforms[index]);
 82                                 break;
 83                             }
 84                         }
 85                     }
 86 
 87                     CombineInstance ci = new CombineInstance();
 88 
 89                     ci.mesh = smr.sharedMesh;
 90                     ci.subMeshIndex = sub;
 91                     combineInstances.Add(ci);
 92                }
 93                materials.AddRange(smr.sharedMaterials);
 94 
 95             }
 96         }
 97 
 98         SkinnedMeshRenderer oldSkin = GetComponent<SkinnedMeshRenderer>();
 99         if(oldSkin!=null)
100         {
101             GameObject.DestroyImmediate(oldSkin);
102         }
103 
104         SkinnedMeshRenderer newSmr = gameObject.AddComponent<SkinnedMeshRenderer>();
105         newSmr.sharedMesh = new Mesh();
106         newSmr.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
107         newSmr.bones = bones.ToArray();
108         newSmr.rootBone = rootBone.transform;
109         newSmr.materials = materials.ToArray();
110     }
111 }

4~15行,一些基本變量,存放用於換裝的Prefab的引用,以及索引下標,bones用來存儲Skin合並后的骨骼引用,rootBone用來存儲根骨骼。

18行,找到根骨的節點,此處的Bip001是3Dmax中Bip結構的默認根節點。主干部分的蒙皮導出時帶有骨骼,所以可以在Prefab的子節點上找到。

27~30行,建立一些List用來存儲SkinMeshRenderer合並過程中所用到的一些中間內容。這里再提一下SkinMeshRenderer,我們會發現一個SkinMeshRenderer一般都只包含一個Material,但它是可以包含多個的。當我們的SkinMeshRenderer里對應的Mesh是包含多個subMesh的時候,那么他們需要多個材質球來對應每個SubMesh。我們導出的各個部件里都有自己的SkinMeshRenderer,我們要做的是把他們合為一個整體,這樣做會對計算性能上有提升,邏輯處理上也更統一。后面我們再細說。

32~66行,這部分是根據各個部位的索引號去配置好的Prefab數組中查找到對應配件,只獲取SkinMeshRenderer組件就夠了,因為他里面包含了我們所需的蒙皮的所有信息。把他們放到List中后面統一處理。

68~96行,循環遍歷處理我們前面獲取的各個部件的SkinMeshRenderer.這里要說一下關於SkinMeshRenderer的Bones變量,它返回的是這個Skin綁定了哪些骨骼,Unity是以Transform引用數組的形式返回的,引用的是原來每個部件Prefab下自己的Bip下的骨骼節點,當我們把這些SkinMeshRenderer整合成一個的時候,就需要把引用重新指定成主體模型上的相應骨骼節點,這正是73~85行做的事。注意我這里根據部位里是否有多個subMesh來重復添加多次骨骼,這是必須的,而且順序也是一定要保證的。在FBX上的Optimize Mesh選項可以解決這個問題,不過會引入其它問題,這里不展開了。每個部件Skin對應的材質球也都按順序放到List中。89~91行的CombineInstance是Unity用來進行Mesh合並的一個數據結構,我們最終是需要把每個部件Skin對應的Mesh合並到一起,這里注意,合並到一起,並不一定是真的變成了一個Mesh,因為部件和部件之間的材質不一定完全一致,這時候的Mesh合並實際上只是一種邏輯上的合並,真正渲染時各個部件的Mesh頂點數據還是各走一個DrawCall。即使是這樣,邏輯上的這種整合對於Unity的性能也是有好處的,這涉及到渲染層面節省頂點Buffer的問題,也涉及到提高Unity引擎一些自身邏輯效率的問題,這里不展開。subMeshIndex這個變量,對於普通的部件Skin里只包含一個subMesh,所以一般指定0,但有時候會包含多個,如果在max里部件本身就由多個材質構成,那么每個材質負責的Mesh部分到了Unity里就變成一個SubMesh了。93行我們把材質球也按順序(順序很重要),放到了List里,你也許會問為什么不合並呢?理論上如果所有部件用的都是統一材質,或者材質基本相似的話是可以通過合並貼圖,重新賦值UV來讓所有部件正真的合並在一起,只用一個Mesh。

104~106行,我們最終要把所有分散的SkinMeshRenderer合並到一起,添加一個SkinnedMeshRenderer組件,但是這個組件的所有變量都是默認空的。所以105行我們給這個Renderer新建一個空的Mesh。106行通過CombineMesh來利用我們前面創建的CombineInstance數據把Mesh合並。這里說明一下后兩個參數,第一個參數如果為true,則表示會把所有Mesh真的合並到一起,也就是合並之后subMeshCount為1。這一搬是與我前面提到的材質合並配合使用的。第三個參數為true的話我們需要給每個CombineInstance提供一個變換矩陣,在它們被合並之前,它們會先利用這個矩陣進行一次空間變換。

107行,將前面骨骼節點集合傳遞給前面新建的SkinMeshRenderer,必須保證順序。

108行,rootBone習慣性的賦值為骨骼結構的根節點,這里設為空也沒問題。

109行,同骨骼節點一樣集合一樣,材質球集合傳遞給SkinMeshRenderer,保證順序與部件合並的順序相同。


總結


  換裝功能的實現代碼並沒有統一規范,這跟部件的設計規則有很大關系,所以本文只提供一種最簡單基本的思路。還可以在這個基礎上繼續展開,深入優化。有些地方我沒有深入去剖析,一筆帶過。一方面是有些內容我也並不深入了解,另一方面是怕大家過於糾結細節,迷失方向。對於一些更深入的內容,我計划有時間再寫一篇來分享。上面的實現在實際項目中也有很多問題,比如每個部件的Fbx導出都需要帶全套骨骼。這造成一些數據上的冗余,如果要是在資源打包上依然沒有辦法去掉冗余的話,就會造成運行時內存的浪費,希望大家來一起討論。   

  尊重他人智慧成果,若要轉載,請注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/EquipChange_SimpleArchive.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM