TwinMakerで3Dの人間に走るモーションをさせてみる

> 6/22追記 -> 6/23再追記
> MMDのモデルにTwinMaker内で踊ってもらう方法について追記しました

> 7/2追記
> MMDモデルの白飛び対応について追記、画像とGIFを更新しました

# この記事について

- AWSのTwinMaker上で3Dモーションを再生する方法を紹介します
※下のGIFのようになります

![twinmaker-animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/d49ea58a-7a78-6a5d-b932-54638ee2b635.gif)

- また、MMDに踊ってもらう方法を紹介します

![gif-image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/daed1c6f-750b-1ac4-75ad-d8bd8dcc1dc9.gif)

**どうやって？**

iot-app-kitで、SceneComposerの外側からThree.jsのシーンオブジェクトを参照して、アニメーション付きのGLTFを配置します

**何が嬉しいの？**

- 3Dソフトを覚えることなく、ぐりぐりとアニメーションするWebアプリを作ることができます

**注意**

この記事の方法は非公開関数を使ったハックです。将来的に実現方法が変わる可能性、ふさがれる可能性がありますので、業務で利用される際は事前の検証をお願いいたします

# 前準備

iot-app-kitの構築が終わっているところから手順を説明しますので、TwinMakerはどう操作すればいいか、iot-app-kitの構築方法はどうすればいいかについては、以前書いた記事をご参照ください。

【TwinMakerの基本的な使い方についての記事】
https://qiita.com/ShotaOki/items/612811ef34317f033911

【iot-app-kitの構築についての記事】
https://qiita.com/ShotaOki/items/fe4ab8eba816ca3ec4c1

# 実装方法

ソースはこちらです。gitにある以下の2つのファイルを、iot-app-kitのApp.tsxのあるフォルダに配置してください。
- App.tsx
- AppendScene.ts

https://github.com/ShotaOki/TwinMakerAnimationSample

**実装方法の説明**

TwinMakerをiot-app-kitで構築すると、以下のようなコンポーネントを作っていると思います。

```react: App.tsx
import "./App.css";
import { initialize } from "@iot-app-kit/source-iottwinmaker";
import { SceneViewer } from "@iot-app-kit/scene-composer";

function App() {
  // TwinMakerのシーンを読み込む
  const sceneLoader = initialize("WorkSpaceName", { // ワークスペース名を設定
    awsCredentials: { // 認証情報を設定
      accessKeyId: "XXXXXXXXXXXXXXXXXXXXXXXXXx",
      secretAccessKey: "XXXXXXXXXXXXXXXXXXXXXXXXX",
    },
    awsRegion: "REGION_NAME", // リージョン名を設定
  }).s3SceneLoader("SceneName"); // シーン名を設定

  // 
  return (
    <div className="App">
      <SceneViewer
        sceneLoader={sceneLoader}
      />
    </div>
  );
}

export default App;
```

このテンプレートに以下の処理を書き加えることで、TwinMakerの3Dオブジェクトを直接参照できるようになります。

```react
import { useStore } from "@iot-app-kit/scene-composer/dist/src/store";

function App() {
  // 中略...

  // 任意のコンポーザーID（useStoreで参照する情報と、SceneViewerが使っている情報を合わせるために、何かしらの定数を作ります）
  const composerId = "abcdef-eeggff";
  // TwinMaker（クラウド側）の画面構成情報を参照する(※nodeMap＝S3にあるJsonデータのこと)
  const nodeMap = useStore(composerId)((state) => state.document.nodeMap);
  // Jsonのタグ情報に紐づいた3Dオブジェクトを参照する
  const getObject3DBySceneNodeRef = useStore(composerId)(
    (state) => state.getObject3DBySceneNodeRef
  );

  // 中略...
  // コンポーザーIDを固定する
  return (
    <div className="App">
      <SceneViewer
        sceneComposerId={composerId}
        sceneLoader={sceneLoader}
      />
    </div>
  );
}
```

`nodeMap`はTwinMakerが管理しているツリー情報、`getObject3DBySceneNodeRef`関数は、ツリー情報からThree.jsの3Dオブジェクトを直接参照する関数です。

以下のようにすれば、TwinMakerを動かしているThree.jsの基盤にアクセスできます。

```typescript: ルートになるシーンを参照する関数
/** 再帰でルートシーンを取得する関数 */
function findRootScene (target: THREE.Object3D<THREE.Event> | undefined) {
  if (target === undefined) {
    return undefined;
  }
  let current: THREE.Object3D<THREE.Event> = target;
  while (current.parent !== undefined && current.parent !== null) {
    current = current.parent as THREE.Object3D<THREE.Event>;
  }
  return current;
};

// ※documentにアクセス可能になるまで待機する
// アクセスできるようになったら、documentから3Dシーンを取得する
for (let ref of Object.keys(nodeMap)) {
  // オブジェクトを参照する
  const object3D = getObject3DBySceneNodeRef(ref);
  // シーンオブジェクトを取得する（※このrootSceneはThree.jsのSceneインスタンスです）
  const rootScene = findRootScene(object3D) as THREE.Scene;
}
```

あとはrootSceneにGLTFLoaderで取得したアニメーション付きのモデルを配置するだけです。

```typescript
    // アニメーションミキサーを定義
    let mixer: THREE.AnimationMixer;

    // GLTFを読み込む
    const loader = new GLTFLoader();
    // 3Dモデルはhttps://threejs.org/examples/#webgl_animation_skinning_additive_blendingのモデルを使います
    loader.loadAsync("Xbot.glb").then((mesh) => {
      rootScene.add(mesh.scene);
      
      mixer = new THREE.AnimationMixer(mesh.scene);
      mesh.animations.forEach((clip) => {
        if (clip.name === 'run') {
          mixer.clipAction( clip ).play();
        }
      });
    });
    
    // アニメーションを実行する
    const clock = new THREE.Clock();
    function animate() {
      requestAnimationFrame(animate);
      
      var delta = clock.getDelta();
      if (mixer) mixer.update(delta);
    }
    animate();
```

