Skip to content

Repository files navigation

183804

YALI · CAMPUS 3D

A browser-based 3D viewer for the Yali Middle School campus, built on photogrammetry reconstruction tiles. Fly through the grounds freely with keyboard + mouse — no plugins required.

React TypeScript Vite three.js Tailwind CSS


Features

  • Browser-native 3D — Renders 18 photogrammetry OBJ tiles with textured phong materials in a WebGL2 canvas via React Three Fiber.
  • Hybrid controls — Orbit / pan / zoom with the mouse, plus WASD + Q/E keyboard fly controls for first-person-style exploration. Shift doubles the fly speed.
  • Atmosphere switch — Toggle between day and dusk lighting moods with matching sky gradient and fog.
  • Resilient loader — Serial download→parse queue with capped exponential-backoff retries and a recovery pass so a single aborted fetch never cascades.
  • Performance-aware — Frustum culling per tile, DPR clamped to 2, live FPS meter, target 60 fps desktop / 30 fps mobile.
  • "Survey archive" aesthetic — Deep charcoal backdrop with a single amber accent, glassmorphic overlays, serif + monospaced typography.

Tech Stack

Layer Choice
Framework React 18 + TypeScript + Vite 6
3D three 0.169 · @react-three/fiber · @react-three/drei · @react-three/postprocessing
State Zustand
Styling Tailwind CSS 3
Icons lucide-react
Backend None — pure static assets

Project Structure

Models/OBJ/Data/            # 18 photogrammetry tiles (*.obj + *.mtl + *.jpg)
public/Models               # directory junction -> ../Models (served by Vite publicDir)
src/
├── components/
│   ├── scene/              # CampusScene, TileModels, CameraRig, Lighting, SkyDome, FpsMeter
│   └── ui/                 # TopBar, ControlHints, LoadingScreen
├── pages/Home.tsx          # single page: 3D viewport + UI overlays + loading screen
├── store/sceneStore.ts     # Zustand store (load progress / atmosphere / fps)
├── three/
│   └── tiles.ts            # MTL+OBJ loader, serial queue with retries
└── App.tsx

Getting Started

Prerequisites

  • Node.js 18+
  • The public/Models junction must point at the project-root Models/ folder. Create it once:
New-Item -ItemType Junction -Path public\Models -Target Models

Install & Run

npm install
npm run dev      # start Vite dev server
npm run build    # type-check + production build
npm run preview  # preview the production build
npm run check    # tsc --noEmit type-check only

Controls

