Unityでブレンドシェイプを切り替えるスクリプトを動かしたのでメモ。VRMモデルの表情をキーで切り替え

Unity
※記事内に広告が含まれています。

バーチャル3Dクリエイター神部まゆみです(*^_^*)

この記事はUnityでブレンドシェイプを切り替えるスクリプトを動かしたのでメモです。

動作確認した最新バージョンはUnity 6000.3.1、VRoid Studio v2.7.0でエクスポートしたVRM1.0とVRM0.xのモデルです。

ブレンドシェイプだけではなくアニメーション自体を切り替える方法は↓こちらの記事に書きました。

Unityでアニメーションクリップを切り替えるスクリプトを書いた。速度も変更できる
Unityでアニメーションクリップを切り替えるスクリプトを書きました。速度も変更できます。

あとVRoidの場合は↓こちらの記事で表情ブレンドシェイプのパラメーターを画像付きでまとめておいたので参考になるかも。

VRoidの表情まとめ。パラメーター数値や組み合わせ方などを画像で紹介
VRoidの表情まとめです。パラメーター数値や組み合わせ方などを画像で紹介します。

●PRスペース●

〇まゆみマート|BOOTH

BOOTHでVRoidテクスチャやVRChat向けオブジェクトなどを販売しています。いいねしてくれると励みになります(*^-^*)

  1. VRM1.0の場合はVRM Instanceの設定を Update Typeなし にしたら動いた
    1. 追記:UniVRMのバージョンの問題の可能性も?
  2. ブレンドシェイプを切り替えるスクリプト VRMExpressionController.cs
    1. 使い方はモデルにアタッチしてFaceを指定、ブレンドシェイプ名を追加する
    2. 注意点:元の表情にプラスされる感じなので、無表情のアニメーションに使ったほうがいい
  3. 指定した秒数で表情遷移する改良版 VRMExpressionController2.cs
  4. 2025/10/12追記:複数のスキンメッシュレンダラーを指定し、キーも指定できる改良版 VRMExpressionController3.cs
  5. 2026/09/14追記:更に改良版 ロックするかのチェック、数値指定も
  6. 指定したオブジェクトに設定されてるブレンドシェイプをコンソールに出力するコード BlendShapeLister.cs
    1. 使い方:モデルにアタッチしてスキンメッシュレンダラーのあるオブジェクトを指定、実行ボタンを押す
  7. 追記:アニメーターコントローラーに不具合があると動かないことがあるっぽい?
    1. アニメーターコントローラーを新規に作ったら正常に動いた
    2. あとVRM Look At Headとか特定のスクリプトとは相性が悪い…
    3. 追記:Face以外のブレンドシェイプをスクリプトで操作できるか試してみるのも良さそう
  8. 追記:モーションキャプチャー+リップシンクと併用もできた
  9. おわりに

VRM1.0の場合はVRM Instanceの設定を Update Typeなし にしたら動いた

VRM0.xだとこのページのコードは問題なく動作しましたが、VRM1.0の場合はこれをしないと動かなかった。

VRM1 ブレンドシェイプ 動かない

こちらで解説してます。

UnityでVRM1.0モデルのブレンドシェイプが動かない時の対処法メモ
UnityでVRM1.0モデルのブレンドシェイプが動かない時の対処法メモです。

ついでにブレンドシェイプ自体がない時の対処法の記事も貼っておきます↓

unityでシェイプキー(ブレンドシェイプ)がない時の対処法。blenderで作ったシェイプキーが消えた
unityでシェイプキー(ブレンドシェイプ)がない時の対処法です。

追記:UniVRMのバージョンの問題の可能性も?

VRM1.0でもUniVRMのバージョンを変えたらUpdate Typeなしにしなくても普通に動いたため、単にUniVRMとUnityバージョンの相性の問題かも?

UniVRM 0.128.3とUnity6000.0.32f1の組み合わせだとVRM1のLate Updateでも普通にブレンドシェイプ動きました。

vrm1 ブレンドシェイプ 動かない

いったんUniVRMを削除してみて別のバージョンを入れれば普通に動く可能性があるかも?

