threlte logo
@threlte/xr

Getting Started

The package @threlte/xr provides tools and abstractions to more easily create VR and AR experiences.

Installation

Terminal
npm install @threlte/xr

Usage

@threlte/xr is in beta. Major API changes at this point are not expected, but some breaking changes may occur before it reaches 1.0.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.

Scene.svelte
<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>

<div>
  <Canvas>
    <World gravity={[0, 0, 0]}>
      <Scene />
    </World>
  </Canvas>
  <VRButton />
</div>

<style>
  div {
    height: 100%;
  }
</style>
<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, useTask } 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()

  useTask(() => {
    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>