Skip to content

chore: serdun patch series on upstream - #9

Draft
SERDUN wants to merge 83 commits into
mainfrom
serdun
Draft

SERDUN wants to merge 83 commits into
mainfrom
serdun

Conversation

@SERDUN

@SERDUN SERDUN commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Do not merge. This PR stays open for good. It shows what serdun carries on top of upstream Flame. The patches live in this fork permanently and are not proposed upstream.

  • main mirrors upstream flame-engine/flame main and only moves fast-forward.
  • serdun is a linear series of patches on top of main. It has no merge commits. When main moves, the series is rebased on it and force-pushed.
  • Games pin an immutable tag serdun/<flame version>-<n>, never the branch. The current tag is serdun/2.0.0-dev.0-5, on upstream b9ef18e.
  • New work goes in a feature branch off serdun, with its own PR into serdun. That PR is merged by rebase, so serdun stays linear, and its patch gets a row in this table.

Patches

# Patch Platform What it does
1 feat(flame_lighting): lights and roles for a wet world all; shaders checked on Metal (macOS) and Vulkan (Pixel 9) A new package: LightSource, Lighting, Emissive, Ground (the depth projection), WorldLookup, and the pool and cone shaders.
2 feat(flame_water): rain, water and wetness all; shaders checked on Metal (macOS) and Vulkan (Pixel 9) A new package: Rain at depth with its veil, RainCatcher, RainDeflector, Wettable, and WaterSurface with ReflectionPass.
3 feat(examples): wet world stories all The wet world stories in Widgetbook, the wet_bench tool and wet_world_test.
4 feat(flame_water): rain in and out of focus all; checked on Metal (macOS) and Vulkan (Pixel 9) Rain.focusDepth and Rain.aperture: drops off the focused depth widen and fade by as much, with as much light, so a game keeps sharp the rain that matters (#10).
5 fix(flame_water): rain over the view a camera moves to all Drops are aimed over the ground from the view now to the view when they land, widened downwind by the drift within the view: a following camera no longer leaves its leading side dry (#11).
6 perf(flame_water): start rain drops over the view all Drops start a metre over the view, where they would be from the cloud: no simulation of rain no one sees; Umbrella's frame 4.10 -> 2.56 ms (#12).
7 fix(flame_water): fill the air the view shows all The rain a scene opens on, and air a view newly shows, is filled as it would be had the rain been falling all along (#13).
8 feat(flame_water): ripples by the wave equation on the gpu iOS, macOS, Android with Flutter GPU on; rings elsewhere Water moves by the wave equation on the GPU: rings spread, cross and reflect off edges; Pixel 9 raster 2.85 -> 2.0 ms (#14).

The core packages/flame is untouched. Our patches only add packages/flame_lighting, packages/flame_water and examples.

Tags

  • serdun/1.38.2-0 .. -8 sit on the 1.38.2 release (upstream hotfix/v1.38.2, not on main).
  • They are kept so that old pins still resolve.

@SERDUN SERDUN added the do not merge Fork carrier PR, never merged label Oct 1, 2026
SERDUN added 3 commits October 1, 2026 13:57
A new package for the light a rainy scene is lit by. LightSource is a
lamp with a radius, a cone and a source size; Lighting darkens the
world, adds haze and draws each light's pool and cone with the pool
and light shaders. Emissive marks what glows and where it stands, so
its mirror image is taken about its base. Ground is the one depth
projection of a street that light, rain and water share. WorldLookup
answers what stands where, so rain and water can ask for the light at a point
without walking the component tree.

Shader uniforms are scalars only: on Impeller Vulkan a vec2 after a
lone float loses the whole frame.
A new package for water as a property of the world rather than of a
scene. Rain falls at depth on the Ground's projection, from a cloud,
in the wind where each drop is, and far rain is drawn as a veil from
a formula. RainCatcher and RainDeflector let components soak or throw
drops back; Wettable gives a surface its wetting, drying, porosity
and gloss. WaterSurface is a film or a puddle that mirrors the world
through ReflectionPass, limited to the area it can reflect, with
Fresnel reflectance from the view angle.

Depends on flame_lighting for the light the rain and water are seen by.
Three Widgetbook stories show the two packages together: a mirror
puddle, ripples, and a rainy night street with knobs for wetness,
drying and the lamps. The rainy night keeps running while its knobs
change, so the street dries or soaks in time instead of starting over.
wet_bench times a frame of the street, and wet_world_test renders the
rainy night once.
Every drop was drawn equally sharp at whatever depth it fell, so the
drops nearest the eye - the biggest and fastest on the screen - were the
most prominent, though they pass in front of whatever a game is about.

Rain.focusDepth and Rain.aperture make the eye a lens. A drop is blurred
by aperture times the difference between its perspective and the
focused depth's (a thin lens blurs as |1/z - 1/z_focus|, and the
perspective is 1/z), its streak widened by that much and faded by as
much as it widened, so its light stays the same; its ends fade out over
the blur too. Splashes follow the same rule. Aperture 0, the default,
draws every drop sharp, as before.
SERDUN added 23 commits October 1, 2026 15:10
Drops were aimed at the view as it was when they left the cloud. From
12 m up a drop takes about two seconds down, and a camera following a
walker moves on by two or three metres meanwhile, so the rain landed
behind and the leading side of the view stayed dry; in a wind the
upper part of that side was emptier still, since a drop seen there may
yet travel metres downwind before it lands.

Rain now follows how fast the view moves and aims each drop over the
ground from the view now to the view when it lands, widened downwind by
as far as the wind carries a drop while it falls through the view. It
releases as many drops per metre over that wider ground, so the density
in view stays the same.
A drop was simulated from the cloud down: in Umbrella the cloud is 12 m
up and the view about 5 m tall, so most of every drop's way was above
the view, unseen. With drops aimed over the ground a moving view will
reach, that cost about 0.7 ms a frame.

A drop now starts a margin over the view, where and as fast as it would
be had it fallen from the cloud; whether the cloud lets it go is still
judged where it left the cloud. In view nothing changes: the same drops
per quarter of the view, and about 40 % fewer drops in the air in all.
With drops starting over the view (#12) two gaps showed that the cloud's
height used to hide. The rain a scene opens on was counted at a 2 mm
drop's speed, though small and far drops cross the view more slowly and
hang in the air longer: a third as many drops as the rain settles to.
And a view that grows or rises (a game's camera settling on its first
frames) showed empty air over the old start until drops fell into it.

Both now fill the air as it would be had the rain been falling all
along: drops released over as long as the slowest drop takes down, each
as far on as it would be by now, those already down or outside the band
left out. In Umbrella's downpour the air is even from the first frames
instead of after about 1.5 s.
A water surface drew the rain's rings as up to 32 travelling bumps
passed to its shader: each ring on its own, none crossing or meeting an
edge, and every pixel of water looping over all of them.

Where Flutter GPU is on, a surface now carries a field of heights that
the wave equation moves on the GPU (WaveField: three float textures in
turn, a step per pass): drops dent it, and the rings spread, cross and
reflect off the edges. The water shader bends the mirror by the field's
slope, read between cells by hand since not every GPU filters a float
texture, and the steep crests catch the light. The field's cell is a
sixth of the rings' wavelength, it runs as fast as a ring grows and
fades over twice its life; a drop's dent is shrunk by the square root
of how many rings cross a point at once, so a downpour stirs the
surface about as much as one ring does. Elsewhere - the web, tests, an
app without the flag - the rings stay.

The wave shaders are built by the package's own hook into a shader
bundle listed as its asset. flutter_gpu is imported only where dart:ffi
is, so the web still builds. The water shader's new uniforms are two
vectors after the ring array, and the base and tint colours became
vectors on 16-byte boundaries: Metal binds each uniform on its own, at
most 31, and Vulkan lays a vector out on its own boundary.

On a Pixel 9 (Vulkan) the ripples example rasters in 2.0 ms at p50
against 2.85 with the rings; Dart's time is the same.
The fork had no common ground for its systems. The street projection was
a set of getters on a lighting mixin that asked the camera on every call,
light and water found each other by walking the tree and through
statics, and each consumer of light walked the live lights again.

flame_stage is the base the other packages will stand on. StreetProjection
is the depth projection as a value made once a frame, with its optics as
a DepthCamera, its inverses, and ahead(depth), the real distance in front
of the street line that light and rain must use instead of depth times
span. A Stage, one per world and updated before the rest, keeps a
registry of who is on it and builds each frame: the view, the projection
from its Ground, every light its LightCarriers carry and the night its
Ambience makes, and what its ShadowCasters put in the way.

Lights are data and any component can carry them: point, cone, area,
line and directional, with smooth or physical falloff, depth, flicker, a
dimmer and lamps that come on with the night; intensity is not capped at
1, for an HDR buffer. Shadows are capsules at a depth; light reaching a
point is cut by what stands between, with a penumbra from the size of
the source.
LightCarrier, Ambience and ShadowCaster were plain mixins on Component:
a component that took one of them but forgot OnStage never joined the
stage and silently lit or shaded nothing. Ground joined by itself, so
two roles behaved differently. Every role is now a mixin on OnStage, so
the forgotten one is a compile error and joining happens in one place.

A light with no street under it used to stand at its own height, which
mirrors a lamp about its bulb. standsAtOf is now null there, and the
mirror decides what the line is.

The bulk shader writers of LightField and ShadowSet had no reader. The
lighting writes one light with writeLight and copies the capsules as
they are, so both writers go.
Lighting, rain and water each found the world on their own: Ground.of
and WorldLookup walked the tree, the lighting gathered every
LightSource and LightMirror itself, and water learnt whether the
lighting had drawn its rings from a flag left over from the last
frame. Each read the projection, the view and the lights at a
different moment.

Now the stage builds one frame before anything updates, and the rest
read it.

The lighting draws the frame's lights with one shader, light.frag. It
handles every shape (bulb, cone, window, tube, moon), on the wall plane
and as the pool on the ground. The frame's capsules shade it, with a
penumbra, so a light that moves drags the shadows with it. The shader
finds the nearest approach to a capsule exactly, as ShadowSet does,
not from samples. Its uniforms are one vec4 array named by #define;
LightShader writes them by name.

Water mirrors the lights itself, over the night, as a LightReflector.
The old way drew every glowing part into an image, which stops at
white. The water shader now has a lights mode that works each light
out where the mirror puts it. A bulb is brighter than white
(glowGain); a window or a tube is as bright as it is drawn. The halo
in the haze and the light cast on the walls and the air come along.
All of it is bent by the same drops as the street's reflection and
smeared by a rough surface. Its uniforms are one vec4 array too.
Tests parse both shaders and check the Dart offsets against the
names.

Rain finds its catchers and deflectors on the stage, which already
knows them, instead of walking the tree every step. It reads the light
from the frame's field. A drop drawn under the night layer was darkened
twice, once by its own light and once by the night. It now carries
only the light there, so rain is as bright under the night as over it.

The examples follow. Lit windows are area lights. The walker casts a
shadow from a step in front of the house fronts, and carries the torch
there.
Every rippled surface made a wave field, the road's film included. The
field stepped at a fixed rate, a whole cell's worth of wave at a time,
so on a road the cells are big and the field moved 3.8 times a second
and was seen to jump. A slow frame dropped steps whole, and drops still
went into the rings, which were updated and never drawn. A surface
taken off the stage kept its field.

The wave shader now takes how far a wave goes in a step (the Courant
number). Each frame takes as few steps as keep that under 1/sqrt(2),
and at least one, so a slow surface moves a little every frame. Damping
follows the time stepped, not the number of steps. With a field, drops
go only into the field.

The field is opt-in (gpuWaves): worth it for a puddle, where the waves
cross and reflect off the rim; a film on a road is better served by its
rings. A surface disposes its field when it is removed.
WaterSurface.depth was how much water lies there, a film or a puddle,
while depth everywhere else on the stage is how far in front of the
street line a thing stands - and the surface reads that depth too, for
the drops it catches and the things it mirrors. The water's own is now
waterDepth.
Each water drew the whole reflected world into a picture of its own
every frame: the road's film and every puddle on it, each with the
rain, the decor and the lamps. On a Pixel 9 that was the UI thread's
bottleneck, and it grew with every puddle a level placed.

What water shows of the world depends only on the line it mirrors about
and how it squeezes the mirror. A point lands at the same place in
every water that shares them, and a thing standing nearer the eye is
moved about its base by the same amount in each. So those waters now
share one picture, MirrorPass: the world drawn once over the part of the
view they cover, at the finest resolution any of them needs. Each water
reads its own rectangle of it through the shader, bent by its own rings
and smeared by its own roughness. The shader takes the picture's corner
and size (uImage); the sampler no longer assumes the picture is the
water's rectangle.

The picture is drawn when the first water of a frame asks for it. The
first water to ask a second time starts the next frame, so a scene that
renders without updating still sees the world as it is.

A water out of the view draws nothing and is in no picture. Waters that
differ in line, squash or what they reflect get a pass each.
ReflectionPass now holds the Mirror being drawn (the pass, or a water
drawing plainly), and the rain takes its darkness from the water it
lands in.
The rain was an object for every drop and droplet, with two vectors and
a Color made anew for each every frame. Each step it walked the world
for its catchers and deflectors and asked a water whether it covered a
drop by building the water's outline as a Path. Every slice, in the
street and again in the water, filtered every drop by depth. A veil was
up to 1600 lines laid out on the CPU each frame. All of it was one
class that simulated, shaded and drew.

Now the drops and droplets are rows of flat arrays (DropPool,
DropletPool). A row is dropped by moving the last one into its place,
and the colours are packed ARGB. The rain simulates in Rain, shades
once a frame, and sorts the drops into the slices on the stage that
draw them. A slice off the stage still filters for itself.
RainStreaks builds the triangles in buffers it keeps. Nothing is made
for a drop in a step; a drop that lands makes the one point it hands
to its catcher.

A catcher gets prepareCatch once a step. Water works out its outline
there, and covers a drop by arithmetic.

A veil is drawn by a shader, RainVeil (veil.frag): columns along the
wind's slant, a few streaks in each, each on its own phase. A streak
finer than a pixel is as faint as a thin stroke drawn by the canvas,
with the same cap of 1600 streaks, so it lights about as much as the
lines did. Its parallax was the wrong way round: it slid by as
1 - scale of the view's movement, so the farthest veil moved almost
with the street. It now slides by scale of it, as anything at its
depth does. Until the shader is loaded (RainVeil.load, or on first
use), a veil is its haze and spray.
A street lamp in the examples was two lights at one point: the cone onto
the street, and a small light all round for the bulb lighting its own
head and the air about it. Each paid for the cut, the light cast, the
halo, the sheen, the water and every drop.

A cone now carries both. spill is the share of its light the source
throws all round, within spillRadius, and reach takes the larger of the
cone and the spill. A light is five vectors for a shader, the fifth with
the spill, and lightFloats names their count. The forward scatter's
x^1.5 is x * sqrt(x) rather than pow.
The lighting kept one shader per draw on the belief that a shader holds
its uniforms by reference until the frame is drawn. A draw takes a copy,
so LightShader.shader is now one shader for every draw. A test with a
red and a blue lamp in one frame pins it.

The rest of the review, finding by finding:

- light.frag reads a light's fifth vector, the spill, as the field does.
  The street lamp in the examples is one light again: a cone whose bulb
  lights its own head.
- A light whose reach does not touch the view is not drawn. The margin
  round the view is a share of it, not 4 units.
- The halo the lighting cuts and the one a light draws come from one
  profile, LightSource.halo, so no ring can show round a bulb.
- Rivulets on a wet wall are set in world units (rivuletSpacing,
  rivuletWidth), not per 100 pixels. They take the colour of the light
  on them, not a fixed warm one.
- Glossy maps world coordinates to its parent through every ancestor's
  transform, so a wall in a scaled layer glints where the lamp is; it
  subtracted the parent's corner and lost the scale.
- New tests: the shader lights as much as LightField.reach says,
  including across a cone's soft edge and in a spill; a zoomed and
  moved camera lights where the lamp is; the canvas fallback is tested
  on purpose (LightShader.reset).
Each surface and each veil kept its own shader instances, on the same
false belief as the lighting: a draw takes a copy of the uniforms and
images, so one shader per program serves them all (WaterShader.shaderOf,
and one for the veils). A surface keeps only its uniform arrays.

The mirror walked the tree its own way. It applied a PositionComponent's
transform and nothing else of its decorator, and it drew what was hidden.
It now goes down through each component's decorator, as the component's
own renderTree would, and skips a component that is not visible with
everything it holds. A component that draws its children its own way (a
parallax layer) is mirrored with them only when it is reflected itself;
the doc says so.

The light a cone's source spills all round shows in the water with the
rest of the light it casts.
Something the whole frame's drawing reads had no place to be made: a
wet wall draws before the lighting does, so a light buffer made by the
lighting as it draws would reach the wall a frame late.

The stage draws first in its world, as it updates first. It now runs
every FrameStep on it then, in the order they joined, with the canvas
the world's components get. The frame's order is written down on Stage:
build the frame, update the world, prepare the frame's drawing, draw the
world.

A light's fifth shader vector carries its falloff as well, so a shader
that draws every light in one pass needs nothing else per light.
Stage.worldOf(world) gave the game above the world, so Stage.of(world)
made a second stage on the game; a world is now its own world.
The lighting drew every light on the canvas: on the wall plane and on
the ground, once to cut the night and once to add the light cast, each a
full-reach rect through light.frag. A wet wall drew every light again
over itself. With the street's sixteen lights that is about seventy
draws a frame before any water. And the light stopped at an 8-bit
image's white.

Where flutter_gpu is on, the lighting is now a FrameStep. Before
anything draws it adds every light of the frame up in one pass
(gpu_shaders/light_buffer.frag) into a half-float image at half the
view's pixels: the light cast in rgb, brighter than white where it is,
and what of the night is left in alpha. That shader follows light.frag
and LightField.reach: shapes, falloff, cone, spill, incidence on the
ground, shadows with their penumbra, and the halo's profile. The night
and the light cast are then one draw each of it (light_compose.frag),
and so is a wet wall's sheen. Three targets turn, so a frame's image is
never written while it is still being drawn.

Elsewhere (the web, tests, an app without the flag) LightBuffer.create
gives null and the canvas path stays as it was; useBuffer turns the
buffer off. The shader bundle is built by the package's own hook, as the
water's waves are.

On macOS (Metal) the rainy night example with the buffer matches the
canvas path frame for frame by eye. Tests cover the parameters it
writes (the strongest lights in view, the sky left out) and the layout
light_buffer.frag reads. The GPU pass itself only runs on a device.
A light mirrored in water was never narrower than 0.5 world units: a
floor meant to keep the shader from dividing by zero, written for a
world in pixels. The game's world is in metres, so its 7 cm street lamp
bulbs came out in a wet road as burnt-out discs a metre wide. A lit
window's edge had the same 0.5 added, and a tube was at least 2 units
thick, in the water and as its light source draws it.

The floor is now a ten-thousandth of the water's size. A window's edge
is 4 % of its smaller side, and a tube is at least a fiftieth of its
length thick. A test in a world of metres mirrors a 5 cm bulb as a
bulb; with the old floor it lit 30 cm round it at full white.
The lighting was a night: a dark layer of an ambient colour at some
darkness, with each light cutting its shape out of it. A scene had a
night mode and a day mode, and a game that also tinted its colours by
the time of day darkened the night twice.

Light now works one way. An Ambience is a sky: the colour of its light
and how much it gives (skyLight), in the units lights' intensities are
in, so noon is tens of lamps and a cloudy night a fraction of one.
Directional lights (the moon) add to it. The eye adapts to the sky:
LightField.exposure is one over the sky's level, but no more than one
over the ambience's adaptation, a lamp's worth by default.

The lighting multiplies everything under it by the illumination as the
eye sees it: the sky's light, every light on the wall plane and on the
ground, what glows itself (a bulb, a lit pane, a tube, at full so it
shows as drawn), and the halo the haze makes round it. Each light's
colour is then added in the air at glow. There is no night and no day:
under a bright sky the lamps vanish against it, and under a dark one
they carry the scene. With the sky at or above white and no lamp on, it
draws nothing.

The rest follows the one model:

- A light's sensor reads the sky (switchOnBelow, the sky's level at
  which it is fully on), not a darkness.
- The light buffer's alpha is what glows itself, no longer the night
  left; the compose shader multiplies by (sky + light + glow) x exposure.
- Light drawn in the air (halos, a pane's spill, a tube's glow), the
  sheen on wet walls, the lights in water and the light on rain are all
  as much as the exposure makes of them.
- A drop drawn under the lighting carries what the light there leaves
  out, rather than giving back a darkness.

Tests: a lamp carries a dark sky and is a speck under a noon sky, a
white wall takes a dusk sky's colour, the moon lights everything alike,
a sensor switches a lamp as the sky dims. The examples' rainy night has
a sky's light where it had a darkness, and the same street goes from
night to noon on that one knob.
How wet things got and how drops behaved were numbers set thing by
thing. A wall had a wetRate, a dryRate and a porosity; a puddle dried
by a rule of thumb (0.002 over its depth); every surface bounced a drop
with the same 0.35 and splashed it at the same impact. Drying came from
a bare multiplier on the rain.

Now:

- A Substance is what a surface is made of. It has porosity, the water
  it holds (mm), how fast water soaks into it, roughness (its wet
  gloss), how a drop bounces off it (restitution, slip), the impact
  above which a drop splashes, how loud rain is on it, and friction.
  Asphalt, brick, concrete, glass, metal, canvas, cloth, foliage, wood
  and water are given.
- Weather is a stage role: rain in mm an hour, wind, humidity,
  temperature, cloud cover, and the world's pace (world seconds a game
  second, for runs that cross an evening in minutes). The stage reads it
  into StageFrame.weather, which works out evaporation (Dalton's law),
  haze and visibility (Koschmieder).
- A Wettable holds water in mm. Rain comes in at its share (an upright
  wall little, a hollow more); evaporation and soaking go out; past what
  it holds the rest runs off. All of it runs in world time. So a road
  dries before a puddle because it holds a millimetre and the puddle
  twenty, not because of a tuned rate. A puddle's width goes as the
  cube root of its water (a cone-shaped hollow).
- Rain is the stage's Weather, with humidity, temperature, cloud cover
  and pace; drying is gone. A bounce takes the restitution and slip of
  what it meets (RainDeflector.surface) and counts its loudness, and a
  drop splashes above the impact of what it lands on
  (RainCatcher.surface): water, cloth, foliage or asphalt.
- colorOfKelvin gives a light's colour from its temperature.
- The eye adapts over time (Ambience.adaptsIn): quickly to brighter
  light, four times as slowly to dimmer.
- The lighting's haze comes from the weather unless one is given.

The examples' houses are brick in an upright wall's share of the rain,
and the rainy night takes a humidity where it took a drying speed.
A scene's sky was picked by hand: a colour and an amount of light per
time of day. And only lamps cast shadows; the sun did not exist.

Daylight.at gives the day for a sun's elevation and the cloud cover, in
lamps (20 lux):

- the direct sun is thinned by the air it crosses (Kasten and Young's
  air mass) and reddens as the blue scatters out;
- the sky gives some 16 000 lux at a high sun and 400 at sunset, and
  falls about tenfold every 2.5 degrees below the horizon;
- clouds take the direct light away and spread a quarter of the clear
  day's light, grey;
- the night's floor is the moon and a town's glow thrown back by clouds,
  warm;
- the sky's own top and horizon come out as the eye sees them beside a
  white thing in that light, the horizon warming round sunset.

Light.sun is direct light with a direction in three dimensions. It is
kept as a light of the field rather than folded into the sky, and
everything in its way shades it. Its source sits at
LightField.sunDistance, half a degree across, so a shadow is sharp at
its foot and softens with distance. On the ground it falls as steeply
as it comes down; on the house fronts, as much as it comes from the
eye's side. The eye adapts to it with the sky.

That made two flaws in ShadowSet.through visible, fixed in Dart and in
both shaders:

- An occluder had to lie between 2 % and 98 % of the way from the light,
  which on the sun's long way blurred every shadow and dropped
  occluders near the receiver. The margin is now a capsule's radius,
  never more than 2 %.
- A receiver standing in the occluder's depth slab took the occluder's
  distance where the way crossed the slab's plane, at the receiver
  itself. It now takes the part of the way that runs through the slab
  and finds that part's nearest approach to the capsule. The part is
  taken from the receiver back, so far-off light keeps its precision.

The shaders no longer floor a light's source at 1 unit, which in a
world of metres made every penumbra a metre wide.
A zero rivulet spacing was kept off by a floor of 1e-3 world units,
which in a world of metres let a wall loop over millions of rivulets.
The floor is now a two-hundredth of the wet area. The pixel defaults of
the spacing and width are documented as such.
A game that turns the day into a sun light needs where the sun stands
as well as how much light it gives; Daylight now carries the elevation
it was worked out for.
The eye adapted to the sky alone. At night, with the adaptation's floor
at a few lux, every lamp's pool came out several times brighter than
white and the street drowned in it, though an eye standing on a lit
street sees the pools as bright, not blinding.

LightField.seenLevel is now what the eye adapts to: the light falling
over a grid across the view, the sky's and every light's, on the house
fronts above the street line and on the ground below it, and of that
the brightest a tenth of the view gets. That is the anchoring of
lightness: the brightest thing in view looks white if it is big enough
to count. A lit street at night is seen in its lamps' light; a far bulb
is a speck and leaves the night dark. A geometric mean was tried first
and barely moved: the dark sky outweighs the pools in it.

With no view it is the sky and the suns on open ground, as before. The
stage hands the frame's view and projection to LightField.finish.
SERDUN added 30 commits October 2, 2026 19:50
Water left the rain's veils out ("a veil is far behind"): a bench pale
in the haze above stood dark and sharp in its reflection, as if the way
through the water crossed no air.

The way to what water mirrors crosses the same air and more: the veils
are drawn in the mirror's picture too, their haze out of the picture's
multiply. Mirrors draw their pictures apart (LitPicture.apart), so what
draws unlit in them keeps to the picture's layers.
What shows under water had no model: a colour the scene picked, the
same for a millimetre on asphalt and a pond.

WaterMedium is the water's optics per colour: its refractive index (the
surface mirrors by Fresnel from it, 2 % straight down), its absorption
and scattering (Beer-Lambert down and up along the refracted way), and
how much deep water sends back of the light falling on it (Gordon:
pi Rrs, Rrs = 0.0949 u + 0.0794 u^2). Two of them: clear (pure water,
Pope and Fry) and pond (leaf-brown and algae-green, its bed gone under
half a metre). Substance.mud is a pond's bed, full, soaking no more.

Tests: 2 % straight down; a 2 cm puddle passes its bottom, a metre of
pond passes none; deep clear water is blue, a pond green-brown, both
dark; refraction keeps the way under within 49 degrees of down.
Water drew a colour of its own under its mirror, mirrored a fixed 2 %
straight down, and a puddle on the wet road was a second layer over the
film there.

A surface has a medium (WaterMedium) and is as deep as the water it
holds (depthM, from waterMm). It draws twice: what is under it - the
ground, lit as it is - multiplied by what comes back through its surface
and its depth, and the ground darkened as the water soaks it; then the
light it adds, its mirror at the medium's Fresnel and the glow of its
depth in the light falling on it. A film or a puddle shows its asphalt
and the lamp's pool on it; a pond its colour only. Without the shader
the same at the water's middle. A film leaves the ground to standing
water lying on it: one surface there. color is now the bottom, for a
surface with nothing under it, clear by default.

Also: the clear image for water with no picture made no canvas on its
recorder and threw.

Tests: a puddle shows its bottom and a pond hides it, plain and through
the shader; a film darkens its asphalt but not under a puddle on it.
What was drawn over what was each component's priority, picked by hand
per scene: a lamp's bulb and halo added last were drawn over the walker
passing in front of it, and rain behind the street line over the lamps
standing on it. Nothing tied the order to how far things stand.

AtDepth: a thing on the stage says its depth under the projection and
what it is there (DepthOrder: ground, flat, rain, standing, air, light).
Each frame the stage gives them priorities far to near; at one depth the
ground first, then what lies on it, the rain behind what stands there,
what stands, the air over it, a lighting cut last; then the order they
joined. Ground is at the street line, under all of it. A priority given
outright places a thing by hand instead (placedByDepth).

RainSlice is at its nearer edge, rain; its edges can move with what
stands. WaterSurface lies at its far edge, flat. LitCut is at the depth
it lights, light. The wet street example drops a default argument.

Tests: things added near to far are drawn far to near; at one depth in
the order above.
The air was one number, the visibility through the rain and the mist,
the same everywhere; a lamp's halo came from a weighting of rain and
humidity picked by hand. A fog lying low, rolling in from one side or
thicker past the pond had no place in it.

AirField is the air's extinction per metre at a point (x along the road,
ahead of the street line, height): the rain and the mist everywhere as
its base, and up to three AirBanks over parts of the scene - a fog, a
storm cell's rain - each past an edge across the ground (any way: along
the road, away from the eye), eased in over its width, leaning with
height, thinning upward over its scale height. Optical depth along a way
(Koschmieder), transmittance, visibility at a point, and the banks'
vertical optical depth for the sun. A Weather gives its banks
(airBanks); WeatherState.air is the field. The halo round lamps is the
share of their light the air there scatters over a halo's reach
(hazeAt), the light in the air the air's glow along the eye's way to
the street - both from the field.