しかし削除すると、UniVRMのバージョンによってはマテリアル設定し直しになったことがあったから自己責任だけど…。

ダメなら仕方ないのでUpdate Type なし でやるしかないですね…。今のところ特に不具合はないけども。

追記おわり。

ブレンドシェイプを切り替えるスクリプト VRMExpressionController.cs

ChatGPT氏に書いてもらいました。

VRMExpressionController.cs

using UnityEngine;

public class VRMExpressionController : MonoBehaviour
{
    [SerializeField] private SkinnedMeshRenderer faceMeshRenderer; // Faceメッシュ
    [SerializeField] private string[] blendShapeNames;             // ブレンドシェイプ名のリスト

    private void Update()
    {
        // キー入力で表情を切り替え
        if (Input.GetKeyDown(KeyCode.Alpha1)) // "1"キーで表情1
        {
            SetExpression(blendShapeNames[0]);
            Debug.LogWarning("1キーが押されました");
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2)) // "2"キーで表情2
        {
            SetExpression(blendShapeNames[1]);
            Debug.LogWarning("2キーが押されました");
        }
        // 必要に応じて他のキーも設定
    }

    private void SetExpression(string blendShapeName)
    {
        // すべてのブレンドシェイプをリセット
        for (int i = 0; i < faceMeshRenderer.sharedMesh.blendShapeCount; i++)
        {
            faceMeshRenderer.SetBlendShapeWeight(i, 0f);
        }

        // 指定したブレンドシェイプを適用
        int index = faceMeshRenderer.sharedMesh.GetBlendShapeIndex(blendShapeName);
        if (index >= 0)
        {
            faceMeshRenderer.SetBlendShapeWeight(index, 100f); // 値を100に設定(調整可能)
        }
        else
        {
            Debug.LogWarning($"ブレンドシェイプ '{blendShapeName}' が見つかりません。");
        }
    }
}

使い方はモデルにアタッチしてFaceを指定、ブレンドシェイプ名を追加する

モデルにアタッチしてスキンメッシュレンダラーのあるオブジェクトを指定、ブレンドシェイプ名を手動で追加します。

これでキー入力で表情を変えることに成功!

注意点:元の表情にプラスされる感じなので、無表情のアニメーションに使ったほうがいい

特にVRoidのJoyとかSurprisedなどの大口を開けるブレンドシェイプが設定されているアニメーションにこのスクリプトを使ってしまうと、そこから更に口を開けてしまい破綻する可能性が高いです。

なのでできるだけ無表情のアニメーションにスクリプトを使うことを推奨。

これリップシンク系のアセットを使う時も同じような問題が起きるんだけど、表情はアニメーションで表現するのか、スクリプトで動かすのか事前に決めてからアニメーション作った方が良いですね。

ブレンドシェイプを0にして初期化するスクリプトも試しに書いてみたんだけど、アニメーションがループしたら戻ってしまった。

指定した秒数で表情遷移する改良版 VRMExpressionController2.cs

上のスクリプトだと一瞬で表情が切り替わって違和感があったので、指定した秒数で表情遷移させるようにした。

using UnityEngine;
using System.Collections;

public class VRMExpressionController2 : MonoBehaviour
{
    [SerializeField] private SkinnedMeshRenderer faceMeshRenderer; // Faceメッシュ
    [SerializeField] private string[] blendShapeNames;             // ブレンドシェイプ名のリスト
    [SerializeField] private float transitionDuration = 0.5f;      // 切り替えにかかる時間(秒)

