Conversation
The sun lens flare decided its own visibility per fragment, from scratch, every frame: nine effectively binary depth taps in a 0.02 UV disk plus a single point-sampled texel of the sun for brightness, with a hard return when that texel was not HDR-bright. Cover the sun's centre pixel and the whole flare -- a streak across the frame, halo, ghosts, starburst -- went out in one frame and came back in the next. A camera moving behind fence posts or foliage turned that into a strobe, and the only smoothing on the CPU was a per-frame lerp of the screen-edge fade, which the depth test never passed through. Move the measurement into a 2x1 state pass (generateLensFlareState, lensFlareStateF.glsl) that runs right before colorCorrect against the frame's final depth and ping-pongs like the exposure map. It probes 48 taps on a golden-angle Fermat spiral, aspect-corrected and centre weighted, sized from the sun disc's own angular radius through the current FOV, and takes the sun colour from the unoccluded taps only. The result is filtered in luminance with RGB following: asymmetric time constants, a slew cap relative to a decaying reference luminance so no frame can move the drive by more than a bounded fraction of the sun's recent brightness, and adaptive damping that slows the filter when the raw target keeps reversing direction. One reversal (an ordinary reveal) costs nothing; a fence-post train settles to the mean coverage within a few crossings. computeLensFlare now reads one texel of that texture and no longer touches depth or the scene buffer, which also removes ten texture reads per fragment for a value that was constant across the frame. Every constant comes from scripts/content_tools/check_lens_flare_state.py, which mirrors the shader statement for statement and replays edges, poles and square-wave occlusion at 30/60/144 fps: a 3 Hz strobe settles to a 0.07 swing, a clean step rises 10-90% in FadeTime, and the slew alone bounds the flare to 1.8 full cycles per second at the default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RenderLensFlareOcclusionTaps has nothing left to control: the state pass always uses all 48 taps, and at two fragments a frame they are free. RenderLensFlareOcclusionRadius was a fixed fraction of the screen, so zooming in shrank the probe relative to the sun and a thin post could cut the flare. It becomes RenderLensFlareOcclusionScale, a multiple of the sun disc's apparent radius, which follows zoom and the sky's sun scale. RenderLensFlareFadeTime is the one temporal knob: seconds for a full fade-in, with the fade-out and the rate limit derived from it. Both keys are declared with their ranges, swapped into the Looks whitelist and all three bundled Looks at their defaults, and take the two rows of the Lightbox Occlusion group at the same heights. The architecture note gains the state pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Size now runs 0.25 to 2.0 disc radii and Fade 0.1 to 1.0 seconds: the upper halves of the old ranges were beyond anything a Look would want, and a shorter slider puts the useful travel under the mouse. The clamps in generateLensFlareState, the setting comments, the three bundled Looks and the two Lightbox rows all move together. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fifteen review findings and one field report, in one pass because they touch the same forty lines. The identifier `packed` was a GLSL reserved word before 4.20: the state shader would not have compiled on macOS GL 4.1 or on Mesa, and the loader would have dropped the whole deferred set with it. The material-preview gate compared `src` against `mRT->screen` after the preview had already swapped `mRT` to the auxiliary pack, so it never fired and the world's flare was painted onto the preview sphere; it is now `mRT == &mMainRT`, and chromatic aberration on the preview is left as it was. The step detector behind the adaptive damping keyed on the per-frame delta, so a fence post crossing the probe over several frames never registered at high frame rates while the same scene was damped at 30 fps. It now measures displacement from an anchor (texel 1's blue channel) and normalises by the brightest unoccluded tap rather than by a reference that had decayed to the occluded level, which had made the reveal after a long partial occlusion three times slower. The sun behind alpha-masked pine needles flickered with the camera and the trees perfectly still. The per-frame golden-angle rotation of the tap pattern re-sampled the needle mask every frame, and the HDR gate on the mean colour of the unoccluded taps opened and closed at the threshold. The pattern is now fixed and has 256 taps (48 fixed taps read a half-plane with a worst bias of 0.14 and give one tap 15% of the weight; 256 read within 0.036 with 3.1%), and each tap is gated on its own: an occluded tap contributes nothing, an unoccluded one its overbright colour. A still scene now gives an identical estimate every frame; a drifting camera moves the filtered drive by half a percent per frame. Also: off-screen taps clamp to the frame edge for depth and colour, so the screen-edge margin works again and nothing leaks through an occluder at the edge, and the probe radius is capped; the drive snaps to exact zero below 1e-4 once the target is black, because a half-float target pins a geometric decay at a denormal and the reader's early-out needs true black; a no-post snapshot holds the history instead of clearing it; a sun/moon flip clears it; the probe carries the drawn disc's horizon enlargement and the tiled-snapshot zoom; the ping-pong is a handle swap and only the target read as history is cleared; colorCorrect no longer binds a depth texture nothing samples; the shader derives its rates from one FadeTime uniform and is the declared source of truth, with the script as its mirror and regression test, now covering ramped edges, partial occlusion under a held camera, and needles with a still and a drifting camera. Stale prose is trimmed to pointers and LIGHTBOX.md names all three clean-plate gates. Tested in world: no flicker in the needle scene or elsewhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The colour read in the lens flare state pass runs only for unoccluded taps, so it sits in non-uniform control flow, where implicit derivatives are undefined. The target has no mips and is sampled nearest, so every driver was returning the right texel anyway; textureLod makes that defined rather than incidental before the pass meets a stricter compiler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (15)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughThe change adds a temporal lens-flare state pass. It replaces direct occlusion sampling with persistent state textures, configurable fade timing, and sun-disc-relative probe sizing. It updates shader contracts, presets, UI controls, documentation, and offline regression checks. ChangesLens flare state pipeline
Estimated code review effort: 4 (Complex) | ~60 minutes Merge Risk: ⚪ Minimal · up to The shader, pipeline, settings, preset, and UI contracts are aligned, with no actionable merge-blocking risk identified. Sequence Diagram(s)sequenceDiagram
participant LLPipeline
participant LensFlareStateShader
participant StateHistory
participant ColorCorrection
LLPipeline->>LensFlareStateShader: Generate sun or moon state
LensFlareStateShader->>StateHistory: Write filtered state
LLPipeline->>ColorCorrection: Bind state texture
ColorCorrection->>StateHistory: Sample lens-flare state
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 27.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 8 files. (7 skipped: 7 unsupported.)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Replaces per-fragment binary occlusion checks with a 256-tap spiral probe and a temporally filtered ping-pong state pass (lensFlareStateF.glsl). Texel 0 holds filtered flare drive with slew caps and adaptive damping to eliminate violent strobing through thin occluders (fences, foliage, avatar limbs). computeLensFlare() reads one state texel instead of running repeated depth taps every fragment. Replaces deprecated RenderLensFlareOcclusionRadius/Taps with RenderLensFlareOcclusionScale and RenderLensFlareFadeTime. Ported from Alchemy PR AlchemyViewer#386 by taylnos.
Problem
The sun lens flare strobed whenever a moving camera alternately hid and revealed the sun:
fence posts, foliage, orbiting an avatar with the sun behind it. Every element of the flare
(a streak across the full frame width, halo, ghosts, starburst, glow) went out in one frame
and came back in the next. At that size and rate it is a photosensitivity hazard.
computeLensFlaredecided visibility per fragment, from scratch, every frame: nineeffectively binary depth taps in a 0.02 UV disk, plus one point-sampled sun texel with a
hard return when it was not HDR-bright. Covering the sun's centre pixel switched the whole
flare off. The CPU lerp that looked like smoothing only touched the screen-edge fade, at a
per-frame rate.
What this does
A per-frame state pass.
LLPipeline::generateLensFlareState()runs right beforecolorCorrect(both HDR paths) and draws the newclass1/alchemy/lensFlareStateF.glslintoa 2x1 RGBA16F target pair, ping-ponged by handle swap like the exposure map. Texel 0 holds a
filtered, premultiplied flare drive (sun colour x HDR gate x unoccluded fraction x edge
fade) plus an instability score; texel 1 holds the raw target, a decaying reference
luminance and the step detector's anchor.
computeLensFlare(vec2 uv)reads one texel and nolonger touches depth or the scene buffer, which also removes ten texture reads per fragment
for a frame-wide constant.
The probe. 256 fixed taps on a Fermat spiral around the sun, weighted
exp(-8 r^2),each gated on its own: an occluded tap contributes nothing, an unoccluded one its overbright
colour. The radius is the sun disc's own angular radius through the current FOV (with the
horizon enlargement the drawn quad gets, and the snapshot zoom), scaled by a setting. A
still scene gives an identical estimate every frame; an occluder narrower than the disc dims
the flare instead of cutting it.
The filter. In luminance with RGB following: fade-in
tau = FadeTime/3, fade-out 0.6x,a slew cap relative to the reference luminance so no frame moves the drive by more than a
bounded fraction of the sun's recent brightness (a full off-on-off cycle cannot complete in
under 1.6 FadeTime), and adaptive damping: direction reversals of the raw target, measured as
displacement from an anchor so the verdict does not depend on frame rate, slow the filter
above a dead zone. One reversal (an ordinary reveal) costs nothing; a fence-post train settles
to the mean coverage within a few crossings. The drive snaps to exact zero once the target is
black, because a half-float target pins a geometric decay at a denormal.
Settings.
RenderLensFlareOcclusionRadiusandRenderLensFlareOcclusionTapsarereplaced by
RenderLensFlareOcclusionScale(multiple of the sun disc, 0.25-2, default 1)and
RenderLensFlareFadeTime(0.1-1 s, default 0.35), insettings_alchemy.xml, the Lookswhitelist, the three bundled Looks and the Lightbox Occlusion rows (Size, Fade). The
shader derives every rate from the one FadeTime uniform and is the declared source of truth;
scripts/content_tools/check_lens_flare_state.pymirrors it statement for statement and isthe regression test that chose the constants.
Numbers (from the script)
Verification
#version 400and420, with and withoutREVERSE_Z, plus a reserved-word scan of thecode lines (glslangValidator does not enforce the reserved list;
packedgot through itonce).
alchemy-binRelease: 0 errors, 0 warnings in the touched files. ctest 139/139 on thecommitted state.
scene that had still flickered mid-branch.
Checklist
Please ensure the following before requesting review:
Additional Notes
dead material-preview gate and a frame-rate-dependent step detector) plus the still-camera
needle flicker. Its message carries the detail.
colorCorrectand increateLUTBuffers/renderFinalize; expect insertion-pointconflicts only.
untouched when applied; bundled Looks are updated, but seeding is once per name, so an
existing install keeps its copies.
other screen-space pass there already is.
hardware: every touched shader compiles at
#version 400, a scan of all 237 shaders findsno reserved-word identifier outside the CAS and FXAA vendor headers' inactive sections,
and the one texture read in non-uniform control flow uses an explicit level.
🤖 Generated with Claude Code