forked from pmndrs/xr
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add teleportation support (pmndrs#263)
- Loading branch information
1 parent
df017c8
commit a5e748b
Showing
7 changed files
with
214 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Canvas } from '@react-three/fiber' | ||
import { Hands, XR, VRButton, TeleportationPlane, Controllers } from '@react-three/xr' | ||
|
||
export default function () { | ||
return ( | ||
<> | ||
<VRButton onError={(e) => console.error(e)} /> | ||
<Canvas> | ||
<color attach="background" args={['black']} /> | ||
<XR> | ||
<Controllers /> | ||
<TeleportationPlane leftHand /> | ||
<mesh position={[1, 0, 0]}> | ||
<boxGeometry args={[0.1, 0.1, 0.1]} /> | ||
<meshBasicMaterial color="red" /> | ||
</mesh> | ||
<mesh position={[0, 1, 0]}> | ||
<boxGeometry args={[0.1, 0.1, 0.1]} /> | ||
<meshBasicMaterial color="green" /> | ||
</mesh> | ||
<mesh position={[0, 0, 1]}> | ||
<boxGeometry args={[0.1, 0.1, 0.1]} /> | ||
<meshBasicMaterial color="blue" /> | ||
</mesh> | ||
<ambientLight intensity={0.5} /> | ||
</XR> | ||
</Canvas> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,14 @@ | ||
import { defineConfig } from 'vite' | ||
import path from 'node:path' | ||
import react from '@vitejs/plugin-react' | ||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; | ||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin' | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig({ | ||
plugins: [react(), vanillaExtractPlugin()] | ||
plugins: [react(), vanillaExtractPlugin()], | ||
resolve: { | ||
alias: { | ||
'@react-three/xr': path.resolve(__dirname, '../src') | ||
} | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import * as THREE from 'three' | ||
import * as React from 'react' | ||
import { useFrame, useThree } from '@react-three/fiber' | ||
import { Interactive, type XRInteractionEvent } from './Interactions' | ||
|
||
const _q = /* @__PURE__ */ new THREE.Quaternion() | ||
|
||
/** | ||
* Teleport callback, accepting a world-space target position to teleport to. | ||
*/ | ||
export type TeleportCallback = (target: THREE.Vector3 | THREE.Vector3Tuple) => void | ||
|
||
/** | ||
* Returns a {@link TeleportCallback} to teleport the player to a position. | ||
*/ | ||
export function useTeleportation(): TeleportCallback { | ||
const frame = React.useRef<XRFrame>() | ||
const baseReferenceSpace = React.useRef<XRReferenceSpace | null>(null) | ||
const teleportReferenceSpace = React.useRef<XRReferenceSpace | null>(null) | ||
|
||
useFrame((state, _, xrFrame) => { | ||
frame.current = xrFrame | ||
|
||
const referenceSpace = state.gl.xr.getReferenceSpace() | ||
baseReferenceSpace.current ??= referenceSpace | ||
|
||
const teleportOffset = teleportReferenceSpace.current | ||
if (teleportOffset && referenceSpace !== teleportOffset) { | ||
state.gl.xr.setReferenceSpace(teleportOffset) | ||
} | ||
}) | ||
|
||
return React.useCallback((target) => { | ||
const base = baseReferenceSpace.current | ||
if (base) { | ||
const [x, y, z] = Array.from(target as THREE.Vector3Tuple) | ||
const offsetFromBase = { x: -x, y: -y, z: -z } | ||
|
||
const pose = frame.current?.getViewerPose(base) | ||
if (pose) { | ||
offsetFromBase.x += pose.transform.position.x | ||
offsetFromBase.z += pose.transform.position.z | ||
} | ||
|
||
const teleportOffset = new XRRigidTransform(offsetFromBase, _q) | ||
teleportReferenceSpace.current = base.getOffsetReferenceSpace(teleportOffset) | ||
} | ||
}, []) | ||
} | ||
|
||
export interface TeleportationPlaneProps extends Partial<JSX.IntrinsicElements['group']> { | ||
/** Whether to allow teleportation from left controller. Default is `false` */ | ||
leftHand?: boolean | ||
/** Whether to allow teleportation from right controller. Default is `false` */ | ||
rightHand?: boolean | ||
/** The maximum distance from the camera to the teleportation point. Default is `10` */ | ||
maxDistance?: number | ||
/** The radial size of the teleportation marker. Default is `0.25` */ | ||
size?: number | ||
} | ||
|
||
/** | ||
* Creates a teleportation plane with a marker that will teleport on interaction. | ||
*/ | ||
export const TeleportationPlane = React.forwardRef<THREE.Group, TeleportationPlaneProps>(function TeleportationPlane( | ||
{ leftHand = false, rightHand = false, maxDistance = 10, size = 0.25, ...props }, | ||
ref | ||
) { | ||
const teleport = useTeleportation() | ||
const marker = React.useRef<THREE.Mesh>(null!) | ||
const intersection = React.useRef<THREE.Vector3>() | ||
const camera = useThree((state) => state.camera) | ||
|
||
const isInteractive = React.useCallback( | ||
(e: XRInteractionEvent): boolean => { | ||
const { handedness } = e.target.inputSource | ||
return !!((handedness !== 'left' || leftHand) && (handedness !== 'right' || rightHand)) | ||
}, | ||
[leftHand, rightHand] | ||
) | ||
|
||
return ( | ||
<group ref={ref} {...props}> | ||
<mesh ref={marker} visible={false} rotation-x={-Math.PI / 2}> | ||
<circleGeometry args={[size, 32]} /> | ||
<meshBasicMaterial color="white" /> | ||
</mesh> | ||
<Interactive | ||
onMove={(e) => { | ||
if (!isInteractive(e) || !e.intersection) return | ||
|
||
const distanceFromCamera = e.intersection.point.distanceTo(camera.position) | ||
marker.current.visible = distanceFromCamera <= maxDistance | ||
marker.current.scale.setScalar(1) | ||
|
||
intersection.current = e.intersection.point | ||
marker.current.position.copy(intersection.current) | ||
}} | ||
onHover={(e) => { | ||
if (!isInteractive(e) || !e.intersection) return | ||
|
||
const distanceFromCamera = e.intersection.point.distanceTo(camera.position) | ||
marker.current.visible = distanceFromCamera <= maxDistance | ||
marker.current.scale.setScalar(1) | ||
}} | ||
onBlur={(e) => { | ||
if (!isInteractive(e)) return | ||
marker.current.visible = false | ||
}} | ||
onSelectStart={(e) => { | ||
if (!isInteractive(e) || !e.intersection) return | ||
|
||
const distanceFromCamera = e.intersection.point.distanceTo(camera.position) | ||
marker.current.visible = distanceFromCamera <= maxDistance | ||
marker.current.scale.setScalar(1.1) | ||
}} | ||
onSelectEnd={(e) => { | ||
if (!isInteractive(e) || !intersection.current) return | ||
|
||
marker.current.visible = true | ||
marker.current.scale.setScalar(1) | ||
|
||
const distanceFromCamera = intersection.current.distanceTo(camera.position) | ||
if (distanceFromCamera <= maxDistance) { | ||
teleport(intersection.current) | ||
} | ||
}} | ||
> | ||
<mesh rotation-x={-Math.PI / 2} visible={false} scale={1000}> | ||
<planeGeometry /> | ||
</mesh> | ||
</Interactive> | ||
</group> | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters