threlte logo
@threlte/xr

teleportControls

The teleportControls plugin creates teleportation controls similar to many native XR experiences: pressing the thumbstick forward on a controller will create a visible ray to a teleport destination, and when the the thumbstick is released the user will be teleported to the end of the ray.

<script>
  import { teleportControls } from '@threlte/xr'
  teleportControls('left' | 'right')
</script>

Any mesh within this component and all child components can now be treated as a navigation mesh to which the user can teleport to.

To register a mesh with teleportControls, add a teleportSurface property.

<T.Mesh teleportSurface>
  <T.CylinderGeometry args={[20, 0.01]} />
  <T.MeshStandardMaterial />
</T.Mesh>

If you wish to add teleport controls for both hands / controllers, simply call the plugin for both hands.

<script>
  import { teleportControls } from '@threlte/xr'
  teleportControls('left')
  teleportControls('right')
</script>

Teleport controls can be enabled or disabled when initialized or during runtime.

<script>
  import { teleportControls } from '@threlte/xr'
  // "enabled" is a currentWritable
  const { enabled } = teleportControls('left', { enabled: false })

  // At some later time...
  enabled.set(true)
</script>

A mesh can also be registered as a teleportBlocker, meaning that it will prevent teleportation through it. This can be useful when creating walls and doors that the user must navigate around.

<T.Mesh teleportBlocker>
  <T.BoxGeometry args={[0.8, 2, 0.1]} />
  <T.MeshStandardMaterial />
</T.Mesh>

This plugin can be composed with the teleportControls plugin to allow both teleporting and interaction.

<script>
  import { pointerControls, teleportControls } from '@threlte/xr'
  teleportControls('left')
  pointerControls('right')
</script>

This will result in pointerControls taking over when pointing at a mesh with events, and teleportControls taking over otherwise.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas } from '@threlte/core'
  import { Pane, Checkbox } from 'svelte-tweakpane-ui'
  import { VRButton } from '@threlte/xr'

  let showSurfaces = $state(false)
  let showBlockers = $state(false)
</script>

<Pane
  title="Teleport objects"
  position="fixed"
>
  <Checkbox
    bind:value={showSurfaces}
    label="Show teleport surfaces"
  />
  <Checkbox
    bind:value={showBlockers}
    label="Show teleport blockers"
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {showSurfaces}
      {showBlockers}
    />
  </Canvas>
  <VRButton />
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import Surfaces from './Surfaces.svelte'
  import { OrbitControls, Sky, useDraco, useGltf } from '@threlte/extras'
  import { PointLight } from 'three'
  import { SimplexNoise } from 'three/examples/jsm/Addons.js'
  import { T, useTask } from '@threlte/core'
  import { XR, Controller, Hand } from '@threlte/xr'

  type Props = {
    showBlockers: boolean
    showSurfaces: boolean
  }

  let { showBlockers, showSurfaces }: Props = $props()

  const noise = new SimplexNoise()

  const light1 = new PointLight()
  const light2 = new PointLight()

  let torchX = $state(0)
  let torchZ = $state(0)
  let candlesX = $state(0)
  let candlesZ = $state(0)

  const dracoLoader = useDraco()
  const gltf = useGltf('/models/xr/ruins.glb', {
    dracoLoader
  }).then((gltf) => {
    gltf.scene.traverse((node) => {
      node.castShadow = true
      node.receiveShadow = true
    })
    torchX = gltf.nodes.Torch1.position.x
    torchZ = gltf.nodes.Torch1.position.z
    candlesX = gltf.nodes.Torch1.position.x
    candlesZ = gltf.nodes.Torch1.position.z

    return gltf
  })

  let time = 0

  useTask((delta) => {
    time += delta / 5
    const x = noise.noise(time, 0) / 10
    const y = noise.noise(0, time) / 10
    const lightPositionX = torchX + x
    const lightPositionZ = torchZ + y
    light1.position.x = lightPositionX
    light2.position.x = lightPositionX
    light1.position.z = lightPositionZ
    light2.position.z = lightPositionZ
  })
</script>

<XR>
  <Controller left />
  <Controller right />
  <Hand left />
  <Hand right />

  {#snippet fallback()}
    <T.PerspectiveCamera
      makeDefault
      position.y={1.8}
      position.z={1.5}
      oncreate={(ref) => ref.lookAt(0, 1.8, 0)}
    >
      <OrbitControls
        target={[0, 1.8, 0]}
        enablePan={false}
        enableZoom={false}
      />
    </T.PerspectiveCamera>
  {/snippet}
</XR>

{#await gltf then { scene, nodes }}
  <T is={scene} />
  <T
    is={light1}
    intensity={8}
    color="red"
    position.y={nodes.Torch1.position.y + 0.45}
  />

  <T
    is={light2}
    intensity={4}
    color="red"
    position.y={nodes.Candles1.position.y + 0.45}
  />
{/await}

<Sky
  elevation={-3}
  rayleigh={8}
  azimuth={-90}
/>

<Surfaces
  {showSurfaces}
  {showBlockers}
/>

<T.AmbientLight intensity={0.25} />

<T.DirectionalLight
  intensity={0.5}
  position={[5, 5, 1]}
  castShadow
  shadow.camera.top={50}
  shadow.camera.right={50}
  shadow.camera.left={-50}
  shadow.camera.bottom={-50}
  shadow.mapSize.width={1024}
  shadow.mapSize.height={1024}
  shadow.camera.far={10}
/>
<script lang="ts">
  import type { Snippet } from 'svelte'
  import { T } from '@threlte/core'
  import { teleportControls } from '@threlte/xr'
  import { useDraco, useGltf } from '@threlte/extras'

  type Props = {
    children?: Snippet
    showBlockers: boolean
    showSurfaces: boolean
  }

  let { children, showBlockers, showSurfaces }: Props = $props()

  teleportControls('left')
  teleportControls('right')

  const dracoLoader = useDraco()
  const gltf = useGltf('/models/xr/ruins.glb', {
    dracoLoader
  })
</script>

{@render children?.()}

{#await gltf then { nodes }}
  {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as n}
    <T
      is={nodes[`teleportBlocker${n}`]}
      visible={showBlockers}
      teleportBlocker
    />
  {/each}

  {#each [1, 2, 3] as n}
    <T
      is={nodes[`teleportSurface${n}`]}
      visible={showSurfaces}
      teleportSurface
    />
  {/each}
{/await}