Skip to content

Commit 4f7cc72

Browse files
authored
Merge pull request #334 from taeuscherpferd/adding-locomotion-convenience-hook
Added a hook for simply adding locomotion
2 parents 1175d20 + 5d5e7a7 commit 4f7cc72

File tree

10 files changed

+3500
-3128
lines changed

10 files changed

+3500
-3128
lines changed

docs/getting-started/all-hooks.md

+52
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,58 @@ Hook for getting the geometry from the detected plane.
141141

142142
Hook for getting all detected planes with the provided semantic label.
143143

144+
### `useControllerLocomotion`
145+
146+
Hook for abstracting boilerplate needed to use controller based locomotion in XR.
147+
148+
- `target`: Either a `THREE.Group` ref, or a callback function. Recieves movement input. (required)
149+
- `translationOptions`:
150+
- `speed`: The speed at which the user moves.
151+
- `rotationOptions`:
152+
- `deadZone`: How far the joystick must be pushed to trigger a turn.
153+
- `type`: Controls how rotation using the controller functions. Can be either 'smooth' or 'snap'.
154+
- `degrees`: If `rotationType` is 'snap', this specifies the number of degrees to snap the user's view by.
155+
- `speed`: If `rotationType` is 'smooth', this specifies the speed at which the user's view rotates.
156+
- `translationController`: Specifies which hand will control the translation. Can be either 'left' or 'right' (i.e. `XRHandedness`).
157+
158+
```tsx
159+
// Example showing basic usage
160+
export const userMovement = () => {
161+
const originRef = useRef<THREE.Group>(null);
162+
useControllerLocomotion(originRef);
163+
return <XROrigin ref={originRef} />
164+
}
165+
166+
// Example using rapier physics
167+
export const userMovementWithPhysics = () => {
168+
const userRigidBodyRef = useRef<RapierRigidBody>(null);
169+
170+
const userMove = (inputVector: Vector3, rotationInfo: Euler) => {
171+
if (userRigidBodyRef.current) {
172+
const currentLinvel = userRigidBodyRef.current.linvel()
173+
const newLinvel = { x: inputVector.x, y: currentLinvel.y, z: inputVector.z }
174+
userRigidBodyRef.current.setLinvel(newLinvel, true)
175+
userRigidBodyRef.current.setRotation(new Quaternion().setFromEuler(rotationInfo), true)
176+
}
177+
}
178+
179+
useControllerLocomotion(userMove)
180+
181+
return <>
182+
<RigidBody
183+
ref={userRigidBodyRef}
184+
colliders={false}
185+
type='dynamic'
186+
position={[0, 2, 0]}
187+
enabledRotations={[false, false, false]}
188+
canSleep={false}
189+
>
190+
<CapsuleCollider args={[.3, .5]} />
191+
<XROrigin position={[0, -1, 0]} />
192+
</RigidBody>
193+
}
194+
```
195+
144196
## Controller model and layout
145197

146198
@react-three/xr exposes some hook to load controller models and layouts without actual xr controllers for building controller demos/tutoials.

examples/minecraft/src/Player.tsx

+52-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import * as THREE from 'three'
2-
import { useRef } from 'react'
3-
import { useFrame } from '@react-three/fiber'
41
import { useKeyboardControls } from '@react-three/drei'
5-
import { CapsuleCollider, interactionGroups, RapierRigidBody, RigidBody, useRapier } from '@react-three/rapier'
2+
import { useFrame } from '@react-three/fiber'
3+
import {
4+
CapsuleCollider,
5+
interactionGroups,
6+
RapierRigidBody,
7+
RigidBody,
8+
useRapier,
9+
Vector3Object,
10+
} from '@react-three/rapier'
611
import { IfInSessionMode } from '@react-three/xr'
12+
import { useRef } from 'react'
13+
import * as THREE from 'three'
714

815
import { Axe } from './Axe.jsx'
916
import { VRPlayerControl } from './VRPlayerControl.jsx'
@@ -15,10 +22,13 @@ const sideVector = new THREE.Vector3()
1522
const rotation = new THREE.Vector3()
1623

1724
const vectorHelper = new THREE.Vector3()
25+
const quaternionHelper = new THREE.Quaternion()
26+
const quaternionHelper2 = new THREE.Quaternion()
27+
const eulerHelper = new THREE.Euler()
1828

