|
前言
最近学习了Unity中Avatar换装功能实现,参考了网上的几篇文章,总结了一个Demo。Unity的换装实现参考网上的教程,总体有两种实现,一种是官方Demo给出的合并Mesh实现, 还有一种采用的以前端游的做法,共享骨骼的方式。两种方式各有特点。个人Demo实现了以上两种做法。下面是Demo视频。
https://www.zhihu.com/video/1011580391949549568
准备资源
手头没有换装资源,所以用了官方Demo的资源作为示例,不过官方的Demo把切分的部件打包成assetbundle, 不易查看,所以通过工具把人物的各个部件生成prefab用来展示
如上图所以, 对于女性或者男性角色,拆分成eyes, face, hair, pants, shoes, top6组部件和一个skeleton文件。对于同一部件,由于material不同,mesh不同,可能会生成很多类型的prefab。这里有个问题,如下图所示。
所有的prefab中的Mesh都指向了同一个fbx文件中子mesh. 对于在实际项目,美术人员在导出fbx文件的时候,需要单独导出各个子fbx, 这样比较清晰,避免可能出现的资源重复打包问题。 如下图的Demo所示
端游做法,共享骨骼方式实现换装
共享骨骼的实现方式在场景中如下所示, 骨骼obj下挂载了各个子部件obj。
对于各个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.gameObject;
// 目标的SkinnedMeshRenderer.bones保存的只是目标mesh相关的骨骼,要获得目标全部骨骼,可以通过查找的方式.
newBones = 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的实现,实现效果图如下
大致代码如下。
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(&#34;walk&#34;))
{
mAnim.wrapMode = WrapMode.Loop;
mAnim.Play(&#34;walk&#34;);
}
}
}通过收集的CombineInstance数组combineInstances,骨骼数组bones,以及材质数组materials, 组成一个新的Mesh, 添加到新创建的SkinnedMeshRenderer中。从而可以产生动画。
优缺点:
这种合并Mesh的方式缺点很明显,如果需要更新一个部件,需要重新创建新的Mesh和SkinnedMeshRenderer, 不太灵活。
不过这种合并Mesh的方式可以在合并Mesh的时候合并材质,减少DrawCall, 提高渲染效率。但是大多数情况下不一定能够合并材质,如果单个部件的材质使用的贴图数目不同,就无法合并材质了。 Demo链接地址
参考项目
端游做法,共享骨骼方式实现换装的参考文章
合并Mesh实现换装的参考文章 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|