文章目录
- 换装骨骼合并
- 前言
- 换装的实现原理
- 人物模型
- 动态加载
换装骨骼合并
前言
前司有个换装需求,把身上的衣服当做部件来制作,每个单独的裙子、手头、翅膀、帽子上都完整的骨骼部分(即主骨骼),然后有特殊需求的部分会加上额外骨骼。
换装的实现原理
换装是游戏里面常见的模块,现在已经有非常多的游戏实装了换装的模块,一般来说,换装的核心实现就是以下几种方案:
- 材质替换(材质或贴图)
- 网格替换(
Mesh
) - 骨骼绑定部件替换
- 模型替换
还有其余的一些方法(通过框架比如Universal Multiplayer Avatar
等),我下面使用的是主要是第三种方法即使用部件替换。材质替换适用于那种简单的皮肤换色,换纹理,比较朴素(糊弄)的方法,有点是性能消耗少;网格替换使用的也不多,不适合灵活部件的替换,一般会使用合并的方法减少Draw Call
(CombineMesh
或者自己写)
人物模型
- 使用网格替换和部件的,都需要基于同一套骨骼或者共享同一骨骼树(如果第三方的部件替换也需要遵循此要求),下面以简单的例子举例:
这是一个人物,如果我们不需要动态添加部件,那么人物的节点信息可以简单的设置成如下(不考虑性能)
可以看到,SkinnedMeshRenderer
蒙皮骨骼节点的Root Bone
指向的是root
节点,
所以这个人物换装的思路就很明确了,因为是公用的一组骨骼,而且本身部件上不带自己的骨骼,所以需要的时候直接SetActive
就能达到换装的效果,通过捣鼓配置就能达成换装的目的(ScriptObject
啥的),当场景中人物过多的时候,可以执行网格合并减少DC,但是如果组件是从网上下载的(比如通过WebRequest
下载部件,然后动态的一个个加上去,而且部件自身带了完整的骨骼,就会稍微麻烦一些)
动态加载
- 动态加载要做的比上面静态放置的多几个步骤,去掉或者合并自带的骨骼(没有更好),将骨骼减少到一条主骨骼(比如
body
身体上的部分)。这样做的好处有很多,一是减少了动画控制器的数量,如果你要这几个组件同时做动画,不处理的情况下,有几个部件就需要创建几个动画控制器(虽然可以复用),会增加内存、CPU的调用,Animation.Update
会消耗不少性能,还有一个问题就是可能会不协调穿帮(因为是部件组成,会存在部件接缝衔接不上的情况,你可以实际中试试)
- 如果是下载的,需要初始化到物体身上,放到父节点下面
- 编写脚本进行动态合并:
public List<GameObject> Equips = new List<GameObject>(); // 在外面的面板上可以看到对应的组件
Dictionary<string, Transform> m_MainBonesDic = new Dictionary<string, Transform>(); // 记录骨骼// 初始化主骨骼
private void InitMainBones()
{m_MainBonesDic.Clear();Transform outtran = null;var childtrans = GetComponentsInChildren<Transform>();for (int i = 0; i < childtrans.Length; ++i){var tran = childtrans[i];if (!m_MainBonesDic.TryGetValue(tran.name, out outtran))m_MainBonesDic[tran.name] = tran;}
}// 讲物体全部放到父节点下面后(组件的原点信息最好都是0点)
private void WearParts(List<GameObject> Equips)
{for (int i = 0; i < Equips.Count; i++){var equip = Equips[i];CombineSourceBones(equip);}
}// 合并组件的骨骼到一条骨骼身上
public void CombineSourceBones(GameObject sourceClothing)
{sourceClothing.transform.SetParent(transform);var skinnedMeshRenderers = sourceClothing.GetComponentsInChildren<SkinnedMeshRenderer>(true);foreach (var sourceRenderer in skinnedMeshRenderers){sourceRenderer.bones = TranslateTransforms(sourceRenderer.bones);sourceRenderer.rootBone = GetBoneByName(sourceRenderer.rootBone.name);UpdateSkinMeshMagicaClothInfo(sourceClothing); // (如果有Magic Cloth,可以在这里处理)}// 如果原来的部件身上都带有完整骨骼,需要销毁DestroyRootBones(sourceClothing);sourceClothing.transform.localPosition = Vector3.zero;sourceClothing.transform.localRotation = Quaternion.identity;
}// 举个例子
private void DestroyRootBones(GameObject sourceClothing)
{var rootbone = sourceClothing.transform.Find("Bip"); // 或者其他名字if (rootbone == null)rootbone = sourceClothing.transform.Find("Root");if (rootbone != null)Destroy(rootbone.gameObject);
}private Transform GetSourceBoneParentInMainBones(Transform sourceBone, out Transform curCheckBoneParent)
{var sourceboneparent = sourceBone.parent;var mainbone = GetBoneByName(sourceboneparent.name);if (mainbone == null){return GetSourceBoneParentInMainBones(sourceboneparent, out curCheckBoneParent);}curCheckBoneParent = sourceBone;return mainbone;
}private Transform GetBoneByName(string boneName)
{if (m_MainBonesDic.TryGetValue(boneName, out Transform tran)){return tran;}return null;
}
- 你可以在
Start
里面执行初始化和穿装备,半途更新需要的话就封装一个接口调用,这样的话很方便别人调用。
实际效果如何就不展示了,如果你的项目需要,可以到具体的场景中比较效果。