Skip to content

Commit b7a7673

Browse files
authored
Merge pull request #186 from nsalminen/main
Add new 3DGS walkthrough demo
2 parents 6eb9c51 + 99dec71 commit b7a7673

File tree

5 files changed

+255
-0
lines changed

5 files changed

+255
-0
lines changed

demos/3dgs-walkthrough/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<title>3DGS Scene Walkthrough</title>
5+
<meta charset="utf-8" />
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, user-scalable=no"
9+
/>
10+
<link type="text/css" rel="stylesheet" href="../demo.css" />
11+
<script>
12+
window.litDisableBundleWarning = true;
13+
</script>
14+
<script type="importmap">
15+
{
16+
"imports": {
17+
"three": "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js",
18+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/",
19+
"@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/0.1.10/spark.module.js",
20+
"xrblocks": "../../build/xrblocks.js",
21+
"xrblocks/addons/": "../../build/addons/"
22+
}
23+
}
24+
</script>
25+
</head>
26+
27+
<body>
28+
<script type="module" src="main.js"></script>
29+
</body>
30+
</html>

demos/3dgs-walkthrough/main.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import {LongSelectHandler} from 'xrblocks/addons/ui/LongSelectHandler.js';
2+
3+
import {SplatMesh, SparkRenderer} from '@sparkjsdev/spark';
4+
import * as THREE from 'three';
5+
import * as xb from 'xrblocks';
6+
7+
const PROPRIETARY_ASSETS_BASE_URL =
8+
'https://cdn.jsdelivr.net/gh/xrblocks/proprietary-assets@main/';
9+
10+
const SPLAT_ASSETS = [
11+
{
12+
url: PROPRIETARY_ASSETS_BASE_URL + '3dgs_scenes/nyc.spz',
13+
scale: new THREE.Vector3(1.3, 1.3, 1.3),
14+
position: new THREE.Vector3(0, -0.15, 0),
15+
quaternion: new THREE.Quaternion(1, 0, 0, 0),
16+
},
17+
{
18+
url: PROPRIETARY_ASSETS_BASE_URL + '3dgs_scenes/alameda.spz',
19+
scale: new THREE.Vector3(1.3, 1.3, 1.3),
20+
position: new THREE.Vector3(0, 0, 0),
21+
quaternion: new THREE.Quaternion(1, 0, 0, 0),
22+
},
23+
];
24+
25+
const FADE_DURATION_S = 1.0; // seconds
26+
const MOVE_SPEED = 0.05;
27+
28+
function easeInOutSine(x) {
29+
return -(Math.cos(Math.PI * x) - 1) / 2;
30+
}
31+
32+
const forward = new THREE.Vector3();
33+
const right = new THREE.Vector3();
34+
const moveDirection = new THREE.Vector3();
35+
36+
/**
37+
* An XR-Blocks demo that displays room-scale 3DGS models, allowing smooth
38+
* transitions via number keys (1, 2) or a 1.5 s long-pinch.
39+
*/
40+
class WalkthroughManager extends xb.Script {
41+
async init() {
42+
this.add(new THREE.HemisphereLight(0xffffff, 0x666666, 3));
43+
44+
// Load all splat meshes in parallel.
45+
this.splatMeshes = await Promise.all(
46+
SPLAT_ASSETS.map(async (asset) => {
47+
const mesh = new SplatMesh({url: asset.url});
48+
await mesh.initialized;
49+
mesh.position.copy(asset.position);
50+
mesh.quaternion.copy(asset.quaternion);
51+
mesh.scale.copy(asset.scale);
52+
return mesh;
53+
})
54+
);
55+
56+
// Create a SparkRenderer for gaussian splat rendering and register it so
57+
// the simulator can toggle encodeLinear for correct color space.
58+
const sparkRenderer = new SparkRenderer({
59+
renderer: xb.core.renderer,
60+
maxStdDev: Math.sqrt(5),
61+
});
62+
xb.core.registry.register(new xb.SparkRendererHolder(sparkRenderer));
63+
xb.add(sparkRenderer);
64+
65+
// Show the first splat.
66+
this.currentIndex = 0;
67+
xb.add(this.splatMeshes[this.currentIndex]);
68+
69+
// fadeProgress tracks animation time: null = idle, 0‥FADE_DURATION_S =
70+
// fading out, FADE_DURATION_S‥2×FADE_DURATION_S = fading in.
71+
this.fadeProgress = null;
72+
this.nextIndex = null;
73+
74+
// Locomotion state.
75+
this.locomotionOffset = new THREE.Vector3();
76+
this.baseReferenceSpace = null;
77+
this.keys = {w: false, a: false, s: false, d: false};
78+
79+
document.addEventListener('keydown', this.onKeyDown.bind(this));
80+
document.addEventListener('keyup', this.onKeyUp.bind(this));
81+
82+
xb.add(
83+
new LongSelectHandler(this.cycleSplat.bind(this), {
84+
triggerDelay: 1500,
85+
triggerCooldownDuration: 1500,
86+
})
87+
);
88+
}
89+
90+
/** Starts a crossfade to the next splat (wrapping around). */
91+
cycleSplat() {
92+
if (this.fadeProgress !== null) return;
93+
this.nextIndex = (this.currentIndex + 1) % this.splatMeshes.length;
94+
this.fadeProgress = 0;
95+
}
96+
97+
onKeyDown(event) {
98+
const key = event.key.toLowerCase();
99+
if (key in this.keys) this.keys[key] = true;
100+
101+
// Number key → jump to that splat (1-indexed).
102+
const idx = parseInt(key, 10) - 1;
103+
if (
104+
idx >= 0 &&
105+
idx < this.splatMeshes.length &&
106+
idx !== this.currentIndex &&
107+
this.fadeProgress === null
108+
) {
109+
this.nextIndex = idx;
110+
this.fadeProgress = 0;
111+
}
112+
}
113+
114+
onKeyUp(event) {
115+
const key = event.key.toLowerCase();
116+
if (key in this.keys) this.keys[key] = false;
117+
}
118+
119+
onXRSessionEnded() {
120+
super.onXRSessionEnded();
121+
this.baseReferenceSpace = null;
122+
this.locomotionOffset.set(0, 0, 0);
123+
}
124+
125+
update() {
126+
super.update();
127+
const dt = xb.getDeltaTime();
128+
129+
this.updateFade(dt);
130+
this.updateLocomotion();
131+
}
132+
133+
/** Handles the fade-out → fade-in crossfade between splats. */
134+
updateFade(dt) {
135+
if (this.fadeProgress === null) return;
136+
137+
this.fadeProgress += dt;
138+
const currentMesh = this.splatMeshes[this.currentIndex];
139+
140+
if (this.fadeProgress < FADE_DURATION_S) {
141+
// Fading out the current splat.
142+
currentMesh.opacity =
143+
1 - easeInOutSine(this.fadeProgress / FADE_DURATION_S);
144+
} else if (this.fadeProgress < 2 * FADE_DURATION_S) {
145+
// Swap on the first frame of the fade-in phase.
146+
if (currentMesh.parent) {
147+
xb.scene.remove(currentMesh);
148+
this.currentIndex = this.nextIndex;
149+
const nextMesh = this.splatMeshes[this.currentIndex];
150+
nextMesh.opacity = 0;
151+
xb.add(nextMesh);
152+
}
153+
// Fading in the new splat.
154+
const inProgress =
155+
(this.fadeProgress - FADE_DURATION_S) / FADE_DURATION_S;
156+
this.splatMeshes[this.currentIndex].opacity = easeInOutSine(inProgress);
157+
} else {
158+
// Fade complete.
159+
this.splatMeshes[this.currentIndex].opacity = 1;
160+
this.fadeProgress = null;
161+
this.nextIndex = null;
162+
}
163+
}
164+
165+
/** WASD locomotion via XR reference space offset. */
166+
updateLocomotion() {
167+
const xr = xb.core.renderer?.xr;
168+
if (!xr?.isPresenting) return;
169+
170+
const camera = xr.getCamera();
171+
if (!camera) return;
172+
173+
camera.getWorldDirection(forward);
174+
forward.y = 0;
175+
forward.normalize();
176+
right.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
177+
178+
moveDirection.set(0, 0, 0);
179+
if (this.keys.w) moveDirection.add(forward);
180+
if (this.keys.s) moveDirection.sub(forward);
181+
if (this.keys.a) moveDirection.sub(right);
182+
if (this.keys.d) moveDirection.add(right);
183+
if (moveDirection.lengthSq() === 0) return;
184+
moveDirection.normalize();
185+
186+
if (!this.baseReferenceSpace) {
187+
this.baseReferenceSpace = xr.getReferenceSpace();
188+
}
189+
190+
this.locomotionOffset.addScaledVector(moveDirection, -MOVE_SPEED);
191+
const transform = new XRRigidTransform(this.locomotionOffset);
192+
xr.setReferenceSpace(
193+
this.baseReferenceSpace.getOffsetReferenceSpace(transform)
194+
);
195+
}
196+
}
197+
198+
document.addEventListener('DOMContentLoaded', function () {
199+
const options = new xb.Options();
200+
options.reticles.enabled = false;
201+
options.hands.enabled = true;
202+
options.hands.visualization = true;
203+
options.hands.visualizeMeshes = true;
204+
options.simulator.scenePath = null; // Prevent simulator scene from loading.
205+
206+
xb.add(new WalkthroughManager());
207+
xb.init(options);
208+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
id: 3DGS-Walkthrough
3+
title: 3DGS Walkthrough
4+
hide_title: true
5+
breadcrumbs: false
6+
pagination_next: null
7+
pagination_prev: null
8+
---
9+
10+
import {SamplesIFrame} from './SamplesIFrame';
11+
12+
<SamplesIFrame
13+
demo="3dgs-walkthrough"
14+
link="https://github.com/google/xrblocks/tree/main/demos/3dgs-walkthrough/"
15+
></SamplesIFrame>

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const sidebars: SidebarsConfig = {
8484
'samples/Math-Tutor',
8585
'samples/AI-Simulator',
8686
'samples/Virtual-Screens',
87+
'samples/3DGS-Walkthrough',
8788
],
8889
},
8990
],

src/xrblocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export * from './utils/ModelLoader';
109109
export * from './utils/ObjectPlacement';
110110
export * from './utils/RotationUtils';
111111
export * from './utils/SceneGraphUtils';
112+
export * from './utils/SparkRendererHolder';
112113
export * from './utils/Types';
113114
export * from './utils/utils';
114115
export * from './ux/DragManager';

0 commit comments

Comments
 (0)