Input Action
Left mouse drag Orbit
Right mouse drag Pan
Mouse wheel Zoom
W / S Fly forward / back (along the camera's horizontal forward axis)
A / D Strafe left / right
Q / E Move down / up
Shift (held) Double fly speed

Keyboard input is captured on the canvas element. WASD motion is projected onto the horizontal plane so flight stays level regardless of pitch; the OrbitControls target follows the camera so orbit and free-flight stay coherent.

The Model

The scene is driven by a photogrammetric reconstruction of the Yali Middle School campus, exported as a tiled OBJ dataset.

Source & Format

  • Origin — Photogrammetry reconstruction produced by DaSpatial (the MTL header reads Wavefront MTL generated by www.daspatial.com for 3D Users). The original capture is a drone aerial survey of the campus.
  • Container — Wavefront OBJ geometry + companion MTL material file per tile, plus one or more JPEG textures referenced by map_Kd.
  • Layout — 18 tiles arranged in a 4×5 (with gaps) ENU grid under Models/OBJ/Data/<Tile>/, named Tile_+XXX_+YYY after their grid offsets.
Tile_+000_+001  Tile_+000_+002  Tile_+000_+003
Tile_+001_+000  Tile_+001_+001  Tile_+001_+002  Tile_+001_+003  Tile_+001_+004
Tile_+002_+000  Tile_+002_+001  Tile_+002_+002  Tile_+002_+003  Tile_+002_+004
Tile_+003_+000  Tile_+003_+001  Tile_+003_+002  Tile_+003_+003  Tile_+003_+004
  • Per-tile contents<Tile>.obj (geometry), <Tile>.mtl (material), and 1–3 JPEG textures named <Tile>_texture_N.jpg. Most tiles use a single texture; a few denser ones split albedo into 2–3 atlases.

Spatial Reference

Models/OBJ/metadata.xml records the geodetic anchor:

<ModelMetadata version="1">
    <SRS>ENU:28.1721900000,112.9845800000</SRS>
    <SRSOrigin>-444.000000,-510.000000,28.000000</SRSOrigin>
</ModelMetadata>
  • SRS — Local East-North-Up (ENU) tangent frame, anchored at 28.17219°N, 112.98458°E (Changsha, Hunan — the Yali Middle School campus).
  • SRSOrigin(-444, -510, 28) meters; the model's local origin is offset so vertex coordinates stay in a manageable numeric range.
  • Vertex axes — Raw OBJ coordinates are ENU meters with Z-up: X ≈ 200–600 (east), Y ≈ 295–800 (north), Z ≈ 24–52 (up, i.e. elevation above the anchor).

Runtime Alignment

The viewer applies a fixed transform at load time so the campus sits at the three.js origin with ground at y = 0:

  1. Rotate the assembled tile group by -π/2 around X so the Z-up ENU height axis maps to three.js's Y-up.
  2. Compute the world-space bounding box of the rotated group.
  3. Translate by (-centerX, -minY, -centerZ) so the campus is horizontally centered and the lowest ground point drops to y = 0.

OrbitControls targets the origin; the camera starts at [420, 320, 460] for an overview framing of the whole site.

Material Model

Each tile's MTL declares a Lambertian-style material with white ambient/diffuse, zero specular, and a single diffuse map_Kd JPEG. At runtime the loader forces side = FrontSide (photogrammetry shells are single-sided) and leaves the rest to three's built-in phong shading plus the scene's hemisphere + directional lighting.

Loading Strategy

  • Serial queue (concurrency = 1)OBJLoader parses each large OBJ synchronously on the main thread. With concurrency > 1, a parse blocks the main thread and starves/aborts in-flight sibling fetches (net::ERR_ABORTED). Serial download→parse guarantees no fetch is in flight during a parse.
  • Retry with backoff — Each tile download is wrapped with capped exponential backoff + jitter (5 retries, 500ms base). fetch uses cache: 'no-store' to bypass dev-server 304 revalidation paths that interact badly with three's FileLoader under load.
  • Recovery pass — Tiles that still fail after the first pass get a second quiet pass once connections are idle; most aborts succeed here.
  • Non-fatal partial loads — A failed tile is logged but does not block the rest from rendering.

UI Design

  • Palette — charcoal #0E0F13 background, amber #E8A33D single accent, fog gray #A8A8AE secondary text, slate #3A3D44 panels.
  • TypographyCormorant Garamond for titles (serif, archival), JetBrains Mono for UI (monospaced, technical).
  • Layout — full-bleed 3D canvas with edge-anchored glassmorphic overlays; nothing invades the central viewport.
  • Responsive — desktop-first (1280px+); tablet collapses the side panel into a drawer; mobile moves panels to a bottom drawer and hides the FPS meter.

Performance & Compatibility

  • Frustum culling — every tile mesh has frustumCulled = true; three.js skips off-screen tiles automatically.
  • DPR clampdpr capped at Math.min(devicePixelRatio, 2) to avoid mobile oversampling.
  • Keep-alive tuning — a custom tameKeepAlive Vite plugin widens the dev server's keep-alive window so the browser never races a connection reuse against a closing socket (a major source of net::ERR_ABORTED on large OBJ/texture streams).
  • Targets — 60 fps on desktop, 30 fps on mobile; ~30–50k vertices total across all tiles.
  • Browsers — any WebGL2-capable browser from the past 2 years (Chrome / Edge / Firefox / Safari).

License

Project-specific. Model data © its respective photogrammetry source.

About

3D model of Yali High School, for educational display.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages