前言

最近学习了Unity中Avatar换装功能实现,参考了网上的几篇文章,总结了一个Demo。Unity的换装实现参考网上的教程,总体有两种实现,一种是官方Demo给出的合并Mesh实现, 还有一种采用的以前端游的做法,共享骨骼的方式。两种方式各有特点。个人Demo实现了以上两种做法。

 

准备资源

手头没有换装资源,所以用了官方Demo的资源作为示例,不过官方的Demo把切分的部件打包成assetbundle, 不易查看,所以通过工具把人物的各个部件生成prefab用来展示

unity中Avatar换装实现(三)之美_Unity

如上图所以, 对于女性或者男性角色,拆分成eyes, face, hair, pants, shoes, top6组部件和一个skeleton文件。对于同一部件,由于material不同,mesh不同,可能会生成很多类型的prefab。这里有个问题,如下图所示。

unity中Avatar换装实现(三)之美_Unity教程_02

所有的prefab中的Mesh都指向了同一个fbx文件中子mesh. 对于在实际项目,美术人员在导出fbx文件的时候,需要单独导出各个子fbx, 这样比较清晰,避免可能出现的资源重复打包问题。

如下图的Demo所示

unity中Avatar换装实现(三)之美_Unity教程_03

端游做法,共享骨骼方式实现换装

共享骨骼的实现方式在场景中如下所示, 骨骼obj下挂载了各个子部件obj。

unity中Avatar换装实现(三)之美_Unity_04

对于各个part的挂载,除了指定父节点是skeleton节点外,还需要添加如下代码

 private void ChangeEquipUnCombine(ref GameObject go, GameObject resgo)
    {
        if (go != null)
        {
            GameObject.DestroyImmediate(go);
        }

        go = GameObject.Instantiate(resgo);
        go.Reset(mSkeleton);
        go.name = resgo.name;

        SkinnedMeshRenderer render = go.GetComponentInChildren<SkinnedMeshRenderer>();
        ShareSkeletonInstanceWith(render, mSkeleton);
    }

    // 共享骨骼
    public void ShareSkeletonInstanceWith(SkinnedMeshRenderer selfSkin, GameObject target)
    {
        Transform[] newBones = new Transform[selfSkin.bones.Length];
        for (int i = 0; i < selfSkin.bones.GetLength(0); ++i)
        {
            GameObject bone = selfSkin.bones[i].gameObject;
            
            // 目标的SkinnedMeshRenderer.bones保存的只是目标mesh相关的骨骼,要获得目标全部骨骼,可以通过查找的方式.
            newBones[i] = FindChildRecursion(target.transform, bone.name);
        }

        selfSkin.bones = newBones;
    }

    // 递归查找
    public Transform FindChildRecursion(Transform t, string name)
    {
        foreach (Transform child in t)
        {
            if (child.name == name)
            {
                return child;
            }
            else
            {
                Transform ret = FindChildRecursion(child, name);
                if (ret != null)
                    return ret;
            }
        }

        return null;
    }

代码的大致意思就是对于各个部件,找到SkinnedMeshRenderer成份,然后调用ShareSkeletonInstanceWith函数,递归查找skeleton下的bone节点,赋值给SkinnedMeshRenderer的bones变量。因为动画影响的skeleton下的骨骼变化。对于各个部件,需要把SkinnedMeshRenderer中的bones变量指定到skeleton的骨骼。这样才能有动画的效果。

优缺点:
这种共享骨骼的好处是对于更换单个部件,只需要删除单个部件,然后再创建新的部件。理论上性能开销较小,但是这种做法不能像合并mesh的做法那样可以合并材质,减少DrawCall

官方Demo的合并Mesh实现

对于官方Demo的实现,实现效果图如下

unity中Avatar换装实现(三)之美_Unity教程_05

