The world was complete on disk before it existed in a browser.
Before I wrote a single line of React Three Fiber code, I decided the world needed to actually exist first — as a folder of assets on my machine, if not yet in a browser. The logic was simple: you can't write a scene if you don't know what's going to be in it. So I spent a long stretch of hours doing what felt less like coding and more like going on a very specific scavenger hunt across four websites I'd never used before.
I want to be clear about what I knew at this point: almost nothing. I had never sourced 3D assets. I didn't know what a GeoTIFF was, what PBR textures required, or why you couldn't just drop an MP3 into a Three.js scene and call it spatial audio. I was learning each of these things at the same time I was trying to use them.
The goal was Lauterbrunnen valley — specifically. Not a generic alpine world with stock assets. Real elevation data from the real place, surface textures that matched the actual geology, audio recordings of the sounds you'd actually hear standing in that valley. I had never been there. I was building it from photographs, Wikipedia elevation charts, and YouTube videos of people hiking through it.
The world was complete on disk before it existed in code. That turned out to be its own kind of progress.
The terrain was the most technically unfamiliar part. I knew the world needed real topography — not a hand-sculpted mesh, not a noise function. Lauterbrunnen has a very specific shape: a deep flat-bottomed valley carved between vertical cliff walls, with the valley floor around 800m and surrounding peaks at 3,000m+. That profile is what makes it recognisably Lauterbrunnen. A generic mountain heightmap would produce a generic mountain.
The first thing I searched was terrain.party — a tool I'd seen recommended everywhere in 3D web tutorials. Draw a box on a map, download a PNG heightmap, done. I spent twenty minutes trying to get it to load.
The standard recommendation for instant PNG heightmaps from real elevation data. Draw a rectangle on a map, export greyscale, done. Except it's been offline for a while with no replacement or explanation. Every tutorial pointing at it is pointing at a dead link. This was the first obstacle — find an alternative that outputs the same thing from a more technical source.
After some searching I landed on OpenTopography. It's built for geologists, not designers — you need a free account, the interface is clinical, and the output is a GeoTIFF rather than a PNG. I didn't know what a GeoTIFF was. I spent an hour figuring out how to convert it. The conversion step is where the whole build later failed — but at this stage it was just an unfamiliar file format standing between me and the data. The data itself was exactly right.
Real Lauterbrunnen SRTM elevation data at 30m resolution
What I got
output_SRTMGL1.tif
Raw elevation data at 30m resolution — the real valley profile
heightmap.png
Converted output — this is where the pipeline silently failed
Terrain shape is one thing. What it looks like — the texture of alpine grass, the roughness of a cliff face, the way wet mud catches light — is a completely separate problem. This is where I learned what PBR textures are: sets of images that together tell a 3D renderer how a surface behaves under light. Each set has a colour map, a normal map for micro-level light bounce, a roughness map, and ambient occlusion. You need all four for a surface to look physically real.
The plan was texture splatting — a shader that reads slope angle and altitude and blends materials procedurally. Grass in the valley floor. Rock on steep slopes. Mud near the river. Snow above 2,500m. Automatic, seamless, no manual painting. I had never written a vertex shader. I was looking forward to it. It never got tested.
Four PBR surface texture sets + alpine HDRI sky
What I got
alpine_grass
Colour, normal, roughness, AO at 2K — valley floor and lower slopes
rock_face
Cliff walls and exposed stone — steep angle surfaces
alpine_mud
Valley floor near the river channel
mountain_snow
High-elevation areas above the treeline
sky.hdr
Alpine lighting reference, cool morning temperature
On texture splatting: the shader reads slope angle and vertex height to decide which texture to apply, blending smoothly at transition zones. A vertex at 15° gets mostly grass. At 60° it fades to rock. Above 2,500m it transitions to snow. Procedural, no manual painting required. It was one of the parts of the build I was most looking forward to — and one of the parts that never got tested, because the heightmap conversion failed before the terrain had any valid slope data for the shader to read.
The terrain and surfaces were the foundation. The 3D objects — trees, grass, flowers — were what would make the scene feel inhabited. Alpine valleys have specific vegetation: birch and fir in the lower valley, shrubs and wildflowers on open slopes, nothing but rock and snow above the treeline.
I chose low-poly models deliberately. The plan was a world that felt designed rather than simulated — intentionally stylised geometry against physically-based textures and a real HDR sky. A contrast that says this place was made, not scanned. What I didn't anticipate was how badly that contrast would read when the terrain underneath was broken. Low-poly trees floating above a flat plane look like clip art dropped on a photograph. The aesthetic only works when all three layers work together.
Low-poly nature pack — trees, grass, flowers, alpine plants (CC0)
What I got
birch.glb
Valley floor and lower slopes
fir.glb
Denser coverage at higher elevations
grass_tuft.glb
Ground cover across meadows
wildflower.glb
Scattered in open areas
alpine_shrub.glb
Mid-elevation sparse coverage
The audio was the part I'd thought about most carefully before sourcing anything. The plan wasn't ambient background music — it was genuine spatial audio. Individual sound sources positioned in 3D space, volume responding to distance, using the Web Audio API's HRTF PannerNode for real binaural positioning through headphones. The cowbell is somewhere to your left. The waterfall gets louder as you approach it. You hear where you are.
I spent hours on Freesound finding individual sources — not layered ambience, not "forest atmosphere," but single cowbells, specific waterfall recordings, one eagle call. Individual sources can be positioned in 3D. Atmospheric layers cannot. The distinction mattered for how the spatial system was designed to work.
12 CC0 spatial audio recordings — individually sourced, zone-assigned
What I got
Zone 1 — Valley Floor
cowbell_distant.mp3
Zone 1 — Valley Floor
wind_light.mp3
Zone 2 — River Edge
river_flow.mp3
Zone 2 — River Edge
frogs_water.mp3
Zone 3 — Waterfall
waterfall_large.mp3
Zone 3 — Waterfall
mist_wind.mp3
Zone 4 — Forest Path
birds_forest.mp3
Zone 4 — Forest Path
leaves_rustling.mp3
Zone 5 — High Meadow
birds_open.mp3
Zone 5 — High Meadow
wind_strong.mp3
Zone 6 — Cliff Edge
eagle_call.mp3
Zone 6 — Cliff Edge
wind_high.mp3
The audio files were the most complete part of the entire build. All twelve loaded correctly. All were named and referenced properly in code. The AudioSystem component simply refused to mount inside the R3F Canvas — a React context conflict I didn't diagnose before stashing the branch. The valley ran in silence. The cowbell is still in the folder, waiting.
By the end of the sourcing phase, the asset folder was complete. Terrain data, four PBR texture sets, one HDRI sky, five 3D model files, twelve audio recordings. All that remained was the code that would put it together.
Terrain shape
SRTM data from OpenTopography. Correct elevation profile. In the folder.
Surface textures
Four PBR sets from Polyhaven at 2K. All maps present.
Sky lighting
Alpine HDRI from Polyhaven. Correct colour temperature.
3D objects
Five GLB files from Quaternius. Loaded and ready for instancing.
Spatial audio
12 CC0 recordings from Freesound. Zone-assigned. Correctly named.
Heightmap conversion
GeoTIFF to PNG failed silently. Terrain rendered completely flat.
Foliage placement
No terrain height data. Every object placed at the same wrong Y. Everything floated.
Audio mounting
React context conflict inside R3F Canvas. Twelve recordings. No sound.
Five of eight things worked. The three failures were all downstream of the same root problem: the heightmap conversion. The GeoTIFF from OpenTopography converted to a PNG without erroring — but the output was flat. Every vertex in the terrain mesh had the same Y value. The cliff walls, the valley floor, the river channel — all gone. A plane.
The cascade: flat terrain meant the foliage instancer had no height data to read. Without height data, every tree, every shrub, every piece of ground cover was placed at the same uniform Y position — which happened to be above where the lake mesh sat. Trees floating in midair. Lake suspended at the wrong elevation. Every layer vertically wrong. Three visible failures from one silent conversion error.
The audio context conflict was almost certainly fixable — it's a known issue with mounting Web Audio nodes inside R3F Canvas, and there are documented workarounds. I would have found it if I'd had a working scene to debug inside. Instead I had twelve audio files, a floating lake, and a flat plane that was supposed to be Lauterbrunnen.
The stash was the right call. The asset folder was intact. The failure modes were documented. The next session started with the heightmap pipeline — just that, verified completely — before touching anything else.
The conversion has been attempted a second time. The GeoTIFF was re-processed with proper 0–255 normalization. The heightmap now has real values — actual elevation data from Lauterbrunnen valley, not a field of zeros. Everything sourced in that long session is in the build: the cowbell recordings, the birch trees, the PBR surfaces, the full spatial audio system.
What's on the side quests page is the full build — real SRTM terrain, PBR texture splatting, HRTF spatial audio, instanced foliage placed using heightmap readings. The terrain has shape now. But the trees are still floating. The GPU displacement map and the CPU function that reads height for object placement use the same file and the same scale constant, and they still disagree by enough to matter. Every tree hovers. The lake hasn't settled. The assets were never the problem. The height alignment is the one thing that hasn't resolved.