■前書き
文字列を受け取って、読みに変換し
あいうえおの母音に直した後、これを順に処理しながら
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; } }
バッチリ動きました。
■あいうえお ニュートラル の 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); } }
できた。
NMeCab のやつ #Unity 上で入力した文をその場で読み上げる口の動きを確認できました。
— Simplestar (@lpcwstr) May 12, 2019
キャラデータ(VRMファイル)は「童田明治-わらべだめいじー- ファンアート」をお借りしてます。https://t.co/9kMIZhNfqH
裏で辞書引いてるから語彙力だけなら全人類を上回ります。かしこい#いめいじー pic.twitter.com/i3sVUTIm8M