# まとめ

TwinMakerを使うと、アニメーションさせるキャラクター以外はノーコードで作ることができます。Reactのソースが出てくるだけで、Unityのようなゲーム向けの開発環境も出てきません。

0からごりごりと作るよりも、TwinMakerで作るほうが楽に入っていける方も多いのではないでしょうか。

**TwinMakerだけではアニメーションできないの？**

はい。アニメーションのついたGLTFを読み込ませてもアニメーションは実行されません。
iot-app-kitが必要です。

**ところでMMDは読み込める？**

~~現時点では無理なようです。MMDLoaderをTwinMakerの中で実行するとエラーになります。~~
問題なく読み込むことができます。（6/23追記）

MMDモデル（兎田ぺこらさん）
![pekora-dance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/67e15290-7337-b123-73d5-dd5dadf5d608.png)

※以降、2023/6/23追記
依存関係を修正することで、問題なくMMDを読み込むことができます。
package.jsonに、以下のdependenciesを指定してください

> ※iot-app-kitの6.2.0がthree@0.139.0に依存しているため、**全体的に古めのバージョン**になります。three-mesh-bvhなど、そのままインストールしてしまうと新しいバージョンで入ってしまうライブラリがあるため、package.jsonでバージョン指定してインストールさせます。

```json: package.json
{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@iot-app-kit/components": "^6.2.0",
    "@iot-app-kit/react-components": "^6.2.0",
    "@iot-app-kit/scene-composer": "^3.3.0",
    "@iot-app-kit/source-iottwinmaker": "^6.2.0",
    "@react-three/drei": "^9.44.1",
    "@react-three/fiber": "^8.13.3",
    "three-mesh-bvh": "^0.5.15",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.35",
    "@types/react": "^18.2.11",
    "@types/react-dom": "^18.2.4",
    "jsx-runtime": "^1.2.0",
    "node-polyfill-webpack-plugin": "^2.0.1",
    "path-browserify": "^1.0.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "sass": "^1.63.3",
    "three": "^0.139.0",
    "three-stdlib": "^2.18.1",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-app-rewired  start",
    "build": "react-app-rewired  build",
    "test": "react-app-rewired  test",
    "eject": "react-app-rewired  eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      "last 1 chrome version"
    ],
    "development": [
      "last 1 chrome version"
    ]
  },
  "devDependencies": {
    "@types/three": "^0.139.0",
    "react-app-rewired": "^2.2.1"
  }
}
```

MMDを読み込むソースはこちらです。

2023/07/02追記: 
そのままMMDを読み込むと白飛びしてしまうため、色味を調整する処理を追加しました。

```typescript: MMDを読み込むソース
import * as THREE from "three";
import { MMDLoader } from "three/examples/jsm/loaders/MMDLoader";
import { RootState } from "@react-three/fiber";

/**
 * 2023/07/02追記
 * R3FのStateを取得する(GLRenderer、Scene、Cameraが取得できる)
 */
function getState(rootScene: THREE.Scene): RootState {
  const d3fScene: any = rootScene;
  return d3fScene.__r3f.root.getState() as RootState;
}

/**
 * 2023/07/02追記
 * TwinMakerのシーン描画(色の描画)をMMDに合わせて調整する
 */
function setupSceneForMMD(gl: THREE.WebGLRenderer) {
  gl.shadowMap.enabled = true;
  gl.shadowMap.type = THREE.PCFSoftShadowMap;
  // LinearEncodingにすると色彩が強くなる
  gl.outputEncoding = THREE.LinearEncoding;
  gl.toneMapping = THREE.LinearToneMapping;
}

/** MMDをTwinMakerのシーンに読み込む */
function AttachMMDFunction(scene: THREE.Scene) {
  scene.add(new THREE.AmbientLight(0xffffff, 0.6));

  // 2023/07/02追記========================
  // RendererをMMDに合わせて最適化する
  const { gl } = getState(scene);
  setupSceneForMMD(gl);
  // 2023/07/02追記========================

  const loader = new MMDLoader();
  const animationLoader = new MMDLoader();
  let mixer: THREE.AnimationMixer;
  loader.loadAsync("${PMXのファイル名}").then((mesh) => {
    mesh.scale.set(0.088, 0.088, 0.088);
    mesh.position.set(0, 0.1, 2.0);
    mesh.castShadow = true;
    mesh.receiveShadow = true;

    mixer = new THREE.AnimationMixer(mesh);

    for (let m of mesh.material as THREE.Material[]) {
      let ma: any = m;
      ma.emissive.multiplyScalar(0.1);
      ma.userData.outlineParameters.thickness = 0.001;
      ma.needsUpdate = true;
    }

    scene.add(mesh);

    animationLoader.loadAnimation(
      "${モーションのファイル名}",
      mesh,
      (motion) => {
        mixer.clipAction(motion as THREE.AnimationClip).play();
      }
    );

    // アニメーションを実行する
    const clock = new THREE.Clock();
    function animate() {
      requestAnimationFrame(animate);

      var delta = clock.getDelta();
      if (mixer) mixer.update(delta);
    }
    animate();
  });
}
```

MMDのモデルはこちらをお借りしています（兎田ぺこら【公式】）
https://3d.nicovideo.jp/works/td88332

ダンスモーションはこちらをお借りしています（ニコニ立体ちゃん特設サイト）
https://3d.nicovideo.jp/alicia/