Skip to content
v0.17.0 npm

One number per lantern

The lanterns demo hangs six thousand lights over a landscape. Every colour, every flicker, every bob comes from one whole number that each lantern carries. This page is about that number: where it comes from, what the graphics card asks of it, and what breaks when it is kept in the wrong kind of box.

generated live · drawn in your browser  

every colour a lantern could be → 

The idea

A lantern is one number wearing a costume.

Hand a lantern the number 3 274 118 905 and it becomes a particular shade of orange, pulsing at a particular speed, slightly out of step with the lantern next to it. Hand it a different number and it becomes a different lantern.

Nothing is stored about the colour. Nothing is stored about the flicker. There is only the number, and a rule for reading it — and that is a very good deal, because a number is tiny. Six thousand lanterns is six thousand numbers, twenty-four kilobytes, and the graphics card works out everything else while it draws.

The library never picks a colour. It picks a number, and the number picks the colour.

Two halves

One half says where. The other says when.

pcg-ts builds the field. It works out where each lantern hangs, how big it is, and which number it carries — and then it stops. It has no clock at all, on purpose, so nothing it produces can move. The same seed lays out exactly the same field on any machine, today or next year.

The web page picks it up from there. The page owns the clock, and the page owns the little program that runs on the graphics card. Between the two halves there is one wire, and the only thing travelling down it is that one number per lantern.

the graph cooked once  ·  no clock where it hangs how big it is which number it carries same seed → same field, on every machine, for ever THE ONLY WIRE seed one 32-bit whole number per lantern. that is all. the page sixty times a second  ·  owns the clock what colour it is how brightly it is lit right now how far it has bobbed knows nothing about where anything is, or why
The split. The library ships the data, not the shader. It does emit a shading language, but only compute kernels: there is nothing in pcg-ts that draws an instance. That is exactly why this demo needs a web page, and why the wire between the halves has to be an agreement rather than a convenience.

The number

Nobody invents it. Every point already had one.

A pile of points in pcg-ts is not just positions. Every point carries a little row of named values, and one of them is called seed — the point's name badge. It is a scramble of where the point came from: which node made it, which number it was in the queue, and — in a streamed world — which patch of ground it fell on. Scramble the same things on another machine and you get the same badge, which is the currency the library's whole determinism claim is paid in.

ONE POINT, AND WHAT IT ALREADY CARRIES P where it is scale how big color not used here density not used here seed its name badge 3274118905 0 … 4 294 967 295 The demo reads the badge and adds nothing. It could not add one even if it wanted to: the arithmetic that builds attributes runs in floats, and a float cannot count past about 16.7 million one at a time. The badge has to ARRIVE whole. That is the next section.
Carried, not computed. seed is already on every point cloud the library makes. The demo's only job is to get it out to the graphics card with its type intact.

Reading the badge

Three questions, asked of three slices of the same number.

A 32-bit number is thirty-two yes-or-no switches in a row. The little program on the graphics card does not use it as a quantity at all — it reaches in and reads slices, the way you would read a phone number as an area code and a line.

THE BIG END  ·  SWITCH 31 SWITCH 0  ·  THE SMALL END not used switches 24…18 7 of them switches 17…8 10 of them switches 7…0 the ones, twos and fours how fast it pulses 128 speeds when it pulses 1024 offsets, so no two beat together what colour it is 256 hues around the wheel Note which end is which, because everything after this follows from it. The COLOUR is at the small end — the ones and twos and fours. The SPEED is far higher up, from a quarter of a million to sixteen million.
The three slices. Chosen to make the next section legible rather than to hide it: the colour sits where the damage lands first, the speed sits where it lands last.

The catch

A float cannot count that high one at a time.

Almost every number a graphics card handles is a float — the kind that can be 0.5 or 3.14. A float buys that flexibility by keeping only about seven digits' worth of detail and then remembering separately how big the number is. Small numbers get counted exactly. Big ones get rounded to the nearest step, and the step keeps doubling as the numbers grow.

WHOLE NUMBERS A FLOAT CAN ACTUALLY HOLD up to 16 777 216 every one of them to 33 million every 2nd to 67 million every 4th … doubling … every 8th, 16th above 2 147 483 648 every 256th A lantern's badge is spread over that whole line, and 3 274 118 905 lands in the red. Store it in a float and it moves to the nearest number the float CAN hold: 3274118905 3274118912 seven higher. The SPEED switches are untouched — but the last eight, the whole COLOUR, are gone.
Where the damage lands. Rounding always eats the small end first, and the small end is exactly where the colour was kept. The speed sits far higher up, and almost always survives.

