feat: flame_water and flame_lighting for the wet world - #1
Merged
Merged
Conversation
On Flutter 3.47 dashbook 0.1.19 takes ThemeData from material_ui, so passing it the framework's ThemeData failed to compile and the examples app did not build at all. Taken from upstream 76c8e81 (flame-engine#4066): main.dart imports material_ui and dashbook keeps its own theme.
A side-view game in the rain lives on reflections: puddles and wet ground that show the street above them. flame_water is a new package in the monorepo for that. WaterSurface is a stretch of water (a rect or an ellipse) that draws the Reflectable part of the world again, upside down about its water line, clipped to its shape, squeezed for a low viewing angle, faded with depth and tinted over the water's own colour. It walks the world tree itself: a reflectable component is drawn with all it holds, any other one only as a way down to its children with its transform, and water is never reflected in water. No image of the scene is made, so it costs draw calls rather than an offscreen render and works on every renderer, the web included - the base quality tier the shader tiers build on. ReflectionPass tells a component it is being drawn as a reflection, so a lamp can reflect as a glow. RippleRings is a fixed pool of flattened rings spreading from where drops hit, fed through WaterSurface.splash. Tests render a surface to an image and read the pixels: a reflectable block is mirrored below the line and a plain one is not, a squashed reflection is shorter, the pass flag is set only inside the reflection.
A chapter of the fork's own samples, first in the list: a street at dusk (sky, houses with lit windows, two lamps, a walker under an umbrella, a road) where everything but the road is Reflectable. Mirror puddle: a wet road with a faint mirror and two puddles showing the same reflection brighter, with sliders for reflectivity, squash and fade and a switch for the wet road. The lamps reflect as glows. Ripples: the same street in the rain; a drop that lands in a puddle starts a ring sized by the drop, and a tap splashes. Code links point at the SERDUN fork, where these samples live.
In the wet world samples a drop and the ring it made did not read as one event. Every drop lands at its own depth on the road, but all drops looked alike, so a streak crossing a puddle seemed to hit it, fell on and landed nearer, while a ring started under some other drop; and a drop just vanished where it landed. Depth now shows in the drop: a near one is longer, brighter, thicker and crosses the screen faster than a far one. Where it lands it bursts into a few droplets in the same step that starts the ring, so the burst and the ring meet. Drops born in one step are spread over the way they fall in it (they fell as lines at a low frame rate), and the scene opens with the rain already falling. The rain is shared by both samples, and the mirror puddle sample rains too.
Rings from drops were drawn over a still reflection: a drop hit the water and the mirrored street did not stir, which water never does. WaterQuality.rippled renders the mirror into an image once a frame, at resolution pixels per unit, and draws it through water.frag. The shader treats every ring as a travelling bump on the surface and looks the reflection up a little to one side where it passes, so the mirrored world wobbles around each drop and nowhere else. The newest 16 rings go in as a uniform array, with the ring flattening and the wavelength. WaterShader loads the program once (on the web it compiles at load); until it has, and at WaterQuality.mirror, a surface draws the plain mirror as before, with no offscreen render. ImageFilter.shader is not used: it fails on the web. A pixel test renders stripes over two surfaces: still water shows them as the mirror does, a struck one moves them near the drop and leaves the far ends alone - which also proves FlutterFragCoord comes in the surface's local coordinates.
Both samples load the water shader and land every drop somewhere: in the puddle that covers the spot, else in the film on the road, which has small quick rings of its own. The rings now bend the reflection, so the ring lines are fainter.
The rainy street needs its night: lamps, lit windows, and the light they throw on wet things. flame_lighting is a new package for that. LightSource is a point or cone light with a colour, an intensity, a reach, a flicker and the radius of what glows itself. Its light falls off smoothly with distance, and a cone eases out to its sides over the outer part of each side instead of ending on a straight line. Lighting covers the view with the night's colour at a darkness, cuts every light out of it with a soft falloff and adds the lights' colour on top. A cone is the disc of light masked across by a sweep gradient in a layer, so it has no hard edge. lightAt tells how lit a point is and in what colour, for things that shine in the light, like rain. A LightMirror (water, a wet road) shows the lights above it mirrored: the light's own source as a bright core nearly as strong as the source - water seen low across mirrors most of what falls on it - and its glow drawn out down the surface into a streak. Only blend modes, gradients and one layer: no shader, so it works on every renderer, the web included. Tests render a game and read pixels: dark away from a light, a lamp lights below it and not above, a cone's edge has no step, the mirrored lamp is nearly as bright as the lamp.
WaterSurface is a LightMirror: under a Lighting the lights above it show in it as a bright core and a glow drawn out lightStretch times down the water - a round spot in a puddle, a long streak on a wet road. The package now depends on flame_lighting.
Everything at once, at night: lamps light soft cones of the street and the lit windows glow, one flickering; the wet road draws every lamp out into a streak and the puddles mirror it as a bright spot; rain draws above the night as bright as the light where it is, in its colour; every drop ripples the water it lands in. Sliders for darkness, glow and rain.
Compared with photos of lamps on a wet night two things were missing. A lamp's reflection on wet ground is not a smooth streak: the surface is never flat, so it breaks into a column of short horizontal slivers that shimmer. A LightMirror now draws a light's glow that way: slivers from its water line down past the mirrored source, brightest there and fading both ways, each wavering in width and brightness on its own in time. The bright core of the source stays whole. And wet air scatters light: a lamp glows far beyond its bulb. haze (0..1) gives every light a wide soft halo round its source, sized by the source's radius, so a window gets a small one and a lamp a big one. Also fixed: the lighting's paints are shared, and a sliver's colour alpha dimmed every gradient drawn after it - the mirrored core and the next lights came out too faint. The paint's colour is reset after the slivers. Tests: the mirrored lamp is nearly as bright as the lamp again, the glow is looked for over a strip (a point can fall between slivers), and thick air lights the space round a bulb but not the whole sky.
The lamps scatter into halos in the wet air (a haze slider), a lamp gets one halo rather than two, windows get small ones, and the wet road shows every lamp as a shimmering column of slivers.
Two faults in how wet ground showed a lamp. The slivers made an accordion: evenly spaced, all alike and as wide as the light's reach, they laid regular stripes across half the road. A reflection on wet ground is a narrow column a few bulbs wide. It is now a soft narrow glow with bright slivers over it at uneven gaps and heights, set a little off the axis, each coming and going on its own. And drops did nothing to it: the lighting drew the lights' reflections apart from the water, so ripples bent the mirrored street but not the lamp in it. A LightMirror now says how its own movement shifts what it mirrors at a point (disturbanceAt, zero on still water). Every sliver moves by it and catches more light where the surface tilts, and the mirrored source is drawn in thin slices, each moved as the water under it moves - a ring passing through the lamp tears it apart. Test: water that shifts its mirror 10 to the right moves the mirrored lamp's core 10 to the right.
WaterSurface reports its ripples through disturbanceAt: the water shader wave (newest rings, flattening, wavelength, amplitude) worked out on the CPU for a point, so the lighting moves the lamps reflections exactly as the shader moves the mirrored street. Tested: nothing on still water, a shift near a fresh ring, nothing far from it.
The lights' reflections were drawn by the lighting on its own, apart from the water: a column of slivers faked over the surface. However it was tuned it was not a reflection - drops that bent the mirrored street did nothing to the lamp in it, and the slivers read as an accordion. What gives light itself is now its own pass. Emissive parts (a light's source, the halo haze makes round it, anything else that glows) are drawn by the Lighting over the night, undimmed, after it has cut the lights out of the dark and added their cast colour. Then every LightMirror is handed that glow and draws it mirrored the way it draws any reflection, so whatever the surface does - ripples, roughness - happens to the lamps too. LightSource is Emissive: its source disc and its haze halo. The slivers, the sliced core, disturbanceAt and the mirror's line, squash, strength, stretch and clip are gone; LightMirror is one method now, renderMirroredGlow. Tests: still water (a plain mirror in the test) shows the lamp nearly as bright as the lamp, and nothing beside it.
WaterSurface draws the world's glow as it draws the world: mirrored about its line into an image and through water.frag, added over the night at its reflectivity times a grazing factor (water seen low across mirrors most of the light) times glowGain. The rings bend a lamp's reflection exactly as they bend the street's. The shader gains a vertical smear, streak: a rough surface mirrors each point over a range of heights, mostly above it, so a lamp runs down a wet road towards the viewer - and since every row is bent by the ripples on its own, the streak breaks where drops land. The mirrored street takes a tenth of it, a soft mirror. A lamp is far brighter than the screen's white, so a streaking surface can raise glowGain to keep the thin smear bright; the lamp's own core clips to white, as in a photo. Tests: still water shows the lamp as a lamp and no streak under it, a drop next to it bends it, a rough surface smears it down narrowly.
The wet road is rough (streak 70, glowGain 10): every lamp and lit window runs down it into a streak the drops break. The second puddle lies under the right lamp, where its reflection falls, and shows the lamp. A smoke test renders the rainy night for three seconds; with WET_WORLD_FRAME=<path> it saves the last frame to look at.
The shader smeared a rough surface's reflection with 14 lookups down the column. A lamp a few pixels tall, sampled at steps as long as itself, came out as a chain of beads - copies of the lamp - under every light and window. The smear is now a real vertical blur (ImageFilter.blur, sigma a third of streak) on the mirror before the shader sees it: one continuous column of light the ripples then bend. The shader only ripples now. A test walks down the streak and finds it only ever fades.
With the blur the road smear is streak 45 and glowGain 5: a soft warm column under each lamp and window, torn at its edges by stronger road ripples (3.5).
Only fresh rings bent a reflection, and they rarely crossed a lamp's streak, so it hung still in pouring rain. Rain keeps the whole surface astir: chop is a restless wave everywhere in the water shader, mostly sideways and changing fastest from row to row, so a reflection breaks into wavering bands. The shader also takes the 32 newest rings, not 16. And one physics for all water: a wet road mirrors as much as a puddle, only rough, so the same streak smears the street and the lamps alike (the street took a tenth of it), and glowGain is the lights' own brightness over white, 4 for every surface, not a per-surface knob.
The wet road is water like the puddles (reflectivity 0.6), differing only in roughness; both are kept astir by the rain in proportion to how hard it rains, and the road rings are bigger and stronger. Both lamps now reflect the same way: a streak broken into wavering bands, a bright lamp where a puddle lies under it.
A puddle and asphalt under a film of water are the same thing with more or less water in it. WaterSurface.depth (0 a film, 1 a puddle) now sets the rings drops leave (RippleRings.forDepth: radius 6 to 24, life 0.3 to 0.7 s), how much they bend the reflection and how plainly they show: rings everywhere, wide and slow in a puddle, small and quick and faint on a film. And a puddle no longer ends on a hard ellipse. With edgeSoftness (0.35 of its radius by default) it is drawn in a layer and faded out towards its rim by a radial mask over its whole rectangle; its mirrored glow too, in an additive layer so the lamp keeps its brightness. A rect (a road) keeps its hard edge. Tests: deeper water leaves wider longer rings; a puddle is bright in its middle, thinner near its rim, empty past it.
The road film is depth 0.1 and the puddles the default 1, instead of ring settings of their own; the puddles fade out at their rims.
Rings on the road could not be seen: the small faint strokes were drawn with the water, under the night, which darkened them to nearly nothing. A ring's crest is a glint - it mirrors whatever light is round it. Under a Lighting the rings are now drawn with the mirrored glow, over the night, each as bright as the light where it lies (Lighting.lightAt) plus a little sky: bright by a lamp, faint but there in the dark. Without a Lighting they are drawn with the water as before. A film's rings are a little bigger (radius 9 at depth 0) and less muted.
The road film's rings are drawn in a denser white, so the small rings drops leave on the asphalt show all along it.
Rain wets more than the road: walls and roofs glint where the lights fall on them. Glossy is a wet surface that is no mirror - rough and upright, it does not show the street. The Lighting lays a sheen of every light over its glossArea, as strong as its gloss, and water running down it in short rivulets that start anywhere down the wall, each as bright as the light where it is, with a bead of water sliding down that catches more of it. Test: a lamp lights both sides of it alike, but the wet side glints brighter.
The house fronts are wet (Glossy, gloss 0.55): the lamps glint off them and water runs down them. Rain breaks on their roofs and lit window sills too: a far drop - falling by the fronts - that crosses a ledge bursts there, and every splash is lit like the drops.
The rivulets on wet walls read as bright lines with beads running down them. Water on a wall at night is barely seen: each is now a damp trail - a wide blurred band fading in and out along its length, only just lighter than the wall, breathing slowly - and the beads are gone. Still only where light falls on the wall.
A wall's sheen and damp trails, and every lamp's and window's glow, were drawn by the Lighting over everything, so a walker in front of a house had the water on the wall and the window's halo painted over him. They belong to their surfaces. A Glossy component now draws its sheen itself at the end of its render (renderSheen, which asks the world's Lighting through Lighting.of), and a LightSource draws its source and halo in its own render - so what stands in front hides them. The night is cut round every halo as round every light, so the glow is not dimmed; the Lighting still hands the glow to the mirrors. Emissive components are to call renderEmissive from their render.
The houses draw their own sheen after their fronts, so the walker in front of them hides it.
A torch pointed at the ground lights a patch of it, stretched along the beam; a lamp lights a wide one round its foot. The lights here lit only the air in their cones, and the road under them stayed dark. Lighting.floor is the ground seen from the side: a band whose top edge is the line things stand on, going away from the viewer. For each cone that reaches it, the stretch of ground lit is worked out from the light's height and the angles of the cone's edges (an edge that points up or runs out first ends at the light's reach), and an oval of light is laid over it on the band, cut out of the night and warmed like the light itself, fading with the way the light went. Test: a torch held low and tilted down lights the ground ahead of it, not behind.
The rainy night's road is the lighting's floor, so the lamps lay pools of light under them and the torch one ahead of the walker; the torch reaches 380 so its pool is not lost to the falloff. The smoke test can look at any moment with WET_WORLD_SECONDS.
What the lighting did for a scene was set up in the scene: the floor for pools by hand, a wall's sheen by a call from its render, a torch aimed each frame by its holder. Those are properties of the elements. Ground marks the ground seen from the side; the lighting (and the rain) find it themselves - Lighting.floor stays only as an override. Glossy draws its sheen right after its component, with nothing to call. Facing says which way a component faces, and Torch - a LightSource in a hand - turns with a Facing holder, mirroring where it is held and where it points. Lighting.of finds a world's lighting. Tests: a torch turns round with its holder and changes hands; a torch's pool lands on a Ground element without a floor given.
The rain, where drops land, how water stirs and how wet things get were all written into the sample scene. They belong to the elements, so a game only puts them together. Rain falls over the camera's view as real rain does (RainDrops, the game's drop physics: Marshall-Palmer sizes for the intensity, Atlas terminal speeds, drag response to the wind, a streak per exposure) at metre world units per metre. Seen from the side each drop falls at a depth: onto the world's Ground that far towards the viewer, faster on screen the nearer (speed goes as one over distance), or behind the street line, where only roofs see it. It is lit by the world's Lighting. RainCatcher is what rain lands on: each catcher says where it stops a drop and what the drop does to it; the first on a drop's way takes it, and of two at one height the one with more water. WaterSurface catches drops at their depth where it covers the ground and rings them. Wettable gets wet in the rain and dries after it, not under cover. WaterSurface.chopPerRain stirs the water as hard as it rains; a film (the water on a road) mirrors only as much as it is wet. Tests: rain lands in a puddle over the road, on the road beside it and on a roof behind the street line; things get wet, dry, stay dry under cover; the chop follows the rain; the drop physics tests from the game.
The samples carried their own rain, the landing of drops (landOn), the roofs and sills, the chop worked out from the rain, the floor for the light pools and the aiming of the torch. They are elements now: the road is the Ground, the road water a film that stirs with the rain, the houses get wet, glint as wet as they are and catch the rain behind the street line on their roofs, the walker faces a way and carries a Torch, and Rain comes from flame_water. A scene is a list of them.
How wet the world gets was all or nothing: under rain everything went to streaming. Wettable.wetnessCap now says how wet a thing gets in this weather, and a puddle holds water as wet as the ground is - a damp street has small pools with a soft rim, a soaked one wide ones. A film still mirrors as much as it is wet, and the rings of drops fade with the water they fall into. The rim mask covers the whole surface in the shrunk pool's scale, so nothing drawn outside the pool is left. Tests: wetness stops at the cap; a pool shrinks as the ground dries.
The rainy night was always soaked. A slider takes it from barely damp to streaming: it sets how wet every Wettable in the scene gets.
A water surface brings the glow it mirrors back glowGain times brighter, since a lamp's source is brighter than an image's white. It brought the halo round the lamp back that much brighter too, though the light the haze scatters is no brighter than it looks. Wherever a lamp's image fell, the water burnt out white over the halo's whole width: a small drying puddle under a lamp became a white disc, like a spotlight on the road. The mirror now tells the lighting how much it will brighten (headroom), and the lighting draws what the air scatters - the halo, the beam in the wet air - that much fainter. Brought back, the source comes out brighter than white and the halo as bright as it is: a puddle shows the lamp as a bright spot in a dim glow. Tests: with headroom the mirrored halo is fainter, the bulb the same.
A drying puddle shrank only to a third of its size: on dry ground it still lay there, lit by the lamp over it. Its area now goes with the water in it - across it shrinks as the square root of its wetness - so on barely wet ground it is a small patch and on dry ground it is gone, catching no drops and drawing nothing. Tests: a dry puddle has no outline and covers nothing.
How wet a thing got was capped by hand (wetnessCap): the rain soaked it up to the cap however hard it rained, and it dried at one fixed rate. Wetness is now the water a thing holds. Rain brings water on at wetRate times the intensity, the more slowly the fuller it is; water leaves at dryRate times the weather's Rain.drying, the faster the more there is. A steady rain holds it where the two meet - a downpour near soaked, a drizzle less - and without rain it drains to nothing at the weather's pace. wetRate and dryRate are each element's own, so a wall sheds its water first and a puddle keeps it longest. The cap is gone. Tests: a drizzle and a downpour hold a wall at their balance; without rain it dries as fast as the weather says, and not at all at drying 0; a puddle outlasts a wall.
Every change of a property built the street anew, so it could never be seen drying: stopping the rain started a fresh, soaked scene. The story keeps one street running and changes its weather as it goes. A drying speed joins the sliders; ground wetness wets everything that much at once. Stop the rain and the walls dry first, the asphalt next, the puddles last.
That a puddle outlasts the asphalt was set by hand on the sample's puddle: a WaterSurface made anywhere else dried as fast as a film. Water now dries as long as it is deep: WaterSurface.dryRate comes from its depth - a film (0.1) at the usual 0.02, a puddle (1) ten times as slowly - unless set outright. The sample's puddle no longer sets it. Tests: a film dries as the asphalt, a puddle ten times as slowly, and a rate set outright wins.
On Impeller (macOS, and iOS on the same Metal backend) the rainy night drew four frames and froze: the raster thread waited for good on a new Metal command buffer. Each damp trail on a wet wall was a rect with a mask blur added with plus; inside a reflection's toImageSync every one of them took a blur pass of its own, and a wall of some seventy trails used up the command queue within a snapshot. A trail is now soft all round by its own radial gradient, an ellipse fading from its middle out: the same blurred, barely-there streak, drawn in one pass. The scene runs on macOS at about 50 fps.
Several things were set by hand where the angle of view decides them. Ground now says how the ground lies before the eye - how deep the band really is (depthSpan), how high the eye is (eyeHeight), how far the nearest ground is (nearDistance) - and from that the angle each depth is seen at (sinElevation) and how much still water mirrors there (waterReflectance, Fresnel by Schlick's approximation for water). A cone's pool on the ground is worked out, not drawn as an oval: a new shader (LightingShader, shaders/pool.frag) lights every point of the ground band by the light that reaches it in 3D and the cosine it falls at - round under a lamp, a strip along the street ahead of a torch. Until the shader is loaded the oval stays. Rain is seen by the light it scatters to the eye: scatteredLightAt weights each light by the Henyey-Greenstein phase of a drop between it and the eye (g 0.7), so a drop in front of a lamp flares and one behind it dims. LightSource.lightAt takes how far in front of the light a point is, so a window no longer lights a drop metres before it. Glossy has a darkening: a wet wall is darker under whatever light then falls on it. Damp trails draw nothing new. Tests: a lamp's pool is brightest under it and fades along the street and towards the eye; a torch's pool lies ahead along the street line; a drop in front of a lamp outshines one beside it, one behind dims.
Water mirrored as much as a hand-set reflectivity said, times a hand-set grazing boost, evenly from top to bottom; a rough surface smeared every point into the same thin streak; everything was mirrored about one line; every drop that landed splashed; a wet film mirrored as soon as it was damp. - Fresnel: through the shader each row of water mirrors as much as water does at the angle it is seen at (from the world's Ground) - most far off, little near. reflectivity now says how much of the surface is clear water (1 still water) and grazing is gone. glowGain is 10: a lamp at night is that much brighter than the screen's white, which is what keeps its reflection bright at a third of the light mirrored. - A rough surface's streak is as wide as its length times the sine of the angle of view: a thin line far off, rounder near. Taps outside the mirror read nothing, so its clamped edge no longer runs on as a line. - A Reflectable may say where it stands (reflectionBase); water mirrors it about that. Drops and their droplets are mirrored about where they fall (mirrorShift). - Wettable has a porosity: a porous thing darkens as its pores fill (wetDarkening, to a fifth of its colour at full porosity) and shines only once a film forms (wetGloss), which goes first as it dries. A film darkens the ground under it and mirrors only as it shines. - Rain: a streak is as bright as the light it scatters to the eye times its exposure (diameter over speed); a drop splashes only above the impact number K = We^0.5 Re^0.25 = 57.7, more droplets the harder. Tests: a drop at 1 mm splashes and one at 0.5 mm does not; something standing lower is mirrored about where it stands.
The rainy night loads the pool shader; the houses are brick and plaster (porosity 0.6) and darken and shine by their wetness; the road's film lies on asphalt (0.4); puddles are clear water (reflectivity 1), the film 0.85; a street lamp reaches 420, some metres of street round it, so its light reaches the ground.
Every drop asked the lighting how lit it was twice a frame, and each ask walked the whole world for its lights; Lighting.of, Ground.of and Rain.of walked it too, from every light, surface and drop. On macOS (profile, Impeller) the rainy night spent 26 ms of Dart a frame. WorldLookup finds the one component of a kind in a world and remembers it until it is unmounted; Lighting, Ground and Rain find themselves through it. The lighting gathers its lights and mirrors once a frame. The same scene now spends 4 ms of Dart a frame. examples/tool/wet_bench.dart runs the rainy night alone and prints frame timings (flutter run -d macos --profile -t tool/wet_bench.dart).
Each cone - a lamp's, a torch's - was a disc masked by a sweep inside a layer the size of its reach, up to 840 square, once to cut the night, once to light it and once in every water's reflection. On Impeller every such layer is an offscreen target. A second shader (shaders/light.frag, loaded with the pool one by LightingShader.load, which now takes the shaders' directory) works out the same falloff and soft edge per point: one draw, no layer. Until it is loaded the layer stays.
Water opened a layer for its soft rim, another for how much it mirrors, a blur layer inside each mirrored image and a fade mask; its glow the same again. The rain was a drawLine per drop and a drawPoints per droplet, each asking the lighting for every drop, again in every reflection. - The water shader now does it all in one draw: the smear (a sigma down, taps a fraction of it apart over an image rendered no finer than the smear needs, read with linear filtering), the water or wet ground under the mirror, the tint, the fade below the line and the rim. The plain mirror keeps the layers. - The rain works out each drop's look once a frame (dropLightAt: what it scatters, how lit it is and its colour in one pass over the lights) and draws every streak and droplet as one batch of vertices, each streak a strip full down its middle and clear at its edges. On macOS (profile) the rainy night went from 26 ms of Dart and 11 ms of raster a frame to 1.9 and 6.7 over this and the lookup change before.
On a Pixel 9 (Impeller on Vulkan) every frame with the lighting was lost: the screen kept a bare street, with no night, no rain and no water in it, while the same scene drew whole on macOS. Without the lighting's shaders, or with only the cone one, the frame was whole again. The pool shader declared a vec2 right after a lone float (uLight, uHeight, uAxis). Flutter sets uniforms as floats in order; on Vulkan that vec2 is laid out at the next 8-byte boundary, so the two disagree. Its uniforms are now all floats, and the angle off the beam comes from atan of the cross and dot products instead of acos. Checked on the Pixel 9: the night, the cones, the pools on the road, the rain and the reflections all draw.
…the world Three rules lived in the sample's elements instead of the fork's: - Water mirrored about its own top edge unless told the line things stand on; every scene set waterLine by hand. It now takes the top of the world's Ground when not told otherwise. - A wet wall tied its gloss and darkening to its wetness by hand. The WetSheen mixin does it for anything Glossy and Wettable: it darkens as its pores fill and shines up to soakedGloss once a film forms. - A house refused rain falling in front of it in its catchDrop. RainCatcher.frontDepth says how near the eye a catcher's front stands; the rain passes nearer drops by. Tests: the rain tests, unchanged, still land drops on the roof only from behind the street line.
The scene still drew a lamp's glow by hand when it was drawn in water, from before lights showed in mirrors on their own; it is gone. The houses use WetSheen and frontDepth, and the water takes its line from the road. The water no longer squashes what it mirrors (squash 0.6): a still puddle mirrors full height. The lamps, about 135 above the street line, are mirrored about 135 below it, past the bottom of the view; the windows, the walker and the posts show taller. The mirror puddle story still has squash as a slider, now from 1.
feat: lighting and rain roles for the wet world
What stood where in a side-view scene was worked out apart in each place: the rain had its own perspective (1 / (1 - 0.6 depth)) and its own landing line with pixel insets, the water its own, and a game laying things out in depth (Umbrella's 2.5D road) a third. Ground now holds the one projection everything shares. Everything stands at a depth - below 0 behind the street line, 0 on it, 0..1 on the ground towards the eye - and from it follow how far it is (distanceAt: rows are equal steps of 1 / distance, crowding towards the line), where it meets the ground (yAt, depthAt), how big and how fast it slides by (scaleAt, projectX) and the angle the eye looks down at it (sinElevation, now from the same distances). Tests: the projection's places, sizes, parallax and crowding.
The rain and the water now take the world's projection (Ground) instead of their own: a drop lands where its depth meets the ground (yAt) and crosses the view as fast as that depth makes it (scaleAt); water catches it there. Without a ground the old perspective stays. The rain's margins past the view are a metre, not 40 world units, so it works in metres as in pixels. Reflectable.groundDepth: something standing at a depth is mirrored about where that depth meets the ground, without working out its base itself. Tests: a block at a depth is mirrored about its spot on the ground.
feat: one depth projection for the street
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.
Reflections and light are the main look of the Umbrella game. This branch builds them into the fork: a wet night street made of elements. Lights, rain, water and wet surfaces are roles that components take on. The physics belongs to those roles; a scene only puts elements together and sets their values.
Includes #2 (
feat/lighting) and #3 (feat/street-depth), merged into this branch.Examples build fix
On Flutter 3.47, Dashbook takes
ThemeDatafrommaterial_ui, so the examples app did not compile. The fix is carried over from upstream76c8e81a6(flame-engine#4066).flame_lighting (new package)
Lighting: the night over a world.LightSourcecuts its cone and halo out of the dark and adds its colour.Ground.Emissivepart and the light hanging in wet air go to eachLightMirror.LightSource: a point or cone light with a soft edge, reach, flicker, haze halo and the size of its source.Torch: a cone held by aFacingholder. It turns with the holder.Groundholds the ground band and how it is seen:depthSpan,eyeHeight,nearDistance,sinElevation. It also computes water's Fresnel reflectance from these.Glossy: a wet wall's sheen, damp trails anddarkening, drawn right after the component itself.scatteredLightAt/dropLightAt: how much light a drop sends to the eye. Uses a Henyey–Greenstein phase, so a drop in front of a lamp flares and one behind it dims. Light from a source reaches a drop by its 3D distance.WorldLookup: finds a world'sLighting,GroundorRainonce and remembers it.LightingShader.load). Until they load, a layer or oval is drawn instead.light.frag: a cone in one pass.pool.frag: a pool, computed per point of the ground from the light that reaches it and the angle it falls at.Depth projection (#3)
Groundholds the one projection a side-view scene shares. Everything stands at a depth: below 0 behind the street line, 0 on it, 0..1 on the ground towards the eye. From the depth:distanceAt: rows are equal steps of 1/distance;yAt/depthAt: where the thing meets the ground;scaleAt/projectX: its size and parallax;sinElevation: the angle of view.RainandWaterSurfaceland and mirror by it.Reflectable.groundDepthmirrors a thing about its spot on the ground.flame_water
New package in the monorepo.
Reflectablemarks what water mirrors.ReflectionPasstells a component it is being drawn as a reflection.RippleRings: rings from drops. The water shader bends the mirror around the newest ones.Rain: drops over the camera's view at a depth from the street line to the eye.RainCatcher: what rain lands on.frontDepth: drops falling in front of something upright pass it by.Wettable: wetness is the water an element holds.dryRate×Rain.drying.porositydriveswetDarkening(the element darkens first) andwetGloss(it shines once a film forms).WetSheenties these toGlossy.WaterSurfaceis aLightMirror, aRainCatcherandWettable. Throughwater.frag, in one draw:Groundline;Reflectableabout its ownreflectionBase, and each drop about the spot it falls to;reflectivityis now how much of the surface is clear water.glowGain(10) is how much brighter than white a lamp is.examples
The "Wet world" chapter:
tool/wet_bench.dartprints frame timings.--dart-define=BENCH_WITHOUT=lighting,water,rainleaves systems out.Frame timings (profile)
On the phone the cost is Dart: every water surface re-records the world for its mirror and for its glow.
Tests and devices
flame_lighting: 15 tests,flame_water: 28. Most check pixels: pools, cones, halo headroom, drop scattering, splash threshold, mirroring about an element's own base, drying.flutter test.Impeller traps
MaskFilter.blurdraws inside onetoImageSyncuse up Metal's command queue. The frame then freezes for good.vec2uniform right after a lonefloatloses the whole frame. The fix is to keep runtime-shader uniforms as scalars.toImageSync(targetFormat: rgbaFloat32)clamps values at 1.flutter_gpufloat targets do not: verified on macOS and the Pixel 9.Follow-ups
serdun/1.38.2-1tag, then moving these packages into the game in place of itsRainLayerand puddles.flutter_gpu, with the canvas path kept for the web.