Unity3D换装系统

Posted 静茹♂鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D换装系统相关的知识,希望对你有一定的参考价值。

转载请注明出处: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这个角色根物体上。

 

 

模型就不放出来了~

 

以上是关于Unity3D换装系统的主要内容,如果未能解决你的问题,请参考以下文章

Unity3d 换装 之 模型动画分离

角色换装

Unity记录一下skinnedmeshrender换装 系统的关键

unity实现换装系统

基于Spine动画的AVATAR换装系统优化

基于Spine动画的AVATAR换装系统优化