@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>