技巧| Unity中Avatar换装实现

技巧| Unity中Avatar换装实现

Avatar换装是MMO游戏不可缺少的一部分,一个人物模型通常可拆分为头、身体、手臂、腿、武器等部分,如何将这些部分组合到一起呢?本文将阐述如何将在Unity中实现人物模型的换装功能。

这是侑虎科技第65篇原创文章,感谢作者邹春毅(QQ:442319386)供稿。欢迎转发分享,未经作者授权请勿转载。同时如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)

作者Github:https://github.com/zouchunyi


资源准备

1.每一套装备模型必须使用同一套骨骼,并单独将骨骼数据保存成一个Prefab。红色部分为武器挂节点(也可以把武器做成一个SkinnedMesh,不采用挂接点的形式),骨骼数据在Unity中的展示形式就是Transform。
UWA Tech Doc

2.将模型拆分成多个部分,将每一个部分单独保存成Prefab,武器也单独保存为一个Prefab。
UWA Tech Doc
UWA Tech Doc

每一个Prefab都含有自身的SkinnedMeshRenderer。
UWA Tech Doc


实现过程

1.创建骨骼GameObject,所有装备的蒙皮数据会最终合成到这个Prefab中。

2.创建装备GameObject,用于搜集其中蒙皮数据以生成新的SkinnedMeshRenderer到骨骼Prefab中。

3.public void CombineObject(GameObject skeleton, SkinnedMeshRenderer[] meshes, bool combine = false)传入骨骼的GameObject和蒙皮数据。

4.搜集装备蒙皮数据中的有效信息。

// Collect information from meshes
        for (int i = 0; i < meshes.Length; i ++)
        {
            SkinnedMeshRenderer smr = meshes[i];
            materials.AddRange(smr.materials); // Collect materials
            // Collect meshes
            for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)
            {
                CombineInstance ci = new CombineInstance();
                ci.mesh = smr.sharedMesh;
                ci.subMeshIndex = sub;
                combineInstances.Add(ci);
            }
            // Collect bones
            for (int j = 0 ; j < smr.bones.Length; j ++)
            {
                int tBase = 0;
                for (tBase = 0; tBase < transforms.Count; tBase ++)
                {
                    if (smr.bones[j].name.Equals(transforms[tBase].name))
                    {
                        bones.Add(transforms[tBase]);
                        break;
                    }
                }
            }
        }

5.为骨骼GameObject生成新的SkinnedMeshRenderer。

// Create a new SkinnedMeshRenderer
SkinnedMeshRenderer oldSKinned = skeleton.GetComponent<SkinnedMeshRenderer> ();
    if (oldSKinned != null) {
    GameObject.DestroyImmediate (oldSKinned);
    }
SkinnedMeshRenderer r = skeleton.AddComponent<SkinnedMeshRenderer>();
r.sharedMesh = new Mesh();
r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);// Combine meshes
r.bones = bones.ToArray();// Use new bones

6.挂接武器。

Transform[] transforms = Instance.GetComponentsInChildren<Transform>();
foreach (Transform joint in transforms) {
if (joint.name == "weapon_hand_r") 
{// find the joint (need the support of art designer)
        WeaponInstance.transform.parent = joint.gameObject.transform;
        break;
    }   
    }

其中WeaponInstance为武器实例GameObject,Instance为骨骼实例GameObject。

合成后的效果
UWA Tech Doc


如何优化

UWA Tech Doc
合成之后的模型拥有4个独立材质,加上独立的对象武器,也就是会产生5个Draw Call;如果将在骨骼中的这4个材质合并成一个,那么就能将Draw Call减少到2个。

其中实现过程如下:
优化CombineObject方法,其中Combine为bool类型,用于标识是否合并材质。

// merge materials
if (combine)
{
    newMaterial = new Material (Shader.Find ("Mobile/Diffuse"));
    oldUV = new List<Vector2[]>();
    // merge the texture
    List<Texture2D> Textures = new List<Texture2D>();
    for (int i = 0; i < materials.Count; i++)
    {
        Textures.Add(materials[i].GetTexture(COMBINE_DIFFUSE_TEXTURE) as Texture2D);
    }

    newDiffuseTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
    Rect[] uvs = newDiffuseTex.PackTextures(Textures.ToArray(), 0);
    newMaterial.mainTexture = newDiffuseTex;

// reset uv
    Vector2[] uva, uvb;
    for (int j = 0; j < combineInstances.Count; j++)
    {
        uva = (Vector2[])(combineInstances[j].mesh.uv);
        uvb = new Vector2[uva.Length];
        for (int k = 0; k < uva.Length; k++)
        {
            uvb[k] = new Vector2((uva[k].x * uvs[j].width) + uvs[j].x, (uva[k].y * uvs[j].height) + uvs[j].y);
        }
        oldUV.Add(combineInstances[j].mesh.uv);
        combineInstances[j].mesh.uv = uvb;
    }
    }

