Unity Metaverse(五)、Avatar数字人换装系统的实现方案

简介: Avatar数字人换装系统的实现方案

🎈 前言

"Avatar换装"

随着 元宇宙概念的火热,数字人换装捏脸的实现方案逐渐受到更多关注,本篇内容主要介绍如何在Unity中实现数字人的 换装系统,涉及的所有美术资源均来源于 RPM(Ready Player Me),地址: Ready Player Me主页

实现该系统涉及到的无非是老生常谈的几项内容:

  • Skinned Mesh Renderer - 蒙皮网格
  • Material - 材质球
  • Avatar Bone - 骨架

重要点,也是核心点,就是基于Avatar数字人的同一套骨架,也就是讲当数字人进行换装时,切换的是Skinned Mesh Renderer中的Mesh网格及Material材质球,骨架是不会去改变的。

🎈 如何将RPM中编辑的Avatar导入到Unity

本专栏的第一篇内容中有介绍RPM的使用以及将Avatar导入Unity的过程,下面简要说明。

首先要下载其SDK,地址:Ready Player Me - Unity SDK,将下载的.unitypackage包导入到Unity中,可以看到菜单栏中有了Ready Player Me的选项,Avatar Loader可以将我们自定义的Avatar模型导入到Unity中:

Avatar Loader

在RPM的Avatar Hub中,选择我们想要导入到Unity中的Avatar,通过Copy .glb URL复制链接。

Copy .glb URL
回到Unity中,将复制的链接粘贴到Avatar URL or Short Code中,点击Load Avatar

Load Avatar
下载完成后,在Resources文件夹下可以看到下载的.glb模型文件:

glb 模型
Unity中常用的模型文件格式为.fbx类型,可以通过Blender软件将.glb格式文件转换为.fbx格式文件,需要注意在导出选项里,将路径模式改为复制,并选中后面的内嵌纹理,否则导入到Unity中只是一个白模,并没有材质、贴图。

导出fbx

🎈 如何提取模型中的Mesh网格、Material材质、及Texture贴图

Mesh网格和Material材质的提取可以直接在Skinned Mesh Renderer组件中获取并通过实例化并调用AssetDatabase类中的CreateAsset来创建和保存资产:

// 摘要:
//     Creates a new asset at path.
//
// 参数:
//   asset:
//     Object to use in creating the asset.
//
//   path:
//     Filesystem path for the new asset.
[MethodImpl(MethodImplOptions.InternalCall)]
[NativeThrows]
[PreventExecutionInState(AssetDatabasePreventExecution.kGatheringDependenciesFromSourceFile, PreventExecutionSeverity.PreventExecution_ManagedException, "Assets may not be created during gathering of import dependencies")]
public static extern void CreateAsset([NotNull("ArgumentNullException")] UnityEngine.Object asset, string path);
  • asset:第一个参数为要进行保存/创建的资产;
  • path:第二个参数为该资产生成的文件夹路径。

Texture贴图资源可以通过调用AssetDatabase类中的GetDependencies方法获取材质球的依赖项文件路径:

// 摘要:
//     Returns an array of all the assets that are dependencies of the asset at the
//     specified pathName. Note: GetDependencies() gets the Assets that are referenced
//     by other Assets. For example, a Scene could contain many GameObjects with a Material
//     attached to them. In this case, GetDependencies() will return the path to the
//     Material Assets, but not the GameObjects as those are not Assets on your disk.
//
// 参数:
//   pathName:
//     The path to the asset for which dependencies are required.
//
//   recursive:
//     Controls whether this method recursively checks and returns all dependencies
//     including indirect dependencies (when set to true), or whether it only returns
//     direct dependencies (when set to false).
//
// 返回结果:
//     The paths of all assets that the input depends on.
public static string[] GetDependencies(string pathName)
{
    return GetDependencies(pathName, recursive: true);
}

根据路径调用LoadAssetAtPath方法加载贴图资源:

