threlte logo
@threlte/xr

<ARButton>

<ARButton /> is an HTML <button /> that can be used to init an AR session. It will also display info about browser support.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { ARButton } from '@threlte/xr'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
  <ARButton />
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { VirtualEnvironment } from '@threlte/extras'
  import { XR, Controller, Hand, pointerControls } from '@threlte/xr'
  import { type Group, Vector3 } from 'three'
  import Spaceship from './models/spaceship.svelte'
  import Stars from './Stars.svelte'

  pointerControls('left')
  pointerControls('right')

  const scale = 0.02
  // Toy hovering at eye level, ~30 cm ahead of the user.
  const home = new Vector3(0, 1.4, -0.3)

  let intersectionPoint: Vector3 | undefined
  let translAccelleration = 0
  let angleAccelleration = 0

  let spaceShipRef = $state<Group>()
  let translY = $state(0)
  let angleZ = $state(0)

  const up = new Vector3(0, 1, 0)
  const dir = new Vector3()
  const pivot = new Vector3()

  useTask(() => {
    if (intersectionPoint === undefined) return

    const targetY = intersectionPoint.y - home.y
    translAccelleration += (targetY - translY) * 0.01
    translAccelleration *= 0.92
    translY += translAccelleration

    pivot.set(home.x, home.y + translY, home.z)
    dir.copy(intersectionPoint).sub(pivot).normalize()
    const dirCos = dir.dot(up)
    const angle = Math.acos(dirCos) - Math.PI * 0.5
    angleAccelleration += (angle - angleZ) * 0.02
    angleAccelleration *= 0.9
    angleZ += angleAccelleration
  })
</script>

<XR />

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

<T.PerspectiveCamera
  makeDefault
  position={[0, 1.5, 0.3]}
  fov={50}
  oncreate={(ref) => {
    ref.lookAt(home)
  }}
/>

<T.DirectionalLight
  intensity={1.8}
  position={[0, 2, 0.5]}
  castShadow
  shadow.bias={-0.0001}
/>

<!-- Invisible ray-target plane. Controller and hand rays drive the
     spaceship's motion via event.point. -->
<T.Mesh
  position.x={home.x}
  position.y={home.y}
  position.z={home.z}
  visible={false}
  onpointermove={(event) => {
    intersectionPoint = event.point
  }}
>
  <T.PlaneGeometry args={[2, 2]} />
  <T.MeshBasicMaterial />
</T.Mesh>

<Spaceship
  bind:ref={spaceShipRef}
  {scale}
  position={[home.x, home.y + translY, home.z]}
  rotation={[angleZ, 0, angleZ, 'ZXY']}
/>

<!-- Stars are both visible in the main scene and captured into the cube
     map that lights the spaceship's reflections. -->
<VirtualEnvironment visible>
  <Stars />
</VirtualEnvironment>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { Instance, InstancedMesh, useTexture } from '@threlte/extras'
  import { Color, DoubleSide, MathUtils, type Vector3Tuple } from 'three'

  let STARS_COUNT = 350
  let colors = ['#fcaa67', '#C75D59', '#ffffc7', '#8CC5C6', '#A5898C'] as const
  let stars = $state<Star[]>([])

  const map = useTexture('/spaceship-tutorial/textures/star.png')

  function r(min: number, max: number): number {
    let diff = Math.random() * (max - min)
    return min + diff
  }

  interface Star {
    id: string
    position: Vector3Tuple
    length: number
    speed: number
    color: Color
  }

  function resetStar(star: Star) {
    if (r(0, 1) > 0.8) {
      star.position = [r(-10, -30), r(-5, 5), r(6, -6)]
      star.length = r(1.5, 15)
    } else {
      star.position = [r(-15, -45), r(-10.5, 1.5), r(30, -45)]
      star.length = r(2.5, 20)
    }

    star.speed = r(19.5, 42)
    star.color
      .set(colors[Math.floor(Math.random() * colors.length)] ?? 'white')
      .convertSRGBToLinear()
      .multiplyScalar(1.3)
  }

  for (let i = 0; i < STARS_COUNT; i++) {
    const star: Star = {
      id: MathUtils.generateUUID(),
      position: [0, 0, 0],
      length: 0,
      speed: 0,
      color: new Color()
    }

    resetStar(star)
    stars.push(star)
  }

  useTask((delta) => {
    for (const star of stars) {
      star.position[0] += star.speed * delta
      if (star.position[0] > 40) {
        resetStar(star)
      }
    }
  })
