Skip to content

Architecture

Alex Coulombe edited this page Apr 30, 2026 · 1 revision

Architecture

Holodeck-in-a-Pocket is a static single-page app: one HTML file, one JS bundle, zero backend.

Stack

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

File layout

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

Scene module contract

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.

Three-scene design: Four Seasons {#four-seasons-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

Carol — Stave 1 {#carol-scene}

  • Options: lighting (gas / ghost / ember), fog (off / thin / thick)
  • Small room (12×12m) with four-poster bed, fireplace, window
  • Each lighting preset remaps the PointLight colors and GlowLayer intensity
  • Model target: public/models/carol/bedroom.glb

D&D Encounter {#dnd-scene}

  • 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

Asset loading pipeline

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);

Build pipeline

npm run build    # tsc --noEmit && vite build → dist/
npm run preview  # serve dist/ locally

Vite 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.


Controls

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: WebXRDefaultExperience in main.ts enables immersive-vr mode when the device supports it

Clone this wiki locally