    private Coroutine transitionCoroutine;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1)) // "1"キーで表情1
        {
            StartExpressionTransition(blendShapeNames[0]);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2)) // "2"キーで表情2
        {
            StartExpressionTransition(blendShapeNames[1]);
        }
    }

    private void StartExpressionTransition(string blendShapeName)
    {
        if (transitionCoroutine != null)
        {
            StopCoroutine(transitionCoroutine);
        }
        transitionCoroutine = StartCoroutine(TransitionExpression(blendShapeName));
    }

    private IEnumerator TransitionExpression(string blendShapeName)
    {
        int newIndex = faceMeshRenderer.sharedMesh.GetBlendShapeIndex(blendShapeName);
        if (newIndex < 0)
        {
            Debug.LogWarning($"ブレンドシェイプ '{blendShapeName}' が見つかりません。");
            yield break;
        }

        // 現在のブレンドシェイプの値を取得
        float[] initialWeights = new float[faceMeshRenderer.sharedMesh.blendShapeCount];
        for (int i = 0; i < initialWeights.Length; i++)
        {
            initialWeights[i] = faceMeshRenderer.GetBlendShapeWeight(i);
        }

        float elapsedTime = 0f;
        while (elapsedTime < transitionDuration)
        {
            elapsedTime += Time.deltaTime;
            float t = elapsedTime / transitionDuration; // 0 から 1 へ補間

            // すべてのブレンドシェイプをリセット(徐々に0へ)
            for (int i = 0; i < initialWeights.Length; i++)
            {
                faceMeshRenderer.SetBlendShapeWeight(i, Mathf.Lerp(initialWeights[i], 0f, t));
            }

            // 目的のブレンドシェイプを徐々に適用
            faceMeshRenderer.SetBlendShapeWeight(newIndex, Mathf.Lerp(0f, 100f, t));

            yield return null;
        }

        // 最終的な状態を適用
        for (int i = 0; i < initialWeights.Length; i++)
        {
            faceMeshRenderer.SetBlendShapeWeight(i, 0f);
        }
        faceMeshRenderer.SetBlendShapeWeight(newIndex, 100f);
    }
}

ここで遷移間隔が指定でき、長い数値を指定するとゆっくり遷移します。

ブレンドシェイプ unity 切り替え

2025/10/12追記:複数のスキンメッシュレンダラーを指定し、キーも指定できる改良版 VRMExpressionController3.cs

これは複数のスキンメッシュレンダラーを指定出来て、キーも指定できる改良版です。

これが一番使いやすいかな?

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

[System.Serializable]
public class MeshBlendShapeSet
{
    public SkinnedMeshRenderer meshRenderer;
    public string[] blendShapeNames;
    public KeyCode[] keys; // 各ブレンドシェイプに対応するキー
}

public class VRMExpressionController3 : MonoBehaviour
{
    [SerializeField] private List<MeshBlendShapeSet> meshBlendShapeSets = new List<MeshBlendShapeSet>();
    [SerializeField] private float transitionDuration = 0.5f;

    private Coroutine transitionCoroutine;

    private void Update()
    {
        // 各メッシュごとにキー入力をチェック
        for (int m = 0; m < meshBlendShapeSets.Count; m++)
        {
            var set = meshBlendShapeSets[m];
            for (int i = 0; i < set.keys.Length; i++)
            {
                if (Input.GetKeyDown(set.keys[i]))
                {
                    StartExpressionTransition(m, i);
                }
            }
        }
    }

    private void StartExpressionTransition(int meshIndex, int expressionIndex)
    {
        if (transitionCoroutine != null)
        {
            StopCoroutine(transitionCoroutine);
        }
        transitionCoroutine = StartCoroutine(TransitionExpression(meshIndex, expressionIndex));
    }

    private IEnumerator TransitionExpression(int meshIndex, int expressionIndex)
    {
        var set = meshBlendShapeSets[meshIndex];
        var mesh = set.meshRenderer.sharedMesh;
        int targetIndex = mesh.GetBlendShapeIndex(set.blendShapeNames[expressionIndex]);
        if (targetIndex < 0)
        {
            Debug.LogWarning($"ブレンドシェイプ '{set.blendShapeNames[expressionIndex]}' が見つかりません。");
            yield break;
        }

        float[] initialWeights = new float[mesh.blendShapeCount];
        for (int i = 0; i < mesh.blendShapeCount; i++)
            initialWeights[i] = set.meshRenderer.GetBlendShapeWeight(i);

        float elapsed = 0f;
        while (elapsed < transitionDuration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / transitionDuration;

            for (int i = 0; i < mesh.blendShapeCount; i++)
                set.meshRenderer.SetBlendShapeWeight(i, Mathf.Lerp(initialWeights[i], 0f, t));

            set.meshRenderer.SetBlendShapeWeight(targetIndex, Mathf.Lerp(0f, 100f, t));
            yield return null;
        }

        for (int i = 0; i < mesh.blendShapeCount; i++)
            set.meshRenderer.SetBlendShapeWeight(i, 0f);
        set.meshRenderer.SetBlendShapeWeight(targetIndex, 100f);
    }
}

