How it works

This floor heals itself

I taught a neural network to grow the Mona Lisa from a single cell, then rebuilt that network out of Pixel Processors in Substance Designer. It lays the portrait in 13,101 stones. Cut it and it regrows. Nobody animated the growing or the healing, and further down you can cut it yourself.

16,512 weights1,504 network steps chained in one graph13,101 stones18.4 s, 442 frames, no frame repeats
I
Try it

The floor

Every cell in this image is a tiny copy of the same neural network. A cell can see only its eight neighbours. There is no picture stored anywhere, no target and no timeline. Each cell looks around, works out what it should be, and changes a little. Run that for a few hundred steps from one living cell and a face assembles itself.

Below is the real thing, not a video. The same 16,512 weights that drive the film are running on your graphics card right now, one step per frame, with the same rules as the Designer graph. Drag across the face to cut it.

Figure 1 · The network, running on your GPU
Drag across the face to cut it
Step
0
Living cells
1
Held open
0
Film timeline, 0 to 1,504Dragging back replays from one cell
Run
Steps per frame
Damage it
See
Stones view is the look pass in shaders, with fixed relief and without the graph's noise fields. Cells view is the raw network, 128×160. Hidden shows the 28 channels with no meaning I assigned: the network invented them to talk to itself. Big cuts can leave scars, for the same reason as in chapter 8.
II
Sixteen thousand numbers

One cell

This is a neural cellular automaton, the idea Mordvintsev, Randazzo, Niklasson and Levin published in 2020. A cell holds 32 numbers. The first four are colour and alpha. The other 28 are free: the network uses them however it likes, and training decides what they mean.

Each step a cell measures its surroundings: its own 32 values, plus how each one is changing left to right and top to bottom, using Sobel filters. That's 96 numbers. They go through 128 hidden units and come out as 32 small changes to the cell. That's the whole model, 16,512 weights, about the size of a small icon file. Pick a cell and watch those numbers.

Figure 2 · Inside one cellcell 52, 63
State · 32RGBA, then hidden
Perception · 96self, ∂x, ∂y
Hidden · 128relu, computed in JS now
Update · 32added on half the steps
Terracotta is positive, green negative, pale is zero. The numbers come from your live floor, and the last two rows are computed here in JavaScript from the same weights.

Two rules hold the system together. A cell only updates if a coin flip says so, so cells can't lock into a global clock. And a cell with no neighbour above alpha 0.1 is dead: it's zeroed, every channel. The face is an agreement the cells keep reaching, one neighbour at a time.

The coin flip needs care. Designer has no random numbers that would match Python's, and for a proof I need them to match. So the flip is a small integer hash of x, y and step, using only values under 224. Those are exact in 32-bit floats, so the mask is bit-identical on CPU, on Metal, in Designer and in your browser.

Figure 3 · The fire maskstep 0

h = (37x + 101y + 59k + 7) % 997
h = (h² + 13k + 3x) % 997
h = (89h + 7y) % 997
fire = h < 498.5

III
Four networks

Teaching it

Training looks like this: start from one cell, run a random number of steps, compare the picture to the target, and push the error back through every step. Then damage the result, put it in a pool, and start from it again later, so that the network learns to survive and not just to arrive. The first version took a 48×64 face. It read as the Mona Lisa, through a visible pixel grid, and a mosaic made from it was mush.

The second problem took longer to see. v3 had four times the cells, and its final picture was far better. But it only had a loss at the end, so nothing stopped it from looking bad on the way there. Growth split into 13 to 17 blobs, lopsided 4 to 1, and read as damage spreading. The heal ghosted a doubled brow.

For v4 I added losses during the rollout: a growth front that has to expand, and a hole that has to close from its edges, checked every 12 steps. Warm-started from v3, 8,000 iterations, 3.7 hours on the Mac.

