6/22追記 -> 6/23再追記
MMDのモデルにTwinMaker内で踊ってもらう方法について追記しました
7/2追記
MMDモデルの白飛び対応について追記、画像とGIFを更新しました
この記事について
- AWSのTwinMaker上で3Dモーションを再生する方法を紹介します
※下のGIFのようになります
- また、MMDに踊ってもらう方法を紹介します
どうやって?
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で構築すると、以下のようなコンポーネントを作っていると思います。
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追記)
※以降、2023/6/23追記
依存関係を修正することで、問題なくMMDを読み込むことができます。
package.jsonに、以下のdependenciesを指定してください
※iot-app-kitの6.2.0がthree@0.139.0に依存しているため、全体的に古めのバージョンになります。three-mesh-bvhなど、そのままインストールしてしまうと新しいバージョンで入ってしまうライブラリがあるため、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を読み込むと白飛びしてしまうため、色味を調整する処理を追加しました。
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/