見出し画像

UnityでモーションUGCを実現するMuscle圧縮技術を大公開!

はじめに

お世話になっております。REALITY株式会社GREE VR Studio Laboratory(ラボ)でインターンをしている香山です。今回は今週アメリカ・ロサンゼルスで開催されているACM SIGGRAPH2023というCG・インタラクティブ技術のトップカンファレンスで発表予定の研究 "MMM"と、そこで用いられている要素技術について紹介したいと思います。


What’s MMM?

概要

ラボで開発したアバターファッションショー ”MMM (Metaverse Mode Maker)” は、メタバース分野で重要度が高まり続けているUGC (User-Generated Content) に焦点を当て、生成AIを活用することでHMDを装着したまま手軽に高品質なUGCを作成することを目的としたPoC (概念実証)プロジェクトです。
ファッションショーのランウェイをモチーフとしており、ユーザーが体験の中で服テクスチャとモーションを作成することで、それらを組み合わせたランウェイシーンがYouTubeLiveを介してリアルタイムに配信されます。

論文の公開とともにモーション周りの一部のソースコードをOSSとして公開しており、今回のブログはそちらの日本語での詳細解説という位置付けになります。
OSSにはUnityでモーションデータをランタイムに保存・再生する上での知見や、ラボがかねてよりQuestを使った簡易モーションキャプチャ「QueTra」として発表してきた技術が組み込まれています。キャプチャシステムやAITuberへの応用などさまざまな使い道があると思いますし、学生VR作品での活用も可能ではないかと思います。OSSへのリンクは記事末尾にて公開しておりますので、こちらぜひご活用ください。

システム構成

 

“MMM”のシステムは、

  • StableDiffusionを利用してアバターの服テクスチャを生成する MetaInk

  • HMD装着時の上半身モーションをキャプチャし、圧縮して保存する MuscleCompressor

  • アバターとモーションを読み込んで事前に用意された下半身アニメーションと合成し、ランウェイシーンとして配信する RunwayBroadcaster

以上の3ステップに分割されます。MetaInk, MuscleCompressorとRunwayBroadcasterはそれぞれ独立に動いており、特にフランスで開催されたLaval Virtual Revolution展示時にはいわゆるオフラインのVR展示ではなく、MetaInkとMuscleCompressorがフランス現地でVRMファイルとモーションファイルを生成し、それを六本木のPCで実行されているRunwayBroadcasterで受け取り、YouTubeLiveを介してフランスの会場(というか世界中)に配信する…というちょっとした構成を取っていました。

もちろん展示会場だけで終わらせることもできるのですが、YouTubeで配信するとオンラインで参加・シェアできる人も出てきて嬉しいですよね。加えて研究としてはワンオペ展示でも記録を全て残せるという利点もありました。

したがって、このような長距離通信を含むシステムで快適なUXを実現するために、モーション周りでは以下の要素を満たす必要がありました。

  • ランタイムでモーションの読み書きができること

  • 可能な限り軽量なデータとしてモーションを保存すること

  • 新しい体験者によるモーションを確実に受け取れること

  • 大量に生成されるモーションを残して分析できること

本記事では、これらの課題を乗り越えるために作成したMuscleCompressorモジュールについて、サンプルコードを交えた解説を行っていきたいと思います。

MuscleCompressor - アバターの動きを軽量に外部保存

MuscleCompressorのデモ。ランタイムでもモーション読み書き可能。

