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 { Canvas } from '@threlte/core'
  import { VRButton } from '@threlte/xr'
  import Scene from './Scene.svelte'
  import { Pane, Checkbox } from 'svelte-tweakpane-ui'

  let showSurfaces = false
  let showBlockers = 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 { PointLight } from 'three'
  import { T, useTask } from '@threlte/core'
  import { OrbitControls, Sky, useGltf } from '@threlte/extras'
  import { XR, Controller, Hand } from '@threlte/xr'
  import Surfaces from './Surfaces.svelte'
  import { createNoise2D } from 'simplex-noise'

  export let showSurfaces: boolean
  export let showBlockers: boolean

  const noise = createNoise2D()

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

  const gltf = useGltf('/models/xr/ruins.glb', {
    useDraco: true
  })

  $: $gltf?.scene.traverse((node) => {
    node.castShadow = node.receiveShadow = true
  })

  let time = 0

  $: torchX = $gltf?.nodes.Torch1.position.x ?? 0
  $: torchZ = $gltf?.nodes.Torch1.position.z ?? 0
  $: candlesX = $gltf?.nodes.Candles1.position.x ?? 0
  $: candlesZ = $gltf?.nodes.Candles1.position.z ?? 0

  useTask((delta) => {
    time += delta / 5
    const x = noise(time, 0) / 10
    const y = noise(0, time) / 10

    light1.position.x = torchX + x
    light1.position.z = torchZ + y

    light2.position.x = candlesX + x
    light2.position.z = candlesZ + y
  })
</script>

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

  <T.PerspectiveCamera
    slot="fallback"
    makeDefault
    position.y={1.8}
    position.z={1.5}
  >
    <OrbitControls
      target={[0, 1.8, 0]}
      enablePan={false}
      enableZoom={false}
    />
  </T.PerspectiveCamera>
</XR>

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

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

<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 { T } from '@threlte/core'
  import { teleportControls } from '@threlte/xr'
  import { useGltf } from '@threlte/extras'

  export let showSurfaces: boolean
  export let showBlockers: boolean

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

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

<slot />

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

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