The premise
When you know the angle, stop building a world
Eastshade Studios put it in one sentence in a devlog about Songs of Glimmerwick: when you know the exact angle the world will be seen from, you can stop building a world and start building a picture of one. Everything below is a consequence of taking that literally.
The camera here sits 34 degrees above the horizon, 84 metres back, behind a 15-degree lens. It follows the traveller and it never rotates. That lens at that distance shows a band of world about 22 metres tall β two and a half buildings β and converges so little that it is very nearly an orthographic projection, which turns out to matter more than anything else in the project.
The town is one street about ninety metres long, a ravine, a bridge over it, and a shrine on the far side. There are eight structures, twenty-four paintings, and about sixty prop cards. Behind every facade there is grass and a boundary, and that is all there is: nothing is terraced off and nothing is filled in, because from a fixed camera it is obvious where you cannot go and you never lose sight of yourself walking into it.
The restriction is easier to think about as a shared coordinate system. Once the art pipeline and the engine agree on one angle, a picture and a place are the same object.
Layer 0 Β· The projection
Texture coordinates you measure, not author
A structure is two things made separately that know nothing about each other: a
painting from gpt-image-2, and a proxy of three to five boxes typed
out by hand. src/project.ts is the whole seam between them, and it is
about forty lines.
Take every vertex of every part of the proxy and measure two numbers: how far
right, and how far up, from the camera's own axes. Not screen pixels β world metres
along RIGHT and UP, the vectors in src/rig.ts
derived from that one 34-degree number. Find the box those measurements fall in,
normalise every vertex into it, and write the result into the geometry's
uv attribute. That is the entire technique.
const u = p.dot(RIGHT); // world metres, screen-right const v = p.dot(UP); // world metres, screen-up β¦ uv[i] = (u - minU) / w; uv[i + 1] = (v - minV) / h;
Orthographic, not perspective, and that is the load-bearing choice. An orthographic projection along a fixed axis is invariant under camera translation: the mapping depends only on which way the camera faces, never on where it stands. So the UVs are computed once at load and never touched again, and the camera can follow the traveller ninety metres without the paint sliding a pixel. A perspective bake would have tied every building to the spot the camera happened to be standing when it was baked.
Three things fall out of this that a flat sprite would not have given us. The proxy
may be wrong and still read β anything the painting left transparent is cut
away by alphaTest: 0.35, so a pitched roof is a box with the corners
painted out, and the mesh only has to be right where the picture is opaque. Depth is
real: a roof modelled a metre proud of the wall stands a metre closer to the camera,
so it sorts, occludes and takes a shadow like geometry, because it is geometry. And
the lie is local β two planes six metres apart, measured against one frame,
reassemble into one arch, and nothing in project.ts knows that is
happening.
The real projectFrame, the real box and
gable proxies, and the real house-guild.webp, imported
by this page. Three solids. Turn on the proxy and the roof turns out to be a
triangular prism with its corners cut off by the alpha test.
There is no unwrapping anywhere in this project, and no authored texture coordinate. The painting lands where the proxy lands, because those are now the same measurement.
The art pipeline
Asking for a frame, not for a house
The fixed camera dissolves the one thing image models are worst at. If the camera never moves, you never ask for a second view β and multi-view consistency, the failure mode that makes generated art unusable as world art, stops being a requirement at all.
Twenty-four paintings, one call each, through fal's gpt-image-2:
eight structures, eight props, four seamless ground tiles, three scenery flats and
one figure. The prompts are in tools/art/manifest.mjs and the output is
committed to public/art as WebP β nothing a visitor loads ever touches
an API, and the script exists so the town can be repainted, not fetched.
Naming the angle does not work. The first pass said "the viewer is
about 34 degrees above the horizon" and got back a beautiful, useless flat front
elevation β a perfect architectural drawing with no roof plane in it at all. The
number meant nothing to the model. What works is describing the picture
instead of the camera: which half of the frame the roof fills, that the walls are
visibly foreshortened by being seen from above, and then four more clauses refusing
the elevation it wants to draw. That paragraph is the single most load-bearing
string in the repository, and it is the same 34 degrees as CAM.pitch.
And then it overshoots. Fed to "a single broad deciduous tree", the same emphatic paragraph about roofs and top surfaces produced β every time, for the tree, the cypress and the boulders alike β a small terracotta-roofed house with the subject planted on top of it. The model was answering the instruction it had been given most forcefully, and it was not the subject. Props now get their own angle paragraph with the word "roof" removed from it.
The cut is a chroma key, not a segmentation model. The prompt asks for a flat
#FF00FF field and tools/art/generate.mjs mattes it out in
a browser canvas: distance from pure magenta ramped rather than thresholded so the
anti-aliased rim survives as partial coverage, a despill pass on the one hue where
red and blue both run ahead of green, then a trim to the surviving pixels and a
series of halvings down to size. Deterministic, and a hard colour gives a clean edge
where a segmentation mask gives a soft one with the backdrop's light still in it.
The one grade in the pipeline goes on the ground tiles: each channel a tenth of the way to the tile's own mean. A cobblestone with as much contrast as a building competes with the building.
The geometry
A building's width is not something you type
Every structure in src/world.ts is authored as a painting, a list of
crude solids, and a place to stand. None of them is authored with a width.
Widths are derived. Stand the proxy up, project it, see how tall its frame came out,
multiply by the painting's aspect ratio, and scale the group in x until the two
agree. It works exactly rather than approximately because the projection's
u axis is world x: scaling in x moves the frame's width and
leaves its height untouched, so one multiplication lands it. A building is authored
entirely in terms of how tall it is and how far back it reaches, and its width is
whatever the painting turned out to be.
The proxy vocabulary is five shapes β box, gable,
cyl, cone, flat β and there are twenty-five
of them in the whole town. The windmill is a tapered drum, a thin ring and a box.
The corner house is a box, a prism, a cylinder and a cone. The bridge is five
solids, one of them rotated to lie along the ramp. There is no modelling tool in
this project and no imported mesh anywhere in it.
Every proxy is DoubleSide, which is a deliberate cost. Backface culling
would be free and correct for a world seen from one angle β and it would make every
building vanish the instant the reveal slider swung the camera past its own walls,
which is the one thing the reveal exists to prevent. A facade seen from behind
should look like a facade seen from behind.
Deriving the width means the model gets a vote on the proportions of the town.
The lies
One arch, two planes, and three metres of air
All geometric truth is negotiable if the camera cannot move. The rock arch over the street is two flat planes 4.4 metres apart that you walk between; they are an arch from one direction and from nowhere else.
Getting them to join took a correction that is worth the arithmetic. A point's
height in the frame is yΒ·cos 34Β° β zΒ·sin 34Β°, so moving a plane 4.4
metres toward the camera drops it 2.46 metres down the picture. Both legs stand on
the same ground, so without compensation the far leg's foot sits two and a half
metres higher in frame than the near one's, the two halves of the painting stop
lining up, and the arch comes apart into two rock stumps β which is exactly what the
first version did.
The fix is to lift the near plane by Ξz Β· tan 34Β° = 2.97 m. Then both
legs occupy the identical band of the frame, the halves rejoin seamlessly, and one
leg of the arch is hanging in mid-air with its foot three metres above the grass.
From the camera it is planted. From anywhere else it is a rock formation floating
over a street.
The same projectFrame and the same flat proxy, rebuilt on
every slider move. Take the lift off its solution and watch one painting become two
rocks; swing the camera and watch the solution stop being one.
The floor is tipped. The shrine wanted to be seen more head-on than the rest of the town, and moving the camera was not available β every other painting assumes 34 degrees. So the plateau it stands on tilts 14 degrees away from the viewer instead, which takes the effective angle onto anything standing there from 34 down to 20. That is the difference between looking at the shrine's roof and looking at the shrine. The traveller's height is read off the same height function as everything else, so they walk up it without noticing.
The traveller shrinks. Crossing the bridge, they lose 42% of their height β 1.85 m down to 1.07 β on a smoothstep between 24 and 6 metres from the shrine's threshold. Walking speed stays at 6.2 m/s the whole way, so travel time stays sane and nothing feels like wading. What a visitor perceives is not a person shrinking; it is a shrine that turns out to be much further away, and much bigger, than the ravine suggested.
The mountains are 26 metres tall. The far range stands 66 metres behind the street, which is a hill you could walk up in a quarter of an hour doing the work of a mountain forty kilometres off. The near hills are 11 metres. Both are one painting each, repeated seven times with every other copy mirrored, and both sit behind sheets of tinted transparent card that produce atmospheric perspective for the cost of one draw call. The water between them is a plane tipped a degree and a half out of level, because there is no horizon anywhere in this world to measure it against.
The first pass stood the near hills up at twice this size and their little painted village came out the same height as the real houses twenty metres from the camera. Scale is the only depth cue a flat has left.
The camera
Five shots, blended by distance
Composed places carry their own framing. Five of them β the mill, the arch, the street, the bridge, the shrine β each with a look target, a distance and a pitch, and each blended in by how close you are rather than by crossing a line.
Every magnet has an inner radius where its shot is fully in charge and an outer one where it has no say; between them the weight is a smoothstep of the distance. Nothing is triggered, entered or exited. Walk half-way into the shrine's court and you get half of the shrine's shot; walk back out and you hand it back exactly as smoothly as you took it. The camera then follows its goal on an exponential with a rate of 1.6 per second, which is loose enough that the traveller leads the frame a little.
The blend is a weighted average, with the plain follow-cam holding whatever weight is left over, and the obvious implementation is subtly wrong. Lerping the goal toward each magnet in turn lets a weak distant one drag the goal off a strong near one: standing directly under the arch, the street's magnet β 16 metres away and still worth 0.4 β put the camera seven metres east of it. Which is fine for a house, and fatal for the one structure whose two halves have to line up, because the tiny residual perspective in a 15-degree lens is only negligible on the optical axis. Off-axis it is enough to open a visible slot down the middle of a rock arch.
There is no minimap, no map, and no marker over the world. A fixed orientation means the world never turns under you β screen up is world away, for the whole visit β and navigation comes from landmarks and composition instead. It is also what made the foreground matter: a 34-degree camera puts about twenty metres of ground across the bottom of every frame, and the street is only fourteen deep, so until a row of trees and boulders went in between the camera and the street, every shot in the town was two thirds empty grass.
Composing to the camera is not only about what the facades look like. It is also about what is in front of them.
The lighting
Five translucent quads, and one light that lights nothing
Every painted surface in this town is a MeshBasicMaterial. Nothing is
shaded. The paintings carry their own light β asked for from the upper left, every
time, or the town falls apart into twenty separate afternoons β and the engine adds
none.
What stands in for a lighting rig is five translucent gradient cards floated over the world, painting macro light by hand: a warm additive wash down the street, a cool multiply over the mill's knoll and another in the ravine, a bright one on the shrine, a small warm pool under the arch. Because a card is a physical object between the camera and the world, anything that walks behind one is brightened or shaded by it for free, with no light maths anywhere.
There is one directional light and it lights nothing at all β it exists to cast
shadows onto a second copy of the terrain wearing a ShadowMaterial, which
draws only where something occludes the sun. A lit ground material would shade
directionally, and a ground that takes a sun the buildings don't is a ground that
stops matching them.
The terrain is the one honestly three-dimensional thing here: a real mesh sampled from a height function, 150 Γ 92 segments over 146 Γ 76 metres. Its texture is a single 2774 Γ 1444 map drawn once at load β four generated tiles laid down through blurred masks, so the street is a stroked path rather than a per-vertex weight. Occlusion is baked into it from the same height function, and it had to be: the ravine is eight metres deep and, unlit and seen at a grazing angle, it read as a wide brown smear you would have walked straight into.
The mill's sails are the only real geometry in the town, for the reason Glimmerwick gives: full 3D is reserved for things whose animation demands it, and a painting cannot turn.
The bill
What this costs
One angle, forever. That is the honest headline, and everything below is a way of restating it.
The camera cannot turn, and the world cannot afford it to. There is no rotation, no zoom you did not author, no over-the-shoulder anything. A magnet may nudge the pitch by five degrees; more than that and the paintings visibly disagree with the geometry underneath them. The reveal slider is not a feature the world supports, it is the world being broken on purpose.
Near-orthographic is not orthographic. A 15-degree lens leaves enough perspective that two surfaces at very different depths do not project identically, and the arch β the one place in the town that requires an exact match β is where it shows. It is corrected by the lift, which is only exact on the optical axis; the further off axis the arch is when you look at it, the more the two halves disagree. Going fully orthographic would make every projection exact everywhere and would also remove all parallax, since translating an orthographic camera shifts every depth by the same amount. That trade was not worth it; the residual is.
The traveller has one direction. Four poses out of one generation is enough for a walk cycle and not enough for a turn, so facing left is facing right mirrored, and there is no back view of anybody. The same fixed camera that makes a single painting sufficient for a building makes a single painting insufficient for anything that should be able to walk away from you.
Ninety metres of ribbon. The walkable world is four rectangles and a list of footprints. Adding a district means authoring a district: the placement, the proxies, the magnets and the ground map are all hand-written numbers arrived at by standing something up, taking a screenshot, and moving it. Nothing here generates layout.
The ground map is a compromise. Glimmerwick vertex-splats tiling textures across its terrain at runtime; this bakes the same layering into one 3.7 megapixel canvas, which costs a texture instead of a shader and buys the ability to draw the street. It has been made bigger twice and coarser once, both times because a magnet pulled the camera back far enough to show a black wedge where the world stopped.
Debts, by name: every technique here is from Eastshade Studios'
Making Beautiful Top-down Worlds
and the Game Developer
deep dive
on the projected 2D workflow of Songs of Glimmerwick. The paintings are
gpt-image-2's.
The claim is narrow and worth stating narrowly: an image model cannot hold a subject across views, and a fixed camera is a way of never asking it to.
The town is out there now, being a picture. The slider is at the bottom of the screen.
Open the town β