Skip to content

Commit

Permalink
Merge pull request #334 from taeuscherpferd/adding-locomotion-conveni…
Browse files Browse the repository at this point in the history
…ence-hook

Added a hook for simply adding locomotion
  • Loading branch information
bbohlender authored Sep 24, 2024
2 parents 1175d20 + 5d5e7a7 commit 4f7cc72
Show file tree
Hide file tree
Showing 10 changed files with 3,500 additions and 3,128 deletions.
52 changes: 52 additions & 0 deletions docs/getting-started/all-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,58 @@ Hook for getting the geometry from the detected plane.

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

### `useControllerLocomotion`

Hook for abstracting boilerplate needed to use controller based locomotion in XR.

- `target`: Either a `THREE.Group` ref, or a callback function. Recieves movement input. (required)
- `translationOptions`:
- `speed`: The speed at which the user moves.
- `rotationOptions`:
- `deadZone`: How far the joystick must be pushed to trigger a turn.
- `type`: Controls how rotation using the controller functions. Can be either 'smooth' or 'snap'.
- `degrees`: If `rotationType` is 'snap', this specifies the number of degrees to snap the user's view by.
- `speed`: If `rotationType` is 'smooth', this specifies the speed at which the user's view rotates.
- `translationController`: Specifies which hand will control the translation. Can be either 'left' or 'right' (i.e. `XRHandedness`).

```tsx
// Example showing basic usage
export const userMovement = () => {
const originRef = useRef<THREE.Group>(null);
useControllerLocomotion(originRef);
return <XROrigin ref={originRef} />
}

// Example using rapier physics
export const userMovementWithPhysics = () => {
const userRigidBodyRef = useRef<RapierRigidBody>(null);

const userMove = (inputVector: Vector3, rotationInfo: Euler) => {
if (userRigidBodyRef.current) {
const currentLinvel = userRigidBodyRef.current.linvel()
const newLinvel = { x: inputVector.x, y: currentLinvel.y, z: inputVector.z }
userRigidBodyRef.current.setLinvel(newLinvel, true)
userRigidBodyRef.current.setRotation(new Quaternion().setFromEuler(rotationInfo), true)
}
}

useControllerLocomotion(userMove)

return <>
<RigidBody
ref={userRigidBodyRef}
colliders={false}
type='dynamic'
position={[0, 2, 0]}
enabledRotations={[false, false, false]}
canSleep={false}
>
<CapsuleCollider args={[.3, .5]} />
<XROrigin position={[0, -1, 0]} />
</RigidBody>
}
```

## Controller model and layout

@react-three/xr exposes some hook to load controller models and layouts without actual xr controllers for building controller demos/tutoials.
Expand Down
72 changes: 52 additions & 20 deletions examples/minecraft/src/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
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 { useFrame } from '@react-three/fiber'
import {
CapsuleCollider,
interactionGroups,
RapierRigidBody,
RigidBody,
useRapier,
Vector3Object,
} from '@react-three/rapier'
import { IfInSessionMode } from '@react-three/xr'
import { useRef } from 'react'
import * as THREE from 'three'

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

const vectorHelper = new THREE.Vector3()
const quaternionHelper = new THREE.Quaternion()
const quaternionHelper2 = new THREE.Quaternion()
const eulerHelper = new THREE.Euler()

export function Player({ lerp = THREE.MathUtils.lerp }) {
const axe = useRef<THREE.Group>(null)
const ref = useRef<RapierRigidBody>(null)
const rigidBodyRef = useRef<RapierRigidBody>(null)
const { rapier, world } = useRapier()
const [, getKeys] = useKeyboardControls()

Expand All @@ -27,32 +37,54 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
backward,
left,
right,
rotation,
rotationVelocity,
velocity,
newVelocity,
}: {
forward: boolean
backward: boolean
left: boolean
right: boolean
rotation: THREE.Euler
velocity?: any
rotationVelocity: number
velocity?: Vector3Object
newVelocity?: THREE.Vector3
}) => {
if (rigidBodyRef.current == null) {
return
}
if (!velocity) {
velocity = ref.current?.linvel()
velocity = rigidBodyRef.current?.linvel()
}

//apply rotation
const { x, y, z, w } = rigidBodyRef.current.rotation()
quaternionHelper.set(x, y, z, w)
quaternionHelper.multiply(quaternionHelper2.setFromEuler(eulerHelper.set(0, rotationVelocity, 0, 'YXZ')))
rigidBodyRef.current?.setRotation(quaternionHelper, true)

if (newVelocity) {
// If we have a new velocity, we're in VR mode
rigidBodyRef.current?.setLinvel({ x: newVelocity.x, y: velocity?.y ?? 0, z: newVelocity.z }, true)
return
}

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)
direction
.subVectors(frontVector, sideVector)
.applyQuaternion(quaternionHelper)
.setComponent(1, 0)
.normalize()
.multiplyScalar(SPEED)
rigidBodyRef.current?.setLinvel({ x: direction.x, y: velocity?.y ?? 0, z: direction.z }, true)
}

const playerJump = () => {
if (ref.current == null) {
if (rigidBodyRef.current == null) {
return
}
const ray = world.castRay(
new rapier.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 }),
new rapier.Ray(rigidBodyRef.current.translation(), { x: 0, y: -1, z: 0 }),
Infinity,
false,
undefined,
Expand All @@ -61,21 +93,21 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
const grounded = ray != null && Math.abs(ray.timeOfImpact) <= 1.25

if (grounded) {
ref.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true)
rigidBodyRef.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true)
}
}

