vibespire.ai / Backlot /

how it's made

open the town β†—

Vibespire presents

How the Backlot Is Made

There is no modelled building in this town. There is no unwrapped mesh, no authored texture coordinate, and no artist. Every structure you can walk up to is one generated picture stretched over three to five boxes, and the boxes only have to be right from one direction β€” because the camera has exactly one, and it never turns. Here is what that buys, and what it costs.

ONE PAINTING THREE BOXES ONE BUILDING

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.

show

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.

Structures8 Β· projected onto proxies
Props8 Β· sprite cards, no proxy
Ground4 seamless tiles, baked into one map
Scenery3 flats, repeated and mirrored
The traveller1 image, 4 poses

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.

Multiply cards must be white where they do nothing. Multiply blending has no notion of coverage, so a shade card's gradient runs from its tint out to pure white and its opacity stays at 1. Fading a multiply card with opacity fades it toward black.
The gradients are blotched on purpose. A clean radial gradient reads as a gradient; twenty-two soft ellipses at 10% alpha read as a sky with something in it.

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 β†—
Marcin Balinski

Marcin Balinski

I build things quickly, mostly by hand and increasingly with a model sitting next to me. Most of what I make starts as a question about how something works and turns into a small machine for finding out.

vibespire is where those experiments live. Poke at them, break them, read how they're made β€” and if something sparks an idea for you too, say hello.