CueSync Pixel
LED pixel fixtures for Unreal Engine, driven live over Art-Net or sACN. Every fixture also ships
as a .gdtf, with its geometry inside it, to patch from in any tool that reads GDTF 1.2 —
at the same cell count and channel layout as the fixture you render in Unreal.
Version 1.2.1 · Unreal Engine 5.8 · changelog · licence · support@cuesync.live
Flashing fixtures can trigger seizures. The Strobe look, or DMX sent faster than three flashes a second, can trigger seizures in people with photosensitive epilepsy; see Flashing before you show one.
Quick start
You do not need a lighting console to see the pack work. Open CueSyncPixel.uproject.
It opens on Lvl_Overview — one of each of the thirty-four fixture types, patched, and captioned
in-world with its address — and every fixture is already running a look across its own
cells: a rainbow, a chase, a wave, fire, rain, sixteen of them in rotation. Looks are built into the
material and driven by engine time, so they run in the viewport with nothing sending to the
machine. They are how you evaluate the fixtures, the pitch, the diffusion and the density before
you connect anything — and, because each fixture's look is set in its Details panel, keyed in
Sequencer or called from a Blueprint, they are also how you drive the pack with no console at
all. See Looks: driving it without a console. A fixture
you drag into a level of your own runs the plain idle chase until you give it a look: select its
Body component and set Custom Primitive Data Defaults — index 0 is the look, 0-17. Either way,
live DMX takes over the moment a desk sends to the fixture's patch; see
Inside the pixel material.
Open Lvl_DemoStage for a lit stage. Its 82 fixtures carry a look per zone — a rainbow across
the tube wall, a cyan wave the width of the video wall, amber blinders breathing, a violet floor —
so the rig opens as a show, in the viewport and in Play, before anything is connected.
Look at a fixture that is off, too. Every cell is a lens: a dark cell shows the glossy optic of a batten or panel, the yellow phosphor of a blinder's COB, the milky diffuser of a tube or a bead — not a hole in the housing. See The lens.
Press Play and the rig runs a show. Lvl_Overview carries CS_DemoShow, a
LevelSequenceActor bound to LS_CueSyncDemo — thirty seconds of DMX on
Sequencer's own DMX Library track, addressing all thirty-four patches cell by cell, set to
auto-play and to loop. It goes down the same path a console does: the track writes to the output
port, the port loops back to the engine, the patches feed the fixtures' matrix cells. Nothing in
it touches the material, so nothing in it can make a fixture do something a desk could not.
It needs Play rather than the viewport, and that is not a limitation of the show: Sequencer's DMX track writes to output ports, and a value only reaches a patch on a game-thread tick. The looks are what the viewport gets; the show is what Play gets.
When you do want to drive it from a desk:
- Point an Art-Net or sACN source at this machine: your console, or any DMX test tool on the same network. The project ships an input port for each, covering universes 1-179 and listening on every interface, so there is nothing to configure first.
- Open
Lvl_Patchand press Play. It is the 82-fixture demo rig with its address captioned on every fixture, and it runs no show of its own. - Send Art-Net or sACN on universe 1, channels 1-3. That is cell 1 of
Tube_Upstage_01, the tube captioned1.1. The moment cell data arrives a fixture stops its look and shows what the desk is sending. Stop sending and it holds the last frame it received, as a real fixture does when its signal drops.
Play is what makes the fixture follow: the patches receive in the editor, but a fixture reads
its patch on Tick and an actor does not tick until a session runs — to drive the editor
viewport instead, run editor_live.py (Tools → Execute Python Script, again to
stop). Test on Lvl_Patch rather than Lvl_Overview, because in Play the overview's demo show
writes to universes 40-49 and alternates with the desk frame by frame.
The overview map is patched on universes 40-49. The 82-fixture demo rig, which lives in
Lvl_DemoStage and Lvl_Patch, is on universes 1-25 and is addressed fixture by fixture
in PatchSheet.html. The overview map's thirty-four patches, in patch order:
OV_Tube_50 40/1 OV_DotString_100 42/1
OV_Tube_100 40/25 OV_Ring_24 42/301
OV_Tube_200 40/73 OV_Neon_20 42/373
OV_Bar_8 40/169 OV_Strobe_24 42/433
OV_Bar_12 40/193 OV_Ring_48 43/1
OV_Bar_16 40/229 OV_Strip_30 43/145
OV_VideoTile_P26 40/277 OV_Strip_60 43/235
OV_VideoTile_P39 40/325 OV_Ring_12 43/415
OV_VideoTile_P69 40/373 OV_Strip_120 44/1
OV_Blinder_4 40/421 OV_Arc_48 44/361
OV_Blinder_16 40/433 OV_FloorTile_64 45/1
OV_BeamBar_10 40/481 OV_Curtain_96 45/193
OV_DotString_25 41/1 OV_Matrix_120 46/1
OV_DotString_50 41/76 OV_Neon_40 46/361
OV_Matrix_60 41/226 OV_Arc_96 47/1
OV_Arc_24 41/406 OV_Strip_150 48/1
OV_Blinder_8 41/478 OV_Curtain_144 49/1
Nothing lighting? See If nothing lights.
What else is in this folder
| File | What it is |
|---|---|
SETUP.md | Dropping the fixtures into a project of your own — plugins, DMX ports, renderer settings |
SUPPORT.md | What to check before writing to us, what to send when you do, and what is deliberate rather than broken |
ChannelMaps.html | Every mode of every fixture, channel by channel. The authority on cell counts |
PatchSheet.html | Every patch in the pack — the 82-fixture demo rig and the thirty-four overview-map fixtures — addressed, in a form you can read at the truss |
PatchSheet.csv | The same patches and the same columns, for a spreadsheet or a script. One sheet to read at the truss, one to parse |
LICENSE.md | What the Fab licence means for the .gdtf and .mvr files, and the third-party and trademark notices |
CHANGELOG.md | What changed in each version, and what a version bump means for a fixture pack |
The Tools download: the pack's Python scripts
Fab does not allow Python files in a content project's download, so the scripts are not in it —
and nothing in the project needs them to run. They are the listing's Additional Files
download, CueSyncPixel_Tools_1_2_1.zip. Unzip it over the project folder (the directory
holding CueSyncPixel.uproject): it holds only Content/CueSyncPixel/Docs/…, so every file below
lands in this folder, where the links and commands in these documents expect it. TOOLS.md in that
download says the same.
| File | What it is |
|---|---|
verify_pack.py | Cross-checks the shipped files against each other |
verify_in_editor.py | Ten checks that only an open editor can make: the DMX Library's channel layout against the .gdtf files, the cell index in UV1 and the dot grid in UV0 on every mesh, where every mesh and every placed fixture draws each cell — against its .gdtf, and on the demo maps against the rig export_rig_to_cuesync.py writes, which side of the housing every mesh's emitters sit on and that the housing faces out, every fixture's spot light against fixture_catalogue.py, the live-DMX surface on every fixture Blueprint, that Lvl_Overview plays the demo show on Play, and both master materials — lit, their live-DMX sample intact, the Look contract on its Custom Primitive Data indices, and each instance's lens |
build_materials.py | Writes both master materials from code — M_CueSyncPixel (cells, live DMX, the lens, the eighteen looks) and M_CueSyncPixelBody (the painted housing and its grain) — and sets each fixture family's lens on its instance. The material graph is this file; read it to see exactly what a cell computes |
apply_demo_looks.py | Gives Lvl_DemoStage a look per zone and Lvl_Overview a look per fixture. The worked example of driving the pack without a console |
make_grain_texture.py | Writes T_BodyGrain.png, the powder-coat grain the housings are shaded with. Deterministic, standard library only; --check says whether the shipped PNG is still what it writes |
editor_live.py | Drives every placed fixture from the desk in the editor viewport, without Play: run it from Tools → Execute Python Script, run it again to stop. See If nothing lights |
export_rig_to_cuesync.py | Writes the patched rig out as a CueSync rig file |
make_sheets.py | Writes ChannelMaps.html, PatchSheet.html and PatchSheet.csv out of the .gdtf files, the .mvr and the DMX Library. It is what makes the "generated from" line in the footer of those files true; edit a fixture and re-run it rather than editing a sheet by hand |
fixture_catalogue.py | Every fixture in the pack, as data — cell layout, photometrics, weight, identity. This is where you add a fixture |
build_fixtures.py | Writes the .gdtf files and the demo rig's .mvr from the catalogue, and refuses to write if it would move any addressing 1.0.0 shipped; --reproduce proves the shipped files are exactly what it writes |
gdtf_models.py | The glTF geometry that goes inside each .gdtf, and that build_meshes.py reuses for the Unreal mesh, so a fixture is the same shape in previz and in the render |
build_meshes.py | Builds the static mesh and material instance for a catalogued fixture that has none |
build_fixture_actors.py | Gives a new fixture its imported GDTF asset, its DMX Library type and patch, its Blueprint, and its captioned actor in Lvl_Overview |
build_demo_sequence.py | Builds LS_CueSyncDemo, the Sequencer show that drives the overview rig |
write_cell_uvs.py | Writes each cell's index into UV1 on every fixture mesh, and turns off the lightmap unwrap that would overwrite it |
lightmap_atlas.py | The atlas layout write_lightmap_uvs.py and verify_in_editor.py share: one triangle per cell of a square grid, no overlap, standard library only |
write_lightmap_uvs.py | Authors every mesh's lightmap channel (UV2): one triangle per cell of a square atlas, so none overlaps, with the Lightmap Coordinate Index at 2 and the engine's lightmap packer off |
reimport_fixtures.py | Re-imports the .gdtf files and grows the DMX Library to match them, checking that every patch keeps its mode, address and cell order. It saves what it imported and names the assets it replaced |
wire_contract.json | The DMX layout 1.0.0 shipped, frozen. build_fixtures.py checks itself against it |
library_modes.json | The modes each fixture type needs in the DMX Library. Generated; read by reimport_fixtures.py and build_fixture_actors.py |
pixel_pack_spec.py | The shared model verify_pack.py, export_rig_to_cuesync.py and make_sheets.py read the pack through. verify_in_editor.py reads the .gdtf files' channel layouts itself, and the rig through this module for its cell-position check |
What is in the box
- 34 fixture configurations across 14 families — among them, new in 1.2.0, a narrow-optic pixel batten, pixel neon flex, a pixel strobe-wash panel, an eight-lite blinder, a 60 cm ring, a half-hoop arc, the 5 m tape reel and a 4.2 m curtain
- A
.gdtfper configuration, and an.mvrof the demo rig (Content/CueSyncPixel/DMX/GDTF). Each.gdtfcarries up to ten modes —Simple,Dimmer,Pixel N,Pixel N + Master,Pixel N Dim,Pixel N Dim + Master,Pixel N 16bit,Pixel N Dim 16bit,Pixel N 16bit + MasterandPixel N Dim 16bit + Master— and a thumbnail drawn from that fixture's own cell layout rather than a generic icon - A DMX Library with every fixture type and all 116 patches, addressed and ready — 82 for the demo stage, 34 for the overview
- 34 drag-and-drop fixture Blueprints. Drop
BP_PixelTube_100into a level, point its DMX component at a patch, press Play, and every cell takes its colour from the console. The project ships with All Fixture Patches Receive DMX In Editor on, so the patches are live in the editor viewport — but the fixture reads them onTick, and an actor does not tick until Play or Simulate. To drive the viewport itself without Play, runeditor_live.py(in the Tools download) from Tools → Execute Python Script; it does from an editor tick what the Blueprint does in Play, and running it again stops it LS_CueSyncDemo, a thirty-second Sequencer show that drives all thirty-four overview fixtures cell by cell — 4,599 channels of keyframes down the same DMX path a console uses. Press play and the rig runs without a desk on the network- Eighteen built-in looks — solid, chase, wave, pulse, rainbow, gradient, sparkle, strobe, off, comet, bounce, fire, wipe, alternate, plasma, twinkle, rain and the idle chase — set per fixture in the Details panel, keyed on a Sequencer track or called from a Blueprint, with no console and no DMX setup at all. Live DMX takes over from any of them. See Looks
- Real lenses. Every cell is lit and shaded as an optic: glossy domes on the battens and panels, phosphor on the blinders, a continuous frosted diffuser on the tubes, milky beads on the strings and hoops — and a powder-coat grain on every housing
- 3 maps, a patch sheet and full channel maps (this folder)
Where everything is:
Content/CueSyncPixel/
Blueprints/ the 34 fixtures and their base class
DMX/ the DMX Library, 34 .gdtf files, the demo rig .mvr, 34 imported GDTF assets
Docs/ documentation, patch sheet, channel maps, the licence and the changelog
Maps/ Lvl_Overview, Lvl_DemoStage, Lvl_Patch
Materials/ two master materials, 34 fixture instances, the stage floor and the truss
Meshes/ 35 static meshes
Sequences/ LS_CueSyncDemo
Textures/ the placeholder cell texture, the housings' powder-coat grain, and the PNG
each is imported from
Config/ renderer settings, the Art-Net and sACN input ports, and the loopback-only
output port the demo show plays through
About the pictures on the store page. They are renders of these Blueprints, and the truss, the
stage floor and the fog in the stage frames are the pack's own (SM_TrussSegment, MI_StageFloor,
CS_StageFog). Some of what they show is not in the download. The thumbnail and the hero frame are an
arrangement: the same Blueprints placed for the frame in a small studio set-up of lights, camera
and backdrop that is not included, so drag them into your own level and arrange your own;
Lvl_DemoStage is the stage you get, and it is a different, darker, truss-based rig. The contact sheet,
the looks sheets and the lens and diffusion close-ups are composed from single-fixture renders in the
same kind of set-up. The live-rig frame is Lvl_DemoStage taking a lighting cue sent from outside
Unreal by a script that is not included, and the clip is an authored cue on a camera move, not
LS_CueSyncDemo. No audience, venue or set appears in any picture beyond the truss and floor that ship.
The pictures were rendered on an earlier build (2026-09-24 to 09-28): since then the spot lights have
been given more candela and reach, so the pools they throw in the project are larger and brighter than
in the frames, and the truss is a lighter aluminium.
The maps
| Map | What it is |
|---|---|
Lvl_Overview | One of each of the 34 fixture types, patched and captioned, each running a look in the viewport (sixteen looks in rotation). The startup map, the quickest way to see what a fixture is before you use it, and the map the demo show runs on — press Play. |
Lvl_DemoStage | The 82-fixture demo rig on truss — the pack's own truss geometry, and the rig the .mvr describes. Open this one to look at a rig rather than a fixture. It opens lit: every zone carries a look (see Looks), in the viewport and in Play, and a console takes over any fixture it sends to. LS_CueSyncDemo addresses universes 40–49, which is the overview map; the demo rig is on 1–25. |
Lvl_Patch | The same 82 fixtures with the truss stripped out and an address caption floating on every one of them. Open this one when a cell is lighting and you need to know which universe and channel it is on. |
All three carry a dark floor (CS_StageFloor, a low-albedo instance of the pack's own body
material) and one very dim movable key light (CS_KeyLight, 0.02 lux).
The key light is dim on purpose. The housings are dark painted metal, and what lights them is the fixtures themselves: raising the key light 150 times, to 3 lux, barely changes the frame.
What you see on the floor is the fixtures' spot lights, not Lumen bouncing their faces. The emissive face is what a fixture looks like; its spot light is what it lights with.
Lvl_Overview and Lvl_DemoStage also carry CS_StageFog, an exponential height fog with
volumetric fog on. That is what gives a beam something to scatter off; without it the fixtures
light surfaces and nothing else. Lvl_Patch is deliberately left clear — haze in front of an
address caption is the one thing that would make that map worse.
Both run a density of 0.015. Density is the single largest control over how much of a frame is not black; turn it down if you are lighting a room rather than shooting a rig. It is one field on one actor, in the CueSync Stage folder.
All three actors sit in a CueSync Stage folder in the World Outliner, so they are easy to find and easy to delete if you would rather light the rig yourself.
Reading the fixture names
Most families number themselves by cell count — Pixel Bar 16 has 16 cells,
Pixel Strip 120 has 120, Floor Tile 64 has 64 in an 8 × 8 grid.
Two families do not.
The tube family's number is length in centimetres. Pixel Tube 50 / 100 / 200 are half-,
one- and two-metre tubes carrying 8, 16 and 32 cells — 62.5 mm pitch throughout, which is
the conventional spacing for a sixteen-cell one-metre tube.
The arc family's number is cell count, but its shape is a sweep of a circle rather than a
straight line: Pixel Arc 24 is 45 degrees of a 1.5 m radius, Pixel Arc 48 is 90 degrees and
Pixel Arc 96 is 180 — a half hoop — of the same radius, so all three share a 49 mm arc pitch
and butt together into a continuous hoop. The .gdtf describes the arc's own bounding box, not the circle it is a piece of, and
its origin sits on the arc rather than at the centre of curvature a radius away.
The video tile family's number is LED pitch in millimetres. LED Video Tile P2.6 / P3.9 / P6.9 are the same tile three times over — a 500 mm square, LED face and housing both,
which is the size the .gdtf states and the pitch the demo wall hangs them at — and all three
carry 16 DMX cells in a 4 × 4 grid. What changes is how fine the LED grid drawn behind those
cells is: 192 × 192 dots on the P2.6, 128 × 128 on the P3.9, 72 × 72 on the P6.9, across the
full 500 mm — a true pitch of 2.604, 3.906 and 6.944 mm, which the names round and the .gdtf
Pixel models state. Tiles butt edge to edge on 500 mm centres and the dot grid runs on across
the seam unbroken. That grid is cosmetic density, not resolution — all 36,864 dots on a P2.6
take their colour from just 16 addresses, the way a real wall processor patches a tile as zones.
In a visualiser, each of those 16 cells draws as its 125 mm zone. The fixture types are named
CS LED Video Tile P2_6, P3_9 and P6_9 in the .gdtf files and the DMX Library, because GDTF
does not allow a period in a name; the files are CS_LED_Video_Tile_P2_6.gdtf and so on, the
LongName keeps the label with its period, and the Blueprints and meshes are still ..._P26.
The three families 1.2.0 adds number themselves by cell count like the rest.
Pixel Beam Bar 10 is a one-metre batten of ten cells behind 4 degree optics. Its .gdtf
describes ten narrow beams. In Unreal it throws one: like every
fixture in the pack it carries a single spot light, driven by the average of its cells, so you get
one narrow 4 degree beam from the centre of the bar in the cells' average colour, and the ten
cells on its face. Pixel Neon 20 and Pixel Neon 40 are one and two metres of front-emitting neon
flex on a 50 mm segment, one continuous diffused face like the tubes. Pixel Strobe 24 is a
one-metre strobe-wash panel, 12 × 2 cells behind wide optics. Its shutter channel is in the .gdtf,
ranged for strobing, but in Unreal the shutter is addressed and not acted on (see Modes):
the panel strobes in the render when the desk flashes its cells, or with the built-in Strobe look.
All three are RGB, like everything else here: a white or amber emitter would be a channel the
render cannot follow.
Each fixture's GDTF description states its cell count, and so does
ChannelMaps.html.
Driving it
With a console. Point Art-Net or sACN (ANSI E1.31) at the machine. The project ships an input port for each, both covering universes 1-179 with the same numbering — a patch on universe 5 receives Art-Net universe 5 and sACN universe 5 — so the rig responds the moment you start sending. Both listen on every network interface, because desks are conventionally on 2.x.x.x or 10.x.x.x and laptops on 192.168.x.x, and guessing wrong is a rig that never lights. To pin a port to one NIC, set its device address in Project Settings → Plugins → DMX.
Art-Net arrives on UDP 6454, broadcast or unicast. sACN arrives on UDP 5568 and is multicast: the sACN port joins one group per universe it covers, 239.255.0.1 to 239.255.0.179, on the interface the operating system routes multicast through. If your lighting network is on a second network card and sACN does not arrive, set the sACN port's Network Interface Card IP Address to that card in Project Settings → Plugins → DMX. A desk that unicasts sACN to this machine needs no group and arrives either way. Where several sACN sources send one universe, the one at the highest priority drives it, which is the standard's own rule.
Send one protocol. A desk sending the same universe as both Art-Net and sACN reaches the same patches through two ports, and sACN priority does not arbitrate between protocols: the two signals alternate frame by frame.
The pack sends no DMX onto your network. Joining the sACN groups makes the operating system announce its membership (IGMP), which is the only traffic the machine sends because of it.
With CueSync. CueSync is our separate show-control application: a DJ and theatre show controller that locks to DJ players and timecode, and it runs on macOS only. It drives this rig over ordinary Art-Net, so the lighting is on the same clock as the music rather than reacting to it. The exporter described below is useful only if you use it — this pack is fully functional from any Art-Net console without it. cuesync.live
Part of the CueSync line. CueSync for Unreal is the free in-engine plugin that drives a
rig from the music instead of from a desk — tag these fixtures CueSyncGroup:<Name> and its
fixture group driver chases their spot lights in label order once a key colour arrives; the
faces keep their look or their DMX, and a tagged group throws no light before the first key.
(Plugin 1.12.2 and earlier run that chase last label to first.) RigSync builds the truss, hangs the
fixtures, numbers them and patches them, and tags them as it goes. Neither is required to use
this pack. cuesync.live
Looks: driving it without a console
Most people who put LED fixtures in an Unreal scene do not own a lighting desk. Every fixture in the pack can run a look instead: a colour, a chase, a rainbow, a sparkle, animated by the material itself, with no DMX, no ports and nothing to patch. You set it on the fixture.
In the Details panel. Select a placed fixture, select its Body component, and open
Rendering → Custom Primitive Data Defaults. Each index there is one field of the table
below: set index 0 (LookMode) to 5 and the fixture runs a rainbow in the viewport straight
away; set indices 1-3 to a colour and a Solid or Chase takes it. It is saved with the level.
In Sequencer. Add the fixture to a sequence, add a track on its Body component, and choose
Custom Primitive Data. Every field below is a channel you can key: fade a look out with
LookFade, cut between looks on LookMode, sweep a colour.
From a Blueprint. Call Set Scalar Parameter for Custom Primitive Data (or its Vector twin) on the Body component with the field's name, or Set Custom Primitive Data Float with its index. Either works at runtime.
| Look | Mode | What it does | Its own speed and size |
|---|---|---|---|
| 0 | Idle chase | The pack's plain chase, in the instance's CellColor, at its DemoSpeed/DemoWidth/DemoPhase. The default: a fixture nobody has touched runs this | — |
| 1 | Solid | The whole fixture in colour A | — |
| 2 | Chase | A band of colour A travelling over colour B | 0.5 per second; band 0.2 of the fixture |
| 3 | Wave | A smooth sine of A over B, travelling | 0.5 per second; wavelength 0.5 of the fixture |
| 4 | Pulse | The whole fixture breathing between B and A | 0.5 per second |
| 5 | Rainbow | Every hue along the fixture, flowing. Ignores the colours | 0.1 per second; one full spectrum per fixture |
| 6 | Gradient | A at one end, B at the other; still unless you give it a speed | still; one span per fixture |
| 7 | Sparkle | Cells twinkling at random in A over B | 1 per second; LookSize sharpens the twinkle |
| 8 | Strobe | Flashes of A over B | 4 per second; LookSize is the fraction of each flash that is on (0.12) |
| 9 | Off | Black | — |
| 10 | Comet | A bright head of A with a tail fading to B behind it, travelling | 0.5 per second; tail 0.35 of the fixture |
| 11 | Bounce | A band of A sweeping to the end and back, easing at each end — a scanner | 0.25 per second (one sweep there and back in 4 s); band 0.15 of the fixture |
| 12 | Fire | Flicker from B through A to a yellow-white core; on a grid, flames rising up each column and hottest at the far end of the axis — the bottom, with LookAxis 2. A left unset is flame orange | 2 per second |
| 13 | Wipe | A fills the fixture from the start of the axis, then B clears it the same way | 0.25 per second; LookSize softens the edge (0.04) |
| 14 | Alternate | Odd and even cells — a checkerboard on a grid — swap A and B | 0.5 per second; LookSize is the crossfade, near 0 a hard swap, 1 a full dissolve (0.02) |
| 15 | Plasma | Four interfering waves flowing across the face: a rainbow by default, between B and A once A is set | 0.1 per second; LookSize scales the pattern |
| 16 | Twinkle | A random share of cells flash to A and decay to B, each on its own clock | 1.2 per second; LookSize is the share that flash each time (0.15) |
| 17 | Rain | A drop of A with a fading tail falls down every column of a grid, each at its own rate; a one-row fixture is one lane along the axis | 0.5 per second; tail 0.3 of the fixture |
| Field | Custom Primitive Data | What it is |
|---|---|---|
LookMode | 0 | Which look, 0-17 as above. A number past the last look is Off |
LookColorA | 1-4 | The main colour. Left black — unset — it is the instance's CellColor |
LookColorB | 5-8 | The second colour: the background of Chase, Wave, Pulse and Sparkle, the far end of Gradient. Black by default |
LookSpeed | 9 | Cycles per second. 0 is the look's own speed from the table above |
LookSize | 10 | Band width, wavelength, tail or span, as a fraction of the fixture; Sparkle's sharpness; Strobe's on-time; Wipe's edge; Alternate's crossfade; Plasma's scale; Twinkle's density. 0 is the look's own |
LookFade | 11 | 0 is full level, 1 is black. Key it 0 → 1 to fade a look out |
LookPhase | 12 | An offset in cycles, so fixtures running the same look need not move together |
LookAxis | 13 | What the look travels along: 0 the cell order (the DMX order), 1 across the face, 2 down it, 3 out from its centre |
The same fields appear on every MI_Pixel* instance, under 07 Look (Custom Primitive Data),
because the material declares them. Setting them there does nothing: a look belongs to a placed
fixture, on its Body component, so two fixtures sharing an instance can run different looks.
The layout is a contract: those indices will not move in a later version, and a new field will only ever take the next free one. Every default is 0, and 0 always means "as before" — the engine reads a field nobody set as 0, so an untouched fixture runs the idle chase exactly as it did before looks existed.
One show, not 82 fixtures. A look travels along its own fixture. To make a wave cross a whole
video wall rather than repeat inside every tile, give each tile a LookPhase equal to its place
on the wall and a LookSize equal to the wall's width in tiles. That is what
apply_demo_looks.py does for Lvl_DemoStage, and it is the file to read
for a worked example: a table of zones, each with a look, and three lines that turn a fixture's
position into its phase.
What a look does not do. It colours the fixture's face. The fixture's spot light — what
lights the floor and the haze — follows DMX, as it always has, so a look lights no surfaces. For
light on the set without a desk, key the fixture's Fixture SpotLight component (its colour and
intensity) in the same sequence; for face and light from one source, drive the fixture over DMX —
the pack's own LS_CueSyncDemo is a Sequencer show that does exactly that, with no desk.
Live DMX always wins. The moment cell data arrives on a fixture's patch, the fixture shows the desk and not its look; the look is what it runs until then. A look never changes what a console sees, and a console never needs to know a look is set.
Flashing
Nothing in this pack limits how fast a fixture flashes. The Strobe look runs four flashes a second
by default and LookSpeed raises it, DMX flashes at whatever rate a desk sends, and the shutter
channel in the .gdtf is ranged up to 25 flashes a second (Unreal does not act on it). Flashing
more than three times in any one second over a large part of the picture can trigger seizures in
people with photosensitive epilepsy. Do not show a strobing render, live or recorded, to an
audience that has not been warned, and check anything you publish before you do.
If nothing lights
Press Play (or Simulate) first, or run editor_live.py. The patches
receive in the editor viewport, because bAllFixturePatchesReceiveDMXInEditor ships on, but
the fixture reads its patch on Tick and an actor does not tick in the editor world.
editor_live.py (Tools → Execute Python Script; run it again to stop) drives every placed
fixture from an editor tick the way the Blueprint does in Play. Once the rig is in a Play
session, or editor_live.py is running, if it stays dark the problem is almost always upstream
of Unreal. In order:
- Is the input port's device address on the same subnet as the desk? The shipped ports
listen on
0.0.0.0, which covers every interface; a port pinned to one NIC does not. - Is a firewall holding inbound UDP 6454 (Art-Net) or 5568 (sACN)? Art-Net uses its port for both broadcast and unicast. If your desk unicasts to known nodes, confirm it is sending to this machine's address on the lighting NIC, and not only to the subnet broadcast. For sACN multicast on a machine with more than one network card, set the sACN port's Network Interface Card IP Address to the lighting card (see Driving it); unicast sACN arrives either way.
- Does the patch's universe fall inside the ports' range? The shipped ports cover 1-179. A patch above that range simply never receives, silently.
- Is the desk numbering Art-Net universes from 0? Art-Net's universe space starts at zero, and
the shipped port's extern window starts at 1 — so a desk sending on Art-Net universe 0
is not received at all, and one whose numbering is offset lights the wrong fixtures rather
than none. The port maps
LocalUniverse = ExternUniverse - (ExternUniverseStart - LocalUniverseStart); both are 1 here, so the two agree. If the whole rig lights but every fixture shows a neighbour's colours, this is the reason and not the patch — changeExternUniverseStartinConfig/DefaultEngine.ini, or in Project Settings under Plugins > DMX. sACN has no universe 0, so the sACN port's numbering always agrees. - Is the fixture in a mode Unreal renders?
SimpleandDimmercarry no cell data, so in the render the face keeps its look and the light stays black, by design — see Which modes drive the render. - Is the fixture patched
TopLeftToRight? Any other distribution still receives; the chase just runs in an order you did not ask for. See The one thing to get right.
If you migrated the content into a project of your own rather than opening this one, the input
ports and the renderer settings did not travel with it. SETUP.md is the page for
that.
The one thing to get right
Patch order is TopLeftToRight. Cells run left to right, then top to bottom, in one
unbroken run of RGB triples. Unreal offers sixteen distributions and changing it does not
error — the chase simply runs backwards, or snakes, or starts in the middle. Every fixture
in this pack is patched in the order above, and ChannelMaps.html gives
the exact channel of every cell so you can check it rather than trust it.
Modes
| Mode | Channels | What it is for | Lights up in Unreal |
|---|---|---|---|
Simple | 3 | The whole fixture as one RGB colour. | no — the face keeps its look, the light stays dark |
Dimmer | 5 | Dimmer, RGB, shutter — the fixture as a colour-changer. | no — as Simple |
Pixel N | N×3 | Every cell addressed, no header. The fixture's start channel is cell 1's red. | yes |
Pixel N + Master | N×3+3 | Dimmer, shutter and control in front of the cells, so the fixture answers an intensity master, a highlight and a blackout the way the rest of the rig does. Cells start at channel 4. | cells only — the master is ignored |
Pixel N Dim | N×4 | Every cell with its own dimmer after its RGB. | yes |
Pixel N Dim + Master | N×4+3 | The same, behind the three master channels. | cells only — the master is ignored |
Pixel N 16bit | N×6 | Every cell's red, green and blue at 16 bits. | yes, at 8 bits — the engine's cell texture is 8-bit |
Pixel N Dim 16bit | N×8 | 16-bit colour and a 16-bit dimmer, per cell. | yes, at 8 bits |
Pixel N 16bit + Master | N×6+3 | 16-bit colour behind the master dimmer, shutter and control — smooth fades that still answer a master and a blackout. New in 1.2.0. | cells only, at 8 bits — the master is ignored |
Pixel N Dim 16bit + Master | N×8+3 | 16-bit colour and dimmer behind the master header. The widest mode the pack offers. New in 1.2.0. | cells only, at 8 bits |
The first four modes keep the exact channel layout they have had since 1.0.0, and every build
is checked against wire_contract.json, which records it. A later version
can add modes; only a major version may move one. 1.2.0's two are appended after the eight
before them, so every existing mode keeps its place in the list — its RDM personality number and
its index in an Unreal patch included.
A mode has to fit a universe. An Unreal fixture patch cannot straddle one, so a mode wider
than 512 channels is not offered for that fixture rather than shipped as something the engine
will refuse to patch. Pixel N 16bit costs six channels a cell and Pixel N Dim 16bit eight,
and their + Master twins three more, so the 16-bit modes run past a universe first, the widest
first. Today twenty-six fixtures carry all ten modes. The other eight carry fewer: nine on
CS Pixel Floor Tile 64, which loses only Pixel 64 Dim 16bit + Master (515 channels); six on
CS Pixel Arc 96, CS Pixel Curtain 96, CS Pixel Dot String 100, CS Pixel Matrix 120 and
CS Pixel Strip 120, which keep no 16-bit mode; and four on CS Pixel Curtain 144 and
CS Pixel Strip 150, which keep Pixel N and Pixel N + Master beside Simple and Dimmer.
ChannelMaps.html is generated from the archives and lists exactly what
each fixture has; where this paragraph and that file disagree, that file is right.
Per-cell dimming reaches the render; per-cell strobe and a white emitter would not. The engine's matrix path gives each cell four values — three colour and one more — so a per-cell dimmer fits and anything else would have to displace it. Declaring a channel the picture cannot follow is the one thing this pack exists not to do, so those modes are absent rather than present and inert.
The shutter is ranged the way a real fixture's is — 0-31 closed, 32-63 open, 64-223 strobe
from 0.5 to 25 Hz, 224-255 random — not a single ramp. It powers on open: the shutter
channel declares an initial function of Open, so a fixture that has never been addressed is
not sitting dark waiting for a value nobody sent.
One naming difference to expect, because it looks like a mistake and is not. The third master
channel is Control1 in the .gdtf and in the channel maps, which is the GDTF attribute name.
The DMX Library shows it as Effects, because that is what Unreal calls the same attribute in
its own fixed list. They are the same channel; only the vocabulary changes at the import
boundary.
The masters. Each mode's dimmer is marked in the .gdtf as a grand-master target
(Master="Grand"), which is how GDTF says a grand master should reach it, and a mode with no
dimmer channel of its own carries a virtual one that takes no DMX slot. That is what the file
carries for a console's Grand Master, blackout and group masters to act on. What Unreal does with the dimmer channel of a + Master mode
is under Which modes drive the render.
Which modes drive the render
Every mode is complete in the .gdtf: patch any of them from the file. The channel offsets are
the ones the DMX Library, ChannelMaps.html and the patch sheet were generated from and compared
against; no console has imported these files yet. What the Unreal fixtures do with them is narrower,
and it is worth knowing before you patch.
BP_PixelFixture derives from Epic's DMXFixtureActorMatrix: a DMXComponent holds the
patch, and exactly one attribute component — a DMXFixtureComponentColor — consumes it. The
engine's matrix fixture only paints what arrives as cell data. Simple and Dimmer have no
cells — they are fixture-level colour — so in Unreal the fixture ignores the desk: the face
keeps running its look and the spot light stays black. They are there for the desk: patch
a tube in Simple when you want it in the rig as a plain colour-changer and do not need 32
cells of it in the visualiser.
Pixel N + Master renders its cells correctly; the dimmer, shutter and control channels in front
of them are addressed but not acted on, because the engine applies a matrix dimmer per cell and
this one is fixture-level. Measured: the master at 50% leaves the face and the light at full.
That includes the masters. The .gdtf marks a mode's Dimmer channel as a grand-master target,
and in a + Master mode Unreal does not act on that channel, so a master that drives it — a Grand
Master, a blackout, a group master — does not dim the fixture in the render. What Unreal does act
on is the cell data: the colour values, and the per-cell dimmer of the Pixel N Dim modes. Where
the picture has to dim, send the dimming as those, in Pixel N or Pixel N Dim. How a console's
own masters treat the colour values of a mode with no dimmer channel depends on the console: check
it on yours.
The two 16-bit modes are decoded at 16 bits on the patch and rendered at 8: the engine writes
the cell texture as bytes, and its colour component skips any cell whose values move by less
than 1% (SkipThreshold). They cost a console nothing and buy the render nothing.
And the light follows a different curve from the face, and a different colour. The face reads
the cell texture linearly, and the per-cell dimmer scales the light's intensity linearly, but
the light's colour goes through an 8-bit FColor that the renderer decodes as sRGB (Epic's
matrix fixture calls SetLightColor(average, false)): all cells at DMX 128 give a face at 50%
and a light at 22%. That is the engine's push and not this pack's. Two consequences follow, both
computed with the engine's own conversion rather than measured on a render. The colour is the
plain average of the cells' colours, so a fixture with half its cells white and half black throws
22% of its all-white light where an LED fixture would throw 50%. And each colour channel is
bent on its own, so a mixed colour moves toward its strongest channel: an amber cell of
(255, 128, 0) lights the haze and the floor as (255, 55, 0), and a pink comes out more saturated
than its face. Pure red, green, blue and their sums at full level are exact. The level reads as a
square-law dimmer curve on the spill, which is what most real LED fixtures default to; the
colour shift has no real counterpart, so judge a fixture's colour by its face.
So: use Pixel N when you want what Unreal shows to be exactly what the desk is sending.
Use the others when the console needs them and the render is not the point.
Photometrics: what the GDTF numbers are, and are not
Every .gdtf declares a <Beam> carrying LuminousFlux, PowerConsumption, BeamAngle,
FieldAngle, ColorRenderingIndex and ColorTemperature, plus a <Properties> block with
weight, operating temperature and a fixture-level power figure. Consoles, previs tools and
truss-load calculators read those fields, so they are filled in, and they are internally
consistent: the beam sits on the shared Pixel geometry and is instantiated once per cell, so
flux and power are per cell and the fixture total is that figure times the cell count.
CS Pixel Tube 100 declares 80 lm and 2 W per cell across 16 cells, and 32 W at the fixture.
These figures are representative of the class of fixture. They are not measured. No photometric file and no goniometer report stands behind any of them, and no manufacturer's data sheet does either, with one exception: the three Blinders' lumens are set so that a 60° beam lands on the candela of published data sheets of comparable blinders (no sheet names these products). The rest are chosen so that a blinder cell reads as brighter than a dot node, and so that a power estimate lands in the right order of magnitude in a visualiser — nothing more. Do not use them for a lux calculation, a photometric plot, or an electrical load schedule. Put the real fixture's numbers in before any of those matter.
Two consequences are worth stating rather than leaving to be discovered:
ColorRenderingIndexis 30, not the 90 a spec sheet might lead you to expect. These are RGB-only emitters with no white LED, so a CRI near 90 would be a fiction, and the 6500 K colour temperature is a nominal white point for the R+G+B mix rather than a measured CCT.FieldAngleis wider thanBeamAngleon every fixture, as it has to be — field angle is the 10% point and beam angle the 50% point.
<Properties> carries the same caution. Weight scales with the fixture's size within a family
— a 2 m tube weighs twice a 1 m one, a 100-node string four times a 25-node one — which through
1.1.4 it did not: all three tubes declared 2.4 kg. The fixture power figure is the per-cell figure
multiplied out rather than a measured draw.
Each emitter declares its chromaticity and the dominant wavelength that chromaticity has. The chromaticities are the sRGB primaries — the colour space Unreal renders the cells in — so a console mixing from them picks the colour the render shows, and the wavelengths follow from them: 611, 549 and 464 nm. Through 1.1.4 the files paired those primaries with 625, 525 and 465 nm, the wavelengths of a typical physical RGB LED, which no sRGB primary has.
What the revision history records
Each .gdtf carries a <Revisions> block, the file's own revision history. It has one entry for each release of the pack that changed the file — 1.0.0, 1.1.0, 1.1.4,
1.2.0 and 1.2.1 for the twenty original fixtures — and each names that version and says in a sentence
what changed. (The 1.2.1 entry records the model orientation only; the file names, the video tiles'
type names and the blinders' flux are in CHANGELOG.md.) A fixture added later starts at the release that added it, with an entry saying
so, rather than claiming a history from before it existed. Read it as
provenance: it lets a .gdtf that reaches a house library by some other route be traced back to
this pack and the version it came from. CHANGELOG.md is the file to
read if you are comparing a .gdtf you already have against the one in this download.
Inside the pixel material
The material has three parts, and they work differently. This matters the moment you want to
change how a fixture looks, or put MI_Pixel* on a mesh of your own. It is written by
build_materials.py — the graph is that file, so it is also the place to
read exactly what a cell computes.
The cell shape is procedural. M_CueSyncPixel tiles UV0 by Rows and Cols, shapes each
dot with FillFactor, and softens its edge with Diffusion. No artwork texture draws the LEDs,
which is what keeps the grid exact at any scale.
The cell colour is not. BP_PixelFixture derives from Epic's ADMXFixtureActorMatrix,
which allocates a small BGRA texture — one column per cell, two rows — writes the live DMX
values into it every tick, and binds it to the material's MatrixData texture parameter. The
material samples row 0 of that texture at (UV1.x, 0.25) — the 0.25 picks row 0, and the
U coordinate is UV1.x taken raw — and takes RGB as the cell colour and alpha as the per-cell
dimmer. T_PixelCellDefault in Textures/ is only the 2 × 2 placeholder the parameter holds
before a fixture runs; it is not what you see on a patched rig.
The colour component's fourth channel is bound to Dimmer, so a fixture patched in one of
the Pixel N Dim modes dims cell by cell. In the modes that carry no per-cell dimmer the engine
leaves that slot at 1 and every cell shows its colour at full.
MatrixDataAmount crossfades between live DMX and the fixture's
look. It is 0 while a fixture has no cell data, which is
why a fixture dropped into a level with no DMX runs its look — the idle chase, unless you chose
another — instead of sitting black, and 1 once cell data is arriving. The Blueprint sets it every
tick, so it is a runtime state rather than a per-fixture constant, and no material instance
overrides it.
The lens is lit. The material is a lit surface with an emissive cell on it, not an unlit sticker, so a cell that is off still shows what it is made of. See the next section.
The lens: what a dark cell shows
A real LED fixture that is off is not black. A batten shows a row of glossy optics that catch the room; a COB blinder shows yellow phosphor in a reflector; a pixel tube is a milky frosted diffuser; an LED wall is black packages in a black mask. Through 1.1.4's first cut every one of them rendered an unlit cell as a hole in the housing, which is the first thing that reads as "CG" in a close shot and the reason a blackout looked like an empty stage.
Each fixture's instance carries its family's lens, set by build_materials.py:
| Parameter | What it does |
|---|---|
LensColor, LensRoughness | The dot when it is dark: its albedo and how glossy it is. A clear optic is dark and glossy (0.05, 0.08); a bead or a diffuser is milky and satin (0.5, 0.35) |
FaceColor, FaceRoughness | The emitter surface between the dots — a black face on a panel, the frosted tube itself on a tube |
LensDome | Shades each dot as a shallow dome, so every lens carries its own highlight. The slope is zero at the dot's rim, so the lens meets the face without a bright ring, and it flattens out in a wide shot where a pixel could not resolve it anyway |
LensHotspot | Makes a lit dot brighter at its centre than at its rim, as an LED behind an optic reads. The dot's average is kept: the profile is divided by its own mean, so the fixture's overall level, every brightness figure in this README and the far-field prefilter are all unchanged |
CellBand | 1 draws each cell as a segment across the whole face instead of a round dot: the tubes, whose segments meet along one continuous diffuser. Near a joint a segment blends toward its neighbour's colour, so two segments of one colour meet with no seam and two of different colours meet in a soft gradient — which is what a frosted tube does to the light of the LEDs inside it. In band mode FillFactor is how much of the joint stays lit and Diffusion how wide the blend is |
| Family | Dark cell reads as |
|---|---|
| Bars, matrix panels | Glossy domed optics in a black face |
| Blinders | Yellow phosphor in the cup |
| Tubes | One frosted diffuser; band cells |
| Strips | White LED packages on a black PCB |
| Dot strings, rings, arcs, curtain | Milky beads (on the string's own bead, or on a black hoop or mesh) |
| Video tiles | Black packages in a matte black mask |
| Floor tile | A milky walkable top |
A lit cell shows its light, not its lens. As a cell lights, its albedo and its specular fade out across the whole cell, dot and face together, and are gone by a quarter of full. A real LED is so much brighter than anything its lens reflects that you never see the lens of a lit cell; this pack's emitters are calibrated to a dark stage's exposure, far closer to its lights than a real LED is, and without the fade a lit panel under a key light greyed out between its dots and a lit tube went white. The one place it shows is a close shot under a strong key: the face round lit cells stays black while the face round dark cells reads as satin paint.
With the lens neutral the output is the 1.1 material's, pixel for pixel. That was measured rather than assumed: the same eleven fixtures, driven by the same cell data and by a frozen idle chase, rendered under the old material and under this one with the lens set to black, rough, flat and even, differ by a mean of 0.003/255 — the 99th percentile of the difference is 0 — while the same comparison with the lens on differs on tens of thousands of pixels per fixture.
The housings are painted. M_CueSyncPixelBody carries a powder-coat grain — a fine tileable
height map, T_BodyGrain, applied in the mesh's own space on all three axes — which breaks up the
reflection the way a painted housing does. It follows the fixture, so a fixture on a moving truss
does not swim through its paint, and it follows the actor's scale, so a scaled mesh does not
coarsen it. GrainNormal, GrainScale and GrainRoughness are on the material; the truss and
the stage floor carry their own settings.
How live DMX reaches a cell
BP_PixelFixture takes the material Epic's ADMXFixtureActorMatrix binds its live cell
texture into — the texture has no UPROPERTY, so that material is the only way to reach it —
and puts it on Body slot 1, the emissive Pixels slot of the fixture's own mesh. From then
on the face and Epic's matrix data are the same material, and raising MatrixDataAmount on it
switches the face from its look to the live cells. Three things have to hold for that to
work, and verify_in_editor.py's check_bodies checks all three:
PixelMesh | The fixture's own mesh. The inherited construction script's only job is Body->SetStaticMesh(PixelMesh), so this variable is how a child gets its mesh onto Body. |
| Slot 1 | Has to be the Pixels slot, because the 1 in CreateDynamicMaterialInstance is a literal. All 34 fixture meshes carry ['Body', 'Pixels']. |
| One mesh component | A second component carrying the same mesh draws it twice in the same place. |
Two contracts to know before you reuse the material or the meshes.
UV1 carries the cell index; the lightmap is UV2. Each cell's geometry carries one constant
value in UV1.x: (cellIndex + 0.5) / cellCount. A mesh without that channel laid out that
way will light, but not cell by cell — and, more to the point, do not tick Generate Lightmap
UVs on a fixture mesh. On the shipped meshes its destination is UV2, where the authored
lightmap lives (one triangle per atlas cell; the engine's packer would replace it with charts
built from UV0, which overlap on these meshes). On a mesh you make yourself its default
destination is 1, the same channel as the cell index, so turning it on replaces the cell index
with an unwrap: the fixture stays lit, every cell reads whatever texel the unwrap happens to
point at, and per-cell colour is gone with nothing in the log to say why.
UV0 matters too, and in the opposite way: it places each emitter inside its own cell of the
dot grid. The invariant is one line — frac(centre × (Cols, Rows)) is (0.5, 0.5) on every
emitter — and one tiled unit is worth the cell's own pitch in centimetres, on both axes. The
first half puts the dot where the emitter is; the second makes it round rather than an ellipse,
because the mask is a circle in UV and arrives on the fixture stretched by whatever ratio those
two axes disagree by.
write_cell_uvs.py writes both halves and rewrites only the meshes that
fail them, and verify_in_editor.py checks the first on every run. Where
a fixture's emissive face is far from square against its cell pitch, UV0 cannot keep the dot
round without pushing an emitter out of its own dot, so the shader does it: M_CueSyncPixel's
CellAspect scalar — how much taller a cell is than it is wide — scales the mask's vertical
component, and the prefilter's with it. It is 1.0 by default and set on seven instances: 1.92 on
CS Pixel Bar 16, 1.63 on the three arcs and 0.51 on the three rings.
The meshes are all authored on the same axis. Every fixture's emissive face points along
-X, and the pivot is at the geometric centre — bounds are symmetric on Y and Z and offset
on X only, by the depth of the body sitting behind the emitter. So SM_PixelFloorTile_64 is a
500 mm panel authored standing up like the rest of them, and needs a pitch of -90 to lie
flat on a deck.
A cell is where the .gdtf says it is. Each emitter is centred on the cell position the
fixture file declares, to within the 2 mm it is pushed forward to clear the housing behind it —
on Unreal's axes. GDTF is right-handed and Unreal left-handed, so a cell the .gdtf puts at
(x, y, z) is drawn at (x, -y, z): cell 1, which the .gdtf puts at the fixture's +Y end
(+Y +Z corner on a grid), is at the mesh's -Y end (-Y +Z corner) — the left, as you
face a fixture whose -X faces you, which is where the .gdtf has it. Through 1.2.0 every mesh
copied the coordinates across unchanged and drew its fixture's mirror image, so a chase a console
ran left to right ran right to left in Unreal. This is what lets export_rig_to_cuesync.py
write out a pixel's position and have it mean the same thing as the pixel you can see, and
verify_in_editor.py checks it on every mesh and every placed fixture.
To stand a fixture where a rig file puts it, turn it half a turn about its own Z from the
orientation the rig's basis gives: a pack Blueprint emits down its own -X, and
pixel_pack_spec.py's bases carry the emission on +X. pixel_pack_spec.actor_basis does it
both ways, and it is how every fixture in Lvl_DemoStage stands against the .mvr.
The light the fixture throws
Separate from the emissive face, and worth knowing because it is inherited rather than authored.
BP_PixelFixture derives from Epic's ADMXFixtureActorMatrix, which builds two lights into
every fixture. Only one of them lights anything:
Fixture SpotLight | The fixture's real light. Cone, intensity, reach and source size all come from that fixture's own photometrics — see the table below. Shadows off. |
Fixture PointLight | Inert. Epic constructs it with bAffectsWorld false, so it emits nothing and never has. |
Four things follow from that, and none of them is obvious from the viewport.
The spot is one colour for the whole fixture. Every tick the engine averages all of a fixture's cells and pushes that single colour into the light. A bar running a rainbow chase therefore throws grey, and a tile showing content throws the mean of that content. The cells are per-cell; the light the cells cast is not. That is the engine's model and this pack does not change it — but do not size a room's look on the assumption that a pixel fixture spills pixels.
The average is quantised to eight bits. SetLightColor stores an FColor, so a fixture
whose cells average below 1/255 throws nothing at all — one cell of a 120-cell strip at a fifth
is under that floor. The face still lights; the spill does not.
Shadows are off, on both lights, by Epic's default. At 82 fixtures that is the right trade and it is stated here so it reads as a trade rather than a surprise.
A fixture nobody has addressed throws no light. LightColor on every fixture's spot is
black, which is the colour Epic's SetDefaultMatrixFixtureState writes on BeginPlay, so
the editor viewport and a Play session agree: no DMX, no light. It is set on the class default
rather than per instance, so it holds for the 198 placed fixtures across the three maps and for
every fixture you drag into a level of your own. The face is a different matter: with no DMX it
runs its look. So Lvl_DemoStage and Lvl_Patch open the
same way in the editor and in Play — every face running its look (a zone's look on the demo
stage, the idle chase on the patch map) and every spot light dark — until a console drives them.
What each fixture throws
Each fixture's spot light is sized from that fixture's own photometrics — the figures in
fixture_catalogue.py, which are also written into its .gdtf:
| Fixture | Lumens | Cone | LightIntensityMax | Reach | Source |
|---|---|---|---|---|---|
| Pixel Beam Bar 10 | 2,500 | 2° / 3.5° | 213,321 cd | 25.0 m | 5 x 90 cm capsule |
| Pixel Blinder 16 | 27,766 | 30° / 40° | 31,472 cd | 25.0 m | 35 cm sphere |
| Pixel Blinder 8 | 27,766 | 30° / 40° | 31,472 cd | 25.0 m | 18 x 36 cm capsule |
| Pixel Bar 16 | 6,400 | 15° / 22.5° | 25,914 cd | 25.0 m | 6 x 88 cm capsule |
| Pixel Bar 12 | 4,800 | 15° / 22.5° | 19,435 cd | 25.0 m | 6 x 88 cm capsule |
| Pixel Blinder 4 | 13,883 | 30° / 40° | 15,736 cd | 25.0 m | 18 cm sphere |
| Pixel Bar 8 | 3,200 | 15° / 22.5° | 12,957 cd | 25.0 m | 6 x 88 cm capsule |
| Pixel Strobe 24 | 19,200 | 45° / 60° | 10,046 cd | 25.0 m | 12 x 75 cm capsule |
| Pixel Arc 96 | 11,520 | 50° / 65° | 5,019 cd | 25.0 m | 80 x 150 cm capsule |
| Pixel Arc 48 | 5,760 | 50° / 65° | 2,509 cd | 21.2 m | 27 x 168 cm capsule |
| Pixel Arc 24 | 2,880 | 50° / 65° | 1,255 cd | 15.0 m | 11 x 103 cm capsule |
| Pixel Matrix 120 | 3,360 | 55° / 70° | 1,240 cd | 14.9 m | 25 x 10 cm capsule |
| Pixel Tube 200 | 2,560 | 60° / 75° | 813 cd | 12.1 m | 4 x 193 cm capsule |
| Pixel Ring 48 | 1,920 | 55° / 70° | 709 cd | 11.2 m | 120 cm sphere |
| Pixel Strip 150 | 1,800 | 55° / 70° | 664 cd | 10.9 m | 1 x 498 cm capsule |
| Pixel Curtain 144 | 2,880 | 75° / 85° | 630 cd | 10.6 m | 200 x 20 cm capsule |
| Pixel Matrix 60 | 1,680 | 55° / 70° | 620 cd | 10.5 m | 15 x 20 cm capsule |
| Pixel Strip 120 | 1,440 | 55° / 70° | 532 cd | 9.7 m | 1 x 398 cm capsule |
| Pixel Curtain 96 | 1,920 | 75° / 85° | 420 cd | 8.7 m | 140 x 120 cm capsule |
| Pixel Tube 100 | 1,280 | 60° / 75° | 407 cd | 8.5 m | 4 x 93 cm capsule |
| Pixel Ring 24 | 960 | 55° / 70° | 354 cd | 8.0 m | 60 cm sphere |
| Pixel Dot String 100 | 1,800 | 80° / 86° | 352 cd | 7.9 m | 1 x 1198 cm capsule |
| Pixel Strip 60 | 720 | 55° / 70° | 266 cd | 6.9 m | 1 x 198 cm capsule |
| LED Video Tile P2.6 | 784 | 60° / 80° | 244 cd | 6.6 m | 25 cm sphere |
| LED Video Tile P3.9 | 784 | 60° / 80° | 244 cd | 6.6 m | 25 cm sphere |
| LED Video Tile P6.9 | 752 | 60° / 80° | 234 cd | 6.5 m | 25 cm sphere |
| Pixel Neon 40 | 720 | 60° / 75° | 229 cd | 6.4 m | 1 x 199 cm capsule |
| Pixel Tube 50 | 640 | 60° / 75° | 203 cd | 6.0 m | 4 x 43 cm capsule |
| Pixel Ring 12 | 480 | 55° / 70° | 177 cd | 6.0 m | 30 cm sphere |
| Pixel Dot String 50 | 900 | 80° / 86° | 176 cd | 6.0 m | 1 x 598 cm capsule |
| Pixel Strip 30 | 360 | 55° / 70° | 133 cd | 6.0 m | 1 x 98 cm capsule |
| Pixel Floor Tile 64 | 390 | 60° / 80° | 122 cd | 6.0 m | 25 cm sphere |
| Pixel Neon 20 | 360 | 60° / 75° | 114 cd | 6.0 m | 1 x 99 cm capsule |
| Pixel Dot String 25 | 450 | 80° / 86° | 88 cd | 6.0 m | 1 x 298 cm capsule |
The cone is half the declared beam and field angles, because GDTF states full angles and Unreal
wants halves. LightIntensityMax is the fixture's peak intensity, its candela: the declared
output spread over its beam the way a photometric report spreads it — half the peak at the beam
angle, a tenth at the field angle, all the flux carried — and IntensityUnits is set to Candelas so
the engine reads that figure as written. Through the earlier 1.2.1 builds the output was spread
evenly over the field cone, which has no hot spot and so under-stated every real beam, by 1.14 times
on the dot strings and 1.94 times on the bars. The Beam Bar, whose 4° / 7° field no data
sheet anchors, keeps the even spread. The blinders' lumens are set so that a 60° beam lands on
the candela of three published data sheets of comparable blinders; nothing else here is anchored or
tuned. The rule is the standard photometric spread described above, and verify_pack.py (in the Tools
download) pins six of its values. That
is also why a 1 m bar outranks a 4 m curtain: 6,400 lumens through a 45° field is a harder beam
than 1,920 through 170°.
LightIntensityMax is the actor's figure; Intensity is the light component's, and the engine
sets one from the other by two different rules. Worth knowing because the two disagree and no
single number satisfies both.
| Path | What it does with LightIntensityMax |
|---|---|
| the matrix push, every DMX frame | SetIntensity(LightIntensityMax * MatrixAverageColor.A) — uses it raw |
FeedFixtureData, on any editor property change | SetIntensity(LightIntensityMax / 2π(1 − cos outer)) — divides by the field cone's solid angle, which the candela above has already divided out |
The runtime path is the one a console drives, so LightIntensityMax carries the candela and the
table above is what a driven fixture throws. On the fixtures the maps ship with, the component's
own Intensity is then written to the same candela, so the number in the details panel is the
number the photometrics state, and build_fixture_actors.py writes it last — anything
written after it makes the engine recompute it. A fixture you drag in or spawn yourself shows
the editor path's figure instead — FeedFixtureData's division, 40,636 cd on a fresh
BP_PixelBar_12 against its 19,435 — in the details panel and, until it is patched, at Play; a
patched fixture holds the candela at Play.
Reach is the distance at which the fixture falls to CUTOFF_LUX — 5.6 lux, which puts the
middle of the pack at about ten metres — clamped to 6–25 m. The floor stops a dim fixture
lighting only its own housing; the ceiling stops four blinders touching every cluster in the
level. Attenuation radius is a cutoff rather than physics, so that clamp is a performance
decision as much as a look one.
Source size is the capsule that fits the emissive face: a sphere of half the short side, swept along the difference, so a square face gives a sphere and no sweep. The sweep runs down the light's own local Z, so fixtures whose emitter is long in Y carry a 90° roll on the light to put the capsule along the fixture rather than across it. Rolling about the forward axis leaves the cone untouched — it is round.
Epic's per-cell beam is not used. ADMXFixtureActorMatrix can generate a raymarched
volumetric beam that samples the live cell data, on the MatrixHead procedural mesh component
it builds for itself. BP_PixelFixture hides MatrixHead on BeginPlay and draws its cells on
its own static meshes instead, so neither the beam nor Epic's copy of the cells ever renders.
Turning the beam on takes an edit to BP_PixelFixture's event graph rather than a property, and
what it buys is small for a raymarch per fixture: Epic sizes the beam volume at
min(sqrt(width x height) x 0.33, 50) centimetres — at most half a metre, a coloured glow at the
face rather than a beam through haze.
How bright a cell reads
Worth knowing before you judge the look, because it is a deliberate choice and it is not
subtle. All three maps run manual exposure — the post-process volume in each sets
AutoExposureMethod to Manual, AutoExposureBias to 1.0, and
AutoExposureApplyPhysicalCameraExposure off — so nothing adapts, a blackout stays black,
and the same DMX value is the same brightness in every shot, through any camera. Against that
fixed exposure, M_CueSyncPixel's Emissive parameter defaults to 8, and twenty-nine of
the thirty-four instances ship on it. The three tubes and the two neons ship at 2.5: their cells are
band segments that light the whole diffuser, about seven
times the face a round dot of the same cell lit, and at 8 every saturated colour on a tube blew
out to pastel.
At 8, the face keeps its colour up the fader. Measured on a 12-hue by 10-level card on
OV_Matrix_120 with the spot black, saturation at DMX 255 reads red 0.99, amber 0.86, blue
0.72, green 0.69, magenta 0.49 and cyan 0.27, and the face still responds up to DMX 128 for
white and green and 192 for red, amber and blue. BloomIntensity in each map's post-process
volume is 2.0, so a cell still reads as an emitter rather than a painted dot. The tonemapper
pushes every channel above 1.0 toward white together, so two-primary colours (cyan, magenta,
yellow) still desaturate in the top quarter of the fader. Those figures were measured on 1.1's
flat dot; LensHotspot now puts more of each dot's level at its centre (the average is
unchanged), so a dot's core reaches white a little lower on the fader than its rim does — the
look of an LED behind an optic, and the reason to set LensHotspot to 0 on an instance whose
colour you are matching by eye.
And one fixture family deliberately does not clip. The three video tiles read as a fine
grey grid rather than a wall of white, and that is the photometrics rather than a fault. A cell
is a dot of FillFactor diameter, so the panel average is Emissive times the dot's area
share, times the 0.82 that the off-axis falloff leaves on axis. On the tiles (0.30 fill, 0.05
diffusion) the dots cover 6.5% of the face, so the panel averages about 0.43 nits against an
emissive of 8, while a blinder's far larger dots average several times that and clip hard (the
table below has every family). That ordering is the declared output: a P2.6 tile is 784 lm across a 50 × 50 cm face,
about 3,100 lm/m², and a Blinder 16 is 27,766 lm across 70 × 70 cm, about 56,700 — eighteen
times more light per unit of face. A real LED wall is bright to stand in front of and dim beside
an audience blinder, and that is what these render as. If you want a tile that clips like a
blinder, raise Emissive on its instance; nothing else in the pack depends on that value.
Viewing angle. A cell's face does not have a viewing-angle setting. It dims toward grazing angles by
a fixed falloff in the material (1 − 0.55 × Fresnel, base reflectance 0.32, exponent 2.4), so
relative to straight on it keeps 0.83 at 70° off-axis, 0.71 at 80° and 0.57 at 89° — the material's own
arithmetic, not a render. Published specifications for indoor LED walls typically quote half the
on-axis luminance at the edge of a 140° to 160° viewing angle (70° to 80° either side), so these faces
dim less at wide angles than such a wall does, and nothing on an instance changes that.
If you want a face that clips harder, or one that holds its hue further up the fader,
Emissive on the fixture's material instance is the dial, and it is per-fixture: it moves the
clip point without touching addressing, cell layout or anything the console sees. It does not
turn the face into a linear fader — a value low enough to ramp smoothly all the way to 255 makes
the fixtures read as grey panels lying flat in the frame — and the further you lower it the more
BloomIntensity in the map's post-process volume has to carry. Every instance but the three
tubes and the two neons ships on the master default of 8, which is the value the numbers above
were measured at.
What Emissive is, in nits
Emissive is a look dial. It is not a luminance, and this pack does not claim a cell is as
bright as the LED it models; this section says how far apart the two are, so you can move the dial
knowing what you are moving. The figures below are the engine's own arithmetic, not a render:
nothing here was photographed, and verify_in_editor.py reads the three maps and the 34 instances
back to hold the table to them.
The unit. With the project settings this pack asks for (ExtendDefaultLuminanceRange on,
see SETUP.md), one unit of emissive — or of light returned by a surface — is one nit (cd/m²).
Epic's own description of r.EyeAdaptation.LensAttenuation says it: "1.0cd/m^2 becomes 1.0 at
EV100". A spot of C candela lights a surface d metres from it with C/d² lux, and a diffuse
surface of albedo ρ returns ρ·E/π nits.
The exposure. Every map's post-process volume is manual, bias 1.0, physical camera off, so the exposure scale is exactly 2 and white is 0.5 nit. A cell at an emissive of 8 is therefore sixteen times white, which is why it clips, and why the paragraphs above are about how long a saturated colour survives.
The table. Each family's face against the two numbers it can be set beside. Declared is
the luminance the fixture's own lumens imply — lumens over π times the area of one cell's
emitter (the lens or dot of the GDTF's pixel size; for a video tile, its 125 mm zone of LEDs).
Face averages is the whole cell's average on axis at the Emissive it ships on. Grey wall is
what a wall of albedo 0.5 five metres in front of the fixture's own spot, on its axis, returns. The
lumens and candela behind the declared and wall columns are this pack's representative
photometrics, not measurements of any product (see Photometrics): the table compares the pack
with itself.
| Family | Variants | Declared by its own lumens, nits | Face averages, nits | Grey wall 5 m from its own spot, nits |
|---|---|---|---|---|
| LED Video Tile | 3 | 957–998 | 0.43 | 1.5–1.6 |
| Pixel Bar | 3 | 54 k | 1.2 | 82–165 |
| Pixel Blinder | 3 | 36 k–72 k | 2.8 | 100–200 |
| Pixel Dot String | 3 | 37 k | 0.04 | 0.56–2.2 |
| Pixel Floor Tile | 1 | 1.9 k | 0.75 | 0.77 |
| Pixel Ring | 3 | 14 k | 0.14 | 1.1–4.5 |
| Pixel Strip | 4 | 49 k | 0.3 | 0.85–4.2 |
| Pixel Tube | 3 | 48 k | 2 | 1.3–5.2 |
| Pixel Matrix | 2 | 13 k | 1.7 | 3.9–7.9 |
| Pixel Arc | 3 | 42 k | 0.44 | 8–32 |
| Pixel Curtain | 2 | 25 k | 0.11 | 2.7–4 |
| Pixel Beam Bar | 1 | 28 k | 1.4 | 1.4 k |
| Pixel Neon | 2 | 51 k | 2 | 0.73–1.5 |
| Pixel Strobe | 1 | 66 k | 2 | 64 |
31 of the 34 fixtures have a face that averages less than the grey wall 5 m in front of their own spot.
What it means:
- The lights are photometric and the faces are not. The spot's candela comes from the lumens
the
.gdtfdeclares; the face'sEmissivedoes not. The maps are exposed so that the two sit together. In a level with another exposure — an auto-exposure camera, a brighter key light, a physical camera — they will not stay in step: changeEmissiveon the instance, not the light. - A real LED face is far hotter than any of these. The declared column is thousands to tens of thousands of nits for a lens or a dot. Published specifications for rental video tiles give 1,000 to 1,500 nits across the whole face; the Video Tile here declares 998 and its dots average 0.43. Against a white point of half a nit that average still reads at 86% of white, and a dot's centre at thirteen times white, which is why a distant tile is a bright flat panel and a close one a fine grid.
Tuning the look
| Parameter | What it does | Across the 34 instances |
|---|---|---|
Rows, Cols | The cell grid the dot shape is tiled into. | 2 × 2 on the small blinder up to 192 × 192 on the P2.6 tile |
FillFactor | Dot diameter as a fraction of the cell. On a band cell, how much of the joint between segments stays lit. | 0.12 on the dot strings, 0.78 on the blinders, 0.96 on the tubes and the neon |
Diffusion | Softness of the dot edge — a look control, not an anti-aliasing dial; the mask prefilters itself. | 0.05 on the video tiles, 0.9 on the floor tile |
CellColor | The colour the idle chase runs in, and a look's colour A when none is set. | white on all thirty-four |
FillBlend | Blends FillFactor toward a filled cell. | master default; no instance overrides it |
Emissive | Per-cell emissive multiplier, in the engine's scene units (one unit is one nit at this pack's exposure, whose white point is 0.5 nit), and the one dial for how hard a cell clips. It is a look dial, not a luminance control: there is no "set this wall to N nits" — see What Emissive is, in nits and How bright a cell reads. | master default of 8; 2.5 on the three tubes and the two neons |
CellBand | 0 round dots, 1 band segments that meet along a continuous diffuser. | 1 on the tubes and the neon, 0 elsewhere |
LensColor, LensRoughness, FaceColor, FaceRoughness | What a dark cell and the face around it look like — see The lens. | per family |
LensDome, LensHotspot | The dot's dome highlight, and how much brighter its centre is than its rim (average kept). | 0.7 / 0.45 on the bars, 0 / 0 on the video tiles; per family |
DemoSpeed, DemoPhase, DemoWidth | Shape the idle chase (look 0) an unpatched fixture runs. DemoWidth sets how much of the fixture the chase covers, not how much of a cell lights — the chase is snapped to whole cells, so a cell is wholly lit or wholly dark. Push it well past 1 to hold every cell on. | master defaults |
MatrixData, MatrixDataAmount | Engine-driven, every tick. Overriding either on an instance changes the editor preview and nothing else. | not overridden |
The parameters are grouped and carry tooltips in the instance editor, so the list above is
also what you see when you open one of the MI_Pixel* assets.
Shoot a cell grid square-on. The blinders and the floor tile carry their cells in modelled
lens wells — recessed, like the real fixtures. Seen off-axis at close range, the near wall of a
well crosses in front of its own disc and that cell renders as a torn crescent. It looks like a
shader fault and it is not one: head on, CS Pixel Blinder 16 renders sixteen complete discs at
1920 × 1080. It is only visible on the families with wells, only close up, and only off-axis —
at rig distance the wells are the depth cue they exist to be. If you are shooting a product
close-up of one of those fixtures, put the camera square to the face.
The demo show
Content/CueSyncPixel/Sequences/LS_CueSyncDemo is a LevelSequence carrying a DMX Library
track — Epic's own Sequencer track for driving fixture patches — with every one of the
thirty-four overview patches on it. Open Lvl_Overview, open the sequence, press play: a chase
runs along every fixture, a hue sweep rotates through them, five hits strobe the rig and it
fades out through a deep blue. Thirty seconds at 30 fps, 193,158 keyframes across 4,599
channels.
It matters that this is a DMX track and not a material effect. The keys are written into
the fixture patches; the patches feed the fixtures' matrix cells; the cells light. Nothing in
it touches M_CueSyncPixel, and nothing in it can make a fixture do something a desk could
not — which is the same reason the pack refuses to ship a mode the render cannot follow. It is
also a worked example of the thing buyers most often have to build for themselves: Sequencer
driving DMX.
Scrub it, drag a key, delete a section — it is an ordinary sequence.
build_demo_sequence.py regenerates it, and is where the four looks
are written down as arithmetic rather than as keyframes.
The show and a console are alternatives, not partners. While the sequence is playing it is writing to the same patches a desk writes to, and every tick the patch takes whichever signal is newest — two live sources alternate frame by frame rather than one winning.
The project ships one output port for this, and only this. Sequencer's DMX track writes to
output ports and nothing else, so without one the show evaluates and lights nothing. It is a
unicast port aimed at 127.0.0.1, and Send DMX by default stays off — so the pack
never transmits on your network: with send off the port opens no sender at all, and if you turn
send on, the only destination is the machine itself. SETUP.md has the settings
if you are migrating the content into a project of your own.
Performance
The demo rig is 82 fixtures and 3,240 addressable cells across
25 universes. Fixture meshes are lightweight by design — 29,444 triangles for all 35
meshes together (each counted once), the largest being the 100-dot string at 4,812 — and every
fixture is one static mesh with two material slots. A map instances them: by the editor's own count
of the placed meshes (LOD0, 2026-10-03), Lvl_DemoStage draws about 89,000 triangles with its truss and
floor, Lvl_Patch about 74,000 and Lvl_Overview about 29,000. Cost still scales with fixture count, though: each
fixture creates its own dynamic material instance to carry its cell data, so the triangle
counts above are the argument here and instancing is not.
There are no LODs, because at these counts there is nothing to gain by dropping detail.
What it actually costs, measured. Measured 2026-09-08 at 1920 × 1080 on an Apple-silicon Mac, Lumen and virtual shadow maps on, nothing in the level but the pack, a warm cache, and three interleaved repeats of every row:
| Map | Fixtures | Fog | Lights | ms/frame | fps |
|---|---|---|---|---|---|
Lvl_DemoStage | 82 | yes | black | 17.8 – 18.1 | 55 – 56 |
Lvl_DemoStage, every channel at full over Art-Net | 82 | yes | driven | 20.7 – 20.9 | 48 |
Lvl_Patch | 82 | no | black | 15.9 – 16.3 | 61 – 63 |
Lvl_Overview, show playing | 25 | yes | driven | 18.5 – 18.6 | 54 |
The first and third rows have no DMX arriving, and a spot light nobody has addressed is black —
which the renderer skips entirely, so they say nothing about what lit candela spots cost in fog
under Lumen. The second row is the one to size a show by: all 82 spots at LightIntensityMax
and every cell white, fed from outside the engine by an Art-Net sender on the loopback (25
universes, 512 channels each at 255, thirty frames a second). It costs about 3 ms over the
same stage idle. The fourth row is the shipped show driving the overview rig, and it costs about
half a millisecond more than the larger stage idle.
Ranges, not single figures, because the spread across repeats of an identical configuration is comparable to the differences between configurations. Read the rows together and they say the useful thing: 82 fixtures and 25 fixtures (the overview had 25 when this was measured; it has 34 now) cost about the same, the haze is about 2 ms, and driving every light on the stage is about 3 ms. The fixtures are not the expensive part.
Those figures were taken on the 1.1.3 material, before the lit lens (1.1.4) and before the spot lights were given their photometric candela and reach (1.2.1), which lights a larger part of the stage per spot. They have not been repeated on 1.2.1, so treat the table as the ordering it shows rather than as today's milliseconds, and size a show on your own machine.
The lit lens and the looks cost nothing measurable. Measured 2026-09-24 on Lvl_DemoStage
with nothing sending, 1920 × 1080, 600 frames, three interleaved pairs of the 1.1 material and
this one on a machine that was busy with other work: 14.4 – 16.2 ms/frame before, 14.8 – 16.4
after (with every zone now running a look). The difference is inside the spread between repeats
of the same build. Those runs are not comparable with the table above — the machine was loaded —
only with each other, which is why they were interleaved.
Your first run will be several times slower, and it is not the rig. One cold run of
Lvl_DemoStage measured 102.6 ms/frame — five times the warm figure. That run paid derived-data
and pipeline-state cache warming and a twelve-second texture-pool stall, none of which recurs.
It is worth stating because it is exactly the number a buyer would otherwise quote back: open a
map twice before you judge it.
To measure your own machine, run a map with
-game -BENCHMARK -BENCHMARKSECONDS=90 -FPS=10 -ForceRes, which renders a fixed 900 frames as
fast as the machine can, and divide by the wall time between LoadMap Load map complete and
RequestExit in the log file. On -stdout, which carries only Display-level lines, bracket the
run with Engine is initialized and PreExit Game instead. Throw the first run away. Without -ForceRes, a -windowed run is quietly
clamped to the size of your screen.
Dense fixtures at distance
Because no artwork draws the cells, there is no mip chain to fall back on. A dot grid with no mip chain is a signal a wide shot cannot sample: the P2.6 draws 192 × 192 dots across its 500 mm face, and at twenty metres a single screen pixel covers something like eight cells in each direction. One sample of eight cells is not a dim tile, it is a random one, and it changes every frame — which is what crawling is.
The material prefilters the cell edge analytically, so this converges rather than crawls.
M_CueSyncPixel takes screen-space derivatives of the cell grid to work out how wide a screen
pixel is measured in cells, widens the dot's soft edge to at least that, and fades the whole
mask toward the grid's exact average — π r², the fraction of a cell a dot of radius r covers —
as a pixel grows past one cell. A tile that is too far away to resolve therefore reads as its
own true average brightness instead of as noise.
Nothing about this changes the close-up look, and that is a property of how it is built: the widened edge is the wider of the authored edge and one pixel, and the fade is fully on the authored side until a pixel covers a whole cell. Where a pixel covers a cell or less — every close shot — the shader returns exactly the authored dot. The prefilter only engages where a single sample would otherwise be a guess.
Diffusion on the fixture's material instance is therefore a look control rather than a
fix. It widens the soft edge of each dot; the video tiles ship at 0.05 for a hard, sharp LED,
and the floor tile at 0.9 and the dot strings at 0.6 show the far end of the range. Reach for
it because you want a softer LED, not because something is shimmering.
Offline rendering: Movie Render Queue is the exception
Everything above is true in realtime and none of it holds under Movie Render Queue. MRQ
replaces TSR's resolver with its own sub-frame accumulation, so the temporal history the
prefilter is counting on is simply not there. At the shipped Diffusion of 0.05 a video tile
then aliases into moiré — not shimmer, which is temporal, but standing interference patterns
inside a single 4K frame at eight spatial samples, at close range as well as far.
Spatial samples do not substitute for it: they supersample within one frame and never reach the resolver. Raising the temporal sample count helps a little and does not fix it.
Under MRQ, Diffusion IS the fix. Set it to about 0.35 on the video tiles, the matrices and
the curtain before rendering, and the cells resolve cleanly. The shipped 0.05 is the right
default for realtime and for a viewport, and it is the wrong one for an offline render — so if
you are making a promo, a turntable or any pre-rendered marketing frame, change it and change it
back afterwards.
This is stated here rather than fixed by changing the default because 0.05 is the correct hard LED look everywhere else, and a pack that softened every tile to suit one workflow would be worse for the many buyers who never render offline at all.
There is no renderer setting that solves this. What the project does pin is
r.AntiAliasingMethod=4 — TSR, which is also the 5.8 default. Carry that one across if you
migrate the content into a project someone has set to FXAA or TAA, because TSR's temporal
accumulation is what lets a dense tile resolve at distance at all.
Requirements
Unreal Engine 5.8. The project enables DMXEngine, DMXProtocol, DMXFixtures, DMXGDTF,
DatasmithMVR and PythonScriptPlugin; the first four are required. DatasmithMVR is only
needed to import the .mvr, and PythonScriptPlugin only to run the pack's scripts inside the
editor (editor_live.py, verify_in_editor.py, the CueSync exporter, all in the Tools download). No C++ — this is a
content pack and compiles nothing.
The pack is tested on macOS with UE 5.8. Nothing in it is platform-specific — there is no compiled code and no platform-gated setting anywhere in it — but Windows has not been tested yet.
Dynamic lighting only. The fixtures are emissive surfaces and everything they put on the
room is Lumen; the meshes carry an authored lightmap in UV2 (Lightmap Coordinate Index 2, for
Fab's rules) that nothing bakes, and Generate Lightmap UVs is off on all 35 of them because
the engine's packer would replace it — see
Inside the pixel material.
The pack does not support baked lighting, and does not
need it. If your own project bakes, treat these fixtures and the truss as dynamic-only and
leave your r.AllowStaticLighting alone — SETUP.md explains why that one line is
the dangerous one to copy.
Dropping the fixtures into an existing project? The look and the input ports live in this
project's Config/, which does not travel with migrated content — and neither do the loose
.gdtf files, the .mvr or this Docs/ folder. See SETUP.md for the settings to
copy across, and for the one line to check against your own project before you paste it.
Taking the rig into CueSync
This pack is patched from the same addresses CueSync drives, so the two can share one
rig rather than two descriptions of one. export_rig_to_cuesync.py (in the Tools download,
which unzips into this folder) reads the shipped .mvr and .gdtf files — or the live level, if you run it inside the
editor — and writes a rig file CueSync imports under
Settings → Pixels → Import rig: zones, positions, and all 82 demo-stage
patches expanded to their 3,240 individual pixels. The eight zones come across as zones,
because the .mvr carries them as MVR Classes with a per-fixture <Classing> rather than as a
naming convention.
# from the project root
python3 Content/CueSyncPixel/Docs/export_rig_to_cuesync.py
With no --out the rig and a human-readable manifest land in Saved/CueSyncExport/. --out
takes a directory if you would rather write them elsewhere. The script needs
pixel_pack_spec.py, which sits beside it.
It refuses rather than truncates: overlapping channels, a start channel past 510 or a universe out of range are reported instead of silently dropped.
Checking the pack
This pack describes the same rig five times over — in the .gdtf files, the .mvr, the patch
sheet, the channel maps and the DMX Library. Five descriptions of one thing drift, and when they
do it is silent: a console accepts a wrong address as readily as a right one. Two scripts compare
them so you do not have to; they are in the Tools download (see the table at the top of this
folder), and run from the project root — the directory holding
CueSyncPixel.uproject. On Windows, spell python3 as py -3.
# from the project root
python3 Content/CueSyncPixel/Docs/verify_pack.py
Reads the GDTFs, the MVR, PatchSheet.csv,
PatchSheet.html, ChannelMaps.html and the imported
.uasset files, and fails if any two disagree. It also checks that every shipped asset the rig
needs is present — the fixture Blueprints, meshes and material instances, the maps, the demo
show, the DMX Library — and that all 116 patches fit inside channels 1-512 with no two sharing a
channel. Exit code 0 means consistent.
What that script cannot see is inside a .uasset, because a .uasset is opaque from outside
the editor. verify_in_editor.py covers the three places that matters:
- The DMX Library. Every fixture type's modes walked through a live patch, and the
Library's channel layout compared cell by cell against the
.gdtfon disk. - The meshes. The cell index in UV1, the dot grid in UV0, and the authored lightmap in UV2 (one triangle per cell, nothing overlapping, the packer off) on all 35 meshes.
- The light. Every blueprint's inherited spot light held against
fixture_catalogue.py— units,LightIntensityMax, the component's ownIntensity, reach, both cone angles, the source capsule, the rotation, and that the no-DMX colour is black. Eight values, each one mutation-tested.
It writes its result to Saved/VerifyInEditor.txt, whose last line is the durable gate, exits
non-zero on a disagreement, and changes nothing.
The simplest way to run it is from an open editor: Tools → Execute Python Script, or the
Python prompt in the Output Log. To run it headless, invoke the engine binary by its full path —
UnrealEditor-Cmd is not on PATH on any platform, and the engine changes its own working
directory during startup, so every path in the command has to be absolute. The engine lives
wherever the Epic Games Launcher installed UE 5.8. Only the macOS command has been run; the Windows
and Linux ones are the same invocation with that platform's engine path and have not been tried.
# macOS
"/Users/Shared/Epic Games/UE_5.8/Engine/Binaries/Mac/UnrealEditor-Cmd" "/path/to/CueSyncPixel/CueSyncPixel.uproject" -ExecutePythonScript="/path/to/CueSyncPixel/Content/CueSyncPixel/Docs/verify_in_editor.py" -unattended -nullrhi
:: Windows
"C:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" "C:\path\to\CueSyncPixel\CueSyncPixel.uproject" -ExecutePythonScript="C:\path\to\CueSyncPixel\Content\CueSyncPixel\Docs\verify_in_editor.py" -unattended -nullrhi
# Linux
"/opt/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd" "/path/to/CueSyncPixel/CueSyncPixel.uproject" -ExecutePythonScript="/path/to/CueSyncPixel/Content/CueSyncPixel/Docs/verify_in_editor.py" -unattended -nullrhi
-ExecutePythonScript, not -run=pythonscript -script=. The commandlet form still runs, but
Generate Lightmap UVs cannot be read from it: those accessors sit behind the Editor Scripting
Utilities' editor-only guard and return defaults to a commandlet, so the check would report every
mesh as broken whatever it holds. It is skipped there rather than guessed, and the script's own
last line says Generate Lightmap UVs NOT checked when that happens — nothing passes quietly.
Outside the pack. Every .gdtf validates against the official GDTF 1.2 schema (XSD) and the
.mvr validates against the MVR schema (gdtf.xsd and mvr.xsd from the MVR development
group's tools, run on the files as shipped in 1.2.1 on 2026-10-03: 34 of 34, and the one .mvr).
Earlier builds were also read by other tools, which have not been re-run on these files: pygdtf and
pymvr, open-source GDTF/MVR readers, read the 1.2.0 .gdtf files and resolved all 82 fixtures in the
.mvr to their mode, and libMVRgdtf, the reference GDTF/MVR SDK, read the 25 files 1.1.4 shipped.
The two scripts above hold the pack's own descriptions to one another; those tools are what hold
the files to the standard.
Edit a .gdtf and you must re-import it — the imported asset caches a copy, the editor does
not tell you it has gone stale, and verify_pack.py will fail until you do. The loose .gdtf
files live in Content/CueSyncPixel/DMX/GDTF/ in the download; if you migrated the content into
another project, they did not come with it, and the download is where to get them.
Licence, version and support
Version 1.2.1, for Unreal Engine 5.8. Config/DefaultGame.ini carries the same number as
ProjectVersion, and CHANGELOG.md records what changed and what a
version bump means for a fixture pack — in short, a major bump is a change to DMX addressing,
because that is the change that invalidates a show file.
Licensing is Fab's, not ours: your rights come from the tier you bought.
LICENSE.md sets out what that means for the .gdtf and .mvr files
in particular, since those are the parts of the pack most likely to leave Unreal, and carries
the third-party and trademark notices. Console, tool and hardware names in this document are
used for compatibility reference only.
Support is support@cuesync.live. Send the fixture, the mode and the address, and — if the
problem is a disagreement between the pack and your desk — the output of verify_pack.py.