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
Section titled “Installation”npm install @threlte/xrThe 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 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.