diff --git a/examples/minecraft/index.tsx b/examples/minecraft/index.tsx
index 2d8ffd8b..99a3db79 100644
--- a/examples/minecraft/index.tsx
+++ b/examples/minecraft/index.tsx
@@ -1,6 +1,6 @@
import { createRoot } from 'react-dom/client'
//@ts-ignore
-import { App } from './src/App.jsx'
+import { App } from './src/App.js'
import { StrictMode } from 'react'
createRoot(document.getElementById('root')!).render(
diff --git a/examples/minecraft/package.json b/examples/minecraft/package.json
index e18bb311..ef7048cd 100644
--- a/examples/minecraft/package.json
+++ b/examples/minecraft/package.json
@@ -1,10 +1,9 @@
{
"dependencies": {
- "@dimforge/rapier3d-compat": "^0.13.1",
- "@react-three/drei": "^9.108.3",
+ "@react-three/drei": "^9.108.4",
"@react-three/rapier": "^1.4.0",
"@react-three/xr": "workspace:^",
- "zustand": "^4.5.2"
+ "zustand": "^4.5.4"
},
"type": "module",
"scripts": {
diff --git a/examples/minecraft/src/App.jsx b/examples/minecraft/src/App.tsx
similarity index 89%
rename from examples/minecraft/src/App.jsx
rename to examples/minecraft/src/App.tsx
index 7a61a4cc..99475554 100644
--- a/examples/minecraft/src/App.jsx
+++ b/examples/minecraft/src/App.tsx
@@ -1,6 +1,6 @@
import { Canvas } from '@react-three/fiber'
import { Sky, PointerLockControls, KeyboardControls } from '@react-three/drei'
-import { Physics } from '@react-three/rapier'
+import { interactionGroups, Physics } from '@react-three/rapier'
import { Ground } from './Ground.jsx'
import { Player } from './Player.jsx'
import { Cube, Cubes } from './Cube.jsx'
@@ -51,9 +51,9 @@ export function App() {
-
+
-
+
diff --git a/examples/minecraft/src/Axe.jsx b/examples/minecraft/src/Axe.tsx
similarity index 63%
rename from examples/minecraft/src/Axe.jsx
rename to examples/minecraft/src/Axe.tsx
index e457c288..72f3b881 100644
--- a/examples/minecraft/src/Axe.jsx
+++ b/examples/minecraft/src/Axe.tsx
@@ -7,14 +7,16 @@ title: Minecraft Diamond Axe
*/
import { useGLTF } from '@react-three/drei'
+import { GroupProps } from '@react-three/fiber'
+import { Mesh } from 'three'
-export function Axe(props) {
+export function Axe(props: GroupProps) {
const { nodes, materials } = useGLTF('axe.glb')
return (
-
-
+
+
)
diff --git a/examples/minecraft/src/Cube.jsx b/examples/minecraft/src/Cube.tsx
similarity index 58%
rename from examples/minecraft/src/Cube.jsx
rename to examples/minecraft/src/Cube.tsx
index c37da0df..47ca3bbc 100644
--- a/examples/minecraft/src/Cube.jsx
+++ b/examples/minecraft/src/Cube.tsx
@@ -1,33 +1,43 @@
import { useCallback, useRef, useState } from 'react'
import { useTexture } from '@react-three/drei'
-import { RigidBody } from '@react-three/rapier'
-import create from 'zustand'
+import { interactionGroups, RapierRigidBody, RigidBody, RigidBodyProps } from '@react-three/rapier'
+import { create } from 'zustand'
+import { Vector3Tuple } from 'three'
+import { ThreeEvent } from '@react-three/fiber'
// This is a naive implementation and wouldn't allow for more than a few thousand boxes.
// In order to make this scale this has to be one instanced mesh, then it could easily be
// hundreds of thousands.
-const useCubeStore = create((set) => ({
- cubes: [],
- addCube: (x, y, z) => set((state) => ({ cubes: [...state.cubes, [x, y, z]] })),
-}))
+const useCubeStore = create<{ cubes: Array; addCube: (x: number, y: number, z: number) => void }>(
+ (set) => ({
+ cubes: [],
+ addCube: (x: number, y: number, z: number) => set((state) => ({ cubes: [...state.cubes, [x, y, z]] })),
+ }),
+)
export const Cubes = () => {
const cubes = useCubeStore((state) => state.cubes)
return cubes.map((coords, index) => )
}
-export function Cube(props) {
- const ref = useRef()
- const [hover, set] = useState(null)
+export function Cube(props: RigidBodyProps) {
+ const ref = useRef(null)
+ const [hover, set] = useState(null)
const addCube = useCubeStore((state) => state.addCube)
const texture = useTexture('dirt.jpg')
- const onMove = useCallback((e) => {
+ const onMove = useCallback((e: ThreeEvent) => {
+ if (e.faceIndex == null) {
+ return
+ }
e.stopPropagation()
set(Math.floor(e.faceIndex / 2))
}, [])
const onOut = useCallback(() => set(null), [])
- const onClick = useCallback((e) => {
+ const onClick = useCallback((e: ThreeEvent) => {
+ if (ref.current == null || e.faceIndex == null) {
+ return
+ }
e.stopPropagation()
const { x, y, z } = ref.current.translation()
const dir = [
@@ -37,11 +47,11 @@ export function Cube(props) {
[x, y - 1, z],
[x, y, z + 1],
[x, y, z - 1],
- ]
+ ] as const
addCube(...dir[Math.floor(e.faceIndex / 2)])
}, [])
return (
-
+
{[...Array(6)].map((_, index) => (
{
- const { forward, backward, left, right, jump } = get()
- const velocity = ref.current.linvel()
- vectorHelper.set(velocity.x, velocity.y, velocity.z)
- // update camera
- const { x, y, z } = ref.current.translation()
- state.camera.position.set(x, y, z)
- // update axe
- if (axe.current != null) {
- axe.current.children[0].rotation.x = lerp(
- axe.current.children[0].rotation.x,
- Math.sin((vectorHelper.length() > 1) * state.clock.elapsedTime * 10) / 6,
- 0.1,
- )
- axe.current.rotation.copy(state.camera.rotation)
- axe.current.position.copy(state.camera.position).add(state.camera.getWorldDirection(rotation).multiplyScalar(1))
- } // movement
- frontVector.set(0, 0, backward - forward)
- sideVector.set(left - right, 0, 0)
- direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(SPEED).applyEuler(state.camera.rotation)
- ref.current.setLinvel({ x: direction.x, y: velocity.y, z: direction.z })
- // jumping
- const world = rapier.world
- // eslint-disable-next-line @react-three/no-new-in-loop
- const ray = world.castRay(new RAPIER.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 }))
- const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.75
- if (jump && grounded) ref.current.setLinvel({ x: 0, y: 7.5, z: 0 })
- })
- return (
- <>
-
-
-
-
- (axe.current.children[0].rotation.x = -0.5)}>
-
-
-
- >
- )
-}
diff --git a/examples/minecraft/src/Player.tsx b/examples/minecraft/src/Player.tsx
new file mode 100644
index 00000000..f833c814
--- /dev/null
+++ b/examples/minecraft/src/Player.tsx
@@ -0,0 +1,143 @@
+import * as THREE from 'three'
+import { useRef } from 'react'
+import { useFrame } from '@react-three/fiber'
+import { useKeyboardControls } from '@react-three/drei'
+import { CapsuleCollider, interactionGroups, RapierRigidBody, RigidBody, useRapier } from '@react-three/rapier'
+import { IfInSessionMode } from '@react-three/xr'
+
+import { Axe } from './Axe.jsx'
+import { VRPlayerControl } from './VRPlayerControl.jsx'
+
+const SPEED = 5
+const direction = new THREE.Vector3()
+const frontVector = new THREE.Vector3()
+const sideVector = new THREE.Vector3()
+const rotation = new THREE.Vector3()
+
+const vectorHelper = new THREE.Vector3()
+
+export function Player({ lerp = THREE.MathUtils.lerp }) {
+ const axe = useRef(null)
+ const ref = useRef(null)
+ const { rapier, world } = useRapier()
+ const [, getKeys] = useKeyboardControls()
+
+ const playerMove = ({
+ forward,
+ backward,
+ left,
+ right,
+ rotation,
+ velocity,
+ }: {
+ forward: boolean
+ backward: boolean
+ left: boolean
+ right: boolean
+ rotation: THREE.Euler
+ velocity?: any
+ }) => {
+ if (!velocity) {
+ velocity = ref.current?.linvel()
+ }
+
+ frontVector.set(0, 0, (backward ? 1 : 0) - (forward ? 1 : 0))
+ sideVector.set((left ? 1 : 0) - (right ? 1 : 0), 0, 0)
+ direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(SPEED).applyEuler(rotation)
+ ref.current?.setLinvel({ x: direction.x, y: velocity.y, z: direction.z }, true)
+ }
+
+ const playerJump = () => {
+ if (ref.current == null) {
+ return
+ }
+ const ray = world.castRay(
+ new rapier.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 }),
+ Infinity,
+ false,
+ undefined,
+ interactionGroups([1, 0], [1]),
+ )
+ const grounded = ray != null && Math.abs(ray.timeOfImpact) <= 1.25
+
+ if (grounded) {
+ ref.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true)
+ }
+ }
+
+ useFrame((state) => {
+ if (ref.current == null) {
+ return
+ }
+ const { forward, backward, left, right, jump } = getKeys()
+ const velocity = ref.current.linvel()
+
+ vectorHelper.set(velocity.x, velocity.y, velocity.z)
+
+ // update camera
+ const { x, y, z } = ref.current.translation()
+ state.camera.position.set(x, y, z)
+
+ // update axe
+ if (axe.current != null) {
+ axe.current.children[0].rotation.x = lerp(
+ axe.current.children[0].rotation.x,
+ Math.sin((vectorHelper.length() > 1 ? 1 : 0) * state.clock.elapsedTime * 10) / 6,
+ 0.1,
+ )
+ axe.current.rotation.copy(state.camera.rotation)
+ axe.current.position.copy(state.camera.position).add(state.camera.getWorldDirection(rotation).multiplyScalar(1))
+ }
+
+ // movement
+ if (ref.current) {
+ playerMove({
+ forward,
+ backward,
+ left,
+ right,
+ rotation: state.camera.rotation,
+ velocity,
+ })
+
+ if (jump) {
+ playerJump()
+ }
+ }
+ })
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {
+ if (axe.current == null) {
+ return
+ }
+ axe.current.children[0].rotation.x = -0.5
+ }}
+ >
+
+
+
+ >
+ )
+}
diff --git a/examples/minecraft/src/VRPlayerControl.tsx b/examples/minecraft/src/VRPlayerControl.tsx
new file mode 100644
index 00000000..6c092d2b
--- /dev/null
+++ b/examples/minecraft/src/VRPlayerControl.tsx
@@ -0,0 +1,61 @@
+import * as THREE from 'three'
+import { useRef } from 'react'
+import { useFrame } from '@react-three/fiber'
+import { useXRControllerState, XROrigin } from '@react-three/xr'
+
+const TURN_SPEED = 1.5,
+ THUMBSTICK_X_WIGGLE = 0.5
+
+const helpers = {
+ euler: new THREE.Euler(),
+ quaternion: new THREE.Quaternion(),
+}
+
+export function VRPlayerControl({
+ playerJump,
+ playerMove,
+}: {
+ playerJump?: () => void
+ playerMove: (params: {
+ forward: boolean
+ backward: boolean
+ left: boolean
+ right: boolean
+ rotation: THREE.Euler
+ }) => void
+}) {
+ const originRef = useRef(null)
+
+ const controllerLeft = useXRControllerState('right')
+ const controllerRight = useXRControllerState('left')
+
+ useFrame((state, delta) => {
+ if (controllerRight != null) {
+ const thumbstick = controllerRight.gamepad?.['xr-standard-thumbstick']
+ if (originRef.current != null && thumbstick?.xAxis != null && thumbstick.xAxis != 0) {
+ originRef.current.rotateY((thumbstick.xAxis < 0 ? 1 : -1) * TURN_SPEED * delta)
+ }
+ }
+
+ if (controllerLeft?.gamepad?.['a-button']?.state === 'pressed') {
+ playerJump?.()
+ }
+
+ const thumbstick = controllerLeft?.gamepad['xr-standard-thumbstick']
+ if (thumbstick?.xAxis != null && thumbstick.yAxis != null) {
+ state.camera.getWorldQuaternion(helpers.quaternion)
+
+ playerMove?.({
+ forward: thumbstick.yAxis < 0,
+ backward: thumbstick.yAxis > 0,
+ left: thumbstick.xAxis < -THUMBSTICK_X_WIGGLE,
+ right: thumbstick.xAxis > THUMBSTICK_X_WIGGLE,
+
+ // rotation: state.camera.rotation
+ rotation: helpers.euler.setFromQuaternion(helpers.quaternion),
+ })
+ }
+ })
+
+ return
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c8b2cd62..f19e2e8d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,12 +101,9 @@ importers:
examples/minecraft:
dependencies:
- '@dimforge/rapier3d-compat':
- specifier: ^0.13.1
- version: 0.13.1
'@react-three/drei':
- specifier: ^9.108.3
- version: 9.108.3(@react-three/fiber@8.16.8)(@types/react@18.3.3)(@types/three@0.164.1)(react-dom@18.3.1)(react@18.3.1)(three@0.167.1)
+ specifier: ^9.108.4
+ version: 9.109.2(@react-three/fiber@8.16.8)(@types/react@18.3.3)(@types/three@0.164.1)(react-dom@18.3.1)(react@18.3.1)(three@0.167.1)
'@react-three/rapier':
specifier: ^1.4.0
version: 1.4.0(@react-three/fiber@8.16.8)(react@18.3.1)(three@0.167.1)
@@ -114,8 +111,8 @@ importers:
specifier: workspace:^
version: link:../../packages/react/xr
zustand:
- specifier: ^4.5.2
- version: 4.5.2(@types/react@18.3.3)(react@18.3.1)
+ specifier: ^4.5.4
+ version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
examples/miniature:
dependencies:
@@ -1407,7 +1404,7 @@ packages:
prompts: 2.4.2
react: 18.3.1
zod: 3.23.8
- zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
+ zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -1432,7 +1429,7 @@ packages:
prompts: 2.4.2
react: 18.3.1
zod: 3.23.8
- zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
+ zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -4974,7 +4971,7 @@ packages:
/tunnel-rat@0.1.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
dependencies:
- zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
+ zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -5408,3 +5405,23 @@ packages:
react: 18.3.1
use-sync-external-store: 1.2.0(react@18.3.1)
dev: false
+
+ /zustand@4.5.4(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ use-sync-external-store: 1.2.0(react@18.3.1)
+ dev: false