Unity 换装合并网格简单实现

Unity的蒙皮网格组件渲染组件(SkinnedMeshRenderer)是Unity自带用来创建蒙皮模型的组件,创建蒙皮模型后我们可以实现比如蒙皮网格动画,或者通过更换蒙皮信息来实现局部变现的改变大刀或装效果。通常为了减少渲染过程中CPU唤起Draw Call命令的次数加大对CPU的负载,因为每次Draw Call调用之前CPU都需要为GPU准备好渲染所需要的信息(所用到的材质,纹理,着色器等),我们需要尽可能的减少SkinnedMeshRenderer组件数量,以减少CPU在调用Draw Call之前的一系列准备工作,提高游戏运行效率。

网上找到的信息,貌似之前Unity Asset Store早期版本有提供一个官方的换装示例资源,叫CharacterCustomezition,不过现在查了一下当前的Asset Store已经找不到这个资源。所以自己试一下就好了。

简要步骤

  1. 加载,关联各个已有的SkinnedMeshRenderer,保持可以获取到对其的指向,这些SkinnedMeshRenderer并不显示
  2. 对应部分共用一个SkinnedMeshRenderer展示,确保坐标和旋转角度的正确
  3. 从关联的SkinnedMeshRenderer中获取需要的信息复制到展示用的SkinnedMeshRenderer
  4. 合并网格,将所有网格的信息合并到使用一个SkinnedMeshRenderer来渲染,降低CPU负担

使用的资源是在别的资料教程中找到的一个,附下链接以表感谢
https://blog.csdn.net/u012801153/article/details/78374822
https://gitee.com/xianglinlove/Avatar

接下来就是贴代码了,需要注意的地方在代码中都添加了注释

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// 
/// Unity 换装以及合并网格
/// 
[RequireComponent(typeof(SkinnedMeshRenderer))]
public class DressingRoom : MonoBehaviour {

    /// 
    /// 载入的换装资源,其中包含了所有的可以更换的部位的SkinnedMeshRenderer
    /// 
    GameObject tempAvatar;

    /// 
    /// 加载的骨骼预设,其中包括了所有的骨骼节点
    /// 
    GameObject targetSkeleton;

    /// 
    /// 所有骨骼节点
    /// 
    Transform[] allBones;

    /// 
    /// 将换装资源信息关联到Dictionary
    /// 
    Dictionary> skinData = new Dictionary>();

    /// 
    /// 当前装扮信息关联到Dictionary,用来获取所有展示部位的SkinnedMeshRenderer
    /// 
    Dictionary skinPerform = new Dictionary();

    void Start()
    {
        LoadResourse();
        InitData(tempAvatar);
        InitSkin();
    }

    /// 
    /// 加载所需要的资源
    /// 
    void LoadResourse()
    {
        tempAvatar = Instantiate(Resources.Load("FemaleSource")) as GameObject;
        tempAvatar.SetActive(false);
        targetSkeleton = Instantiate(Resources.Load("targetmodel")) as GameObject;
        allBones = targetSkeleton.GetComponentsInChildren();
    }

    /// 
    /// 1.将加载的资源的信息记录到skinData
    /// 2.为对应的换装部位创建SkinnedMeshRenderer
    /// 
    /// 
    void InitData(GameObject avata)
    {
        if(avata == null)
        {
            return;
        }
        SkinnedMeshRenderer[] skins = tempAvatar.GetComponentsInChildren();
        for(int i = 0; i < skins.Length; i++)
        {
            // 此处命名示例 : hair-03
            string[] str =  skins[i].name.Split('-');
            if(!skinData.ContainsKey(str[0]))
            {
                skinData.Add(str[0], new Dictionary());
                GameObject part = new GameObject();
                part.name = str[0];
                // 将SkinnedMeshRenderer父对象设置为骨骼根节点
                part.transform.parent = targetSkeleton.transform;
                // 修改坐标和旋转角度
                part.transform.localPosition = skins[i].transform.localPosition;
                part.transform.localRotation = skins[i].transform.localRotation;
                // 添加关联到skinPerform
                skinPerform.Add(str[0], part.AddComponent());
            }
            // 添加关联到skinData
            skinData[str[0]].Add(str[1], skins[i]);
        }
    }

    /// 
    /// 初始化默认的一套装扮
    /// 
    void InitSkin()
    {
        ChangeMesh("coat", "003");
        ChangeMesh("head", "003");
        ChangeMesh("hair", "003");
        ChangeMesh("pant", "003");
        ChangeMesh("foot", "003");
        ChangeMesh("hand", "003");
        CombineMesh();
    }

    /// 
    /// 修改局部的SkinnedMeshRenderer来达到换装目的
    /// 
    /// 部位
    /// 型号
    void ChangeMesh(string type, string model)
    {
        SkinnedMeshRenderer sm = skinData[type][model];
        // 新替换的部件的所需要的骨骼节点
        List bones = new List();
        for(int i = 0; i < sm.bones.Length; i++)
        {
            for(int j = 0; j < allBones.Length; j++)
            {
                if(sm.bones[i].name == allBones[j].name)
                {
                    bones.Add(allBones[j]);
                }
            }
        }
        // 更改网格
        skinPerform[type].sharedMesh = sm.sharedMesh;
        // 更改骨骼
        skinPerform[type].bones = bones.ToArray();
        // 更改材质
        skinPerform[type].materials = sm.materials;
    }

    /// 
    /// 合并展示用的所用的SkinnedMeshRenderer,最终使用一个进行渲染
    /// 
    void CombineMesh()
    {
        List combineInstances = new List();
        List materials = new List();
        List bones = new List();
        // 遍历所有展示的SkinnedMeshRenderer
        foreach(SkinnedMeshRenderer smr in skinPerform.Values)
        {
            // 按顺序添加材质
            materials.AddRange(smr.materials);
            // 处理SubMesh
            // 1. SubMesh会对应同样的shareMesh
            // 2. SubMesh添加骨骼节点时会添加同样的骨骼节点到数组中
            // 3. 关键的是subMeshIndex用来标注当前是第几个SubMesh,对应需要使用的Material
            // 4. 一个Mesh有多少Sub Mesh可以在Mesh资源的Inspector窗口下发显示模型三角形和顶点数的地方显示
            for(int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++ )
            {
                CombineInstance ci = new CombineInstance();
                ci.mesh = smr.sharedMesh;
                // subMeshIndex属性用于指向待合并的SubMesh
                // 会根据这个属性去寻找需要使用的材质的下标
                // 要保证材质,骨骼的添加顺序是跟这个值的顺序是一致的
                ci.subMeshIndex = sub;
                // 
                combineInstances.Add(ci);
                // 
                for(int j = 0; j < smr.bones.Length; j++)
                {
                    for(int k = 0; k < allBones.Length; k++)
                    {
                        if(smr.bones[j].name == allBones[k].name)
                        {
                            bones.Add(allBones[k]);
                            break;
                        }
                    }
                }
            }
            // 旧的SkinnedMeshRenderer可以隐藏或者删除
            // smr.gameObject.SetActive(false);
            DestroyImmediate(smr.gameObject);
        }
        SkinnedMeshRenderer main = gameObject.GetComponent();
        main.sharedMesh = new Mesh();
        // CombineMeshes第三个参数表示使用应用CombineInstance的Transform变换
        // 这里并没有设置所以填false
        main.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
        main.bones = bones.ToArray();
        main.materials = materials.ToArray();
    }
}

总结一下需要注意的地方:

  1. 更换需要展示的蒙皮的时候,也需要对骨骼的修改,遍历查找的时候我们不能打乱其中对应节点的顺序,所以查找的时候,最外层使用目标蒙皮的骨骼数据做循环,以保证骨骼节点的顺序。

  2. 合并网格的时候,也要注意顺序这个问题,要保证多材质的添加顺序,子网格的数据顺序,子网格骨骼节点的顺序,以确保合并之后网格,材质,骨骼节点这些可以正确的对应上。

你可能感兴趣的:(Unity 换装合并网格简单实现)