Tests: uniform air leaves 2 % at its visibility; a ground fog holds the
feet, not the tops, and hardly dims the sun; a bank past its edge is
thick and clear before it; a bank lying away from the eye covers the far
before the near.
The haze was the rain's: a flat rect of the horizon's colour at each
rain veil, as thick as the uniform visibility made it, nothing at the
street line or in front of it.

AirVeil stands at a depth and lays over everything there and behind the
air between it and the next veil toward the eye, worked out pixel by
pixel in air.frag: the eye's way to the point summed through the
stage's air field - far things through all of it, a ground fog over
the feet and not the tops, one side of a fog's edge lost. GroundAir lays
each row of the ground band's air up to the nearest veil nearer than it.
Both are light: drawn out of the multiply, in the horizon's light as
the eye sees it. The rain's veils lose their haze (rain_veil.dart): the
rain's share of the air is its extinction.

Tests: a thick fog loses the far into the air's light and leaves the
ground before it; a ground fog leaves the high clear.
A fog had no say in the day: the sun shone through a deep one as through
clear air.

Daylight.at takes a fog's optical depth (AirField.verticalOpticalDepth)
under the clouds, over the whole sky: it lets through its share of all
the light falling on it as a scattering layer does (cloudTransmittance),
the sun's disc dimmed over the sun's way through it, the moon and the
town's glow with it, and it greys the sky. Without one nothing changes.

