Initial Upload
Browse files- index.html +20 -17
- src/config.js +9 -0
- src/controls.js +84 -0
- src/viewer.js +56 -0
- style.css +23 -22
index.html
CHANGED
|
@@ -1,19 +1,22 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>3DGS Viewer</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css" />
|
| 8 |
+
<script type="importmap">
|
| 9 |
+
{
|
| 10 |
+
"imports": {
|
| 11 |
+
"three": "https://esm.sh/three@0.179.0",
|
| 12 |
+
"three/addons/": "https://esm.sh/three@0.179.0/examples/jsm/",
|
| 13 |
+
"@sparkjsdev/spark": "https://esm.sh/@sparkjsdev/spark@2?deps=three@0.179.0"
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
</script>
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<div id="loading">loading...</div>
|
| 20 |
+
<script type="module" src="src/viewer.js"></script>
|
| 21 |
+
</body>
|
| 22 |
</html>
|
src/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const SPLAT_URL =
|
| 2 |
+
"https://huggingface.co/datasets/dynamic-maps/hard-intersection-multimodal-sample/resolve/main/assets/jp_tokyo_takanawadai-lod.rad"
|
| 3 |
+
export const config = {
|
| 4 |
+
position: [0, 0, 0],
|
| 5 |
+
quaternion: [0, 0, 0, 1],
|
| 6 |
+
scale: 1,
|
| 7 |
+
camera: { position: [0, 0, 10], lookAt: [0, 0, 0] },
|
| 8 |
+
background: "#000000",
|
| 9 |
+
};
|
src/controls.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from "three";
|
| 2 |
+
|
| 3 |
+
const LOOK_SPEED = 0.003;
|
| 4 |
+
const _yawQ = new THREE.Quaternion();
|
| 5 |
+
const _pitQ = new THREE.Quaternion();
|
| 6 |
+
const raycaster = new THREE.Raycaster();
|
| 7 |
+
const _ndc = new THREE.Vector2();
|
| 8 |
+
|
| 9 |
+
function raycastAt(mesh, camera, clientX, clientY) {
|
| 10 |
+
_ndc.set((clientX / innerWidth) * 2 - 1, -(clientY / innerHeight) * 2 + 1);
|
| 11 |
+
raycaster.setFromCamera(_ndc, camera);
|
| 12 |
+
const hits = [];
|
| 13 |
+
mesh.raycast(raycaster, hits);
|
| 14 |
+
hits.sort((a, b) => a.distance - b.distance);
|
| 15 |
+
return hits[0] ?? null;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function setupControls(domElement, camera, mesh) {
|
| 19 |
+
let dragging = false,
|
| 20 |
+
dragButton = 0,
|
| 21 |
+
lastX = 0,
|
| 22 |
+
lastY = 0;
|
| 23 |
+
|
| 24 |
+
domElement.addEventListener("pointerdown", (e) => {
|
| 25 |
+
dragging = true;
|
| 26 |
+
dragButton = e.button;
|
| 27 |
+
lastX = e.clientX;
|
| 28 |
+
lastY = e.clientY;
|
| 29 |
+
domElement.setPointerCapture(e.pointerId);
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
domElement.addEventListener("pointermove", (e) => {
|
| 33 |
+
if (!dragging) return;
|
| 34 |
+
const dx = e.clientX - lastX,
|
| 35 |
+
dy = e.clientY - lastY;
|
| 36 |
+
lastX = e.clientX;
|
| 37 |
+
lastY = e.clientY;
|
| 38 |
+
|
| 39 |
+
if (dragButton === 0) {
|
| 40 |
+
// Left-drag: look around
|
| 41 |
+
const camUp = new THREE.Vector3(0, 1, 0).applyQuaternion(
|
| 42 |
+
camera.quaternion,
|
| 43 |
+
);
|
| 44 |
+
camera.quaternion.premultiply(
|
| 45 |
+
_yawQ.setFromAxisAngle(camUp, -dx * LOOK_SPEED),
|
| 46 |
+
);
|
| 47 |
+
const camRight = new THREE.Vector3(1, 0, 0).applyQuaternion(
|
| 48 |
+
camera.quaternion,
|
| 49 |
+
);
|
| 50 |
+
camera.quaternion.premultiply(
|
| 51 |
+
_pitQ.setFromAxisAngle(camRight, -dy * LOOK_SPEED),
|
| 52 |
+
);
|
| 53 |
+
camera.quaternion.normalize();
|
| 54 |
+
} else if (dragButton === 2) {
|
| 55 |
+
// Right-drag: pan
|
| 56 |
+
const hit = raycastAt(mesh, camera, lastX, lastY);
|
| 57 |
+
const panSpeed = (hit ? hit.distance : 5) * 0.001;
|
| 58 |
+
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(
|
| 59 |
+
camera.quaternion,
|
| 60 |
+
);
|
| 61 |
+
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
|
| 62 |
+
camera.position.addScaledVector(right, -dx * panSpeed);
|
| 63 |
+
camera.position.addScaledVector(up, dy * panSpeed);
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
domElement.addEventListener("pointerup", () => {
|
| 68 |
+
dragging = false;
|
| 69 |
+
});
|
| 70 |
+
domElement.addEventListener("contextmenu", (e) => e.preventDefault());
|
| 71 |
+
|
| 72 |
+
domElement.addEventListener(
|
| 73 |
+
"wheel",
|
| 74 |
+
(e) => {
|
| 75 |
+
const hit = raycastAt(mesh, camera, e.clientX, e.clientY);
|
| 76 |
+
const dist = hit ? hit.distance : 10;
|
| 77 |
+
camera.position.addScaledVector(
|
| 78 |
+
raycaster.ray.direction,
|
| 79 |
+
-(dist * e.deltaY * 0.001),
|
| 80 |
+
);
|
| 81 |
+
},
|
| 82 |
+
{ passive: true },
|
| 83 |
+
);
|
| 84 |
+
}
|
src/viewer.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from "three";
|
| 2 |
+
import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
|
| 3 |
+
import { SPLAT_URL, config } from "./config.js";
|
| 4 |
+
import { setupControls } from "./controls.js";
|
| 5 |
+
|
| 6 |
+
const loadingEl = document.getElementById("loading");
|
| 7 |
+
|
| 8 |
+
const scene = new THREE.Scene();
|
| 9 |
+
scene.background = new THREE.Color(config.background);
|
| 10 |
+
|
| 11 |
+
const camera = new THREE.PerspectiveCamera(
|
| 12 |
+
75,
|
| 13 |
+
innerWidth / innerHeight,
|
| 14 |
+
0.01,
|
| 15 |
+
1000000,
|
| 16 |
+
);
|
| 17 |
+
camera.position.set(...config.camera.position);
|
| 18 |
+
camera.lookAt(...config.camera.lookAt);
|
| 19 |
+
|
| 20 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 21 |
+
renderer.setPixelRatio(devicePixelRatio);
|
| 22 |
+
renderer.setSize(innerWidth, innerHeight);
|
| 23 |
+
document.body.appendChild(renderer.domElement);
|
| 24 |
+
|
| 25 |
+
const spark = new SparkRenderer({
|
| 26 |
+
renderer,
|
| 27 |
+
pagedExtSplats: true,
|
| 28 |
+
coneFov0: 70.0,
|
| 29 |
+
coneFov: 120.0,
|
| 30 |
+
behindFoveate: 0.2,
|
| 31 |
+
coneFoveate: 0.4,
|
| 32 |
+
});
|
| 33 |
+
scene.add(spark);
|
| 34 |
+
|
| 35 |
+
const mesh = new SplatMesh({ url: SPLAT_URL, paged: true, raycastable: true });
|
| 36 |
+
mesh.quaternion.set(...config.quaternion);
|
| 37 |
+
mesh.position.set(...config.position);
|
| 38 |
+
mesh.scale.setScalar(config.scale);
|
| 39 |
+
scene.add(mesh);
|
| 40 |
+
|
| 41 |
+
setupControls(renderer.domElement, camera, mesh);
|
| 42 |
+
|
| 43 |
+
window.addEventListener("resize", () => {
|
| 44 |
+
camera.aspect = innerWidth / innerHeight;
|
| 45 |
+
camera.updateProjectionMatrix();
|
| 46 |
+
renderer.setSize(innerWidth, innerHeight);
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
let firstFrame = true;
|
| 50 |
+
renderer.setAnimationLoop(() => {
|
| 51 |
+
renderer.render(scene, camera);
|
| 52 |
+
if (firstFrame) {
|
| 53 |
+
firstFrame = false;
|
| 54 |
+
loadingEl.classList.add("done");
|
| 55 |
+
}
|
| 56 |
+
});
|
style.css
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
|
| 4 |
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
margin-top: 0;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
}
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
margin: 0;
|
| 4 |
+
}
|
| 5 |
body {
|
| 6 |
+
background: #111;
|
| 7 |
+
overflow: hidden;
|
| 8 |
}
|
| 9 |
+
canvas {
|
| 10 |
+
display: block;
|
| 11 |
+
touch-action: none;
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
+
#loading {
|
| 15 |
+
position: fixed;
|
| 16 |
+
inset: 0;
|
| 17 |
+
display: flex;
|
| 18 |
+
align-items: center;
|
| 19 |
+
justify-content: center;
|
| 20 |
+
background: #111;
|
| 21 |
+
color: rgba(255, 255, 255, 0.4);
|
| 22 |
+
font: 12px monospace;
|
| 23 |
+
z-index: 100;
|
| 24 |
+
transition: opacity 0.4s;
|
| 25 |
}
|
| 26 |
+
#loading.done {
|
| 27 |
+
opacity: 0;
|
| 28 |
+
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|