Repository navigation
Conversation
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.
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.
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.
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.
Do not merge. This PR stays open for good. It shows what
serduncarries on top of upstream Flame. The patches live in this fork permanently and are not proposed upstream.mainmirrors upstreamflame-engine/flamemainand only moves fast-forward.serdunis a linear series of patches on top ofmain. It has no merge commits. Whenmainmoves, the series is rebased on it and force-pushed.serdun/<flame version>-<n>, never the branch. The current tag isserdun/2.0.0-dev.0-5, on upstream b9ef18e.serdun, with its own PR intoserdun. That PR is merged by rebase, soserdunstays linear, and its patch gets a row in this table.Patches
feat(flame_lighting): lights and roles for a wet worldLightSource,Lighting,Emissive,Ground(the depth projection),WorldLookup, and the pool and cone shaders.feat(flame_water): rain, water and wetnessRainat depth with its veil,RainCatcher,RainDeflector,Wettable, andWaterSurfacewithReflectionPass.feat(examples): wet world storieswet_benchtool andwet_world_test.feat(flame_water): rain in and out of focusRain.focusDepthandRain.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).fix(flame_water): rain over the view a camera moves toperf(flame_water): start rain drops over the viewfix(flame_water): fill the air the view showsfeat(flame_water): ripples by the wave equation on the gpuThe core
packages/flameis untouched. Our patches only addpackages/flame_lighting,packages/flame_waterand examples.Tags
serdun/1.38.2-0..-8sit on the 1.38.2 release (upstreamhotfix/v1.38.2, not onmain).