v2 growth stripv3 growth stripv4 growth strip
Top to bottom: v2 at 48×64, v3 at 96×128, v4 with mid-rollout losses.
Table 1 · What the mid-rollout losses boughtseeds 0 to 2, slash at step 300
Measurev3v4
Left/right balance, step 404.351.04
RGB error, step 602.380.0137
Heal band error, +80 steps0.00220.0009
Heal band error, +200 steps0.00130.0007
Separate blobs from step 608 to 173 to 6, main body over 99.8%
My blob target was at most 2, so v4 missed on the letter. The extras are nearly all single cells. Stable to 2,000 steps on three seeds; one of the three settles about 3× worse than the others.
Mid-heal brow, v3 above v4
The same heal, v3 above v4. The doubled brow is gone.
IV
No tensors allowed

A network made of texture nodes

Python did the training. The film doesn't use Python to run the network. Designer does. It has no tensors, no matrix multiply and no memory between frames, so the network has to be spelled out in Pixel Processors.

The 32 channels of the whole grid are packed into one image, 8 RGBA texels per cell. Hidden units get their own image, 32 texels per cell. The weights are a 32×256 bitmap. One nca_step sub-graph holds two Pixel Processors: one forms all 128 hidden units with a while loop, the other turns them into the update and works out who lives. Hover the atlas to see what a texel means.

Figure 4 · The whole network state is this image1024×160 · 8 texels per cell
Hover the atlas
Weights: 32×256, high bytes

Rows 0 to 95: weights into the hidden layer.
Row 96: bias.
Rows 97 to 224: weights out to the 32 channels.

A 16-bit PNG came back from Designer as fp16, about 6e-4 off. Same picture, but the trajectory drifted 0.2 by step 60, which ruins any exact test. So every weight is two 8-bit textures, high byte and low byte, rebuilt in the graph. 8-bit reads are exact: 0 of 32,768 bytes wrong.

Nothing survives between frames in Designer, so nothing can be stored. Frame 900 of the network is seed, then 900 instances of nca_step, recomputed from scratch. An instance past the current frame just passes its input through. The final graph chains 1,504 of them.

All 1,504 nca_step instances in the main graph, plotted from the .sbs file
The main graph, all 1,526 nodes and every wire, plotted straight from the saved .sbs. That line is the network running 1,504 steps. The red nodes are the look pass.
Hidden pass function graph
Hidden pass: 705 function nodes, 35 of them texture samples in red.
Update pass function graph
Update pass: 1,189 nodes, including the alive test and both wounds.

I expected memory to be the problem. On paper the intermediates come to about 17 GB. In practice sbsrender holds about 4 GB with the look pass in, and a frame of state takes 13 seconds on an M3 Max. Your browser does the same 1,504 steps in the time figure 5 reports below, because it can keep state from step to step and only runs the arithmetic it needs. Designer has to prove every frame from nothing, and that's what makes it interesting.

V
Three machines

Proof

A network ported by hand is only worth anything if it's the same network. So every Designer render is diffed against lm_ref.py, a Python reference that steps the same weights by the same rules. Steps 1, 10 and 60 of the final network differ by at most 2.2e-5, with every living cell identical.

Here's a third machine: yours. This figure replays the film on your GPU from a single cell, then compares it against the state Designer rendered on my Mac, stored in this page.

Figure 5 · Designer against your GPU
Designer, sbsrender
Your GPU, computed now
Difference × 1000
Pick a step.
Colour is compared to about 5e-5, the precision stored here. Alive means the cell's alpha is above 0.1. The 1504 button is the honest one: read on.

It isn't perfectly clean, and I'd rather show that. The network is chaotic at its edges. Machines round 32-bit floats differently, and now and then one cell on the rim tips the other way. On the 840-step trial a patch on the left of the oval drifted: 18 cells had a colour off by more than 1/255, the worst by .018. On the final 1,504-step run Designer and Python agree on every living cell at every step I checked, with colour inside 3.5e-4. The hidden channels reach 2.1e-3 by the end. Your GPU rounds differently again, and by step 1,504 the gap has grown: on my Mac in Chrome, 4 of 9,608 cells disagree and 193 are off by more than 1/255, all from that same kind of tipping. The face is the same; the last few rim cells aren't. Designer against itself is exact: a repeat render comes back bit-identical, and that's the determinism the film needs.

VI
Andamento

Cutting stone

A network output is a colour grid. A mosaic isn't. My first pass put tiles on a lattice and it looked like a JPEG with grout. So I went to a real one, a 2nd century Roman floor with a portrait emblema in the Met's open collection, and copied how a setter thinks. Thin pale grout. Field stones in straight courses. Rows that hug the figure, which is called andamento. Finer stones in the face.

The rows in the face follow the image. I take the strongest edges of the healed portrait and lay stones along lines of equal distance from them, 3 fine rows along each feature. Brow, eye, nose and mouth all get rows that bend around them. That's 13,101 stones, placed once. They never move, so the animation is stones changing in place.

This part isn't pure Designer, and I want to be clear about it. The layout needs skeleton tracing, so Python computes it once and it enters the graph as bitmaps, along with static noise for veining and wear. Everything that changes per frame runs in the graph: sharpening, snapping to stone colours, losing stones, setting them back. It matches the Python look within 1/255 on every frame I diffed.

Figure 6 · One frame, map by map
Lit renderSelected map
Left of the line: Blender. Right: the selected map, from Designer, step 840. Each stone also gets a material: matte, honed or polished, by roughness.

Each stone is one texel. The look pass reads the network at the stone's centre, sharpens it the way a mosaicist exaggerates a feature, and pulls it halfway toward one of 13 stone colours. When the network passes through a colour no stone comes in, the stone snaps fully. That's what ended the rainbow stones during growth. Every stone in the floor, in one 256×52 texture:

Figure 7 · 13,101 stones, one texel eachlive from figure 1
The 13 stones

Getting it to look like stone took longer than getting the network into Designer. A list of what failed: tall domed stones read as cobbles, so the floor is ground nearly flat, the way Roman floors are. Dark grout looked wrong too, so the mortar is pale and only darkens under dark stones. Reds came back cyan until I found an 8-bit wraparound. The first bare patch still showed every stone's outline stamped in the bed. Creases read as worms. Smooth drag streaks read as wood grain. What's there now is broken chisel strokes along the wound.

Early look progress
Early flat passes against the Roman reference.
Face progress
Lattice, then contour rows, then the Met face.
VII
The bug I shipped to the 4090

The frozen slash

The first full render went through and I caught something in the final frames. There's a shimmer in the floor, because the network never fully settles. Stones keep trading tiny amounts of colour. During the slash, that stopped.

The clip was cheating. To make the blade travel, I'd frozen the network at step 480 and wiped a mask across the frozen state. The hold on the finished cut was one frame repeated 24 times. The end hold was another. The floor was alive except at the moment it mattered most.

So the blade moved into the network. It's now a rule inside nca_step: the update pass clears cells behind a moving tip, on the step they're reached, while everything else carries on. There are no repeated frames in the film now. Shimmer on a patch of hair measures .00086 per frame before the cut and .00089 during it.

Then the network pushed back. A wound held open for 85 steps never quite healed. The cells beside it spent that time adapting to a hole, and the face came back with a permanent scar, at 2.5× the normal error. So the open wound lasts 12 frames, half a second, which is as long as the network tolerates.

Table 2 · How long can a wound stay open?error at step 1,116, baseline .00054
Blade sweepReleased atEnd errorBad cells
1 step482.0005136Instant cut
18 steps510.0005236The film
18 steps499.0005887
36 steps517.0005989
36 steps530.00079160
18 steps530.00092201
36 steps565.00136422A permanent scar
VIII
Again

The second wound

One heal could be a trick. It had to take damage again. So I ran cheap tests: real weights, plain NumPy, raw colour, about a minute for nine effects. Each one scored by how close it came back. The rule the first round taught me was blunt: the network recovers from thin or small damage, and it can't rebuild a face from nothing. Big central loss never came back.

