1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-20

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

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

この記事について

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

twinmaker-animation.gif

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

gif-image.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

実装方法の説明

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

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オブジェクトを直接参照できるようになります。

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の基盤にアクセスできます。

ルートになるシーンを参照する関数
/** 再帰でルートシーンを取得する関数 */
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で取得したアニメーション付きのモデルを配置するだけです。

    // アニメーションミキサーを定義
    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

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

※iot-app-kitの6.2.0がthree@0.139.0に依存しているため、全体的に古めのバージョンになります。three-mesh-bvhなど、そのままインストールしてしまうと新しいバージョンで入ってしまうライブラリがあるため、package.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を読み込むと白飛びしてしまうため、色味を調整する処理を追加しました。

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/

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?