Unityでモーションを扱う場合はアニメーションクリップ(.animファイル)を用いるのが普通かと思いますが、実はこれ、ランタイムで動的に生成することができないようです。(2016年から長いこと解決していないUnityフォーラム
そこで、今回のPoC実装ではVRMアバターのボーン情報(ボーン名とそのMuscle値)をバイナリとして直接外部ファイルに書き込むことでモーションを保存します。

MuscleとはUnityでHumanoidボーンのFK制御を行うための仕組みで、これにより各ボーンの曲がり具合を[-1, 1]の範囲で表される1つの変数で記述することができます。ボーンが人間の可動域を超えて変形する、いわゆる「グチャる」とか「骨折」といった破綻を防ぎつつ、ボーンの状態を1変数で表すことによってデータ量を削減できるという利点があります。

ここから先は具体的な実装を紹介します。
⭐︎REALITYのプラットフォームで実装されている方法ではなく、GREE VR Studio LaboratoryのPoC開発で使用されている方法になります。

MotionDataClass.cs

まずはモーションを保存するため、muscle値をjson形式で格納するクラスMotionDataClassを定義します。

サンプルコード

using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json;
namespace VRStudioLab.Scripts
{
    [JsonObject]
    [MessagePackObject]
    public class MotionDataClass
    {
        [JsonProperty("GUID")] [Key(0)] public string Guid { get; set; }
        [JsonProperty("Time")] [Key(1)] public float Time { get; set; }
        [JsonProperty("Transforms")] [Key(2)] public List<ObjectTransforms> Transforms { get; set; }
    }
    [JsonObject]
    [MessagePackObject]
    public class ObjectTransforms
    {
        [JsonProperty("ObjectName")] [Key(0)] public string ObjectName { get; set; }
        [JsonProperty("value")] [Key(1)] public float Value { get; set; }
    }
}

ObjectTransformsクラスには各ボーンの名前とそのmuscle値が入り、MotionDataClassには各フレームにおける体の全ボーン分のObjectTransformsとそれが記録された時間、そして固有のGUIDが入ります。これらのデータはMessagePack for C#によりバイナリ化され、より小さいサイズに圧縮されます。

Motion2BytesHumanoid.cs

アバターの動きをMotionDataClassに沿って保存するスクリプトです。

サンプルコード (抜粋)

private async UniTaskVoid RecordMuscleFrameDataAsync()
{
    var humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
    var humanPose = new HumanPose();
    try
    {
        while (true)
        {
            _cts.Token.ThrowIfCancellationRequested();
            humanPoseHandler.GetHumanPose(ref humanPose);
            var animations = new List<ObjectTransforms>();
            foreach (var muscle in HumanTrait.MuscleName.Select((value, index) => new { value, index }))
            {
                if (isHighCompression)
                {
                    animations.Add(new ObjectTransforms
                    { ObjectName = "", Value = humanPose.muscles[muscle.index] });
                    continue;
                }
                // muscle定義名とanimationプロパティ名の対応付け
                switch (muscle.index)
                {
                    case 55:
                        animations.Add(new ObjectTransforms
                        {
                            ObjectName = "LeftHand.Thumb.1 Stretched",
                            Value = humanPose.muscles[muscle.index]
                        });
                        break;
                    case 56:
                        animations.Add(new ObjectTransforms
                        { ObjectName = "LeftHand.Thumb.Spread", Value = humanPose.muscles[muscle.index] });
                        break;
                    case 57:
                        animations.Add(new ObjectTransforms
                        {
                            ObjectName = "LeftHand.Thumb.2 Stretched",
                            Value = humanPose.muscles[muscle.index]
                        });
                        break;
          ...

中身としては、HumanPoseHandler.GetHumanPoseを利用して取得した各ボーンのmuscle値を一定の時間間隔でMotionDataClassに格納しています。

圧縮優先モード (isHighCompression = true) ではMotionDataClassのObjectNameを空文字列とすることで容量を更に小さくしています。互換優先モード (isHighCompression = false) では容量が大きくなる代わりにアニメーションクリップとの互換性を持たせており、後述するBytes2Animスクリプトにより.anim形式にコンバートできるようになっています。
ここでUnityでは指ボーンのmuscle定義名とAnimationClipプロパティ名が一致していない (例: Left Thumb 1 Stretched (Muscle)⇒ LeftHand.Thumb.1 Stretched (AnimationClip)) ため、Switch文でこれらの対応付けを行っています(ここの変換対応がChatGPTやGitHub Copilotで超えづらい根性コードなので、実装したことある人はニヤニヤしてください)。

Bytes2Motion.cs

記録した.dataファイルを読み出し、直接アバターを動かすスクリプトです。行っていることはMotion2BytesHumanoidの逆で、HumanPoseHandler.SetHumanPoseを利用してMuscle値をフレーム毎にアバターのボーンへ適用しています。

サンプルコード (抜粋)

IEnumerator LoadMotion(List<MotionDataClass> data)
{
    recentTime = 0;
    isHandlerable = true;
    foreach (var frame in data.Select((value, index) => new { value, index }))
    {
        foreach (var objectTransforms in frame.value.Transforms.Select((value, index) => new { value, index }))
        {
            tempHumanPose[objectTransforms.index] = objectTransforms.value.Value;
        }
        // 記録した時間に追いつくまで待機
        yield return new WaitUntil(() => recentTime * playSpeed >= frame.value.Time);
    }
}
// HumanPoseHandlerによる操作はLateUpdateで行う
void LateUpdate()
{
    recentTime += Time.deltaTime;
    if (isHandlerable == true)
    {
        handler.GetHumanPose(ref humanPose);
        for (int i = 0; i < 95; i++)
        {
            humanPose.muscles[i] = tempHumanPose[i];
        }
        handler.SetHumanPose(ref humanPose);
    }
}

       
アニメーションクリップを介さず直接muscle値を流し込むことで、ランタイムで動作する外部モーション読み込みを実現しました。
注意点ですが、UnityではUpdate() → Animation → LateUpdate() という順番で処理が行われるため、MMMの「下半身は事前収録したアニメーションを用い、上半身はランタイム読み込みしたモーションを用いる」という例のようにアニメーションとmuscle制御を併用する場合は、muscleの適用処理をLateUpdate()のタイミングで行う必要があります。

Bytes2Anim.cs

記録した.dataファイルを読み出し、アニメーションクリップに変換するエディタ拡張スクリプトです。先ほど紹介した通り、スクリプトからアニメーションカーブを生成するSetCurveメソッドは永遠に解消されないバグのためにランタイム動作しませんが、エディタ上では問題なく動きます。

サンプルコード (抜粋)

public static void Byte2Anim()
{
    var extension = new[] {new ExtensionFilter("data file", "data")};
    var path = StandaloneFileBrowser.OpenFilePanel("Open File", "", extension, false);
    var bytes = File.ReadAllBytes(path[0]);
    var data = MessagePackSerializer.Deserialize<List<MotionDataClass>>(bytes);
    var curves = new AnimationCurve[102];
    var clip = new AnimationClip {legacy = false};
    foreach (var frame in data.Select((value, index) => new {value, index}))
    {
        foreach (var objectTransforms in frame.value.Transforms.Select((value, index) => new {value, index}))
        {
            var keyframe = new Keyframe(frame.value.Time, objectTransforms.value.Value);
            if (curves[objectTransforms.index] == null)
            {
                curves[objectTransforms.index] = new AnimationCurve(keyframe);
            }
            else
            {
                curves[objectTransforms.index].AddKey(keyframe);
            }
            clip.SetCurve("", typeof(Animator), $"{objectTransforms.value.ObjectName}", curves[objectTransforms.index]);}
    }
    if (!Directory.Exists("Assets/VRStudioLab/Animations")) Directory.CreateDirectory("Assets/VRStudioLab/Animations");
    var directory = new DirectoryInfo("Assets/VRStudioLab/Animations/");
    var max = directory.GetFiles($"???.anim")
        .Select(info => Regex.Match(info.Name, @"(\d{3})\.anim"))
        .Where(match => match.Success)
        .Select(match => Parse(match.Groups[1].Value))
        .DefaultIfEmpty(0)
        .Max();
    var fileName = $"{max + 1:d3}.anim";
    AssetDatabase.CreateAsset(clip,$"Assets/VRStudioLab/Animations/{fileName}");
}

モーション圧縮性能の評価

ここまでコードを紹介してきましたが、研究なので評価も大事です。最後にMuscleCompressorによる圧縮結果を確かめるため、Meta Quest 2を用いて60fpsで収録した1分間の上半身モーションをUnity標準の.anim形式、それを圧縮した.unitypackage形式、BVH Toolsにより出力される.bvh形式、そしてMuscleCompressorにより出力される.data形式(圧縮優先モード)で保存し、それぞれのファイルサイズを比較してみます。結果は以下の表の通りです。

提案手法は.animと比較して96.3%, .bvhと比較して94.0%の圧縮を達成しており、既存のモーション取り扱い手法と比較して大幅にサイズを削減できました。UnityPackageと比較しても3/4ほどの容量で保存できており、ランタイムにモーションを読み書きするシステムとして良い性能を出していることがわかります。 

コード・サンプルシーン

以上紹介しましたMuscleCompressorのコードは、OSSとして公開しています。

ラボがこれまで公開してきた論文、”Cross-Platforming “School Life Metaverse” User Experience”, “Avatar Fusion Karaoke: Research on Multi-user Music Play VR Experience in Metaverse” で紹介したモーションキャプチャシステム”QueTra”と複合させたサンプルシーンも用意してあります。皆さんのメタバース開発の一助となれば幸いです。

先行してOSS公開したところ、早速扱ってくれたメディアさんがいて嬉しいです!

至らないところも多くありましてドキドキですが…早速プルリクエストもいただいて感謝です。

さいごに

記事で解説したMuscleCompressorを利用したプロジェクト”MMM”は、今後SIGGRAPH 2023でのポスター発表とCEDEC 2023でのデモ展示を控えています。

AI-Assisted Avatar Fashion Show: Word-to-Clothing Texture Exploration and Motion Synthesis for Metaverse UGC

CEDECの方ではインタラクティブセッションとして体験できるので、お越しの方は是非触れて頂けたらと思います。香山も白井さんも会場でデモしていると思いますので遠慮なくお声がけください。

最後に、圧倒的なUnity力でQueTraやMuscleCompressorの仕組みを作ってくださったやはぎさん、ディレクターとしてプロジェクトをぐいぐいと進めていただき、ワンオペ展示実験までやってOSS公開も応援していただいた白井さんへの謝辞で締めたいと思います。ありがとうございました!

BibTex Infomation

研究者・学生さんへ: 本件の文献としての引用はブログではなくこちらでお願いします。

@inproceedings{10.1145/3588028.3603660,
author = {Kohyama, Kai and Berthault, Alexandre and Kato, Takuma and Shirai, Akihiko},
title = {AI-Assisted Avatar Fashion Show: Word-to-Clothing Texture Exploration and Motion Synthesis for Metaverse UGC},
year = {2023},
isbn = {9798400701528},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3588028.3603660},
doi = {10.1145/3588028.3603660},
booktitle = {ACM SIGGRAPH 2023 Posters},
articleno = {14},
numpages = {2},
location = {Los Angeles, CA, USA},
series = {SIGGRAPH '23}
}