// 摘要:
//     Returns the first asset object of type type at given path assetPath.
//
// 参数:
//   assetPath:
//     Path of the asset to load.
//
//   type:
//     Data type of the asset.
//
// 返回结果:
//     The asset matching the parameters.
[MethodImpl(MethodImplOptions.InternalCall)]
[PreventExecutionInState(AssetDatabasePreventExecution.kGatheringDependenciesFromSourceFile, PreventExecutionSeverity.PreventExecution_ManagedException, "Assets may not be loaded while dependencies are being gathered, as these assets may not have been imported yet.")]
[PreventExecutionInState(AssetDatabasePreventExecution.kDomainBackup, PreventExecutionSeverity.PreventExecution_ManagedException, "Assets may not be loaded while domain backup is running, as this will change the underlying state.")]
[NativeThrows]
[TypeInferenceRule(TypeInferenceRules.TypeReferencedBySecondArgument)]
public static extern UnityEngine.Object LoadAssetAtPath(string assetPath, Type type);

public static T LoadAssetAtPath<T>(string assetPath) where T : UnityEngine.Object;

本篇内容中提取Avatar数字人相关资产的工作流如下:

  • fbx导入到Unity后,在Import Settings导入设置中将Material Location类型改为Use External Materials(Legacy),应用后编辑器会在该fbx文件所在目录下生成相应的材质和贴图资源文件夹:

Materials Location

  • 将所有法线贴图Texture Type改为Normal map,并检查法线贴图是否用在相应材质球上:

Normal map

  • 调用自定义的编辑器方法,提取资产:

提取资产
该方法可以提取Avatar的头部、身体、上衣、裤子及鞋子的相关资产,代码如下:

using UnityEngine;
using UnityEditor;

namespace Metaverse
{
    /// <summary>
    /// 用于提取ReadyPlayerMe的Avatar服装资源
    /// </summary>
    public class RPMAvatarClothingCollecter 
    {
        [MenuItem("Metaverse/Ready Player Me/Avatar Clothing Collect")]
        public static void Execute()
        {
            //如果未选中任何物体
            if (Selection.activeGameObject == null) return;
            //弹出窗口 选择资源提取的路径
            string collectPath = EditorUtility.OpenFolderPanel("选择路径", Application.dataPath, null);
            //如果路径为空或无效 返回
            if (string.IsNullOrEmpty(collectPath)) return;
            //AssetDatabase路径
            collectPath = collectPath.Replace(Application.dataPath, "Assets");
            if (!AssetDatabase.IsValidFolder(collectPath)) return;

            //头部
            Transform head = Selection.activeGameObject.transform.Find("Wolf3D_Head");
            if (head != null) Collect(collectPath, head.GetComponent<SkinnedMeshRenderer>(), "head");

            //身体
            Transform body = Selection.activeGameObject.transform.Find("Wolf3D_Body");
            if (head != null) Collect(collectPath, body.GetComponent<SkinnedMeshRenderer>(), "body");

            //上衣
            Transform top = Selection.activeGameObject.transform.Find("Wolf3D_Outfit_Top");
            if (top != null) Collect(collectPath, top.GetComponent<SkinnedMeshRenderer>(), "top");

            //裤子
            Transform bottom = Selection.activeGameObject.transform.Find("Wolf3D_Outfit_Bottom");
            if (bottom != null) Collect(collectPath, bottom.GetComponent<SkinnedMeshRenderer>(), "bottom");
            
            //鞋子
            Transform footwear = Selection.activeGameObject.transform.Find("Wolf3D_Outfit_Footwear");
            if (footwear != null) Collect(collectPath, footwear.GetComponent<SkinnedMeshRenderer>(), "footwear");
            
            //刷新
            AssetDatabase.Refresh();
        }

        public static void Collect(string path, SkinnedMeshRenderer smr, string suffix)
        {
            //创建Mesh网格资产
            AssetDatabase.CreateAsset(Object.Instantiate(smr.sharedMesh), string.Format("{0}/mesh_{1}.asset", path, suffix));
            //创建Material材质球资产
            Material material = Object.Instantiate(smr.sharedMaterial);
            AssetDatabase.CreateAsset(material, string.Format("{0}/mat_{1}.asset", path, suffix));
            //获取材质球的依赖项路径
            string[] paths = AssetDatabase.GetDependencies(AssetDatabase.GetAssetPath(material));
            //遍历依赖项路径
            for (int i = 0;i < paths.Length; i++)
            {
                //AssetDatabase路径
                string p = paths[i].Replace(Application.dataPath, "Assets");
                //根据路径加载Texture贴图资源
                Texture tex = AssetDatabase.LoadAssetAtPath<Texture>(p);
                if (tex == null) continue;
                TextureImporter textureImporter = AssetImporter.GetAtPath(p) as TextureImporter;
                //主贴图
                if (textureImporter.textureType == TextureImporterType.Default)
                {
                    AssetDatabase.MoveAsset(p, string.Format("{0}/tex_{1}_d.jpg", path, suffix));
                }
                //法线贴图
                if (textureImporter.textureType == TextureImporterType.NormalMap)
                {
                    AssetDatabase.MoveAsset(p, string.Format("{0}/tex_{1}_n.jpg", path, suffix));
                }
            }
        }
    }
}