</script>

{#await map then value}
  <InstancedMesh
    limit={STARS_COUNT}
    range={STARS_COUNT}
    frustumCulled={false}
  >
    <T.PlaneGeometry args={[1, 0.05]} />
    <T.MeshBasicMaterial
      side={DoubleSide}
      alphaMap={value}
      transparent
    />

    {#each stars as { id, position, length, color } (id)}
      <Instance
        {position}
        scale={[length, 1, 1]}
        {color}
      />
    {/each}
  </InstancedMesh>
{/await}
https://sketchfab.com/3d-models/rusty-spaceship-orange-18541ebed6ce44a9923f9b8dc30d87f5
<!--
Auto-generated by: https://github.com/threlte/threlte/tree/main/packages/gltf
Command: npx @threlte/gltf@2.0.0 C:\Users\Utente\Desktop\Trasferimento-PC\Projects\Youtube\Threlte\spaceship-header\static\models\spaceship.glb --root /models/ --printwidth 120 --precision 2
Author: Sousinho (https://sketchfab.com/sousinho)
License: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
Source: https://sketchfab.com/3d-models/rusty-spaceship-orange-18541ebed6ce44a9923f9b8dc30d87f5
Title: Rusty Spaceship - Orange
-->
<script lang="ts">
  import type { Snippet } from 'svelte'
  import { AddEquation, CustomBlending, Group, LessEqualDepth, Material, OneFactor } from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useDraco, useTexture } from '@threlte/extras'

  interface Props {
    ref?: Group
    fallback?: Snippet
    error?: Snippet<[any]>
    children?: Snippet<[any]>
    [key: string]: any
  }

  let { fallback, error, children, ref = $bindable(), ...props }: Props = $props()

  const dracoLoader = useDraco()
  const gltf = useGltf('/spaceship-tutorial/models/spaceship-transformed.glb', {
    dracoLoader
  })
  const map = useTexture('/spaceship-tutorial/textures/energy-beam-opacity.png')

  function alphaFix(material: Material) {
    material.transparent = true
    material.alphaToCoverage = true
    material.depthFunc = LessEqualDepth
    material.depthTest = true
    material.depthWrite = true
  }

  gltf.then((model) => {
    alphaFix(model.materials.spaceship_racer)
    alphaFix(model.materials.cockpit)
  })
</script>

<T.Group
  bind:ref
  dispose={false}
  {...props}
>
  {#await gltf}
    {@render fallback?.()}
  {:then gltf}
    <T.Group
      scale={0.003}
      rotation={[0, -Math.PI * 0.5, 0]}
      position={[0.95, 0, 0]}
    >
      <T.Mesh
        castShadow
        receiveShadow
        geometry={gltf.nodes.Cube001_spaceship_racer_0.geometry}
        material={gltf.materials.spaceship_racer}
      />
      <T.Mesh
        castShadow
        receiveShadow
        geometry={gltf.nodes.Cube005_cockpit_0.geometry}
        material={gltf.materials.cockpit}
      />

      {#await map then mapValue}
        <T.Mesh
          position={[0, 0, -1350]}
          rotation.x={Math.PI * 0.5}
        >
          <T.CylinderGeometry args={[70, 25, 1600, 15]} />
          <T.MeshBasicMaterial
            color={[1.0, 0.4, 0.02]}
            alphaMap={mapValue}
            transparent
            blending={CustomBlending}
            blendDst={OneFactor}
            blendEquation={AddEquation}
          />
        </T.Mesh>
      {/await}
    </T.Group>
  {:catch err}
    {@render error?.({ error: err })}
  {/await}

  {@render children?.({ ref })}
</T.Group>

Component Signature

Events

name
payload
description

click
{ state: 'unsupported' | 'insecure' | 'blocked' | 'supported'; nativeEvent: MouseEvent }
Fired when a user clicks the AR button.

error
Error
Fired when an enter / exit session error occurs.