Builtin Library
README.md
lensOptics
The render passes behind the LensFx component and the lensFx module.
Three compute passes after lighting.
lfx_bright reduces the frame to the parts bright enough to have an optical
response, at quarter resolution. The block each texel stands for is averaged
rather than point-sampled, so a single bright pixel still contributes. The
threshold has a soft knee below it: a hard cut makes a bright object's flare pop
on and off as it crosses the boundary, and the knee rolls the contribution in so
the response is continuous.
lfx_features builds the flare. A flare is not painted at the light — it is
what the lens does with the light. Each air-glass interface reflects a little of
the incoming cone back down the barrel, and those reflections land on the sensor
at positions mirrored through the optical axis. That is the whole geometry of the
effect: sampling the bright buffer at points reflected through the centre of the
frame. It is why the ghosts sweep across the frame as a light moves rather
than sitting on top of it, and why a source near the axis produces ghosts bunched
near the axis.
Each feature carries a share of its source, and that share is the bound on the
whole effect: a reflection returns a fraction of the cone that entered, so a
ghost lands about a hundredth of the light its source put in, the halo less
(the same reflection spread around a ring) and the streak less again (one
smeared along a whole axis). The bright buffer holds light, so a share of it is
light — a lamp at 20 flares ten times as hard as one at 2. What a feature lands
is its share times flareIntensity, so at the lens as built (intensity 1, the
largest share a hundredth) a source flares at a fraction of what it shines.
The chain marches from the mirrored source along that axis, each ghost a step further, fading as it approaches the frame edge where the reflected cone would be vignetted by the barrel. Each ghost pulls its three channels to slightly different radii, because glass disperses and a real ghost carries colour fringing rather than being a tinted copy. The halo is the widest of those reflections wrapping into a ring at a fixed radius, which is why a bright light near the edge throws an arc rather than another disc. The streak is a cylindrical element spreading a highlight along one axis, gathered at each pixel's own position because it belongs to the source rather than to its reflections.
lfx_composite adds the flare back and lays down the grain. The flare is read
bilinearly out of the quarter-resolution buffer so its edges stay soft instead of
stepping in blocks of four pixels.
The dirt mask modulates the flare, not the scene: a clean lens shows the same
picture, just without the bloom-driven grime lighting up. It is a two-octave value
noise field, so it needs no texture asset and follows dirtScale continuously.
The grain is drawn against a clock the module advances, so it is a fresh field every frame. Grain size is in pixels because silver halide crystals are a fixed physical size and do not scale with what is in front of the lens. Its amplitude peaks in the midtones and falls away at both ends — shadows hold too little exposed silver to vary and highlights are saturated — which is why film grain reads as texture in skin and skies rather than as uniform noise over the frame.
Parameters arrive through the lens_fx_params buffer, packed by the lensFx
module.