@threlte/xr
Getting Started
The package @threlte/xr
provides tools and abstractions to more easily create VR and AR experiences.
Installation
npm install @threlte/xr
Usage
@threlte/xr
is early in development. It is possible there will be frequent breaking changes
until it reaches 0.1.0.
Setup
The following adds a button to start your session and controllers inside an XR manager to prepare your scene for WebXR rendering and interaction.
<script>
import { Canvas } from '@threlte/core'
import { VRButton } from '@threlte/xr'
import Scene from './scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>
<VRButton />
Then, in scene.svelte
:
<script>
import { XR, Controller, Hand } from '@threlte/xr'
</script>
<XR />
<Controller left />
<Controller right />
<Hand left />
<Hand right />
This will set up your project to be able to enter a VR session with controllers and hand inputs added.
If you want hands, controllers, or any other objects to be added to your THREE.Scene
only when the XR session starts, make them children of the <XR>
component:
<script>
import { XR, Controller, Hand } from '@threlte/xr'
</script>
<XR>
<Controller left />
<Controller right />
<Hand left />
<Hand right />
</XR>
The <XR>
, <Controller>
, and <Hand>
components can provide a powerful foundation when composed with other Threlte components.
For example, it doesn’t take much more to get to the point of a simple BeatSaber-inspired experience:
<script lang='ts'>
import { Canvas } from '@threlte/core'
import { World } from '@threlte/rapier'
import { VRButton } from '@threlte/xr'
import Scene from './Scene.svelte'
</script>
<Canvas>
<World gravity={[0, 0, 0]}>
<Scene />
</World>
</Canvas>
<VRButton />
<script lang='ts'>
import * as THREE from 'three'
import { T } from '@threlte/core'
import { InstancedMesh, Instance, RoundedBoxGeometry } from '@threlte/extras'
import { Collider, RigidBody } from '@threlte/rapier'
const colors = ['#ff5252', '#ff4081', '#d500f9', '#3d5afe', '#40c4ff', '#18ffff', '#f9a825', '#ffd740', '#bf360c'] as const
const positions = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]] as const
type Block = {
position: THREE.Vector3
color: string
}
let cubes: Block[] = []
let numCubes = 100
const margin = 0.4
const spacing = 8
for (let i = 0; i < numCubes; i += 1) {
const [x, y] = positions[Math.trunc(Math.random() * positions.length)]!
cubes.push({
position: new THREE.Vector3(x - margin, y - margin, -i * spacing),
color: colors[i % colors.length]!
})
}
const boxRadius = 0.15
const boxSize = 0.6
const offsetY = 1.5
const offsetZ = 50
const speed = 12
</script>
<InstancedMesh limit={numCubes}>
<RoundedBoxGeometry radius={boxRadius} args={[boxSize, boxSize, boxSize]} />
<T.MeshStandardMaterial roughness={0} metalness={0.2} />
{#each cubes as { position, color }, index (index)}
<T.Group
position.x={position.x}
position.y={position.y + offsetY}
position.z={position.z - offsetZ}
>
<RigidBody linearVelocity={[0, 0, speed]}>
<Collider shape='cuboid' mass={0.5} args={[boxSize / 2, boxSize / 2, boxSize / 2]} />
<Instance {color} />
</RigidBody>
</T.Group>
{/each}
</InstancedMesh>
<script lang='ts'>
import * as THREE from 'three'
import { T, useFrame } from '@threlte/core'
import { Collider, RigidBody } from '@threlte/rapier'
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { Controller, Hand, useXR } from '@threlte/xr'
const { isHandTracking } = useXR()
let rigidBodyLeft: RapierRigidBody
let rigidBodyRight: RapierRigidBody
const sabers: { left: THREE.Mesh, right: THREE.Mesh } = { left: undefined!, right: undefined! }
const handSabers: { left: THREE.Mesh, right: THREE.Mesh } = { left: undefined!, right: undefined! }
const v3 = new THREE.Vector3()
const q = new THREE.Quaternion()
useFrame(() => {
const left = isHandTracking.current ? handSabers.left : sabers.left
const right = isHandTracking.current ? handSabers.right : sabers.right
if (left) {
rigidBodyLeft.setTranslation(left.getWorldPosition(v3), true)
rigidBodyLeft.setRotation(left.getWorldQuaternion(q), true)
}
if (right) {
rigidBodyRight.setTranslation(right.getWorldPosition(v3), true)
rigidBodyRight.setRotation(right.getWorldQuaternion(q), true)
}
})
const saberRadius = 0.02
const saberLength = 1.4
</script>
<Controller left>
<T.Mesh
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
on:create={({ ref }) => (sabers.left = ref)}
>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<T.MeshPhongMaterial color='red' />
</T.Mesh>
</Controller>
<Controller right>
<T.Mesh
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
on:create={({ ref }) => (sabers.right = ref)}
>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<T.MeshStandardMaterial roughness={0} color='red' />
</T.Mesh>
</Controller>
<Hand left>
<T.Mesh
slot='wrist'
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
on:create={({ ref }) => (handSabers.left = ref)}
>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<T.MeshStandardMaterial roughness={0} color='red' />
</T.Mesh>
</Hand>
<Hand right>
<T.Mesh
slot='wrist'
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
on:create={({ ref }) => (handSabers.right = ref)}
>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<T.MeshPhongMaterial color='red' />
</T.Mesh>
</Hand>
<RigidBody type='kinematicPosition' bind:rigidBody={rigidBodyLeft}>
<Collider shape='capsule' args={[saberLength / 2, saberRadius]} />
</RigidBody>
<RigidBody type='kinematicPosition' bind:rigidBody={rigidBodyRight}>
<Collider shape='capsule' args={[saberLength / 2, saberRadius]} />
</RigidBody>
<script lang='ts'>
import { T } from '@threlte/core'
import { XR } from '@threlte/xr'
import Sabers from './Sabers.svelte'
import Blocks from './Blocks.svelte'
</script>
<XR>
<Sabers />
<Blocks />
</XR>
<T.AmbientLight />
<T.DirectionalLight />
<T.PerspectiveCamera
makeDefault
position={[0, 1.8, 1]}
on:create={({ ref }) => ref.lookAt(0, 1.8, 0)}
/>
<!-- floor -->
<T.Mesh position.y={-50}>
<T.CylinderGeometry args={[2, 2, 100]} />
<T.MeshStandardMaterial color='white' />
</T.Mesh>