The constraint
One angle, and everything downstream of it
The method comes from Eastshade Studios' Songs of Glimmerwick, and from the Game Developer deep-dive on its projected-2D workflow. The idea in one sentence: when you know the exact angle a place will be seen from, you can stop building a place and start building a picture of one, then hang just enough geometry behind the picture to make it behave.
The camera in this project sits 42 degrees above the horizon, about 161 metres back, behind a 2.5-degree lens. It never rotates. Forty-two rather than the thirty-odd a top-down town would use, because a room seen at thirty degrees is a picture of a wall with a sliver of floor along the bottom, and the floor is where the occupants live. At forty-two a table top and the ground in front of it are both properly visible.
The distance is the number that took longest to get right, and it is worth
explaining because it is the one mistake in this technique that looks like a
different bug. Texture coordinates here are baked orthographically โ a
parallel projection along the view axis, which is what makes them independent of
where the camera stands. They are then rendered through a lens that is not quite
parallel, so anything not at the camera's focal depth comes out very slightly the
wrong size, by roughly offset รท distance. The first draft of
src/rig.ts used a 17-degree lens 23 metres out. The Hollow's
foreground roots sit about 1.8 metres nearer than the camera's focus; at 23 metres
that magnified them by eight per cent, which is a third of a metre of drift at the
edge of an eight-metre picture. The roots peeled off the room like a sticker.
At 161 metres the same offset is worth about one per cent โ four centimetres, less than the soft edge the matte leaves on every painting anyway. An orthographic camera would make the error exactly zero, and it is declined for one reason: an orthographic camera that translates produces no parallax at all, and there is a small pointer-driven drift in these rooms that needs a trace of real perspective to exist. A very long lens keeps it.
MeshBasicMaterial.
A room's shot is not stored as a distance. It is stored as
{ look, w, h } โ a point to look at, and how many metres of picture
have to be in frame โ and the rig solves for the distance against the viewport's
own aspect every frame. That is the whole answer to portrait phones: the lens never
widens, the camera simply stands further back, which costs nothing and quietly
makes the projection more accurate as it goes.
A fixed camera 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.
Method one ยท the small interior
One painting, nine solids, one projection
The deep-dive describes two interior workflows, and this project runs one of each.
For small rooms, the artist paints the complete illustration and it is projected
onto a low-poly mesh roughly the shape of the room. The Hollow โ a burrow kitchen
under a tree's roots โ is that: one call to gpt-image-2, one image
1319 ร 962 after the matte, and nine crude solids underneath it.
The projection itself is forty lines in src/project.ts. Take every
vertex of every solid 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 derived from that one 42-degree constant.
Normalise into a rectangle, write the result into the geometry's uv
attribute, and stop.
const u = p.dot(RIGHT); // world metres, screen-right const v = p.dot(UP); // world metres, screen-up โฆ uv[i] = (u - frame.u0) / frame.w; uv[i + 1] = (v - frame.v0) / frame.h;
The rectangle is where the two methods part company. projectFrame
derives it from the solids โ measure them, take their bounding box in the
frame, fit the painting to that. It is what you want when one painting is one
object, and every piece of the Cutting Floor uses it.
projectInto is given the rectangle: the painting is nailed to
a fixed patch of the picture plane and every solid samples whatever part of it that
solid happens to cover. That is the Hollow. Its
frame is 8.23 metres across and 6.00 tall, anchored
so that picture row 0.47 โ where the back wall meets the floor โ lands at
y = 0, z = โ2.4 m. Every other number in the room falls out of that.
It is a stranger way to work than it sounds, and much more powerful, because the picture is the fixed thing and the geometry is negotiable. Put a box under the stove and the stove's own pixels land on it. Move the box a metre left and it stops being a stove and becomes a slice of floor. Delete every solid but one and the room is still a complete picture โ the last of the nine is the whole painting on a single flat, hung behind everything, and the other eight are refinements in front of it.
The real unproject, the real projectInto, the real
hollow.webp, imported by this page. A 90-centimetre cube, placed by
pointing at a pixel. Note what the two sliders cannot do: set its depth.
There is no unwrapping anywhere in this project and no authored texture coordinate. The painting lands where the solid lands, because those are now the same measurement.
Authoring
The depth of anything on the floor is not a choice
Run the projection backwards and it becomes an authoring tool.
unproject(frame, u, v, z) takes a fraction of the painting and a depth
and hands back a world point. The interesting case is when you do not get to pick
the depth.
A floor is a plane and a line of sight meets it exactly once. So given the pixel a
thing's feet are standing on, the projection has already decided how far back it
stands. That is Placer.stand(u, v) in
src/rooms.ts, three lines long, and it is how the Hollow was laid out
in an afternoon rather than a week: open the painting, read off where the stove's
feet are โ thirty per cent across, 60.5 per cent down โ and the room answers
x = โ1.65 m, z = โ1.19 m. You do not place things. You point at them.
stand(u, v) {
const p = unproject(this.frame, u, v, 0);
return [p.x, 0, -p.y * C / S]; // C, S = cos/sin of the pitch
}The rest of a room comes off the same picture. The table's top is two rows of pixels โ its back edge at 0.455 and its front at 0.525 โ and the distance between them at the height the table happens to be is exactly how deep the table is: 84 centimetres. Its front face runs from row 0.525 down to 0.585, which makes it 48 centimetres high. The ceiling comes out at 2.18 m, the floor at 6.4 ร 3.86 m. Nobody chose any of those. They were measured out of a picture that a model made in one call, and they are consistent with each other because they were all read off the same projection.
Three things this costs, all learned the same way โ by standing something up and looking at it.
The cradle stopped being a box
A box's upward face is a horizontal rectangle, and a horizontal rectangle takes
whatever band of painting sits above the box and lays it flat. Perfect for a
table top. Fatal for a hanging basket, whose opening is painted as a hole: the
box version put a lid on the cradle. It is a flat now.
Solids stop where the surface does
The stove box was first built up to row 0.45, the top of the pot standing on it โ so its lid lay the pot's own front down flat like a tabletop. It stops at 0.487 now, the top of the stove, and the pot lives on the flat behind.
Foreground flats are only free at an edge
The roots crossing in front of the chamber are a separate flat, 1.5 m nearer than the floor, which is what lets a creature walk behind them. There were three of these once, up the margins too, and they had to go: adjacent parts of one continuous painting held at different depths do not join, and a per cent of eight metres is a visible step in a root. The bottom edge of a picture is the one boundary nothing has to line up with.
The band along the bottom is also hung a little below the picture's own edge, where the UVs run past one and the texture clamps โ so its last row of root smears downward and covers the sliver of the flat behind it that would otherwise show through, magnified by exactly that one per cent.
Method two ยท the large interior
Twelve paintings, made apart, lit the same
A room too big to hold at full resolution in one image gets broken into a kit: each piece painted on its own, each reprojected onto its own surface. The Cutting Floor โ a stone cellar on two levels โ is twelve paintings, ten of them projected across thirteen solids, and two laid flat as tiles.
Each piece derives its own frame from its own proxy, so this half is authored the way you would author a street: put a thing down at a place on the floor, say how tall it is in metres, and let the painting decide how wide. The group is then scaled in x until its frame's aspect matches the painting's โ and because x is the projection's own right vector, scaling in x moves the frame's width and nothing else. gpt-image-2 gets a vote on the proportions of the room.
Two of the twelve turn that off. A stove is a particular width because it is a
stove; a wall of rock is whatever width the wall is, and nobody has ever noticed a
rock being stretched a third wider. Insisting on the shell's aspect would have made
an eleven-metre chamber seven metres tall, which is a cathedral rather than a
cellar, so the shell and the broken rim are marked free and take both
dimensions from the floor plan instead.
The staircase is the piece where the geometry has no freedom at all. It is one
plane leaning 42 degrees back โ which is to say, parallel to the picture plane,
which is why a painting of a stair lands on it undistorted. Its length is fixed by
arithmetic: the top tread has to arrive at the lip of the upper deck and the bottom
one has to reach the sump, the deck is 1.7 metres up, and a plane at that lean
covers cos 42ยฐ = 0.743 metres of height per metre of plane. So the
stair is 2.288 metres of plane, and everything else about a
staircase โ treads, risers, the rust-red handrail โ is in the picture.
What holds twelve separate generations together is the prompt. Every one of them carries the same palette block, quoted at the model with hex values rather than adjectives, and the same sentence about where the room's light comes from: a hard cold shaft down a chimney at the upper left, an ember glow from a stove on the right. Twelve images made in twelve calls will agree about very little on their own, and light direction is the one disagreement nobody can unsee.
Neither method knows the other exists. The two rooms stand thirty-four metres apart in the same black void so the difference can be looked at rather than described.
The occupants
Cards that are still being drawn
Small props in this technique are sprite cards tipped back to face the fixed camera, with a soft contact shadow underneath, and characters are treated the same way. The six creatures in these rooms are exactly that โ except the texture on each quad is being solved from scratch every frame.
Each of them is a rigged, traced, second-order-sprung puppet, imported whole from
gluty/src/cast/gluty.ts, a sibling package in this repo that turns a
set of polygons cut out of a gouache concept painting into something that walks.
It does the springs, the gait, the two-bone IK and the stamps;
gluty/src/render/painter.ts does the polygons, the decals and the pale
dry fringe along every cut edge. src/occupants.ts is a picture frame
with a room's coordinates on it, and nothing else.
This is a happier marriage than it sounds, because such a puppet was always a flat thing. It is cut out of one painting, from one side, and has no other view of itself โ image models cannot hold a character consistent across views, which is exactly why nobody tried to give it one. A creature with a single view is precisely what a fixed camera wants. The two constraints cancel.
The arithmetic is small. A creature is constructed at scale one, measured at
rest, then rescaled so that one of its own units is one canvas pixel โ
which keeps the painter's hairlines and its eight-direction fringe at the size
they were tuned for. The Keeper is drawn 330 pixels tall on a
297 ร 446 canvas at 4.3 mm per pixel, and
painting her is one translate with no scale in it.
The canvas has to be a good deal bigger than she is standing still โ a Keeper with four arms out, mid-stride, at the top of a bob is much larger than a Keeper at rest, and anything that leaves the canvas is guillotined. A creature gets 53 per cent of its card's width and 74 per cent of its height. The first draft was tighter and spent an afternoon wondering why her key kept disappearing when she reached for the pot.
A Gluty lives in a one-dimensional world: it has an x, a ground line,
and feet planted in world space that the body walks out from under. A room has a
floor. So every occupant carries a track โ a polyline across the
floor in room metres โ and the puppet's x is the distance travelled
along it. Walking is unchanged, down to the last spring. It is the floor that is
bent, not the gait. Which way the creature faces is read off the track's
screen-x, not its direction of travel, because a creature walking away from
the camera along a track that also runs left is walking left.
And then one trick that pays for the whole arrangement. A card tipped 42 degrees
back reaches into the room: a 1.42-metre creature's head sits a metre deeper than
her feet, which is enough to put a Keeper's head inside a wall she is standing well
clear of. The fix is to push the card along the view axis โ
tall ยท tan 42ยฐ / 2, 64 centimetres for her โ until it straddles the
point it is standing on instead of leaning back from it.
Moving anything along the view axis is invisible, because the
projection is orthographic along exactly that direction. It changes no pixel of any
painting and no pixel of any card. What it changes is depth. The contact shadow
does not get the offset; it belongs to the floor.
The real Occupant class, the real Gluty rig, and the
same canvas shown beside it above โ this figure and that image are one object.
Swing the camera and she lies down, because she was never facing you. She was
facing the angle.
Nothing here is a sprite sheet and nothing here is a recording. If the rig hitches, the room hitches.
Lighting
Seven quads and no lights
There is no light source in this project. The paintings arrived with their own firelight, their own window and their own lantern baked in at generation time, and shading them a second time in the engine would be a second opinion nobody asked for. What does the atmospheric work is seven translucent gradient cards floated over the two rooms โ three in the Hollow, four in the Cutting Floor.
A card is a quad with a blotchy radial gradient on it, tipped to the same 42 degrees as everything else. Glow cards blend additively; shade cards blend by multiply. It is not lighting, it is values โ a card pushes one area down and pulls another up the way a painter would. And because it is a physical translucent object hanging between the camera and the room, anything that walks under one gets warmed or cooled by it for free, with no light maths anywhere.
Which is the entire reason the occupants look like they are in the rooms. A Gluty
is painted in flat unlit gouache โ the charcoal of its body is #4c4a48
whatever is happening around it โ so a creature dropped into a dark stone chamber
would sit on top of the picture like a sticker. Walk it under the firelight card
and it warms. Walk it into the sump and it goes cold. The engine does not know it
did that.
A shaft of cold daylight down the Cutting Floor's chimney is not a special kind of object either. It is a glow card with its gradient stretched to a third of its width and rolled fifteen degrees โ a rotation within the picture plane, which costs the projection nothing, because the card still faces the camera square-on.
The reveal
Breaking it on purpose
Every technique above was invented to be invisible. A shipped fixed-camera game can never show you that its room is a floor, two flats and six boxes, because the moment it did it would stop being a game about a room and start being a game about a trick. An article can tell you. It cannot let you look.
The slider on the rooms does one thing: it swings the camera off the axis the place was built for. Everything else follows without being animated. The projections do not un-project โ the UVs were baked from one direction and they stay baked, so a painting that fitted a box perfectly now runs down its side like wet paint. The cards do not turn to face you; they are welded at 42 degrees, so the cast lies down like a shelf of postcards. Nothing comes apart, because nothing was ever together.
src/reveal.ts adds only the two things that are absences and
so cannot show themselves. The kit: one wireframe per solid, in world space rather
than parented to the room, because a room's group carries a non-uniform x scale and
a line drawn under that scale is a line drawn eight per cent thin. And, for the
Hollow, the picture plane โ the literal rectangle of space its one painting is
nailed to, with the painting hanging in it, stood one frame-width to the right so
the flat picture and the place it turned into are next to each other.
The captions are projected by hand: three.js knows nothing about them, and
reveal.ts multiplies each anchor point through the camera every frame
and writes a CSS transform. One of them reads the table's depth out of the room at
load rather than quoting it, so it cannot go stale โ which is also true of the
plate under each room, whose "9 solids" is counted rather than claimed.
The bill
What two rooms like this cost
The method buys a hand-painted interior for the price of a prompt and eleven boxes. Here is the other side of it, in the same register.
You get one angle, forever
Not "a camera with limits" โ one angle. Every painting was asked for at 42 degrees, every UV measured along it, every card welded to it. Changing the pitch means regenerating all thirteen paintings and re-laying both rooms, and the reveal slider exists precisely because the alternative was to describe what is under there.
Continuity across depths is not free
A foreground flat and the wall behind it show the same continuous painting at two magnifications, and however long the lens, the join is visible somewhere. The rule that survived is that a foreground piece is only free where its boundary already is โ an edge of the picture, or the outline of the room. Two of the three original root flats were deleted rather than solved.
Everything is authored twice
Once in a prompt paragraph written as a composition โ left, middle, right, near, far โ and once as a list of solids read back off the result. If the model puts the stove somewhere else, the second half is redone. There is no importable mesh anywhere in the project and no modelling tool; if a shape cannot be said with a box, a drum, a flat, a lying slab or a ramp, the answer is that the painting has to carry it.
A puppet costs a texture upload
Three live canvases per room, redrawn and uploaded every frame. Resolution is a hand-set number per creature and it has to be computed rather than eyeballed: the Porter's canvas is four and a half times wider than it is tall, and at the 300 pixels it was first given it was half a megapixel being rebuilt sixty times a second to be shown eighty-three pixels tall.
A card leans into the furniture
Pushing a card along the view axis halves the intrusion; it does not remove it. A tall creature standing right against a tall box still has a torso inside it, and the answer in both rooms was to move the creature. Tracks are authored around the furniture, not through it, and that is a constraint on staging that a real 3D character would not impose.
Values, not lighting
Seven cards can grade an area; they cannot cast anything, react to anything, or respect a corner. A creature standing in the doorway is lit by whatever card is hanging over the doorway, which is right about eighty per cent of the time and is why the cards are drawn with no depth test โ a correct-looking rectangle in the wrong place is worse than a soft blob in roughly the right one.
The method is Eastshade Studios'; the workflow is described in the Game Developer deep-dive on Songs of Glimmerwick's projected 2D, and Danny Weinbaum's devlog on top-down worlds is where the light cards and the perspective lies come from. The paintings are gpt-image-2 through fal, cut with a chroma key that ramps from 0.16 to 0.40 of the distance from pure magenta so the anti-aliased rim survives the downsample as coverage rather than a staircase.
The thing worth taking is not the projection. It is the inversion: the picture is the fixed object and the geometry is negotiable, and once you believe that, a room takes an afternoon.
Go and take them apart
Two rooms, six creatures, and a slider that shows you the boxes. Hold space for a spring-loaded look behind the picture, or pull the slider and leave it there.
Open Cutaway โ