Test: a shallow fog hardly dims the sun, a deep one puts it out and lets
between a fifth and three fifths of the day through.
A bank thinned upward as exp(-h / H): a ground fog a metre deep still
hazed the eye at 1.7 m and everything it looked at, and the tree tops.
A fog lies under a warmer air that keeps it down, with clear air above.

AirBank is a layer up to its top (topM), thinning out over topWidthM
round it (a logistic step); its vertical optical depth the layer summed
from the ground up. air.frag reads the same. Water lies on the ground
band (depth 0) for drawing: under the air over the ground and whatever
stands on it, wherever on the band it is - water at its shore drew over
the ground's air and left a seam at the front veil.

Tests: a ground fog 1.2 m deep holds the feet and leaves the head; a
point 0.4 m up at a far veil is in it, 2 m up clear.
The optical depth along the eye's way was 24 steps in Dart and 16 in
air.frag, each step every bank: six passes of it a frame for the veils.

A bank's layer has a closed integral along a straight way, the
logistic's h - s ln(1 + e^((h - top) / s)), and so has its eased edge,
u^3 - u^4 / 2 over the ease. AirBank.meanShare averages each along the
way and multiplies them: exact when either is even along it, as for a
rain cell or a deep fog seen low. Dart and the shader sum the same; no
loop over the way remains. A veil draws only where it lays anything -
above where its depth meets the ground, the ground's air over the band -
and none in air as thin as the clear base over the whole way.

