您需要 登录 才可以下载或查看,没有账号?注册
x
本帖最后由 元素精灵 于 2018-9-27 17:48 编辑
前言最近学习了Unity中Avatar换装功能实现,参考了网上的几篇文章,总结了一个Demo。Unity的换装实现参考网上的教程,总体有两种实现,一种是官方Demo给出的合并Mesh实现, 还有一种采用的以前端游的做法,共享骨骼的方式。两种方式各有特点。个人Demo实现了以上两种做法。
准备资源手头没有换装资源,所以用了官方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节点外,还需要添加如下代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| 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的实现,实现效果图如下 大致代码如下。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| 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数组中
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| 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, 提高渲染效率。但是大多数情况下不一定能够合并材质,如果单个部件的材质使用的贴图数目不同,就无法合并材质了。
知乎@谢刘建
|