【Unity动态换装骨骼合并】

文章目录

换装骨骼合并

前言

前司有个换装需求,把身上的衣服当做部件来制作,每个单独的裙子、手头、翅膀、帽子上都完整的骨骼部分(即主骨骼),然后有特殊需求的部分会加上额外骨骼。

换装的实现原理

换装是游戏里面常见的模块,现在已经有非常多的游戏实装了换装的模块,一般来说,换装的核心实现就是以下几种方案:

  • 材质替换(材质或贴图)
  • 网格替换(Mesh
  • 骨骼绑定部件替换
  • 模型替换

还有其余的一些方法(通过框架比如Universal Multiplayer Avatar等),我下面使用的是主要是第三种方法即使用部件替换。材质替换适用于那种简单的皮肤换色,换纹理,比较朴素(糊弄)的方法,有点是性能消耗少;网格替换使用的也不多,不适合灵活部件的替换,一般会使用合并的方法减少Draw CallCombineMesh或者自己写)

人物模型

  • 使用网格替换和部件的,都需要基于同一套骨骼或者共享同一骨骼树(如果第三方的部件替换也需要遵循此要求),下面以简单的例子举例:

这是一个人物,如果我们不需要动态添加部件,那么人物的节点信息可以简单的设置成如下(不考虑性能)

可以看到,SkinnedMeshRenderer蒙皮骨骼节点的Root Bone指向的是root节点,

所以这个人物换装的思路就很明确了,因为是公用的一组骨骼,而且本身部件上不带自己的骨骼,所以需要的时候直接SetActive就能达到换装的效果,通过捣鼓配置就能达成换装的目的(ScriptObject啥的),当场景中人物过多的时候,可以执行网格合并减少DC,但是如果组件是从网上下载的(比如通过WebRequest下载部件,然后动态的一个个加上去,而且部件自身带了完整的骨骼,就会稍微麻烦一些)

动态加载

  • 动态加载要做的比上面静态放置的多几个步骤,去掉或者合并自带的骨骼(没有更好),将骨骼减少到一条主骨骼(比如body身体上的部分)。这样做的好处有很多,一是减少了动画控制器的数量,如果你要这几个组件同时做动画,不处理的情况下,有几个部件就需要创建几个动画控制器(虽然可以复用),会增加内存、CPU的调用,Animation.Update会消耗不少性能,还有一个问题就是可能会不协调穿帮(因为是部件组成,会存在部件接缝衔接不上的情况,你可以实际中试试)
  1. 如果是下载的,需要初始化到物体身上,放到父节点下面
  2. 编写脚本进行动态合并:
csharp 复制代码
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;
}
  1. 你可以在Start里面执行初始化和穿装备,半途更新需要的话就封装一个接口调用,这样的话很方便别人调用。

实际效果如何就不展示了,如果你的项目需要,可以到具体的场景中比较效果。

相关推荐
AA陈超3 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P06-14 属性菜单 - 文本值行
c++·游戏·ue5·游戏引擎·虚幻
李趣趣4 小时前
数据库字段类型bit容易被忽视的bug
c#·bug
虚行5 小时前
C#OPC客户端通信实操
c#
chao1898448 小时前
C#模拟鼠标键盘操作的多种实现方案
开发语言·c#·计算机外设
future_studio8 小时前
聊聊 Unity(小白专享、C# 小程序 之 联机对战)
unity·小程序·c#
wuk9988 小时前
C#和NModbus库实现Modbus从站
开发语言·c#
攻城狮CSU12 小时前
类型转换汇总 之C#
java·算法·c#
CodeCraft Studio14 小时前
国产化Word处理控件Spire.Doc教程:用Java实现TXT文本与Word互转的完整教程
java·c#·word·spire.doc·word文档转换·txt转word·word转txt
Aevget14 小时前
DevExpress WinForms v25.1亮点 - 电子表格组件、富文档编辑器全新升级
c#·编辑器·界面控件·devexpress·ui开发·winforms
一个专注写bug的小白猿15 小时前
.net实现ftp传输文件保姆教程
后端·c#·.net