1929
export function Player({ lerp = THREE.MathUtils.lerp }) {
2030
const axe = useRef<THREE.Group>(null)
21-
const ref = useRef<RapierRigidBody>(null)
31+
const rigidBodyRef = useRef<RapierRigidBody>(null)
2232
const { rapier, world } = useRapier()
2333
const [, getKeys] = useKeyboardControls()
2434

@@ -27,32 +37,54 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
2737
backward,
2838
left,
2939
right,
30-
rotation,
40+
rotationVelocity,
3141
velocity,
42+
newVelocity,
3243
}: {
3344
forward: boolean
3445
backward: boolean
3546
left: boolean
3647
right: boolean
37-
rotation: THREE.Euler
38-
velocity?: any
48+
rotationVelocity: number
49+
velocity?: Vector3Object
50+
newVelocity?: THREE.Vector3
3951
}) => {
52+
if (rigidBodyRef.current == null) {
53+
return
54+
}
4055
if (!velocity) {
41-
velocity = ref.current?.linvel()
56+
velocity = rigidBodyRef.current?.linvel()
57+
}
58+
59+
//apply rotation
60+
const { x, y, z, w } = rigidBodyRef.current.rotation()
61+
quaternionHelper.set(x, y, z, w)
62+
quaternionHelper.multiply(quaternionHelper2.setFromEuler(eulerHelper.set(0, rotationVelocity, 0, 'YXZ')))
63+
rigidBodyRef.current?.setRotation(quaternionHelper, true)
64+
65+
if (newVelocity) {
66+
// If we have a new velocity, we're in VR mode
67+
rigidBodyRef.current?.setLinvel({ x: newVelocity.x, y: velocity?.y ?? 0, z: newVelocity.z }, true)
68+
return
4269
}
4370

4471
frontVector.set(0, 0, (backward ? 1 : 0) - (forward ? 1 : 0))
4572
sideVector.set((left ? 1 : 0) - (right ? 1 : 0), 0, 0)
46-
direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(SPEED).applyEuler(rotation)
47-
ref.current?.setLinvel({ x: direction.x, y: velocity.y, z: direction.z }, true)
73+
direction
74+
.subVectors(frontVector, sideVector)
75+
.applyQuaternion(quaternionHelper)
76+
.setComponent(1, 0)
77+
.normalize()
78+
.multiplyScalar(SPEED)
79+
rigidBodyRef.current?.setLinvel({ x: direction.x, y: velocity?.y ?? 0, z: direction.z }, true)
4880
}
4981

5082
const playerJump = () => {
51-
if (ref.current == null) {
83+
if (rigidBodyRef.current == null) {
5284
return
5385
}
5486
const ray = world.castRay(
55-
new rapier.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 }),
87+
new rapier.Ray(rigidBodyRef.current.translation(), { x: 0, y: -1, z: 0 }),
5688
Infinity,
5789
false,
5890
undefined,
@@ -61,21 +93,21 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
6193
const grounded = ray != null && Math.abs(ray.timeOfImpact) <= 1.25
6294

6395
if (grounded) {
64-
ref.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true)
96+
rigidBodyRef.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true)
6597
}
6698
}
6799

