Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>3DGS Viewer</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { width: 100%; height: 100%; overflow: hidden; | |
| background: radial-gradient(ellipse at 50% 60%, #1a2035 0%, #080b12 100%); } | |
| canvas { display: block; } | |
| #hud { | |
| position: fixed; top: 12px; left: 16px; | |
| color: #94a3b8; font: 12px/1.5 system-ui, sans-serif; | |
| pointer-events: none; user-select: none; | |
| } | |
| #loading { | |
| position: fixed; inset: 0; | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| color: #94a3b8; font: 14px system-ui, sans-serif; | |
| gap: 12px; | |
| } | |
| #spinner { | |
| width: 36px; height: 36px; | |
| border: 3px solid #334155; | |
| border-top-color: #60a5fa; | |
| border-radius: 50%; | |
| animation: spin 0.9s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| #error { | |
| display: none; | |
| position: fixed; inset: 0; | |
| align-items: center; justify-content: center; | |
| color: #f87171; font: 13px system-ui, sans-serif; | |
| padding: 32px; text-align: center; white-space: pre-wrap; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading"> | |
| <div id="spinner"></div> | |
| <div id="loading-label">Loading splat…</div> | |
| </div> | |
| <div id="error"></div> | |
| <div id="hud">drag to orbit · scroll to zoom · right-drag to pan</div> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.180.0/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.180.0/examples/jsm/", | |
| "@sparkjsdev/spark": "https://unpkg.com/@sparkjsdev/spark@2.0.0/dist/spark.module.js" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| import * as THREE from "three"; | |
| import { OrbitControls } from "three/addons/controls/OrbitControls.js"; | |
| import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark"; | |
| const params = new URLSearchParams(location.search); | |
| const plyURL = params.get("ply"); | |
| const loadingEl = document.getElementById("loading"); | |
| const loadLabel = document.getElementById("loading-label"); | |
| const errorEl = document.getElementById("error"); | |
| function showError(msg) { | |
| loadingEl.style.display = "none"; | |
| errorEl.style.display = "flex"; | |
| errorEl.textContent = msg; | |
| } | |
| if (!plyURL) { | |
| showError("No ?ply= parameter provided."); | |
| } else { | |
| init(plyURL); | |
| } | |
| function init(url) { | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000); | |
| camera.position.set(0, 0.3, 1.8); | |
| const renderer = new THREE.WebGLRenderer({ antialias: false }); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.body.appendChild(renderer.domElement); | |
| const spark = new SparkRenderer({ renderer }); | |
| scene.add(spark); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.07; | |
| controls.minDistance = 0.2; | |
| controls.maxDistance = 50; | |
| controls.target.set(0, 0, 0); | |
| const splat = new SplatMesh({ url }); | |
| // DEG saves PLYs with -Y up and the front of the object facing roughly | |
| // along the horizontal axis. Re-orient for Three.js (+Y up, camera on | |
| // +Z looking toward -Z): a Group lets us apply a yaw first (to bring the | |
| // front-facing side to +Z) and then flip 180° around X to put up on +Y. | |
| const splatRoot = new THREE.Group(); | |
| splatRoot.add(splat); | |
| splat.rotation.y = Math.PI / 2; // yaw the model so its front faces the camera | |
| splatRoot.rotation.x = Math.PI; // flip Y/Z so it stands upright | |
| scene.add(splatRoot); | |
| let framed = false; | |
| function tryFrame() { | |
| if (framed) return; | |
| const box = new THREE.Box3().setFromObject(splatRoot); | |
| if (box.isEmpty() || !isFinite(box.min.x)) return; | |
| framed = true; | |
| // const center = new THREE.Vector3(); | |
| // const size = new THREE.Vector3(); | |
| // box.getCenter(center); | |
| // box.getSize(size); | |
| // console.log(size); | |
| // const maxDim = Math.max(size.x, size.y, size.z); | |
| // // With a 45° FOV, half-fov tan ≈ 0.414, so the minimum distance to fit | |
| // // a sphere of diameter `maxDim` in view is maxDim / (2*0.414) ≈ 1.21*maxDim. | |
| // // Use a small margin (1.15) so the object nearly fills the viewport. | |
| // const dist = maxDim * 1.15; | |
| // camera.position.copy(center).add(new THREE.Vector3(0, maxDim * 0.15, dist)); | |
| // controls.target.copy(center); | |
| // controls.update(); | |
| loadingEl.style.display = "none"; | |
| } | |
| fetch(url, { method: "HEAD" }).then(r => { | |
| if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`); | |
| const len = r.headers.get("content-length"); | |
| if (len) loadLabel.textContent = `Loading splat (${(len / 1024 / 1024).toFixed(1)} MB)…`; | |
| }).catch(e => showError("Fetch failed: " + e.message)); | |
| let checkFrames = 0; | |
| function checkLoaded() { | |
| checkFrames++; | |
| if (checkFrames < 5) return; | |
| tryFrame(); | |
| } | |
| window.addEventListener("resize", () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| renderer.setAnimationLoop(() => { | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| checkLoaded(); | |
| }); | |
| // Fallback: hide loading after 4s regardless of bbox-readiness. | |
| setTimeout(() => { loadingEl.style.display = "none"; }, 4000); | |
| } | |
| </script> | |
| </body> | |
| </html> | |