前言

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


准备资源

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

unity中Avatar换装实现(三)之美_递归

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

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

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

如下图的Demo所示

unity中Avatar换装实现(三)之美_数组_03

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

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

unity中Avatar换装实现(三)之美_递归_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换装实现(三)之美_递归_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​

参考项目

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

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

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