main.ts
examples/spark/basic-xr/main.ts · raw · open the example · all examples · site home
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
// Spark's basic-xr: a cozy cottage on the desktop or in WebXR, rendering
// paused after 30 seconds without movement. The file holds a LoD tree;
// Radix picks no LoD nodes, so this draws its leaves (every original
// splat) through an index list where Spark draws a budget of LoD nodes.
// The controls move the camera, and in XR its parent, the local frame.
import * as THREE from "three";
import GUI from "lil-gui";
import { CameraControls, SplatCollection, SplatRenderer } from "@sparkjsdev/radix";
import { asset } from "../js/assets.js";
import { examplePage, windowRenderer } from "../js/page.js";
import { fileObject, loadSplatFile } from "../js/files.js";
import { setupXr, xrBackend } from "../js/xr.js";
const RENDER_TIMEOUT_MS = 30 * 1000;
const page = examplePage("basic-xr", { note: "draws every leaf: no LoD selection" });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.01, 1000);
const localFrame = new THREE.Group();
localFrame.add(camera);
scene.add(localFrame);
// createRenderer, sized to the window and added to the page; it follows
// window resizes, the camera's aspect too (js/page.ts)
const renderer = await windowRenderer(camera, { xr: true, backend: xrBackend() });
const controls = new CameraControls({ canvas: renderer.domElement, object: camera, storageKey: "spark-basic-xr" });
const splats = new SplatRenderer({ renderer, params: { maxStdDev: Math.sqrt(4) } });
scene.add(splats);
setupXr(renderer, [splats], controls);
const world = new THREE.Object3D();
world.position.set(0, -1, 0);
scene.add(world);
// A LoD file: its leaves through an index list (file.indices)
const file = await loadSplatFile(renderer, asset("cozy-cottage-lod.spz"), { layout: "packedSplats", onProgress: page.progress("cozy cottage") });
splats.collection = new SplatCollection([fileObject(file, { matrixWorld: world.matrixWorld })]);
let renderEnabled = true;
const gui = new GUI({ title: "Settings" });
gui.add({ disableRender: () => (renderEnabled = false) }, "disableRender").name("Disable render");
gui.add(splats.params, "maxStdDev", 1, Math.sqrt(8), 0.01);
page.ready(renderer);
if (file.indices) {
page.status(`${file.indices.count.toLocaleString()} leaves of ${file.data.count.toLocaleString()} LoD nodes`);
setTimeout(() => page.status(""), 4000);
}
let lastMoved = 0;
const lastPos = new THREE.Vector3().setScalar(-Infinity);
const lastDir = new THREE.Vector3();
const pos = new THREE.Vector3();
const dir = new THREE.Vector3();
renderer.setAnimationLoop((time) => {
controls.update();
camera.getWorldPosition(pos);
camera.getWorldDirection(dir);
if (pos.distanceTo(lastPos) > 0.0001 || dir.dot(lastDir) < 0.9999) {
lastMoved = time;
renderEnabled = true;
lastPos.copy(pos);
lastDir.copy(dir);
}
if (time - lastMoved > RENDER_TIMEOUT_MS) {
renderEnabled = false;
}
if (renderEnabled || renderer.xr.isPresenting) {
renderer.render(scene, camera);
}
});