Physically-based lens flares#7654
Open
The-E wants to merge 6 commits into
Open
Conversation
The-E
marked this pull request as ready for review
July 26, 2026 10:27
Implements the Lee & Eisemann 2013 matrix method for camera lens flares:
every ordered pair of refractive surfaces in a lens prescription produces a
two-reflection "ghost" image of the iris, and the starburst is the
Fraunhofer transform of that same iris. Ghost enumeration, the per-ghost
paraxial ray-transfer matrices and their coated-Fresnel tints are computed
once at table load, so a frame only has to place one textured quad per
ghost.
There is one lens, because there is one camera: every sun in a background
is imaged through the same glass, which is what keeps their flares
consistent with each other. Which lens that is comes from "$Default Lens:"
in lens_flares.tbl, unset by default so retail content is unaffected.
graphics/lens_flare.h the module's whole API
graphics/lens_flare_optics.cpp ray-transfer matrices, ghost enumeration,
coated-Fresnel reflectance
graphics/lens_flare_aperture.cpp iris mask + its Fraunhofer starburst
graphics/lens_flare_table.cpp lens_flares.tbl / *-lens.tbm parsing
graphics/lens_flare.cpp module state, the camera lens, the texture
cache, and the per-frame build
The optics and image-synthesis halves are pure functions of a prescription
or an iris -- no engine state -- with one exception: ghost enumeration stops
at the shader's instance budget, since a ghost with no instance slot to draw
it in is not worth keeping.
Rendering is one instanced draw per visible sun, sharing the mounted lens's
iris and starburst textures for the whole pass, composited additively into
the HDR scene colour immediately before bloom so bloom and the tonemapper
treat the flare's energy like any other scene light. Both backends are
covered (opengl_post_pass_lens_flare / VulkanLensFlare); Vulkan needs the
negative-viewport Y flip that the other post passes use.
Because the flare is fused into the pre-tonemap buffer, the SDR and HDR
tonemappers would otherwise render an identically-tuned flare at very
different brightness, so the HDR path is rescaled by
Hdr_flare_headroom / LENS_FLARE_SDR_REFERENCE_WHITE with SDR as the
calibration reference.
A mounted lens suppresses the legacy sprite $Flare: path, and when it has a
starburst the bitmap sun glow is skipped too, so the two never stack.
Per-sun opt-out is the pre-existing $NoGlare:.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gives content authors a way to pick the lens a mission is shot through, and
tooling to see what it does.
Mission side, following $Lighting Profile: exactly since it is the same kind
of setting -- a global render choice belonging to one mission:
$Camera Lens: in the mission info section (missionparse), written back by
missionsave behind a version bypass comment, and edited in
the background editors of both FRED (IDC_CAMERA_LENS) and
qtFRED (cameraLensCombo)
set-camera-lens swaps the mounted lens at runtime; <none> for no flares,
<default> for what lens_flares.tbl declares
set-lens-aperture / -grating / -scratches / -dust
restyle the mounted lens's iris. They take no lens name,
so a mission cannot edit a lens it isn't looking through.
The lens sexps all edit tabled state in place, which
lens_flare_reset_for_level() undoes when the mission ends, so nothing leaks
into the next one. Like the other background operators they are not packed
for multiplayer, so in a networked game they affect the host only.
The lab gets a "Lens flare options" panel (its own translation unit, since
lab_ui.cpp has no room for another one): an override of the mission's lens,
live iris editing coalesced so a slider drag doesn't rebuild a 512^2 mask
and its FFT every frame, the global brightness/HDR-headroom calibration, and
a per-sun readout of what the last flare pass actually drew. The lab's
background loader parses $Camera Lens: the same way it parses the lighting
profile, so switching backgrounds there mounts the right lens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Introduce "Anamorphic Squeeze" and "Anamorphic Streak" options for lens flares, enabling stretched ghost footprints and horizontal streak artifacts for cinematic anamorphic looks. Changes include: - Shader updates and new geometry for streak rendering. - Table parsing for customization of squeeze, streak length, thickness, and tint. - Per-lens configuration in the lab and live updates without texture rebuilds. - Default behavior preserves prior spherical lens content.
…sistencies Decouple lens flare state setup from rendering by introducing `lens_flare_frame_update()` for reusable, deterministic frame data. Replace `build_frame_data()` usages, ensuring sun sprite and lens flare passes rely consistently on published results. Update shaders, backends, and logic for more precise artifact type handling and clarity.
Ensure missions requesting no flares or default lens retain their behavior even with a declared $Default Lens:. Update lens selection logic, mission parsing, and editor handling to preserve intentional "<none>" values. Add tests and refactor lens APIs for consistency and clarity.
Introduce a "+Camera Lens Flare:" option in *stars.tbl* to explicitly enable or disable physically-based camera lens flares for individual suns. This allows fine-grained control over flare behavior, complementing or replacing legacy $Flare: blocks. Update parsing, rendering logic, and add comprehensive tests to verify correct behavior across all combinations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements physically-based camera lens flares using the matrix method from Lee & Eisemann, "Practical Real-Time Lens-Flare Rendering" (2013), with the iris/starburst synthesis ported from realflare's aperture kernels.
Every ordered pair of refractive surfaces in a lens prescription produces one two-reflection "ghost" image of the iris, and the starburst is the Fraunhofer transform of that same iris. Ghost enumeration, the per-ghost paraxial ray-transfer matrices and their coated-Fresnel tints are all computed once at table load, so a frame only has to draw one textured quad per ghost.
One camera, one lens
A mission is shot through one lens, and every sun in its background flares through it — that is what keeps two suns' flares consistent with each other instead of looking like they came through different glass. In precedence order:
$Default Lens:inlens_flares.tbl/*-lens.tbm$Camera Lens:in a mission's info sectionset-camera-lens<none>for no flares,<default>for the table'sset-lens-aperture/-grating/-scratches/-dustrestyle the mounted lens's iris from a mission. They deliberately take no lens name, so a mission cannot edit a lens it isn't looking through. All lens edits are undone when the mission ends.Per-sun opt-out is the pre-existing
$NoGlare:, which the flare path already honours.Backwards compatibility
Nothing changes for existing content. The shipped
lens_flares.tblleaves$Default Lens:unset, so a mod gets no flares until it opts in — either with one line in a*-lens.tbmor per mission. When a lens is mounted it suppresses that sun's legacy sprite$Flare:path, and if the lens has a starburst the bitmap sun glow is skipped too, so the old and new effects never stack.The pass needs post-processing; with it off, or in a full nebula, or in VR (a per-eye camera artifact would be wrong), it simply doesn't run. Both renderer backends are covered —
opengl_post_pass_lens_flare()andVulkanLensFlare— so this is not a Vulkan-only feature.Structure
Four translation units behind one public header,
graphics/lens_flare.h, withlens_flare_internal.has the private interface between them:lens_flare_optics.cpp— ray-transfer matrices, ghost enumeration, coated-Fresnel reflectancelens_flare_aperture.cpp— the iris mask and the starburst that is its Fraunhofer transform (CPU FFT)lens_flare_table.cpp—lens_flares.tbl/*-lens.tbmparsing; holds no statelens_flare.cpp— module state, the camera lens, the texture cache, the per-frame buildThe first two are pure functions of a prescription or an iris and touch no engine state at all. Rendering is one instanced draw per visible sun (the flare axis and tint are per-sun; the prescription, iris and starburst are not), sharing one texture bind for the whole pass, composited additively into the HDR scene colour immediately before bloom so bloom and the tonemapper treat the flare's energy like any other scene light.
Because the flare is fused into the pre-tonemap buffer, the SDR and HDR tonemappers would otherwise render an identically-tuned flare at very different brightness, so the HDR contribution is rescaled by
Hdr_flare_headroom / LENS_FLARE_SDR_REFERENCE_WHITE, with SDR as the calibration reference.Commits
The first stands alone (it configures and builds without the second), so bisect stays clean.
Testing
Not yet verified, and where review attention would help most:
IDC_CAMERA_LENScombo,fred.rcdialog resize,resource.hid — are Windows-only and have not been compiled here. CI's Windows leg is their first real test.🤖 Generated with Claude Code