🎈 如何提取RPM网页中的图片资源

提取网页中的图片资源可以使用 ImageAssistant图片助手,一款Chrome浏览器中用于嗅探、分析网页图片、图片筛选、下载等功能的扩展程序,当然也可以在Edge浏览器中去使用,地址: Image Assistant

图片助手

选中想要下载的图片资源并开始下载:

下载图片

🎈 资源配置

正常开发工作中,建议构建出不同服装资源的AB(AssetsBundle)包,通过加载AB包来实现各种服装的切换,本篇内容中通过Scriptable Object配置各种服装资源来实现Demo,首先编写外观数据类:

using System;
using UnityEngine;

namespace Metaverse
{
    /// <summary>
    /// Avatar外观数据
    /// </summary>
    [Serializable]    
    public class AvatarOutlookData
    {
        /// <summary>
        /// 头部网格
        /// </summary>
        public Mesh headMesh;
        /// <summary>
        /// 头部材质
        /// </summary>
        public Material headMaterial;

        /// <summary>
        /// 身体网格
        /// </summary>
        public Mesh bodyMesh;
        /// <summary>
        /// 身体材质
        /// </summary>
        public Material bodyMaterial;

        /// <summary>
        /// 上衣网格
        /// </summary>
        public Mesh topMesh;
        /// <summary>
        /// 上衣材质
        /// </summary>
        public Material topMaterial;

        /// <summary>
        /// 裤子网格
        /// </summary>
        public Mesh bottomMesh;
        /// <summary>
        /// 裤子材质
        /// </summary>
        public Material bottomMaterial;

        /// <summary>
        /// 鞋子网格
        /// </summary>
        public Mesh footwearMesh;
        /// <summary>
        /// 鞋子材质
        /// </summary>
        public Material footwearMaterial;

        /// <summary>
        /// 缩略图
        /// </summary>
        public Sprite thumb;
    }
}

编写配置类如下,实现后创建一个新的配置表并配置数据:

using UnityEngine;

namespace Metaverse
{
    /// <summary>
    /// Avatar服装配置
    /// </summary>
    [CreateAssetMenu(menuName = "Metaverse/Avatar Clothing Config")]
    public class AvatarClothingConfig : ScriptableObject
    {
        public AvatarOutlookData[] data = new AvatarOutlookData[0];
    }
}

数据配置

测试脚本如下:

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField] private SkinnedMeshRenderer head;
    [SerializeField] private SkinnedMeshRenderer body;
    [SerializeField] private SkinnedMeshRenderer top;
    [SerializeField] private SkinnedMeshRenderer bottom;
    [SerializeField] private SkinnedMeshRenderer footwear;

    [SerializeField] private Metaverse.AvatarOutlookConfig config;

    private void OnGUI()
    {
        if (GUILayout.Button("服装一", GUILayout.Width(200f), GUILayout.Height(50f))) Apply(0);
        if (GUILayout.Button("服装二", GUILayout.Width(200f), GUILayout.Height(50f))) Apply(1);
        if (GUILayout.Button("服装三", GUILayout.Width(200f), GUILayout.Height(50f))) Apply(2);
    }

    private void Apply(int index)
    {
        head.sharedMesh = config.data[index].headMesh;
        head.sharedMaterial = config.data[index].headMaterial;

        body.sharedMesh = config.data[index].bodyMesh;
        body.sharedMaterial = config.data[index].bodyMaterial;
       
        top.sharedMesh = config.data[index].topMesh;
        top.sharedMaterial = config.data[index].topMaterial;
        
        bottom.sharedMesh = config.data[index].bottomMesh;
        bottom.sharedMaterial = config.data[index].bottomMaterial;
        
        footwear.sharedMesh = config.data[index].footwearMesh;
        footwear.sharedMaterial = config.data[index].footwearMaterial;
    }
}

