Why rendering gobos in a browser is harder than it looks
A gobo looks simple: pick a wheel slot, load its image and put it into the beam. Then surface projection, volumetric light, filtering and real GDTF data get involved. This is what it took to make browser-based gobo rendering look believable.
A gobo looks simple.
Take a black-and-white image, put it into a light beam, project it onto a surface. Done.
That was approximately the plan.
It survived for about five minutes.
The first version technically rendered a gobo. Unfortunately, it rendered all of them. At the same time.
What a gobo actually arrives as
In a GDTF fixture, a gobo is not just an arbitrary image attached to a lamp. The fixture definition describes wheels and wheel slots, while DMX channel functions and channel sets determine which slot is selected.
The wheel-slot artwork is stored as a PNG with an alpha channel. That sounds pleasantly concrete, and the GDTF wheel-slot image definition is indeed refreshingly specific about it.
But the renderer still has to connect several separate pieces of information:
Fixture state — which gobo attribute is active and which DMX range selected it.
Wheel and slot — which resource belongs to the selected position.
Optics — where the light originates, its beam angle, zoom, focus and aperture.
Geometry — which surfaces receive the projection and which objects block it.
Presentation — how the pattern remains readable in both the air and on a surface.
If GDTF itself is still the mysterious file inside an MVR archive, the longer introduction is in What is GDTF — and why do fixture profiles need a standard?.
The deceptively simple first approach
The obvious browser implementation is to load the wheel images into a texture atlas, select one tile and sample it while drawing the beam.
That is not a bad starting point. Texture atlases avoid binding a new texture for every fixture and make it practical to draw many beams in one scene.
It also creates a very efficient way to display sixteen gobos when the user selected one.
The application state was correct. The control panel showed the expected wheel slot. The shader, however, sampled beyond the selected tile and happily included neighbouring slots from the atlas. The bug did not look like a broken index. It looked like somebody had emptied the whole gobo case into one projector.
Constraining the sample to the selected tile fixed the obvious problem. As graphics programming tradition demands, it exposed the next one.
A gobo has to work in two different places
There are two related but different visual results.
The surface projection is the pattern landing on scenery, a floor or a wall. It needs a stable projector transform, sensible clipping, depth testing and filtering that keeps the image recognisable across distance and angle.
The volumetric beam is the pattern suggested inside the visible cone of light. It is not a solid object and it is not a flat decal. The renderer samples the beam volume along the view ray, accounts for its shape and stops light when scene depth says a wall is in the way.
A gobo can therefore look acceptable on a wall while looking terrible in the air. Fixing one does not automatically fix the other, because they are sampled in different spaces and at very different scales.
This matters in a stage-lighting visualizer. A sharp projection attached to a noisy, disconnected beam does not read as one light. It reads as two rendering techniques that happened to meet on the same wall.
A beam is not just a cone with a texture
The early mental model is usually a cone: one point at the fixture, one large circle at the far end.
Real optics are less accommodating. The visible beam starts at an aperture with an area, then expands according to the beam angle. A frustum is a better working shape than a mathematically perfect cone, especially close to the fixture.
That distinction becomes visible with gobos. If the pattern collapses into a single point at the origin, the volume can pinch, shimmer or become implausibly dense. Giving the beam a finite aperture makes its near field behave more like light leaving an optical system and less like a Three.js demo escaping from a vertex.
This is still an approximation. FlexViz is a planning and previsualization tool, not an optical bench. The approximation earns its keep when it preserves the cues a lighting designer actually reads.
Thin beams, fine patterns and crispy pixels
Once the selected slot and beam shape were correct, long and narrow beams revealed visible grain. Sampling that looked fine in a broad wash became obvious when stretched over a thin volume.
Increasing everything until the GPU stops smiling is technically a strategy. It is not a particularly useful browser strategy.
The better fix was to make sampling follow the beam more intelligently and reduce noise where it was most visible. That solved the general case.
Then two gobos remained.
Their patterns were fine enough to expose another sampling problem. Most wheel slots looked correct, but these two arrived on the wall with edges that were less “engraved metal” and more “crispy JPEG from 1998”.
This is where texture atlases, filtering and mipmaps stop being implementation trivia. A renderer may need padding between atlas tiles so filtering does not borrow pixels from the neighbour. It needs suitable lower-resolution levels for distant projections. It also has to avoid smoothing a detailed pattern until its defining lines disappear.
A particularly wide zoom combined with a very fine pattern is a useful stress test: the same image covers more pixels in the volume, fewer useful pixels on a distant surface and several different sampling conditions in between.
There is no single “make gobo sharp” switch. There is only a collection of trade-offs that must fail gracefully.
Physically plausible is not always visually useful
A renderer can produce a mathematically defensible result that still looks like broken stage light.
For FlexViz, the target is roughly physical and visually trustworthy. Beam angle should behave consistently. A narrower zoom should not suddenly look weaker than a wide one because the renderer forgot how energy is distributed. A wall should stop the beam because walls are not optional. Fine gobos should remain recognisable without turning every frame into an offline render.
That is not an excuse for arbitrary eye candy. It is a prioritisation rule.
If a lighting designer cannot tell whether the pattern, direction and coverage support the intended look, more physical vocabulary in the shader does not make the preview more useful.
What FlexViz does today
FlexViz reads gobo selection from the same fixture state used by its control and DMX paths, resolves the corresponding GDTF wheel slot and uses that image for both projected and volumetric light.
The renderer now handles the normal test scene convincingly, including long beams, narrow zooms and the wheel-slot selection that originally displayed everything at once.
There are still pathological patterns and fixture files worth testing. There always will be. GDTF standardises how the data is described; it does not guarantee that every image, optical setup and exported fixture profile is equally friendly to a real-time browser renderer.
That is the less glamorous part of building lighting previsualization: the feature is not finished when a gobo appears. It is finished when the wrong gobos do not appear, the right one survives the trip through the beam, and the result remains useful on an ordinary machine.
Which is a fairly good summary of graphics programming.