68100
useFrame((state) => {
69-
if (ref.current == null) {
101+
if (rigidBodyRef.current == null) {
70102
return
71103
}
72104
const { forward, backward, left, right, jump } = getKeys()
73-
const velocity = ref.current.linvel()
105+
const velocity = rigidBodyRef.current.linvel()
74106

75107
vectorHelper.set(velocity.x, velocity.y, velocity.z)
76108

77109
// update camera
78-
const { x, y, z } = ref.current.translation()
110+
const { x, y, z } = rigidBodyRef.current.translation()
79111
state.camera.position.set(x, y, z)
80112

81113
// update axe
@@ -90,13 +122,13 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
90122
}
91123

92124
// movement
93-
if (ref.current) {
125+
if (rigidBodyRef.current) {
94126
playerMove({
95127
forward,
96128
backward,
97129
left,
98130
right,
99-
rotation: state.camera.rotation,
131+
rotationVelocity: 0,
100132
velocity,
101133
})
102134

@@ -109,7 +141,7 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
109141
return (
110142
<>
111143
<RigidBody
112-
ref={ref}
144+
ref={rigidBodyRef}
113145
colliders={false}
114146
mass={1}
115147
type="dynamic"
+24-39
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import * as THREE from 'three'
2-
import { useRef } from 'react'
31
import { useFrame } from '@react-three/fiber'
4-
import { useXRControllerState, XROrigin } from '@react-three/xr'
5-
6-
const TURN_SPEED = 1.5,
7-
THUMBSTICK_X_WIGGLE = 0.5
8-
9-
const helpers = {
10-
euler: new THREE.Euler(),
11-
quaternion: new THREE.Quaternion(),
12-
}
2+
import { Vector3Object } from '@react-three/rapier'
3+
import { useControllerLocomotion, useXRInputSourceState, XROrigin } from '@react-three/xr'
4+
import * as THREE from 'three'
135

146
export function VRPlayerControl({
157
playerJump,
@@ -21,39 +13,32 @@ export function VRPlayerControl({
2113
backward: boolean
2214
left: boolean
2315
right: boolean
24-
rotation: THREE.Euler
16+
rotationVelocity: number
17+
velocity?: Vector3Object
18+
newVelocity?: THREE.Vector3
2519
}) => void
2620
}) {
27-
const originRef = useRef<THREE.Group>(null)
28-
29-
const controllerLeft = useXRControllerState('left')
30-
const controllerRight = useXRControllerState('right')
31-
32-
useFrame((state, delta) => {
33-
const thumbstickRight = controllerRight?.gamepad?.['xr-standard-thumbstick']
34-
if (originRef.current != null && thumbstickRight?.xAxis != null && thumbstickRight.xAxis != 0) {
35-
originRef.current.rotateY((thumbstickRight.xAxis < 0 ? 1 : -1) * TURN_SPEED * delta)
36-
}
37-
21+
const controllerRight = useXRInputSourceState('controller', 'right')
22+
23+
const physicsMove = (velocity: THREE.Vector3, rotationVelocity: number) => {
24+
playerMove({
25+
forward: false,
26+
backward: false,
27+
left: false,
28+
right: false,
29+
rotationVelocity,
30+
velocity: undefined,
31+
newVelocity: velocity,
32+
})
33+
}
34+
35+
useControllerLocomotion(physicsMove, { speed: 5 })
36+
37+
useFrame(() => {
3838
if (controllerRight?.gamepad?.['a-button']?.state === 'pressed') {
3939
playerJump?.()
4040
}
41-
42-
const thumbstickLeft = controllerLeft?.gamepad['xr-standard-thumbstick']
43-
if (thumbstickLeft?.xAxis != null && thumbstickLeft.yAxis != null) {
44-
state.camera.getWorldQuaternion(helpers.quaternion)
45-
46-
playerMove?.({
47-
forward: thumbstickLeft.yAxis < 0,
48-
backward: thumbstickLeft.yAxis > 0,
49-
left: thumbstickLeft.xAxis < -THUMBSTICK_X_WIGGLE,
50-
right: thumbstickLeft.xAxis > THUMBSTICK_X_WIGGLE,
51-
52-
// rotation: state.camera.rotation
53-
rotation: helpers.euler.setFromQuaternion(helpers.quaternion),
54-
})
55-
}
5641
})
5742

58-
return <XROrigin ref={originRef} position={[0, -1.25, 0]} />
43+
return <XROrigin position={[0, -1.25, 0]} />
5944
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { RootState, useFrame } from '@react-three/fiber'
2+
import { RefObject, useMemo } from 'react'
3+
import { Vector3, Object3D } from 'three'
4+
import {
5+
type ControllerLocomotionRotationOptions,
6+
type ControllerLocomotionTranslationOptions,
7+
createControllerLocomotionUpdate,
8+
} from '@pmndrs/xr/internals'
9+
import { useXRStore } from './xr.js'
10+
11+
/**
12+
* A hook for handling basic locomotion in VR
13+
* @param target Either a `THREE.Group` ref, or a callback function. Recieves movement input (required).
14+
* @param translationOptions Options that control the translation of the user. Set to `false` to disable.
15+
* @param translationOptions.speed The speed at which the user moves.
16+
* @param rotationOptions Options that control the rotation of the user. Set to `false` to disable.
17+
* @param rotationOptions.deadZone How far the joystick must be pushed to trigger a turn.
18+
* @param rotationOptions.type Controls how rotation using the controller functions. Can be either 'smooth' or 'snap'.
19+
* @param rotationOptions.degrees If `type` is 'snap', this specifies the number of degrees to snap the user's view by.
20+
* @param rotationOptions.speed If `type` is 'smooth', this specifies the speed at which the user's view rotates.
21+
* @param translationControllerHand Specifies which hand will control the movement. Can be either 'left' or 'right'.
22+
*/
23+
export function useControllerLocomotion(
24+
target:
25+
| RefObject<Object3D>
26+
| ((velocity: Vector3, rotationVelocityY: number, deltaTime: number, state: RootState, frame?: XRFrame) => void),
27+
translationOptions: ControllerLocomotionTranslationOptions = {},
28+
rotationOptions: ControllerLocomotionRotationOptions = {},
29+
translationControllerHand: Exclude<XRHandedness, 'none'> = 'left',
30+
) {
31+
const store = useXRStore()
32+
const update = useMemo(() => createControllerLocomotionUpdate(), [])
33+
useFrame((state, delta, frame: XRFrame | undefined) =>
34+
update(
35+
typeof target === 'function' ? target : target.current,
36+
store,
37+
state.camera,
38+
delta,
39+
translationOptions,
40+
rotationOptions,
41+
translationControllerHand,
42+
delta,
43+
state,
44+
frame,
45+
),
46+
)
47+
}

packages/react/xr/src/hooks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RefObject, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
2-
import { useXR } from './xr.js'
32
import { Object3D } from 'three'
3+
import { useXR } from './xr.js'
44

55
export function useHover(ref: RefObject<Object3D>): boolean
66

packages/react/xr/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './hit-test.js'
1515
export * from './anchor.js'
1616
export * from './dom-overlay.js'
1717
export * from './layer.js'
18+
export * from './controller-locomotion.js'
1819

1920
//react-three/xr v5 compatibility layer
2021
export * from './deprecated/index.js'

0 commit comments

Comments
 (0)