こんな感じで指定できます。

ブレンドシェイプ 切り替え script

2026/09/14追記:更に改良版 ロックするかのチェック、数値指定も

これまでのスクリプトだとブレンドシェイプを切り替えると前のブレンドシェイプは0にリセットされる仕様だったので、0にせずロックするか選べるようにした。

あとブレンドシェイプの数値が100のみだと使いにくかったので数値指定もできるようにしました。

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

[System.Serializable]
public class BlendShapeKeySetting
{
    [Tooltip("操作するブレンドシェイプ名")]
    public string blendShapeName;


[Tooltip("このブレンドシェイプを切り替えるキー")]
    public KeyCode key;

    [Tooltip("ONの場合、他のブレンドシェイプを切り替えても維持する")]
    public bool isLocked;

    [Tooltip("このブレンドシェイプをONにした時の値")]
    [Range(0f, 100f)]
    public float targetWeight = 100f;


}

[System.Serializable]
public class MeshBlendShapeSet
{
    [Tooltip("操作するSkinned Mesh Renderer")]
    public SkinnedMeshRenderer meshRenderer;


[Tooltip("ブレンドシェイプ設定")]
    public List<BlendShapeKeySetting> blendShapeSettings =
    new List<BlendShapeKeySetting>();


}

public class VRMExpressionController3 : MonoBehaviour
{
    [SerializeField]
    private List<MeshBlendShapeSet> meshBlendShapeSets =
    new List<MeshBlendShapeSet>();


[SerializeField]
    private float transitionDuration = 0.5f;

    private Coroutine transitionCoroutine;

    private void Update()
    {
        // 各メッシュごとにキー入力をチェック
        for (int m = 0; m < meshBlendShapeSets.Count; m++)
        {
            var set = meshBlendShapeSets[m];

            if (set.meshRenderer == null)
                continue;

            for (int i = 0;
                 i < set.blendShapeSettings.Count;
                 i++)
            {
                var setting =
                    set.blendShapeSettings[i];

                if (Input.GetKeyDown(setting.key))
                {
                    StartExpressionTransition(m, i);
                }
            }
        }
    }

    private void StartExpressionTransition(
        int meshIndex,
        int expressionIndex
    )
    {
        if (transitionCoroutine != null)
        {
            StopCoroutine(transitionCoroutine);
        }

        transitionCoroutine =
            StartCoroutine(
                TransitionExpression(
                    meshIndex,
                    expressionIndex
                )
            );
    }

