1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】VRMモデルをランタイムロードして表情・体・口を動かす

Last updated at Posted at 2025-12-13

はじめに

この記事は Iwaken Lab. Advent Calendar 2025 9日目の記事です。

2025年度から Iwaken Lab. に参加させて頂いている dokudami です。先日行われた XRKaigi2025 の Iwaken Lab. ブースにて、86人目のAIラボメン「86ちゃん」という対話システムの展示をさせていただきました。

この対話システムのフロント実装を担当させていただいたのですが、VRMモデルを制御するのに色々試行錯誤しました。その試行錯誤を経た VRM 制御の個人的なベストプラクティスを備忘録として記事にしたいと思います。特に、①VRMファイルのロード、②ロードしたVRMファイル制御(表情、モーション、リップシンク)について紹介します。

環境

  • VRM 1.0 (UniVRM10)
  • Unity 6000.2.7f2

UniVRM を Unity プロジェクトに導入

そもそもなのですが、Unity で VRM を使用したい場合 まず VRM コンソーシアムが提供する UniVRM という標準実装パッケージをインストールします。公式リポジトリの Installation の箇所にインストール方法が記載されています。UniVRM のバージョン管理が容易になるため、本記事では UPM Package によるインストールを推奨します。

  • Window > Package Management > Package Manager を開く
  • + > Install package from git URL... を選択し、パッケージの URL を登録する
  • UniVRM 1.0 系を利用する場合は、com.vrmc.gltfcom.vrmc.vrm の git URLを追加する

image.png

VRMを利用したUnityアプリを作るときは毎回この作業を行います。2025年現在はVRM1.0になり、昔に比べてバージョンが安定しています(個人の感想)。しかし、仕様が変わる可能性もあるのでReleasesにある記載をよく読みインストールしてください。

VRMモデルのランタイムロード

UniVRM でVRMファイルを利用したアプリケーションを作成する場合、使いたいVRMファイルをランタイムロードさせる実装にすることをお勧めします。ランタイムロードとは、シーン上にVRMファイルから生成された3Dモデルを直接置くのではなく、VRMファイルのパスを持っておいてそのパスからVRMファイルを動的にロードして利用するということです。

VRM モデルをランタイムロードし、動的に必要な機能をセットアップする実装などにすることで、別の VRM ファイルを使用したくなった際も ファイルパスを変更するだけで差し替えが可能 になります。そもそも VRM はモデルを差し替えて使うことを想定した形式であるため、こうした設計のほうが都合が良い場合が多いです。

Image from Gyazo

StreamingAssets 等に使いたいVRMファイルを置いておき、それをアプリ開始とともにロードする実装などがすぐにできる無難な実装です。下記に StreamingAssets にある avatar.vrm というVRMファイルをロードするサンプルを示します。Vrm10.LoadPathAsync()でVRMファイルをロードし、返り値でVRM10Instanceを受け取ります。using UniVRM10;を記述する必要がある点に注意してください。

using UnityEngine;
using UniVRM10;

public class Test : MonoBehaviour
{
    [SerializeField] private string fileName = "avatar.vrm";

    private async void Start()
    {
        string path = $"{Application.streamingAssetsPath}/{fileName}";
        Vrm10Instance vrm10Instance = await Vrm10.LoadPathAsync(path);
        Debug.Log("VRM Loaded: " + vrm10Instance);
    }
}

ドキュメントによると、VRM ファイルのロードには Vrm10.LoadPathAsync()Vrm10.LoadBytesAsync() の2種類があります。上記のサンプルでは前者の Vrm10.LoadPathAsync() を用いましたが、Web 環境やネットワーク経由で既にバイト列として取得した VRM を扱う場合は、後者のVrm10.LoadBytesAsync() を利用するのが良さそうです。

VRMモデルを制御する

表情😄

まずは VRM モデルのキャラクターの表情を制御したいです。

