A 3D interactive ocean and terrain model of the Vesterålen archipelago in northern Norway, built with BabylonJS and Mapbox terrain data.
Demo can be found at: https://resist.hcilab.no/oceanmodel/
✅ Tested in Brave broweser and in Quest 3.
The application renders a table-top scale 3D terrain from Mapbox Terrain-RGB tiles and overlays geographic data such as aquaculture site locations. It supports both standard browser (2D/3D) and WebXR (VR headset) modes.
- Vite + TypeScript
- Tailwind CSS — utility-first styling
- BabylonJS v8 — 3D rendering engine + WebXR support
- Mapbox — DEM tiles for terrain elevation and texture
- Martini RTIN — adaptive mesh simplification from elevation data
- Real terrain geometry decoded from Mapbox Terrain-RGB DEM tiles with adaptive mesh simplification via
- Satellite texture overlay
- GeoJSON point layer — Norwegian Aquaculture Registry sites (colour-coded by status)
- WebXR support (VR headset with motion controllers)
- Hexagonal/ports-and-adapters architecture (data layer has zero BabylonJS dependency)
-
Install dependencies:
npm install
-
Set up your environment file:
cp .env.template .env
-
Add your Mapbox access token to
.env:VITE_MAPBOX_TOKEN=pk.your_token_hereSee the Mapbox access tokens guide if you need one.
-
Start the dev server:
npm run dev
-
Open the local URL shown in the terminal.
npm run buildPackage and publish the build to cpanel (if ssh is set-up)
scp -r ./dist/* cpanel:~/public_html/resist/oceanmodelWhen a compatible VR headset is connected, a "headset" button will appear on the lower right corner, press it to enter VR mode.
- Install Android SDK Platform Tools to get
adb - Connect with USB-C
- Run
adb devicesto see if the device is connected - Enable port forwarding with
adb reverse tcp:5173 tcp:5173ifhttp://localhost:5173
Using Immersive Web Emulator (IWE)
- Install Chromium-based browser
- Install IWE browser extension
- Launch the desktop browser’s developer tool panel
- Navigate to the “WebXR” tab to control the emulated device
src/
├── main.ts <- composition root only
├── data/
│ ├── types.ts <- LatLngAltLike, TerrainData, TerrainGeometry
│ ├── geo.ts <- pure coordinate math (no BabylonJS)
│ ├── ports/terrainProvider.ts <- ITerrainProvider interface
│ ├── adapters/mapboxTerrainAdapter.ts <- Mapbox tile fetch + DEM decode
│ ├── TerrainBuilder.ts <- Martini RTIN geometry builder
│ └── loaders/geojsonLoader.ts <- generic GeoJSON point loader
├── scene/
│ ├── SceneManager.ts <- Engine + Scene factory
│ ├── TerrainMesh.ts <- BabylonJS mesh + coordinate API
│ └── PointLayer.ts <- sphere markers for GeoJSON points
├── xr/XRManager.ts <- WebXRDefaultExperience setup
└── public/data/
└── Akvakulturregisteret150.geojson <- 150 aquaculture sites, Vesterålen
Layer rule: data/ never imports BabylonJS. Concrete adapters are wired only in main.ts.
Static data files live in public/data/ (served by Vite, not bundled).
Akvakulturregisteret150.geojson — 150 aquaculture localities from the Norwegian Aquaculture Registry covering the Vesterålen area