The run sandbox drawn every frame for forty seconds went from 93 s to
38 s in the tests (18 s with no air drawn).
The layer's softplus took exp of how far above its top a point is, in
quarters of the top's width: for a ground fog (0.4 m) a tree top a few
metres up is hundreds of them, and exp of that is infinite. The branch
that uses it is never chosen there, but a GPU may work out both sides
of a select, and an infinity under fast maths is undefined. Clamped to
the range the branch is chosen in.
A leaf or a scrap of paper as a plate in the air (AirBody, AirBodyKind):
the quasi-steady model of a falling plate (Andersen, Pesavento and Wang
2005) in its own axes - its weight, the air it carries across its face
and the Munk moment that turns it broadside, the lift of the air turned
round it aslant and spinning (as much of a long plate's as a wing of its
aspect gets, Helmbold), the drag from edge on to face on and on its
turning; drag along its span. Stepped by Runge-Kutta: a plain step feeds
energy into its swing.

The air near the ground is turbulent: AirStir.nearGround gives the eddies'
spread and time from the wind, the height and the ground's roughness
length (Panofsky and Dutton, Hanna), and each body stirs its own eddy as a
Langevin process (Thomson 1987). Below the height a scene's wind is
given at (a walker's umbrella) it slows toward the ground as the surface
layer's logarithmic wind over the surface's own roughness
(AirStir.windShare).

At rest: lying, the wind just over the ground lifts it once its drag
outdoes the friction of its weight and the grip of the rain in it;
afloat, it drifts at 3.5 % of the wind. Rain soaks it, darkening it as
porous as it is, and dry air takes it back, in the world's time.
A scene knew where its sun stood and nothing more of its sky. Celestial
works out a place's sky from the day of the year, the local solar time
and the latitude: the sun's and the moon's altitude and bearing, the
moon's phase and the light it gives level ground under a clear sky
(its phase law, its height, the air on the way down), and where any
star stands. A low-precision ephemeris after Meeus, good to a degree;
the solar time a scene's sun elevation implies (solarHourOf).

StarSky is a seeded sky of stars by brightness as the real one has
them, and the faintest one the eye sees against a sky of a given
brightness (Schaefer).

Tested against the eclipse and the full moon of April 2024, the noon
sun through the year at 50 N, an October sunset, the north star.
The glow a lamp makes in the air was its light at the point times a way
pi * dist long: what a way past an inverse-square light gathers. But a
lamp's light falls off as (1 - r/R)^2, or 1/(1 + 16 r^2/R^2), and with
those the product peaks a third (a quarter) of the radius out: in a fog
a lamp's beam showed a bright blob halfway down its post, the lamp over
it dark.

Now the glow is the falloff itself summed along the eye's way past the
light, in closed form for the smooth one and as an arctangent for the
physical one: largest at the lamp, falling away down its beam. Both the
canvas shader (light.frag) and the GPU buffer (light_buffer.frag).
Tested down a beam in thick air; the old glow fails it.
The street's projection had one street line for the whole view: the
road was level by construction. A Ground now says how high it stands
at any x (groundLiftAt, level by default), and the stage samples that
over the band once a frame into the projection (GroundLift): the street
line at x (lineAt), where something at a depth meets the ground at x
(groundYAt), the depth of the ground seen at a point (depthAtPoint), and
the lift written out for shaders (writeLift). A hill is a rise across
the whole view at its x - the street line and the ground in front of it
together. Level ground changes nothing.
The light and the air read one street line for the whole view: the
ground plane began at band.top in every column. Both shaders of the
light (the canvas light.frag and the GPU light_buffer.frag) and the
air's (air.frag) now take the ground's lift across the band, 32
samples, and raise the street line and the ground in front of it by it
in each column: where the light falls on the ground and at what depth,
where a veil's air stops and the ground's begins, how high a point
stands over the ground under it. Over a hill the wall's and the
ground's rects overlap; each draw keeps to its own side. A light's foot
(standsAt) and the level the eye sees are taken where the ground is.

The rain lands on the ground where it comes down, its spray runs along
the raised line, and water - which lies only on level ground - mirrors
about the street line where it lies and catches drops there.

Level ground writes no samples and draws as before.
Water laid over the ground - a wet road's film, a pond - was a rect from
the street line down: over hills its flat top hung over the slope, the
world mirrored above the ground. A surface that liesOnGround now has
its outline's top along the street line under it, and is drawn clipped
to that over hills. On level ground nothing changes.
The light in the air was rendered at an eighth of the light buffer and
halved twice more: a thirty-second of the buffer, about ten pixels
across a phone's portrait view. Each was a metre and a half wide, as
wide as a lamp, and stretched bilinearly over the screen: a lamp's glow
came out a square, brightest wherever that pixel's centre fell - half
way down a tall post rather than at its lamp.

It is now made at eight pixels a metre whatever the screen, and halved
twice to the half a metre it was meant to be soft by.
A mirror draws the world again apart from it, the stage among its
children, and the stage ran its frame steps again: the lighting added
every lamp's light up anew, a second time for the same frame, and
turned its GPU buffer's ring of targets twice a frame.

The stage now runs its steps once a frame however often it is drawn.
A cut placed by how far ahead of the street line it stands worked its
depth back through the projection: a rounding off the depth of the
layer it lights, now before it, now after. Drawn before it, the cut
lit nothing, and the layer was lit on the street line instead - a
frame now and then.

LitCut.at takes the very depth the layer stands at; the depth order
puts it after the layer every frame.
What draws unlit lays down what the world drew before it, lit by the
first cut not drawn yet - asked by the frame's index. A frame is drawn
more than once (a screen faster than the steps, a slowed clock): drawn
again, every cut had drawn already at that index, and what stands
behind the street line was lit as on the street line instead, by the
lamps there. A tree twenty metres back, out of every lamp's reach,
showed lit every other drawing of a frame - the blinking a report
caught.

The lighting counts its drawings of the world, ending each; a cut
notes the drawing it drew in.
A slice's edges were set once a step, before the stage builds the
frame's projection: a step behind the depth the stage orders the
decor by, now a rounding before it, now after. The rain behind a
layer then drew over it for a frame, and the veil at a layer fell now
in the slice behind it, now in the one before.

RainSlice.between reads its edges each time it is asked.
The eye adapted to the light falling on things in view - the sky's and
every lamp's where it reaches - but not to the light in the air before
them, which the lighting lays over the view. At night in a downpour
that is the brightest of what the eye sees, and several lamps' glows
together came out a blown yellow wall.

LightField.seenLevel adds the air's light at each point it samples:
glow times each lamp's share of the air between the eye and the street
line (airLightAt), as the shaders' air pass works it (glowLength,
airShare), from the frame's air depth and visibility.
Rain drawn under the lighting is multiplied by what the lighting shows
there, the sky's colour and the lamps' with it. Its drops and splashes
were drawn in the air's colour tinted toward the lamps' first, and so
took the light's colour twice: a splash by the camera at dusk came out
a bright orange blot on the water.

