LOG 003 // 03 OCT 2026
Light that travels through Quaso
I wanted to walk from a festival street into a neon city without changing the rules for light halfway through. The lanterns should light the paving stones. The signs should color the smoke. Daylight should enter a building through its open door, and a wall should actually stop it.
That sounds ordinary until you try to do it in a 2D engine. A quick glow painted around each lamp looks fine in a still image, but it does not know about the wall between the lamp and the player. Adding more special cases only made the scene harder to reason about. So I rebuilt lighting in Quaso around one idea: objects put light into the world, and the world decides where it can travel.

First, draw what is there
Quaso uses deferred rendering. That means I draw the scene's materials first, then calculate lighting from those materials. The first draw fills four images, together called a G-buffer: the visible color, emitted light, light absorption, and surface direction. Four is also the minimum number of render targets WebGL2 guarantees, so the layout has to earn its space.
A lantern writes its color into emission. A wall writes absorption. A normal map tells the lighting which way a brick or a leaf faces. The neat part is that one draw and one blend mode fill all four images. Regular color layers blend over each other, while emission and absorption add up. Several thin layers of fog can therefore become one thicker patch.
Imagine one paving-stone pixel under a lantern. The first pass records the stone's color and the direction its surface faces, without trying to light it yet. Later, the light pass asks how much lantern light reaches that position. The final pass multiplies the stone's color by that light and uses the surface direction to shade its bumps. Keeping those questions apart is what lets a new light affect the whole street without drawing the street again.

Absorption is more useful than a simple "solid" flag. Each color channel says how much light a material removes over distance. Blue glass can pass blue light and eat the red. Fog can remove a little light at every step. It still does not scatter light, so a lit fog bank will not glow from within. That is one of the limits of this version.
The math is small enough to picture. For each bit of a ray's path, I turn the material's absorption and the distance traveled into a surviving fraction of light. A wall leaves almost nothing. A thin patch of fog leaves most of it. I do that separately for red, green and blue, which is why colored glass can tint the light behind it instead of just making it dimmer.
Then, let the light travel
The light solver uses Radiance Cascades, a method developed by Alexander Sannikov. Close to a point, I need many nearby samples but only a few directions. Far away, I need fewer positions and many more directions. Each cascade trades one kind of detail for the other. The near one sees the room clearly for a short distance. The next sees farther with less position detail. They join from far to near into a light map for the whole scene.
In Quaso the nearest cascade places a probe every two scene pixels and looks in four directions. The next has probes twice as far apart, but looks in sixteen directions over a longer stretch. The pattern continues outward. I solve the farthest stretch first. Each nearer stretch then adds its own light to the farther result, dimming that farther light by whatever the nearer stretch passed through. It is like looking through several panes of tinted glass: each pane contributes its own glow and filters what is behind it.
Before tracing those stretches, I turn absorption and emission into opacity and already-filtered light, then make smaller copies for distant reads. After the cascades merge, I store the result as a light map. The final scene pass reads that map for every visible pixel. This is the practical shape of the renderer: draw materials, trace light, shade the picture.
This is why I can add another lantern or another firework spark without adding a lighting pass for that source. The cost follows the scene resolution and cascade settings, not the number of lights. The sun and sky join the same calculation when a ray leaves the scene. Put a roof in the way and daylight reaches the interior through the door rather than through the roof.
There are less glamorous steps around the cascades. Two distance fields help rays skip empty space and find the outside edge of an opaque object. A transparent tree crown or a particle is drawn later, after the main light map is ready, and reads that map directly. This forward pass lets it stay truly transparent. A ten percent margin around the screen also keeps a lamp just outside the camera from suddenly losing its influence.
The bugs were visible
The first version lost tiny lights when I made smaller copies of the material images. I was averaging both absorption and emission, so a small bright source faded twice. Converting them to opacity and premultiplied emission before making those copies fixed it.
Then there were stripes along bright edges. Smooth surfaces sampled the light map in a handful of directions, and each direction painted its own stripe. Sampling a blur wide enough for each direction's sector made the bands disappear. A roof had the opposite problem: its transparent top read the light map inside an opaque wall, where the value was black. Now that position takes light from just outside the nearest edge.
Moving the camera revealed another flaw. My probe grid moved with the screen, so light seemed to crawl over the world. Snapping the grid to world positions, then blending the newest result with the previous frame, made it settle down. A distance-field skip smaller than one texel also managed to stall the GPU. That one was a less subtle reminder to check every loop that runs in a shader.
There is still a faint diagonal pattern near a few tiny, very bright lights. Narrow gaps do not always produce clean beams. I have not tested this path on WebGL2 yet. But the larger change is already there: a lantern, a sign, a spark and the sky all speak the same lighting language.