VRM10 の仕様では、Emotion5種・LipSync5種・Blink3種・LookAt4種・Other1種の計18種の表情(Expression)を設定することが定められています。仕様に準拠した VRM(特に VRoid Studio 製モデルなど)であれば、これらの表情に対応する BlendShape が標準で用意されており、表情名とその度合い(ウェイト)を指定することで、モデルの表情を制御できます。

例えば、感情表現に応じた表情を設定したい場合、Emotion5種のHappy, Angry, Sad, Relaxed, Surprisedはあらかじめ用意されているので、VRMをインポートした瞬間から利用できます。

Image from Gyazo

具体的には、vrm10Instance から vrm10Instance.Runtime.Expression を取得し、vrm10RuntimeExpression に対して SetWeight() を呼び出すことで、Expression として VRM 登録している表情のウェイトを再生できます。下図に表情切り替えの例を示します。

using UnityEngine;
using UniVRM10;

public class SimpleExpressionSample : MonoBehaviour
{
    [SerializeField] Vrm10Instance vrm10Instance;

    // RuntimeExpression を保持
    private Vrm10RuntimeExpression _expression;

    /// <summary>
    /// 適当な初期化処理
    /// </summary>
    void Start()
    {
        // vrm10Instance.Runtime.Expression を抜き出す
        _expression = vrm10Instance.Runtime.Expression;
    }

    /// <summary>
    /// Happy の表情にする
    /// </summary>
    public void SetHappy()
    {
        _expression.SetWeight(ExpressionKey.Happy, 1f);
    }
}

さらに高度・独自の表情を設定する場合は、3Dモデルとして表情のウェイト(モーフ)を手動やAnimatorで切り替えることで表情を変えることができます。ここまでくると VRM の仕様の範疇を超えてくるので Unity で任意の3Dモデルの表情を設定する話と同じになります。今回はVRMモデルであること活かしたいので割愛します。

体の動き🕺

次に、VRMモデルの体の動きを制御したいです。

Unity では AnimationClip という形式でアニメーション情報を管理するのが一般的です。AnimationClip 形式でアセットストアで公開されていたりもするので、この形式を利用するのが最も無難でしょう。

ただ、AnimationClip を使う場合、一般的には AnimationController というステートマシンを作成し、そこにAnimationClipをならべて状態遷移図と遷移条件を定義し、遷移条件をスクリプトから制御してアニメーションを再生するという手法がとられています。これが個人的に面倒で、AnimationClip を再生したいだけなのに余計なステートマシン(AnimatonController)を構成するのが非常に手間だなぁと感じていました。

そこで、今回の制作では下記のように状態遷移図を必要とせずスクリプトで完結する形で実装しました。本格的なゲーム開発であればステートマシンを作ってアニメーションを制御した方が結果的に良いと思いますが、ボタンが押されたらこのポーズ!みたいな状況であればステートマシンをスクリプトで作ってしまい、そこで再生するだけで済ましてしまった方がスクリプトだけで完結して嬉しいです。

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
using UniVRM10;

public class SimpleClipPlayer : MonoBehaviour
{
    // Playables 全体を管理するグラフ
    PlayableGraph graph;

    // グラフの計算結果を Animator に流すための出口
    AnimationPlayableOutput output;

    /// <summary>
    /// VRM をロードした後に一度だけ呼ぶ初期化
    /// </summary>
    public void Setup(Vrm10Instance vrm)
    {
        // VRM に付いている Animator を取得
        // (AnimatorController は不要)
        var animator = vrm.GetComponent<Animator>();

        // PlayableGraph を作成(この時点ではまだ再生されない)
        graph = PlayableGraph.Create();

        // ゲーム時間に合わせてグラフの時間を進める
        graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);

        // グラフの出力先として Animator を指定
        output = AnimationPlayableOutput.Create(graph, "Animation", animator);

