threlte logo
@threlte/xr

useHitTest

Provides a hit test result on each frame during an immersive-ar session.

Hit testing lets you position virtual items in a real-world view.

<script>
  import { useHitTest } from '@threlte/xr'

  let ref

  useHitTest((hitMatrix, hit) => {
    if (!ref) return
  
    if (hit) {
      ref.visible = true
      ref.matrix.copy(hitMatrix)
    } else {
      ref.visible = false
    }
  })
</script>

<T.Mesh bind:ref>
  <T.SphereGeometry args={[0.1]}>
  <T.MeshBasicMaterial />
</T.Mesh>

This hook can optionally specify one of three origins from which to cast the hit test ray: viewer (the default), leftInput or rightInput.

useHitTest((hitMatrix, hit) => {
  // Perform a hit test from the left controller or hand.
}, { source: 'leftInput' })

In the following example, hit testing is set up from both controllers and hands.

<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 * as THREE from 'three'
  import { T } from '@threlte/core'
  import { XR, Controller, Hand, useHitTest } from '@threlte/xr'

  const geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.2, 32).translate(0, 0.1, 0)

  let meshes: THREE.Mesh[] = []
  let cursors = { left: undefined! as THREE.Mesh, right: undefined! as THREE.Mesh }

  const hands = ['left', 'right'] as const
  type Hands = (typeof hands)[number]

  const handleSelect = (hand: Hands) => () => {
    if (!cursors[hand].visible) return

    const material = new THREE.MeshPhongMaterial({ color: 0xffffff * Math.random() })
    const mesh = new THREE.Mesh(geometry, material)
    cursors[hand].matrix.decompose(mesh.position, mesh.quaternion, mesh.scale)
    mesh.scale.y = Math.random() * 2 + 1
    meshes.push(mesh)
    meshes = meshes
  }

  const handleHitTest =
    (hand: Hands) => (hitMatrix: THREE.Matrix4, hit: XRHitTestResult | undefined) => {
      if (!cursors[hand]) return

      if (hit) {
        cursors[hand].visible = true
        cursors[hand].matrix.copy(hitMatrix)
      } else {
        cursors[hand].visible = false
      }
    }

  useHitTest(handleHitTest('left'), { source: 'leftInput' })
  useHitTest(handleHitTest('right'), { source: 'rightInput' })
</script>

<XR>
  {#each hands as hand}
    <Controller
      {hand}
      on:select={handleSelect(hand)}
    />
    <Hand
      {hand}
      on:pinchend={handleSelect(hand)}
    />
  {/each}
</XR>

<T.Mesh
  bind:ref={cursors.left}
  matrixAutoUpdate={false}
>
  <T.RingGeometry
    args={[0.15, 0.2, 32]}
    on:create={({ ref }) => ref.rotateX(-Math.PI / 2)}
  />
  <T.MeshBasicMaterial />
</T.Mesh>

<T.Mesh
  bind:ref={cursors.right}
  matrixAutoUpdate={false}
>
  <T.RingGeometry
    args={[0.15, 0.2, 32]}
    on:create={({ ref }) => ref.rotateX(-Math.PI / 2)}
  />
  <T.MeshBasicMaterial />
</T.Mesh>

<T.HemisphereLight
  args={[0xffffff, 0xbbbbff, 1]}
  position={[0.5, 1, 0.25]}
/>

<T.AmbientLight intensity={0.5} />

{#each meshes as mesh, index (index)}
  <T is={mesh} />
{/each}

Signature

useHitTest((hitMatrix: THREE.Matrix4, hit: XRHitTestResult | undefined) => {})