轉載請注明出處:http://www.cnblogs.com/shamoyuu/p/6505561.html
一、換裝原理
游戲角色換裝分為以下幾步:
1.替換蒙皮網格
2.刷新骨骼
3.替換材質
上面這種是比較簡單的換裝,可以實現,但是一般我們為了降低游戲的Draw Call會合並模型的網格,這就需要我們重新計算UV,還要合並貼圖和材質。這種復雜的實現分為以下幾步:
1.替換蒙皮網格(或者直接替換模型換裝部位的GameObject,因為合並的時候會合並所有的蒙皮網格,而不會關心它是否屬於原來角色身體的一部分,而且如果需要替換的部位有多個配件擁有獨立的網格和貼圖,那這種方式都可以正常執行。我下面的代碼就是直接替換了換裝部位的GameObject)
2.合並所有蒙皮網格
3.刷新骨骼
4.附加材質(我下面是獲取第一個材質作為默認材質)
5.合並貼圖(貼圖的寬高最好是2的N次方的值)
6.重新計算UV
二、換裝實現
using System; using System.Collections; using UnityEngine; using System.Collections.Generic; public class CharacterCombine : MonoBehaviour { // 目標物體(必須是骨骼的父物體,不然蒙皮失效) public GameObject target; // 最終材質(合並所有模型后使用的材質) public Material material; // 物體所有的部分 private GameObject[] targetParts = new GameObject[9]; private string[] defaultEquipPartPaths = new string[9]; void Start() { // 把FBX的模型按部件分別放入Resources下對應的文件夾里,可以留空,模型需要蒙皮,而且所有模型使用同一骨骼 // 最后的M是Fbx的模型,需要的Unity3D里設置好材質和貼圖,部件貼圖要勾選Read/Write Enabled defaultEquipPartPaths[0] = "Model/Player/GirlPlayer/Head/Head0000/M"; defaultEquipPartPaths[1] = "Model/Player/GirlPlayer/Face/Face0000/M"; defaultEquipPartPaths[2] = "Model/Player/GirlPlayer/Hair/Hair0000/M"; defaultEquipPartPaths[3] = ""; defaultEquipPartPaths[4] = "Model/Player/GirlPlayer/Body/Body0000/M"; defaultEquipPartPaths[5] = "Model/Player/GirlPlayer/Leg/Leg0000/M"; defaultEquipPartPaths[6] = "Model/Player/GirlPlayer/Hand/Hand0000/M"; defaultEquipPartPaths[7] = "Model/Player/GirlPlayer/Foot/Foot0000/M"; defaultEquipPartPaths[8] = "Model/Player/GirlPlayer/Wing/Wing0001/M"; Destroy(target.GetComponent<SkinnedMeshRenderer>()); for (int i = 0; i < defaultEquipPartPaths.Length; i++) { UnityEngine.Object o = Resources.Load(defaultEquipPartPaths[i]); if (o) { GameObject go = Instantiate(o) as GameObject; go.transform.parent = target.transform; go.transform.localPosition = new Vector3(0, -1000, 0); go.transform.localRotation = new Quaternion(); targetParts[i] = go; } } StartCoroutine(DoCombine()); } /// <summary> /// 使用延時,不然某些GameObject還沒有創建 /// </summary> /// <returns></returns> IEnumerator DoCombine() { yield return null; Combine(target.transform); } /// <summary> /// 合並蒙皮網格,刷新骨骼 /// 注意:合並后的網格會使用同一個Material /// </summary> /// <param name="root">角色根物體</param> private void Combine(Transform root) { float startTime = Time.realtimeSinceStartup; List<CombineInstance> combineInstances = new List<CombineInstance>(); List<Transform> boneList = new List<Transform>(); Transform[] transforms = root.GetComponentsInChildren<Transform>(); List<Texture2D> textures = new List<Texture2D>(); int width = 0; int height = 0; int uvCount = 0; List<Vector2[]> uvList = new List<Vector2[]>(); // 遍歷所有蒙皮網格渲染器,以計算出所有需要合並的網格、UV、骨骼的信息 foreach (SkinnedMeshRenderer smr in root.GetComponentsInChildren<SkinnedMeshRenderer>()) { for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++) { CombineInstance ci = new CombineInstance(); ci.mesh = smr.sharedMesh; ci.subMeshIndex = sub; combineInstances.Add(ci); } uvList.Add(smr.sharedMesh.uv); uvCount += smr.sharedMesh.uv.Length; if (smr.material.mainTexture != null) { textures.Add(smr.GetComponent<Renderer>().material.mainTexture as Texture2D); width += smr.GetComponent<Renderer>().material.mainTexture.width; height += smr.GetComponent<Renderer>().material.mainTexture.height; } foreach (Transform bone in smr.bones) { foreach (Transform item in transforms) { if (item.name != bone.name) continue; boneList.Add(item); break; } } } // 獲取並配置角色所有的SkinnedMeshRenderer SkinnedMeshRenderer tempRenderer = root.gameObject.GetComponent<SkinnedMeshRenderer>(); if (!tempRenderer) { tempRenderer = root.gameObject.AddComponent<SkinnedMeshRenderer>(); } tempRenderer.sharedMesh = new Mesh(); // 合並網格,刷新骨骼,附加材質 tempRenderer.sharedMesh.CombineMeshes(combineInstances.ToArray(), true, false); tempRenderer.bones = boneList.ToArray(); tempRenderer.material = material; Texture2D skinnedMeshAtlas = new Texture2D(get2Pow(width), get2Pow(height)); Rect[] packingResult = skinnedMeshAtlas.PackTextures(textures.ToArray(), 0); Vector2[] atlasUVs = new Vector2[uvCount]; // 因為將貼圖都整合到了一張圖片上,所以需要重新計算UV int j = 0; for (int i = 0; i < uvList.Count; i++) { foreach (Vector2 uv in uvList[i]) { atlasUVs[j].x = Mathf.Lerp(packingResult[i].xMin, packingResult[i].xMax, uv.x); atlasUVs[j].y = Mathf.Lerp(packingResult[i].yMin, packingResult[i].yMax, uv.y); j++; } } // 設置貼圖和UV tempRenderer.material.mainTexture = skinnedMeshAtlas; tempRenderer.sharedMesh.uv = atlasUVs; // 銷毀所有部件 foreach (GameObject goTemp in targetParts) { if (goTemp) { Destroy(goTemp); } } Debug.Log("合並耗時 : " + (Time.realtimeSinceStartup - startTime) * 1000 + " ms"); } /// <summary> /// 獲取最接近輸入值的2的N次方的數,最大不會超過1024,例如輸入320會得到512 /// </summary> private int get2Pow(int into) { int outo = 1; for (int i = 0; i < 10; i++) { outo *= 2; if (outo > into) { break; } } return outo; } }
三、效果展示
在執行上面的代碼前,角色的每個部分都是單獨的,並且是激活的狀態。
執行后,角色所有的部位都會刪除,因為它們的網格都合並到了Player_Girl這個角色根物體上。
模型就不放出來了~