换装

目录
相关文章
|
2月前
|
图形学 开发者 UED
Unity游戏开发必备技巧:深度解析事件系统运用之道,从生命周期回调到自定义事件,打造高效逻辑与流畅交互的全方位指南
【8月更文挑战第31天】在游戏开发中,事件系统是连接游戏逻辑与用户交互的关键。Unity提供了多种机制处理事件,如MonoBehaviour生命周期回调、事件系统组件及自定义事件。本文介绍如何有效利用这些机制,包括创建自定义事件和使用Unity内置事件系统提升游戏体验。通过合理安排代码执行时机,如在Awake、Start等方法中初始化组件,以及使用委托和事件处理复杂逻辑,可以使游戏更加高效且逻辑清晰。掌握这些技巧有助于开发者更好地应对游戏开发挑战。
120 0
|
3月前
|
图形学 C# 开发者
Unity粒子系统全解析:从基础设置到高级编程技巧,教你轻松玩转绚丽多彩的视觉特效,打造震撼游戏画面的终极指南
【8月更文挑战第31天】粒子系统是Unity引擎的强大功能,可创建动态视觉效果,如火焰、爆炸等。本文介绍如何在Unity中使用粒子系统,并提供示例代码。首先创建粒子系统,然后调整Emission、Shape、Color over Lifetime等模块参数,实现所需效果。此外,还可通过C#脚本实现更复杂的粒子效果,增强游戏视觉冲击力和沉浸感。
182 0
|
3月前
|
开发者 图形学 前端开发
绝招放送:彻底解锁Unity UI系统奥秘,五大步骤教你如何缔造令人惊叹的沉浸式游戏体验,从Canvas到动画,一步一个脚印走向大师级UI设计
【8月更文挑战第31天】随着游戏开发技术的进步,UI成为提升游戏体验的关键。本文探讨如何利用Unity的UI系统创建美观且功能丰富的界面,包括Canvas、UI元素及Event System的使用,并通过具体示例代码展示按钮点击事件及淡入淡出动画的实现过程,助力开发者打造沉浸式的游戏体验。
95 0
|
3月前
|
开发者 图形学 UED
深度解析Unity游戏开发中的性能瓶颈与优化方案:从资源管理到代码执行,全方位提升你的游戏流畅度,让玩家体验飞跃性的顺滑——不止是技巧,更是艺术的追求
【8月更文挑战第31天】《Unity性能优化实战:让你的游戏流畅如飞》详细介绍了Unity游戏性能优化的关键技巧,涵盖资源管理、代码优化、场景管理和内存管理等方面。通过具体示例,如纹理打包、异步加载、协程使用及LOD技术,帮助开发者打造高效流畅的游戏体验。文中提供了实用代码片段,助力减少内存消耗、提升渲染效率,确保游戏运行丝滑顺畅。性能优化是一个持续过程,需不断测试调整以达最佳效果。
90 0
|
3月前
|
图形学
Unity动画☀️Unity动画系统Bug集合
Unity动画☀️Unity动画系统Bug集合
|
5月前
|
存储 JSON 关系型数据库
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
159 2
|
5月前
|
Rust 图形学
【unity实战】使用unity制作一个类似Rust的3D生存建造建筑系统,具有很好的吸附性(附项目源码)
【unity实战】使用unity制作一个类似Rust的3D生存建造建筑系统,具有很好的吸附性(附项目源码)
130 1
|
5月前
|
图形学
【制作100个unity游戏之25】3D背包、库存、制作、快捷栏、存储系统、砍伐树木获取资源、随机战利品宝箱10(附带项目源码)
【制作100个unity游戏之25】3D背包、库存、制作、快捷栏、存储系统、砍伐树木获取资源、随机战利品宝箱10(附带项目源码)
48 1
|
5月前
|
图形学
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统(下)
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统
81 0
|
5月前
|
图形学 容器
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统(上)
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统
81 0