生成新的SkinnedMeshRenderer时略有区别:
UWA Tech Doc

最终效果如下:
UWA Tech Doc
可以看出,新的SkinnedMeshRenderer只有一个材质,Draw Call自然也就降低了。


示例工程

本人已将此示例工程分享到了GitHub中:https://github.com/zouchunyi/UnityAvater
感兴趣的朋友可以下载。工程中代码大家可以直接使用,但是美术资源不得用于任何商业用途。
UWA Tech Doc

  • fubbi1000 发表在 2018年12月04日 回复

    请问下博主,如果勾选了Optimize GameObject,并且有IK,还可以用这种方法吗

    • 邹春毅 发表在 2018年12月05日 回复

      这种方式需要遍历骨骼节点,所有Optimize GameObject后骨骼就不可见了,因此无法使用。IK是可以的。

      • fubbi1000 发表在 2018年12月05日 回复

        谢谢解答

  • 蔡志杰 发表在 2018年05月22日 回复

    你好,我想请问下,如果材质球上还有法线贴图的话,怎么合并,你这种方法是合并一张图贴的,带法线不知道怎么合并好

  • pp 发表在 2018年01月10日 回复

    你好,在换装过程中,我遇到一个问题,我这边除了武器以外还有很多的小部件例如帽子,眼镜之类的需要挂载,这时候人物的drawcall可能非常大,不知道有没有优化的方式呢?

  • 许宏冰 发表在 2017年11月30日 回复

    用Unity2017.2版本,会报这个错,不知道楼主能不能指点下,怎么处理
    SkinnedMeshRenderer: Trying to set a mesh ("") that is unfit for use with SkinnedMeshRenderer. Most likely it doesn't have Skin information and/or is only assigned to the renderer during runtime, in which case Unity isn't aware of its use with SkinnedMeshRenderer. Mark the mesh as Readable or assign it to the renderer in the Editor before building.
    UnityEngine.SkinnedMeshRenderer:set_sharedMesh(Mesh)
    UCombineSkinnedMgr:CombineObject(GameObject, SkinnedMeshRenderer[], Boolean) (at Assets/Avater/Script/CombineSkinnedMgr.cs:115)
    UCharacterController:ChangeEquipment(Boolean) (at Assets/Avater/Script/CharacterController.cs:267)
    UCharacterController:.ctor(Int32, String, String, String, String, String, String, Boolean) (at Assets/Avater/Script/CharacterController.cs:105)
    UCharacterMgr:Generatecharacter(String, String, String, String, String, String, Boolean) (at Assets/Avater/Script/CharacterMgr.cs:44)
    Main:Start() (at Assets/Avater/Script/Main.cs:76)

    • 周锋 发表在 2018年05月06日 回复

      var tempMesh = new Mesh();
      tempMesh.CombineMeshes(combineInstances.ToArray(), combine, false);// Combine meshes
      r.sharedMesh = tempMesh;

      这么写就ok了

  • LoriChen 发表在 2017年11月11日 回复

    如果一个SkinnedMeshRenderer下面拥有多个Material,合并不成功!!!!

    • aladdin 发表在 2018年10月15日 回复

      我也碰到了这个问题,你们是分解模型了么,让一个SkinnedMeshRender对应一个材质么

  • JAROD 发表在 2017年11月09日 回复

    非常感谢您的无私分享,非常感谢。

  • jim 发表在 2016年09月21日 回复

    请问下,PackTextures 后得到的 Texture 还是压缩格式(比如ETC)吗,或者是未压缩(RGB)纹理了?

    • 邹春毅 发表在 2016年09月21日 回复

      你好,newDiffuseTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true); 在这里设置PackTextures之后的纹理格式。因为为了方便测试直接使用了RGBA32。移动平台可以通过宏或者platform属性指定不同的纹理格式。

  • Walker 发表在 2016年07月25日 回复

    有两个问题求解~~
    1、如果不同换装部位使用不同Shader(十几个部位,有3个可选的Shader)要怎么处理?
    2、透明贴图单独分离出来,一个Material最多关联两张贴图?
    谢谢~~

    • 邹春毅 发表在 2016年08月01日 回复

      您好,
      1,不同部位使用不同的shader无法合并材质,因为合并后的材质只能使用一种shader,但是依然可以实现换装,让不同部位引用独立材质。
      2,不清楚你说的一个Material最多关联两张贴图是什么,如果合并材质可以为新的材质指定多张贴图,但是示例代码中只写了一张贴图的生成。