✨
React+TypeScript+R3Fのtutorial応用編5(glTFで3Dアニメーション(AnimationMixer))
Abstract
今回の参考はここ(Animation Controller)。このソースをTypeScriptで実装しなおす。
アニメーションデータは複数読込むことが出来て、それを管理してくれるのが、AnimationMixer。ちょっと便利。
ポイント
- *.glbの3Dモデル読込みと表示
- *.glbのアニメーションデータ読込みと実行
- アニメーションをキーボード操作で切替え
結論
今回の成果物はココ↓
前提
- React+Typescriptの開発環境は構築済 [環境構築]WindowsにVSCode+React+TypeScriptの開発環境を構築してみた。
- このスケルトンコードから始める。react-r3f-base-onebox
手順
1.プロジェクト生成 -> VSCodeで開く
めんどいから、このスケルトンコードから始める。react-r3f-base-onebox
で、下記コマンドでフォルダ名とか整備する。
フォルダリネームとか
$ D:
$ cd .\Products\React.js\ # ご自身の適当なフォルダで。
$ rd /q /s D:\Products\React.js\react-r3f-advanced005
$ git clone https://github.com/aaaa1597/react-r3f-base-onebox.git
$ rd /q /s react-r3f-base-onebox/.git
$ ren react-r3f-base-onebox react-r3f-advanced005
$ cd react-r3f-advanced005
準備
コマンドプロンプト
$ npm install --save three
$ npm install --save @types/three
$ npm install --save @react-three/fiber
$ npm install --save @react-three/drei
準備2
- ここからDLしたモデル一式をプロジェクトの"react-r3f-advanced005/public/assets"配下にコピー。
.eslintrc.jsを修正
エラーになるので、ignoreに追加
.eslintrc.js
"rules": {
- "react/no-unknown-property": ['error', { ignore: ['css', "args", 'wireframe', 'rotation-x', 'rotation'] }],
+ "react/no-unknown-property": ['error', { ignore: ['css', "args", 'position', 'angle', 'penumbra', 'castShadow',
+ "shadow-mapSize-width", "shadow-mapSize-height", 'intensity', 'dispose', 'rotation', 'object',
+ 'frustumCulled', 'geometry', 'material', 'skeleton'] }], }
App.tsx
まず全体。
App.tsx
-import React, {useRef} from 'react';
+import React, { Suspense} from 'react';
import './App.css';
-import { Canvas, useFrame, MeshProps } from '@react-three/fiber'
-import * as THREE from 'three'
-import { OrbitControls, Environment } from '@react-three/drei'
+import { Canvas } from '@react-three/fiber'
+import { Stats, OrbitControls, Environment } from '@react-three/drei'
+import Eve from './Eve'
-const Box = (props: MeshProps) => {
- const ref = useRef<THREE.Mesh>(null!)
-
- useFrame((_, delta) => {
- if( !ref.current) return
- ref.current.rotation.x += 1 * delta
- ref.current.rotation.y += 0.5 * delta
- })
-
- return (
- <mesh {...props} ref={ref}>
- <boxGeometry />
- <meshNormalMaterial />
- </mesh>
- )
-}
+const Loader = () => {
+ return <div className="loader"></div>
+}
const App = () => {
return (
- <div style={{ width: "100vw", height: "75vh" }}>
- <Canvas camera={{ position: [3, 1, 2] }}>
- <Box position={[1, 1, 1]} name="A" />
- <Environment preset="forest" background />
- <OrbitControls />
- <axesHelper args={[5]} />
- <gridHelper />
- </Canvas>
- </div>
+ <>
+ <Suspense fallback={<Loader />}>
+ <div style={{ width: "100vw", height: "75vh" }}>
+ <Canvas camera={{ position: [0, 1, 2] }}>
+ <spotLight position={[2.5, 5, 5]} angle={Math.PI / 3} penumbra={0.5} castShadow shadow-mapSize-height={2048} shadow-mapSize-width={2048} intensity={Math.PI * 50} />
+ <spotLight position={[-2.5, 5, 5]} angle={Math.PI / 3} penumbra={0.5} castShadow shadow-mapSize-height={2048} shadow-mapSize-width={2048} intensity={Math.PI * 50} />
+ <Environment preset="forest" background />
+ <Eve />
+ <OrbitControls />
+ <axesHelper args={[5]} />
+ <gridHelper />
+ <Stats />
+ </Canvas>
+ </div>
+ </Suspense>
+ <div id="instructions"> // ①後勝ちになる。
+ <kbd>W</kbd> to walk<br /> // ①後勝ちになる。
+ <kbd>W</kbd> & <kbd>⇧ Shift</kbd> to run.<br /> // ①後勝ちになる。
+ <kbd>Space</kbd> to jump<br /> // ①後勝ちになる。
+ <kbd>Q</kbd> to fancy pose<br /><br /> // ①後勝ちになる。
+ Model from{" "} // ①後勝ちになる。
+ <a href="https://www.mixamo.com" target="_blank" rel="nofollow noreferrer">// ①後勝ちになる。
+ Mixamo
+ </a> // ①後勝ちになる。
q+ </div> // ①後勝ちになる。
+ </>
);
}
export default App;
Eve.tsx
次はモデル描画部分。全部新規追加。
Eve.tsx
import React, { useRef, useMemo, useState, useEffect } from 'react';
import { useFrame } from '@react-three/fiber'
import { useGLTF } from '@react-three/drei'
import { AnimationAction, AnimationMixer, SkinnedMesh } from 'three'
type jsonkeymap = { [key: string]: boolean } // ②json形式の定義方法
const useKeyboard = () => {
const keyMap = useRef<jsonkeymap>({}) // ③json形式の初期化は{}
useEffect(() => {
const onDocumentKey = (e: KeyboardEvent) => {
keyMap.current[e.code] = e.type === 'keydown'
}
document.addEventListener('keydown', onDocumentKey)
document.addEventListener('keyup', onDocumentKey)
return () => {
document.removeEventListener('keydown', onDocumentKey)
document.removeEventListener('keyup', onDocumentKey)
}
})
return keyMap.current
}
type jsoactions = { [key: string]: AnimationAction }
const Eve = () => {
const ref = useRef<THREE.Group>(null!)
const { nodes, materials } = useGLTF('./assets/eve.glb')
const idleAnimation = useGLTF('./assets/eve@idle.glb').animations
const walkAnimation = useGLTF('./assets/eve@walking.glb').animations
const runningAnimation= useGLTF('./assets/eve@running.glb').animations
const jumpAnimation = useGLTF('./assets/eve@jump.glb').animations
const poseAnimation = useGLTF('./assets/eve@pose.glb').animations
const actions = useRef<jsoactions>({}) // ④useMemoだと実装が分からん
const mixer:AnimationMixer = useMemo<AnimationMixer>(() => new AnimationMixer(null!), [])
const keyboard = useKeyboard()
const [actionName, setActionName] = useState<string>(null!)// ⑤useState<AnimationAction>だとsetAction()呼び出してもundefinedにしかならない。
const [wait, setWait] = useState<boolean>(false)
let actionAssigned = false
useEffect(() => {
actions.current['idle'] = mixer.clipAction(idleAnimation [0], ref.current)
actions.current['walk'] = mixer.clipAction(walkAnimation [0], ref.current)
actions.current['running']= mixer.clipAction(runningAnimation[0], ref.current)
actions.current['jump'] = mixer.clipAction(jumpAnimation [0], ref.current)
actions.current['pose'] = mixer.clipAction(poseAnimation [0], ref.current)
actions.current['idle'].play()
setActionName('idle')
}, [])
useEffect(() => {
const act: AnimationAction = actions.current[actionName]
act?.reset().fadeIn(0.1).play()
return () => {
act?.fadeOut(0.1)
}
}, [actionName])
useFrame((_, delta) => {
if (!wait) {
actionAssigned = false
if (keyboard['KeyW'] && keyboard['ShiftLeft']) {
setActionName('running')
actionAssigned = true
}
else if (keyboard['KeyW']) {
setActionName('walk')
actionAssigned = true
}
if (keyboard['Space']) {
setActionName('jump')
actionAssigned = true
setWait(true) // wait for jump to finish
setTimeout(() => setWait(false), 1000)
}
if (keyboard['KeyQ']) {
setActionName('pose')
actionAssigned = true
}
!actionAssigned && setActionName('idle')
}
mixer.update(delta)
})
return (
<group ref={ref} dispose={null}>
<group name="Scene">
<group name="Armature" rotation={[Math.PI / 2, 0, 0]} scale={0.01}>
<primitive object={nodes.mixamorigHips} />
<skinnedMesh castShadow name="Mesh" frustumCulled={false}
geometry={(nodes.Mesh as SkinnedMesh).geometry}
material={materials.SpacePirate_M} skeleton={(nodes.Mesh as SkinnedMesh).skeleton} />
</group>
</group>
</group>
)
}
export default Eve;
で、実行。
出来た!!
ハマったところ
①先に定義した<div/>タグ子要素の文字列群が表示されなかった。
HTMLは、後勝ちらしくって、後ろに定義したら表示されるようになった。
②json形式の定義方法
json形式の型定義が分からんくって、地味に時間食った。
③json形式の初期化は{}
TypeScriptでずっとエラーが取れず、解決策が分からなかった。
④useMemoだと実装が分からん
参考元のコードだとuseMemo使ってたんだけど、関数でビルドエラーが取れんくって、仕方なくuseRef()に変更した。const actions: jsoactions = {}ってやると、値が保持してくれなかった。
⑤ useState<AnimationAction>だとsetAction()呼び出してもundefinedにしかならない。
なぜか、AnimationAction型のuseStateを定義してもundefinedにしかならず、不明。仕方なくstringに変更した。
React+TypeScript+R3Fのtutorial応用編4(glTFで3Dアニメーション(モデルとモーション別々ファイル読込み))
Discussion