@threlte/xr
Getting Started
The package @threlte/xr
provides tools and abstractions to more easily create
VR and AR experiences.
<script lang="ts">
import { Canvas } from '@threlte/core'
import { World } from '@threlte/rapier'
import { VRButton } from '@threlte/xr'
import Scene from './Scene.svelte'
</script>
<div>
<Canvas>
<World gravity={[0, 0, 0]}>
<Scene />
</World>
</Canvas>
<VRButton />
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { Vector3 } from 'three'
import { T } from '@threlte/core'
import { InstancedMesh, Instance, RoundedBoxGeometry, Outlines } from '@threlte/extras'
import { Collider, RigidBody } from '@threlte/rapier'
const colors = [
'#ff5252',
'#ff4081',
'#d500f9',
'#3d5afe',
'#40c4ff',
'#18ffff',
'#f9a825',
'#ffd740',
'#bf360c'
] as const
const positions = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 0],
[0, 1],
[1, -1],
[1, 0],
[1, 1]
] as const
type Block = {
position: Vector3
color: string
}
let cubes: Block[] = []
let numCubes = 100
const margin = 0.4
const spacing = 8
for (let i = 0; i < numCubes; i += 1) {
const [x, y] = positions[Math.trunc(Math.random() * positions.length)]!
cubes.push({
position: new Vector3(x - margin, y - margin, -i * spacing),
color: colors[i % colors.length]!
})
}
const boxRadius = 0.15
const boxSize = 0.6
const offsetY = 1.8
const offsetZ = 50
const speed = 9
</script>
<InstancedMesh limit={numCubes}>
<RoundedBoxGeometry
radius={boxRadius}
args={[boxSize, boxSize, boxSize]}
/>
<T.MeshStandardMaterial
roughness={0}
metalness={0.2}
/>
<Outlines />
{#each cubes as { position, color }, index (index)}
<T.Group
position.x={position.x}
position.y={position.y + offsetY}
position.z={position.z - offsetZ}
>
<RigidBody linearVelocity={[0, 0, speed]}>
<Collider
shape="cuboid"
mass={0.5}
args={[boxSize / 2, boxSize / 2, boxSize / 2]}
/>
<Instance {color} />
</RigidBody>
</T.Group>
{/each}
</InstancedMesh>
<script lang="ts">
import { Vector2 } from 'three'
import { T } from '@threlte/core'
import { Edges } from '@threlte/extras'
const positions = Array(35)
.keys()
.map((index) => {
const size = Math.random() * 20 + 4
return {
size,
position: new Vector2(Math.cos(index), Math.sin(index))
.subScalar(0.5)
.normalize()
.multiplyScalar(100)
}
})
</script>
<T.Mesh
rotation.x={-Math.PI / 2}
position.y={-0.1}
>
<T.CircleGeometry args={[100]} />
<T.MeshBasicMaterial color="rgb(14, 22, 37)" />
</T.Mesh>
{#each positions as { position, size }}
<T.Group
position={[position.x, size / 2 - 1, position.y]}
oncreate={(ref) => ref.lookAt(0, size / 2, 0)}
>
<T.Mesh rotation.z={Math.PI / 2}>
<T.CircleGeometry args={[size, 3]} />
<Edges />
<T.MeshBasicMaterial color="rgb(14, 22, 37)" />
</T.Mesh>
</T.Group>
{/each}
<script lang="ts">
import { Vector3, Quaternion, Group } from 'three'
import { T, useTask } from '@threlte/core'
import { FakeGlowMaterial, Outlines } from '@threlte/extras'
import { Collider, RigidBody } from '@threlte/rapier'
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { Controller, Hand, useXR } from '@threlte/xr'
const { isHandTracking } = useXR()
let rigidBodyLeft = $state.raw<RapierRigidBody>()
let rigidBodyRight = $state.raw<RapierRigidBody>()
let leftSaber = $state.raw<Group>()
let rightSaber = $state.raw<Group>()
let leftHandSaber = $state.raw<Group>()
let rightHandSaber = $state.raw<Group>()
const left = $derived($isHandTracking ? leftHandSaber : leftSaber)
const right = $derived($isHandTracking ? rightHandSaber : rightSaber)
const vec3 = new Vector3()
const quaternion = new Quaternion()
useTask(() => {
if (left) {
rigidBodyLeft?.setTranslation(left.getWorldPosition(vec3), true)
rigidBodyLeft?.setRotation(left.getWorldQuaternion(quaternion), true)
}
if (right) {
rigidBodyRight?.setTranslation(right.getWorldPosition(vec3), true)
rigidBodyRight?.setRotation(right.getWorldQuaternion(quaternion), true)
}
})
const saberRadius = 0.02
const saberLength = 1.4
</script>
{#snippet saber()}
<T.Mesh>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
<T.Mesh position={[0, saberLength / 2 + 0.05, 0]}>
<T.CylinderGeometry args={[saberRadius, saberRadius, 0.1]} />
<T.MeshStandardMaterial
color="gray"
roughness={0}
metalness={0.5}
/>
</T.Mesh>
<T.Mesh>
<T.CylinderGeometry args={[saberRadius, saberRadius, saberLength]} />
<FakeGlowMaterial glowColor="red" />
<Outlines
color="hotpink"
thickness={0.005}
/>
</T.Mesh>
{/snippet}
<Controller left>
<T.Group
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
bind:ref={leftSaber}
>
{@render saber()}
</T.Group>
</Controller>
<Controller right>
<T.Group
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
bind:ref={rightSaber}
>
{@render saber()}
</T.Group>
</Controller>
<Hand left>
{#snippet wrist()}
<T.Group
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
bind:ref={leftHandSaber}
>
{@render saber()}
</T.Group>
{/snippet}
</Hand>
<Hand right>
{#snippet wrist()}
<T.Group
rotation.x={Math.PI / 2}
position.z={-saberLength / 2}
bind:ref={rightHandSaber}
>
{@render saber()}
</T.Group>
{/snippet}
</Hand>
<RigidBody
type="kinematicPosition"
bind:rigidBody={rigidBodyLeft}
>
<Collider
shape="capsule"
args={[saberLength / 2, saberRadius]}
/>
</RigidBody>
<RigidBody
type="kinematicPosition"
bind:rigidBody={rigidBodyRight}
>
<Collider
shape="capsule"
args={[saberLength / 2, saberRadius]}
/>
</RigidBody>
<script lang="ts">
import { Color } from 'three'
import { T, useThrelte } from '@threlte/core'
import { Text, Grid, Outlines, VirtualEnvironment, Stars } from '@threlte/extras'
import { XR, useXR } from '@threlte/xr'
import Sabers from './Sabers.svelte'
import Blocks from './Blocks.svelte'
import Mountains from './Mountains.svelte'
import { Spring } from 'svelte/motion'
const { scene } = useThrelte()
const { isPresenting } = useXR()
scene.environmentIntensity = 2
scene.background = new Color('#0e1625')
const spring = new Spring(1, { stiffness: 0.1, damping: 0.5 })
$effect.pre(() => {
spring.set($isPresenting ? 0 : 1)
})
</script>
<XR>
<Sabers />
<Blocks />
{#snippet fallback()}
<T.PerspectiveCamera
makeDefault
position={[0, 1.8, 1]}
oncreate={(ref) => {
ref.lookAt(0, 1.8, 0)
}}
/>
{/snippet}
</XR>
<Text
anchorX="center"
anchorY="center"
position={[0, 1.9, 0]}
text="bonksaber!"
font="/fonts/adrip1.ttf"
color="red"
fillOpacity={spring.current}
strokeOpacity={spring.current}
/>
<T.AmbientLight />
<T.DirectionalLight />
<Mountains />
<Stars />
<Grid
infiniteGrid
cellColor="purple"
type="lines"
axis="x"
/>
<!-- floor -->
<T.Mesh>
<T.CylinderGeometry args={[2, 2, 0.1, 128]} />
<T.MeshStandardMaterial
color="white"
roughness={0}
metalness={0.1}
/>
<Outlines color="hotpink" />
</T.Mesh>
<!-- sun -->
<T.Mesh
position={[-30, 40, -100]}
oncreate={(ref) => ref.lookAt(0, 0, 0)}
>
<T.MeshBasicMaterial color="#FF4F4F" />
<T.CircleGeometry args={[5 / 2]} />
</T.Mesh>
<VirtualEnvironment frames={20}>
<T.Mesh
position={[-8, 8, -10]}
oncreate={(ref) => ref.lookAt(0, 0, 0)}
>
<T.MeshBasicMaterial color="#FF4F4F" />
<T.CircleGeometry args={[5 / 2]} />
</T.Mesh>
<T.Mesh
position={[6, 8, -10]}
oncreate={(ref) => ref.lookAt(0, 0, 0)}
>
<T.PlaneGeometry args={[10, 10]} />
<T.MeshBasicMaterial color="#FFD0CB" />
</T.Mesh>
<T.Mesh
position={[4, 10, 5]}
oncreate={(ref) => ref.lookAt(0, 0, 0)}
>
<T.PlaneGeometry args={[10, 10]} />
<T.MeshBasicMaterial color="#2223FF" />
</T.Mesh>
</VirtualEnvironment>
Installation
npm install @threlte/xr
Usage
Setup
The following adds a button to start your session and controllers inside an XR manager to prepare your scene for WebXR rendering and interaction.
<script>
import { Canvas } from '@threlte/core'
import { VRButton } from '@threlte/xr'
import Scene from './scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>
<VRButton />
Then, in scene.svelte
:
<script>
import { XR, Controller, Hand } from '@threlte/xr'
</script>
<XR />
<Controller left />
<Controller right />
<Hand left />
<Hand right />
This will set up your project to be able to enter a VR session with controllers and hand inputs added.
If you want hands, controllers, or any other objects to be added to your
THREE.Scene
only when the XR session starts, make them children of the <XR>
component:
<script>
import { XR, Controller, Hand } from '@threlte/xr'
</script>
<XR>
<Controller left />
<Controller right />
<Hand left />
<Hand right />
</XR>
The <XR>
, <Controller>
, and <Hand>
components can provide a powerful
foundation when composed with other Threlte components.
HTML
HTML cannot be rendered inside an XR environment, this is just a limitation of the WebXR API. An alternative approach for creating an HTML-like UI within your XR session is to use the threlte-uikit package.