Walk an army into a stone arch on one side of the map and it steps out of that arch's twin on the other. The arch is a picture, generated once and never redrawn: grey rock, carved runes, and a doorway with nothing in it. The light turning over inside that doorway is not in the picture and never has been. It is drawn by code, every frame, in both of the game's renderers.
Adam wanted a linked-pair portal network. Neutral arches that the map generator drops in pairs, creep-guarded at both ends, that any player may march an army through, plus a buildable version for two of the factions. There are four gates now, because the Ratmen already had a hole in the ground doing the same job and it had never had a portal in it at all.
One decision came before all the others: each gate is drawn once, unpowered, and the portal is left to code.
Why a second generated image cannot be the lit gate
The obvious way to light a gate is to generate a second picture of it, on. That falls over on the first activation. Every prompt redraws the whole subject, so a second generation differs in silhouette, in shading and in where the pixel grid falls, and the gate visibly jumps the instant it links. A still picture also cannot turn, and a portal that does not move reads as a painted window.
So the lit sprite is baked from the dormant one. The generator draws the arch with a flat magenta slab filling the opening, and the chroma pass that removes the background removes it from the enclosed doorway too, which is how the arch arrives with a genuinely transparent hole in it. That trick and the passes around it are the subject of turning generated images into game sprites.
![]()
Everything the bake writes is built out of those same pixels. Outside the doorway the alpha channel comes out byte-identical on all three arch gates, and a --verify flag asserts that instead of assuming it, so a bake that had eaten into the artwork would be caught by the tool rather than by looking at it.
![]()
Opening a gate is three beats, not a fade
A portal that ramps up looks like a dimmer being turned, which is the opposite of a hole tearing open. The activation is therefore three beats, in the order a gate told to open would go through them.
The horizon strikes first: a hard bright rim appears at the boundary and shivers, with nothing behind it yet. Then a surge blows outward past that boundary and is clipped by the doorway, so the gate flares against its own stonework. Then it settles to the steady turning field. The surge deliberately overshoots, running about 1.7 times the brightness of the steady state across the aperture, because the flare is what makes the opening read as violent rather than gradual.
It takes two seconds end to end, matched to the gate's sound so the cue lands on the frame the portal reaches full strength rather than trailing it. In 2D that is a 20 frame strip whose last frame is the loop's first frame, so the handover into the steady loop is invisible. In 3D the same three beats are driven by one uniform running from 0 to 1.
Press the switch below and both renderers play it on all four gates at once. The 2D pane is the real compositing path, a frame of the shipped strip with the gate's stonework drawn over the top. The 3D pane is the real model with the real shader on it, and opening the gates is what fetches it, so a reader who scrolls past pays for none of it.
Waygate
Arch gatePrism Gate
Arch gateStar Gate
Arch gateGnawhole
Floor gateFrom one sprite to one model, and what the model does not know
Each gate's 3D model is generated from that same sprite by Meshy, through the pipeline in turning 2D sprites into 3D buildings. The sprite is the entire brief: no separate concept art, no second prompt. So the model inherits the arch the painting already decided on.
What it does not inherit is any idea of where its doorway is. Nothing in a model file records that, and there is no reliable way to recover it from geometry, so the portal plane's position, size and yaw are dialled in by hand for each gate and stored as data beside it. The plane is then parented to the model root, which means it picks up whatever scale, rotation and recentring a given viewer applied and no viewer has to declare its own transform to anything.
That parenting is why the same four gates work in a match, in the model viewer and in the figure above without three different placements to keep in agreement.
The Ratmen Gnawhole needed almost none of this to transfer, because it is a hole rather than an arch. Its plane lies flat, it grows a column of light above the mouth, and it hangs a painted disc of shaft art below the portal, because the generated model's interior is a smooth featureless funnel and an overhead camera looks straight down it. That disc is deliberately not part of the portal: an unlinked hole still has a shaft you can see down.
One definition of the look, in two languages
The portal's appearance lives in exactly one file, as a scalar field of position and loop phase. The build tool samples that field per pixel per frame to write the 2D strips. The 3D material is a line-for-line port of it into shader code, evaluated per pixel at whatever zoom the camera happens to be at.
Both exist because the two pipelines need different things: a build script cannot run a shader to bake a PNG, and a 3D scene should not sample a pre-baked strip when it can evaluate the field continuously. Neither one is the original.
The first version of that field was a soft radial blob with spiral arms, and it read as a light rather than as a hole. Part of the fix was the edge described above. The larger part was relief. A spiral painted in brightness stays flat however pretty the spiral is; what reads as three-dimensional is a lit surface. So the field builds a height map from ripples, arms and fractal noise, pushes it around with a domain warp so it undulates like disturbed water instead of spinning like a pinwheel, takes its gradient to get a normal, and lights that.
// height() is any scalar field of position and loop phase.
// Sampling it four times buys a normal, and a normal buys depth.
float e = 0.035;
float dx = height(p + vec2(e, 0.0), t) - height(p - vec2(e, 0.0), t);
float dy = height(p + vec2(0.0, e), t) - height(p - vec2(0.0, e), t);
vec3 nrm = normalize(vec3(-dx, -dy, 0.55));
float ndl = max(0.0, dot(nrm, normalize(vec3(-0.55, -0.62, 0.56))));
float spec = pow(ndl, 22.0) * 1.35; // the glint that sells the depth
float sheen = pow(ndl, 3.0) * 0.30;The specular term does the work, because it slides across the swell at a different rate from the surface underneath it. Brightness alone can never do that.
Relief costs something, and the cost is the loop. The 2D strips are 12 frames on repeat, so phase 1 has to land exactly on phase 0 or the portal jumps once a cycle. Every angular term is a whole multiple of the arm count, and the noise is advected around a circle rather than along a line so that it too returns to where it started.
// A looping effect has to land back where it started.
const EPS = 1e-9;
for (const [u, v] of samplePoints) {
const start = field(u, v, 0);
const end = field(u, v, 1);
if (Math.abs(start - end) > EPS) {
throw new Error(`loop seam at ${u}, ${v}: ${start} vs ${end}`);
}
}The glyphs streaming out of an open gate are the same trick at a different scale. Each faction's letters come from the generated alphabets behind inventing a magic alphabet, and the ring of them turns at a rate that divides the loop exactly, so a Prism Gate turns the same runes High Elf spell effects already draw. In 3D they keep streaming for as long as the gate is open, on top of a shockwave ring that draws at every quality tier and a raymarched plume that draws only on the top two, all reading the same activation value the surface does. Layers drop out whole as quality falls rather than shrinking, on the same reasoning as the effect work in making spells look like spells: shrinking an effect erodes the shape a player recognises and makes it look broken instead of simple.
Checking your own two-renderer effect for drift
If one look has two implementations, the check is not code review. Put both on a single screen, drive them from one control, and watch them together. That is what the figure above is, and it is the same view I kept open while building this: a divergence between the baked frames and the live shader is obvious side by side and effectively invisible in the code.
Two rules make it cheap. Keep the definition in one file that both consumers sample, so a port is obviously a port and not a second opinion. And if the effect loops, assert the closure numerically in a test rather than judging it by eye: the failure that costs an afternoon is seeing a small seam in motion and tuning the wrong term for it.
The Lizardmen Star Gate is finished as art and as a model but stays out of the build menu until its one-way Warp Beam exists, because an inert building at 250 gold is worse than an absent one. That beam is the next piece, and it wants a shape the current field does not draw: a portal that goes one way, and has to look like it does.





