Occlusion-aware lens flare
A low sun behind a line of pines. The lens flare switches off when a trunk covers the sun and comes back when it clears, decided by one depth texel per frame.
The problem
A screen-space flare is a set of sprites drawn on top of the image. Nothing in that draw knows whether the sun is actually visible. Walk behind a tree and the ghosts stay on screen, floating over a dark trunk.
How it works
Each frame the sun's direction is projected to a pixel. A tiny compute pass reads the depth buffer at that one pixel and converts it to linear depth. For a directional light, depth at the far plane means sky, so the sun is visible. Anything nearer means something is in front.
The answer goes back to the CPU and is used on the next frame. A small state machine then fades the flare in or out over 0.2 s instead of popping.
The elements sit on a line through the screen centre: position = centre + (sun - centre) × offset. Offsets run from -1.37 to +1, so +1 sits on the sun and negative offsets land on the far side of the centre. Size grows from 50% to 100% with the fade, and colour is multiplied by it. Sprites are added with One One blending, no depth test.
What I hit
The source multiplies a distance-from-centre falloff into vertex alpha only. With One One blending alpha is never used, so the flare is just as bright with the sun at the edge as at the centre. I kept that.
While the sun is hidden, the state machine alternates between fade in and fade out every update, leaving a one-step residue. I run two updates per video frame, as a 60 fps game would, and the captured frames land on zero.
My build
Ported the element layout, the 45° rotation rule for rotating sprites, the fade states and the occlusion test to three.js. The depth read is a 1×1 fragment pass with texelFetch on a depth texture, read back each frame. Offsets, scales and colours of the 8 elements come from the repo's example flare asset. The sprites themselves are procedural: glow with star streaks, rings and hexagon apertures.
Trade-offs
One texel is a yes-or-no answer. A thin branch over the sun's pixel hides the whole flare, and a gap of one pixel shows all of it. The fade hides most of the popping. The test also lags one frame behind the image, and a sun half covered by a trunk counts as fully visible or fully hidden.
When to use it
Directional sun or a few point lights in open scenes, on hardware where a full occlusion query per light is too much. For dense foliage, sample a small grid and average it.
Source and credits
Technique source: Moonflow-Lensflare-System by Moonflow-Studio (Reguluz), https://github.com/Moonflow-Studio/Moonflow-Lensflare-System, MIT license, commit 6e0d5f8ee9acd92b20ed0fe33840a66021cce478. Ported to three.js from MFLensFlare (screen position of the light, in-screen check, fade state machine, element layout along the axis, size and colour from the fade, the rotation rule), the LightOcclusionChecker compute shader (one depth texel to linear eye depth), the One One blend of MFLensflare.shader, and the element offsets, scales and colours of the example flare asset EXPFlareBySpriteEditor.
Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, r170. Used for drawing, depth textures and render targets. It is not the source of the flare technique.
My own code: the procedural flare sprites, the forest scene, the no-occlusion comparison, the debug overlay, camera, labels and video. No external models or assets.