The badges are not small numbers. At the demo's default seed, 5 894 of the 6 000 come back from a float changed.

How the demo shows it

Five nodes, and the fourth one is deliberate damage.

pcg-ts is a node graph: little boxes that each do one thing to a pile of points, wired nose to tail. The lantern graph is five of them, and it would be four if it were not trying to prove something.

THE WHOLE GRAPH scatter 6000 points, flat lift push them into hills size vary how big widen the badge, in a float spawn hand over both the only node here that is a mistake on purpose it copies each badge into a second column that is made of floats, which is what a renderer would do to ALL of them if it did not keep types Both columns ride out on the same spawn, onto the same lanterns, in the same order. The picker in the demo just chooses which one the shader reads. Nothing is re-generated.
Two columns, one order. Lantern k's exact badge, lantern k's float copy and lantern k's position are the same lantern. That is what lets the page draw one against the other without matching anything up.
1scatterLay 6 000 points on a flat square. Evenly — nothing about the final shape is smuggled in here.
2liftPush each point up by a noise value, plus a little hover, so the sheet becomes a cloud over hills rather than a surface.
3sizeGive each lantern its own size. Size belongs to the graph: a lantern that changed size with the clock would be the page animating structure, which the split forbids.
4widenCopy the badge into a float. This is the damage, done on purpose and in one place, so it can be drawn instead of argued about.
5spawnHand the renderer the positions and both badge columns, by name.

What you are looking at

The colour is the measurement, not the decoration.

Because the hue comes straight off the small end of the badge, the spread of colour in the field is a readout of how much of the badge survived the trip. Switch the demo's picker to the float column and the field falls in on itself. Four of the five numbers below are the ones the demo's own panel prints, cooked at its default seed. The fifth is counted the same way, off the same two columns:

6,000lanterns
5,894badges changed by the float
±128worst drift
256 → 72colours still in use
4,029lanterns that end up one colour

Two thirds of the field lands on a single hue. Three hues take nearly nine in ten. The strip at the top of this page is that collapse drawn live — and it is not a contrived worst case, it is the ordinary case for a name badge. Try another seed: the numbers barely move.

Destroyed

The colour

  • Lives in the last 8 switches — the ones the rounding eats first
  • 256 hues in use become about 70
  • You can see this happen. That is the point of the demo

Spared

The pulse

  • Lives far higher up, where the rounding only reaches by carrying — 3 lanterns in 6,000
  • Every lantern still blinks at its own speed, out of step with its neighbours
  • Which is the trap

It still twinkles. It is still wrong.

That second column is the reason this demo exists rather than a note in the docs. If the page had scrambled the badge again in the shader instead of reading its switches directly, the broken field would have looked exactly as random as the good one — different, but not visibly different, which is worse. Half of a wrong answer looking right is how a bug like this survives review.

And in this case the same value has to come back tomorrow, on someone else's machine. “Looks right” is not the test. The claim is about the number.

If you are wiring this up yourself

The type has to match at both ends, and it will not fail quietly.

The library hands each named column to the renderer as an attribute of its own, keeping the whole-number type. The page declares it on the other side as a whole number too, and if those two disagree the graphics driver simply refuses to draw — it does not round the badge for you behind your back.

The one thing worth knowing is that the two columns fail in different ways if a graph stops producing them. A missing whole-number column is a hard stop: the page comes up empty. A missing float column reads as zero in silence, and because it is only looked at on the far side of the picker, the demo would look perfect until someone flipped the switch. So the demo names both columns as requirements when it builds the meshes, and gets told which one went missing rather than diagnosing it from whichever picture it happened to draw.

The demo's own source is worth reading for one more reason: the long comment explaining a line that is not there. Common advice says to flag this kind of channel as an integer for the renderer. Measured on this path, the channel is already an integer without the flag, so the flag does nothing — while on a float column that same flag makes the driver reject the draw. It is not dead everywhere: on a yes-or-no channel, which is stored as single bytes, it is the thing that picks the integer path at all. A line that is a no-op in a file people copy is worse than no line.

Next

Where to go from here

The demo itself is at demos/lanterns — the picker is in the panel on the left, under “id source”, and the panel prints most of the numbers this page quotes. Dressing a roadside is the other guide, about a demo that decides what stands where rather than what colour it is. Architecture is how the library underneath is put together, and the corpus gallery is every graph in the repository, cooked and shot.

← back to the landing page