Round 1. S-curve, Spiral, Claw, Cross, Rock, Bite, Shatter, Dissolve, Bullets. Bite and Dissolve never recovered.

Round 2 tried S-curves with real brush shapes, impacts with debris, and an idea from real life: someone scrubbing with a pencil eraser. Wiping out the eyes of the most looked-at face in art was the first thing with meaning, not just damage. It also lost 3,215 cells and came back.

Round 2. The squares are a placeholder overlay for debris. The network can't throw stones.

Then the stroke had to become a real gesture: one continuous zigzag, eraser never lifted, from the eyes to the bottom of the portrait. Every first attempt failed. A faster stroke was better, which told me the limit was time open, so I swept 24 settings of rows, width, speed and hold. A 1-second stroke healed. It was too fast to read. At 2 seconds I expected it to break, because 32 steps had been worse than 24. It didn't, but 4 rows couldn't stay open longer than 4 steps. With 3 rows the strips left between passes are wider, those strips are what the face regrows from, and each spot can stay bare for 12.

Figure 8 · The strokestep 0
Open now
0
Touched so far
0

The stroke's timing is one baked number per cell: the step the eraser arrives. Dark is open now, tan has been released.

Table 3 · Stroke sweep2 s stroke, from the healed state
RowsWidthStepsHoldEnd errorResult
43.5484.00053Heals. Mostly one band visible at a time
43.5486.00114Doesn't reach .001
43.5488.00310Damaged
43.54810.01451Damaged
33.54812.00053The film
33.04812.00052Heals faster, less visible
Timing is a narrow margin. A Long Pause variant of a stepped eraser had the same bands as a passing one and failed on timing alone. Each change gets retested.
Flat mosaic comparison at 2 s: 4 rows, bare 4 steps, on the left; 3 rows, bare 12 steps, on the right.

It joined the film the honest way. The network just keeps running from the healed state. Half a second after step 1,032, the eraser lands. Step 1,032 in the longer graph is bit-identical to the approved clip, so there's no join to see, and in the exact stepping the face returns to .00052 by step 1,504. It's the same error as before the eraser.

Eraser mid-stroke in stone
Eraser at the end of the stroke
IX
Blender, Cycles, RTX 4090

Light

Designer makes three maps for every frame: base colour, height and roughness. It doesn't draw the picture you see. Blender gets a displaced plane and three area lights: a warm key about 18 degrees above the floor, a cool fill, and a soft box placed in the camera's mirror angle so polished stones catch it and matte ones don't. Raking light is what turns height into stone.

Designer stays on my Mac, where the license is. The maps get copied to a Windows machine with an RTX 4090, and Cycles renders the frames there. Frames through step 480 were unaffected by the blade fix, so those stayed.

The Mac did have one last bad night. An early clip run used a shell throttle that counts 0 in zsh, so 162 render jobs started at once, the machine ran out of memory, and the kernel panicked. Since then, every heavy job has a tested worker limit and a memory guard.

The healed face, close
The healed face, close.
The slash, close
The wound from an earlier look pass. Bare plaster, broken edge, real cast shadow.
X
Ledger

Numbers

16,512weights
32 / 128channels, hidden units
128×160cell grid
9,582living cells at step 480
1,504nca_step instances
1,894function nodes in one step
13,101stones
13stone colours
4 GBsbsrender memory, with look
~16 sone frame of maps, M3 Max
442frames, none repeated
3.7 hv4 training

What's still wrong

The hair has no ringlets: the network smoothed them away in every version. The eye and mouth lines are harder than the painting. The left eye comes back a little more drawn than it started. One training seed of three settles worse than the others. The wound has no debris, because a network that only removes cells can't throw stones.

What's not pure Designer

The weights were trained in Python. The stone layout, the static noise and the eraser's timing map are baked inputs. Blender does the final light. The network itself, both wounds, and the per-frame look are Pixel Processors, 1,504 steps of them.