The screen ยท src/screen/screen.frag
One pass turns a photograph into a print
Bearer draws its world with an ordinary 2D canvas โ sprites, terrain blits, text, the lot โ into a 512ร512 greyscale buffer, and paints a second, much smaller canvas, 128ร128, with where the world is in focus. Those two texture uploads and one full-screen triangle are the entire GPU workload. Everything in the project that looks like a decision about art is made in the fragment shader that runs over them.
The function that does it is printed(), and it does five things in a
fixed order. It samples the scene buffer at a texel centre โ
floor(uv ร 512) + 0.5, so tone is read on the buffer's own lattice
and never interpolated. It applies lift and exposure.
It stretches contrast about uPivot rather than about mid grey,
because the pivot has to follow the light: pulling a dark frame toward 0.5 makes
it grey rather than dim, which is exactly what the first night looked like. It
adds a trace of noise, then multiplies by the vignette โ before quantisation, so
a darkened pixel is pushed down the ramp rather than tinted off it. Only
then does it screen the result into dots.
There are two lattices, and they are different sizes on purpose.
Tone is sampled and quantised on the scene buffer's own grid,
uScenePx = 512. The screen โ the dot lattice โ is evaluated on a
finer one, uGrid, which is 1024 in the shipped defaults. That is
four lattice units to a scene texel, two per axis, which is what
lets a texel carry a dot with an edge in it instead of being one. Across the
square there are uCells = 128 halftone cells, so a cell covers
sixteen scene texels, four per axis.
The fine lattice is capped at whatever the display can actually draw.
Screen.resize sets it to floor(size / 128) ร 128,
clamped between 512 and 1024: a 1024-unit dot grid rendered into 640 output
pixels beats against the pixel grid and turns into moirรฉ. Keeping it a multiple
of the cell count guarantees a cell is always a whole number of lattice units,
which is what keeps the dot grid even, and evenness is most of the illusion.
The corner cut runs on that same fine lattice, so the rounded corners
step rather than feather. corner: 0.0222 of the square is
the 24 pixels the reference's corners measure at 1080. A feathered corner would
be an anti-aliased edge in a picture that contains no other anti-aliased edges.
Screen class from src/screen/gl.ts, running
the real screen.frag over a scene drawn into its own
sctx with the game's own fonts. Every slider writes
into screen.params โ the same object applyLook writes
into on every frame of the game.
The palette, the tint and the corner cut all happen once, afterwards, on the finished tone โ because printed() is called up to nine times per fragment, and everything that only has to happen once was moved out of it.
The halftone
The dot is a diamond, and the cross is the paper
Look at a mid tone in the reference and you see plus signs and diagonal crosses, and the natural conclusion is that the dot is cross-shaped. The extracted matrix says the opposite: ink starts at the centre of a cell and the last part to fill is its corners, so the crosses are the paper between four neighbouring dots. The first implementation used a diagonal cross matrix on the strength of that misreading, and it tiled into a checkerboard.
What ships is five lines:
vec2 pd = floor(uv * uGrid) + 0.5;
float cell = uGrid / uCells;
vec2 f = fract(pd / cell) - 0.5;
float dd = abs(f.x) + abs(f.y);
float thr = dd <= 0.5 ? 2.0*dd*dd : 1.0 - 2.0*(1.0-dd)*(1.0-dd);
|x|+|y| is a diamond distance, so the threshold contours are
nested diamonds growing from the cell centre. The two branches are the second
half, and they are the half that matters. A diamond's area is not proportional to
that distance โ a fifth of a cell sits below 0.32 โ so handing the raw distance
in as a threshold bends the tone curve, and dark tones land lighter than they
were asked to be. The branches are the diamond's own area function inverted,
which puts a half tone at half coverage. What a cell cannot do is be exact on its
own: at the shipped 1024/128 it is eight lattice units across, so it has
sixty-four units to spend, and the tone in between is carried by the sixteen
scene texels underneath it landing on different sides of the threshold.
GRID / CELLS in
src/screen/gl.ts and the ink and paper are the first and fifth
entries of RAMP_HEX; the threshold is
screen.frag's own expression, transcribed, because GLSL cannot be
imported. The shaded region on the right is the paper four dots leave between
them โ the plus you can see in a screenshot of the reference.
Which shape, though? tools/measure/reference.mjs rolls the
extracted matrix so its first-inked cell is central and correlates it against
four candidates. All four come back positive, because the loudest thing the
matrix says is that ink grows from the middle โ but the ranking is clear enough
to follow: the diamond |dx|+|dy| at +0.798, the
disc at +0.765, the cross at +0.709 and the square last at +0.655. Note where
the cross comes: third, despite being the shape you can actually see. That is
the same misreading, caught by the same number.
A square shipped here for an afternoon, on the strength of a written description of the reference rather than the correlation, and the correlation puts it last of the four. Both are equalised and both quantise the same way, so the tone histogram did not move by a tenth of a per cent between them โ which is why it took running the script against the shipped value to catch it.
The plus you can see in a screenshot is not a mark on the page. It is the part of the page nobody printed.
Measurement ยท tools/measure, test/compare
You cannot answer "does it look right" by staring at it
node tools/measure/reference.mjs fetches eight published store
screenshots of O EMPIRE! WARD OFF THY ROT, by Dean Moynihan,
decodes them in a headless Chromium and extracts the seven quantities the screen
shader needs. It is committed, and every figure in screen.frag's
header comment is a line of its output.
It measures the frame first, because everything downstream is expressed in that coordinate system: the picture is a 1080ร1080 square pillarboxed at x = 420, and all eight screenshots agree to the pixel. 512 is the buffer size that divides 1080 exactly โ 2.109375 โ which is why Bearer's is 512 and why its upscale is nearest-neighbour rather than filtered.
One: the halftone period is 8.4375 pixels, which is 1080/128. The obvious objection is that 8.4375 is close to 8, and 8 is JPEG's block size, so what is being measured might be the codec. The sweep settles it. Bin every pixel by its position within a candidate cell, subtract the local mean, average, and take the standard deviation of the resulting matrix โ if a dither grid of that period is really there, sub-cells that take ink early come out darker than their surroundings and the matrix has structure.
โโ the halftone period โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
8.0000 px structure 0.30
8.1000 px structure 0.15
8.2000 px structure 0.17
8.3000 px structure 0.32
8.4375 px structure 1.33 โ
8.5000 px structure 1.05
8.6000 px structure 0.36
9.0000 px structure 0.14
1080 / 8.4375 = 128.0 cells across the square
At JPEG's own 8.0 the score is 0.30, less than a quarter of the winner's 1.33,
and both neighbouring periods are lower still. The grid belongs to the game, so
CELLS = 128.
Two: the ramp is dead neutral grey. Every screenshot reads as a sepia photocopy โ mauve shadows, warm paper โ and none of that is in the palette. Sample only pixels lying along one diagonal band, so screen position is held roughly constant; bin them by luminance; look at RโG. Across twenty-six bins it averages 0.12 and never passes 0.45. What does move is GโB, and only at the top: about zero through the mid tones, then climbing to 22.9 in the brightest bin. That is why the top of the ramp is cream and nothing else is.
The warmth is a fixed diagonal gradient in the red channel laid over the finished
picture, which in screen.frag is three lines at the end:
float d = 0.8 * vUv.x - 0.6 * vUv.y + 0.3;
col.r += uTint * d;
col.b -= uTint * d * 0.55;
(0.8, โ0.6) is a unit vector, so d is a plain signed
distance along one diagonal, and the shipped tint is 0.05. Baking the warmth into
the palette instead was the first version and it turned the shadows purple, which
they are not. The ramp itself is six steps from ink 53 to paper 230, with only the
top one drifting to cream:
#353536 #575758 #79797a #9b9b9a #bebeb9 #e6e3cb. There is no night
palette. Night is the same six with the exposure slid down them.
Three: there is no grain. High-pass the flattest region available
with a Laplacian and what survives is 0.37 of 255 โ 0.14% RMS โ
and it rises with tone the way residual dither does and noise does not. It is
JPEG. What looks like film grain in a screenshot of the reference is the
compression. Bearer's first build shipped 4.5%, thirty times that; what ships now
is grain: 0.012 peak to peak, about 0.35% RMS. That is above the
reference's floor rather than below it, and deliberately so: a generated
heightfield bands where a hand-built one does not, and a trace of noise is what
breaks the bands.
A fourth measurement was less surprising and had to be got exactly right
anyway. The vignette is corners only, and it is later and
steeper than it looks. Binning the 90th-percentile tone by
r = |uvยท2โ1| โ 1.0 at the midpoint of an edge, 1.414 at a corner
โ gives a profile that is flat out to r = 1.24 and then falls off a cliff.
1.20 0.958
1.24 0.947
1.28 0.909
1.32 0.706
1.36 0.460
Bearer ships vignette: 5.6, vigStart: 1.26, fitted to those four
points. The bin at 1.40 is left out of the fit because the rounded-corner mask
is already eating it. A vignette that starts at the edge instead โ the default
assumption, and what a vignette shader normally does โ puts a grey haze
along all four sides, and the reference's sides measure within 6% of the flat
value the whole way out to r = 1.24.
screen.frag, drawn to scale. The circle is vigStart = 1.26; the whole middle of every edge falls inside it, and only the four corner caps darken.
Then the measurement is turned around. The thing being copied is a
distribution โ how much of a frame sits on each of the six tone levels โ
and that is measurable on both sides. test/compare.mjs starts a Vite
server, drives a real browser to a real seed, walks it east for a given number
of seconds, clusters every pixel of the canvas onto the six ramp luminances and
prints the histogram beside the same measurement taken from six of the
reference's own screenshots. Below is node test/compare.mjs ash 30. Anything below the darkest level is excluded, because the corner cut
multiplies toward black and that is frame, not picture.
tone levels, dark โ light
ss3 karst, day 14.9% 30.1% 24.7% 19.2% 10.1% 1.1% mean 114
ss5 village, day 20.2% 27.5% 16.1% 20.1% 14.1% 2.0% mean 117
ss1 moor, bright 6.0% 5.1% 11.2% 33.2% 38.1% 6.3% mean 159
ss7 tower, bright 2.5% 10.8% 20.3% 37.1% 27.6% 1.7% mean 149
ss6 map, night 60.0% 9.5% 11.4% 10.6% 6.1% 2.4% mean 87
ss4 moon, night 87.4% 5.4% 3.0% 1.1% 0.2% 2.8% mean 63
------------------------------------------------------------------
bearer (ash) 19.0% 28.7% 15.0% 21.8% 15.0% 0.5% mean 122
daylight 0.91 ยท exposure 0.803 ยท lift 0.012 ยท vignette 5.639
closest reference frame: ss5 village, day (3.8% of the frame would have to move)
One number falls out of it: the total variation distance to the nearest reference
frame. 3.8% of this frame would have to change tone to become the
reference's village at midday. The exposure and contrast curves in
applyLook were set against this harness, and the harness drives them
from outside the build โ window.bearer.tune multiplies exposure,
contrast and vignette without a rebuild between guesses, so a forty-eight
candidate sweep is one browser and one world.
The histogram also shows where Bearer is not the reference. The brightest level is
the stubborn one: 0.5% here against 2.0% there, and it took a
pivoted contrast curve to get that close, because a generated heightfield lit by
one sun has no specular anything. The camera scale was the other thing eyeballing
got wrong. In the reference's village shot a timber cottage spans about 160 of the
square's 1080 pixels, which at seven metres of cottage is 10.6 buffer pixels to
the metre; the first build used 6, derived from the size of the figure
instead, and every village in it looked like a model railway. PPM
ships at 10.
Three afternoons of arithmetic, and three things that had already been built the other way round.
Focus ยท the light field
The soft areas are a real defocus, and that is the mechanic
In the reference, part of every frame is a crisp halftone and the rest is flat posterised shapes. The cheap explanation is that the dither is only applied in the sharp part. Two measurements say otherwise.
The first is sharpness: for each band of rows, the median ratio of a one-pixel first difference to a three-pixel one. A sharp edge changes as fast over one pixel as over three, so the ratio is high; a blurred edge does not. It runs from 0.36 in the soft rows to 0.55 in the band. The second is the carrier: the strength of the 8.4375-pixel periodic signal in the same rows, as a fraction of the local contrast. It goes with it โ 0.24 to 0.66 โ and it never reaches zero. The dots are still there in the soft rows, smeared. That is what a defocus does to a print, and it is not what switching the dither off would do.
So printed() โ sample, quantise, screen โ is written as a function
that can be called at an offset, and the main body calls it nine times in two
rings whenever the blur radius clears a threshold: the centre at weight 0.28, four
taps at ฯ weighted 0.12, four diagonal taps at 2ฯ weighted 0.06, summing to one.
Two rings rather than a box of nine, because a box leaves the dot lattice visible
as a beat pattern; two rings put the kernel's zeros near the carrier and kill it.
The result is then interpolated between ramp entries rather than snapped
to one, because a defocused print really does land between two levels.
Which leaves where the sharp band is, and that is the game.
Composer.lightField paints the 128ร128 focus canvas each frame: a
floor of round(day ร 96) out of 255 over the whole square, then a
blob of 26-metre radius on the bearer and a disc around anything burning, drawn
under lighter. The shader adds its own floor โ
ambient = 0.14 + day ร 0.26, a different quantity โ reads the field,
and sets the blur radius to
(1 โ smoothstep(0.12, 0.92, sharp)) ร uBlur.
It gives the day cycle away for free. At noon the floor is high, the frame is
dithered nearly everywhere, and the flat shapes are pushed out to the corners. At
three in the morning the floor is near zero and the field collapses to whatever
you are carrying a light for. There is no second set of art anywhere in the
project; the whole cycle is exposure, lift,
contrast and this floor.
Walking is the act of bringing the world into focus.
The ground ยท src/world
Albedo, form and texture are three separate numbers
A pixel of ground gets its tone from three things, and the version of this that works keeps them apart: how bright the material is, how it is lit, and what it is made of. Three multiplications and three owners.
Albedo is a table in src/world/surfaces.ts: twelve
numbers, one per surface, spread deliberately wide because six tone levels is not
many and a world where canopy and scree sit within 0.1 of each other spends four
of them on nothing. Canopy is 0.30, the darkest thing in the world that is not a
silhouette; old snow is 0.94. Form is the hillshade, computed in
chunks.ts on a 65ร65 lattice per 256-pixel chunk โ one node every
four buffer pixels, bilinear in between, because the shape of land is
low-frequency. Texture is the generated aerial plate, and it only
ever modulates: every plate is normalised to a flat mean of 0.5 and enters as
0.75 + tex ร 0.5.
The first version had brightness baked into the photographs โ each plate normalised to its own mean, so "how bright is snow" was a number in the art pipeline. Every change to the sun broke a biome. With the three separated, moving the sun changes one term and nothing else has to be touched.
Two details in the hillshade are worth the space. The Lambert term is
quantised โ round(max(0, ฮป) ร 6) / 6 โ because the
reference's terrain is flat-shaded polygons whose facet interiors are one constant
tone, and a smooth Lambert over a smooth heightfield gives the opposite: an
unbroken gradient. And the gradient is exaggerated by 4.5 before it is lit,
because a true hillshade of this continent is nearly featureless โ the moor is an
11% grade at its worst โ and a nearly featureless hillshade quantises to one grey.
Vertical exaggeration before shading is ordinary practice in relief cartography;
the Swiss manual tradition Eduard Imhof wrote up does it by hand.
Then there is the micro-relief. The heightfield is two metres to the texel, which at ten pixels to the metre means the finest shape it can hold is twenty pixels across. But the plate is a photograph of ground, so its own local slope is a height gradient already: two extra array reads give it, and lighting that slope with the same sun gives per-pixel form at the plate's frequency for the cost of the reads.
const bx = (texR - tex) / 255, by = (texD - tex) / 255;
const micro = 1 + (bx * 0.62 + by * 0.55) * BUMP;
0.62 and 0.55 are the sun's own x and y. It applies where a chunk is covered by a single surface; where two are blending, the two extra reads would have to be done per surface and the term falls back to 1.
Chunks are baked at 256 buffer pixels and cached โ a few milliseconds once,
then a drawImage forever, with an LRU of 96 and a budget of two
bakes a frame. The camera never travels more than a pixel or two between
frames, so a chunk that misses its turn is off screen by the time it would
have mattered.
Roads and rivers are stroked into the chunk in world space rather than
evaluated per pixel: a distance field on the low-resolution lattice would put
a road edge on a four-pixel staircase, and the road is the one thing in the
frame the player is actually following. The road goes down under
lighter and the river under source-over, because a
road is the brightest line on the moor and a river is the darkest.
Everything nobody planted is not stored at all. The world is cut into 14-metre cells and each cell's contents come out of a hash of its coordinates, so a query for "what is in this rectangle" is a loop over a few hundred cells and no memory. Walk east for a kilometre and back and the same tree is still there, because it was never anywhere.
The art ยท tools/art
Never ask the model for the look
Seventy assets are specified in tools/art/manifest.mjs and generated
with gpt-image-2 through fal.ai. The model is never asked for the look.
The only place any of it is mentioned is as a negation: the shared
NO_STYLE fragment, concatenated into every prompt, says "not pixel
art, not vector art" and "no grain filter". The file's own header states the rule
โ every asset is generated as an ordinary continuous-tone photograph, in colour,
at high resolution, and the screen shader does the rest.
Ask for "1-bit dithered top-down pixel art" and you get a picture of dithering โ a drawing whose dots are content โ and running a real dither over that produces interference with no legible form left in it. Ask for "an aerial photograph of moorland, overcast" and you get material with genuine tonal structure, which the screen shader turns into the look in one pass. The model's job is tone and form.
Four shared fragments assemble every prompt: NO_STYLE,
PLATE for terrain, OBJ for anything that stands up and
ICON for the item pictures. They are opinionated about lighting for a
reason. Plates are asked for flat overcast light, no sun, no cast shadows โ
because the terrain's form comes from the engine's own hillshade, and if the
photograph brings its own sun the photograph's shadows fall one way and the
hillshade's fall the other. Objects get the opposite: hard directional sunlight
from the upper left, camera 70ยฐ above horizontal, because their form is all they
have. They are drawn a couple of dozen pixels tall and the only thing that says a
cottage is a cottage is the difference between its lit roof plane and its shaded
one.
1 ยท matte
Objects are shot on flat magenta. Alpha comes from distance to pure magenta, ramped between 0.16 and 0.40 rather than thresholded, so the anti-aliased rim survives the downsample as partial coverage. Despill acts on the magenta region โ anywhere red and blue both sit above green โ which is somewhere a brown, a grey or a green never is.
2 ยท downsample
Repeated halving in a canvas rather than one draw. A single draw from 1024 to 30 pixels samples far too sparsely and loses whole branches; halving keeps them as tone.
3 ยท seamless
Plates only. Roll the tile by half in both axes and blend the two copies through a mask that is 1 at the border and 0 at the centre, so the rolled copy's own seam lands where the mask has already zeroed it. A four-way mirror was the first version; scree gave it away immediately.
4 ยท tone
Colour is thrown away. Luminance at 0.32/0.55/0.13 โ Rec.709 with green pulled down, because foliage is the commonest subject here and true 709 sends every conifer to the same value โ then normalised between the 2nd and 98th percentile to a stated mean and contrast.
What is committed is a single channel of tone. An asset that brought its own
colour would be the one thing in the frame that is off-ramp. Eight of the
vegetation sprites go further and keep only their shape โ
ink: true forces every opaque pixel to the bottom of the ramp โ
because the reference's conifers and figures are solid black with no interior tone
at all.
Two things were deliberately not generated. Cast shadows are baked at
load by shearing each sprite's own alpha away from the sun, flattening it to one
tone and blurring it; thirty generated shadows would have been thirty chances for
one of them to disagree about where the sun is. And the figures are drawn by hand
as character grids in src/draw/figures.ts, because the bearer is
fourteen pixels tall and there is nothing for a photograph to be at fourteen
pixels.
The run produced 65 of the 70. The fal.ai account ran out of
credit with relic, i-candle, i-herb,
i-boot and i-icon outstanding, and the engine draws
documented fallbacks for all five: a missing item icon is a plain filled square in
the hand screen, a missing sprite a black lozenge of the right height, so the
composition, the sorting and the shadows stay honest. The burden โ the thing the
whole walk is about โ is a grey square in your pack.
Two screens ยท src/ui
Your inventory is a photograph of your own hand
The inventory is not a grid of boxes. It is a generated photograph of a bare left hand, cropped by the bottom of the frame so the wrist is out of shot, and the five equipment slots are its fingers. The idea is lifted from the reference almost move for move, and it changes what an inventory is: an item description like "A symbol of love. The ring looks old." only lands because the ring is on a hand.
The five finger positions are not laid out by hand. They are measured off the
generated plate: take the top-most opaque row of each column of
public/art/hand.png and it has five local minima, one per fingertip,
at 0.906, 0.633, 0.386, 0.228 and 0.045 of the width. Those five numbers are the
slot positions in src/sim/items.ts, with a comment saying that
regenerating the plate means taking them again. The names come from the reference
and they are real: thumb, shooting finger, long finger,
medicine finger โ the ring finger has been the medicine finger
since Old English, because it was the one a physician stirred with โ and ear
finger.
A ring is not drawn as jewellery. At 512 pixels a ring is four pixels of gold and reads as a scratch, so it goes down as a band across the finger: an 11ร2 bar of paper with an 11ร1 bar of ink under it, which is what a ring looks like when a halftone is all you have.
The other screen is the map โ a hex field on a circular vignette, nine hexes
across a 1536-metre continent. Every hex is filled by screenFill, a
4ร4 ordered dither laid into the buffer at a coverage set by the hex's elevation,
so the map is a hypsometric tint made of nothing but dot density. A hex within
sight but not yet walked into gets 42% of its coverage and a question mark; one
further off gets 16% and no glyph, so the continent has a shape from the first
minute and the walk fills it in. The circle is sized so the square continent's
corners fall just outside it: you are looking at a square world through a round
hole.
Both screens are also why the world is drawn with an ordinary 2D canvas rather
than in GL. It costs a texture upload per frame โ about 0.4 ms for 512ยฒ โ and buys
ctx.fillText, which is how the blackletter gets into the buffer
before the screen shader rather than on top of it. The type is treated in
two ways for the same reason: the display face keeps its anti-aliasing and is left
for the shader to break up, and the eight-pixel body face is thresholded to one
bit at 0.42 alpha first, so the shader has nothing to break.
Everything on screen goes into the same 512-pixel buffer and through the same shader; a UI layer composited after the screen pass would be the only crisp thing in the frame.
The rot
The only thing the rot does is make the screen worse
rot is one number between 0 and 1. It rises only while the burden is
on your back, faster the further you have carried it, and nothing in the world
removes it. It is read in five places, and none of them is speed() or
the stamina drain: applyLook writes it into six shader uniforms,
hand.ts picks which of the burden's four descriptions you are shown,
hud.ts decides how many blocks of the burden meter are drawn, and
screens.ts uses it twice โ once to choose the ending, once to print a
percentage on the end screen.
The world does not become harder to cross. It becomes harder to read. And because the sharp band is the mechanic, a rotted screen is one you have to walk further into to see anything, which costs stamina, which is the stat that decides how far you get in a day.
The weight is the other half of the arithmetic. The burden is 4.0 kg against a working limit of 6.5, and the fourteen things you can pick up and carry weigh 5.59 kg between them, so the one object you did not choose to bring is most of your capacity before you have found anything. Past the limit the stamina drain goes quadratic: under it an extra kilo costs you a few seconds, over it an extra kilo costs you the afternoon.
The rot also decides how the walk ends. Reach the shore having put the burden down and you get Set Down. Reach it still carrying it with rot above 0.72 and you get Carried In. Reach it carrying it with less than that and you get Turned Back. Three endings, one threshold.
The number that decides how the walk ends is the same number that has been coarsening the print for the quarter of an hour it takes to get there.
The bill
What measuring a look costs
Measuring a look rather than eyeballing it buys certainty about the parts you measured and nothing at all about the parts you did not. Here is the list.
The terrain is not the same construction as the reference's. Its ground is genuinely flat-shaded low-poly geometry: real facets, straight facet edges, a hard tonal break where two planes meet. Bearer's is a heightfield with a quantised Lambert term over it. Rounding the shading into sixths gets facet-like bands out of a smooth surface, and at six tone levels and 512 pixels a still frame holds up โ but the edges between bands follow contours of a smooth field, not the edges of triangles, and if you go looking for straight facet boundaries in a Bearer screenshot you will not find them.
The dot shape is the weakest of the measurements. The extracted matrix proves ink grows from the cell centre and proves the corners fill last, and it ranks the four candidate shapes โ but all four correlate positively with it, and 0.798 against 0.655 is a preference rather than a proof. The diamond is in there because it comes first. A square shipped for an afternoon on a written description instead, and cost nothing measurable: both shapes are equalised, and the tone histogram did not move between them.
printed() calls each, sampling, quantising and screening every time. Cheap on a desktop GPU; the reason the buffer is 512 and not 1024.The debt, stated plainly: the rendering studied here is Dean Moynihan's, in O EMPIRE! WARD OFF THY ROT. The measurements were taken from that game's published store screenshots and the technique was reimplemented from them, not ported. Everything on the other side of the shader โ the continent, the burden, the fingers, the endings โ is Bearer's own.
A measurement can only settle the question somebody thought to ask, and nothing in the study asked what the ground was made of.