#unity/日常积累 ![[Pasted image 20240130194709.png]] ## 前言 最近学习了Unity中Avatar换装功能实现,参考了网上的几篇文章,总结了一个Demo。Unity的换装实现参考网上的教程,总体有两种实现,一种是官方Demo给出的合并Mesh实现, 还有一种采用的以前端游的做法,共享骨骼的方式。两种方式各有特点。个人Demo实现了以上两种做法。下面是Demo视频。 ![[录制_2024_01_30_19_45_09_433.mp4]] ## 准备资源 手头没有换装资源,所以用了官方Demo的资源作为示例,不过官方的Demo把切分的部件打包成assetbundle, 不易查看,所以通过工具把人物的各个部件生成prefab用来展示 ![[Pasted image 20240130194748.png]] 如上图所以, 对于女性或者男性角色,拆分成**eyes, face, hair, pants, shoes, top**6组部件和一个**skeleton**文件。对于同一部件,由于**material**不同,**mesh**不同,可能会生成很多类型的**prefab**。这里有个问题,如下图所示。 ![[Pasted image 20240130194755.png]] > 所有的prefab中的Mesh都指向了同一个fbx文件中子mesh. 对于在实际项目,美术人员在导出fbx文件的时候,需要单独导出各个子fbx, 这样比较清晰,避免可能出现的资源重复打包问题。 如下图的Demo所示 ![[Pasted image 20240130194802.png]] ## 端游做法,共享骨骼方式实现换装 共享骨骼的实现方式在场景中如下所示, 骨骼obj下挂载了各个子部件obj。 ![[Pasted image 20240130194810.png]] 对于各个part的挂载,除了指定父节点是skeleton节点外,还需要添加如下代码 ``` cs 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(); 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的实现,实现效果图如下 ![[Pasted image 20240130194837.png]] 大致代码如下。 ``` cs private void ChangeEquipCombine(GameObject resgo, ref List combineInstances, ref List materials, ref List bones) { Transform[] skettrans = mSkeleton.GetComponentsInChildren(); GameObject go = GameObject.Instantiate(resgo); SkinnedMeshRenderer smr = go.GetComponentInChildren(); 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数组中 ``` cs 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(); List combineInstances = new List(); List materials = new List(); List bones = new List(); 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(); if (r != null) { GameObject.DestroyImmediate(r); } r = mSkeleton.AddComponent(); 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, 提高渲染效率。但是大多数情况下不一定能够合并材质,如果单个部件的材质使用的贴图数目不同,就无法合并材质了。