        // グラフ全体の再生を開始(これを呼ばないと何も動かない)
        graph.Play();
    }

    /// <summary>
    /// 指定した AnimationClip をそのまま再生する
    /// </summary>
    public void Play(AnimationClip clip)
    {
        if (clip == null) return;

        // AnimationClip を再生可能な Playable に変換
        var playable = AnimationClipPlayable.Create(graph, clip);

        // 再生位置を先頭にリセット
        playable.SetTime(0);

        // 出力の入力元をこの Playable に差し替える
        // (Animator にこの Clip が適用される)
        output.SetSourcePlayable(playable);
    }
}

上記のような関数をあらかじめ作り、インスペクターなどで AnimationClip を登録しておき、それを引数として呼び出すことで、インスタントに3Dモデルにモーションを設定することができます。下図にループでモーションを切り替えている例を示します。

Image from Gyazo

リップシンク👄

最後に、リップシンクを実装したくなります。

今回は、Unity で有名なリップシンクライブラリ uLipSync を利用しました。こちらも UPM Package でインポートしましょう。

  • git add URL...https://github.com/hecomi/uLipSync.git#upm

uLipSyncの使い方について最初は良く分かっていなかったのですが、有難いことにuLipSyncはサンプル実装が非常に充実しています。今回は、サンプル04. VRM10. Runtime Setup を参考に下記のようなuLipSyncセットアップ処理を実装しました。

using UnityEngine;
using System.Collections.Generic;
using UniVRM10;

/// <summary>
/// uLipSync を制御するコントローラー
/// </summary>
public class LipSyncController : MonoBehaviour
{
    [System.Serializable]
    public class PhonemeBlendShapeInfo
    {
        public string phoneme;
        public UniVRM10.ExpressionPreset expression;
    }

    [SerializeField] private uLipSync.Profile profile;
    [SerializeField] private List<PhonemeBlendShapeInfo> phonemeBlendShapeTable = new();
    private uLipSync.uLipSync _lipsync;
    private uLipSync.uLipSyncExpressionVRM _expression;

    /// <summary>
    /// LipSyncController のセットアップ
    /// </summary>
    /// <param name="vrmInstance"></param>
    public void Setup(Vrm10Instance vrmInstance)
    {
        // uLipSyncExpressionVRM をアタッチ
        _expression = vrmInstance.gameObject.AddComponent<uLipSync.uLipSyncExpressionVRM>();
        
        // phoneme を登録
        foreach (var info in phonemeBlendShapeTable)
        {
            _expression.AddBlendShape(info.phoneme, info.expression.ToString());
        }

        // uLipSync をアタッチ
        _lipsync = vrmInstance.gameObject.AddComponent<uLipSync.uLipSync>();
        
        // profile を登録
        _lipsync.profile = profile;

        // lipsync の更新関数を登録
        _lipsync.onLipSyncUpdate.AddListener(_expression.OnLipSyncUpdate);

        // AudioSource のアタッチ
        AudioSource = vrmInstance.gameObject.AddComponent<AudioSource>();
    }
}

やっていることは単純で、uLipSyncの設定ファイルをあらかじめ持っておき、それを動的にロードされたVRMモデルに設定しています。uLipSyncでは、uLipSyncコンポーネントをAudioSourceと同じGameObjectにアタッチして設定ファイルを設定さえしておけば、あとは良しなにuLipSyncがリップシンクをしてくれます。下図にリップシンクしている様子を示します。

Image from Gyazo

おわりに

いかかでしたでしょうか。本記事では、①VRMファイルのロード、②ロードしたVRMファイル制御(表情、モーション、リップシンク)の方法について、個人的なベストプラクティスを紹介いたしました。

UniVRMの利用は、ドキュメントが整備されてなかったりと大変なところも多いですが、3Dモデルをポータブルに利用できる仕様はとても素晴らしいです。本記事が皆さんの VRM ファイルを利用したアプリケーション制作の参考になれば幸いです。

おまけ

今回紹介した制御方法すべてを組み合わせると、下図のようにキャラクター制御できます!

Image from Gyazo

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?