main.ts
examples/spark/viewer/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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
// Spark's viewer: drop, choose or paste the URL of a splat file (?url= too)
// and fly around it; the button saves it as SPZ (writeSplats). Radix
// reads PLY, SPZ, RAD and RDX; Spark also reads .splat, .ksplat and SOGS
// .zip files, and keeps spherical harmonics, which Radix skips.
import * as THREE from "three";
import { CameraControls, SplatCollection, SplatRenderer, loadSplatFile, writeSplats, type ArrayData, type ShPalette } from "@sparkjsdev/radix";
import { examplePage, windowRenderer } from "../js/page.js";
import { fileObject, fileOf, releaseFile } from "../js/files.js";
const page = examplePage("viewer", { note: "no .splat / .ksplat / .zip / .sog; no spherical harmonics" });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.01, 1000);
camera.position.set(0, 0, 1);
// 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);
const controls = new CameraControls({ canvas: renderer.domElement, object: camera, storageKey: "spark-viewer", settings: { mode: "fly" } });
// Most captures are stored upside down: half a turn about x
const holder = new THREE.Object3D();
holder.quaternion.set(1, 0, 0, 0);
scene.add(holder);
const splats = new SplatRenderer({ renderer });
scene.add(splats);
const element = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
const start = element("start");
const drop = element("drop");
const save = element<HTMLButtonElement>("save");
const fileInput = element<HTMLInputElement>("file");
const urlInput = element<HTMLInputElement>("url");
let shown: { data: ArrayData; shPalette?: ShPalette; name: string } | undefined;
// A new collection of the one file (LoD files: their leaves); the last
// one's arrays are freed once nothing draws it
async function load(source: string | File) {
const name = typeof source === "string" ? source.split("/").pop()!.split("?")[0] : source.name;
start.hidden = true;
const { splats: data, shPalette } = await loadSplatFile(source, { name, onProgress: page.progress(name) });
const file = fileOf(renderer, data, shPalette);
const release = () => releaseFile(file);
splats.collection = new SplatCollection([{ ...fileObject(file, { matrixWorld: holder.matrixWorld }), release }]);
shown = { data, shPalette, name };
save.hidden = false;
page.status("");
}
for (const target of [start, renderer.domElement]) {
target.addEventListener("dragover", (event) => {
event.preventDefault();
drop.classList.add("over");
});
target.addEventListener("dragleave", () => drop.classList.remove("over"));
target.addEventListener("drop", (event) => {
event.preventDefault();
drop.classList.remove("over");
const file = event.dataTransfer?.files[0];
if (file) {
load(file).catch(page.fail);
}
});
}
fileInput.onchange = () => fileInput.files?.[0] && load(fileInput.files[0]).catch(page.fail);
urlInput.form?.addEventListener("submit", (event) => {
event.preventDefault();
const url = new URL(location.href);
url.searchParams.set("url", urlInput.value);
history.pushState(null, "", url);
load(urlInput.value).catch(page.fail);
});
save.addEventListener("click", async () => {
if (!shown) {
return;
}
const bytes = await writeSplats(shown.data, "spz", { version: 3, shPalette: shown.shPalette });
const link = Object.assign(document.createElement("a"), { href: URL.createObjectURL(new Blob([bytes])), download: `${shown.name.split(".")[0]}.spz` });
link.click();
URL.revokeObjectURL(link.href);
});
const url = new URLSearchParams(location.search).get("url");
if (url) {
await load(url);
}
page.ready(renderer);
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});