TripoSplat / static /viewer /viewer.html
bennyguo
initial commit
c43e1bf
<!doctype html>
<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 &nbsp;·&nbsp; scroll to zoom &nbsp;·&nbsp; 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>