simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:形態素解析してしゃべるVRMのサンプル

■前書き
文字列を受け取って、読みに変換し
あいうえおの母音に直した後、これを順に処理しながら
VRM の、ブレンドシェイプに繋げます。

小ゴールを切りながら、VRM キャラがしゃべる様子を確認できればゴールです。

■読みの抽出

公式ページ
MeCab: オリジナル辞書/コーパスからのパラメータ推定
より

ipadic の例です. 素性列として

品詞
品詞細分類1
品詞細分類2
品詞細分類3
活用型
活用形
基本形
読み
発音
が定義されています.

ということは、こうですね。

using NMeCab;
using UnityEngine;
using UnityEngine.UI;

public class NMeCabTest : MonoBehaviour
{
    public Text text;

    void Start()
    {
        string result = "";
        string sentence = "Unityで形態素解析";

        MeCabParam param = new MeCabParam();
        param.DicDir = $"{Application.streamingAssetsPath}/NMeCab/dic/ipadic";

        var t = MeCabTagger.Create(param);
        MeCabNode node = t.ParseToNode(sentence);
        while (node != null)
        {
            if (node.CharType > 0)
            {
                var splits = node.Feature.Split(',');
                var yomi = splits.Length < 8 ? node.Surface : splits[7];
                result += yomi + " ";
            }
            node = node.Next;
        }
        this.text.text = result;
    }
}

バッチリ動きました。

f:id:simplestar_tech:20190512180141p:plain
Unityで形態素解析

■あいうえお ニュートラル の 6 つの値に直す

ケイタイソ→エイアイオ
に直す変換です。

Japanese Katakana Unicode Chart
sites.psu.edu

ここによれば 12449 ~ 12539 の int の値を見れば、対応表が作れそうですね。

                var c = yomi.ToCharArray();
                int[] arr = new int[c.Length];
                for (int i = 0; i < c.Length; i++)
                {
                    arr[i] = (int)c[i];
                    result += arr[i].ToString();
                }

確かに、表と同じ値が確認できました。
マップを作る

12483 ッ
12484 ツ
12485 ヅ

のところだけ規則性から外れてしまう

12490 ナ
な行だけは、濁点がない

12495 ハ
は行は ハ、バ、パ の三文字単位で特殊

12510 ま行もまた、一つ

12516 ヤ
ユヨ はこれまでの5 文字連続から外れて、三文字

12521 ラ
ラ行もまた 一つ

12526 ワ
以降はもう、特殊文字として、一つ一つ、アイウエオ割り当ててあげましょう。

   int YomiToAIUEO(int yomi)
    {
        if (12449 <= yomi && 12539 > yomi)
        {
            if (12484 >= yomi)
            {
                return ((yomi - 12449) / 2) % 5;
            }
            else if (12485 == yomi)
            {
                return 2;
            }
            else if (12490 > yomi)
            {
                return ((yomi - 12486) / 2) % 5 + 3;
            }
            else if (12495 > yomi)
            {
                return (yomi - 12490) % 5;
            }
            else if (12510 > yomi)
            {
                return ((yomi - 12495) / 3) % 5;
            }
            else if (12515 > yomi)
            {
                return (yomi - 12490) % 5;
            }
            else if (12521 > yomi)
            {
                return ((yomi - 12515) / 2) * 2;
            }
            else if (12526 > yomi)
            {
                return (yomi - 12521) % 5;
            }
            else
            {
                switch (yomi)
                {
                    case 12526:
                    case 12527:
                        return 0;
                    case 12528:
                    case 12529:
                        return 3;
                    case 12530:
                        return 4;
                    case 12532:
                        return 2;
                    case 12533:
                    case 12534:
                    case 12535:
                        return 0;
                    case 12536:
                    case 12537:
                        return 3;
                    case 12538:
                        return 4;
                    default:
                        break;
                }
            }
        }
        return 6;
    }

    string DebugYomi(int aiueo)
    {
        switch (aiueo)
        {
            case 0:
                return "あ";
            case 1:
                return "い";
            case 2:
                return "う";
            case 3:
                return "え";
            case 4:
                return "お";
            default:
                break;
        }
        return "ん";
    }

ローマ字を処理してほしいところです。

}
        else if (65 <= yomi && 123 > yomi)
        {
            switch (yomi)
            {
                case 65:
                case 97:
                    return 0;
                case 73:
                case 105:
                    return 1;
                case 85:
                case 117:
                case 87:
                case 119:
                    return 2;
                case 69:
                case 101:
                    return 3;
                case 79:
                case 111:
                    return 4;
                case 78:
                case 110:
                    return 6;
                default:
                    return 7;
            }
        }
        return 7;

正しく動きました。

VRMの口を動かす

あいうえお の番号をキューに詰めて、毎秒取り出してきて、これに同期してブレンドシェイプを指定して動かせるか試します。

このコードでひとまず口は動かせました。(ブレンド値を滑らかにつなぐなどの調整が必要だけど、今回はそこまでやらない)

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using VRM;

public class LipSyncTest : MonoBehaviour
{
    [SerializeField] GameObject vrmRoot;
    [SerializeField] Text text;
    [SerializeField] NMeCabTest nMeCabTest;
    [SerializeField] float wordTime = 0.3f;

    VRMBlendShapeProxy proxy;
    BlendShapeKey[] aiueoKey = new BlendShapeKey[5];
    Queue<int> aiueoQueue = new Queue<int>();
    float prevTime = 0;

    float fade = 0;

    // Start is called before the first frame update
    void Start()
    {
        this.proxy = vrmRoot.GetComponent<VRMBlendShapeProxy>();
        for (int aiueo = 0; aiueo < this.aiueoKey.Length; aiueo++)
        {
            this.aiueoKey[aiueo] = new BlendShapeKey
            {
                Preset = BlendShapePreset.A + aiueo
            };
        }
    }

    // Update is called once per frame
    void Update()
    {
        float time = Time.realtimeSinceStartup - this.prevTime;
        int sayWord = -1;
        if (time >= this.wordTime)
        {
            this.prevTime = Time.realtimeSinceStartup;

            if (0 < this.aiueoQueue.Count)
            {
                sayWord = this.aiueoQueue.Dequeue();
            }
        }
        
        if (Input.GetKey(KeyCode.A))
        {
            sayWord = 0;            
        }
        if (Input.GetKey(KeyCode.I))
        {
            sayWord = 1;
        }
        if (Input.GetKey(KeyCode.U))
        {
            sayWord = 2;
        }
        if (Input.GetKey(KeyCode.E))
        {
            sayWord = 3;
        }
        if (Input.GetKey(KeyCode.O))
        {
            sayWord = 4;
        }
        for (int aiueo = 0; aiueo < this.aiueoKey.Length; aiueo++)
        {
            float lastKey = this.proxy.GetValue(aiueoKey[aiueo]);
            this.proxy.AccumulateValue(aiueoKey[aiueo], sayWord == aiueo ? 1 : lastKey * 0.95f);
        }
        this.proxy.Apply();
    }

    public void OnSayClick()
    {
        var sentence = text.text;
        nMeCabTest.ReturnAiueo(sentence, ref aiueoQueue);
    }
}

できた。