useFrame((state) => {
if (ref.current == null) {
if (rigidBodyRef.current == null) {
return
}
const { forward, backward, left, right, jump } = getKeys()
const velocity = ref.current.linvel()
const velocity = rigidBodyRef.current.linvel()

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

// update camera
const { x, y, z } = ref.current.translation()
const { x, y, z } = rigidBodyRef.current.translation()
state.camera.position.set(x, y, z)

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

// movement
if (ref.current) {
if (rigidBodyRef.current) {
playerMove({
forward,
backward,
left,
right,
rotation: state.camera.rotation,
rotationVelocity: 0,
velocity,
})

Expand All @@ -109,7 +141,7 @@ export function Player({ lerp = THREE.MathUtils.lerp }) {
return (
<>
<RigidBody
ref={ref}
ref={rigidBodyRef}
colliders={false}
mass={1}
type="dynamic"
Expand Down
63 changes: 24 additions & 39 deletions examples/minecraft/src/VRPlayerControl.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
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(),
}
import { Vector3Object } from '@react-three/rapier'
import { useControllerLocomotion, useXRInputSourceState, XROrigin } from '@react-three/xr'
import * as THREE from 'three'

export function VRPlayerControl({
playerJump,
Expand All @@ -21,39 +13,32 @@ export function VRPlayerControl({
backward: boolean
left: boolean
right: boolean
rotation: THREE.Euler
rotationVelocity: number
velocity?: Vector3Object
newVelocity?: THREE.Vector3
}) => void
}) {
const originRef = useRef<THREE.Group>(null)

const controllerLeft = useXRControllerState('left')
const controllerRight = useXRControllerState('right')

useFrame((state, delta) => {
const thumbstickRight = controllerRight?.gamepad?.['xr-standard-thumbstick']
if (originRef.current != null && thumbstickRight?.xAxis != null && thumbstickRight.xAxis != 0) {
originRef.current.rotateY((thumbstickRight.xAxis < 0 ? 1 : -1) * TURN_SPEED * delta)
}

const controllerRight = useXRInputSourceState('controller', 'right')

const physicsMove = (velocity: THREE.Vector3, rotationVelocity: number) => {
playerMove({
forward: false,
backward: false,
left: false,
right: false,
rotationVelocity,
velocity: undefined,
newVelocity: velocity,
})
}

useControllerLocomotion(physicsMove, { speed: 5 })

useFrame(() => {
if (controllerRight?.gamepad?.['a-button']?.state === 'pressed') {
playerJump?.()
}

const thumbstickLeft = controllerLeft?.gamepad['xr-standard-thumbstick']
if (thumbstickLeft?.xAxis != null && thumbstickLeft.yAxis != null) {
state.camera.getWorldQuaternion(helpers.quaternion)

playerMove?.({
forward: thumbstickLeft.yAxis < 0,
backward: thumbstickLeft.yAxis > 0,
left: thumbstickLeft.xAxis < -THUMBSTICK_X_WIGGLE,
right: thumbstickLeft.xAxis > THUMBSTICK_X_WIGGLE,

// rotation: state.camera.rotation
rotation: helpers.euler.setFromQuaternion(helpers.quaternion),
})
}
})

return <XROrigin ref={originRef} position={[0, -1.25, 0]} />
return <XROrigin position={[0, -1.25, 0]} />
}
47 changes: 47 additions & 0 deletions packages/react/xr/src/controller-locomotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { RootState, useFrame } from '@react-three/fiber'
import { RefObject, useMemo } from 'react'
import { Vector3, Object3D } from 'three'
import {
type ControllerLocomotionRotationOptions,
type ControllerLocomotionTranslationOptions,
createControllerLocomotionUpdate,
} from '@pmndrs/xr/internals'
import { useXRStore } from './xr.js'

/**
* A hook for handling basic locomotion in VR
* @param target Either a `THREE.Group` ref, or a callback function. Recieves movement input (required).
* @param translationOptions Options that control the translation of the user. Set to `false` to disable.
* @param translationOptions.speed The speed at which the user moves.
* @param rotationOptions Options that control the rotation of the user. Set to `false` to disable.
* @param rotationOptions.deadZone How far the joystick must be pushed to trigger a turn.
* @param rotationOptions.type Controls how rotation using the controller functions. Can be either 'smooth' or 'snap'.
* @param rotationOptions.degrees If `type` is 'snap', this specifies the number of degrees to snap the user's view by.
* @param rotationOptions.speed If `type` is 'smooth', this specifies the speed at which the user's view rotates.
* @param translationControllerHand Specifies which hand will control the movement. Can be either 'left' or 'right'.
*/
export function useControllerLocomotion(
target:
| RefObject<Object3D>
| ((velocity: Vector3, rotationVelocityY: number, deltaTime: number, state: RootState, frame?: XRFrame) => void),
translationOptions: ControllerLocomotionTranslationOptions = {},
rotationOptions: ControllerLocomotionRotationOptions = {},
translationControllerHand: Exclude<XRHandedness, 'none'> = 'left',
) {
const store = useXRStore()
const update = useMemo(() => createControllerLocomotionUpdate(), [])
useFrame((state, delta, frame: XRFrame | undefined) =>
update(
typeof target === 'function' ? target : target.current,
store,
state.camera,
delta,
translationOptions,
rotationOptions,
translationControllerHand,
delta,
state,
frame,
),
)
}
2 changes: 1 addition & 1 deletion packages/react/xr/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefObject, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import { useXR } from './xr.js'
import { Object3D } from 'three'
import { useXR } from './xr.js'

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

Expand Down
1 change: 1 addition & 0 deletions packages/react/xr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './hit-test.js'
export * from './anchor.js'
export * from './dom-overlay.js'
export * from './layer.js'
export * from './controller-locomotion.js'

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

0 comments on commit 4f7cc72

Please sign in to comment.