    private IEnumerator TransitionExpression(
        int meshIndex,
        int expressionIndex
    )
    {
        var set =
            meshBlendShapeSets[meshIndex];

        if (set.meshRenderer == null)
            yield break;

        if (expressionIndex < 0 ||
            expressionIndex >=
            set.blendShapeSettings.Count)
        {
            yield break;
        }

        var setting =
            set.blendShapeSettings[
                expressionIndex
            ];

        var mesh =
            set.meshRenderer.sharedMesh;

        if (mesh == null)
            yield break;

        int targetIndex =
            mesh.GetBlendShapeIndex(
                setting.blendShapeName
            );

        if (targetIndex < 0)
        {
            Debug.LogWarning(
                $"ブレンドシェイプ " +
                $"'{setting.blendShapeName}' " +
                $"が見つかりません。"
            );

            yield break;
        }

        // Inspectorで指定した値
        float targetWeight =
            Mathf.Clamp(
                setting.targetWeight,
                0f,
                100f
            );

        float currentTargetWeight =
            set.meshRenderer.GetBlendShapeWeight(
                targetIndex
            );

        // ==================================
        // ロック中で、すでにONならOFFにする
        // ==================================

        if (setting.isLocked &&
            currentTargetWeight > 0.01f)
        {
            yield return StartCoroutine(
                TransitionSingleBlendShape(
                    set.meshRenderer,
                    targetIndex,
                    currentTargetWeight,
                    0f
                )
            );

            yield break;
        }

        // ==================================
        // 現在のBlendShape値を保存
        // ==================================

        float[] initialWeights =
            new float[
                mesh.blendShapeCount
            ];

        for (int i = 0;
             i < mesh.blendShapeCount;
             i++)
        {
            initialWeights[i] =
                set.meshRenderer
                    .GetBlendShapeWeight(i);
        }

        float elapsed = 0f;

        // ==================================
        // フェード処理
        // ==================================

        while (elapsed < transitionDuration)
        {
            elapsed += Time.deltaTime;

            float t =
                Mathf.Clamp01(
                    elapsed /
                    transitionDuration
                );

            for (int i = 0;
                 i < mesh.blendShapeCount;
                 i++)
            {
                bool keepLocked =
                    ShouldKeepLockedBlendShape(
                        set,
                        mesh,
                        i,
                        targetIndex
                    );

                // ロック対象は維持する
                if (!keepLocked)
                {
                    set.meshRenderer
                        .SetBlendShapeWeight(
                            i,
                            Mathf.Lerp(
                                initialWeights[i],
                                0f,
                                t
                            )
                        );
                }
            }

            // 選択したBlendShapeを
            // 指定した数値へ
            set.meshRenderer
                .SetBlendShapeWeight(
                    targetIndex,
                    Mathf.Lerp(
                        currentTargetWeight,
                        targetWeight,
                        t
                    )
                );

            yield return null;
        }

        // ==================================
        // 最終状態
        // ==================================

        for (int i = 0;
             i < mesh.blendShapeCount;
             i++)
        {
            bool keepLocked =
                ShouldKeepLockedBlendShape(
                    set,
                    mesh,
                    i,
                    targetIndex
                );

            if (!keepLocked)
            {
                set.meshRenderer
                    .SetBlendShapeWeight(
                        i,
                        0f
                    );
            }
        }

        // 選択したBlendShapeを
        // 指定値にする
        set.meshRenderer
            .SetBlendShapeWeight(
                targetIndex,
                targetWeight
            );
    }

    // ==================================
    // 指定BlendShapeが
    // ロック状態で維持対象か判定
    // ==================================

    private bool ShouldKeepLockedBlendShape(
        MeshBlendShapeSet set,
        Mesh mesh,
        int blendShapeIndex,
        int targetIndex
    )
    {
        // 今回選択したBlendShapeは
        // 別処理で設定する
        if (blendShapeIndex == targetIndex)
            return false;

        string blendShapeName =
            mesh.GetBlendShapeName(
                blendShapeIndex
            );

        // 設定一覧から
        // 同じBlendShapeを探す
        foreach (
            var setting
            in set.blendShapeSettings
        )
        {
            if (setting.blendShapeName ==
                    blendShapeName &&
                setting.isLocked)
            {
                // ロックされていて
                // 現在値が0より大きければ維持
                if (set.meshRenderer
                        .GetBlendShapeWeight(
                            blendShapeIndex
                        ) > 0.01f)
                {
                    return true;
                }
            }
        }

        return false;
    }

    // ==================================
    // 1つのBlendShapeだけを
    // フェード切り替え
    // ==================================

    private IEnumerator TransitionSingleBlendShape(
        SkinnedMeshRenderer renderer,
        int blendShapeIndex,
        float from,
        float to
    )
    {
        float elapsed = 0f;

        while (elapsed < transitionDuration)
        {
            elapsed += Time.deltaTime;

            float t =
                Mathf.Clamp01(
                    elapsed /
                    transitionDuration
                );

            renderer.SetBlendShapeWeight(
                blendShapeIndex,
                Mathf.Lerp(
                    from,
                    to,
                    t
                )
            );

            yield return null;
        }

        renderer.SetBlendShapeWeight(
            blendShapeIndex,
            to
        );
    }


}
ブレンドシェイプ 切り替え

指定したオブジェクトに設定されてるブレンドシェイプをコンソールに出力するコード BlendShapeLister.cs