Under the lighting a drop is now drawn as what, multiplied, comes out
as it is drawn over it: its colour over over what the lighting shows,
channel by channel (LightSample.shownRed/Green/Blue, as the compose
shader works it); past white the rest goes into its cover.
A drop's splash was thrown as much one way as the other, whatever way
the drop came: in a wind the rain slanted, its splashes did not. A
slanting drop's splash keeps the speed it came across the surface with
(Bird, Tsai & Stone 2009): each droplet now has it besides its own
throw, and the splashes fly where the wind drives the rain.
How a light reaches a point was written three to five times - in
light.frag, in the GPU buffer's light_buffer.frag, in water.frag and in
LightField - and nothing held the copies together. One had already
drifted: the GPU buffer cut the night round a bulb by a smoothstep
while the bulb is drawn fading linearly, a ring round every lamp.

shaders/light_math.glsl now holds them once (lightFall, coneAt,
glowLength, airShareOf, bulbItself, halo, haloRadius, segments), and
the three shaders include it; LightField.haloRadius owns the halo's
size in Dart. test/light_math_test.dart draws light.frag and holds it
to LightField.reach on a wall and LightField.airLightAt in the air,
both falloffs. The bulb's cut now fades as it is drawn. Water bends
light by its medium's own index (from the f0 it is given) rather than
a fixed 1.333; WaterSurface.fresnel, unused, is gone.
A cut with no lighting to draw it dropped an image of its own without
disposing it; and the GPU buffer kept three textures for every slot a
cut had ever used, a removed cut's included. The cut now lets go of
such an image at once, and the lighting lets go of the slots past the
cuts it has (LightBuffer.releaseSlotsFrom).

