A realistic geometry Bending the pixel map until it matches the metal
Chromatik got us started for Burning Man and carried us as far as a live VJ and DJ set. We wanted to take everything further.
Chromatik, formerly LX Studio, was the first lighting design software we ever used. It lit Naga in the desert at Black Rock City, it lit the piece again when it was showcased in Petaluma, and it is still the software running the sculpture in the pond at Golden Gate Park today. You place fixtures in a 3D model, you get a pixel map, and you get a room full of patterns, modulators and blend modes that already understand it. Going from a pile of LED strip to a serpent breathing colour took days rather than months, and almost none of those days went on plumbing.
Naga uses two of the simplest primitives Chromatik offers: strips, for the runs of pixels along the fins and spikes, and spirals, for the helices wound through the body. That is the whole vocabulary the sculpture needed. The software is capable of a great deal more. Arcs, grids and points come as standard, and above those sits a custom fixture system where a fixture is a JSON file with its own named parameters, expressions, transforms and nested components, composed out of the primitives. The example files build a cube, a fan, a spiky sphere. Drop one in the fixtures folder and the parameters it declares turn into controls in the application alongside the built-in types. Our model is plain because a serpent is plain, not because that is where the ceiling is.
Fixtures are the shallow end. You can also write your own modules, your own patterns and effects, and have them appear in the application beside the ones that ship with it. It is genuinely remarkable what Mark Slee has built here, and it is all open. The one catch, for me, was that those modules are written in Java, which I was not looking forward to. We were so strapped for time getting Naga physically built that the eight hour drive to Burning Man turned out to be the only clear space left, so I wrote most of the code by voice on the way, took a two hour lunch in Truckee to make some modifications, kept dictating for the rest of the drive, and put the finishing touches on it out on the playa.
For the Grand Lighting Ceremony at Rainbow Falls in Golden Gate Park we used the rest of what Chromatik offers: its modulators, and everything it can drive from audio. We ran the piece live from a laptop at the edge of the pond, through a DJ set, changing scenes by hand while the audio reactivity drove the serpent off the music the crowd was hearing. Not much software lets you do that on the night, in front of people, without a rehearsal you cannot have.
What the model knows, and what it does not
A model describes fixtures the way you would build them. This strip
starts here and runs that way, for this many pixels. That spiral is
a helix of a given pitch and radius wound around the body. The file
Chromatik saves, a .lxm, is JSON: readable, diffable,
pleasant to parse, no reverse engineering required. That turned out
to matter more than anything else in this article.
The description is exact about the design, which is not the same as exact about the build. It says where each fixture was placed, and placement is a statement of intent. It does not claim to know where a hundred feet of welded steel and forged scale ended up once it had been trucked across a state, craned into a pond, and settled onto its footings. That was never the model's job. In it the animal is straight. In the water the animal rises and dives through three humps and lifts its head out of the water.
Two geometries, not one
Here is the part I did not expect. The straightened model is not a bug waiting to be fixed. It is useful exactly as it is, and I wanted to keep it.
Laid out as the model describes, the serpent is very nearly a rod: 331 units head to tail and 12 units tall. That is the right shape for anything travelling along the animal. A pulse running head to tail is one coordinate in that frame, and it stays correct no matter how the body is arranged, in a pond or on a truck or in a desert.
Installed, the same 540 pixels sit in a very different box: 209 units long and 42 tall, because a serpent that rises and dives is shorter and taller than the same serpent stretched out. That is the right shape for anything belonging to the place rather than the animal. A sheet of light crossing the pond, a rising horizon, anything that should read as weather instead of anatomy.
So the goal was never to replace one geometry with a better one. It was to have both, over the same pixel index, so an effect could say which one it meant and be answered.
Step one: read the model
Here is the whole shape of a .lxm, trimmed to one
fixture and to the parameters that describe geometry. The real file
carries 25 of these and about 35 parameters each, most of them
about wiring: protocol, host, universe, byte order.
{
"version": "1.0.0",
"fixtures": [
{
"class": "heronarts.lx.structure.SpiralFixture",
"parameters": {
"label": "Body head",
"x": 0.0, "y": 0.0, "z": 0.0, // where it sits
"yaw": 0.0, "pitch": 0.0, "roll": 45.0, // how it is turned
"numPoints": 38, // 38 pixels
"numTurns": 8.5,
"radius": 2.0,
"length": 44.0
}
}
]
}
That is it. A class name saying which primitive, a position and an orientation, and a few numbers describing the shape. No library, no reverse engineering, nothing to ask anyone's permission for. Being that easy to read and edit is the point and the trap: it is why the file is a good source to build on, and it is why nothing we add can live inside it.
An extractor walks the .lxm, resolves every fixture's
own transform, unrolls its strips and helices into points, and
writes out two files: the Art-Net map, and 540 positions in the
straightened frame. Nothing about that step is clever. It is worth
saying only because the alternative, retyping a pixel map by hand,
is how installations acquire a geometry nobody dares touch.
Those files are generated and never hand-edited, and CI regenerates them and fails on drift. Whatever comes next has to sit on top of them rather than inside them, or the next time the model is saved in Chromatik the fit is quietly lost.
Step two: somewhere to bend it
The editor is a small web application that runs on my laptop and is never deployed anywhere. It draws every pixel as a point in space with the wiring order traced through it, so a spiral looks like a spiral and a miswired strip looks obviously wrong. Selecting a group fades everything else, which is how you find one segment among 540 points. It reads the topology files off disk on every request, so a regeneration or a hand-edit shows up on reload. That is the picture at the top of this article.
On top of the viewer sits the actual point of the tool: corrections. Each one is a typed thing with a target and a few parameters, and there are only three types.
- Translate
- Moves a target in space. Half the fit is this, because most of what a build gets wrong is where something sits rather than what shape it is.
- Rotate
- Turns a target about an axis through its own centre. With translate, that is enough to place a segment.
- Bend
- Rotates every point in the target by an amount proportional to how far along the body it sits, so a straight run of pixels becomes an arc with the pixels still evenly spaced along it.
Bend is the one that made the project work. Picture bending a rod over a knee: the far end travels furthest and the near end barely moves. Give it a segment, an axis and an angle per unit, and two bends with a rotate is a hump. Ten bends is most of a serpent.
Layers, because a fit is never one edit
The first version wrote all the corrections into one file, which lasted about a week. Fit work does not arrive as one edit. It arrives as passes: get the head and neck right, then the first hump, then the second, then the tail, then shove the whole thing into position over the pond.
So the corrections live in files, and the files compose as a stack that applies bottom to top. Each layer can be dragged up and down the order, and each has a checkbox. Switching one off leaves it in place but skips it, which turns out to be the most-used control in the tool: toggle the neck layer off, watch the head fall back into the straight model, toggle it on, and see exactly what that layer was holding. It is also how you tell a bad bend from a bad placement underneath it.
The serpent's current fit is 39 corrections across six layers. Every one of them is a line of readable YAML with a comment above it, which means the fit reads in a diff as "we moved the fin two inches", not as 540 changed coordinate triples.
What comes out
The generator applies the enabled stack and bakes one artifact carrying all three frames at once over a single shared pixel index: wire for effects that walk the strand and ignore position at all, body for the straightened animal, and world for the sculpture as installed. Selecting any fixture or group is a slice of the same index in whichever frame you asked for.
{
"total_pixels": 540,
"fixtures": [
{ "key": "body-head", "kind": "spiral", "pixel_range": [0, 38] },
{ "key": "body-h1", "kind": "spiral", "pixel_range": [38, 76] },
... 23 more, contiguous, up to [537, 540]
],
"coordinate_frames": {
"wire": { "kind": "indexed", "count": 540 },
"body": { "kind": "points", "points": [
{ "x": 1.414, "y": 1.414, "z": 0.0 }, // pixel 0
{ "x": -1.223, "y": 1.582, "z": 1.189 }, // pixel 1
... 538 more
]},
"world": { "kind": "points", "points": [
{ "x": 1.415, "y": 34.787, "z": 1.743 }, // pixel 0 again
{ "x": -1.222, "y": 34.329, "z": 2.849 }, // pixel 1 again
... 538 more
]}
}
}
Pixel 0 appears twice and it is the same pixel both times, the first LED in the head spiral. In body it sits at the near end of a straight rod, a unit and a half off the axis. In world it is nearly 35 units in the air, because on the real sculpture the head is reared up out of the water. Nothing about the wiring moved. Take the extremes of each list and you get the two boxes: body runs 331 along and 12 tall, world 209 by 42.
That is the whole payoff. An effect now names the frame it thinks in. A chase runs head to tail in body and does not care that the middle of the animal is upside down under a bridge. A wash crosses the pond in world and does not care which pixel belongs to which strip. The same 540 pixels answer both.
.lxm, parsed as JSONpixels 540, across 25 fixtures
frames wire, body, world, one shared index
primitives translate, rotate, bend
current fit 39 corrections in six toggleable layers
editor Nuxt and Three.js, laptop only, never deployed
straight 331 long, 12 tall
installed 209 long, 42 tall