threlte logo
@threlte/extras

<HUD>

Renders a heads-up-display (HUD). Each HUD creates a new scene rendered on top of the main scene with a separate Threlte context and camera.

The HUD component creates a partially new Threlte context, specifically a new scene and camera. Everything else in useThrelte is preserved and reused.

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

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

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { interactivity, useCursor, useViewport } from '@threlte/extras'
  import { Mesh, Quaternion } from 'three'

  interface Props {
    quaternion: Quaternion
    onselect: (arg: string) => void
  }

  let { quaternion, onselect }: Props = $props()

  const viewport = useViewport()

  let meshes: [Mesh, Mesh, Mesh] = [null!, null!, null!]

  const boxCursor = useCursor('pointer')
  const torusCursor = useCursor('pointer')
  const torusKnotCursor = useCursor('pointer')

  interactivity()

  useTask(
    () => {
      for (const mesh of meshes) {
        mesh.quaternion.copy(quaternion)
      }
    },
    { autoInvalidate: false }
  )

  const boxHovering = boxCursor.hovering
  const torusHovering = torusCursor.hovering
  const torusKnotHovering = torusKnotCursor.hovering
</script>

<T.OrthographicCamera
  makeDefault
  zoom={80}
  position={[0, 0, 10]}
/>
<T.AmbientLight intensity={Math.PI / 2} />

<T.PointLight
  position={[10, 10, 10]}
  decay={0}
  intensity={Math.PI * 2}
/>

<T.Mesh
  bind:ref={meshes[0]}
  position={[$viewport.width / 2 - 1, $viewport.height / 2 - 1, 0]}
  onpointerenter={boxCursor.onPointerEnter}
  onpointerleave={boxCursor.onPointerLeave}
  onclick={() => onselect('box')}
  scale={$boxHovering ? 1.1 : 1}
>
  <T.BoxGeometry args={[0.5, 0.5, 0.5]} />
  <T.MeshToonMaterial color={$boxHovering ? 'hotpink' : 'gray'} />
</T.Mesh>

<T.Mesh
  bind:ref={meshes[1]}
  position={[$viewport.width / 2 - 2, $viewport.height / 2 - 1, 0]}
  onpointerenter={torusCursor.onPointerEnter}
  onpointerleave={torusCursor.onPointerLeave}
  onclick={() => onselect('torus')}
  scale={$torusHovering ? 1.1 : 1}
>
  <T.TorusGeometry args={[0.25, 0.1]} />
  <T.MeshToonMaterial color={$torusHovering ? 'hotpink' : 'gray'} />
</T.Mesh>

<T.Mesh
  bind:ref={meshes[2]}
  position={[$viewport.width / 2 - 3, $viewport.height / 2 - 1, 0]}
  onpointerover={torusKnotCursor.onPointerEnter}
  onpointerleave={torusKnotCursor.onPointerLeave}
  onclick={() => onselect('torusknot')}
  scale={$torusKnotHovering ? 1.1 : 1}
>
  <T.TorusKnotGeometry args={[0.215, 0.08, 256]} />
  <T.MeshToonMaterial color={$torusKnotHovering ? 'hotpink' : 'gray'} />
</T.Mesh>
<script lang="ts">
  import { T, useTask, useThrelte } from '@threlte/core'
  import { Float, OrbitControls, HUD } from '@threlte/extras'
  import { Quaternion } from 'three'
  import HudScene from './HudScene.svelte'

  let selected = $state('box')
  let rotation = $state(0)

  const quaternion = new Quaternion()
  const { camera } = useThrelte()

  useTask(
    (delta) => {
      rotation += delta

      // Spin mesh to the inverse of the default cameras matrix
      quaternion.copy(camera.current.quaternion).invert()
    },
    { autoInvalidate: false }
  )
</script>

<T.PerspectiveCamera
  position={[11, 5, 11]}
  makeDefault
  fov={30}
>
  <OrbitControls enableZoom={false} />
</T.PerspectiveCamera>

<T.DirectionalLight position={[0, 10, 10]} />

<T.AmbientLight intensity={0.6} />

<T.GridHelper args={[5]} />

<HUD>
  <HudScene
    {quaternion}
    onselect={(arg) => {
      selected = arg
    }}
  />
</HUD>

<Float
  speed={8}
  rotation.y={rotation}
>
  {#if selected === 'box'}
    <T.Mesh
      position.y={0.8}
      scale={2}
    >
      <T.BoxGeometry args={[0.5, 0.5, 0.5]} />
      <T.MeshToonMaterial color="turquoise" />
    </T.Mesh>
  {:else if selected === 'torus'}
    <T.Mesh
      position.y={0.8}
      scale={1.8}
    >
      <T.TorusGeometry args={[0.25, 0.1]} />
      <T.MeshToonMaterial color="turquoise" />
    </T.Mesh>
  {:else if selected === 'torusknot'}
    <T.Mesh
      position.y={0.8}
      scale={1.8}
    >
      <T.TorusKnotGeometry args={[0.215, 0.08, 256]} />
      <T.MeshToonMaterial color="turquoise" />
    </T.Mesh>
  {/if}
</Float>

Because creating a <HUD> is somewhat similar to creating a <Canvas>, it is recommended to use the same best practices and place all objects you want in the HUD within a new Scene component:

MyHUD.svelte
<script>
  import Scene from './Scene.svelte'
</script>

<HUD>
  <Scene />
</HUD>
Scene.svelte
<script>
  import { T } from '@threlte/core'
</script>

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

<T.AmbientLight />

<T.Mesh>
  <T.BoxGeometry />
  <T.MeshStandardMaterial />
</T.Mesh>

Component Signature

Props

name
type
required
description

autoRender
boolean
no
Whether the HUD should automatically render its contents

stage
Stage
no
Defaults to the render stage

toneMapping
THREE.ToneMapping
no
Defaults to the parent context toneMapping