🤖
React+TypeScript+R3Fのtutorial応用編2(Octreeで衝突判定の絞り込み)
Abstract
今回の参考はここ(FPS Octree)。このソースをTypeScriptで実装しなおす。
ここで使う3Dモデルも上記のモデルを、ありがたく使わせてもらいました。
ポイント
- Octree
- PointerLockControl ... マウスを非表示にして、マウス動きに追従する
↑実はコレ、コメント化してて、OrbitControlsを有効化している。
結論
今回の成果物はココ↓
前提
- 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-advanced002
$ 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-advanced002
$ cd react-r3f-advanced002
準備
コマンドプロンプト
$ npm install --save three
$ npm install --save @types/three
$ npm install --save @react-three/fiber
$ npm install --save @react-three/drei
$ npm install --save-dev leva
$ npm install --save-dev gltfjsx
準備2
- [ここ](https://github.com/aaaa1597/react-r3f- advanced002/blob/main/public/assets/kloofendal_48d_partly_cloudy_puresky_1k.hdr)からDLしたkloofendal_48d_partly_cloudy_puresky_1k.hdr をプロジェクトのreact-r3f-advanced002/public/assets/"配下にコピー。
- [ここ](https://github.com/aaaa1597/react-r3f- advanced002/blob/main/public/assets/Apartment.glb)からDLしたApartment.glb をプロジェクトの"react-r3f-advanced002/public/assets/"配下にコピー。
npx gltfjsxコマンドを実行 -> TSXファイル生成
'C:\Users\xxx\AppData\Roaming'配下にnpmフォルダを作って、npx gltfjsxコマンド実行。
コマンドプロンプト
$ npx gltfjsx public/assets/house-water-transformed.glb --types --shadows
参考
で、プロジェクトフォルダに出力されたApartment2.tsxを自分のプロジェクトに持ってくる。
.eslintrc.jsを修正
エラーになるので、ignoreに追加
.eslintrc.js
+ "@typescript-eslint/no-namespace": "off",
+ "react/jsx-uses-react": "off",
+ "react/react-in-jsx-scope": "off",
- "react/no-unknown-property": ['error', { ignore: ['css', "args", 'wireframe', 'rotation-x', 'rotation'] }],
+ "react/no-unknown-property": ['error', { ignore: [
+ 'args', 'intensity', 'castShadow', 'shadow-bias', 'shadow-radius', 'shadow-blur', 'shadow-mapSize', 'position', 'rotation',
+ 'shadow-camera-left', 'shadow-camera-right', 'shadow-camera-top', 'shadow-camera-bottom', 'dispose', 'receiveShadow', 'geometry', 'material'] }],
App.tsx
まず全体。
App.tsx
import React, {useRef} 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 { Stats, Environment, OrbitControls, PointerLockControls } from '@react-three/drei'
+import Game from './Game';
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 App = () => {
return (
- <div style={{ width: "75vw", height: "75vh" }}>
+ <div style={{ width: "100vw", height: "75vh" }}>
- <Canvas camera={{ position: [3, 1, 2] }}>
+ <Canvas shadows>
+ <directionalLight
+ intensity={1}
+ castShadow={true}
+ shadow-bias={-0.00015}
+ shadow-radius={4}
+ shadow-blur={10}
+ shadow-mapSize={[2048, 2048]}
+ position={[85.0, 80.0, 70.0]}
+ shadow-camera-left={-30}
+ shadow-camera-right={30}
+ shadow-camera-top={30}
+ shadow-camera-bottom={-30}
+ />
<Box position={[1, 1, 1]} name="A" />
- <Environment preset="forest" background />
+ <Environment files="/assets/kloofendal_48d_partly_cloudy_puresky_1k.hdr" background />
+ <Game />
<OrbitControls />
+ {/* <PointerLockControls /> */}
<axesHelper args={[5]} />
<gridHelper />
+ <Stats />
</Canvas>
</div>
);
}
export default App;
全体構成を定義している。"<Game />"以外は特に普通。
Game.tsx
Game.tsx
import { useEffect, useMemo, useRef } from 'react'
import { OctreeHelper } from 'three/examples/jsm/helpers/OctreeHelper'
import { useThree } from '@react-three/fiber'
import { useGLTF } from '@react-three/drei'
import { Octree } from 'three/examples/jsm/math/Octree'
import { Capsule } from 'three/examples/jsm/math/Capsule'
import { useControls } from 'leva'
import * as THREE from 'three'
import SphereCollider from './SphereCollider'
import { GLTF } from 'three-stdlib'
import Player from './Player'
namespace Constants {
export const ballCount = 100
export const radius = 0.2
export const balls = [...Array(ballCount)].map(() => ({ position: [Math.random() * 50 - 25, 20, Math.random() * 50 - 25] }))
export const v1 = new THREE.Vector3()
export const v2 = new THREE.Vector3()
export const v3 = new THREE.Vector3()
}
type GLTFResult = GLTF & {
nodes: { Suzanne007: THREE.Mesh }
materials: { Suzanne007: THREE.MeshStandardMaterial }
scene: THREE.Object3D<THREE.Object3DEventMap>
}
type SphereCapsule = {
sphere?: THREE.Sphere,
velocity:THREE.Vector3,
capsule?:Capsule,
}
const useOctreeHelper = (octree: Octree) => {
//console.log('in useOctreeHelper')
const { scene } = useThree()
useEffect(() => {
console.log('new OctreeHelper')
const helper = new OctreeHelper(octree, 'hotpink')
helper.name = 'octreeHelper'
scene.add(helper)
return () => {
console.log('removing OctreeHelper')
scene.remove(helper)
}
}, [octree, scene])
useControls('Octree Helper', {
visible: {
value: false,
onChange: (v) => {
scene.getObjectByName('octreeHelper')!.visible = v
//if (document.getElementById('Octree Helper.visible')) document.getElementById('Octree Helper.visible').blur()
}
}
})
}
const useOctree = (scene: THREE.Object3D<THREE.Object3DEventMap>) => {
console.log('in useOctree')
const octree = useMemo(() => {
console.log('new Octree')
return new Octree().fromGraphNode(scene)
}, [scene])
return octree
}
const Ball = (props: { radius: number }) => {
return (
<mesh castShadow>
<sphereGeometry args={[props.radius, 16, 16]} />
<meshStandardMaterial />
{/* <meshNormalMaterial wireframe /> */}
</mesh>
)
}
const Physics = () => {
// const { nodes, scene } = useGLTF('assets/Apartment2.glb') as GLTFResult
const { nodes, scene } = useGLTF('assets/scene-transformed.glb') as GLTFResult
const octree = useOctree(scene)
useOctreeHelper(octree)
const colliders = useRef<SphereCapsule[]>([])
const checkSphereCollisions = (sphere: THREE.Sphere, velocity: THREE.Vector3) => {
for (let i = 0, length = colliders.current.length; i < length; i++) {
const c = colliders.current[i]
if (c.sphere) {
const d2 = sphere.center.distanceToSquared(c.sphere.center)
const r = sphere.radius + c.sphere.radius
const r2 = r * r
if (d2 < r2) {
const normal = Constants.v1.subVectors(sphere.center, c.sphere.center).normalize()
const impact1 = Constants.v2.copy(normal).multiplyScalar(normal.dot(velocity))
const impact2 = Constants.v3.copy(normal).multiplyScalar(normal.dot(c.velocity))
velocity.add(impact2).sub(impact1)
c.velocity.add(impact1).sub(impact2)
const d = (r - Math.sqrt(d2)) / 2
sphere.center.addScaledVector(normal, d)
c.sphere.center.addScaledVector(normal, -d)
}
}
else if (c.capsule) {
const center = Constants.v1.addVectors(c.capsule.start, c.capsule.end).multiplyScalar(0.5)
const r = sphere.radius + c.capsule.radius
const r2 = r * r
for (const point of [c.capsule.start, c.capsule.end, center]) {
const d2 = point.distanceToSquared(sphere.center)
if (d2 < r2) {
const normal = Constants.v1.subVectors(point, sphere.center).normalize()
const impact1 = Constants.v2.copy(normal).multiplyScalar(normal.dot(c.velocity))
const impact2 = Constants.v3.copy(normal).multiplyScalar(normal.dot(velocity))
c.velocity.add(impact2).sub(impact1)
velocity.add(impact1).sub(impact2)
const d = (r - Math.sqrt(d2)) / 2
sphere.center.addScaledVector(normal, -d)
}
}
}
}
}
return (
<>
<group dispose={null}>
<mesh castShadow receiveShadow geometry={nodes.Suzanne007.geometry} material={nodes.Suzanne007.material} position={[1.74, 1.04, 24.97]} />
</group>
{Constants.balls.map(({ position }, i) => (
<SphereCollider key={i} id={i} radius={Constants.radius} octree={octree} position={position} colliders={colliders.current} checkSphereCollisions={checkSphereCollisions}>
<Ball radius={Constants.radius} />
</SphereCollider>
))}
<Player ballCount={Constants.ballCount} octree={octree} colliders={colliders.current} />
</>
)
}
export default Physics;
- Game.tsxをimportさせるのに、エントリポイントは、Physics()という。こんな使い方もできるんやね。なんか気持ち悪い。
- scene-transformed.glbのモデルをuseGLTF()で読込むと、GLTFResult型が返ってくるらしい。
- GLTFResult型は、以下の型を取る。いろんなモデルをgit gltfjsxに掛けてみると分かった。
type GLTFResult = GLTF & {
nodes: { なんちゃら: THREE.Mesh }
materials: { なんちゃら: THREE.MeshStandardMaterial }
scene: THREE.Object3D<THREE.Object3DEventMap>
}
- GLTFResult型のなんちゃらの部分は、モデルをBlenderで開くと、Suzanne007しかいなかったのでそれを設定。いっぱいいたら、いっぱい設定する。
あと、Player.tsxとか、SphereCollider.tsxとかいるけど、公開しているのでgithubの方を見てください。
で、実行。
出来た!!
- W/A/S/Dボタン押下で移動。
- マウスクリックで発射。
ハマりポイント
javascript → typescriptに変更するのに、結構ハマったけど...。
いろいろありすぎて、覚えてない~。
ただ、TypeScriptの型に関してはいつも苦しむ。
宿題
- 起動すぐのカメラの画角を調整したい。向きとか、サイズとか。
- 真ん中にあるカラフルな立方体には、衝突判定が効いてない。効かせたい。
- 独自の3Dモデルに置き換えて、挑戦したい。
React+TypeScript+R3Fのtutorial応用編1(annotations, GLTFSX, SVG)
Discussion