Germania
- ( Unreal 5.7 )
- ( Architecture )
- ( VR & PC )
A first-person walkthrough of Welthauptstadt Germania, the unbuilt 1930s plan to rebuild Berlin. You explore the city on foot, trigger information panels on the landmarks, travel from a map or a searchable building list, and can hand over to a guided tour that walks the route for you.
Removing VR from a VR-shaped codebase
Almost every system was VR-shaped: gaze and proximity for pointing, widgets floating in world space, an entry room instead of a menu, a health-and-safety disclaimer, teleport locomotion. We spent the first week auditing the codebase and cataloguing what would have to change before writing a line. Then we replaced each VR-ism rather than hiding it: a crosshair line trace with Enhanced Input for interaction, a screen-space HUD with compass, minimap and prompt, a 2D main menu where the entry room had been, and a first-person controller that surfaced the usual problems of walking through geometry nobody had ever walked through, stairs and gravity included.
What the 5.7 upgrade broke
We ran the 5.7 move as a timeboxed spike first. Import, build, write down every breaking change, then decide. Just as well, because the damage landed in exactly the systems that render into something other than the main view. The options menu stopped applying and persisting settings, the minimap stopped drawing and tracking the player, the building list lost its bindings, the trigger zones drifted off their buildings, and the information panels rendered pitch black.
The POI panel rendering black
The obvious suspect was the widget, so we rebuilt it from scratch and it came back just as black. That result was the useful one. It eliminated the card and pointed at the scene, where the cause turned out to be exposure and eye adaptation rather than anything in the UI. While the real fix was being built, an interim swap kept a working demo in the client's hands.
Forward shading was disabling Nanite
The VR build was costing around 189 ms a frame in the worst city views. The project was running forward shading, which disables Nanite outright, and the city was authored Nanite-first: 98% of structures and 91% of props, with none of the roughly 3,693 static meshes carrying a traditional level-of-detail chain. Every mesh was falling back to a decimated stand-in that never got cheaper with distance. Moving to the deferred renderer restored Nanite and brought the same views to roughly 50 ms, about three and a half times faster. Finding it was the harder half. Comparing identical viewpoints in flatscreen and in VR showed the cost was the same in both, which ruled out "VR is just expensive" and pointed at per-object scaling. Toggling Nanite's proxy view confirmed it: under forward shading the entire city disappeared.
Three bugs, one cause
The in-world panels had soft text, the pointer left streaks trailing across them, and scrolling smudged the words. All three came from a single property: the panel material was set to a masked blend mode, which forces every pixel into a hard keep-or-discard decision and destroys the antialiased edges of text. The renderer then tried to reconstruct that smoothing across frames, which is where the streaking and smudging came from. A translucent blend with responsive antialiasing took the panel out of that accumulation while the rest of the city kept it. Panel resolution was doubled separately for sharpness, and the pointer's reach was cut from 15 m to 5 m so it stopped reaching panels from halfway down the street. There was a trap underneath: a runtime timeline was overwriting the designer's values on every open, so four attempts failed identically before we thought to look there.
One authority over cursor, input mode and pause
Every menu had been closing as though it were the only thing on screen, so shutting the pause menu while a detail panel was open behind it would yank the cursor away and unpause the game. We rewrote it so one event decides all three, from ground truth and not from flags. It asks each widget whether it is actually in the viewport, because closed panels stay referenced and a validity check alone reports them as open and traps the cursor for good. Only the pause menu and the map actually pause, since the others run timers that a pause would freeze.
Four hand-wired markers to one widget
The map's four location markers were four hand-wired buttons, their labels, and thirteen downstream graph nodes. We replaced them with one reusable marker widget backed by C++. Adding a marker is now dropping it on the map and setting its data asset, with no event wiring at all. Draggable windows got the same treatment as a shared base class both panels inherit, and travel now routes through a teleport subsystem, so a new destination needs data and not wiring.
Music that never announces its loop
Music runs off a subsystem that takes a game state (menu, exploring or tour) and never a track name, so callers say what the game is doing. Exploration shuffles beds with randomised silence between them, which stretches eighteen minutes of music across a much longer walk without the repetition becoming obvious. A passive mix modifier ducks the score whenever narration is audible.
Ambience placed by script
Ambience came in three layers. Two dozen seamless loops built from thirteen recordings through a repeatable processing pass, two of them synthesised because no usable recording existed for a distant crowd or road roar. Around seventy emitters placed by script from the level geometry, so moving a fountain and re-running updates its sound. And a C++ actor driving invisible emitters along the avenue and both tram lines with randomised speed, pitch and spacing, on routes taken from the tram poles already in the level.
Source control for 353,000 binary assets
The content directory holds roughly 353,000 binary assets, and World Partition stores every placed actor as its own file, so the world's actor set was sitting outside source control entirely. The cost of that showed up when a material was corrupted mid-week and produced visible banding on the buildings: with nothing to roll back to, it had to be diagnosed from scratch. The response was staged. A whitelist ignore file so tracking is opt-in rather than opt-out, the POI trigger zones moved into their own level instance and force-added so a pending art overwrite could not take them with it, heavy art moved to a separate LFS repository, and finally Perforce for the art pipeline. Two gaps found along the way meant no fresh clone could build at all. The same period produced a useful false alarm: the city logs around 15,555 missing-dependency errors, which read like recent data loss and were treated as such until a comparison against a complete backup showed the files missing from both and per-cell counts matching exactly. Long-standing dangling references, not loss.