Three-VRMで組み合わせて表情を登録する(Custom Expression)

# 概要

Three-VRMでプリセットの表情ではなく、パーツごとに組み合わせた表情(眉毛、目、口などでそれぞれ異なった表情にする)を反映させてみます。プリセットの表情は特に設定なしに使うことができますが、パーツごとに変える場合はCustom Expressionとしての登録が必要となります。

Unityなどでブレンドシェイプを作成することで実現する方法もあるみたいですが(試してませんが)、ここでは、JavaScriptの実行時にプログラム的に設定し使用します。

# 動作確認

- VRoidStudio 1.29. 1 -- VRMファイルのエクスポート
- Three-VRM 3.1.2
- Firefox 131

# 実装
## プリセットの使用

Three-VRMでブラウザ上でVRMを非常に簡単に扱うことができますが、`expressionManager`経由で表情を反映させることができます。以降のコードでは、変数`vrm`は`VRM`オブジェクトとします。VRMファイルの読み込みはThree-VRMのサンプルスクリプト等を参照してください。ここで扱うコードはVRMオブジェクトがある前提です。

Three-VRM 2.0.0より前は`blendShapeProxy`でしたが、2.0.0以降は`expressionManager`経由で表情を操作するようです。

```javascript
// プリセット名の確認
vrm.expressionManager.expressionMap
// Object { happy: {…}, angry: {…}, sad: {…}, relaxed: {…}, surprised: {…}, aa: {…}, ih: {…}, ou: {…}, ee: {…}, oh: {…}, … }

// プリセットの使用
vrm.expressionManager.setValue("relaxed", 0.8);
```

`vrm.expressionManager.setValue()`はプリセットだけではなく、今回登録するCustom Expressionにも使うのですが、登録する前はプリセットしかありません。

## カスタム表情の追加

上記のようにプリセットの表情(happy, angry, sad, relaxed, surprisedおよび口の形)はそのまま使えるのですが、VRoidStudioのようにそれぞれのパーツの表情を組み合わせたいことは多いと思います。その場合、`VRMExpression`オブジェクトを作成し、`expressionManager.registerExpression()`を使って追加登録します。

なお、この部分はThree-VRM 3.1.2の`packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts`の143行目付近のコードを参考にしています。そこで表情の登録をしているので、一連の流れを追えばさらに詳細が理解できると思います。

```javascript
// カスタム表情の追加登録
const expressionName = "smile";        // 表情の登録名
const targetName     = "Fcl_MTH_Fun";  // 反映させるターゲット
const targetWeight   = 0.8;            // ウェイト

const expression = new VRMExpression(expressionName);
vrm.scene.add(expression);

// MorphTargetBind.primitivesに登録するのはFaceグループのSkinnedMesh
const primitives = [...vrm.scene.getObjectByName("Face").children];

// FaceグループのSkinnedMesh#morphTagetDictionaryから、目的のmorphTargetIndexを得る
// FaceグループにはFace_(merged)などのSkinnedMeshが登録されている
const morphTargetDictionary = vrm.scene.getObjectByName("Face").children[0].morphTargetDictionary;

// バインドする
// 他のパーツも足していくことができる
expression.addBind(
    new VRMExpressionMorphTargetBind({
        primitives,
        index: morphTargetDictionary[targetName],
        weight: targetWeight,
    })
);

// Expressionを登録する(customExpressionMapに追加される)
vrm.expressionManager.registerExpression(expression);
```

このコード片では、`Fcl_MTH_Fun`すなわち、口の「楽しい」にウェイトをかけています。`VRMExpression`を初期化する際に渡す名前で`expressionManager.customExpressionMap`に登録されます(ここでは`smile`)。ここでは`Fcl_MTH_Fun`のみを`addBind()`していますが、`addBind()`は複数できるので、例えば`Fcl_BRW_Fun`、`Fcl_EYE_Spread`などを組み合わせることができます。使用できるターゲット名は、このコード中の`vrm.scene.getObjectByName("Face").children[0].morphTargetDictionary`で得ることができます。

登録した表情を使うのはプリセットと同様です。

```javascript
vrm.expressionManager.setValue("smile", 1.0);
```

このようにすることで、様々な表情を簡単に作ることができます。

# 参考

- GitHub Three-VRM
    - https://github.com/pixiv/three-vrm
    - Examplesに簡単な使い方コードがあるのでまずはそこから
- GitHub Three-VRM VRMExpressionLoaderPlugin.ts:143
    - https://github.com/pixiv/three-vrm/blob/add5746f2391fbb6baa118a7211462fbc0fad9d3/packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts#L143
- VRM1.0でランタイム実行時にカスタムクリップを追加できるようにする
    - https://qiita.com/lumis/items/5739e178afded19aa6da
- VRM仕様 VRMC_vrm: expression
    - https://vrm.dev/vrm1/expression/