大致代码如下。

  private void ChangeEquipCombine(GameObject resgo, ref List<CombineInstance> combineInstances,
                        ref List<Material> materials, ref List<Transform> bones)
    {
        Transform[] skettrans = mSkeleton.GetComponentsInChildren<Transform>();

        GameObject go = GameObject.Instantiate(resgo);
        SkinnedMeshRenderer smr = go.GetComponentInChildren<SkinnedMeshRenderer>();

        materials.AddRange(smr.materials);
        for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)
        {
            CombineInstance ci = new CombineInstance();
            ci.mesh = smr.sharedMesh;
            ci.subMeshIndex = sub;
            combineInstances.Add(ci);
        }

        // As the SkinnedMeshRenders are stored in assetbundles that do not
        // contain their bones (those are stored in the characterbase assetbundles)
        // we need to collect references to the bones we are using
        foreach (Transform bone in smr.bones)
        {
            string bonename = bone.name;
            foreach (Transform transform in skettrans)
            {
                if (transform.name != bonename)
                    continue;

                bones.Add(transform);
                break;
            }
        }

        GameObject.DestroyImmediate(go);
    }

对于各个组件

1, 通过CombineInstance收集SkinnedMeshRenderer, 添加到CombineInstance的list数组中。

2, 对于SkinnedMeshRenderer使用的骨骼,遍历查找添加到bones数组中。

3, 同时使用的材质添加到materials数组中

private void GenerateCombine(AvatarRes avatarres)
    {
        if (mSkeleton != null)
        {
            bool iscontain = mSkeleton.name.Equals(avatarres.mSkeleton.name);
            if (!iscontain)
            {
                GameObject.DestroyImmediate(mSkeleton);
            }
        }

        if (mSkeleton == null)
        {
            mSkeleton = GameObject.Instantiate(avatarres.mSkeleton);
            mSkeleton.Reset(gameObject);
            mSkeleton.name = avatarres.mSkeleton.name;
        }

        mAnim = mSkeleton.GetComponent<Animation>();

        List<CombineInstance> combineInstances = new List<CombineInstance>();
        List<Material> materials = new List<Material>();
        List<Transform> bones = new List<Transform>();
        ChangeEquipCombine((int)EPart.EP_Eyes, avatarres, ref combineInstances, ref materials, ref bones);
        ChangeEquipCombine((int)EPart.EP_Face, avatarres, ref combineInstances, ref materials, ref bones);
        ChangeEquipCombine((int)EPart.EP_Hair, avatarres, ref combineInstances, ref materials, ref bones);
        ChangeEquipCombine((int)EPart.EP_Pants, avatarres, ref combineInstances, ref materials, ref bones);
        ChangeEquipCombine((int)EPart.EP_Shoes, avatarres, ref combineInstances, ref materials, ref bones);
        ChangeEquipCombine((int)EPart.EP_Top, avatarres, ref combineInstances, ref materials, ref bones);

        // Obtain and configure the SkinnedMeshRenderer attached to
        // the character base.
        SkinnedMeshRenderer r = mSkeleton.GetComponent<SkinnedMeshRenderer>();
        if (r != null)
        {
            GameObject.DestroyImmediate(r);
        }

        r = mSkeleton.AddComponent<SkinnedMeshRenderer>();
        r.sharedMesh = new Mesh();
        r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
        r.bones = bones.ToArray();
        r.materials = materials.ToArray();

        if (mAnim != null)
        {
            if (!mAnim.IsPlaying("walk"))
            {
                mAnim.wrapMode = WrapMode.Loop;
                mAnim.Play("walk");
            }
        }
    }

通过收集的CombineInstance数组combineInstances,骨骼数组bones,以及材质数组materials, 组成一个新的Mesh, 添加到新创建的SkinnedMeshRenderer中。从而可以产生动画。

优缺点:
这种合并Mesh的方式缺点很明显,如果需要更新一个部件,需要重新创建新的Mesh和SkinnedMeshRenderer, 不太灵活。
不过这种合并Mesh的方式可以在合并Mesh的时候合并材质,减少DrawCall, 提高渲染效率。但是大多数情况下不一定能够合并材质,如果单个部件的材质使用的贴图数目不同,就无法合并材质了。

Demo链接地址

https://github.com/xieliujian/UnityDemo_Avatar

参考项目

端游做法,共享骨骼方式实现换装的参考文章

https://blog.csdn.net/qq18052887/article/details/52648909

合并Mesh实现换装的参考文章

https://blog.uwa4d.com/archives/avartar.html