-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Alex Coulombe edited this page Apr 30, 2026
·
1 revision
Holodeck-in-a-Pocket is a static single-page app: one HTML file, one JS bundle, zero backend.
| Layer | Technology |
|---|---|
| 3D engine | Babylon.js v7 |
| Language | TypeScript (strict) |
| Build | Vite — outputs dist/ as a fully static bundle |
| Deploy | GitHub Pages via .github/workflows/deploy.yml
|
| XR | Babylon.js WebXRDefaultExperience — wires up automatically when the device supports it |
src/
main.ts — engine init, scene router, hash-change listener, XR setup, UI
types.ts — SceneDefinition, SceneApi, SceneId
controls.ts — WASD/mouse (desktop) + joystick/look-pad (touch)
brand.ts — shared Color3 constants (#FE00B5 pink, #3500A7 purple)
scenes/
four-seasons.ts — Four Seasons Lake Austin
carol.ts — A Christmas Carol Stave 1
dnd.ts — D&D Encounter (tavern + dungeon)
styles.css — minimal overlay UI
public/
models/
four-seasons/ — venue.glb
carol/ — bedroom.glb
dnd/ — tavern.glb, dungeon.glb
screenshots/ — PNG captures served from /screenshots/
index.html — single canvas + overlay divs
Every scene is a TypeScript module that exports one SceneDefinition as default:
interface SceneDefinition {
id: SceneId; // matches #scene=<id> in URL
title: string;
subtitle: string;
placeholder: boolean; // shows "Real venue model loads in the live demo" note when true
options: OptionDef[]; // 0–3 design-option dropdowns
build(engine: Engine): SceneApi;
}
interface SceneApi {
scene: Scene;
camera: Camera;
meshes: AbstractMesh[];
applyOption(id: string, value: string): void;
dispose(): void;
}build() is called once per scene load. applyOption() is called whenever a design-option dropdown changes. dispose() tears down the scene when the user navigates to a different scene.
- Default design option:
time-of-day(noon / golden / night) - Second option:
layout(rounds / theater / open mingle) - Spawn at
(0, 1.7, 8)looking toward the pavilion center - Model target:
public/models/four-seasons/venue.glb
- Options:
lighting(gas / ghost / ember),fog(off / thin / thick) - Small room (12×12m) with four-poster bed, fireplace, window
- Each lighting preset remaps the
PointLightcolors andGlowLayerintensity - Model target:
public/models/carol/bedroom.glb
- Options:
room(tavern / dungeon),tokens(off / on) - Two rooms sharing a wall at z=0 with a doorway
-
applyRoom()teleports the camera into the selected room - Model targets:
public/models/dnd/tavern.glb,public/models/dnd/dungeon.glb
import "@babylonjs/loaders/glTF"; // already imported in main.ts
const result = await BABYLON.SceneLoader.ImportMeshAsync(
"", // meshNames — "" = all
"/models/<slug>/", // rootUrl
"<filename>.glb", // sceneFilename
scene
);Babylon.js handles Draco-compressed GLBs automatically via the built-in Draco decoder (no extra import needed in v7).
For Gaussian splats (.spz Niantic format or .splat):
import "@babylonjs/loaders/gaussianSplatting";
const splat = new BABYLON.GaussianSplattingMesh("venue", "/models/four-seasons/venue.spz", scene);npm run build # tsc --noEmit && vite build → dist/
npm run preview # serve dist/ locallyVite tree-shakes Babylon.js aggressively — only imported submodules end up in the bundle. The output is a single JS chunk + index.html, deployable to any static host.
src/controls.ts wraps Babylon's UniversalCamera with:
- Desktop: WASD to move, mouse drag to look
- Touch: left-side joystick (MeshBuilder quad) for locomotion, right-side drag for look
-
WebXR:
WebXRDefaultExperienceinmain.tsenables immersive-vr mode when the device supports it