Splat II · Deep dive
Buildings and the City
Three building types, rule-based placement, a vertex-shader bend with matching CPU colliders, and the route system that keeps every map clearable.
ProceduralBuildingComponent is the biggest single component in the project. It owns generation, rendering, collision, placement rules, and the route system.
Three types
| Type | Notes |
|---|---|
| Flat top | Straight rectangular, cheap at scale |
| Curved | Bent by vertex shader, convex hull collider on CPU |
| Arched | Profile-blended curve, like a tunnel mouth |
The facade shaders run interior mapping on top so windows look like they have rooms behind them. No interior geometry exists.

Placement: five checks per grid cell
The generator walks each terrain chunk on a 28 metre grid and runs five gates per cell, in this order:
- Density roll: 45% chance, else skip the cell.
- Jitter offset: up to 8 m random, breaks the grid look.
- Slope check: too steep, reject (a building would float in the air).
- Height check: outside allowed band, reject.
- Spacing check: any neighbour closer than 10 m, reject.
Only cells that pass all five get a building.
Building parameters
Each surviving building rolls its own dimensions inside fixed bands:
| Field | Range |
|---|---|
| Footprint | 7.5 m to 18 m |
| Aspect ratio | 0.6 to 1.6 |
| Floors | 3 to 12 |
| Floor height | 3.2 m to 4.4 m |
A directional height-scale field grows buildings taller the further they are from the map origin along a configurable vector. The skyline grows as the player progresses, so the city itself signals “you are heading the right way.”
Density zones come from a separate noise mask: some areas dense downtown, others sparse outskirts.
Curved buildings: the GPU side
The visual bend happens entirely in the vertex shader using a curvature struct:
struct BuildingCurve {
vec3 direction; // bend axis
float strength; // how much to bend
float exponent; // bend curve shape
float waveAmplitude; // optional wave on top
float waveFrequency;
float wavePhase;
float profileBlend; // arched vs curved profile mix
};
Per-instance values let every curved building be a different shape from the same mesh, at zero CPU cost.
Curved buildings: the CPU side
The GPU mesh is not what the player collides with. The collider has to bend too, in world space, closely enough that grapples and player physics feel correct.
I build a convex hull on the CPU that approximates the bent geometry. First version drifted off the visual. Second was off near the ends. Multiple rewrites later it lined up. The twisting bug when bending around two axes at once took its own fix.
Both sides read from the same BuildingCurve struct per instance, so the CPU collider always reflects what the GPU is drawing.

Recalculating the AABB
A bent building has a different bounding box than its straight version. First pass used the straight AABB for broad-phase culling and placement checks. Two bugs followed:
- Curved buildings clipped into each other at placement time.
- The route system did not clear enough space around them.
Fix samples a handful of points along the bent shape and grows the box to fit. Cheap, ended both bugs.

Landmark buildings
Two landmark buildings get injected manually, one at the start of the map and one at the end. They give the player a recognisable anchor at each end so the procedural city does not feel directionless.
The route system
A pure procedural city can lock the goal behind solid buildings. The route system carves a guaranteed path from start landmark to end landmark.
The path is a sine wave between the two endpoints. Always some curvature, never a straight hallway.

The route does not delete every building in its radius. It pushes them outward, away from the centre line. Only buildings too close to the centre get deleted.
for building in placed_buildings:
d = distance_from_centre_line(building.pos)
if d < delete_radius:
remove(building)
elif d < push_radius:
push_dir = normalize(building.pos - nearest_point_on_path)
building.pos += push_dir * (push_radius - d)
The cutscene system follows these routes (see Systems & Tools). The opening flyover runs along the real player path, not hardcoded waypoints.
Rendering at scale
Every building of the same type renders through GPU instancing. One draw call per type per chunk, regardless of how many buildings are in view. Instancing alone cut building draw calls by around 70%.
Bugs worth a sentence
- Vertex limit crash when building meshes grew past the buffer cap. Fixed by trimming the vertex budget.
- Angle-based splat detection so paint VFX sits correctly on slanted surfaces.
- Anti-splat tagging so certain buildings never receive paint.