A cut placed by hand (LitCut with ahead) now needs its priority: one
placed by depth stands at the depth of what it lights (LitCut.at),
not at one worked back from how far ahead it stands.
What must happen once a drawing of the world guessed at it in three
ways: the stage ran its steps once a frame because a mirror drew the
stage again; the lighting counted its own passes; and the mirror
started a new frame when a water asked a second time - so a water that
sat a drawing out and asked first in the next read the picture of the
one before.

Stage.drawing is now the one count: the stage draws first in its world
and counts each drawing; a mirror leaves the stage out (MirrorPass
.mirrors). The mirror's passes are drawn once a drawing by it, and a
cut notes the drawing it drew in by it.
ReflectionPass.run drew nothing when a reflection was already being
drawn: a second kind of mirror shown in water - a shop window in a
puddle - would have vanished without a word. It now draws as the inner
mirror and gives the outer one back after, as LitPicture already does;
water is still not reflected in water, which its own list of what it
mirrors leaves out.
A mirror's picture of the world reaches far past the view - as far
again each way as water can bring into it - but the frame's light
images cover the view alone. Read past their edge they were clamped:
the edge's light stretched over all that stood beyond it. And where
there were no images, each light was cast only within the view.

Past the images the picture is now lit light by light over its own
area (the sky's light and every lamp cast there, at a cut's depth for
what stands behind the street line); within them, as before.
A mirror picture past the light images was lit through a path clip
that cut the covered part out: smoothed at its edge as the image's
own draw was, a pixel on the line was darkened twice by half and a
bright seam showed; and every light was cast over the whole picture to
be clipped away, on every rebuild.

It is lit as up to four rectangles round what the images cover, each
by the lights that reach it, and the light's multiply is not smoothed
at its edges. A frame with no light to add up, or no cut to draw, lets
go of the cuts' GPU slots too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Fork carrier PR, never merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant