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;


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

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

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

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

    /// <summary>
    /// 将换装资源信息关联到Dictionary
    /// </summary>
    Dictionary<string, Dictionary<string, SkinnedMeshRenderer>> skinData = new Dictionary<string, Dictionary<string, SkinnedMeshRenderer>>();

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

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

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

    /// <summary>
    /// 1.将加载的资源的信息记录到skinData
    /// 2.为对应的换装部位创建SkinnedMeshRenderer
    /// </summary>
    /// <param name="avata"></param>
    void InitData(GameObject avata)
    {
        if(avata == null)
        {
            return;
        }
        SkinnedMeshRenderer[] skins = tempAvatar.GetComponentsInChildren<SkinnedMeshRenderer>();
        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<string, SkinnedMeshRenderer>());
                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<SkinnedMeshRenderer>());
            }
            // 添加关联到skinData
            skinData[str[0]].Add(str[1], skins[i]);
        }
    }

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

    /// <summary>
    /// 修改局部的SkinnedMeshRenderer来达到换装目的
    /// </summary>
    /// <param name="type">部位</param>
    /// <param name="model">型号</param>
    void ChangeMesh(string type, string model)
    {
        SkinnedMeshRenderer sm = skinData[type][model];
        // 新替换的部件的所需要的骨骼节点
        List<Transform> bones = new List<Transform>();
        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;
    }

    /// <summary>
    /// 合并展示用的所用的SkinnedMeshRenderer,最终使用一个进行渲染
    /// </summary>
    void CombineMesh()
    {
        List<CombineInstance> combineInstances = new List<CombineInstance>();
        List<Material> materials = new List<Material>();
        List<Transform> bones = new List<Transform>();
        // 遍历所有展示的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<SkinnedMeshRenderer>();
        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. 合并网格的时候,也要注意顺序这个问题,要保证多材质的添加顺序,子网格的数据顺序,子网格骨骼节点的顺序,以确保合并之后网格,材质,骨骼节点这些可以正确的对应上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容