これスキンメッシュレンダラーのところに書いてあるけど、手入力だと間違うのでコピペできるように作った。

BlendShapeLister.cs

using UnityEngine;

public class BlendShapeLister : MonoBehaviour
{
    [SerializeField] private SkinnedMeshRenderer faceMeshRenderer; // Faceメッシュ

    private void Start()
    {
        if (faceMeshRenderer == null)
        {
            Debug.LogError("SkinnedMeshRendererが設定されていません!");
            return;
        }

        Mesh mesh = faceMeshRenderer.sharedMesh;
        if (mesh == null)
        {
            Debug.LogError("SkinnedMeshRendererにMeshが設定されていません!");
            return;
        }

        Debug.Log($"ブレンドシェイプの数: {mesh.blendShapeCount}");

        for (int i = 0; i < mesh.blendShapeCount; i++)
        {
            string blendShapeName = mesh.GetBlendShapeName(i);
            Debug.Log($"ブレンドシェイプ[{i}]: {blendShapeName}");
        }
    }
}

使い方:モデルにアタッチしてスキンメッシュレンダラーのあるオブジェクトを指定、実行ボタンを押す

これでコンソールに出力されるのでコピペできます。

追記:アニメーターコントローラーに不具合があると動かないことがあるっぽい?

このスクリプトたまに使うんだけど、モデルによってスムーズに動く時もあれば、モデルが違うと全く動かない時もあっておかしいなと思っていた。

どうもアニメーターファイルに不具合があると動かないことがあるっぽい?↓だと同じアニメーションを読み込んだ別のアニメーターを指定しただけで動くようになりました。

↑では普通に動いたけど、VRM Humanoid Desriptionをオフにすると動いたりした。

こんなのあるのか…?(-_-;)

あとは動いたモデルのアバターファイルとアニメーターに切り替えたら動いたとかもありましたね。

もし動かなければそのあたりをいじってみると動作するかもしれません。

アニメーターコントローラーを新規に作ったら正常に動いた

その後、アニメーターコントローラーを新規に作成して使ったら普通に動きました。

アニメーターコントローラーが壊れていたとか?ChatGPTに聞いてみたけどよくわからないっぽいです。

まぁこういうこともあるよってことで。

あとVRM Look At Headとか特定のスクリプトとは相性が悪い…

あとVRM Look At Headとは相性が悪いっぽい?Update Typeなしにしたら動いたけど。

unity ブレンドシェイプ 動かない

他にもLook Animatorっていう似たようなアセットでも動作がちょっと微妙な感じだったので、競合する可能性がありそうです。

追記:Face以外のブレンドシェイプをスクリプトで操作できるか試してみるのも良さそう

なぜかVRoidのFaceの表情ブレンドシェイプだけこのスクリプトで操作できないことがちょくちょくあるんだけど、外付けで仕込んだ舌オブジェクトや、Blenderで改変してBodyに仕込んだブレンドシェイプはちゃんと動いたりした。

FaceはVRMのシステム的に結構重要っぽいのか、内部的に何か別の動きがされているのか?なんか動かないことがありますね…。

問題の切り分けがしやすくなるので、他のメッシュのブレンドシェイプは動くか試してみるのも良さげ。

追記:モーションキャプチャー+リップシンクと併用もできた

このスクリプトはFace以外にもシェイプキーがあるオブジェクトは全て制御できるため、↓のように赤面をキーでコントロールしたりもできます。

ちなみにこれはEVMC4UってやつとXR Animatorって言うモーションキャプチャーアプリでやりました。

EVMC4UをXR Animatorで動かしてみたメモ。Unityの世界に入ってモーションキャプチャーができるだと…Σ(゚Д゚)
EVMC4UをXR Animatorで動かしてみた記事です。Unityの世界に入ってモーションキャプチャーができました。

んーこれ普通にシチュエーション系の動画とか作れそうだ…。

おわりに

ブレンドシェイプを切り替えられると便利なので作ってみました。

参考になればつかってみてください。

また何かあれば追記します(*^_^*)

タイトルとURLをコピーしました