LV. 18
GP 75

【心得】換裝功能簡易實作(高清GIF賣女兒流量注意

樓主 空空雨落 kennussu
看網路上已經有很多資源,這邊只能主打簡潔易讀了,給其他同為萌新的朋友參考。

這篇技術上概略來說,就是把「fbx的mesh拉成prefab,穿上時生成並設定骨骼參數(綁上骨頭)」,就成了。步驟如下:

Step.0 素材準備

(0) 這邊使用自製Blender輸出fbx模型檔,內容長這樣:
其中模型(mesh)都是綁在同一個Armature骨架上。

(1) 將mesh拖進project window製成prefab,這邊只要綁的骨骼相同,拉其他fbx檔的模型也可以。然後Scene裡的物件都可以刪掉了。

Step.1 製作程式

(0) Editor端輸入,將Body作為骨骼資訊參考來源放入腳本。為展示方便,要穿的物件的資訊(這邊用prefab名稱)也在editor輸入。
[SerializeField] private SkinnedMeshRenderer bodyMesh;

[SerializeField] private string[] meshesToEquip;


(1) 正戲開始,訂一個Equip()來穿上裝備:
public void Equip()
{
    for (int i = 0; i < meshesToEquip.Length; i++)
    {
        //為展示方便,以Resources.Load取得mesh
        GameObject newGameObject = Resources.Load(meshesToEquip[i]) as GameObject;

        //將mesh生成至場景中
        SkinnedMeshRenderer newMesh = Instantiate(newGameObject).GetComponent<SkinnedMeshRenderer>();

        //將骨骼資料從參考來源複製到新mesh
        newMesh.bones = bodyMesh.bones;
        newMesh.rootBone = bodyMesh.rootBone;

        //為管理方便,設定parent
        newMesh.transform.parent = bodyMesh.transform;
    }
}

就這麼簡單不到10行搞定,脫掉TakeOff()只要Destroy bodyMesh.transform底下的物件就行了,不另贅述。用2個Button呼叫Equip()跟TakeOff(),效果如下:

Step.2 進一步優化
以上已經算是完成了,進一步可以將SkinnedMesh同材質合併,以減少draw call,實作上需要牽扯到更多骨骼資料的調整,程式上比較複雜:

設計上把Combine拉出去做,Equip僅管理要穿的列表
private List<SkinnedMeshRenderer> meshes;
public void Equip()
{
    meshes = new List<SkinnedMeshRenderer>();

    for (int i = 0; i < meshesToEquip.Length; i++)
    {
        GameObject newGameObject = Resources.Load(meshesToEquip[i]) as GameObject;

        //將mesh加進待生成列表
        meshes.Add(newGameObject.GetComponent<SkinnedMeshRenderer>());
    }

    CombineMesh();
}

取得要合併的meshes列表,原則要有相同material(含參數設定),而且合併會吃掉Blend Shape,這部分的判定就看個人,這邊不多提。

CombineMesh如下
public void CombineMesh()
    {
        //待合併的instancesxu列表,每個mesh會做成一個CombineInstance存進來
        List<CombineInstance> combineInstances = new List<CombineInstance>();

        //原本直接擷取bodyMesh.bones,這邊要各別收集了
        List<Transform> bones = new List<Transform>();

        //合併mesh會影響權重,相關資料也要收集
        List<BoneWeight> boneWeights = new List<BoneWeight>();
        List<Matrix4x4> bindPoses = new List<Matrix4x4>();

        //存個material合併後使用
        Material material = meshes[0].sharedMaterial;

        for (int j = 0; j < meshes.Count; j++)
        {
            SkinnedMeshRenderer smr = meshes[j];

            for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)
            
{
                //將renderer資料(mesh、transform)存進一個new CombineInstance,再加進待合列表
                CombineInstance ci = new CombineInstance();
                ci.mesh = smr.sharedMesh;
                ci.subMeshIndex = sub;
                ci.transform = smr.transform.localToWorldMatrix;
                combineInstances.Add(ci);

                //蒐集renderer權重資料
                BoneWeight[] meshBoneweight = smr.sharedMesh.boneWeights;
                for (int w = 0; w < meshBoneweight.Length; w++)
                {
                    boneWeights.Add(meshBoneweight[w]);
                }
            }

            //每個renderer,都會寫入一次整個bodyMesh.bones資料
            for (int b = 0; b < bodyMesh.bones.Length; b++)
            {                
                bones.Add(bodyMesh.bones[b]);

                //蒐集每根骨頭的bindposes資料
                bindPoses.Add(smr.sharedMesh.bindposes[b] * smr.transform.worldToLocalMatrix);
            }
        }

        //生成新物件,名字隨便用第一個代表
        GameObject newPart = new GameObject(meshes[0].name);
        SkinnedMeshRenderer combinedMesh = newPart.AddComponent<SkinnedMeshRenderer>();
        combinedMesh.sharedMesh = new Mesh();

        //CombineMeshes後面要set true才會真正合併mesh,剩下很白話就不另外註解了
        combinedMesh.sharedMesh.CombineMeshes(combineInstances.ToArray(), true);
        combinedMesh.material = material;

        combinedMesh.bones = bones.ToArray();
        combinedMesh.sharedMesh.boneWeights = boneWeights.ToArray();
        combinedMesh.sharedMesh.bindposes = bindPoses.ToArray();
        combinedMesh.sharedMesh.RecalculateBounds();

        combinedMesh.rootBone = bodyMesh.rootBone;

        combinedMesh.gameObject.layer = meshes[0].gameObject.layer;

        newPart.transform.parent = bodyMesh.transform;
    }

從下圖可以看出,基本28生成3個物件變成40(每個4次draw call),變成28+1x4=32,物件越多節省越多(每人5件,差4*4=16個,8人PVP畫面就差16*8=128個draw call)。
p.s. 這邊基本4個包含alpha 2個,shadow、outline各1個。

小小心得供各位朋友參考,也請大神不吝指教,多謝賞閱。
板務人員:

238 筆精華,10/12 更新
一個月內新增 0
歡迎加入共同維護。


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】