From 2e858e130f8ab49c6e32b92532d10d0a39da0d16 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 19:35:49 +0300 Subject: [PATCH 1/6] Shadow cascades under an orthographic camera split by depth The cascades were split by distance from the eye, half way to logarithmic, which is right through a perspective lens and wrong through an orthographic one: there every metre of depth gets the same texels on screen, and the eye is only where the camera was put, often tens of metres back. The near cascades covered air in front of the lens and every shadow came from the last, whole-level map. Under an orthographic camera the depths where the view box meets the casters' bounds, capped at ShadowSettings.viewDistance, are now cut into equal slabs, one per near cascade, each fitted to the corners of its own share of the box and the bounds. The last cascade is still the whole scene and the reach back towards the light is kept. ShadowFactor picks a cascade by depth along the view axis through that lens; the light shafts and the volumetric fog pick by the way along the ray from where depth is nought, which is the same distance as before through a perspective lens. The software backend mirrors all three. orthographic-shadow is in all four golden sets; the other ninety Impeller references still pass unchanged. --- ARCHITECTURE.md | 8 +- README.md | 4 +- packages/flutter3d/CHANGELOG.md | 3 + packages/flutter3d/README.md | 4 +- .../example/lib/src/spike/backend_web.dart | 2 +- .../example/lib/src/spike/golden_runner.dart | 2 +- .../example/lib/src/spike/golden_scenes.dart | 10 + .../example/lib/src/spike/golden_stages.dart | 50 +- .../lib/src/spike/golden_store_web.dart | 4 +- packages/flutter3d/test/ambient_test.dart | 2 +- packages/flutter3d/test/anisotropy_test.dart | 4 +- packages/flutter3d/test/auto_batch_test.dart | 4 +- packages/flutter3d/test/fog_test.dart | 2 +- .../test/goldens/orthographic-shadow.png | Bin 0 -> 24140 bytes packages/flutter3d/test/grade_test.dart | 2 +- packages/flutter3d/test/halation_test.dart | 2 +- .../test/irradiance_bounce_test.dart | 2 +- .../flutter3d/test/masked_shadow_test.dart | 2 +- .../test/material_vertex_stage_test.dart | 2 +- .../test/orthographic_cascade_test.dart | 228 + .../test/per_view_exposure_test.dart | 2 +- .../flutter3d/test/render_scale_test.dart | 2 +- packages/flutter3d/test/sharpen_test.dart | 2 +- .../flutter3d/test/sky_procedural_test.dart | 2 +- packages/flutter3d/test/ssao_blur_test.dart | 2 +- packages/flutter3d_core/CHANGELOG.md | 11 + .../engine/render/orthographic_cascades.dart | 184 + .../src/engine/render/render_settings.dart | 4 +- .../lib/src/engine/render/renderer.dart | 26 +- .../engine/render/renderer_frame_nodes.dart | 6 + .../src/engine/render/renderer_post_pass.dart | 4 +- .../engine/render/renderer_shadow_pass.dart | 28 +- .../formats/ktx2_supercompression_test.dart | 2 +- packages/flutter3d_cpu/CHANGELOG.md | 4 + .../lib/src/cpu_shaders_post.dart | 2 +- .../lib/src/cpu_shaders_reflections.dart | 4 +- .../src/cpu_shaders_shadow_directional.dart | 18 +- .../lib/src/cpu_shaders_volumetric_fog.dart | 7 +- .../flutter3d_cpu/lib/src/cpu_texture.dart | 2 +- .../flutter3d_cpu/lib/src/portable_log2.dart | 2 +- .../test/cross_backend_test.dart | 4 + .../test/goldens/orthographic-shadow.png | Bin 0 -> 18744 bytes .../test/orthographic_cascade_pick_test.dart | 105 + .../test/gpu_formats_test.dart | 2 +- packages/flutter3d_shaders/CHANGELOG.md | 5 + .../flutter3d_shaders/shaders/lib/shadow.glsl | 8 + .../shaders/post/light_shafts.frag | 10 +- .../shaders/post/volumetric_fog.frag | 6 +- packages/flutter3d_webgl/CHANGELOG.md | 3 + .../flutter3d_webgl/lib/engine_shaders.dart | 64 +- .../test/cross_backend_test.dart | 3 + .../test/goldens/orthographic-shadow.png | Bin 0 -> 24846 bytes .../test/lighting_models_test.dart | 2 +- packages/flutter3d_webgl/tool/golden_web.sh | 2 +- packages/flutter3d_webgpu/CHANGELOG.md | 3 + .../flutter3d_webgpu/lib/engine_shaders.dart | 4328 +++++++++-------- .../test/cross_backend_test.dart | 1 + .../test/goldens/orthographic-shadow.png | Bin 0 -> 24153 bytes site/content/core/architecture.md | 2 +- site/content/core/index.md | 2 +- site/content/core/rendering.md | 2 +- site/content/quickstart.md | 2 +- site/content/reference/packages.md | 2 +- site/content/reference/testing.md | 14 +- tool/ci.sh | 2 +- 65 files changed, 2994 insertions(+), 2229 deletions(-) create mode 100644 packages/flutter3d/test/goldens/orthographic-shadow.png create mode 100644 packages/flutter3d/test/orthographic_cascade_test.dart create mode 100644 packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart create mode 100644 packages/flutter3d_cpu/test/goldens/orthographic-shadow.png create mode 100644 packages/flutter3d_cpu/test/orthographic_cascade_pick_test.dart create mode 100644 packages/flutter3d_webgl/test/goldens/orthographic-shadow.png create mode 100644 packages/flutter3d_webgpu/test/goldens/orthographic-shadow.png diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2627251b2..0cec52f94 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2691,7 +2691,7 @@ storage. |---|---| | Style | `dart format` | | Analysis | `flutter analyze` clean across the workspace, no warnings | -| Unit tests | **11340 tests** across 43 packages and 10 applications | +| Unit tests | **11345 tests** across 43 packages and 10 applications | | Structure rules | 35, `dart run tool/structure.dart`, the first CI step | | CI | GitHub Actions over `tool/ci.sh`, on `ubuntu-latest`, with no graphics card | @@ -2731,7 +2731,7 @@ material parameter — is the case `doc/boundary-0.5.0.md` worked through for set belongs to whoever builds on the package rather than to the package itself. -**Golden render tests.** 90 scenes against **four complete independent +**Golden render tests.** 91 scenes against **four complete independent reference sets** — Impeller, the software rasteriser, WebGL2 and WebGPU — each held to zero differing pixels against its own set, with a per-channel tolerance of 8. @@ -3143,7 +3143,7 @@ metres. The directional light's cascades fit the view up to that distance and nothing beyond it casts — a level whose far end matters visually wants the number raised, and pays for it in texels. -**The web backend draws all ninety golden scenes the way Impeller does**, +**The web backend draws all ninety-one golden scenes the way Impeller does**, between 0.01% and 0.42% of pixels differing by more than 8 per channel — the silhouette's worth of disagreement two rasterisers always have. Two of those numbers fell when the minification filter learned to read a sampler's @@ -3259,7 +3259,7 @@ and charge each of them the bytes, and neither is a change to make on somebody else's behalf. A build that wants it says so in one flag, and the engine's own example takes the same answer from `?backend=webgpu` in the URL — a query parameter rather than a define, because the browser golden stand's whole saving -is one dart2js run serving ninety scenes and both browser backends. +is one dart2js run serving ninety-one scenes and both browser backends. **`flutter3d_shaders` is one text and no two backends take it the same way.** Impeller compiles the GLSL with `impellerc`; the WebGL2 generator translates it diff --git a/README.md b/README.md index d468a8dd9..e9fa9ee9d 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,9 @@ Or one package at a time: (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11340 tests across forty-three packages and nine applications. The +There are 11345 tests across forty-three packages and nine applications. The only ones that need a GPU are the Impeller half of the golden set. The other -half is rendered by the software backend, which is what makes 90 scenes +half is rendered by the software backend, which is what makes 91 scenes checkable in a headless run. Several steps run in a browser. `flutter test --platform chrome` covers the two diff --git a/packages/flutter3d/CHANGELOG.md b/packages/flutter3d/CHANGELOG.md index 95f48b367..dc09e261b 100644 --- a/packages/flutter3d/CHANGELOG.md +++ b/packages/flutter3d/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- The `orthographic-shadow` golden scene: posts down a long floor through an + isometric camera, the shadow cascades split by depth (`P7`). + - **`generatedMaterialPathFor`**: where the build hook writes a `.f3dmat`'s compiled bundle, `flutter3d_generated/.f3dshaders`. diff --git a/packages/flutter3d/README.md b/packages/flutter3d/README.md index 06f46228d..497762afa 100644 --- a/packages/flutter3d/README.md +++ b/packages/flutter3d/README.md @@ -104,7 +104,7 @@ What works today: `CUBICSPLINE` with authored tangents), slerped rotations, an `AnimationPlayer` with play/pause/seek/speed and once/loop/ping-pong, and the decoded node hierarchy rebuilt on instantiation so an animated parent carries its subtree; -- 1659 tests covering projection, scene, sorting, debug draw, raycasting, +- 1662 tests covering projection, scene, sorting, debug draw, raycasting, animation, skinning, lighting, render targets, BVH, LOD, glTF, OBJ and `.f3d`, plus a real frame drawn through `flutter3d_cpu`'s software rasteriser for the ones that need one, all without a GPU. The geometry the engine is written in @@ -322,7 +322,7 @@ lib/src/engine/assets/ glTF, OBJ and .f3d decoders, isolate loading, ca example/lib/ the demo, and the frame capture hook skills/ the conventions, as agent skills — see below bin/skills.dart what copies them into a project that uses this -test/ 1659 tests, all runnable without a GPU +test/ 1662 tests, all runnable without a GPU ``` The GLSL is not here. Every shader this package draws with lives in diff --git a/packages/flutter3d/example/lib/src/spike/backend_web.dart b/packages/flutter3d/example/lib/src/spike/backend_web.dart index 983e7134d..6e347face 100644 --- a/packages/flutter3d/example/lib/src/spike/backend_web.dart +++ b/packages/flutter3d/example/lib/src/spike/backend_web.dart @@ -13,7 +13,7 @@ import 'golden_store.dart'; /// /// Read from the URL, exactly the way the golden scene and the record/compare /// direction are read, and for exactly the same arithmetic: one dart2js run -/// serves ninety scenes because the scene is a query parameter, and one +/// serves ninety-one scenes because the scene is a query parameter, and one /// dart2js run serves the browser backends because this is one too. A /// `--dart-define` here would be a build per backend, which is the whole saving /// of the browser golden stand spent on a single word. diff --git a/packages/flutter3d/example/lib/src/spike/golden_runner.dart b/packages/flutter3d/example/lib/src/spike/golden_runner.dart index a04f2a839..caa5896ef 100644 --- a/packages/flutter3d/example/lib/src/spike/golden_runner.dart +++ b/packages/flutter3d/example/lib/src/spike/golden_runner.dart @@ -122,7 +122,7 @@ final class GoldenRunner { scene, // The store's answer wins where it has one. A desktop run takes the // direction from the environment and a browser run from the URL, for the - // same reason on both: one build has to serve ninety scenes in both + // same reason on both: one build has to serve ninety-one scenes in both // directions, and anything the compiler sees is another build. update: updateOverride ?? diff --git a/packages/flutter3d/example/lib/src/spike/golden_scenes.dart b/packages/flutter3d/example/lib/src/spike/golden_scenes.dart index 72e6f9e9f..6fafe846b 100644 --- a/packages/flutter3d/example/lib/src/spike/golden_scenes.dart +++ b/packages/flutter3d/example/lib/src/spike/golden_scenes.dart @@ -1264,6 +1264,16 @@ final List kGoldenScenes = [ configure: GoldenStages.orthographicMetalSettings, ), + // P7. Posts down a long floor through an orthographic camera, the shadow + // cascades split by depth across what it sees. + const GoldenScene( + name: 'orthographic-shadow', + source: 'Cube', + bloom: false, + ground: false, + stage: GoldenStages.orthographicShadow, + ), + // P6. `shadow-teapot` wiped at the middle: lit on the left, the shading // normal on the right. const GoldenScene( diff --git a/packages/flutter3d/example/lib/src/spike/golden_stages.dart b/packages/flutter3d/example/lib/src/spike/golden_stages.dart index 05a408b82..29a575b6d 100644 --- a/packages/flutter3d/example/lib/src/spike/golden_stages.dart +++ b/packages/flutter3d/example/lib/src/spike/golden_stages.dart @@ -1269,6 +1269,51 @@ abstract final class GoldenStages { fog: FogSettings(color: Vector3(0.7, 0.75, 0.8), density: 0.03), ); + /// `orthographic-shadow`: a floor eighty metres long running away from an + /// isometric camera forty metres back, with a row of posts and slabs down + /// it from the bottom of the frame to the top. Split by distance from the + /// eye, the near cascades covered air in front of the lens and every + /// shadow came from the whole-floor map; split by depth across what the + /// view sees, the posts at the top of the frame cast as sharp a shadow as + /// the ones at the bottom. + static Future orthographicShadow(GoldenStage stage) async { + final device = stage.device; + stage.sun.setLocalForward(Vector3(-0.6, -1.0, 0.35).normalized()); + final stone = Material( + baseColor: Vector4(0.62, 0.6, 0.56, 1.0), + roughness: 0.9, + ); + final post = Material(baseColor: Vector4(0.7, 0.3, 0.2, 1.0)); + return GoldenStaged( + nodes: [ + _slab( + device, + Vector3(10.0, 0.2, 80.0), + Vector3(0.0, -0.1, 30.0), + stone, + ), + for (var i = 0; i < 6; i++) ...[ + _slab( + device, + Vector3(0.3, 2.0, 0.3), + Vector3(-2.5, 1.0, i * 4.0), + post, + ), + _slab( + device, + Vector3(1.6, 0.2, 0.6), + Vector3(2.0, 1.8, i * 4.0 + 1.0), + post, + ), + ], + ], + everyFrame: (_, _) => stage.camera + ..projection = const OrthographicProjection(height: 14.0, far: 200.0) + ..setPosition(-14.0, 26.0, -26.0) + ..lookAt(Vector3(0.0, 0.0, 10.0)), + ); + } + // ------------------------------------------------------------------ P6 /// `debug-view-split`: the lit teapot left of the middle and its shading @@ -1276,10 +1321,7 @@ abstract final class GoldenStages { /// makes, and the composite leaving the right half out of the tone curve. static RenderSettings debugViewSplit(RenderSettings settings) => settings.copyWith( - debugView: const DebugViewSettings( - view: DebugView.normal, - split: 0.5, - ), + debugView: const DebugViewSettings(view: DebugView.normal, split: 0.5), ); // ------------------------------------------------------------------ R6 diff --git a/packages/flutter3d/example/lib/src/spike/golden_store_web.dart b/packages/flutter3d/example/lib/src/spike/golden_store_web.dart index 689c97aa2..863de3987 100644 --- a/packages/flutter3d/example/lib/src/spike/golden_store_web.dart +++ b/packages/flutter3d/example/lib/src/spike/golden_store_web.dart @@ -40,7 +40,7 @@ void reportLine(String message) { /// The scene named in the page's URL, if any. /// /// A run-time choice here, where the desktop path takes a compile-time define. -/// The reason is arithmetic: the suite is ninety scenes, and rebuilding +/// The reason is arithmetic: the suite is ninety-one scenes, and rebuilding /// the bundle for each is seventy-eight dart2js runs to compare seventy-eight /// pictures. One build and seventy-eight navigations is the same information /// in a fraction of the time. @@ -58,7 +58,7 @@ String? get sceneOverride { /// Whether this run records rather than compares, from the page's URL. /// /// A run-time choice for the same reason [sceneOverride] is one: the suite is -/// ninety scenes and rebuilding for each would be seventy-eight dart2js +/// ninety-one scenes and rebuilding for each would be seventy-eight dart2js /// runs. One build serves both directions, and the URL says which. bool get updateOverride => Uri.base.queryParameters['update'] == '1'; diff --git a/packages/flutter3d/test/ambient_test.dart b/packages/flutter3d/test/ambient_test.dart index 1ee55c99b..8d45cea89 100644 --- a/packages/flutter3d/test/ambient_test.dart +++ b/packages/flutter3d/test/ambient_test.dart @@ -141,7 +141,7 @@ void main() { // approximately, bit for bit. // // Mutation: default either end of the hemisphere to anything but white. - // Ninety goldens move at once. + // Ninety-one goldens move at once. final lit = await _pixels(_ball(), _noSky); final top = _band(lit, 0.15, 0.35); final bottom = _band(lit, 0.65, 0.85); diff --git a/packages/flutter3d/test/anisotropy_test.dart b/packages/flutter3d/test/anisotropy_test.dart index 1a195eb38..717bb8d90 100644 --- a/packages/flutter3d/test/anisotropy_test.dart +++ b/packages/flutter3d/test/anisotropy_test.dart @@ -24,7 +24,7 @@ /// which spends the taps and throws away what they bought — contrast drops /// *below* the trilinear baseline and the first assertion fails; and letting /// a sampler that asked for one reach the new path at all, which would move a -/// backend ninety golden scenes are recorded on. +/// backend ninety-one golden scenes are recorded on. /// /// **One they do not catch, said rather than hidden:** taking the taps along /// the *short* axis instead of the long one passes every line here. The @@ -213,7 +213,7 @@ void main() { }); test('asking for one tap is the old sampler exactly', () async { - // What makes this safe to add to the backend ninety golden scenes are + // What makes this safe to add to the backend ninety-one golden scenes are // recorded on. Not "close": the same bytes, because with `anisotropy` at // one the new path is not reached at all. expect(await _floorAt(1), await _floorAt(1)); diff --git a/packages/flutter3d/test/auto_batch_test.dart b/packages/flutter3d/test/auto_batch_test.dart index 1671e90c8..b35cdde90 100644 --- a/packages/flutter3d/test/auto_batch_test.dart +++ b/packages/flutter3d/test/auto_batch_test.dart @@ -22,7 +22,7 @@ /// three backends.** It computes in Dart doubles; Impeller, WebGL and WebGPU /// compute in 32-bit floats, where those two expressions have far less room /// before they part. Nothing here can run them, so the setting stays off by -/// default — which is also what keeps the ninety goldens where they are. +/// default — which is also what keeps the ninety-one goldens where they are. library; import 'dart:math' as math; @@ -191,7 +191,7 @@ void main() { }); test('nothing is batched unless it is asked for', () async { - // Which is what keeps the ninety goldens where they are: the default + // Which is what keeps the ninety-one goldens where they are: the default // frame is the frame it always was, to the byte. final device = _device(); final scene = _field(device, count: 100); diff --git a/packages/flutter3d/test/fog_test.dart b/packages/flutter3d/test/fog_test.dart index 3544d7373..9f0338c1e 100644 --- a/packages/flutter3d/test/fog_test.dart +++ b/packages/flutter3d/test/fog_test.dart @@ -151,7 +151,7 @@ void main() { test('no fog is byte-identical to the fog nobody asked for', () async { // The early return at zero density is not an optimisation. The golden sets - // — ninety scenes, zero-pixel threshold, three backends — are all + // — ninety-one scenes, zero-pixel threshold, three backends — are all // recorded with the default `FogSettings()`, and this is the property that // lets them stay recorded. final none = FogSettings(); diff --git a/packages/flutter3d/test/goldens/orthographic-shadow.png b/packages/flutter3d/test/goldens/orthographic-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..e911a446911c39f0733a8b926c896ad20d3a990b GIT binary patch literal 24140 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 HRoMRkqP55W literal 0 HcmV?d00001 diff --git a/packages/flutter3d/test/grade_test.dart b/packages/flutter3d/test/grade_test.dart index c1b9e67a2..3a323c538 100644 --- a/packages/flutter3d/test/grade_test.dart +++ b/packages/flutter3d/test/grade_test.dart @@ -63,7 +63,7 @@ Future> _ramp(LookSettings look, {int width = 256}) async { void main() { test('every default is an exact identity', () async { - // The promise ninety goldens rest on. Neutral for these three is + // The promise ninety-one goldens rest on. Neutral for these three is // (0,0,0) added, (1,1,1) as an exponent and (1,1,1) multiplied, and the // composite has to take all three without moving a byte. final plain = await _ramp(const LookSettings()); diff --git a/packages/flutter3d/test/halation_test.dart b/packages/flutter3d/test/halation_test.dart index f23c355d5..4a9d5d2ab 100644 --- a/packages/flutter3d/test/halation_test.dart +++ b/packages/flutter3d/test/halation_test.dart @@ -87,7 +87,7 @@ void main() { test('zero is an exact identity, and is the default', () async { // The multiplier is one on every channel at zero, so the chain comes out - // byte for byte as it did — which is what ninety goldens need. + // byte for byte as it did — which is what ninety-one goldens need. expect(const BloomSettings().halation, 0.0); final a = await _glow(halation: 0.0); final b = await _glow(halation: 0.0); diff --git a/packages/flutter3d/test/irradiance_bounce_test.dart b/packages/flutter3d/test/irradiance_bounce_test.dart index b86199775..6f3a64733 100644 --- a/packages/flutter3d/test/irradiance_bounce_test.dart +++ b/packages/flutter3d/test/irradiance_bounce_test.dart @@ -136,7 +136,7 @@ double _tint(Uint8List rgba) { void main() { test('off, the picture is the bytes it was', () async { // The clause every recorded frame depends on. A field is null by default, - // so this is the path ninety goldens take. + // so this is the path ninety-one goldens take. final a = await _draw(field: false, redWall: true); final b = await _draw(field: false, redWall: true); expect(a, orderedEquals(b)); diff --git a/packages/flutter3d/test/masked_shadow_test.dart b/packages/flutter3d/test/masked_shadow_test.dart index a5044f2fe..f6620e262 100644 --- a/packages/flutter3d/test/masked_shadow_test.dart +++ b/packages/flutter3d/test/masked_shadow_test.dart @@ -223,7 +223,7 @@ void main() { test('a caster with no map keeps the shadow it always cast', () async { // **The half that lets this land.** A material that is not cut out goes // through the stage it has always gone through, with no sampler in the - // pipeline and no texture bound per draw, so the ninety goldens + // pipeline and no texture bound per draw, so the ninety-one goldens // recorded against that stage cannot move. Asking for MASK with no map to // read is the same case: there is nothing to cut out. final plain = await _frame(masked: false, withTexture: false); diff --git a/packages/flutter3d/test/material_vertex_stage_test.dart b/packages/flutter3d/test/material_vertex_stage_test.dart index 406816c2d..fe2788cc4 100644 --- a/packages/flutter3d/test/material_vertex_stage_test.dart +++ b/packages/flutter3d/test/material_vertex_stage_test.dart @@ -172,7 +172,7 @@ void main() { // **The half that lets this land**, and the reason the seam is a null field // rather than a new required one: a material saying nothing about its vertex // stage goes through the pipeline it has always gone through, which is also - // why the ninety goldens cannot move. + // why the ninety-one goldens cannot move. final withSeam = await _draw(lighting: _plain); final device = CpuDevice( diff --git a/packages/flutter3d/test/orthographic_cascade_test.dart b/packages/flutter3d/test/orthographic_cascade_test.dart new file mode 100644 index 000000000..ca6deb344 --- /dev/null +++ b/packages/flutter3d/test/orthographic_cascade_test.dart @@ -0,0 +1,228 @@ +/// Directional shadow cascades under an orthographic camera — `P7`. +/// +/// flutter test test/orthographic_cascade_test.dart +/// +/// The cascades are split by distance from the eye, half way to logarithmic, +/// which is right through a perspective lens: a texel on screen covers more +/// world the further off it is. Through an orthographic lens every metre of +/// depth gets the same texels on screen, and the eye is only where the camera +/// was put along its axis — here forty metres back and forty up. Split from +/// there, the near cascades covered air in front of the lens and everything +/// the camera saw fell to the last cascade, the whole level in one map. What +/// has to hold instead: +/// +/// * the near cascade covers part of what the view sees; +/// * a caster at the bottom of the frame and one at the top each land in a +/// near cascade, of about the same size; +/// * the shadows stay where one whole-level map puts them. +library; + +import 'package:flutter3d/flutter3d.dart'; +import 'package:flutter3d/parity_scene.dart'; +import 'package:flutter3d_cpu/flutter3d_cpu.dart'; +import 'package:flutter3d_cpu/testing.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_math/vector_math.dart'; + +const int _width = 96; +const int _height = 72; + +({CpuDevice device, Renderer renderer}) _engine() { + final it = cpuTestDevice(width: _width, height: _height); + return ( + device: it.device, + renderer: Renderer.create( + device: it.device, + fallbackAlbedo: it.albedo, + fallbackNormal: it.normal, + ), + ); +} + +/// Where the two canopies stand: one at the bottom of the frame, one at the +/// top. +final Vector3 _nearCanopy = Vector3(0.0, 3.0, 8.0); +final Vector3 _farCanopy = Vector3(0.0, 3.0, 52.0); + +/// A floor a hundred and sixty metres long with a canopy held over it near the +/// bottom of the frame and another near the top, a sun across it, and an +/// isometric camera forty metres back and forty up. +({Scene scene, CameraNode camera}) _isometricRoom() { + final scene = Scene(); + final device = CpuDevice( + width: 4, + height: 4, + shaders: CpuShaderLibrary(builtinCpuShaders()), + ); + + MeshNode block(Vector3 size, Vector3 at, String name) => MeshNode( + DeviceMesh.upload(device, CuboidShape(size: size).build()), + Material( + name: name, + baseColor: Vector4(0.8, 0.8, 0.8, 1.0), + lighting: LightingModel.pbr, + ), + name: name, + )..setPosition(at.x, at.y, at.z); + + scene + ..add(block(Vector3(40.0, 1.0, 160.0), Vector3(0.0, -0.5, 64.0), 'floor')) + ..add(block(Vector3(10.0, 0.6, 6.0), _nearCanopy, 'near')) + ..add(block(Vector3(10.0, 0.6, 6.0), _farCanopy, 'far')) + ..add( + LightNode( + type: LightType.directional, + intensity: 1.1, + castsShadow: true, + name: 'sun', + )..setLocalForward(Vector3(-0.2, -0.95, 0.25)), + ); + + final camera = CameraNode() + ..projection = const OrthographicProjection(height: 30.0, far: 400.0) + ..setPosition(0.0, 40.0, -40.0) + ..lookAt(Vector3(0.0, 0.0, 30.0)); + return (scene: scene, camera: camera); +} + +Future> _grid( + ({CpuDevice device, Renderer renderer}) engine, + ({Scene scene, CameraNode camera}) room, + ShadowSettings shadows, +) async { + final frame = engine.renderer.render( + width: _width, + height: _height, + scene: room.scene, + views: [RenderView(camera: room.camera)], + settings: RenderSettings( + shadows: shadows, + bloom: const BloomSettings(enabled: false), + ), + ); + final pixels = await engine.device.readPixels(frame.frame); + expect(pixels, isNotNull); + return parityGrid(pixels!.buffer.asUint8List(), _width, _height); +} + +/// The cells that are floor lying in shadow, as `cascade_test.dart` counts +/// them. +Set _shadowed(List grid) => { + for (var i = 0; i < grid.length; i++) + if (grid[i] > 20 && grid[i] < 170) i, +}; + +/// Which cascade the shaders give [point], by the rule `shadow.glsl` follows +/// through an orthographic lens: by depth along the view axis, then on to the +/// next cascade while the chosen one's sphere does not hold the point. +int _cascadeOf(Renderer renderer, CameraNode camera, Vector3 point) { + final splits = renderer.debugCascadeSplits; + final centres = renderer.debugCascadeCentres; + final radii = renderer.debugCascadeRadii; + final depth = (point - camera.readWorldPosition()).dot(camera.readForward()); + final byDepth = depth > splits[1] + ? 2 + : depth > splits[0] + ? 1 + : 0; + return [ + for (var i = byDepth; i < centres.length; i++) + if (centres[i].distanceTo(point) <= radii[i]) i, + centres.length - 1, + ].first; +} + +void main() { + test('the near cascade covers what the view sees, not the air in front of ' + 'the lens', () async { + // Mutation: drop the orthographic branch in `_renderShadowMap`. The near + // cascade goes back to a sphere a few metres in front of the eye, thirty + // metres above the floor, holding no caster at all. + final room = _isometricRoom(); + final engine = _engine(); + await _grid(engine, room, const ShadowSettings(cascades: 3)); + + final centre = engine.renderer.debugCascadeCentres.first; + final radius = engine.renderer.debugCascadeRadii.first; + final bounds = room.scene.computeBounds(castersOnly: true); + final nearest = Vector3.copy(centre)..clamp(bounds.min, bounds.max); + expect( + nearest.distanceTo(centre), + lessThanOrEqualTo(radius), + reason: 'the near cascade, $radius m round $centre, holds no caster', + ); + expect( + radius, + lessThan(engine.renderer.debugCascadeRadii.last / 2.0), + reason: 'the near cascade is as large as the whole level', + ); + }); + + test( + 'casters at the bottom and the top of the frame get the same texels', + () async { + // Every metre of an orthographic view gets the same pixels, so a caster at + // the top of the frame deserves the texels one at the bottom gets. Both + // land in a near cascade, and the two are within a factor of two of each + // other. + // + // Mutation: drop the orthographic branch in `_renderShadowMap`. Both + // canopies are fifty metres and more from the eye, past both splits, and + // fall to the whole-level map. + final room = _isometricRoom(); + final engine = _engine(); + await _grid(engine, room, const ShadowSettings(cascades: 3)); + + final radii = engine.renderer.debugCascadeRadii; + final near = _cascadeOf(engine.renderer, room.camera, _nearCanopy); + final far = _cascadeOf(engine.renderer, room.camera, _farCanopy); + expect( + near, + lessThan(2), + reason: 'the near canopy is in the last cascade', + ); + expect(far, lessThan(2), reason: 'the far canopy is in the last cascade'); + expect( + radii[near] / radii[far], + inInclusiveRange(0.5, 2.0), + reason: 'cascades of ${radii[near]} m and ${radii[far]} m', + ); + }, + ); + + test('three cascades shadow the same things as one', () async { + // A fitted cascade must move nothing, only sharpen it. + // + // **No mutation of the fit makes this fail, and it says so** rather than + // naming one that does not fire. Spheres fitted round each slab's middle + // on the axis, half its depth across, were tried: wherever a near tile + // misses a fragment the shader falls through to the whole-level map, so + // the picture holds. What this guards is the opposite failure — a near + // tile that *does* hold a fragment and records the wrong thing there. + final room = _isometricRoom(); + final single = await _grid( + _engine(), + room, + const ShadowSettings(cascades: 1), + ); + final many = await _grid( + _engine(), + room, + const ShadowSettings(cascades: 3), + ); + + final wasDark = _shadowed(single); + final isDark = _shadowed(many); + expect(wasDark, isNotEmpty, reason: 'there was no shadow to compare'); + expect( + wasDark.difference(isDark).length, + lessThanOrEqualTo(2), + reason: 'the shadow left cells it used to cover: it moved', + ); + expect( + isDark.difference(wasDark).length, + lessThanOrEqualTo(2), + reason: 'the shadow reaches cells it did not before: it moved', + ); + }); +} diff --git a/packages/flutter3d/test/per_view_exposure_test.dart b/packages/flutter3d/test/per_view_exposure_test.dart index 2d53c9f2c..17e9426cc 100644 --- a/packages/flutter3d/test/per_view_exposure_test.dart +++ b/packages/flutter3d/test/per_view_exposure_test.dart @@ -157,7 +157,7 @@ void main() { // **The strongest form of "this costs nothing where it is not wanted".** // One view is the whole frame, so its rectangle is the whole histogram // and its exposure is the frame's. The composite takes its single-draw - // path, and ninety goldens stay where they are. + // path, and ninety-one goldens stay where they are. final off = await frame( const RenderSettings(autoExposure: AutoExposureSettings(enabled: true)), ); diff --git a/packages/flutter3d/test/render_scale_test.dart b/packages/flutter3d/test/render_scale_test.dart index 4ecb4c381..d2f0416ad 100644 --- a/packages/flutter3d/test/render_scale_test.dart +++ b/packages/flutter3d/test/render_scale_test.dart @@ -36,7 +36,7 @@ import 'package:vector_math/vector_math.dart'; void main() { test('one is the whole resolution, and is the default', () { - // Ninety goldens are recorded at the size they asked for. A default + // Ninety-one goldens are recorded at the size they asked for. A default // that shrank anything would move every one of them. expect(const RenderSettings().renderScale, 1.0); expect(_drawn(1.0), (width: 64, height: 64)); diff --git a/packages/flutter3d/test/sharpen_test.dart b/packages/flutter3d/test/sharpen_test.dart index 7cd991571..411c076b1 100644 --- a/packages/flutter3d/test/sharpen_test.dart +++ b/packages/flutter3d/test/sharpen_test.dart @@ -95,7 +95,7 @@ int _steepest(List row) { void main() { test('zero is an exact identity, and is the default', () async { - // Ninety goldens go through this pass. The shader returns the centre + // Ninety-one goldens go through this pass. The shader returns the centre // untouched at zero rather than running a kernel that rounds to nothing, // because "rounds to nothing" is a claim about the target's bit depth. expect(const AntiAliasSettings().sharpen, 0.0); diff --git a/packages/flutter3d/test/sky_procedural_test.dart b/packages/flutter3d/test/sky_procedural_test.dart index db1101498..8f75da215 100644 --- a/packages/flutter3d/test/sky_procedural_test.dart +++ b/packages/flutter3d/test/sky_procedural_test.dart @@ -167,7 +167,7 @@ double _mean(Uint8List rgba, {required int channel}) { void main() { _hardEdgedSun(); test('no sky is byte-identical to the sky nobody asked for', () async { - // The property ninety goldens rest on: a frame with no sky in it is + // The property ninety-one goldens rest on: a frame with no sky in it is // the frame this renderer has always drawn. // // Mutation: drop the early return. The sky is drawn into every scene that diff --git a/packages/flutter3d/test/ssao_blur_test.dart b/packages/flutter3d/test/ssao_blur_test.dart index a7735c48b..236d719ff 100644 --- a/packages/flutter3d/test/ssao_blur_test.dart +++ b/packages/flutter3d/test/ssao_blur_test.dart @@ -75,7 +75,7 @@ void main() { test('no taps is no pass, and is the default', () async { // The occlusion is already off by default, and this is a second // full-screen pass over it — so nothing pays for either until two things - // are switched on. Ninety goldens depend on the frame being what it + // are switched on. Ninety-one goldens depend on the frame being what it // was. expect(const AmbientOcclusionSettings().blurTaps, 0); expect(await _frame(blurTaps: 0), await _frame(blurTaps: 0)); diff --git a/packages/flutter3d_core/CHANGELOG.md b/packages/flutter3d_core/CHANGELOG.md index a99f05f5c..4c438fc1e 100644 --- a/packages/flutter3d_core/CHANGELOG.md +++ b/packages/flutter3d_core/CHANGELOG.md @@ -1,5 +1,16 @@ ## Unreleased +- **Shadow cascades under an orthographic camera split by depth across what + the view sees** — `P7`. They were split by distance from the eye, which an + orthographic camera puts tens of metres back, so the near cascades covered + air in front of the lens and every shadow came from the last, whole-level + map. Now the depths where the view box meets the casters' bounds, capped at + `ShadowSettings.viewDistance`, are cut into equal slabs, one per near + cascade, each fitted to its own share of the box; the last cascade is still + the whole scene, and the reach back towards the light is kept. + `Renderer.debugCascadeSplits` reports where they ended. + `orthographic-shadow` is in all four golden sets. + - **Caustics.** `ShadowSettings.caustics` (with `translucentCasters`, off by default) follows the sun's light through every caster with a volume — a transmission and a thickness. Each is drawn from the sun into two small diff --git a/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart new file mode 100644 index 000000000..007b2dd42 --- /dev/null +++ b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart @@ -0,0 +1,184 @@ +/// Where an orthographic camera's near shadow cascades go — `P7`. +/// +/// Split out of the shadow pass because it is geometry with no device in it: +/// which part of the world an orthographic view can see, cut into slabs along +/// its axis, and the sphere round each slab. A test can ask it directly. +library; + +import 'dart:math' as math; + +import 'package:vector_math/vector_math.dart'; + +/// One near cascade of an orthographic view: the depth along the view axis +/// where it ends, and the sphere its map is fitted to. +typedef OrthographicCascade = ({double end, Vector3 centre, double radius}); + +/// The [slabs] near cascades for an orthographic [viewProjection]. +/// +/// **Split evenly by depth, and fitted to what the view can see.** The split a +/// perspective camera gets runs from a metre in front of the eye, half way to +/// logarithmic, because through that lens a texel on screen covers more world +/// the further away it is. Through an orthographic lens every metre of depth +/// gets the same texels on screen, and the eye is only where the camera was +/// put along its axis — often tens of metres back, so the perspective split +/// spent the near cascades on air and dropped everything into the last and +/// softest one. Here the depths that matter are the ones where the view box +/// meets the casters' bounds, [boundsMin] to [boundsMax]; that range, capped +/// at [reach] metres past its start, is cut into [slabs] equal slabs, and each +/// slab's sphere is fitted to the corners of its own share of the box and the +/// bounds. An isometric camera over a long floor gets strips across the +/// screen, each a fraction of the level wide. +/// +/// Depth is measured as the shaders measure it — `ViewDepth` in `color.glsl`: +/// along [forward] from [eye]. [viewProjection] is in the engine's own +/// convention, depth nought to one; the view box is read off its rows, so a +/// projection with an offset or a tilt is handled the same as a centred one. +/// +/// When the view sees none of the bounds, the slabs are cut from the view box +/// alone: nothing in it casts, and any fit is as good as another. +List orthographicCascades({ + required Matrix4 viewProjection, + required Vector3 eye, + required Vector3 forward, + required Vector3 boundsMin, + required Vector3 boundsMax, + required int slabs, + required double reach, +}) { + final view = _viewBox(viewProjection); + final bounds = <_HalfSpace>[ + (normal: Vector3(1.0, 0.0, 0.0), offset: -boundsMin.x), + (normal: Vector3(-1.0, 0.0, 0.0), offset: boundsMax.x), + (normal: Vector3(0.0, 1.0, 0.0), offset: -boundsMin.y), + (normal: Vector3(0.0, -1.0, 0.0), offset: boundsMax.y), + (normal: Vector3(0.0, 0.0, 1.0), offset: -boundsMin.z), + (normal: Vector3(0.0, 0.0, -1.0), offset: boundsMax.z), + ]; + double depthOf(Vector3 point) => (point - eye).dot(forward); + + final seen = _corners(<_HalfSpace>[...view, ...bounds]); + final volume = seen.isEmpty ? view : <_HalfSpace>[...view, ...bounds]; + final corners = seen.isEmpty ? _corners(view) : seen; + final depths = corners.map(depthOf); + final start = depths.fold(double.infinity, math.min); + final far = depths.fold(double.negativeInfinity, math.max); + // A sliver at least, so a view that meets the bounds in a plane still has + // slabs with an inside. + final end = math.max(math.min(far, start + reach), start + 1e-3); + + return [ + for (var i = 0; i < slabs; i++) + _fit( + volume, + eye: eye, + forward: forward, + from: start + (end - start) * i / slabs, + to: start + (end - start) * (i + 1) / slabs, + ), + ]; +} + +/// `normal · p + offset ≥ 0` for every point p inside. +typedef _HalfSpace = ({Vector3 normal, double offset}); + +/// The six half-spaces of an orthographic view box, from its matrix's rows: +/// clip x and y between −w and w, z between nought and w. +List<_HalfSpace> _viewBox(Matrix4 m) { + _HalfSpace row(double sx, double sy, double sz, double sw) { + // Row r of the matrix is (m[r], m[r + 4], m[r + 8], m[r + 12]). + final s = m.storage; + double at(int column) => + sx * s[column * 4] + + sy * s[column * 4 + 1] + + sz * s[column * 4 + 2] + + sw * s[column * 4 + 3]; + return (normal: Vector3(at(0), at(1), at(2)), offset: at(3)); + } + + return <_HalfSpace>[ + row(1.0, 0.0, 0.0, 1.0), + row(-1.0, 0.0, 0.0, 1.0), + row(0.0, 1.0, 0.0, 1.0), + row(0.0, -1.0, 0.0, 1.0), + row(0.0, 0.0, 1.0, 0.0), + row(0.0, 0.0, -1.0, 1.0), + ]; +} + +/// The slab of [volume] between depths [from] and [to], as a sphere. +OrthographicCascade _fit( + List<_HalfSpace> volume, { + required Vector3 eye, + required Vector3 forward, + required double from, + required double to, +}) { + final corners = _corners(<_HalfSpace>[ + ...volume, + (normal: forward, offset: -(from + eye.dot(forward))), + (normal: -forward, offset: to + eye.dot(forward)), + ]); + if (corners.isEmpty) { + // Only a degenerate box gets here; a point on the axis is still a sphere + // the shader can fall through. + return ( + end: to, + centre: eye + forward.scaled((from + to) * 0.5), + radius: math.max((to - from) * 0.5, 1e-3), + ); + } + final low = corners.fold(Vector3.all(double.infinity), (a, b) { + Vector3.min(a, b, a); + return a; + }); + final high = corners.fold(Vector3.all(double.negativeInfinity), (a, b) { + Vector3.max(a, b, a); + return a; + }); + final centre = (low + high)..scale(0.5); + final radius = corners.fold( + 0.0, + (r, corner) => math.max(r, corner.distanceTo(centre)), + ); + return (end: to, centre: centre, radius: math.max(radius, 1e-3)); +} + +/// The corners of the convex volume [planes] bound: every point where three +/// of them meet that lies inside the rest. +/// +/// **Every triple rather than a clipped polygon.** Fourteen planes are 364 +/// triples, a few microseconds once a frame, and there is no face list to keep +/// consistent as each plane cuts — the bookkeeping that loses a corner where +/// two cutting planes meet inside the box. +List _corners(List<_HalfSpace> planes) { + final scale = planes.fold(1.0, (s, p) => math.max(s, p.offset.abs())); + final tolerance = 1e-6 * scale; + final found = []; + for (var i = 0; i < planes.length; i++) { + for (var j = i + 1; j < planes.length; j++) { + for (var k = j + 1; k < planes.length; k++) { + final point = _meet(planes[i], planes[j], planes[k]); + if (point == null) continue; + if (planes.every( + (p) => p.normal.dot(point) + p.offset >= -tolerance * p.normal.length, + )) { + found.add(point); + } + } + } + } + return found; +} + +/// Where three planes meet, or null when two of them are parallel. +Vector3? _meet(_HalfSpace a, _HalfSpace b, _HalfSpace c) { + final bc = b.normal.cross(c.normal); + final determinant = a.normal.dot(bc); + final size = a.normal.length * b.normal.length * c.normal.length; + if (determinant.abs() <= 1e-9 * size) return null; + // Cramer's rule for n·p = −offset, in its vector form. + final ca = c.normal.cross(a.normal); + final ab = a.normal.cross(b.normal); + return (bc.scaled(-a.offset) + ca.scaled(-b.offset) + ab.scaled(-c.offset)) + ..scale(1.0 / determinant); +} diff --git a/packages/flutter3d_core/lib/src/engine/render/render_settings.dart b/packages/flutter3d_core/lib/src/engine/render/render_settings.dart index 002421bff..f93e22c43 100644 --- a/packages/flutter3d_core/lib/src/engine/render/render_settings.dart +++ b/packages/flutter3d_core/lib/src/engine/render/render_settings.dart @@ -1462,7 +1462,7 @@ final class RenderSettings { /// eight-bit answer — `auto_batch_test.dart` holds a hundred cubes, turned and /// scaled, to byte equality. Impeller, WebGL and WebGPU compute in 32-bit /// floats, where those expressions have far less room before they part, and - /// nothing headless can run them. So the ninety goldens keep the frame + /// nothing headless can run them. So the ninety-one goldens keep the frame /// they have, and an application that wants the draw calls back asks. /// /// Shadows and picking are unaffected: both walk the scene themselves and @@ -2246,7 +2246,7 @@ final class DisplayTransform { /// **Everything here defaults to doing nothing, exactly.** Not nearly nothing: /// a vignette of zero multiplies by one and grain of zero adds zero, so a scene /// that asks for none of it composites to the same bytes it did before this -/// existed. Ninety goldens depend on that being exact, and the composite +/// existed. Ninety-one goldens depend on that being exact, and the composite /// pass already keeps the same promise for ambient occlusion. /// /// Applied in the composite rather than as passes of their own, which is the diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer.dart b/packages/flutter3d_core/lib/src/engine/render/renderer.dart index 68a6e4cff..2258757d6 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer.dart @@ -46,6 +46,7 @@ import 'light_clusters.dart'; import 'material.dart'; import 'mirror_view.dart'; import 'object_id_frame.dart'; +import 'orthographic_cascades.dart'; import 'pass_contributor.dart'; import 'physical_sky.dart'; import 'probe_faces.dart'; @@ -1235,10 +1236,22 @@ final class Renderer implements RenderServices { final vm.Matrix4 _shadowMatrixFar = vm.Matrix4.identity(); final vm.Matrix4 _shadowMatrixFarthest = vm.Matrix4.identity(); - /// x, y: where cascades 0 and 1 end, in metres from the camera. z: how many + /// x, y: where cascades 0 and 1 end, in metres from the camera — through an + /// orthographic lens, in metres of depth along its axis (`P7`). z: how many /// there are. w: one texel of a tile, vertically. Float32List get _shadowCascades => _fragInfo.shadowCascades; + /// Where the near cascades ended, as of the last shadow pass: the two + /// numbers the shaders compare a fragment's distance — or, through an + /// orthographic lens, its depth — against to pick one. + /// + /// For tests, which have to know which cascade a point lands in to say how + /// many texels it gets. + List get debugCascadeSplits => List.unmodifiable([ + _shadowCascades[0], + _shadowCascades[1], + ]); + int _shadowCascadeCount = 1; /// How far each cascade reached, in metres, as of the last shadow pass. @@ -1595,7 +1608,7 @@ final class Renderer implements RenderServices { /// `gfx-76n`'s strength, in x. Neutral is zero, which the composite reads as /// a multiplier of exactly one — the same arrangement the occlusion's - /// strength has, and for the same reason: ninety goldens go through this + /// strength has, and for the same reason: ninety-one goldens go through this /// block and "off" has to be a number the shader cancels, not one it nearly /// cancels. Float32List get _compositeContact => _compositeInfo.contact; @@ -4282,6 +4295,15 @@ final class Renderer implements RenderServices { // splits and one map cannot serve both; the primary view wins, which // is the same answer reflections give. camera: ordered.isEmpty ? null : ordered.first.camera, + aspect: ordered.isEmpty + ? 1.0 + : switch (_viewportPixels( + ordered.first.viewportFraction, + width, + height, + )) { + final rect => rect.width / rect.height, + }, ); // One node per probe the scene holds, in scene order, so the name the // scene reads for the i-th probe is the name the i-th node provides. A diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer_frame_nodes.dart b/packages/flutter3d_core/lib/src/engine/render/renderer_frame_nodes.dart index 039dd0177..7b53d0d50 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer_frame_nodes.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer_frame_nodes.dart @@ -301,12 +301,17 @@ final class _ShadowMapNode extends RenderNode { required this.settings, required this.casterIndex, this.camera, + this.aspect = 1.0, }); /// Where the player is looking, for cascade splits. Null for a scene with no /// views, which is a scene with nothing to split by. final CameraNode? camera; + /// [camera]'s view's width over its height — `P7`: an orthographic view's + /// box is as wide as this makes it, and its cascades are fitted to the box. + final double aspect; + final Renderer _renderer; final Scene scene; final ShadowSettings settings; @@ -332,6 +337,7 @@ final class _ShadowMapNode extends RenderNode { settings: settings, casterIndex: casterIndex, camera: camera, + aspect: aspect, ); if (!drew) return; frame.resources.provide( diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer_post_pass.dart b/packages/flutter3d_core/lib/src/engine/render/renderer_post_pass.dart index 0e8ce83f5..ff39a705f 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer_post_pass.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer_post_pass.dart @@ -205,7 +205,7 @@ extension _PostPasses on Renderer { _fxaaParams[3] = settings.blend.clamp(0.0, 1.0); // `gfx-29n`. Zero exactly when nobody asked: the shader returns the // centre untouched at zero rather than running a kernel that rounds to - // nothing, and ninety goldens depend on that being the same bytes. + // nothing, and ninety-one goldens depend on that being the same bytes. // // After a temporal resolve the robust kernel, at the resolve's own // strength — `R2`: what softens a resolved picture is the history, and @@ -1678,7 +1678,7 @@ extension _PostPasses on Renderer { // itself — then one draw per view, scissored to its own rectangle, so the // exposure in the uniform is the one that view metered. With per-view // metering off, or with a single view, this is the one full-frame draw it - // has always been and the bytes are the bytes ninety goldens hold. + // has always been and the bytes are the bytes ninety-one goldens hold. final perView = settings.autoExposure.enabled && settings.autoExposure.perView && diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer_shadow_pass.dart b/packages/flutter3d_core/lib/src/engine/render/renderer_shadow_pass.dart index fa4061e20..f21a6bc1b 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer_shadow_pass.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer_shadow_pass.dart @@ -650,6 +650,7 @@ extension _ShadowPasses on Renderer { required ShadowSettings settings, required int casterIndex, CameraNode? camera, + double aspect = 1.0, }) { if (!settings.enabled || settings.strength <= 0.0) return false; if (casterIndex < 0) return false; @@ -749,7 +750,30 @@ extension _ShadowPasses on Renderer { final radii = []; final splits = [0.0, 0.0]; - if (count > 1 && camera != null) { + if (count > 1 && + camera != null && + camera.projection is OrthographicProjection) { + // `P7`: through an orthographic lens the eye is only where the camera + // was put, often tens of metres back, and splitting by distance from + // it spent the near cascades on air. Split evenly by depth across what + // the view box sees of the casters instead — see + // [orthographicCascades]. The shaders pick a cascade by the same depth + // through that lens (`shadow.glsl`). + final near = orthographicCascades( + viewProjection: camera.viewProjection(aspect), + eye: camera.readWorldPosition(), + forward: camera.readForward(), + boundsMin: bounds.min, + boundsMax: bounds.max, + slabs: count - 1, + reach: math.max(settings.viewDistance, 1.0), + ); + for (final (i, cascade) in near.indexed) { + splits[i] = cascade.end; + centres.add(cascade.centre); + radii.add(cascade.radius); + } + } else if (count > 1 && camera != null) { final eyeAt = camera.readWorldPosition(); final forward = camera.readForward(); final near = 1.0; @@ -1251,7 +1275,7 @@ extension _ShadowPasses on Renderer { final skinned = skeleton != null; // `gfx-60n`. A cut-out caster goes through a stage with a sampler in // it; everything else keeps the stage it has always had, which is why - // the masked half costs the common path nothing and why ninety + // the masked half costs the common path nothing and why ninety-one // goldens recorded against the plain stage cannot move. final masked = maskedShadowShader != shadowShader && _castsMasked(node); final kind = diff --git a/packages/flutter3d_core/test/formats/ktx2_supercompression_test.dart b/packages/flutter3d_core/test/formats/ktx2_supercompression_test.dart index 76c4d2c0c..19adb5d83 100644 --- a/packages/flutter3d_core/test/formats/ktx2_supercompression_test.dart +++ b/packages/flutter3d_core/test/formats/ktx2_supercompression_test.dart @@ -157,7 +157,7 @@ void main() { }); test('an uncompressed file is untouched by any of this', () { - // The path ninety goldens and every existing asset take. A level with + // The path ninety-one goldens and every existing asset take. A level with // no supercompression is handed back as a view on the file's own bytes, // with nothing copied and nothing decoded. final file = buildKtx2( diff --git a/packages/flutter3d_cpu/CHANGELOG.md b/packages/flutter3d_cpu/CHANGELOG.md index dbef8bead..93d5c55cf 100644 --- a/packages/flutter3d_cpu/CHANGELOG.md +++ b/packages/flutter3d_cpu/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +- The software `ShadowFactor`, light shafts and volumetric fog pick a + cascade as the GLSL now does under an orthographic camera (`P7`); + `orthographic-shadow` is in the software reference set. + - The software caustic stages: a refracting caster's faces, the photon vertex stage with its ray differentials, and the photon quad. - The software `ShadowTransmittance` reads the base colour map and lets the diff --git a/packages/flutter3d_cpu/lib/src/cpu_shaders_post.dart b/packages/flutter3d_cpu/lib/src/cpu_shaders_post.dart index 4d972f26f..fe084ac3e 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_shaders_post.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_shaders_post.dart @@ -180,7 +180,7 @@ final class CompositeShader implements CpuFragmentShader { // Skipped at exactly one, which is what both settings off comes to: a // multiply by one is exact, so this is a shortcut rather than a difference, - // and it keeps the frames ninety goldens hold untouched by arithmetic + // and it keeps the frames ninety-one goldens hold untouched by arithmetic // they never used to go through. if (shade != 1.0) colour.scale(shade); diff --git a/packages/flutter3d_cpu/lib/src/cpu_shaders_reflections.dart b/packages/flutter3d_cpu/lib/src/cpu_shaders_reflections.dart index e31f9af42..553c6f1c0 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_shaders_reflections.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_shaders_reflections.dart @@ -321,7 +321,9 @@ final class LightShaftsShader implements CpuFragmentShader { for (var i = 0; i < steps && i < 64; i++) { final travelled = offset + i * stride; final at = origin + along * travelled; - final lit = litAt(at, (at - eye).length); + // By the way along the ray from where depth is nought, as the GLSL + // picks the cascade — `P7`. + final lit = litAt(at, travelled); inscatter += transmittance * (1.0 - stepTransmittance) * lit; transmittance *= stepTransmittance; } diff --git a/packages/flutter3d_cpu/lib/src/cpu_shaders_shadow_directional.dart b/packages/flutter3d_cpu/lib/src/cpu_shaders_shadow_directional.dart index 2a3563373..d0af2a90e 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_shaders_shadow_directional.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_shaders_shadow_directional.dart @@ -12,6 +12,7 @@ import 'dart:math' as math; import 'package:vector_math/vector_math.dart'; import 'cpu_shader.dart'; +import 'cpu_shaders_color.dart'; import 'cpu_shaders_evsm.dart'; import 'cpu_shaders_surface.dart'; @@ -62,7 +63,11 @@ double shadowFactor( final cascades = b.vec4('FragInfo', 'shadow_cascades', Vector4(0, 0, 1, 0)); final count = (cascades.z + 0.5).floor().clamp(1, 3); final camera = b.vec4('FragInfo', 'camera_position', Vector4.zero()); - final viewDistance = (s.world - Vector3(camera.x, camera.y, camera.z)).length; + // `P7`: by depth along the view axis through an orthographic lens, where + // the renderer splits the cascades into slabs of depth. + final viewDistance = orthographic(b) + ? math.max(0.0, (s.world - _fogEye(b)).dot(_fogForward(b))) + : (s.world - Vector3(camera.x, camera.y, camera.z)).length; var cascade = 0; if (count > 1 && viewDistance > cascades.x) cascade = 1; if (count > 2 && viewDistance > cascades.y) cascade = 2; @@ -285,3 +290,14 @@ double shadowNoise(ShaderBindings b, FragmentContext? c) { double fract(double v) => v - v.floorToDouble(); return fract(52.9829189 * fract(x * 0.06711056 + y * 0.00583715)); } + +/// `FogInfo.eye` and `FogInfo.forward`, which `ViewDepth` measures from. +Vector3 _fogEye(ShaderBindings b) { + final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); + return Vector3(eye.x, eye.y, eye.z); +} + +Vector3 _fogForward(ShaderBindings b) { + final forward = b.vec4('FogInfo', 'forward', Vector4.zero()); + return Vector3(forward.x, forward.y, forward.z); +} diff --git a/packages/flutter3d_cpu/lib/src/cpu_shaders_volumetric_fog.dart b/packages/flutter3d_cpu/lib/src/cpu_shaders_volumetric_fog.dart index 919c02aac..3d5e1f732 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_shaders_volumetric_fog.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_shaders_volumetric_fog.dart @@ -118,9 +118,12 @@ final class VolumetricFogShader implements CpuFragmentShader { var transmittance = 1.0; final inscatter = Vector3.zero(); for (var i = 0; i < steps && i < 64; i++) { - final at = origin + along * (offset + i * stride); + final travelled = offset + i * stride; + final at = origin + along * travelled; final stepTransmittance = math.exp(-density(at.y) * stride); - final sunShare = sunPhase * litAt(at, (at - eye).length); + // By the way along the ray from where depth is nought, as the GLSL + // picks the cascade — `P7`. + final sunShare = sunPhase * litAt(at, travelled); final light = Vector3( sunRadiance.x * sunShare + ambient.x * 0.07957747, sunRadiance.y * sunShare + ambient.y * 0.07957747, diff --git a/packages/flutter3d_cpu/lib/src/cpu_texture.dart b/packages/flutter3d_cpu/lib/src/cpu_texture.dart index 96dd763b4..aaf962999 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_texture.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_texture.dart @@ -300,7 +300,7 @@ final class BoundTexture { // from the short one. // // **A sampler that did not ask is untouched**, which is what makes this - // safe to add to a backend ninety golden scenes are recorded on: with + // safe to add to a backend ninety-one golden scenes are recorded on: with // `anisotropy` at one — the default everywhere in this engine — the // arithmetic below is not reached and the bytes are the ones that were // recorded. `anisotropic-floor` is the one scene that asks. diff --git a/packages/flutter3d_cpu/lib/src/portable_log2.dart b/packages/flutter3d_cpu/lib/src/portable_log2.dart index 1693eceea..f4380711a 100644 --- a/packages/flutter3d_cpu/lib/src/portable_log2.dart +++ b/packages/flutter3d_cpu/lib/src/portable_log2.dart @@ -10,7 +10,7 @@ /// twenty-five five. `anisotropic-floor` is the scene built to sit exactly /// there: 2059 of its 172800 pixels differed between a reference recorded on /// macOS and the same code run on a Linux runner, and nothing else in -/// ninety scenes differed at all. +/// ninety-one scenes differed at all. /// /// **The same rule the simulation already keeps.** `flutter3d_sim`'s /// `Portable` exists for this, and a step in that package is forbidden from diff --git a/packages/flutter3d_cpu/test/cross_backend_test.dart b/packages/flutter3d_cpu/test/cross_backend_test.dart index c8b998526..08cede48f 100644 --- a/packages/flutter3d_cpu/test/cross_backend_test.dart +++ b/packages/flutter3d_cpu/test/cross_backend_test.dart @@ -63,6 +63,10 @@ const Map _budgets = { // multisamples and this set does not; the highlights, fog and sky agree. // Recorded first from an Impeller build by mistake, which read 0%. 'orthographic-metal': 0.65, + // 1.043% measured, on the edges of the posts, the slabs, their shadows + // and the floor, which Impeller multisamples and this set does not; the + // shadows themselves sit where Impeller's do, near and far. + 'orthographic-shadow': 1.15, // 0.567% measured, on the silhouette and the floor's far edge, which // Impeller multisamples; both halves of the wipe agree inside. 'debug-view-split': 0.65, diff --git a/packages/flutter3d_cpu/test/goldens/orthographic-shadow.png b/packages/flutter3d_cpu/test/goldens/orthographic-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..bba6e5a8a185dbf68306d5e4af365a1b64ded128 GIT binary patch literal 18744 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[40.0, 0.0, -29.5, 0.0]; + final bindings = ShaderBindings( + >{ + 'FragInfo': { + 'shadow_params': Float32List.fromList([ + 1.0 / (_tile * 2), + 0.001, + 0.0, + 1.0, + ]), + 'frame_params': Float32List(4), + 'shadow_cascades': Float32List.fromList([ + 40.0, + 200.0, + 2.0, + 1.0 / _tile, + ]), + 'shadow_bias': Float32List.fromList([0.001, 0.001, 0.001, 0]), + 'shadow_matrix': Float32List.fromList(Matrix4.identity().storage), + 'shadow_matrix_far': Float32List.fromList(Matrix4.identity().storage), + 'camera_position': Float32List.fromList(eye), + 'ambient_ground': Float32List(4), + }, + 'FogInfo': { + 'eye': Float32List.fromList(eye), + // Looking along +z: the fragment is thirty metres deep. + 'forward': Float32List.fromList([0.0, 0.0, 1.0, 0.0]), + 'projection': Float32List.fromList([ + if (orthographic) 1.0 else 0.0, + 0.0, + 0.0, + 0.0, + ]), + }, + }, + {'shadow_texture': _atlas()}, + ); + return shadowFactor(surface, bindings, 0, 1.0); +} + +void main() { + test('through an orthographic lens the depth picks the cascade', () { + // Mutation: pick by distance from the eye whatever the lens, as before + // `P7`. The fragment is fifty metres from the eye, past the split, and + // reads the far tile's occluder. + expect(_shadowThrough(orthographic: true), 1.0); + }); + + test('through a perspective lens the distance still does', () { + // Mutation: pick by depth whatever the lens. Every perspective scene's + // cascades are split by distance, and a fragment at the edge of the frame + // would read a nearer tile than the one fitted for it. + expect(_shadowThrough(orthographic: false), 0.0); + }); +} diff --git a/packages/flutter3d_impeller/test/gpu_formats_test.dart b/packages/flutter3d_impeller/test/gpu_formats_test.dart index 7c8b524c6..f0ef20b6b 100644 --- a/packages/flutter3d_impeller/test/gpu_formats_test.dart +++ b/packages/flutter3d_impeller/test/gpu_formats_test.dart @@ -2,7 +2,7 @@ /// /// The hazard this exists for: a wrong mapping compiles, runs, and renders /// wrong only for the values a scene happens to use. The goldens cover the -/// values the ninety scenes exercise and nothing else — `BlendFactor` has +/// values the ninety-one scenes exercise and nothing else — `BlendFactor` has /// fifteen members and the engine draws with two of them — so a mistake in the /// other thirteen would be found by a user rather than by us. /// diff --git a/packages/flutter3d_shaders/CHANGELOG.md b/packages/flutter3d_shaders/CHANGELOG.md index 4e3c05007..45e46828b 100644 --- a/packages/flutter3d_shaders/CHANGELOG.md +++ b/packages/flutter3d_shaders/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +- `ShadowFactor` picks a cascade by depth along the view axis through an + orthographic lens (`P7`); the light shafts and the volumetric fog pick by + the way along the ray from where depth is nought, the same distance through + a perspective lens. + - **`CausticSurface`, `CausticPhotonVertex` and `CausticPhoton`**, the stages of `ShadowSettings.caustics`; `ShadowTransmittance` stops a caster's light when its photons are followed. diff --git a/packages/flutter3d_shaders/shaders/lib/shadow.glsl b/packages/flutter3d_shaders/shaders/lib/shadow.glsl index dedd5d86e..138e0740e 100644 --- a/packages/flutter3d_shaders/shaders/lib/shadow.glsl +++ b/packages/flutter3d_shaders/shaders/lib/shadow.glsl @@ -77,8 +77,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; diff --git a/packages/flutter3d_shaders/shaders/post/light_shafts.frag b/packages/flutter3d_shaders/shaders/post/light_shafts.frag index a8112f0d5..1f4568036 100644 --- a/packages/flutter3d_shaders/shaders/post/light_shafts.frag +++ b/packages/flutter3d_shaders/shaders/post/light_shafts.frag @@ -183,15 +183,17 @@ void main() { float sigma = max(shaft_info.scatter.w, 0.0); float stepTransmittance = exp(-sigma * stride); float transmittance = exp(-sigma * offset); - vec3 eye = shaft_info.camera.xyz; float inscatter = 0.0; for (int i = 0; i < 64; i++) { if (i >= steps) break; float travelled = offset + float(i) * stride; vec3 at = origin + along * travelled; - // The cascade chosen by distance from the eye, the metric `shadow.glsl` - // picks a surface's cascade by, so a shaft and the ground under it agree. - float lit = LitAt(at, length(at - eye)); + // The cascade chosen by the metric `shadow.glsl` picks a surface's + // cascade by, so a shaft and the ground under it agree: the way along + // the ray from where depth is nought, which is the distance from the eye + // through a perspective lens and the depth along the axis through an + // orthographic one (`P7`), whose cascades are slabs of depth. + float lit = LitAt(at, travelled); inscatter += transmittance * (1.0 - stepTransmittance) * lit; transmittance *= stepTransmittance; } diff --git a/packages/flutter3d_shaders/shaders/post/volumetric_fog.frag b/packages/flutter3d_shaders/shaders/post/volumetric_fog.frag index 727eb8d8d..a061df5b1 100644 --- a/packages/flutter3d_shaders/shaders/post/volumetric_fog.frag +++ b/packages/flutter3d_shaders/shaders/post/volumetric_fog.frag @@ -353,7 +353,6 @@ void main() { float g = fog_info.sun.w; float sunPhase = HenyeyGreenstein(dot(along, fog_info.sun.xyz), g); bool clustered = fog_info.albedo.w > 0.5; - vec3 eye = fog_info.camera.xyz; float transmittance = 1.0; vec3 inscatter = vec3(0.0); for (int i = 0; i < 64; i++) { @@ -362,8 +361,11 @@ void main() { vec3 at = origin + along * travelled; float stepTransmittance = exp(-Density(at.y) * stride); + // The cascade by the way along the ray from where depth is nought, as + // `light_shafts.frag` picks it: the distance from the eye through a + // perspective lens, the depth through an orthographic one (`P7`). vec3 light = fog_info.sun_radiance.rgb * - (sunPhase * LitAt(at, length(at - eye))) + + (sunPhase * LitAt(at, travelled)) + fog_info.ambient.rgb * 0.07957747; if (clustered) { light += fog_info.albedo.rgb * ClusterLight(at, along, g); diff --git a/packages/flutter3d_webgl/CHANGELOG.md b/packages/flutter3d_webgl/CHANGELOG.md index 228918340..07d141a07 100644 --- a/packages/flutter3d_webgl/CHANGELOG.md +++ b/packages/flutter3d_webgl/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- The shader table regenerated for the orthographic camera's cascade pick; + `orthographic-shadow` is in the browser reference set. + - The shader table regenerated for the caustic stages. - The shader table regenerated for the painted, unclamped transmittance. - The shader table regenerated for `ShadowTransmittance` and the coloured diff --git a/packages/flutter3d_webgl/lib/engine_shaders.dart b/packages/flutter3d_webgl/lib/engine_shaders.dart index 552bf6411..045d5dbee 100644 --- a/packages/flutter3d_webgl/lib/engine_shaders.dart +++ b/packages/flutter3d_webgl/lib/engine_shaders.dart @@ -9531,8 +9531,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; @@ -12185,8 +12193,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; @@ -14874,8 +14890,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; @@ -18419,8 +18443,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; @@ -21932,8 +21964,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; @@ -29754,15 +29794,17 @@ void main() { float sigma = max(shaft_info.scatter.w, 0.0); float stepTransmittance = exp(-sigma * stride); float transmittance = exp(-sigma * offset); - vec3 eye = shaft_info.camera.xyz; float inscatter = 0.0; for (int i = 0; i < 64; i++) { if (i >= steps) break; float travelled = offset + float(i) * stride; vec3 at = origin + along * travelled; - // The cascade chosen by distance from the eye, the metric `shadow.glsl` - // picks a surface's cascade by, so a shaft and the ground under it agree. - float lit = LitAt(at, length(at - eye)); + // The cascade chosen by the metric `shadow.glsl` picks a surface's + // cascade by, so a shaft and the ground under it agree: the way along + // the ray from where depth is nought, which is the distance from the eye + // through a perspective lens and the depth along the axis through an + // orthographic one (`P7`), whose cascades are slabs of depth. + float lit = LitAt(at, travelled); inscatter += transmittance * (1.0 - stepTransmittance) * lit; transmittance *= stepTransmittance; } @@ -30258,7 +30300,6 @@ void main() { float g = fog_info.sun.w; float sunPhase = HenyeyGreenstein(dot(along, fog_info.sun.xyz), g); bool clustered = fog_info.albedo.w > 0.5; - vec3 eye = fog_info.camera.xyz; float transmittance = 1.0; vec3 inscatter = vec3(0.0); for (int i = 0; i < 64; i++) { @@ -30267,8 +30308,11 @@ void main() { vec3 at = origin + along * travelled; float stepTransmittance = exp(-Density(at.y) * stride); + // The cascade by the way along the ray from where depth is nought, as + // `light_shafts.frag` picks it: the distance from the eye through a + // perspective lens, the depth through an orthographic one (`P7`). vec3 light = fog_info.sun_radiance.rgb * - (sunPhase * LitAt(at, length(at - eye))) + + (sunPhase * LitAt(at, travelled)) + fog_info.ambient.rgb * 0.07957747; if (clustered) { light += fog_info.albedo.rgb * ClusterLight(at, along, g); @@ -37752,8 +37796,16 @@ float ShadowFactor(Surface s, LightSample light, int lightIndex) { // suggests. Falling through to the next one costs a branch and removes a // whole class of missing-shadow bug, and the last cascade is fitted to the // entire scene, so the fall-through always terminates somewhere real. + // + // `P7`: through an orthographic lens the cascades are slabs of depth along + // the view axis, because the eye is only where the camera was put and a + // distance from it is mostly how far it was put back. The renderer splits + // by that depth there, so this picks by it. int cascadeCount = int(frag_info.shadow_cascades.z + 0.5); float viewDistance = length(v_world_position - frag_info.camera_position.xyz); +#ifndef F3D_NO_FOG + if (Orthographic()) viewDistance = max(ViewDepth(), 0.0); +#endif int cascade = 0; if (cascadeCount > 1 && viewDistance > frag_info.shadow_cascades.x) cascade = 1; if (cascadeCount > 2 && viewDistance > frag_info.shadow_cascades.y) cascade = 2; diff --git a/packages/flutter3d_webgl/test/cross_backend_test.dart b/packages/flutter3d_webgl/test/cross_backend_test.dart index 3251d594c..37c625f3a 100644 --- a/packages/flutter3d_webgl/test/cross_backend_test.dart +++ b/packages/flutter3d_webgl/test/cross_backend_test.dart @@ -108,6 +108,9 @@ const Map _budgets = { // does not multisample as Impeller does; the highlights, the fog and the // sky agree. 'orthographic-metal': 0.5, + // 0.394% measured, on the edges of the posts, the slabs and the floor, + // which WebGL2 does not multisample as Impeller does. + 'orthographic-shadow': 0.5, // 0.359% measured, on the silhouette: `shadow-teapot`'s edge between // WebGL2 and Impeller, and the normals' half shows it in brighter colours // than the lit half's. diff --git a/packages/flutter3d_webgl/test/goldens/orthographic-shadow.png b/packages/flutter3d_webgl/test/goldens/orthographic-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..a377c598c44e16aacbb85392d8210b6c8d95405c GIT binary patch literal 24846 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 1i); - phi_3504_ = _e278; - if _e278 { - let _e279 = viewDistance; - let _e282 = frag_info.shadow_cascades[0u]; - phi_3504_ = (_e279 > _e282); + let _e277 = Orthographic_u0028_(); + if _e277 { + let _e278 = ViewDepth_u0028_(); + viewDistance = max(_e278, 0f); } - let _e285 = phi_3504_; - if _e285 { + cascade = 0i; + let _e280 = cascadeCount; + let _e281 = (_e280 > 1i); + phi_3509_ = _e281; + if _e281 { + let _e282 = viewDistance; + let _e285 = frag_info.shadow_cascades[0u]; + phi_3509_ = (_e282 > _e285); + } + let _e288 = phi_3509_; + if _e288 { cascade = 1i; } - let _e286 = cascadeCount; - let _e287 = (_e286 > 2i); - phi_3515_ = _e287; - if _e287 { - let _e288 = viewDistance; - let _e291 = frag_info.shadow_cascades[1u]; - phi_3515_ = (_e288 > _e291); + let _e289 = cascadeCount; + let _e290 = (_e289 > 2i); + phi_3520_ = _e290; + if _e290 { + let _e291 = viewDistance; + let _e294 = frag_info.shadow_cascades[1u]; + phi_3520_ = (_e291 > _e294); } - let _e294 = phi_3515_; - if _e294 { + let _e297 = phi_3520_; + if _e297 { cascade = 2i; } uv_4 = vec2(0f, 0f); @@ -7656,247 +7661,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e295 = attempt; - if (_e295 < 3i) { - let _e297 = cascade; - let _e298 = attempt; - which = (_e297 + _e298); - let _e300 = which; - let _e301 = cascadeCount; - if (_e300 >= _e301) { + let _e298 = attempt; + if (_e298 < 3i) { + let _e300 = cascade; + let _e301 = attempt; + which = (_e300 + _e301); + let _e303 = which; + let _e304 = cascadeCount; + if (_e303 >= _e304) { break; } - let _e303 = which; - if (_e303 == 0i) { - let _e306 = frag_info.shadow_matrix; - local_8 = _e306; + let _e306 = which; + if (_e306 == 0i) { + let _e309 = frag_info.shadow_matrix; + local_8 = _e309; } else { - let _e307 = which; - if (_e307 == 1i) { - let _e310 = frag_info.shadow_matrix_far; - local_9 = _e310; + let _e310 = which; + if (_e310 == 1i) { + let _e313 = frag_info.shadow_matrix_far; + local_9 = _e313; } else { - let _e312 = frag_info.shadow_matrix_farthest; - local_9 = _e312; + let _e315 = frag_info.shadow_matrix_farthest; + local_9 = _e315; } - let _e313 = local_9; - local_8 = _e313; - } - let _e314 = local_8; - matrix = _e314; - let _e317 = matrix[0][0u]; - let _e320 = matrix[1][0u]; - let _e323 = matrix[2][0u]; - axisX = vec3(_e317, _e320, _e323); - let _e327 = matrix[0][1u]; - let _e330 = matrix[1][1u]; - let _e333 = matrix[2][1u]; - axisY = vec3(_e327, _e330, _e333); - let _e335 = axisX; - rowX = max(length(_e335), 0.000001f); - let _e338 = axisY; - rowY = max(length(_e338), 0.000001f); - let _e343 = frag_info.shadow_cascades[3u]; - let _e345 = rowX; - texelMetres = ((2f * _e343) / _e345); - let _e349 = frag_info.shadow_cascades[3u]; - let _e352 = (*s_2).n; - let _e353 = axisX; - let _e356 = rowX; - let _e357 = rowX; - let _e361 = (*s_2).n; - let _e362 = axisY; - let _e365 = rowY; - let _e366 = rowY; - reach = ((3f * _e349) * ((abs(dot(_e352, _e353)) / (_e356 * _e357)) + (abs(dot(_e361, _e362)) / (_e365 * _e366)))); - let _e373 = frag_info.shadow_params[2u]; - let _e374 = texelMetres; - flatOffset = min(_e373, _e374); - let _e376 = v_world_position_1; - let _e378 = (*s_2).n; - let _e379 = flatOffset; - let _e380 = reach; - origin_1 = (_e376 + (_e378 * (_e379 + _e380))); - let _e384 = matrix; - let _e385 = origin_1; - lightSpace = (_e384 * vec4(_e385.x, _e385.y, _e385.z, 1f)); - let _e392 = lightSpace[3u]; - if (_e392 <= 0f) { + let _e316 = local_9; + local_8 = _e316; + } + let _e317 = local_8; + matrix = _e317; + let _e320 = matrix[0][0u]; + let _e323 = matrix[1][0u]; + let _e326 = matrix[2][0u]; + axisX = vec3(_e320, _e323, _e326); + let _e330 = matrix[0][1u]; + let _e333 = matrix[1][1u]; + let _e336 = matrix[2][1u]; + axisY = vec3(_e330, _e333, _e336); + let _e338 = axisX; + rowX = max(length(_e338), 0.000001f); + let _e341 = axisY; + rowY = max(length(_e341), 0.000001f); + let _e346 = frag_info.shadow_cascades[3u]; + let _e348 = rowX; + texelMetres = ((2f * _e346) / _e348); + let _e352 = frag_info.shadow_cascades[3u]; + let _e355 = (*s_2).n; + let _e356 = axisX; + let _e359 = rowX; + let _e360 = rowX; + let _e364 = (*s_2).n; + let _e365 = axisY; + let _e368 = rowY; + let _e369 = rowY; + reach = ((3f * _e352) * ((abs(dot(_e355, _e356)) / (_e359 * _e360)) + (abs(dot(_e364, _e365)) / (_e368 * _e369)))); + let _e376 = frag_info.shadow_params[2u]; + let _e377 = texelMetres; + flatOffset = min(_e376, _e377); + let _e379 = v_world_position_1; + let _e381 = (*s_2).n; + let _e382 = flatOffset; + let _e383 = reach; + origin_1 = (_e379 + (_e381 * (_e382 + _e383))); + let _e387 = matrix; + let _e388 = origin_1; + lightSpace = (_e387 * vec4(_e388.x, _e388.y, _e388.z, 1f)); + let _e395 = lightSpace[3u]; + if (_e395 <= 0f) { continue; } - let _e394 = lightSpace; - let _e397 = lightSpace[3u]; - candidate = (_e394.xyz / vec3(_e397)); - let _e401 = candidate[0u]; - let _e405 = candidate[1u]; - inTile = vec2(((_e401 * 0.5f) + 0.5f), (0.5f - (_e405 * 0.5f))); - let _e410 = inTile[0u]; - let _e411 = (_e410 < 0f); - phi_3676_ = _e411; - if !(_e411) { - let _e414 = inTile[0u]; - phi_3676_ = (_e414 > 1f); - } - let _e417 = phi_3676_; - phi_3683_ = _e417; - if !(_e417) { - let _e420 = inTile[1u]; - phi_3683_ = (_e420 < 0f); - } - let _e423 = phi_3683_; - phi_3690_ = _e423; - if !(_e423) { - let _e426 = inTile[1u]; - phi_3690_ = (_e426 > 1f); - } - let _e429 = phi_3690_; - if _e429 { + let _e397 = lightSpace; + let _e400 = lightSpace[3u]; + candidate = (_e397.xyz / vec3(_e400)); + let _e404 = candidate[0u]; + let _e408 = candidate[1u]; + inTile = vec2(((_e404 * 0.5f) + 0.5f), (0.5f - (_e408 * 0.5f))); + let _e413 = inTile[0u]; + let _e414 = (_e413 < 0f); + phi_3681_ = _e414; + if !(_e414) { + let _e417 = inTile[0u]; + phi_3681_ = (_e417 > 1f); + } + let _e420 = phi_3681_; + phi_3688_ = _e420; + if !(_e420) { + let _e423 = inTile[1u]; + phi_3688_ = (_e423 < 0f); + } + let _e426 = phi_3688_; + phi_3695_ = _e426; + if !(_e426) { + let _e429 = inTile[1u]; + phi_3695_ = (_e429 > 1f); + } + let _e432 = phi_3695_; + if _e432 { continue; } - let _e431 = candidate[2u]; - if (_e431 > 1f) { - let _e433 = which; - let _e434 = cascadeCount; - if (_e433 < (_e434 - 1i)) { + let _e434 = candidate[2u]; + if (_e434 > 1f) { + let _e436 = which; + let _e437 = cascadeCount; + if (_e436 < (_e437 - 1i)) { continue; } candidate[2u] = 1f; } - let _e439 = inTile[0u]; - let _e440 = which; - let _e443 = cascadeCount; - let _e447 = inTile[1u]; - uv_4 = vec2(((_e439 + f32(_e440)) / f32(_e443)), _e447); - let _e449 = candidate; - projected = _e449; - let _e450 = which; - cascade = _e450; - let _e451 = texelMetres; - cascadeTexel = _e451; - let _e454 = matrix[0][2u]; - let _e457 = matrix[1][2u]; - let _e460 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e454, _e457, _e460)), 0.000001f)); - let _e466 = (*s_2).n; - let _e469 = matrix[0][2u]; - let _e472 = matrix[1][2u]; - let _e475 = matrix[2][2u]; - let _e479 = cascadeDepth; - cosSlope = clamp((abs(dot(_e466, vec3(_e469, _e472, _e475))) * _e479), 0.1f, 1f); - let _e482 = texelMetres; - let _e483 = cosSlope; - let _e484 = cosSlope; - let _e490 = cosSlope; - let _e492 = cascadeDepth; - receiverSlope = (((_e482 * sqrt(max((1f - (_e483 * _e484)), 0f))) / _e490) / _e492); + let _e442 = inTile[0u]; + let _e443 = which; + let _e446 = cascadeCount; + let _e450 = inTile[1u]; + uv_4 = vec2(((_e442 + f32(_e443)) / f32(_e446)), _e450); + let _e452 = candidate; + projected = _e452; + let _e453 = which; + cascade = _e453; + let _e454 = texelMetres; + cascadeTexel = _e454; + let _e457 = matrix[0][2u]; + let _e460 = matrix[1][2u]; + let _e463 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e457, _e460, _e463)), 0.000001f)); + let _e469 = (*s_2).n; + let _e472 = matrix[0][2u]; + let _e475 = matrix[1][2u]; + let _e478 = matrix[2][2u]; + let _e482 = cascadeDepth; + cosSlope = clamp((abs(dot(_e469, vec3(_e472, _e475, _e478))) * _e482), 0.1f, 1f); + let _e485 = texelMetres; + let _e486 = cosSlope; + let _e487 = cosSlope; + let _e493 = cosSlope; + let _e495 = cascadeDepth; + receiverSlope = (((_e485 * sqrt(max((1f - (_e486 * _e487)), 0f))) / _e493) / _e495); found = true; break; } else { break; } continuing { - let _e494 = attempt; - attempt = (_e494 + 1i); + let _e497 = attempt; + attempt = (_e497 + 1i); } } - let _e496 = found; - if !(_e496) { + let _e499 = found; + if !(_e499) { return 1f; } - let _e498 = cascade; - if (_e498 == 0i) { - let _e502 = frag_info.shadow_bias[0u]; - local_10 = _e502; + let _e501 = cascade; + if (_e501 == 0i) { + let _e505 = frag_info.shadow_bias[0u]; + local_10 = _e505; } else { - let _e503 = cascade; - if (_e503 == 1i) { - let _e507 = frag_info.shadow_bias[1u]; - local_11 = _e507; - } else { - let _e510 = frag_info.shadow_bias[2u]; + let _e506 = cascade; + if (_e506 == 1i) { + let _e510 = frag_info.shadow_bias[1u]; local_11 = _e510; - } - let _e511 = local_11; - local_10 = _e511; - } - let _e512 = local_10; - bias = _e512; - let _e515 = frag_info.shadow_params[0u]; - let _e518 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e515, _e518); - let _e520 = cascade; - let _e522 = cascadeCount; - let _e526 = texel_1; - tileLo = (vec2((f32(_e520) / f32(_e522)), 0f) + (_e526 * 0.5f)); - let _e529 = cascade; - let _e532 = cascadeCount; - let _e536 = texel_1; - tileHi = (vec2((f32((_e529 + 1i)) / f32(_e532)), 1f) - (_e536 * 0.5f)); - let _e541 = frag_info.shadow_bias[3u]; - if (_e541 > 0.5f) { - let _e543 = uv_4; - let _e544 = tileLo; - let _e545 = tileHi; - let _e547 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e543, _e544, _e545), 0f); - stored_2 = _e547; - let _e549 = stored_2[1u]; - let _e552 = stored_2[2u]; - let _e555 = stored_2[3u]; - through = clamp(vec3((1f - _e549), (1f - _e552), _e555), vec3(0f), vec3(4f)); - let _e560 = through; - let _e561 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e560, vec3(clamp(_e561, 0f, 1f))); - } - let _e567 = frag_info.ambient_ground[3u]; - softness = _e567; + } else { + let _e513 = frag_info.shadow_bias[2u]; + local_11 = _e513; + } + let _e514 = local_11; + local_10 = _e514; + } + let _e515 = local_10; + bias = _e515; + let _e518 = frag_info.shadow_params[0u]; + let _e521 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e518, _e521); + let _e523 = cascade; + let _e525 = cascadeCount; + let _e529 = texel_1; + tileLo = (vec2((f32(_e523) / f32(_e525)), 0f) + (_e529 * 0.5f)); + let _e532 = cascade; + let _e535 = cascadeCount; + let _e539 = texel_1; + tileHi = (vec2((f32((_e532 + 1i)) / f32(_e535)), 1f) - (_e539 * 0.5f)); + let _e544 = frag_info.shadow_bias[3u]; + if (_e544 > 0.5f) { + let _e546 = uv_4; + let _e547 = tileLo; + let _e548 = tileHi; + let _e550 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e546, _e547, _e548), 0f); + stored_2 = _e550; + let _e552 = stored_2[1u]; + let _e555 = stored_2[2u]; + let _e558 = stored_2[3u]; + through = clamp(vec3((1f - _e552), (1f - _e555), _e558), vec3(0f), vec3(4f)); + let _e563 = through; + let _e564 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e563, vec3(clamp(_e564, 0f, 1f))); + } + let _e570 = frag_info.ambient_ground[3u]; + softness = _e570; lit_2 = 0f; - let _e568 = softness; - if (_e568 < 0f) { - let _e570 = uv_4; - let _e571 = tileLo; - let _e572 = tileHi; - let _e574 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e570, _e571, _e572), 0f); - moments_2 = _e574; - let _e576 = projected[2u]; - let _e577 = bias; - let _e579 = softness; - let _e583 = moments_2; - param_61 = _e583; - param_62 = (_e576 - _e577); - param_63 = clamp((-(_e579) - 1f), 0f, 0.95f); - let _e584 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); - lit_2 = _e584; - } else { - let _e585 = softness; - if (_e585 <= 0f) { + let _e571 = softness; + if (_e571 < 0f) { + let _e573 = uv_4; + let _e574 = tileLo; + let _e575 = tileHi; + let _e577 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e573, _e574, _e575), 0f); + moments_2 = _e577; + let _e579 = projected[2u]; + let _e580 = bias; + let _e582 = softness; + let _e586 = moments_2; + param_61 = _e586; + param_62 = (_e579 - _e580); + param_63 = clamp((-(_e582) - 1f), 0f, 0.95f); + let _e587 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); + lit_2 = _e587; + } else { + let _e588 = softness; + if (_e588 <= 0f) { y = -1i; loop { - let _e587 = y; - if (_e587 <= 1i) { + let _e590 = y; + if (_e590 <= 1i) { x = -1i; loop { - let _e589 = x; - if (_e589 <= 1i) { - let _e591 = uv_4; - let _e592 = x; - let _e594 = y; - let _e597 = texel_1; - let _e600 = tileLo; - let _e601 = tileHi; - let _e603 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e591 + (vec2(f32(_e592), f32(_e594)) * _e597)), _e600, _e601), 0f); - occluder = _e603.x; - let _e606 = projected[2u]; - let _e607 = bias; - let _e609 = occluder; - let _e612 = lit_2; - lit_2 = (_e612 + select(1f, 0f, ((_e606 - _e607) > _e609))); + let _e592 = x; + if (_e592 <= 1i) { + let _e594 = uv_4; + let _e595 = x; + let _e597 = y; + let _e600 = texel_1; + let _e603 = tileLo; + let _e604 = tileHi; + let _e606 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e594 + (vec2(f32(_e595), f32(_e597)) * _e600)), _e603, _e604), 0f); + occluder = _e606.x; + let _e609 = projected[2u]; + let _e610 = bias; + let _e612 = occluder; + let _e615 = lit_2; + lit_2 = (_e615 + select(1f, 0f, ((_e609 - _e610) > _e612))); continue; } else { break; } continuing { - let _e614 = x; - x = (_e614 + 1i); + let _e617 = x; + x = (_e617 + 1i); } } continue; @@ -7904,125 +7909,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e616 = y; - y = (_e616 + 1i); + let _e619 = y; + y = (_e619 + 1i); } } - let _e618 = lit_2; - lit_2 = (_e618 * 0.11111111f); + let _e621 = lit_2; + lit_2 = (_e621 * 0.11111111f); } else { - let _e620 = softness; - spread = tan(min(_e620, 0.5f)); - let _e623 = ShadowNoise_u0028_(); - turn_1 = (_e623 * 6.2831855f); - let _e625 = spread; - let _e627 = projected[2u]; - let _e629 = cascadeDepth; - let _e631 = cascadeTexel; - searchRadius = clamp((((_e625 * _e627) * _e629) / _e631), 1f, 16f); + let _e623 = softness; + spread = tan(min(_e623, 0.5f)); + let _e626 = ShadowNoise_u0028_(); + turn_1 = (_e626 * 6.2831855f); + let _e628 = spread; + let _e630 = projected[2u]; + let _e632 = cascadeDepth; + let _e634 = cascadeTexel; + searchRadius = clamp((((_e628 * _e630) * _e632) / _e634), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_5 = 0i; loop { - let _e634 = i_5; - if (_e634 < 16i) { - let _e636 = i_5; - param_64 = _e636; + let _e637 = i_5; + if (_e637 < 16i) { + let _e639 = i_5; + param_64 = _e639; param_65 = 16i; - let _e637 = turn_1; - param_66 = _e637; - let _e638 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); - disc = _e638; - let _e639 = bias; - let _e640 = disc; - let _e642 = searchRadius; - let _e646 = receiverSlope; - tapBias = (_e639 + (max(((length(_e640) * _e642) - 1.5f), 0f) * _e646)); - let _e649 = uv_4; - let _e650 = disc; - let _e651 = texel_1; - let _e653 = searchRadius; - let _e656 = tileLo; - let _e657 = tileHi; - let _e659 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e649 + ((_e650 * _e651) * _e653)), _e656, _e657), 0f); - occluder_1 = _e659.x; - let _e662 = projected[2u]; - let _e663 = tapBias; - let _e665 = occluder_1; - if ((_e662 - _e663) > _e665) { - let _e667 = occluder_1; - let _e668 = blockerSum; - blockerSum = (_e668 + _e667); - let _e670 = blockerCount; - blockerCount = (_e670 + 1f); + let _e640 = turn_1; + param_66 = _e640; + let _e641 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); + disc = _e641; + let _e642 = bias; + let _e643 = disc; + let _e645 = searchRadius; + let _e649 = receiverSlope; + tapBias = (_e642 + (max(((length(_e643) * _e645) - 1.5f), 0f) * _e649)); + let _e652 = uv_4; + let _e653 = disc; + let _e654 = texel_1; + let _e656 = searchRadius; + let _e659 = tileLo; + let _e660 = tileHi; + let _e662 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e652 + ((_e653 * _e654) * _e656)), _e659, _e660), 0f); + occluder_1 = _e662.x; + let _e665 = projected[2u]; + let _e666 = tapBias; + let _e668 = occluder_1; + if ((_e665 - _e666) > _e668) { + let _e670 = occluder_1; + let _e671 = blockerSum; + blockerSum = (_e671 + _e670); + let _e673 = blockerCount; + blockerCount = (_e673 + 1f); } continue; } else { break; } continuing { - let _e672 = i_5; - i_5 = (_e672 + 1i); + let _e675 = i_5; + i_5 = (_e675 + 1i); } } - let _e674 = blockerCount; - if (_e674 <= 0f) { + let _e677 = blockerCount; + if (_e677 <= 0f) { return 1f; } - let _e677 = projected[2u]; - let _e678 = blockerSum; - let _e679 = blockerCount; - let _e683 = cascadeDepth; - gap = (max((_e677 - (_e678 / _e679)), 0f) * _e683); - let _e685 = spread; - let _e686 = gap; - let _e688 = cascadeTexel; - radius_2 = clamp(((_e685 * _e686) / _e688), 1f, 16f); + let _e680 = projected[2u]; + let _e681 = blockerSum; + let _e682 = blockerCount; + let _e686 = cascadeDepth; + gap = (max((_e680 - (_e681 / _e682)), 0f) * _e686); + let _e688 = spread; + let _e689 = gap; + let _e691 = cascadeTexel; + radius_2 = clamp(((_e688 * _e689) / _e691), 1f, 16f); i_6 = 0i; loop { - let _e691 = i_6; - if (_e691 < 16i) { - let _e693 = turn_1; - let _e695 = i_6; - param_67 = _e695; + let _e694 = i_6; + if (_e694 < 16i) { + let _e696 = turn_1; + let _e698 = i_6; + param_67 = _e698; param_68 = 16i; - param_69 = (_e693 + 1f); - let _e696 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); - disc_1 = _e696; - let _e697 = bias; - let _e698 = disc_1; - let _e700 = radius_2; - let _e704 = receiverSlope; - tapBias_1 = (_e697 + (max(((length(_e698) * _e700) - 1.5f), 0f) * _e704)); - let _e707 = uv_4; - let _e708 = disc_1; - let _e709 = texel_1; - let _e711 = radius_2; - let _e714 = tileLo; - let _e715 = tileHi; - let _e717 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e707 + ((_e708 * _e709) * _e711)), _e714, _e715), 0f); - occluder_2 = _e717.x; - let _e720 = projected[2u]; - let _e721 = tapBias_1; - let _e723 = occluder_2; - let _e726 = lit_2; - lit_2 = (_e726 + select(1f, 0f, ((_e720 - _e721) > _e723))); + param_69 = (_e696 + 1f); + let _e699 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); + disc_1 = _e699; + let _e700 = bias; + let _e701 = disc_1; + let _e703 = radius_2; + let _e707 = receiverSlope; + tapBias_1 = (_e700 + (max(((length(_e701) * _e703) - 1.5f), 0f) * _e707)); + let _e710 = uv_4; + let _e711 = disc_1; + let _e712 = texel_1; + let _e714 = radius_2; + let _e717 = tileLo; + let _e718 = tileHi; + let _e720 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e710 + ((_e711 * _e712) * _e714)), _e717, _e718), 0f); + occluder_2 = _e720.x; + let _e723 = projected[2u]; + let _e724 = tapBias_1; + let _e726 = occluder_2; + let _e729 = lit_2; + lit_2 = (_e729 + select(1f, 0f, ((_e723 - _e724) > _e726))); continue; } else { break; } continuing { - let _e728 = i_6; - i_6 = (_e728 + 1i); + let _e731 = i_6; + i_6 = (_e731 + 1i); } } - let _e730 = lit_2; - lit_2 = (_e730 * 0.0625f); + let _e733 = lit_2; + lit_2 = (_e733 * 0.0625f); } } - let _e732 = lit_2; - let _e733 = strength_1; - return mix(1f, _e732, clamp(_e733, 0f, 1f)); + let _e735 = lit_2; + let _e736 = strength_1; + return mix(1f, _e735, clamp(_e736, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_3: ptr, light_2: ptr, index: ptr) -> f32 { @@ -11158,11 +11163,11 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf var param_69: f32; var tapBias_1: f32; var occluder_2: f32; - var phi_3531_: bool; - var phi_3542_: bool; - var phi_3703_: bool; - var phi_3710_: bool; - var phi_3717_: bool; + var phi_3536_: bool; + var phi_3547_: bool; + var phi_3708_: bool; + var phi_3715_: bool; + var phi_3722_: bool; let _e257 = frag_info.shadow_params[3u]; strength_1 = _e257; @@ -11180,29 +11185,34 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf let _e272 = v_world_position_1; let _e274 = frag_info.camera_position; viewDistance = length((_e272 - _e274.xyz)); - cascade = 0i; - let _e278 = cascadeCount; - let _e279 = (_e278 > 1i); - phi_3531_ = _e279; - if _e279 { - let _e280 = viewDistance; - let _e283 = frag_info.shadow_cascades[0u]; - phi_3531_ = (_e280 > _e283); + let _e278 = Orthographic_u0028_(); + if _e278 { + let _e279 = ViewDepth_u0028_(); + viewDistance = max(_e279, 0f); } - let _e286 = phi_3531_; - if _e286 { + cascade = 0i; + let _e281 = cascadeCount; + let _e282 = (_e281 > 1i); + phi_3536_ = _e282; + if _e282 { + let _e283 = viewDistance; + let _e286 = frag_info.shadow_cascades[0u]; + phi_3536_ = (_e283 > _e286); + } + let _e289 = phi_3536_; + if _e289 { cascade = 1i; } - let _e287 = cascadeCount; - let _e288 = (_e287 > 2i); - phi_3542_ = _e288; - if _e288 { - let _e289 = viewDistance; - let _e292 = frag_info.shadow_cascades[1u]; - phi_3542_ = (_e289 > _e292); + let _e290 = cascadeCount; + let _e291 = (_e290 > 2i); + phi_3547_ = _e291; + if _e291 { + let _e292 = viewDistance; + let _e295 = frag_info.shadow_cascades[1u]; + phi_3547_ = (_e292 > _e295); } - let _e295 = phi_3542_; - if _e295 { + let _e298 = phi_3547_; + if _e298 { cascade = 2i; } uv_4 = vec2(0f, 0f); @@ -11213,247 +11223,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e296 = attempt; - if (_e296 < 3i) { - let _e298 = cascade; - let _e299 = attempt; - which = (_e298 + _e299); - let _e301 = which; - let _e302 = cascadeCount; - if (_e301 >= _e302) { + let _e299 = attempt; + if (_e299 < 3i) { + let _e301 = cascade; + let _e302 = attempt; + which = (_e301 + _e302); + let _e304 = which; + let _e305 = cascadeCount; + if (_e304 >= _e305) { break; } - let _e304 = which; - if (_e304 == 0i) { - let _e307 = frag_info.shadow_matrix; - local_8 = _e307; + let _e307 = which; + if (_e307 == 0i) { + let _e310 = frag_info.shadow_matrix; + local_8 = _e310; } else { - let _e308 = which; - if (_e308 == 1i) { - let _e311 = frag_info.shadow_matrix_far; - local_9 = _e311; + let _e311 = which; + if (_e311 == 1i) { + let _e314 = frag_info.shadow_matrix_far; + local_9 = _e314; } else { - let _e313 = frag_info.shadow_matrix_farthest; - local_9 = _e313; + let _e316 = frag_info.shadow_matrix_farthest; + local_9 = _e316; } - let _e314 = local_9; - local_8 = _e314; - } - let _e315 = local_8; - matrix = _e315; - let _e318 = matrix[0][0u]; - let _e321 = matrix[1][0u]; - let _e324 = matrix[2][0u]; - axisX = vec3(_e318, _e321, _e324); - let _e328 = matrix[0][1u]; - let _e331 = matrix[1][1u]; - let _e334 = matrix[2][1u]; - axisY = vec3(_e328, _e331, _e334); - let _e336 = axisX; - rowX = max(length(_e336), 0.000001f); - let _e339 = axisY; - rowY = max(length(_e339), 0.000001f); - let _e344 = frag_info.shadow_cascades[3u]; - let _e346 = rowX; - texelMetres = ((2f * _e344) / _e346); - let _e350 = frag_info.shadow_cascades[3u]; - let _e353 = (*s_2).n; - let _e354 = axisX; - let _e357 = rowX; - let _e358 = rowX; - let _e362 = (*s_2).n; - let _e363 = axisY; - let _e366 = rowY; - let _e367 = rowY; - reach = ((3f * _e350) * ((abs(dot(_e353, _e354)) / (_e357 * _e358)) + (abs(dot(_e362, _e363)) / (_e366 * _e367)))); - let _e374 = frag_info.shadow_params[2u]; - let _e375 = texelMetres; - flatOffset = min(_e374, _e375); - let _e377 = v_world_position_1; - let _e379 = (*s_2).n; - let _e380 = flatOffset; - let _e381 = reach; - origin_1 = (_e377 + (_e379 * (_e380 + _e381))); - let _e385 = matrix; - let _e386 = origin_1; - lightSpace = (_e385 * vec4(_e386.x, _e386.y, _e386.z, 1f)); - let _e393 = lightSpace[3u]; - if (_e393 <= 0f) { + let _e317 = local_9; + local_8 = _e317; + } + let _e318 = local_8; + matrix = _e318; + let _e321 = matrix[0][0u]; + let _e324 = matrix[1][0u]; + let _e327 = matrix[2][0u]; + axisX = vec3(_e321, _e324, _e327); + let _e331 = matrix[0][1u]; + let _e334 = matrix[1][1u]; + let _e337 = matrix[2][1u]; + axisY = vec3(_e331, _e334, _e337); + let _e339 = axisX; + rowX = max(length(_e339), 0.000001f); + let _e342 = axisY; + rowY = max(length(_e342), 0.000001f); + let _e347 = frag_info.shadow_cascades[3u]; + let _e349 = rowX; + texelMetres = ((2f * _e347) / _e349); + let _e353 = frag_info.shadow_cascades[3u]; + let _e356 = (*s_2).n; + let _e357 = axisX; + let _e360 = rowX; + let _e361 = rowX; + let _e365 = (*s_2).n; + let _e366 = axisY; + let _e369 = rowY; + let _e370 = rowY; + reach = ((3f * _e353) * ((abs(dot(_e356, _e357)) / (_e360 * _e361)) + (abs(dot(_e365, _e366)) / (_e369 * _e370)))); + let _e377 = frag_info.shadow_params[2u]; + let _e378 = texelMetres; + flatOffset = min(_e377, _e378); + let _e380 = v_world_position_1; + let _e382 = (*s_2).n; + let _e383 = flatOffset; + let _e384 = reach; + origin_1 = (_e380 + (_e382 * (_e383 + _e384))); + let _e388 = matrix; + let _e389 = origin_1; + lightSpace = (_e388 * vec4(_e389.x, _e389.y, _e389.z, 1f)); + let _e396 = lightSpace[3u]; + if (_e396 <= 0f) { continue; } - let _e395 = lightSpace; - let _e398 = lightSpace[3u]; - candidate = (_e395.xyz / vec3(_e398)); - let _e402 = candidate[0u]; - let _e406 = candidate[1u]; - inTile = vec2(((_e402 * 0.5f) + 0.5f), (0.5f - (_e406 * 0.5f))); - let _e411 = inTile[0u]; - let _e412 = (_e411 < 0f); - phi_3703_ = _e412; - if !(_e412) { - let _e415 = inTile[0u]; - phi_3703_ = (_e415 > 1f); - } - let _e418 = phi_3703_; - phi_3710_ = _e418; - if !(_e418) { - let _e421 = inTile[1u]; - phi_3710_ = (_e421 < 0f); - } - let _e424 = phi_3710_; - phi_3717_ = _e424; - if !(_e424) { - let _e427 = inTile[1u]; - phi_3717_ = (_e427 > 1f); - } - let _e430 = phi_3717_; - if _e430 { + let _e398 = lightSpace; + let _e401 = lightSpace[3u]; + candidate = (_e398.xyz / vec3(_e401)); + let _e405 = candidate[0u]; + let _e409 = candidate[1u]; + inTile = vec2(((_e405 * 0.5f) + 0.5f), (0.5f - (_e409 * 0.5f))); + let _e414 = inTile[0u]; + let _e415 = (_e414 < 0f); + phi_3708_ = _e415; + if !(_e415) { + let _e418 = inTile[0u]; + phi_3708_ = (_e418 > 1f); + } + let _e421 = phi_3708_; + phi_3715_ = _e421; + if !(_e421) { + let _e424 = inTile[1u]; + phi_3715_ = (_e424 < 0f); + } + let _e427 = phi_3715_; + phi_3722_ = _e427; + if !(_e427) { + let _e430 = inTile[1u]; + phi_3722_ = (_e430 > 1f); + } + let _e433 = phi_3722_; + if _e433 { continue; } - let _e432 = candidate[2u]; - if (_e432 > 1f) { - let _e434 = which; - let _e435 = cascadeCount; - if (_e434 < (_e435 - 1i)) { + let _e435 = candidate[2u]; + if (_e435 > 1f) { + let _e437 = which; + let _e438 = cascadeCount; + if (_e437 < (_e438 - 1i)) { continue; } candidate[2u] = 1f; } - let _e440 = inTile[0u]; - let _e441 = which; - let _e444 = cascadeCount; - let _e448 = inTile[1u]; - uv_4 = vec2(((_e440 + f32(_e441)) / f32(_e444)), _e448); - let _e450 = candidate; - projected = _e450; - let _e451 = which; - cascade = _e451; - let _e452 = texelMetres; - cascadeTexel = _e452; - let _e455 = matrix[0][2u]; - let _e458 = matrix[1][2u]; - let _e461 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e455, _e458, _e461)), 0.000001f)); - let _e467 = (*s_2).n; - let _e470 = matrix[0][2u]; - let _e473 = matrix[1][2u]; - let _e476 = matrix[2][2u]; - let _e480 = cascadeDepth; - cosSlope = clamp((abs(dot(_e467, vec3(_e470, _e473, _e476))) * _e480), 0.1f, 1f); - let _e483 = texelMetres; - let _e484 = cosSlope; - let _e485 = cosSlope; - let _e491 = cosSlope; - let _e493 = cascadeDepth; - receiverSlope = (((_e483 * sqrt(max((1f - (_e484 * _e485)), 0f))) / _e491) / _e493); + let _e443 = inTile[0u]; + let _e444 = which; + let _e447 = cascadeCount; + let _e451 = inTile[1u]; + uv_4 = vec2(((_e443 + f32(_e444)) / f32(_e447)), _e451); + let _e453 = candidate; + projected = _e453; + let _e454 = which; + cascade = _e454; + let _e455 = texelMetres; + cascadeTexel = _e455; + let _e458 = matrix[0][2u]; + let _e461 = matrix[1][2u]; + let _e464 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e458, _e461, _e464)), 0.000001f)); + let _e470 = (*s_2).n; + let _e473 = matrix[0][2u]; + let _e476 = matrix[1][2u]; + let _e479 = matrix[2][2u]; + let _e483 = cascadeDepth; + cosSlope = clamp((abs(dot(_e470, vec3(_e473, _e476, _e479))) * _e483), 0.1f, 1f); + let _e486 = texelMetres; + let _e487 = cosSlope; + let _e488 = cosSlope; + let _e494 = cosSlope; + let _e496 = cascadeDepth; + receiverSlope = (((_e486 * sqrt(max((1f - (_e487 * _e488)), 0f))) / _e494) / _e496); found = true; break; } else { break; } continuing { - let _e495 = attempt; - attempt = (_e495 + 1i); + let _e498 = attempt; + attempt = (_e498 + 1i); } } - let _e497 = found; - if !(_e497) { + let _e500 = found; + if !(_e500) { return 1f; } - let _e499 = cascade; - if (_e499 == 0i) { - let _e503 = frag_info.shadow_bias[0u]; - local_10 = _e503; + let _e502 = cascade; + if (_e502 == 0i) { + let _e506 = frag_info.shadow_bias[0u]; + local_10 = _e506; } else { - let _e504 = cascade; - if (_e504 == 1i) { - let _e508 = frag_info.shadow_bias[1u]; - local_11 = _e508; - } else { - let _e511 = frag_info.shadow_bias[2u]; + let _e507 = cascade; + if (_e507 == 1i) { + let _e511 = frag_info.shadow_bias[1u]; local_11 = _e511; - } - let _e512 = local_11; - local_10 = _e512; - } - let _e513 = local_10; - bias = _e513; - let _e516 = frag_info.shadow_params[0u]; - let _e519 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e516, _e519); - let _e521 = cascade; - let _e523 = cascadeCount; - let _e527 = texel_1; - tileLo = (vec2((f32(_e521) / f32(_e523)), 0f) + (_e527 * 0.5f)); - let _e530 = cascade; - let _e533 = cascadeCount; - let _e537 = texel_1; - tileHi = (vec2((f32((_e530 + 1i)) / f32(_e533)), 1f) - (_e537 * 0.5f)); - let _e542 = frag_info.shadow_bias[3u]; - if (_e542 > 0.5f) { - let _e544 = uv_4; - let _e545 = tileLo; - let _e546 = tileHi; - let _e548 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e544, _e545, _e546), 0f); - stored_2 = _e548; - let _e550 = stored_2[1u]; - let _e553 = stored_2[2u]; - let _e556 = stored_2[3u]; - through = clamp(vec3((1f - _e550), (1f - _e553), _e556), vec3(0f), vec3(4f)); - let _e561 = through; - let _e562 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e561, vec3(clamp(_e562, 0f, 1f))); - } - let _e568 = frag_info.ambient_ground[3u]; - softness = _e568; + } else { + let _e514 = frag_info.shadow_bias[2u]; + local_11 = _e514; + } + let _e515 = local_11; + local_10 = _e515; + } + let _e516 = local_10; + bias = _e516; + let _e519 = frag_info.shadow_params[0u]; + let _e522 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e519, _e522); + let _e524 = cascade; + let _e526 = cascadeCount; + let _e530 = texel_1; + tileLo = (vec2((f32(_e524) / f32(_e526)), 0f) + (_e530 * 0.5f)); + let _e533 = cascade; + let _e536 = cascadeCount; + let _e540 = texel_1; + tileHi = (vec2((f32((_e533 + 1i)) / f32(_e536)), 1f) - (_e540 * 0.5f)); + let _e545 = frag_info.shadow_bias[3u]; + if (_e545 > 0.5f) { + let _e547 = uv_4; + let _e548 = tileLo; + let _e549 = tileHi; + let _e551 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e547, _e548, _e549), 0f); + stored_2 = _e551; + let _e553 = stored_2[1u]; + let _e556 = stored_2[2u]; + let _e559 = stored_2[3u]; + through = clamp(vec3((1f - _e553), (1f - _e556), _e559), vec3(0f), vec3(4f)); + let _e564 = through; + let _e565 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e564, vec3(clamp(_e565, 0f, 1f))); + } + let _e571 = frag_info.ambient_ground[3u]; + softness = _e571; lit_2 = 0f; - let _e569 = softness; - if (_e569 < 0f) { - let _e571 = uv_4; - let _e572 = tileLo; - let _e573 = tileHi; - let _e575 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e571, _e572, _e573), 0f); - moments_2 = _e575; - let _e577 = projected[2u]; - let _e578 = bias; - let _e580 = softness; - let _e584 = moments_2; - param_61 = _e584; - param_62 = (_e577 - _e578); - param_63 = clamp((-(_e580) - 1f), 0f, 0.95f); - let _e585 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); - lit_2 = _e585; - } else { - let _e586 = softness; - if (_e586 <= 0f) { + let _e572 = softness; + if (_e572 < 0f) { + let _e574 = uv_4; + let _e575 = tileLo; + let _e576 = tileHi; + let _e578 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e574, _e575, _e576), 0f); + moments_2 = _e578; + let _e580 = projected[2u]; + let _e581 = bias; + let _e583 = softness; + let _e587 = moments_2; + param_61 = _e587; + param_62 = (_e580 - _e581); + param_63 = clamp((-(_e583) - 1f), 0f, 0.95f); + let _e588 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); + lit_2 = _e588; + } else { + let _e589 = softness; + if (_e589 <= 0f) { y = -1i; loop { - let _e588 = y; - if (_e588 <= 1i) { + let _e591 = y; + if (_e591 <= 1i) { x = -1i; loop { - let _e590 = x; - if (_e590 <= 1i) { - let _e592 = uv_4; - let _e593 = x; - let _e595 = y; - let _e598 = texel_1; - let _e601 = tileLo; - let _e602 = tileHi; - let _e604 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e592 + (vec2(f32(_e593), f32(_e595)) * _e598)), _e601, _e602), 0f); - occluder = _e604.x; - let _e607 = projected[2u]; - let _e608 = bias; - let _e610 = occluder; - let _e613 = lit_2; - lit_2 = (_e613 + select(1f, 0f, ((_e607 - _e608) > _e610))); + let _e593 = x; + if (_e593 <= 1i) { + let _e595 = uv_4; + let _e596 = x; + let _e598 = y; + let _e601 = texel_1; + let _e604 = tileLo; + let _e605 = tileHi; + let _e607 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e595 + (vec2(f32(_e596), f32(_e598)) * _e601)), _e604, _e605), 0f); + occluder = _e607.x; + let _e610 = projected[2u]; + let _e611 = bias; + let _e613 = occluder; + let _e616 = lit_2; + lit_2 = (_e616 + select(1f, 0f, ((_e610 - _e611) > _e613))); continue; } else { break; } continuing { - let _e615 = x; - x = (_e615 + 1i); + let _e618 = x; + x = (_e618 + 1i); } } continue; @@ -11461,125 +11471,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e617 = y; - y = (_e617 + 1i); + let _e620 = y; + y = (_e620 + 1i); } } - let _e619 = lit_2; - lit_2 = (_e619 * 0.11111111f); + let _e622 = lit_2; + lit_2 = (_e622 * 0.11111111f); } else { - let _e621 = softness; - spread = tan(min(_e621, 0.5f)); - let _e624 = ShadowNoise_u0028_(); - turn_1 = (_e624 * 6.2831855f); - let _e626 = spread; - let _e628 = projected[2u]; - let _e630 = cascadeDepth; - let _e632 = cascadeTexel; - searchRadius = clamp((((_e626 * _e628) * _e630) / _e632), 1f, 16f); + let _e624 = softness; + spread = tan(min(_e624, 0.5f)); + let _e627 = ShadowNoise_u0028_(); + turn_1 = (_e627 * 6.2831855f); + let _e629 = spread; + let _e631 = projected[2u]; + let _e633 = cascadeDepth; + let _e635 = cascadeTexel; + searchRadius = clamp((((_e629 * _e631) * _e633) / _e635), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_5 = 0i; loop { - let _e635 = i_5; - if (_e635 < 16i) { - let _e637 = i_5; - param_64 = _e637; + let _e638 = i_5; + if (_e638 < 16i) { + let _e640 = i_5; + param_64 = _e640; param_65 = 16i; - let _e638 = turn_1; - param_66 = _e638; - let _e639 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); - disc = _e639; - let _e640 = bias; - let _e641 = disc; - let _e643 = searchRadius; - let _e647 = receiverSlope; - tapBias = (_e640 + (max(((length(_e641) * _e643) - 1.5f), 0f) * _e647)); - let _e650 = uv_4; - let _e651 = disc; - let _e652 = texel_1; - let _e654 = searchRadius; - let _e657 = tileLo; - let _e658 = tileHi; - let _e660 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e650 + ((_e651 * _e652) * _e654)), _e657, _e658), 0f); - occluder_1 = _e660.x; - let _e663 = projected[2u]; - let _e664 = tapBias; - let _e666 = occluder_1; - if ((_e663 - _e664) > _e666) { - let _e668 = occluder_1; - let _e669 = blockerSum; - blockerSum = (_e669 + _e668); - let _e671 = blockerCount; - blockerCount = (_e671 + 1f); + let _e641 = turn_1; + param_66 = _e641; + let _e642 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); + disc = _e642; + let _e643 = bias; + let _e644 = disc; + let _e646 = searchRadius; + let _e650 = receiverSlope; + tapBias = (_e643 + (max(((length(_e644) * _e646) - 1.5f), 0f) * _e650)); + let _e653 = uv_4; + let _e654 = disc; + let _e655 = texel_1; + let _e657 = searchRadius; + let _e660 = tileLo; + let _e661 = tileHi; + let _e663 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e653 + ((_e654 * _e655) * _e657)), _e660, _e661), 0f); + occluder_1 = _e663.x; + let _e666 = projected[2u]; + let _e667 = tapBias; + let _e669 = occluder_1; + if ((_e666 - _e667) > _e669) { + let _e671 = occluder_1; + let _e672 = blockerSum; + blockerSum = (_e672 + _e671); + let _e674 = blockerCount; + blockerCount = (_e674 + 1f); } continue; } else { break; } continuing { - let _e673 = i_5; - i_5 = (_e673 + 1i); + let _e676 = i_5; + i_5 = (_e676 + 1i); } } - let _e675 = blockerCount; - if (_e675 <= 0f) { + let _e678 = blockerCount; + if (_e678 <= 0f) { return 1f; } - let _e678 = projected[2u]; - let _e679 = blockerSum; - let _e680 = blockerCount; - let _e684 = cascadeDepth; - gap = (max((_e678 - (_e679 / _e680)), 0f) * _e684); - let _e686 = spread; - let _e687 = gap; - let _e689 = cascadeTexel; - radius_2 = clamp(((_e686 * _e687) / _e689), 1f, 16f); + let _e681 = projected[2u]; + let _e682 = blockerSum; + let _e683 = blockerCount; + let _e687 = cascadeDepth; + gap = (max((_e681 - (_e682 / _e683)), 0f) * _e687); + let _e689 = spread; + let _e690 = gap; + let _e692 = cascadeTexel; + radius_2 = clamp(((_e689 * _e690) / _e692), 1f, 16f); i_6 = 0i; loop { - let _e692 = i_6; - if (_e692 < 16i) { - let _e694 = turn_1; - let _e696 = i_6; - param_67 = _e696; + let _e695 = i_6; + if (_e695 < 16i) { + let _e697 = turn_1; + let _e699 = i_6; + param_67 = _e699; param_68 = 16i; - param_69 = (_e694 + 1f); - let _e697 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); - disc_1 = _e697; - let _e698 = bias; - let _e699 = disc_1; - let _e701 = radius_2; - let _e705 = receiverSlope; - tapBias_1 = (_e698 + (max(((length(_e699) * _e701) - 1.5f), 0f) * _e705)); - let _e708 = uv_4; - let _e709 = disc_1; - let _e710 = texel_1; - let _e712 = radius_2; - let _e715 = tileLo; - let _e716 = tileHi; - let _e718 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e708 + ((_e709 * _e710) * _e712)), _e715, _e716), 0f); - occluder_2 = _e718.x; - let _e721 = projected[2u]; - let _e722 = tapBias_1; - let _e724 = occluder_2; - let _e727 = lit_2; - lit_2 = (_e727 + select(1f, 0f, ((_e721 - _e722) > _e724))); + param_69 = (_e697 + 1f); + let _e700 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); + disc_1 = _e700; + let _e701 = bias; + let _e702 = disc_1; + let _e704 = radius_2; + let _e708 = receiverSlope; + tapBias_1 = (_e701 + (max(((length(_e702) * _e704) - 1.5f), 0f) * _e708)); + let _e711 = uv_4; + let _e712 = disc_1; + let _e713 = texel_1; + let _e715 = radius_2; + let _e718 = tileLo; + let _e719 = tileHi; + let _e721 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e711 + ((_e712 * _e713) * _e715)), _e718, _e719), 0f); + occluder_2 = _e721.x; + let _e724 = projected[2u]; + let _e725 = tapBias_1; + let _e727 = occluder_2; + let _e730 = lit_2; + lit_2 = (_e730 + select(1f, 0f, ((_e724 - _e725) > _e727))); continue; } else { break; } continuing { - let _e729 = i_6; - i_6 = (_e729 + 1i); + let _e732 = i_6; + i_6 = (_e732 + 1i); } } - let _e731 = lit_2; - lit_2 = (_e731 * 0.0625f); + let _e734 = lit_2; + lit_2 = (_e734 * 0.0625f); } } - let _e733 = lit_2; - let _e734 = strength_1; - return mix(1f, _e733, clamp(_e734, 0f, 1f)); + let _e736 = lit_2; + let _e737 = strength_1; + return mix(1f, _e736, clamp(_e737, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_3: ptr, light_2: ptr, index: ptr) -> f32 { @@ -15051,11 +15061,11 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf var param_95: f32; var tapBias_1: f32; var occluder_2: f32; - var phi_3870_: bool; - var phi_3881_: bool; - var phi_4042_: bool; - var phi_4049_: bool; - var phi_4056_: bool; + var phi_3875_: bool; + var phi_3886_: bool; + var phi_4047_: bool; + var phi_4054_: bool; + var phi_4061_: bool; let _e276 = frag_info.shadow_params[3u]; strength_1 = _e276; @@ -15073,29 +15083,34 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf let _e291 = v_world_position_1; let _e293 = frag_info.camera_position; viewDistance = length((_e291 - _e293.xyz)); + let _e297 = Orthographic_u0028_(); + if _e297 { + let _e298 = ViewDepth_u0028_(); + viewDistance = max(_e298, 0f); + } cascade = 0i; - let _e297 = cascadeCount; - let _e298 = (_e297 > 1i); - phi_3870_ = _e298; - if _e298 { - let _e299 = viewDistance; - let _e302 = frag_info.shadow_cascades[0u]; - phi_3870_ = (_e299 > _e302); + let _e300 = cascadeCount; + let _e301 = (_e300 > 1i); + phi_3875_ = _e301; + if _e301 { + let _e302 = viewDistance; + let _e305 = frag_info.shadow_cascades[0u]; + phi_3875_ = (_e302 > _e305); } - let _e305 = phi_3870_; - if _e305 { + let _e308 = phi_3875_; + if _e308 { cascade = 1i; } - let _e306 = cascadeCount; - let _e307 = (_e306 > 2i); - phi_3881_ = _e307; - if _e307 { - let _e308 = viewDistance; - let _e311 = frag_info.shadow_cascades[1u]; - phi_3881_ = (_e308 > _e311); + let _e309 = cascadeCount; + let _e310 = (_e309 > 2i); + phi_3886_ = _e310; + if _e310 { + let _e311 = viewDistance; + let _e314 = frag_info.shadow_cascades[1u]; + phi_3886_ = (_e311 > _e314); } - let _e314 = phi_3881_; - if _e314 { + let _e317 = phi_3886_; + if _e317 { cascade = 2i; } uv_4 = vec2(0f, 0f); @@ -15106,247 +15121,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e315 = attempt; - if (_e315 < 3i) { - let _e317 = cascade; - let _e318 = attempt; - which = (_e317 + _e318); - let _e320 = which; - let _e321 = cascadeCount; - if (_e320 >= _e321) { + let _e318 = attempt; + if (_e318 < 3i) { + let _e320 = cascade; + let _e321 = attempt; + which = (_e320 + _e321); + let _e323 = which; + let _e324 = cascadeCount; + if (_e323 >= _e324) { break; } - let _e323 = which; - if (_e323 == 0i) { - let _e326 = frag_info.shadow_matrix; - local_9 = _e326; + let _e326 = which; + if (_e326 == 0i) { + let _e329 = frag_info.shadow_matrix; + local_9 = _e329; } else { - let _e327 = which; - if (_e327 == 1i) { - let _e330 = frag_info.shadow_matrix_far; - local_10 = _e330; + let _e330 = which; + if (_e330 == 1i) { + let _e333 = frag_info.shadow_matrix_far; + local_10 = _e333; } else { - let _e332 = frag_info.shadow_matrix_farthest; - local_10 = _e332; + let _e335 = frag_info.shadow_matrix_farthest; + local_10 = _e335; } - let _e333 = local_10; - local_9 = _e333; - } - let _e334 = local_9; - matrix = _e334; - let _e337 = matrix[0][0u]; - let _e340 = matrix[1][0u]; - let _e343 = matrix[2][0u]; - axisX = vec3(_e337, _e340, _e343); - let _e347 = matrix[0][1u]; - let _e350 = matrix[1][1u]; - let _e353 = matrix[2][1u]; - axisY = vec3(_e347, _e350, _e353); - let _e355 = axisX; - rowX = max(length(_e355), 0.000001f); - let _e358 = axisY; - rowY = max(length(_e358), 0.000001f); - let _e363 = frag_info.shadow_cascades[3u]; - let _e365 = rowX; - texelMetres = ((2f * _e363) / _e365); - let _e369 = frag_info.shadow_cascades[3u]; - let _e372 = (*s_4).n; - let _e373 = axisX; - let _e376 = rowX; - let _e377 = rowX; - let _e381 = (*s_4).n; - let _e382 = axisY; - let _e385 = rowY; - let _e386 = rowY; - reach = ((3f * _e369) * ((abs(dot(_e372, _e373)) / (_e376 * _e377)) + (abs(dot(_e381, _e382)) / (_e385 * _e386)))); - let _e393 = frag_info.shadow_params[2u]; - let _e394 = texelMetres; - flatOffset = min(_e393, _e394); - let _e396 = v_world_position_1; - let _e398 = (*s_4).n; - let _e399 = flatOffset; - let _e400 = reach; - origin_1 = (_e396 + (_e398 * (_e399 + _e400))); - let _e404 = matrix; - let _e405 = origin_1; - lightSpace = (_e404 * vec4(_e405.x, _e405.y, _e405.z, 1f)); - let _e412 = lightSpace[3u]; - if (_e412 <= 0f) { + let _e336 = local_10; + local_9 = _e336; + } + let _e337 = local_9; + matrix = _e337; + let _e340 = matrix[0][0u]; + let _e343 = matrix[1][0u]; + let _e346 = matrix[2][0u]; + axisX = vec3(_e340, _e343, _e346); + let _e350 = matrix[0][1u]; + let _e353 = matrix[1][1u]; + let _e356 = matrix[2][1u]; + axisY = vec3(_e350, _e353, _e356); + let _e358 = axisX; + rowX = max(length(_e358), 0.000001f); + let _e361 = axisY; + rowY = max(length(_e361), 0.000001f); + let _e366 = frag_info.shadow_cascades[3u]; + let _e368 = rowX; + texelMetres = ((2f * _e366) / _e368); + let _e372 = frag_info.shadow_cascades[3u]; + let _e375 = (*s_4).n; + let _e376 = axisX; + let _e379 = rowX; + let _e380 = rowX; + let _e384 = (*s_4).n; + let _e385 = axisY; + let _e388 = rowY; + let _e389 = rowY; + reach = ((3f * _e372) * ((abs(dot(_e375, _e376)) / (_e379 * _e380)) + (abs(dot(_e384, _e385)) / (_e388 * _e389)))); + let _e396 = frag_info.shadow_params[2u]; + let _e397 = texelMetres; + flatOffset = min(_e396, _e397); + let _e399 = v_world_position_1; + let _e401 = (*s_4).n; + let _e402 = flatOffset; + let _e403 = reach; + origin_1 = (_e399 + (_e401 * (_e402 + _e403))); + let _e407 = matrix; + let _e408 = origin_1; + lightSpace = (_e407 * vec4(_e408.x, _e408.y, _e408.z, 1f)); + let _e415 = lightSpace[3u]; + if (_e415 <= 0f) { continue; } - let _e414 = lightSpace; - let _e417 = lightSpace[3u]; - candidate = (_e414.xyz / vec3(_e417)); - let _e421 = candidate[0u]; - let _e425 = candidate[1u]; - inTile = vec2(((_e421 * 0.5f) + 0.5f), (0.5f - (_e425 * 0.5f))); - let _e430 = inTile[0u]; - let _e431 = (_e430 < 0f); - phi_4042_ = _e431; - if !(_e431) { - let _e434 = inTile[0u]; - phi_4042_ = (_e434 > 1f); - } - let _e437 = phi_4042_; - phi_4049_ = _e437; - if !(_e437) { - let _e440 = inTile[1u]; - phi_4049_ = (_e440 < 0f); - } - let _e443 = phi_4049_; - phi_4056_ = _e443; - if !(_e443) { - let _e446 = inTile[1u]; - phi_4056_ = (_e446 > 1f); - } - let _e449 = phi_4056_; - if _e449 { + let _e417 = lightSpace; + let _e420 = lightSpace[3u]; + candidate = (_e417.xyz / vec3(_e420)); + let _e424 = candidate[0u]; + let _e428 = candidate[1u]; + inTile = vec2(((_e424 * 0.5f) + 0.5f), (0.5f - (_e428 * 0.5f))); + let _e433 = inTile[0u]; + let _e434 = (_e433 < 0f); + phi_4047_ = _e434; + if !(_e434) { + let _e437 = inTile[0u]; + phi_4047_ = (_e437 > 1f); + } + let _e440 = phi_4047_; + phi_4054_ = _e440; + if !(_e440) { + let _e443 = inTile[1u]; + phi_4054_ = (_e443 < 0f); + } + let _e446 = phi_4054_; + phi_4061_ = _e446; + if !(_e446) { + let _e449 = inTile[1u]; + phi_4061_ = (_e449 > 1f); + } + let _e452 = phi_4061_; + if _e452 { continue; } - let _e451 = candidate[2u]; - if (_e451 > 1f) { - let _e453 = which; - let _e454 = cascadeCount; - if (_e453 < (_e454 - 1i)) { + let _e454 = candidate[2u]; + if (_e454 > 1f) { + let _e456 = which; + let _e457 = cascadeCount; + if (_e456 < (_e457 - 1i)) { continue; } candidate[2u] = 1f; } - let _e459 = inTile[0u]; - let _e460 = which; - let _e463 = cascadeCount; - let _e467 = inTile[1u]; - uv_4 = vec2(((_e459 + f32(_e460)) / f32(_e463)), _e467); - let _e469 = candidate; - projected = _e469; - let _e470 = which; - cascade = _e470; - let _e471 = texelMetres; - cascadeTexel = _e471; - let _e474 = matrix[0][2u]; - let _e477 = matrix[1][2u]; - let _e480 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e474, _e477, _e480)), 0.000001f)); - let _e486 = (*s_4).n; - let _e489 = matrix[0][2u]; - let _e492 = matrix[1][2u]; - let _e495 = matrix[2][2u]; - let _e499 = cascadeDepth; - cosSlope = clamp((abs(dot(_e486, vec3(_e489, _e492, _e495))) * _e499), 0.1f, 1f); - let _e502 = texelMetres; - let _e503 = cosSlope; - let _e504 = cosSlope; - let _e510 = cosSlope; - let _e512 = cascadeDepth; - receiverSlope = (((_e502 * sqrt(max((1f - (_e503 * _e504)), 0f))) / _e510) / _e512); + let _e462 = inTile[0u]; + let _e463 = which; + let _e466 = cascadeCount; + let _e470 = inTile[1u]; + uv_4 = vec2(((_e462 + f32(_e463)) / f32(_e466)), _e470); + let _e472 = candidate; + projected = _e472; + let _e473 = which; + cascade = _e473; + let _e474 = texelMetres; + cascadeTexel = _e474; + let _e477 = matrix[0][2u]; + let _e480 = matrix[1][2u]; + let _e483 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e477, _e480, _e483)), 0.000001f)); + let _e489 = (*s_4).n; + let _e492 = matrix[0][2u]; + let _e495 = matrix[1][2u]; + let _e498 = matrix[2][2u]; + let _e502 = cascadeDepth; + cosSlope = clamp((abs(dot(_e489, vec3(_e492, _e495, _e498))) * _e502), 0.1f, 1f); + let _e505 = texelMetres; + let _e506 = cosSlope; + let _e507 = cosSlope; + let _e513 = cosSlope; + let _e515 = cascadeDepth; + receiverSlope = (((_e505 * sqrt(max((1f - (_e506 * _e507)), 0f))) / _e513) / _e515); found = true; break; } else { break; } continuing { - let _e514 = attempt; - attempt = (_e514 + 1i); + let _e517 = attempt; + attempt = (_e517 + 1i); } } - let _e516 = found; - if !(_e516) { + let _e519 = found; + if !(_e519) { return 1f; } - let _e518 = cascade; - if (_e518 == 0i) { - let _e522 = frag_info.shadow_bias[0u]; - local_11 = _e522; + let _e521 = cascade; + if (_e521 == 0i) { + let _e525 = frag_info.shadow_bias[0u]; + local_11 = _e525; } else { - let _e523 = cascade; - if (_e523 == 1i) { - let _e527 = frag_info.shadow_bias[1u]; - local_12 = _e527; - } else { - let _e530 = frag_info.shadow_bias[2u]; + let _e526 = cascade; + if (_e526 == 1i) { + let _e530 = frag_info.shadow_bias[1u]; local_12 = _e530; - } - let _e531 = local_12; - local_11 = _e531; - } - let _e532 = local_11; - bias = _e532; - let _e535 = frag_info.shadow_params[0u]; - let _e538 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e535, _e538); - let _e540 = cascade; - let _e542 = cascadeCount; - let _e546 = texel_1; - tileLo = (vec2((f32(_e540) / f32(_e542)), 0f) + (_e546 * 0.5f)); - let _e549 = cascade; - let _e552 = cascadeCount; - let _e556 = texel_1; - tileHi = (vec2((f32((_e549 + 1i)) / f32(_e552)), 1f) - (_e556 * 0.5f)); - let _e561 = frag_info.shadow_bias[3u]; - if (_e561 > 0.5f) { - let _e563 = uv_4; - let _e564 = tileLo; - let _e565 = tileHi; - let _e567 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e563, _e564, _e565), 0f); - stored_2 = _e567; - let _e569 = stored_2[1u]; - let _e572 = stored_2[2u]; - let _e575 = stored_2[3u]; - through = clamp(vec3((1f - _e569), (1f - _e572), _e575), vec3(0f), vec3(4f)); - let _e580 = through; - let _e581 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e580, vec3(clamp(_e581, 0f, 1f))); - } - let _e587 = frag_info.ambient_ground[3u]; - softness = _e587; + } else { + let _e533 = frag_info.shadow_bias[2u]; + local_12 = _e533; + } + let _e534 = local_12; + local_11 = _e534; + } + let _e535 = local_11; + bias = _e535; + let _e538 = frag_info.shadow_params[0u]; + let _e541 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e538, _e541); + let _e543 = cascade; + let _e545 = cascadeCount; + let _e549 = texel_1; + tileLo = (vec2((f32(_e543) / f32(_e545)), 0f) + (_e549 * 0.5f)); + let _e552 = cascade; + let _e555 = cascadeCount; + let _e559 = texel_1; + tileHi = (vec2((f32((_e552 + 1i)) / f32(_e555)), 1f) - (_e559 * 0.5f)); + let _e564 = frag_info.shadow_bias[3u]; + if (_e564 > 0.5f) { + let _e566 = uv_4; + let _e567 = tileLo; + let _e568 = tileHi; + let _e570 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e566, _e567, _e568), 0f); + stored_2 = _e570; + let _e572 = stored_2[1u]; + let _e575 = stored_2[2u]; + let _e578 = stored_2[3u]; + through = clamp(vec3((1f - _e572), (1f - _e575), _e578), vec3(0f), vec3(4f)); + let _e583 = through; + let _e584 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e583, vec3(clamp(_e584, 0f, 1f))); + } + let _e590 = frag_info.ambient_ground[3u]; + softness = _e590; lit_2 = 0f; - let _e588 = softness; - if (_e588 < 0f) { - let _e590 = uv_4; - let _e591 = tileLo; - let _e592 = tileHi; - let _e594 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e590, _e591, _e592), 0f); - moments_2 = _e594; - let _e596 = projected[2u]; - let _e597 = bias; - let _e599 = softness; - let _e603 = moments_2; - param_87 = _e603; - param_88 = (_e596 - _e597); - param_89 = clamp((-(_e599) - 1f), 0f, 0.95f); - let _e604 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_87), (¶m_88), (¶m_89)); - lit_2 = _e604; - } else { - let _e605 = softness; - if (_e605 <= 0f) { + let _e591 = softness; + if (_e591 < 0f) { + let _e593 = uv_4; + let _e594 = tileLo; + let _e595 = tileHi; + let _e597 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e593, _e594, _e595), 0f); + moments_2 = _e597; + let _e599 = projected[2u]; + let _e600 = bias; + let _e602 = softness; + let _e606 = moments_2; + param_87 = _e606; + param_88 = (_e599 - _e600); + param_89 = clamp((-(_e602) - 1f), 0f, 0.95f); + let _e607 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_87), (¶m_88), (¶m_89)); + lit_2 = _e607; + } else { + let _e608 = softness; + if (_e608 <= 0f) { y_1 = -1i; loop { - let _e607 = y_1; - if (_e607 <= 1i) { + let _e610 = y_1; + if (_e610 <= 1i) { x_1 = -1i; loop { - let _e609 = x_1; - if (_e609 <= 1i) { - let _e611 = uv_4; - let _e612 = x_1; - let _e614 = y_1; - let _e617 = texel_1; - let _e620 = tileLo; - let _e621 = tileHi; - let _e623 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e611 + (vec2(f32(_e612), f32(_e614)) * _e617)), _e620, _e621), 0f); - occluder = _e623.x; - let _e626 = projected[2u]; - let _e627 = bias; - let _e629 = occluder; - let _e632 = lit_2; - lit_2 = (_e632 + select(1f, 0f, ((_e626 - _e627) > _e629))); + let _e612 = x_1; + if (_e612 <= 1i) { + let _e614 = uv_4; + let _e615 = x_1; + let _e617 = y_1; + let _e620 = texel_1; + let _e623 = tileLo; + let _e624 = tileHi; + let _e626 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e614 + (vec2(f32(_e615), f32(_e617)) * _e620)), _e623, _e624), 0f); + occluder = _e626.x; + let _e629 = projected[2u]; + let _e630 = bias; + let _e632 = occluder; + let _e635 = lit_2; + lit_2 = (_e635 + select(1f, 0f, ((_e629 - _e630) > _e632))); continue; } else { break; } continuing { - let _e634 = x_1; - x_1 = (_e634 + 1i); + let _e637 = x_1; + x_1 = (_e637 + 1i); } } continue; @@ -15354,125 +15369,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e636 = y_1; - y_1 = (_e636 + 1i); + let _e639 = y_1; + y_1 = (_e639 + 1i); } } - let _e638 = lit_2; - lit_2 = (_e638 * 0.11111111f); + let _e641 = lit_2; + lit_2 = (_e641 * 0.11111111f); } else { - let _e640 = softness; - spread = tan(min(_e640, 0.5f)); - let _e643 = ShadowNoise_u0028_(); - turn_1 = (_e643 * 6.2831855f); - let _e645 = spread; - let _e647 = projected[2u]; - let _e649 = cascadeDepth; - let _e651 = cascadeTexel; - searchRadius = clamp((((_e645 * _e647) * _e649) / _e651), 1f, 16f); + let _e643 = softness; + spread = tan(min(_e643, 0.5f)); + let _e646 = ShadowNoise_u0028_(); + turn_1 = (_e646 * 6.2831855f); + let _e648 = spread; + let _e650 = projected[2u]; + let _e652 = cascadeDepth; + let _e654 = cascadeTexel; + searchRadius = clamp((((_e648 * _e650) * _e652) / _e654), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_5 = 0i; loop { - let _e654 = i_5; - if (_e654 < 16i) { - let _e656 = i_5; - param_90 = _e656; + let _e657 = i_5; + if (_e657 < 16i) { + let _e659 = i_5; + param_90 = _e659; param_91 = 16i; - let _e657 = turn_1; - param_92 = _e657; - let _e658 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_90), (¶m_91), (¶m_92)); - disc = _e658; - let _e659 = bias; - let _e660 = disc; - let _e662 = searchRadius; - let _e666 = receiverSlope; - tapBias = (_e659 + (max(((length(_e660) * _e662) - 1.5f), 0f) * _e666)); - let _e669 = uv_4; - let _e670 = disc; - let _e671 = texel_1; - let _e673 = searchRadius; - let _e676 = tileLo; - let _e677 = tileHi; - let _e679 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e669 + ((_e670 * _e671) * _e673)), _e676, _e677), 0f); - occluder_1 = _e679.x; - let _e682 = projected[2u]; - let _e683 = tapBias; - let _e685 = occluder_1; - if ((_e682 - _e683) > _e685) { - let _e687 = occluder_1; - let _e688 = blockerSum; - blockerSum = (_e688 + _e687); - let _e690 = blockerCount; - blockerCount = (_e690 + 1f); + let _e660 = turn_1; + param_92 = _e660; + let _e661 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_90), (¶m_91), (¶m_92)); + disc = _e661; + let _e662 = bias; + let _e663 = disc; + let _e665 = searchRadius; + let _e669 = receiverSlope; + tapBias = (_e662 + (max(((length(_e663) * _e665) - 1.5f), 0f) * _e669)); + let _e672 = uv_4; + let _e673 = disc; + let _e674 = texel_1; + let _e676 = searchRadius; + let _e679 = tileLo; + let _e680 = tileHi; + let _e682 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e672 + ((_e673 * _e674) * _e676)), _e679, _e680), 0f); + occluder_1 = _e682.x; + let _e685 = projected[2u]; + let _e686 = tapBias; + let _e688 = occluder_1; + if ((_e685 - _e686) > _e688) { + let _e690 = occluder_1; + let _e691 = blockerSum; + blockerSum = (_e691 + _e690); + let _e693 = blockerCount; + blockerCount = (_e693 + 1f); } continue; } else { break; } continuing { - let _e692 = i_5; - i_5 = (_e692 + 1i); + let _e695 = i_5; + i_5 = (_e695 + 1i); } } - let _e694 = blockerCount; - if (_e694 <= 0f) { + let _e697 = blockerCount; + if (_e697 <= 0f) { return 1f; } - let _e697 = projected[2u]; - let _e698 = blockerSum; - let _e699 = blockerCount; - let _e703 = cascadeDepth; - gap = (max((_e697 - (_e698 / _e699)), 0f) * _e703); - let _e705 = spread; - let _e706 = gap; - let _e708 = cascadeTexel; - radius_2 = clamp(((_e705 * _e706) / _e708), 1f, 16f); + let _e700 = projected[2u]; + let _e701 = blockerSum; + let _e702 = blockerCount; + let _e706 = cascadeDepth; + gap = (max((_e700 - (_e701 / _e702)), 0f) * _e706); + let _e708 = spread; + let _e709 = gap; + let _e711 = cascadeTexel; + radius_2 = clamp(((_e708 * _e709) / _e711), 1f, 16f); i_6 = 0i; loop { - let _e711 = i_6; - if (_e711 < 16i) { - let _e713 = turn_1; - let _e715 = i_6; - param_93 = _e715; + let _e714 = i_6; + if (_e714 < 16i) { + let _e716 = turn_1; + let _e718 = i_6; + param_93 = _e718; param_94 = 16i; - param_95 = (_e713 + 1f); - let _e716 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_93), (¶m_94), (¶m_95)); - disc_1 = _e716; - let _e717 = bias; - let _e718 = disc_1; - let _e720 = radius_2; - let _e724 = receiverSlope; - tapBias_1 = (_e717 + (max(((length(_e718) * _e720) - 1.5f), 0f) * _e724)); - let _e727 = uv_4; - let _e728 = disc_1; - let _e729 = texel_1; - let _e731 = radius_2; - let _e734 = tileLo; - let _e735 = tileHi; - let _e737 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e727 + ((_e728 * _e729) * _e731)), _e734, _e735), 0f); - occluder_2 = _e737.x; - let _e740 = projected[2u]; - let _e741 = tapBias_1; - let _e743 = occluder_2; - let _e746 = lit_2; - lit_2 = (_e746 + select(1f, 0f, ((_e740 - _e741) > _e743))); + param_95 = (_e716 + 1f); + let _e719 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_93), (¶m_94), (¶m_95)); + disc_1 = _e719; + let _e720 = bias; + let _e721 = disc_1; + let _e723 = radius_2; + let _e727 = receiverSlope; + tapBias_1 = (_e720 + (max(((length(_e721) * _e723) - 1.5f), 0f) * _e727)); + let _e730 = uv_4; + let _e731 = disc_1; + let _e732 = texel_1; + let _e734 = radius_2; + let _e737 = tileLo; + let _e738 = tileHi; + let _e740 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e730 + ((_e731 * _e732) * _e734)), _e737, _e738), 0f); + occluder_2 = _e740.x; + let _e743 = projected[2u]; + let _e744 = tapBias_1; + let _e746 = occluder_2; + let _e749 = lit_2; + lit_2 = (_e749 + select(1f, 0f, ((_e743 - _e744) > _e746))); continue; } else { break; } continuing { - let _e748 = i_6; - i_6 = (_e748 + 1i); + let _e751 = i_6; + i_6 = (_e751 + 1i); } } - let _e750 = lit_2; - lit_2 = (_e750 * 0.0625f); + let _e753 = lit_2; + lit_2 = (_e753 * 0.0625f); } } - let _e752 = lit_2; - let _e753 = strength_1; - return mix(1f, _e752, clamp(_e753, 0f, 1f)); + let _e755 = lit_2; + let _e756 = strength_1; + return mix(1f, _e755, clamp(_e756, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_5: ptr, light_2: ptr, index: ptr) -> f32 { @@ -19784,11 +19799,11 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf var param_142: f32; var tapBias_1: f32; var occluder_2: f32; - var phi_4087_: bool; - var phi_4098_: bool; - var phi_4259_: bool; - var phi_4266_: bool; - var phi_4273_: bool; + var phi_4092_: bool; + var phi_4103_: bool; + var phi_4264_: bool; + var phi_4271_: bool; + var phi_4278_: bool; let _e343 = frag_info.shadow_params[3u]; strength_1 = _e343; @@ -19806,29 +19821,34 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf let _e358 = v_world_position_1; let _e360 = frag_info.camera_position; viewDistance = length((_e358 - _e360.xyz)); + let _e364 = Orthographic_u0028_(); + if _e364 { + let _e365 = ViewDepth_u0028_(); + viewDistance = max(_e365, 0f); + } cascade = 0i; - let _e364 = cascadeCount; - let _e365 = (_e364 > 1i); - phi_4087_ = _e365; - if _e365 { - let _e366 = viewDistance; - let _e369 = frag_info.shadow_cascades[0u]; - phi_4087_ = (_e366 > _e369); - } - let _e372 = phi_4087_; - if _e372 { + let _e367 = cascadeCount; + let _e368 = (_e367 > 1i); + phi_4092_ = _e368; + if _e368 { + let _e369 = viewDistance; + let _e372 = frag_info.shadow_cascades[0u]; + phi_4092_ = (_e369 > _e372); + } + let _e375 = phi_4092_; + if _e375 { cascade = 1i; } - let _e373 = cascadeCount; - let _e374 = (_e373 > 2i); - phi_4098_ = _e374; - if _e374 { - let _e375 = viewDistance; - let _e378 = frag_info.shadow_cascades[1u]; - phi_4098_ = (_e375 > _e378); + let _e376 = cascadeCount; + let _e377 = (_e376 > 2i); + phi_4103_ = _e377; + if _e377 { + let _e378 = viewDistance; + let _e381 = frag_info.shadow_cascades[1u]; + phi_4103_ = (_e378 > _e381); } - let _e381 = phi_4098_; - if _e381 { + let _e384 = phi_4103_; + if _e384 { cascade = 2i; } uv_5 = vec2(0f, 0f); @@ -19839,247 +19859,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e382 = attempt; - if (_e382 < 3i) { - let _e384 = cascade; - let _e385 = attempt; - which = (_e384 + _e385); - let _e387 = which; - let _e388 = cascadeCount; - if (_e387 >= _e388) { + let _e385 = attempt; + if (_e385 < 3i) { + let _e387 = cascade; + let _e388 = attempt; + which = (_e387 + _e388); + let _e390 = which; + let _e391 = cascadeCount; + if (_e390 >= _e391) { break; } - let _e390 = which; - if (_e390 == 0i) { - let _e393 = frag_info.shadow_matrix; - local_12 = _e393; + let _e393 = which; + if (_e393 == 0i) { + let _e396 = frag_info.shadow_matrix; + local_12 = _e396; } else { - let _e394 = which; - if (_e394 == 1i) { - let _e397 = frag_info.shadow_matrix_far; - local_13 = _e397; + let _e397 = which; + if (_e397 == 1i) { + let _e400 = frag_info.shadow_matrix_far; + local_13 = _e400; } else { - let _e399 = frag_info.shadow_matrix_farthest; - local_13 = _e399; + let _e402 = frag_info.shadow_matrix_farthest; + local_13 = _e402; } - let _e400 = local_13; - local_12 = _e400; - } - let _e401 = local_12; - matrix = _e401; - let _e404 = matrix[0][0u]; - let _e407 = matrix[1][0u]; - let _e410 = matrix[2][0u]; - axisX = vec3(_e404, _e407, _e410); - let _e414 = matrix[0][1u]; - let _e417 = matrix[1][1u]; - let _e420 = matrix[2][1u]; - axisY = vec3(_e414, _e417, _e420); - let _e422 = axisX; - rowX = max(length(_e422), 0.000001f); - let _e425 = axisY; - rowY = max(length(_e425), 0.000001f); - let _e430 = frag_info.shadow_cascades[3u]; - let _e432 = rowX; - texelMetres = ((2f * _e430) / _e432); - let _e436 = frag_info.shadow_cascades[3u]; - let _e439 = (*s_5).n; - let _e440 = axisX; - let _e443 = rowX; - let _e444 = rowX; - let _e448 = (*s_5).n; - let _e449 = axisY; - let _e452 = rowY; - let _e453 = rowY; - reach = ((3f * _e436) * ((abs(dot(_e439, _e440)) / (_e443 * _e444)) + (abs(dot(_e448, _e449)) / (_e452 * _e453)))); - let _e460 = frag_info.shadow_params[2u]; - let _e461 = texelMetres; - flatOffset = min(_e460, _e461); - let _e463 = v_world_position_1; - let _e465 = (*s_5).n; - let _e466 = flatOffset; - let _e467 = reach; - origin_1 = (_e463 + (_e465 * (_e466 + _e467))); - let _e471 = matrix; - let _e472 = origin_1; - lightSpace = (_e471 * vec4(_e472.x, _e472.y, _e472.z, 1f)); - let _e479 = lightSpace[3u]; - if (_e479 <= 0f) { + let _e403 = local_13; + local_12 = _e403; + } + let _e404 = local_12; + matrix = _e404; + let _e407 = matrix[0][0u]; + let _e410 = matrix[1][0u]; + let _e413 = matrix[2][0u]; + axisX = vec3(_e407, _e410, _e413); + let _e417 = matrix[0][1u]; + let _e420 = matrix[1][1u]; + let _e423 = matrix[2][1u]; + axisY = vec3(_e417, _e420, _e423); + let _e425 = axisX; + rowX = max(length(_e425), 0.000001f); + let _e428 = axisY; + rowY = max(length(_e428), 0.000001f); + let _e433 = frag_info.shadow_cascades[3u]; + let _e435 = rowX; + texelMetres = ((2f * _e433) / _e435); + let _e439 = frag_info.shadow_cascades[3u]; + let _e442 = (*s_5).n; + let _e443 = axisX; + let _e446 = rowX; + let _e447 = rowX; + let _e451 = (*s_5).n; + let _e452 = axisY; + let _e455 = rowY; + let _e456 = rowY; + reach = ((3f * _e439) * ((abs(dot(_e442, _e443)) / (_e446 * _e447)) + (abs(dot(_e451, _e452)) / (_e455 * _e456)))); + let _e463 = frag_info.shadow_params[2u]; + let _e464 = texelMetres; + flatOffset = min(_e463, _e464); + let _e466 = v_world_position_1; + let _e468 = (*s_5).n; + let _e469 = flatOffset; + let _e470 = reach; + origin_1 = (_e466 + (_e468 * (_e469 + _e470))); + let _e474 = matrix; + let _e475 = origin_1; + lightSpace = (_e474 * vec4(_e475.x, _e475.y, _e475.z, 1f)); + let _e482 = lightSpace[3u]; + if (_e482 <= 0f) { continue; } - let _e481 = lightSpace; - let _e484 = lightSpace[3u]; - candidate = (_e481.xyz / vec3(_e484)); - let _e488 = candidate[0u]; - let _e492 = candidate[1u]; - inTile = vec2(((_e488 * 0.5f) + 0.5f), (0.5f - (_e492 * 0.5f))); - let _e497 = inTile[0u]; - let _e498 = (_e497 < 0f); - phi_4259_ = _e498; - if !(_e498) { - let _e501 = inTile[0u]; - phi_4259_ = (_e501 > 1f); - } - let _e504 = phi_4259_; - phi_4266_ = _e504; - if !(_e504) { - let _e507 = inTile[1u]; - phi_4266_ = (_e507 < 0f); - } - let _e510 = phi_4266_; - phi_4273_ = _e510; - if !(_e510) { - let _e513 = inTile[1u]; - phi_4273_ = (_e513 > 1f); - } - let _e516 = phi_4273_; - if _e516 { + let _e484 = lightSpace; + let _e487 = lightSpace[3u]; + candidate = (_e484.xyz / vec3(_e487)); + let _e491 = candidate[0u]; + let _e495 = candidate[1u]; + inTile = vec2(((_e491 * 0.5f) + 0.5f), (0.5f - (_e495 * 0.5f))); + let _e500 = inTile[0u]; + let _e501 = (_e500 < 0f); + phi_4264_ = _e501; + if !(_e501) { + let _e504 = inTile[0u]; + phi_4264_ = (_e504 > 1f); + } + let _e507 = phi_4264_; + phi_4271_ = _e507; + if !(_e507) { + let _e510 = inTile[1u]; + phi_4271_ = (_e510 < 0f); + } + let _e513 = phi_4271_; + phi_4278_ = _e513; + if !(_e513) { + let _e516 = inTile[1u]; + phi_4278_ = (_e516 > 1f); + } + let _e519 = phi_4278_; + if _e519 { continue; } - let _e518 = candidate[2u]; - if (_e518 > 1f) { - let _e520 = which; - let _e521 = cascadeCount; - if (_e520 < (_e521 - 1i)) { + let _e521 = candidate[2u]; + if (_e521 > 1f) { + let _e523 = which; + let _e524 = cascadeCount; + if (_e523 < (_e524 - 1i)) { continue; } candidate[2u] = 1f; } - let _e526 = inTile[0u]; - let _e527 = which; - let _e530 = cascadeCount; - let _e534 = inTile[1u]; - uv_5 = vec2(((_e526 + f32(_e527)) / f32(_e530)), _e534); - let _e536 = candidate; - projected = _e536; - let _e537 = which; - cascade = _e537; - let _e538 = texelMetres; - cascadeTexel = _e538; - let _e541 = matrix[0][2u]; - let _e544 = matrix[1][2u]; - let _e547 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e541, _e544, _e547)), 0.000001f)); - let _e553 = (*s_5).n; - let _e556 = matrix[0][2u]; - let _e559 = matrix[1][2u]; - let _e562 = matrix[2][2u]; - let _e566 = cascadeDepth; - cosSlope = clamp((abs(dot(_e553, vec3(_e556, _e559, _e562))) * _e566), 0.1f, 1f); - let _e569 = texelMetres; - let _e570 = cosSlope; - let _e571 = cosSlope; - let _e577 = cosSlope; - let _e579 = cascadeDepth; - receiverSlope = (((_e569 * sqrt(max((1f - (_e570 * _e571)), 0f))) / _e577) / _e579); + let _e529 = inTile[0u]; + let _e530 = which; + let _e533 = cascadeCount; + let _e537 = inTile[1u]; + uv_5 = vec2(((_e529 + f32(_e530)) / f32(_e533)), _e537); + let _e539 = candidate; + projected = _e539; + let _e540 = which; + cascade = _e540; + let _e541 = texelMetres; + cascadeTexel = _e541; + let _e544 = matrix[0][2u]; + let _e547 = matrix[1][2u]; + let _e550 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e544, _e547, _e550)), 0.000001f)); + let _e556 = (*s_5).n; + let _e559 = matrix[0][2u]; + let _e562 = matrix[1][2u]; + let _e565 = matrix[2][2u]; + let _e569 = cascadeDepth; + cosSlope = clamp((abs(dot(_e556, vec3(_e559, _e562, _e565))) * _e569), 0.1f, 1f); + let _e572 = texelMetres; + let _e573 = cosSlope; + let _e574 = cosSlope; + let _e580 = cosSlope; + let _e582 = cascadeDepth; + receiverSlope = (((_e572 * sqrt(max((1f - (_e573 * _e574)), 0f))) / _e580) / _e582); found = true; break; } else { break; } continuing { - let _e581 = attempt; - attempt = (_e581 + 1i); + let _e584 = attempt; + attempt = (_e584 + 1i); } } - let _e583 = found; - if !(_e583) { + let _e586 = found; + if !(_e586) { return 1f; } - let _e585 = cascade; - if (_e585 == 0i) { - let _e589 = frag_info.shadow_bias[0u]; - local_14 = _e589; + let _e588 = cascade; + if (_e588 == 0i) { + let _e592 = frag_info.shadow_bias[0u]; + local_14 = _e592; } else { - let _e590 = cascade; - if (_e590 == 1i) { - let _e594 = frag_info.shadow_bias[1u]; - local_15 = _e594; - } else { - let _e597 = frag_info.shadow_bias[2u]; + let _e593 = cascade; + if (_e593 == 1i) { + let _e597 = frag_info.shadow_bias[1u]; local_15 = _e597; - } - let _e598 = local_15; - local_14 = _e598; - } - let _e599 = local_14; - bias = _e599; - let _e602 = frag_info.shadow_params[0u]; - let _e605 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e602, _e605); - let _e607 = cascade; - let _e609 = cascadeCount; - let _e613 = texel_1; - tileLo = (vec2((f32(_e607) / f32(_e609)), 0f) + (_e613 * 0.5f)); - let _e616 = cascade; - let _e619 = cascadeCount; - let _e623 = texel_1; - tileHi = (vec2((f32((_e616 + 1i)) / f32(_e619)), 1f) - (_e623 * 0.5f)); - let _e628 = frag_info.shadow_bias[3u]; - if (_e628 > 0.5f) { - let _e630 = uv_5; - let _e631 = tileLo; - let _e632 = tileHi; - let _e634 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e630, _e631, _e632), 0f); - stored_2 = _e634; - let _e636 = stored_2[1u]; - let _e639 = stored_2[2u]; - let _e642 = stored_2[3u]; - through = clamp(vec3((1f - _e636), (1f - _e639), _e642), vec3(0f), vec3(4f)); - let _e647 = through; - let _e648 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e647, vec3(clamp(_e648, 0f, 1f))); - } - let _e654 = frag_info.ambient_ground[3u]; - softness = _e654; + } else { + let _e600 = frag_info.shadow_bias[2u]; + local_15 = _e600; + } + let _e601 = local_15; + local_14 = _e601; + } + let _e602 = local_14; + bias = _e602; + let _e605 = frag_info.shadow_params[0u]; + let _e608 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e605, _e608); + let _e610 = cascade; + let _e612 = cascadeCount; + let _e616 = texel_1; + tileLo = (vec2((f32(_e610) / f32(_e612)), 0f) + (_e616 * 0.5f)); + let _e619 = cascade; + let _e622 = cascadeCount; + let _e626 = texel_1; + tileHi = (vec2((f32((_e619 + 1i)) / f32(_e622)), 1f) - (_e626 * 0.5f)); + let _e631 = frag_info.shadow_bias[3u]; + if (_e631 > 0.5f) { + let _e633 = uv_5; + let _e634 = tileLo; + let _e635 = tileHi; + let _e637 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e633, _e634, _e635), 0f); + stored_2 = _e637; + let _e639 = stored_2[1u]; + let _e642 = stored_2[2u]; + let _e645 = stored_2[3u]; + through = clamp(vec3((1f - _e639), (1f - _e642), _e645), vec3(0f), vec3(4f)); + let _e650 = through; + let _e651 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e650, vec3(clamp(_e651, 0f, 1f))); + } + let _e657 = frag_info.ambient_ground[3u]; + softness = _e657; lit_2 = 0f; - let _e655 = softness; - if (_e655 < 0f) { - let _e657 = uv_5; - let _e658 = tileLo; - let _e659 = tileHi; - let _e661 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e657, _e658, _e659), 0f); - moments_2 = _e661; - let _e663 = projected[2u]; - let _e664 = bias; - let _e666 = softness; - let _e670 = moments_2; - param_134 = _e670; - param_135 = (_e663 - _e664); - param_136 = clamp((-(_e666) - 1f), 0f, 0.95f); - let _e671 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_134), (¶m_135), (¶m_136)); - lit_2 = _e671; - } else { - let _e672 = softness; - if (_e672 <= 0f) { + let _e658 = softness; + if (_e658 < 0f) { + let _e660 = uv_5; + let _e661 = tileLo; + let _e662 = tileHi; + let _e664 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e660, _e661, _e662), 0f); + moments_2 = _e664; + let _e666 = projected[2u]; + let _e667 = bias; + let _e669 = softness; + let _e673 = moments_2; + param_134 = _e673; + param_135 = (_e666 - _e667); + param_136 = clamp((-(_e669) - 1f), 0f, 0.95f); + let _e674 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_134), (¶m_135), (¶m_136)); + lit_2 = _e674; + } else { + let _e675 = softness; + if (_e675 <= 0f) { y_1 = -1i; loop { - let _e674 = y_1; - if (_e674 <= 1i) { + let _e677 = y_1; + if (_e677 <= 1i) { x_1 = -1i; loop { - let _e676 = x_1; - if (_e676 <= 1i) { - let _e678 = uv_5; - let _e679 = x_1; - let _e681 = y_1; - let _e684 = texel_1; - let _e687 = tileLo; - let _e688 = tileHi; - let _e690 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e678 + (vec2(f32(_e679), f32(_e681)) * _e684)), _e687, _e688), 0f); - occluder = _e690.x; - let _e693 = projected[2u]; - let _e694 = bias; - let _e696 = occluder; - let _e699 = lit_2; - lit_2 = (_e699 + select(1f, 0f, ((_e693 - _e694) > _e696))); + let _e679 = x_1; + if (_e679 <= 1i) { + let _e681 = uv_5; + let _e682 = x_1; + let _e684 = y_1; + let _e687 = texel_1; + let _e690 = tileLo; + let _e691 = tileHi; + let _e693 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e681 + (vec2(f32(_e682), f32(_e684)) * _e687)), _e690, _e691), 0f); + occluder = _e693.x; + let _e696 = projected[2u]; + let _e697 = bias; + let _e699 = occluder; + let _e702 = lit_2; + lit_2 = (_e702 + select(1f, 0f, ((_e696 - _e697) > _e699))); continue; } else { break; } continuing { - let _e701 = x_1; - x_1 = (_e701 + 1i); + let _e704 = x_1; + x_1 = (_e704 + 1i); } } continue; @@ -20087,125 +20107,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e703 = y_1; - y_1 = (_e703 + 1i); + let _e706 = y_1; + y_1 = (_e706 + 1i); } } - let _e705 = lit_2; - lit_2 = (_e705 * 0.11111111f); + let _e708 = lit_2; + lit_2 = (_e708 * 0.11111111f); } else { - let _e707 = softness; - spread = tan(min(_e707, 0.5f)); - let _e710 = ShadowNoise_u0028_(); - turn_1 = (_e710 * 6.2831855f); - let _e712 = spread; - let _e714 = projected[2u]; - let _e716 = cascadeDepth; - let _e718 = cascadeTexel; - searchRadius = clamp((((_e712 * _e714) * _e716) / _e718), 1f, 16f); + let _e710 = softness; + spread = tan(min(_e710, 0.5f)); + let _e713 = ShadowNoise_u0028_(); + turn_1 = (_e713 * 6.2831855f); + let _e715 = spread; + let _e717 = projected[2u]; + let _e719 = cascadeDepth; + let _e721 = cascadeTexel; + searchRadius = clamp((((_e715 * _e717) * _e719) / _e721), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_6 = 0i; loop { - let _e721 = i_6; - if (_e721 < 16i) { - let _e723 = i_6; - param_137 = _e723; + let _e724 = i_6; + if (_e724 < 16i) { + let _e726 = i_6; + param_137 = _e726; param_138 = 16i; - let _e724 = turn_1; - param_139 = _e724; - let _e725 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_137), (¶m_138), (¶m_139)); - disc = _e725; - let _e726 = bias; - let _e727 = disc; - let _e729 = searchRadius; - let _e733 = receiverSlope; - tapBias = (_e726 + (max(((length(_e727) * _e729) - 1.5f), 0f) * _e733)); - let _e736 = uv_5; - let _e737 = disc; - let _e738 = texel_1; - let _e740 = searchRadius; - let _e743 = tileLo; - let _e744 = tileHi; - let _e746 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e736 + ((_e737 * _e738) * _e740)), _e743, _e744), 0f); - occluder_1 = _e746.x; - let _e749 = projected[2u]; - let _e750 = tapBias; - let _e752 = occluder_1; - if ((_e749 - _e750) > _e752) { - let _e754 = occluder_1; - let _e755 = blockerSum; - blockerSum = (_e755 + _e754); - let _e757 = blockerCount; - blockerCount = (_e757 + 1f); + let _e727 = turn_1; + param_139 = _e727; + let _e728 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_137), (¶m_138), (¶m_139)); + disc = _e728; + let _e729 = bias; + let _e730 = disc; + let _e732 = searchRadius; + let _e736 = receiverSlope; + tapBias = (_e729 + (max(((length(_e730) * _e732) - 1.5f), 0f) * _e736)); + let _e739 = uv_5; + let _e740 = disc; + let _e741 = texel_1; + let _e743 = searchRadius; + let _e746 = tileLo; + let _e747 = tileHi; + let _e749 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e739 + ((_e740 * _e741) * _e743)), _e746, _e747), 0f); + occluder_1 = _e749.x; + let _e752 = projected[2u]; + let _e753 = tapBias; + let _e755 = occluder_1; + if ((_e752 - _e753) > _e755) { + let _e757 = occluder_1; + let _e758 = blockerSum; + blockerSum = (_e758 + _e757); + let _e760 = blockerCount; + blockerCount = (_e760 + 1f); } continue; } else { break; } continuing { - let _e759 = i_6; - i_6 = (_e759 + 1i); + let _e762 = i_6; + i_6 = (_e762 + 1i); } } - let _e761 = blockerCount; - if (_e761 <= 0f) { + let _e764 = blockerCount; + if (_e764 <= 0f) { return 1f; } - let _e764 = projected[2u]; - let _e765 = blockerSum; - let _e766 = blockerCount; - let _e770 = cascadeDepth; - gap = (max((_e764 - (_e765 / _e766)), 0f) * _e770); - let _e772 = spread; - let _e773 = gap; - let _e775 = cascadeTexel; - radius_2 = clamp(((_e772 * _e773) / _e775), 1f, 16f); + let _e767 = projected[2u]; + let _e768 = blockerSum; + let _e769 = blockerCount; + let _e773 = cascadeDepth; + gap = (max((_e767 - (_e768 / _e769)), 0f) * _e773); + let _e775 = spread; + let _e776 = gap; + let _e778 = cascadeTexel; + radius_2 = clamp(((_e775 * _e776) / _e778), 1f, 16f); i_7 = 0i; loop { - let _e778 = i_7; - if (_e778 < 16i) { - let _e780 = turn_1; - let _e782 = i_7; - param_140 = _e782; + let _e781 = i_7; + if (_e781 < 16i) { + let _e783 = turn_1; + let _e785 = i_7; + param_140 = _e785; param_141 = 16i; - param_142 = (_e780 + 1f); - let _e783 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_140), (¶m_141), (¶m_142)); - disc_1 = _e783; - let _e784 = bias; - let _e785 = disc_1; - let _e787 = radius_2; - let _e791 = receiverSlope; - tapBias_1 = (_e784 + (max(((length(_e785) * _e787) - 1.5f), 0f) * _e791)); - let _e794 = uv_5; - let _e795 = disc_1; - let _e796 = texel_1; - let _e798 = radius_2; - let _e801 = tileLo; - let _e802 = tileHi; - let _e804 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e794 + ((_e795 * _e796) * _e798)), _e801, _e802), 0f); - occluder_2 = _e804.x; - let _e807 = projected[2u]; - let _e808 = tapBias_1; - let _e810 = occluder_2; - let _e813 = lit_2; - lit_2 = (_e813 + select(1f, 0f, ((_e807 - _e808) > _e810))); + param_142 = (_e783 + 1f); + let _e786 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_140), (¶m_141), (¶m_142)); + disc_1 = _e786; + let _e787 = bias; + let _e788 = disc_1; + let _e790 = radius_2; + let _e794 = receiverSlope; + tapBias_1 = (_e787 + (max(((length(_e788) * _e790) - 1.5f), 0f) * _e794)); + let _e797 = uv_5; + let _e798 = disc_1; + let _e799 = texel_1; + let _e801 = radius_2; + let _e804 = tileLo; + let _e805 = tileHi; + let _e807 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e797 + ((_e798 * _e799) * _e801)), _e804, _e805), 0f); + occluder_2 = _e807.x; + let _e810 = projected[2u]; + let _e811 = tapBias_1; + let _e813 = occluder_2; + let _e816 = lit_2; + lit_2 = (_e816 + select(1f, 0f, ((_e810 - _e811) > _e813))); continue; } else { break; } continuing { - let _e815 = i_7; - i_7 = (_e815 + 1i); + let _e818 = i_7; + i_7 = (_e818 + 1i); } } - let _e817 = lit_2; - lit_2 = (_e817 * 0.0625f); + let _e820 = lit_2; + lit_2 = (_e820 * 0.0625f); } } - let _e819 = lit_2; - let _e820 = strength_1; - return mix(1f, _e819, clamp(_e820, 0f, 1f)); + let _e822 = lit_2; + let _e823 = strength_1; + return mix(1f, _e822, clamp(_e823, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_6: ptr, light_3: ptr, index: ptr) -> f32 { @@ -21680,7 +21700,7 @@ fn ReadLayersOnMaps_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002 var along_1: vec3; var local_41: f32; var local_42: vec3; - var phi_5007_: bool; + var phi_5012_: bool; let _e286 = (*s_12).n_dot_v; let _e290 = g_sheen_roughness; @@ -21729,13 +21749,13 @@ fn ReadLayersOnMaps_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002 let _e344 = turn_2[1u]; along_1 = ((_e338 * _e340) + (_e342 * _e344)); let _e347 = usable; - phi_5007_ = _e347; + phi_5012_ = _e347; if _e347 { let _e348 = along_1; let _e349 = along_1; - phi_5007_ = (dot(_e348, _e349) > 0.000000000001f); + phi_5012_ = (dot(_e348, _e349) > 0.000000000001f); } - let _e353 = phi_5007_; + let _e353 = phi_5012_; if _e353 { let _e356 = layer_info.anisotropy[0u]; local_41 = clamp(_e356, 0f, 1f); @@ -24607,11 +24627,11 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf var param_69: f32; var tapBias_1: f32; var occluder_2: f32; - var phi_3531_: bool; - var phi_3542_: bool; - var phi_3703_: bool; - var phi_3710_: bool; - var phi_3717_: bool; + var phi_3536_: bool; + var phi_3547_: bool; + var phi_3708_: bool; + var phi_3715_: bool; + var phi_3722_: bool; let _e258 = frag_info.shadow_params[3u]; strength_1 = _e258; @@ -24629,29 +24649,34 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf let _e273 = v_world_position_1; let _e275 = frag_info.camera_position; viewDistance = length((_e273 - _e275.xyz)); - cascade = 0i; - let _e279 = cascadeCount; - let _e280 = (_e279 > 1i); - phi_3531_ = _e280; - if _e280 { - let _e281 = viewDistance; - let _e284 = frag_info.shadow_cascades[0u]; - phi_3531_ = (_e281 > _e284); + let _e279 = Orthographic_u0028_(); + if _e279 { + let _e280 = ViewDepth_u0028_(); + viewDistance = max(_e280, 0f); } - let _e287 = phi_3531_; - if _e287 { + cascade = 0i; + let _e282 = cascadeCount; + let _e283 = (_e282 > 1i); + phi_3536_ = _e283; + if _e283 { + let _e284 = viewDistance; + let _e287 = frag_info.shadow_cascades[0u]; + phi_3536_ = (_e284 > _e287); + } + let _e290 = phi_3536_; + if _e290 { cascade = 1i; } - let _e288 = cascadeCount; - let _e289 = (_e288 > 2i); - phi_3542_ = _e289; - if _e289 { - let _e290 = viewDistance; - let _e293 = frag_info.shadow_cascades[1u]; - phi_3542_ = (_e290 > _e293); + let _e291 = cascadeCount; + let _e292 = (_e291 > 2i); + phi_3547_ = _e292; + if _e292 { + let _e293 = viewDistance; + let _e296 = frag_info.shadow_cascades[1u]; + phi_3547_ = (_e293 > _e296); } - let _e296 = phi_3542_; - if _e296 { + let _e299 = phi_3547_; + if _e299 { cascade = 2i; } uv_4 = vec2(0f, 0f); @@ -24662,247 +24687,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e297 = attempt; - if (_e297 < 3i) { - let _e299 = cascade; - let _e300 = attempt; - which = (_e299 + _e300); - let _e302 = which; - let _e303 = cascadeCount; - if (_e302 >= _e303) { + let _e300 = attempt; + if (_e300 < 3i) { + let _e302 = cascade; + let _e303 = attempt; + which = (_e302 + _e303); + let _e305 = which; + let _e306 = cascadeCount; + if (_e305 >= _e306) { break; } - let _e305 = which; - if (_e305 == 0i) { - let _e308 = frag_info.shadow_matrix; - local_8 = _e308; + let _e308 = which; + if (_e308 == 0i) { + let _e311 = frag_info.shadow_matrix; + local_8 = _e311; } else { - let _e309 = which; - if (_e309 == 1i) { - let _e312 = frag_info.shadow_matrix_far; - local_9 = _e312; + let _e312 = which; + if (_e312 == 1i) { + let _e315 = frag_info.shadow_matrix_far; + local_9 = _e315; } else { - let _e314 = frag_info.shadow_matrix_farthest; - local_9 = _e314; + let _e317 = frag_info.shadow_matrix_farthest; + local_9 = _e317; } - let _e315 = local_9; - local_8 = _e315; - } - let _e316 = local_8; - matrix = _e316; - let _e319 = matrix[0][0u]; - let _e322 = matrix[1][0u]; - let _e325 = matrix[2][0u]; - axisX = vec3(_e319, _e322, _e325); - let _e329 = matrix[0][1u]; - let _e332 = matrix[1][1u]; - let _e335 = matrix[2][1u]; - axisY = vec3(_e329, _e332, _e335); - let _e337 = axisX; - rowX = max(length(_e337), 0.000001f); - let _e340 = axisY; - rowY = max(length(_e340), 0.000001f); - let _e345 = frag_info.shadow_cascades[3u]; - let _e347 = rowX; - texelMetres = ((2f * _e345) / _e347); - let _e351 = frag_info.shadow_cascades[3u]; - let _e354 = (*s_2).n; - let _e355 = axisX; - let _e358 = rowX; - let _e359 = rowX; - let _e363 = (*s_2).n; - let _e364 = axisY; - let _e367 = rowY; - let _e368 = rowY; - reach = ((3f * _e351) * ((abs(dot(_e354, _e355)) / (_e358 * _e359)) + (abs(dot(_e363, _e364)) / (_e367 * _e368)))); - let _e375 = frag_info.shadow_params[2u]; - let _e376 = texelMetres; - flatOffset = min(_e375, _e376); - let _e378 = v_world_position_1; - let _e380 = (*s_2).n; - let _e381 = flatOffset; - let _e382 = reach; - origin_1 = (_e378 + (_e380 * (_e381 + _e382))); - let _e386 = matrix; - let _e387 = origin_1; - lightSpace = (_e386 * vec4(_e387.x, _e387.y, _e387.z, 1f)); - let _e394 = lightSpace[3u]; - if (_e394 <= 0f) { + let _e318 = local_9; + local_8 = _e318; + } + let _e319 = local_8; + matrix = _e319; + let _e322 = matrix[0][0u]; + let _e325 = matrix[1][0u]; + let _e328 = matrix[2][0u]; + axisX = vec3(_e322, _e325, _e328); + let _e332 = matrix[0][1u]; + let _e335 = matrix[1][1u]; + let _e338 = matrix[2][1u]; + axisY = vec3(_e332, _e335, _e338); + let _e340 = axisX; + rowX = max(length(_e340), 0.000001f); + let _e343 = axisY; + rowY = max(length(_e343), 0.000001f); + let _e348 = frag_info.shadow_cascades[3u]; + let _e350 = rowX; + texelMetres = ((2f * _e348) / _e350); + let _e354 = frag_info.shadow_cascades[3u]; + let _e357 = (*s_2).n; + let _e358 = axisX; + let _e361 = rowX; + let _e362 = rowX; + let _e366 = (*s_2).n; + let _e367 = axisY; + let _e370 = rowY; + let _e371 = rowY; + reach = ((3f * _e354) * ((abs(dot(_e357, _e358)) / (_e361 * _e362)) + (abs(dot(_e366, _e367)) / (_e370 * _e371)))); + let _e378 = frag_info.shadow_params[2u]; + let _e379 = texelMetres; + flatOffset = min(_e378, _e379); + let _e381 = v_world_position_1; + let _e383 = (*s_2).n; + let _e384 = flatOffset; + let _e385 = reach; + origin_1 = (_e381 + (_e383 * (_e384 + _e385))); + let _e389 = matrix; + let _e390 = origin_1; + lightSpace = (_e389 * vec4(_e390.x, _e390.y, _e390.z, 1f)); + let _e397 = lightSpace[3u]; + if (_e397 <= 0f) { continue; } - let _e396 = lightSpace; - let _e399 = lightSpace[3u]; - candidate = (_e396.xyz / vec3(_e399)); - let _e403 = candidate[0u]; - let _e407 = candidate[1u]; - inTile = vec2(((_e403 * 0.5f) + 0.5f), (0.5f - (_e407 * 0.5f))); - let _e412 = inTile[0u]; - let _e413 = (_e412 < 0f); - phi_3703_ = _e413; - if !(_e413) { - let _e416 = inTile[0u]; - phi_3703_ = (_e416 > 1f); - } - let _e419 = phi_3703_; - phi_3710_ = _e419; - if !(_e419) { - let _e422 = inTile[1u]; - phi_3710_ = (_e422 < 0f); - } - let _e425 = phi_3710_; - phi_3717_ = _e425; - if !(_e425) { - let _e428 = inTile[1u]; - phi_3717_ = (_e428 > 1f); - } - let _e431 = phi_3717_; - if _e431 { + let _e399 = lightSpace; + let _e402 = lightSpace[3u]; + candidate = (_e399.xyz / vec3(_e402)); + let _e406 = candidate[0u]; + let _e410 = candidate[1u]; + inTile = vec2(((_e406 * 0.5f) + 0.5f), (0.5f - (_e410 * 0.5f))); + let _e415 = inTile[0u]; + let _e416 = (_e415 < 0f); + phi_3708_ = _e416; + if !(_e416) { + let _e419 = inTile[0u]; + phi_3708_ = (_e419 > 1f); + } + let _e422 = phi_3708_; + phi_3715_ = _e422; + if !(_e422) { + let _e425 = inTile[1u]; + phi_3715_ = (_e425 < 0f); + } + let _e428 = phi_3715_; + phi_3722_ = _e428; + if !(_e428) { + let _e431 = inTile[1u]; + phi_3722_ = (_e431 > 1f); + } + let _e434 = phi_3722_; + if _e434 { continue; } - let _e433 = candidate[2u]; - if (_e433 > 1f) { - let _e435 = which; - let _e436 = cascadeCount; - if (_e435 < (_e436 - 1i)) { + let _e436 = candidate[2u]; + if (_e436 > 1f) { + let _e438 = which; + let _e439 = cascadeCount; + if (_e438 < (_e439 - 1i)) { continue; } candidate[2u] = 1f; } - let _e441 = inTile[0u]; - let _e442 = which; - let _e445 = cascadeCount; - let _e449 = inTile[1u]; - uv_4 = vec2(((_e441 + f32(_e442)) / f32(_e445)), _e449); - let _e451 = candidate; - projected = _e451; - let _e452 = which; - cascade = _e452; - let _e453 = texelMetres; - cascadeTexel = _e453; - let _e456 = matrix[0][2u]; - let _e459 = matrix[1][2u]; - let _e462 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e456, _e459, _e462)), 0.000001f)); - let _e468 = (*s_2).n; - let _e471 = matrix[0][2u]; - let _e474 = matrix[1][2u]; - let _e477 = matrix[2][2u]; - let _e481 = cascadeDepth; - cosSlope = clamp((abs(dot(_e468, vec3(_e471, _e474, _e477))) * _e481), 0.1f, 1f); - let _e484 = texelMetres; - let _e485 = cosSlope; - let _e486 = cosSlope; - let _e492 = cosSlope; - let _e494 = cascadeDepth; - receiverSlope = (((_e484 * sqrt(max((1f - (_e485 * _e486)), 0f))) / _e492) / _e494); + let _e444 = inTile[0u]; + let _e445 = which; + let _e448 = cascadeCount; + let _e452 = inTile[1u]; + uv_4 = vec2(((_e444 + f32(_e445)) / f32(_e448)), _e452); + let _e454 = candidate; + projected = _e454; + let _e455 = which; + cascade = _e455; + let _e456 = texelMetres; + cascadeTexel = _e456; + let _e459 = matrix[0][2u]; + let _e462 = matrix[1][2u]; + let _e465 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e459, _e462, _e465)), 0.000001f)); + let _e471 = (*s_2).n; + let _e474 = matrix[0][2u]; + let _e477 = matrix[1][2u]; + let _e480 = matrix[2][2u]; + let _e484 = cascadeDepth; + cosSlope = clamp((abs(dot(_e471, vec3(_e474, _e477, _e480))) * _e484), 0.1f, 1f); + let _e487 = texelMetres; + let _e488 = cosSlope; + let _e489 = cosSlope; + let _e495 = cosSlope; + let _e497 = cascadeDepth; + receiverSlope = (((_e487 * sqrt(max((1f - (_e488 * _e489)), 0f))) / _e495) / _e497); found = true; break; } else { break; } continuing { - let _e496 = attempt; - attempt = (_e496 + 1i); + let _e499 = attempt; + attempt = (_e499 + 1i); } } - let _e498 = found; - if !(_e498) { + let _e501 = found; + if !(_e501) { return 1f; } - let _e500 = cascade; - if (_e500 == 0i) { - let _e504 = frag_info.shadow_bias[0u]; - local_10 = _e504; + let _e503 = cascade; + if (_e503 == 0i) { + let _e507 = frag_info.shadow_bias[0u]; + local_10 = _e507; } else { - let _e505 = cascade; - if (_e505 == 1i) { - let _e509 = frag_info.shadow_bias[1u]; - local_11 = _e509; - } else { - let _e512 = frag_info.shadow_bias[2u]; + let _e508 = cascade; + if (_e508 == 1i) { + let _e512 = frag_info.shadow_bias[1u]; local_11 = _e512; - } - let _e513 = local_11; - local_10 = _e513; - } - let _e514 = local_10; - bias = _e514; - let _e517 = frag_info.shadow_params[0u]; - let _e520 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e517, _e520); - let _e522 = cascade; - let _e524 = cascadeCount; - let _e528 = texel_1; - tileLo = (vec2((f32(_e522) / f32(_e524)), 0f) + (_e528 * 0.5f)); - let _e531 = cascade; - let _e534 = cascadeCount; - let _e538 = texel_1; - tileHi = (vec2((f32((_e531 + 1i)) / f32(_e534)), 1f) - (_e538 * 0.5f)); - let _e543 = frag_info.shadow_bias[3u]; - if (_e543 > 0.5f) { - let _e545 = uv_4; - let _e546 = tileLo; - let _e547 = tileHi; - let _e549 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e545, _e546, _e547), 0f); - stored_2 = _e549; - let _e551 = stored_2[1u]; - let _e554 = stored_2[2u]; - let _e557 = stored_2[3u]; - through = clamp(vec3((1f - _e551), (1f - _e554), _e557), vec3(0f), vec3(4f)); - let _e562 = through; - let _e563 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e562, vec3(clamp(_e563, 0f, 1f))); - } - let _e569 = frag_info.ambient_ground[3u]; - softness = _e569; + } else { + let _e515 = frag_info.shadow_bias[2u]; + local_11 = _e515; + } + let _e516 = local_11; + local_10 = _e516; + } + let _e517 = local_10; + bias = _e517; + let _e520 = frag_info.shadow_params[0u]; + let _e523 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e520, _e523); + let _e525 = cascade; + let _e527 = cascadeCount; + let _e531 = texel_1; + tileLo = (vec2((f32(_e525) / f32(_e527)), 0f) + (_e531 * 0.5f)); + let _e534 = cascade; + let _e537 = cascadeCount; + let _e541 = texel_1; + tileHi = (vec2((f32((_e534 + 1i)) / f32(_e537)), 1f) - (_e541 * 0.5f)); + let _e546 = frag_info.shadow_bias[3u]; + if (_e546 > 0.5f) { + let _e548 = uv_4; + let _e549 = tileLo; + let _e550 = tileHi; + let _e552 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e548, _e549, _e550), 0f); + stored_2 = _e552; + let _e554 = stored_2[1u]; + let _e557 = stored_2[2u]; + let _e560 = stored_2[3u]; + through = clamp(vec3((1f - _e554), (1f - _e557), _e560), vec3(0f), vec3(4f)); + let _e565 = through; + let _e566 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e565, vec3(clamp(_e566, 0f, 1f))); + } + let _e572 = frag_info.ambient_ground[3u]; + softness = _e572; lit_2 = 0f; - let _e570 = softness; - if (_e570 < 0f) { - let _e572 = uv_4; - let _e573 = tileLo; - let _e574 = tileHi; - let _e576 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e572, _e573, _e574), 0f); - moments_2 = _e576; - let _e578 = projected[2u]; - let _e579 = bias; - let _e581 = softness; - let _e585 = moments_2; - param_61 = _e585; - param_62 = (_e578 - _e579); - param_63 = clamp((-(_e581) - 1f), 0f, 0.95f); - let _e586 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); - lit_2 = _e586; - } else { - let _e587 = softness; - if (_e587 <= 0f) { + let _e573 = softness; + if (_e573 < 0f) { + let _e575 = uv_4; + let _e576 = tileLo; + let _e577 = tileHi; + let _e579 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e575, _e576, _e577), 0f); + moments_2 = _e579; + let _e581 = projected[2u]; + let _e582 = bias; + let _e584 = softness; + let _e588 = moments_2; + param_61 = _e588; + param_62 = (_e581 - _e582); + param_63 = clamp((-(_e584) - 1f), 0f, 0.95f); + let _e589 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); + lit_2 = _e589; + } else { + let _e590 = softness; + if (_e590 <= 0f) { y = -1i; loop { - let _e589 = y; - if (_e589 <= 1i) { + let _e592 = y; + if (_e592 <= 1i) { x = -1i; loop { - let _e591 = x; - if (_e591 <= 1i) { - let _e593 = uv_4; - let _e594 = x; - let _e596 = y; - let _e599 = texel_1; - let _e602 = tileLo; - let _e603 = tileHi; - let _e605 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e593 + (vec2(f32(_e594), f32(_e596)) * _e599)), _e602, _e603), 0f); - occluder = _e605.x; - let _e608 = projected[2u]; - let _e609 = bias; - let _e611 = occluder; - let _e614 = lit_2; - lit_2 = (_e614 + select(1f, 0f, ((_e608 - _e609) > _e611))); + let _e594 = x; + if (_e594 <= 1i) { + let _e596 = uv_4; + let _e597 = x; + let _e599 = y; + let _e602 = texel_1; + let _e605 = tileLo; + let _e606 = tileHi; + let _e608 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e596 + (vec2(f32(_e597), f32(_e599)) * _e602)), _e605, _e606), 0f); + occluder = _e608.x; + let _e611 = projected[2u]; + let _e612 = bias; + let _e614 = occluder; + let _e617 = lit_2; + lit_2 = (_e617 + select(1f, 0f, ((_e611 - _e612) > _e614))); continue; } else { break; } continuing { - let _e616 = x; - x = (_e616 + 1i); + let _e619 = x; + x = (_e619 + 1i); } } continue; @@ -24910,125 +24935,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e618 = y; - y = (_e618 + 1i); + let _e621 = y; + y = (_e621 + 1i); } } - let _e620 = lit_2; - lit_2 = (_e620 * 0.11111111f); + let _e623 = lit_2; + lit_2 = (_e623 * 0.11111111f); } else { - let _e622 = softness; - spread = tan(min(_e622, 0.5f)); - let _e625 = ShadowNoise_u0028_(); - turn_1 = (_e625 * 6.2831855f); - let _e627 = spread; - let _e629 = projected[2u]; - let _e631 = cascadeDepth; - let _e633 = cascadeTexel; - searchRadius = clamp((((_e627 * _e629) * _e631) / _e633), 1f, 16f); + let _e625 = softness; + spread = tan(min(_e625, 0.5f)); + let _e628 = ShadowNoise_u0028_(); + turn_1 = (_e628 * 6.2831855f); + let _e630 = spread; + let _e632 = projected[2u]; + let _e634 = cascadeDepth; + let _e636 = cascadeTexel; + searchRadius = clamp((((_e630 * _e632) * _e634) / _e636), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_5 = 0i; loop { - let _e636 = i_5; - if (_e636 < 16i) { - let _e638 = i_5; - param_64 = _e638; + let _e639 = i_5; + if (_e639 < 16i) { + let _e641 = i_5; + param_64 = _e641; param_65 = 16i; - let _e639 = turn_1; - param_66 = _e639; - let _e640 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); - disc = _e640; - let _e641 = bias; - let _e642 = disc; - let _e644 = searchRadius; - let _e648 = receiverSlope; - tapBias = (_e641 + (max(((length(_e642) * _e644) - 1.5f), 0f) * _e648)); - let _e651 = uv_4; - let _e652 = disc; - let _e653 = texel_1; - let _e655 = searchRadius; - let _e658 = tileLo; - let _e659 = tileHi; - let _e661 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e651 + ((_e652 * _e653) * _e655)), _e658, _e659), 0f); - occluder_1 = _e661.x; - let _e664 = projected[2u]; - let _e665 = tapBias; - let _e667 = occluder_1; - if ((_e664 - _e665) > _e667) { - let _e669 = occluder_1; - let _e670 = blockerSum; - blockerSum = (_e670 + _e669); - let _e672 = blockerCount; - blockerCount = (_e672 + 1f); + let _e642 = turn_1; + param_66 = _e642; + let _e643 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); + disc = _e643; + let _e644 = bias; + let _e645 = disc; + let _e647 = searchRadius; + let _e651 = receiverSlope; + tapBias = (_e644 + (max(((length(_e645) * _e647) - 1.5f), 0f) * _e651)); + let _e654 = uv_4; + let _e655 = disc; + let _e656 = texel_1; + let _e658 = searchRadius; + let _e661 = tileLo; + let _e662 = tileHi; + let _e664 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e654 + ((_e655 * _e656) * _e658)), _e661, _e662), 0f); + occluder_1 = _e664.x; + let _e667 = projected[2u]; + let _e668 = tapBias; + let _e670 = occluder_1; + if ((_e667 - _e668) > _e670) { + let _e672 = occluder_1; + let _e673 = blockerSum; + blockerSum = (_e673 + _e672); + let _e675 = blockerCount; + blockerCount = (_e675 + 1f); } continue; } else { break; } continuing { - let _e674 = i_5; - i_5 = (_e674 + 1i); + let _e677 = i_5; + i_5 = (_e677 + 1i); } } - let _e676 = blockerCount; - if (_e676 <= 0f) { + let _e679 = blockerCount; + if (_e679 <= 0f) { return 1f; } - let _e679 = projected[2u]; - let _e680 = blockerSum; - let _e681 = blockerCount; - let _e685 = cascadeDepth; - gap = (max((_e679 - (_e680 / _e681)), 0f) * _e685); - let _e687 = spread; - let _e688 = gap; - let _e690 = cascadeTexel; - radius_2 = clamp(((_e687 * _e688) / _e690), 1f, 16f); + let _e682 = projected[2u]; + let _e683 = blockerSum; + let _e684 = blockerCount; + let _e688 = cascadeDepth; + gap = (max((_e682 - (_e683 / _e684)), 0f) * _e688); + let _e690 = spread; + let _e691 = gap; + let _e693 = cascadeTexel; + radius_2 = clamp(((_e690 * _e691) / _e693), 1f, 16f); i_6 = 0i; loop { - let _e693 = i_6; - if (_e693 < 16i) { - let _e695 = turn_1; - let _e697 = i_6; - param_67 = _e697; + let _e696 = i_6; + if (_e696 < 16i) { + let _e698 = turn_1; + let _e700 = i_6; + param_67 = _e700; param_68 = 16i; - param_69 = (_e695 + 1f); - let _e698 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); - disc_1 = _e698; - let _e699 = bias; - let _e700 = disc_1; - let _e702 = radius_2; - let _e706 = receiverSlope; - tapBias_1 = (_e699 + (max(((length(_e700) * _e702) - 1.5f), 0f) * _e706)); - let _e709 = uv_4; - let _e710 = disc_1; - let _e711 = texel_1; - let _e713 = radius_2; - let _e716 = tileLo; - let _e717 = tileHi; - let _e719 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e709 + ((_e710 * _e711) * _e713)), _e716, _e717), 0f); - occluder_2 = _e719.x; - let _e722 = projected[2u]; - let _e723 = tapBias_1; - let _e725 = occluder_2; - let _e728 = lit_2; - lit_2 = (_e728 + select(1f, 0f, ((_e722 - _e723) > _e725))); + param_69 = (_e698 + 1f); + let _e701 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); + disc_1 = _e701; + let _e702 = bias; + let _e703 = disc_1; + let _e705 = radius_2; + let _e709 = receiverSlope; + tapBias_1 = (_e702 + (max(((length(_e703) * _e705) - 1.5f), 0f) * _e709)); + let _e712 = uv_4; + let _e713 = disc_1; + let _e714 = texel_1; + let _e716 = radius_2; + let _e719 = tileLo; + let _e720 = tileHi; + let _e722 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e712 + ((_e713 * _e714) * _e716)), _e719, _e720), 0f); + occluder_2 = _e722.x; + let _e725 = projected[2u]; + let _e726 = tapBias_1; + let _e728 = occluder_2; + let _e731 = lit_2; + lit_2 = (_e731 + select(1f, 0f, ((_e725 - _e726) > _e728))); continue; } else { break; } continuing { - let _e730 = i_6; - i_6 = (_e730 + 1i); + let _e733 = i_6; + i_6 = (_e733 + 1i); } } - let _e732 = lit_2; - lit_2 = (_e732 * 0.0625f); + let _e735 = lit_2; + lit_2 = (_e735 * 0.0625f); } } - let _e734 = lit_2; - let _e735 = strength_1; - return mix(1f, _e734, clamp(_e735, 0f, 1f)); + let _e737 = lit_2; + let _e738 = strength_1; + return mix(1f, _e737, clamp(_e738, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_3: ptr, light_2: ptr, index: ptr) -> f32 { @@ -38868,7 +38893,6 @@ fn main_1() { var sigma: f32; var stepTransmittance: f32; var transmittance: f32; - var eye: vec3; var inscatter: f32; var i: i32; var travelled: f32; @@ -38881,141 +38905,138 @@ fn main_1() { var param_7: f32; var shaft: vec3; - let _e91 = v_uv_1; - let _e92 = textureSample(scene_texture_tex, scene_texture_smp, _e91); - scene = _e92; - let _e95 = shaft_info.forward[3u]; - steps = i32((_e95 + 0.5f)); - let _e98 = steps; - if (_e98 < 1i) { - let _e100 = scene; - frag_color = _e100; + let _e90 = v_uv_1; + let _e91 = textureSample(scene_texture_tex, scene_texture_smp, _e90); + scene = _e91; + let _e94 = shaft_info.forward[3u]; + steps = i32((_e94 + 0.5f)); + let _e97 = steps; + if (_e97 < 1i) { + let _e99 = scene; + frag_color = _e99; return; } - let _e102 = v_uv_1[0u]; - let _e106 = v_uv_1[1u]; - xy = vec2(((_e102 * 2f) - 1f), (1f - (_e106 * 2f))); - let _e111 = shaft_info.inverse_view_projection; - let _e112 = xy; - nearH = (_e111 * vec4(_e112.x, _e112.y, 0f, 1f)); - let _e118 = shaft_info.inverse_view_projection; - let _e119 = xy; - farH = (_e118 * vec4(_e119.x, _e119.y, 1f, 1f)); - let _e124 = nearH; - let _e127 = nearH[3u]; - nearPoint = (_e124.xyz / vec3(_e127)); - let _e130 = farH; - let _e133 = farH[3u]; - let _e136 = nearPoint; - along = normalize(((_e130.xyz / vec3(_e133)) - _e136)); - let _e139 = along; - let _e141 = shaft_info.forward; - cosine_1 = max(dot(_e139, _e141.xyz), 0.0001f); - let _e145 = nearPoint; - let _e146 = along; - let _e147 = nearPoint; - let _e149 = shaft_info.camera; - let _e153 = shaft_info.forward; - let _e156 = cosine_1; - origin = (_e145 - (_e146 * (dot((_e147 - _e149.xyz), _e153.xyz) / _e156))); - let _e160 = v_uv_1; - let _e161 = textureSample(surface_texture_tex, surface_texture_smp, _e160); - surfaceDepth = _e161.w; - let _e163 = surfaceDepth; - if (_e163 > 0f) { - let _e165 = surfaceDepth; - let _e166 = cosine_1; - local_6 = (_e165 / _e166); + let _e101 = v_uv_1[0u]; + let _e105 = v_uv_1[1u]; + xy = vec2(((_e101 * 2f) - 1f), (1f - (_e105 * 2f))); + let _e110 = shaft_info.inverse_view_projection; + let _e111 = xy; + nearH = (_e110 * vec4(_e111.x, _e111.y, 0f, 1f)); + let _e117 = shaft_info.inverse_view_projection; + let _e118 = xy; + farH = (_e117 * vec4(_e118.x, _e118.y, 1f, 1f)); + let _e123 = nearH; + let _e126 = nearH[3u]; + nearPoint = (_e123.xyz / vec3(_e126)); + let _e129 = farH; + let _e132 = farH[3u]; + let _e135 = nearPoint; + along = normalize(((_e129.xyz / vec3(_e132)) - _e135)); + let _e138 = along; + let _e140 = shaft_info.forward; + cosine_1 = max(dot(_e138, _e140.xyz), 0.0001f); + let _e144 = nearPoint; + let _e145 = along; + let _e146 = nearPoint; + let _e148 = shaft_info.camera; + let _e152 = shaft_info.forward; + let _e155 = cosine_1; + origin = (_e144 - (_e145 * (dot((_e146 - _e148.xyz), _e152.xyz) / _e155))); + let _e159 = v_uv_1; + let _e160 = textureSample(surface_texture_tex, surface_texture_smp, _e159); + surfaceDepth = _e160.w; + let _e162 = surfaceDepth; + if (_e162 > 0f) { + let _e164 = surfaceDepth; + let _e165 = cosine_1; + local_6 = (_e164 / _e165); } else { local_6 = 1000000000f; } - let _e168 = local_6; - toSurface = _e168; - let _e171 = shaft_info.camera[3u]; - let _e172 = toSurface; - distance_ = min(_e171, _e172); - let _e174 = distance_; - if (_e174 <= 0f) { - let _e176 = scene; - frag_color = _e176; + let _e167 = local_6; + toSurface = _e167; + let _e170 = shaft_info.camera[3u]; + let _e171 = toSurface; + distance_ = min(_e170, _e171); + let _e173 = distance_; + if (_e173 <= 0f) { + let _e175 = scene; + frag_color = _e175; return; } - let _e177 = distance_; - let _e178 = steps; - stride = (_e177 / f32(_e178)); - let _e181 = TargetFragCoord_u0028_(); - param_3 = _e181; - let _e182 = PixelNoise_u0028_vf2_u003b((¶m_3)); - let _e183 = stride; - offset = (_e182 * _e183); - let _e187 = shaft_info.scatter[3u]; - sigma = max(_e187, 0f); - let _e189 = sigma; - let _e191 = stride; - stepTransmittance = exp((-(_e189) * _e191)); - let _e194 = sigma; - let _e196 = offset; - transmittance = exp((-(_e194) * _e196)); - let _e200 = shaft_info.camera; - eye = _e200.xyz; + let _e176 = distance_; + let _e177 = steps; + stride = (_e176 / f32(_e177)); + let _e180 = TargetFragCoord_u0028_(); + param_3 = _e180; + let _e181 = PixelNoise_u0028_vf2_u003b((¶m_3)); + let _e182 = stride; + offset = (_e181 * _e182); + let _e186 = shaft_info.scatter[3u]; + sigma = max(_e186, 0f); + let _e188 = sigma; + let _e190 = stride; + stepTransmittance = exp((-(_e188) * _e190)); + let _e193 = sigma; + let _e195 = offset; + transmittance = exp((-(_e193) * _e195)); inscatter = 0f; i = 0i; loop { - let _e202 = i; - if (_e202 < 64i) { - let _e204 = i; - let _e205 = steps; - if (_e204 >= _e205) { + let _e198 = i; + if (_e198 < 64i) { + let _e200 = i; + let _e201 = steps; + if (_e200 >= _e201) { break; } - let _e207 = offset; - let _e208 = i; - let _e210 = stride; - travelled = (_e207 + (f32(_e208) * _e210)); - let _e213 = origin; - let _e214 = along; + let _e203 = offset; + let _e204 = i; + let _e206 = stride; + travelled = (_e203 + (f32(_e204) * _e206)); + let _e209 = origin; + let _e210 = along; + let _e211 = travelled; + at_3 = (_e209 + (_e210 * _e211)); + let _e214 = at_3; + param_4 = _e214; let _e215 = travelled; - at_3 = (_e213 + (_e214 * _e215)); - let _e218 = at_3; - let _e219 = eye; - let _e222 = at_3; - param_4 = _e222; - param_5 = length((_e218 - _e219)); - let _e223 = LitAt_u0028_vf3_u003b_f1_u003b((¶m_4), (¶m_5)); - lit = _e223; - let _e224 = transmittance; + param_5 = _e215; + let _e216 = LitAt_u0028_vf3_u003b_f1_u003b((¶m_4), (¶m_5)); + lit = _e216; + let _e217 = transmittance; + let _e218 = stepTransmittance; + let _e221 = lit; + let _e223 = inscatter; + inscatter = (_e223 + ((_e217 * (1f - _e218)) * _e221)); let _e225 = stepTransmittance; - let _e228 = lit; - let _e230 = inscatter; - inscatter = (_e230 + ((_e224 * (1f - _e225)) * _e228)); - let _e232 = stepTransmittance; - let _e233 = transmittance; - transmittance = (_e233 * _e232); + let _e226 = transmittance; + transmittance = (_e226 * _e225); continue; } else { break; } continuing { - let _e235 = i; - i = (_e235 + 1i); - } - } - let _e237 = along; - let _e239 = shaft_info.sun; - param_6 = dot(_e237, _e239.xyz); - let _e244 = shaft_info.sun[3u]; - param_7 = _e244; - let _e245 = HenyeyGreenstein_u0028_f1_u003b_f1_u003b((¶m_6), (¶m_7)); - phase = _e245; - let _e247 = shaft_info.scatter; - let _e249 = phase; - let _e250 = inscatter; - shaft = (_e247.xyz * (_e249 * _e250)); - let _e253 = scene; - let _e255 = shaft; - let _e256 = (_e253.xyz + _e255); - let _e258 = scene[3u]; - frag_color = vec4(_e256.x, _e256.y, _e256.z, _e258); + let _e228 = i; + i = (_e228 + 1i); + } + } + let _e230 = along; + let _e232 = shaft_info.sun; + param_6 = dot(_e230, _e232.xyz); + let _e237 = shaft_info.sun[3u]; + param_7 = _e237; + let _e238 = HenyeyGreenstein_u0028_f1_u003b_f1_u003b((¶m_6), (¶m_7)); + phase = _e238; + let _e240 = shaft_info.scatter; + let _e242 = phase; + let _e243 = inscatter; + shaft = (_e240.xyz * (_e242 * _e243)); + let _e246 = scene; + let _e248 = shaft; + let _e249 = (_e246.xyz + _e248); + let _e251 = scene[3u]; + frag_color = vec4(_e249.x, _e249.y, _e249.z, _e251); return; } @@ -40011,7 +40032,6 @@ fn main_1() { var param_22: f32; var param_23: f32; var clustered: bool; - var eye: vec3; var transmittance: f32; var inscatter: vec3; var i_1: i32; @@ -40026,144 +40046,141 @@ fn main_1() { var param_28: vec3; var param_29: f32; - let _e117 = fog_info.forward[3u]; - steps = i32((_e117 + 0.5f)); - let _e121 = v_uv_1[0u]; - let _e125 = v_uv_1[1u]; - xy = vec2(((_e121 * 2f) - 1f), (1f - (_e125 * 2f))); - let _e130 = fog_info.inverse_view_projection; - let _e131 = xy; - nearH = (_e130 * vec4(_e131.x, _e131.y, 0f, 1f)); - let _e137 = fog_info.inverse_view_projection; - let _e138 = xy; - farH = (_e137 * vec4(_e138.x, _e138.y, 1f, 1f)); - let _e143 = nearH; - let _e146 = nearH[3u]; - nearPoint = (_e143.xyz / vec3(_e146)); - let _e149 = farH; - let _e152 = farH[3u]; - let _e155 = nearPoint; - along_1 = normalize(((_e149.xyz / vec3(_e152)) - _e155)); - let _e158 = along_1; - let _e160 = fog_info.forward; - cosine_1 = max(dot(_e158, _e160.xyz), 0.0001f); - let _e164 = nearPoint; - let _e165 = along_1; - let _e166 = nearPoint; - let _e168 = fog_info.camera; - let _e172 = fog_info.forward; - let _e175 = cosine_1; - origin = (_e164 - (_e165 * (dot((_e166 - _e168.xyz), _e172.xyz) / _e175))); - let _e179 = v_uv_1; - let _e180 = textureSample(surface_texture_tex, surface_texture_smp, _e179); - surfaceDepth = _e180.w; - let _e182 = surfaceDepth; - if (_e182 > 0f) { - let _e184 = surfaceDepth; - let _e185 = cosine_1; - local_12 = (_e184 / _e185); + let _e116 = fog_info.forward[3u]; + steps = i32((_e116 + 0.5f)); + let _e120 = v_uv_1[0u]; + let _e124 = v_uv_1[1u]; + xy = vec2(((_e120 * 2f) - 1f), (1f - (_e124 * 2f))); + let _e129 = fog_info.inverse_view_projection; + let _e130 = xy; + nearH = (_e129 * vec4(_e130.x, _e130.y, 0f, 1f)); + let _e136 = fog_info.inverse_view_projection; + let _e137 = xy; + farH = (_e136 * vec4(_e137.x, _e137.y, 1f, 1f)); + let _e142 = nearH; + let _e145 = nearH[3u]; + nearPoint = (_e142.xyz / vec3(_e145)); + let _e148 = farH; + let _e151 = farH[3u]; + let _e154 = nearPoint; + along_1 = normalize(((_e148.xyz / vec3(_e151)) - _e154)); + let _e157 = along_1; + let _e159 = fog_info.forward; + cosine_1 = max(dot(_e157, _e159.xyz), 0.0001f); + let _e163 = nearPoint; + let _e164 = along_1; + let _e165 = nearPoint; + let _e167 = fog_info.camera; + let _e171 = fog_info.forward; + let _e174 = cosine_1; + origin = (_e163 - (_e164 * (dot((_e165 - _e167.xyz), _e171.xyz) / _e174))); + let _e178 = v_uv_1; + let _e179 = textureSample(surface_texture_tex, surface_texture_smp, _e178); + surfaceDepth = _e179.w; + let _e181 = surfaceDepth; + if (_e181 > 0f) { + let _e183 = surfaceDepth; + let _e184 = cosine_1; + local_12 = (_e183 / _e184); } else { local_12 = 1000000000f; } - let _e187 = local_12; - toSurface = _e187; - let _e190 = fog_info.camera[3u]; - let _e191 = toSurface; - distance_2 = min(_e190, _e191); - let _e193 = steps; - let _e195 = distance_2; - if ((_e193 < 1i) || (_e195 <= 0f)) { + let _e186 = local_12; + toSurface = _e186; + let _e189 = fog_info.camera[3u]; + let _e190 = toSurface; + distance_2 = min(_e189, _e190); + let _e192 = steps; + let _e194 = distance_2; + if ((_e192 < 1i) || (_e194 <= 0f)) { frag_color = vec4(0f, 0f, 0f, 1f); return; } - let _e198 = distance_2; - let _e199 = steps; - stride = (_e198 / f32(_e199)); - let _e202 = TargetFragCoord_u0028_(); - param_21 = _e202; - let _e203 = PixelNoise_u0028_vf2_u003b((¶m_21)); - let _e204 = stride; - offset = (_e203 * _e204); - let _e208 = fog_info.sun[3u]; - g_2 = _e208; - let _e209 = along_1; - let _e211 = fog_info.sun; - param_22 = dot(_e209, _e211.xyz); - let _e214 = g_2; - param_23 = _e214; - let _e215 = HenyeyGreenstein_u0028_f1_u003b_f1_u003b((¶m_22), (¶m_23)); - sunPhase = _e215; - let _e218 = fog_info.albedo[3u]; - clustered = (_e218 > 0.5f); - let _e221 = fog_info.camera; - eye = _e221.xyz; + let _e197 = distance_2; + let _e198 = steps; + stride = (_e197 / f32(_e198)); + let _e201 = TargetFragCoord_u0028_(); + param_21 = _e201; + let _e202 = PixelNoise_u0028_vf2_u003b((¶m_21)); + let _e203 = stride; + offset = (_e202 * _e203); + let _e207 = fog_info.sun[3u]; + g_2 = _e207; + let _e208 = along_1; + let _e210 = fog_info.sun; + param_22 = dot(_e208, _e210.xyz); + let _e213 = g_2; + param_23 = _e213; + let _e214 = HenyeyGreenstein_u0028_f1_u003b_f1_u003b((¶m_22), (¶m_23)); + sunPhase = _e214; + let _e217 = fog_info.albedo[3u]; + clustered = (_e217 > 0.5f); transmittance = 1f; inscatter = vec3(0f, 0f, 0f); i_1 = 0i; loop { - let _e223 = i_1; - if (_e223 < 64i) { - let _e225 = i_1; - let _e226 = steps; - if (_e225 >= _e226) { + let _e219 = i_1; + if (_e219 < 64i) { + let _e221 = i_1; + let _e222 = steps; + if (_e221 >= _e222) { break; } - let _e228 = offset; - let _e229 = i_1; - let _e231 = stride; - travelled = (_e228 + (f32(_e229) * _e231)); - let _e234 = origin; - let _e235 = along_1; - let _e236 = travelled; - at_3 = (_e234 + (_e235 * _e236)); - let _e240 = at_3[1u]; - param_24 = _e240; - let _e241 = Density_u0028_f1_u003b((¶m_24)); - let _e243 = stride; - stepTransmittance = exp((-(_e241) * _e243)); - let _e247 = fog_info.sun_radiance; - let _e249 = sunPhase; - let _e250 = at_3; - let _e251 = eye; - let _e254 = at_3; - param_25 = _e254; - param_26 = length((_e250 - _e251)); - let _e255 = LitAt_u0028_vf3_u003b_f1_u003b((¶m_25), (¶m_26)); - let _e259 = fog_info.ambient; - light = ((_e247.xyz * (_e249 * _e255)) + (_e259.xyz * 0.07957747f)); - let _e263 = clustered; - if _e263 { - let _e265 = fog_info.albedo; - let _e267 = at_3; - param_27 = _e267; - let _e268 = along_1; - param_28 = _e268; - let _e269 = g_2; - param_29 = _e269; - let _e270 = ClusterLight_u0028_vf3_u003b_vf3_u003b_f1_u003b((¶m_27), (¶m_28), (¶m_29)); - let _e272 = light; - light = (_e272 + (_e265.xyz * _e270)); - } - let _e274 = light; - let _e275 = transmittance; - let _e276 = stepTransmittance; - let _e280 = inscatter; - inscatter = (_e280 + (_e274 * (_e275 * (1f - _e276)))); - let _e282 = stepTransmittance; - let _e283 = transmittance; - transmittance = (_e283 * _e282); + let _e224 = offset; + let _e225 = i_1; + let _e227 = stride; + travelled = (_e224 + (f32(_e225) * _e227)); + let _e230 = origin; + let _e231 = along_1; + let _e232 = travelled; + at_3 = (_e230 + (_e231 * _e232)); + let _e236 = at_3[1u]; + param_24 = _e236; + let _e237 = Density_u0028_f1_u003b((¶m_24)); + let _e239 = stride; + stepTransmittance = exp((-(_e237) * _e239)); + let _e243 = fog_info.sun_radiance; + let _e245 = sunPhase; + let _e246 = at_3; + param_25 = _e246; + let _e247 = travelled; + param_26 = _e247; + let _e248 = LitAt_u0028_vf3_u003b_f1_u003b((¶m_25), (¶m_26)); + let _e252 = fog_info.ambient; + light = ((_e243.xyz * (_e245 * _e248)) + (_e252.xyz * 0.07957747f)); + let _e256 = clustered; + if _e256 { + let _e258 = fog_info.albedo; + let _e260 = at_3; + param_27 = _e260; + let _e261 = along_1; + param_28 = _e261; + let _e262 = g_2; + param_29 = _e262; + let _e263 = ClusterLight_u0028_vf3_u003b_vf3_u003b_f1_u003b((¶m_27), (¶m_28), (¶m_29)); + let _e265 = light; + light = (_e265 + (_e258.xyz * _e263)); + } + let _e267 = light; + let _e268 = transmittance; + let _e269 = stepTransmittance; + let _e273 = inscatter; + inscatter = (_e273 + (_e267 * (_e268 * (1f - _e269)))); + let _e275 = stepTransmittance; + let _e276 = transmittance; + transmittance = (_e276 * _e275); continue; } else { break; } continuing { - let _e285 = i_1; - i_1 = (_e285 + 1i); + let _e278 = i_1; + i_1 = (_e278 + 1i); } } - let _e287 = inscatter; - let _e288 = transmittance; - frag_color = vec4(_e287.x, _e287.y, _e287.z, _e288); + let _e280 = inscatter; + let _e281 = transmittance; + frag_color = vec4(_e280.x, _e280.y, _e280.z, _e281); return; } @@ -47262,11 +47279,11 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf var param_69: f32; var tapBias_1: f32; var occluder_2: f32; - var phi_2983_: bool; - var phi_2994_: bool; - var phi_3155_: bool; - var phi_3162_: bool; - var phi_3169_: bool; + var phi_2988_: bool; + var phi_2999_: bool; + var phi_3160_: bool; + var phi_3167_: bool; + var phi_3174_: bool; let _e248 = frag_info.shadow_params[3u]; strength_1 = _e248; @@ -47284,29 +47301,34 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf let _e263 = v_world_position_1; let _e265 = frag_info.camera_position; viewDistance = length((_e263 - _e265.xyz)); + let _e269 = Orthographic_u0028_(); + if _e269 { + let _e270 = ViewDepth_u0028_(); + viewDistance = max(_e270, 0f); + } cascade = 0i; - let _e269 = cascadeCount; - let _e270 = (_e269 > 1i); - phi_2983_ = _e270; - if _e270 { - let _e271 = viewDistance; - let _e274 = frag_info.shadow_cascades[0u]; - phi_2983_ = (_e271 > _e274); - } - let _e277 = phi_2983_; - if _e277 { + let _e272 = cascadeCount; + let _e273 = (_e272 > 1i); + phi_2988_ = _e273; + if _e273 { + let _e274 = viewDistance; + let _e277 = frag_info.shadow_cascades[0u]; + phi_2988_ = (_e274 > _e277); + } + let _e280 = phi_2988_; + if _e280 { cascade = 1i; } - let _e278 = cascadeCount; - let _e279 = (_e278 > 2i); - phi_2994_ = _e279; - if _e279 { - let _e280 = viewDistance; - let _e283 = frag_info.shadow_cascades[1u]; - phi_2994_ = (_e280 > _e283); + let _e281 = cascadeCount; + let _e282 = (_e281 > 2i); + phi_2999_ = _e282; + if _e282 { + let _e283 = viewDistance; + let _e286 = frag_info.shadow_cascades[1u]; + phi_2999_ = (_e283 > _e286); } - let _e286 = phi_2994_; - if _e286 { + let _e289 = phi_2999_; + if _e289 { cascade = 2i; } uv_4 = vec2(0f, 0f); @@ -47317,247 +47339,247 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf receiverSlope = 0f; attempt = 0i; loop { - let _e287 = attempt; - if (_e287 < 3i) { - let _e289 = cascade; - let _e290 = attempt; - which = (_e289 + _e290); - let _e292 = which; - let _e293 = cascadeCount; - if (_e292 >= _e293) { + let _e290 = attempt; + if (_e290 < 3i) { + let _e292 = cascade; + let _e293 = attempt; + which = (_e292 + _e293); + let _e295 = which; + let _e296 = cascadeCount; + if (_e295 >= _e296) { break; } - let _e295 = which; - if (_e295 == 0i) { - let _e298 = frag_info.shadow_matrix; - local_8 = _e298; + let _e298 = which; + if (_e298 == 0i) { + let _e301 = frag_info.shadow_matrix; + local_8 = _e301; } else { - let _e299 = which; - if (_e299 == 1i) { - let _e302 = frag_info.shadow_matrix_far; - local_9 = _e302; + let _e302 = which; + if (_e302 == 1i) { + let _e305 = frag_info.shadow_matrix_far; + local_9 = _e305; } else { - let _e304 = frag_info.shadow_matrix_farthest; - local_9 = _e304; + let _e307 = frag_info.shadow_matrix_farthest; + local_9 = _e307; } - let _e305 = local_9; - local_8 = _e305; - } - let _e306 = local_8; - matrix = _e306; - let _e309 = matrix[0][0u]; - let _e312 = matrix[1][0u]; - let _e315 = matrix[2][0u]; - axisX = vec3(_e309, _e312, _e315); - let _e319 = matrix[0][1u]; - let _e322 = matrix[1][1u]; - let _e325 = matrix[2][1u]; - axisY = vec3(_e319, _e322, _e325); - let _e327 = axisX; - rowX = max(length(_e327), 0.000001f); - let _e330 = axisY; - rowY = max(length(_e330), 0.000001f); - let _e335 = frag_info.shadow_cascades[3u]; - let _e337 = rowX; - texelMetres = ((2f * _e335) / _e337); - let _e341 = frag_info.shadow_cascades[3u]; - let _e344 = (*s_2).n; - let _e345 = axisX; - let _e348 = rowX; - let _e349 = rowX; - let _e353 = (*s_2).n; - let _e354 = axisY; - let _e357 = rowY; - let _e358 = rowY; - reach = ((3f * _e341) * ((abs(dot(_e344, _e345)) / (_e348 * _e349)) + (abs(dot(_e353, _e354)) / (_e357 * _e358)))); - let _e365 = frag_info.shadow_params[2u]; - let _e366 = texelMetres; - flatOffset = min(_e365, _e366); - let _e368 = v_world_position_1; - let _e370 = (*s_2).n; - let _e371 = flatOffset; - let _e372 = reach; - origin_1 = (_e368 + (_e370 * (_e371 + _e372))); - let _e376 = matrix; - let _e377 = origin_1; - lightSpace = (_e376 * vec4(_e377.x, _e377.y, _e377.z, 1f)); - let _e384 = lightSpace[3u]; - if (_e384 <= 0f) { + let _e308 = local_9; + local_8 = _e308; + } + let _e309 = local_8; + matrix = _e309; + let _e312 = matrix[0][0u]; + let _e315 = matrix[1][0u]; + let _e318 = matrix[2][0u]; + axisX = vec3(_e312, _e315, _e318); + let _e322 = matrix[0][1u]; + let _e325 = matrix[1][1u]; + let _e328 = matrix[2][1u]; + axisY = vec3(_e322, _e325, _e328); + let _e330 = axisX; + rowX = max(length(_e330), 0.000001f); + let _e333 = axisY; + rowY = max(length(_e333), 0.000001f); + let _e338 = frag_info.shadow_cascades[3u]; + let _e340 = rowX; + texelMetres = ((2f * _e338) / _e340); + let _e344 = frag_info.shadow_cascades[3u]; + let _e347 = (*s_2).n; + let _e348 = axisX; + let _e351 = rowX; + let _e352 = rowX; + let _e356 = (*s_2).n; + let _e357 = axisY; + let _e360 = rowY; + let _e361 = rowY; + reach = ((3f * _e344) * ((abs(dot(_e347, _e348)) / (_e351 * _e352)) + (abs(dot(_e356, _e357)) / (_e360 * _e361)))); + let _e368 = frag_info.shadow_params[2u]; + let _e369 = texelMetres; + flatOffset = min(_e368, _e369); + let _e371 = v_world_position_1; + let _e373 = (*s_2).n; + let _e374 = flatOffset; + let _e375 = reach; + origin_1 = (_e371 + (_e373 * (_e374 + _e375))); + let _e379 = matrix; + let _e380 = origin_1; + lightSpace = (_e379 * vec4(_e380.x, _e380.y, _e380.z, 1f)); + let _e387 = lightSpace[3u]; + if (_e387 <= 0f) { continue; } - let _e386 = lightSpace; - let _e389 = lightSpace[3u]; - candidate = (_e386.xyz / vec3(_e389)); - let _e393 = candidate[0u]; - let _e397 = candidate[1u]; - inTile = vec2(((_e393 * 0.5f) + 0.5f), (0.5f - (_e397 * 0.5f))); - let _e402 = inTile[0u]; - let _e403 = (_e402 < 0f); - phi_3155_ = _e403; - if !(_e403) { - let _e406 = inTile[0u]; - phi_3155_ = (_e406 > 1f); - } - let _e409 = phi_3155_; - phi_3162_ = _e409; - if !(_e409) { - let _e412 = inTile[1u]; - phi_3162_ = (_e412 < 0f); - } - let _e415 = phi_3162_; - phi_3169_ = _e415; - if !(_e415) { - let _e418 = inTile[1u]; - phi_3169_ = (_e418 > 1f); + let _e389 = lightSpace; + let _e392 = lightSpace[3u]; + candidate = (_e389.xyz / vec3(_e392)); + let _e396 = candidate[0u]; + let _e400 = candidate[1u]; + inTile = vec2(((_e396 * 0.5f) + 0.5f), (0.5f - (_e400 * 0.5f))); + let _e405 = inTile[0u]; + let _e406 = (_e405 < 0f); + phi_3160_ = _e406; + if !(_e406) { + let _e409 = inTile[0u]; + phi_3160_ = (_e409 > 1f); + } + let _e412 = phi_3160_; + phi_3167_ = _e412; + if !(_e412) { + let _e415 = inTile[1u]; + phi_3167_ = (_e415 < 0f); } - let _e421 = phi_3169_; - if _e421 { + let _e418 = phi_3167_; + phi_3174_ = _e418; + if !(_e418) { + let _e421 = inTile[1u]; + phi_3174_ = (_e421 > 1f); + } + let _e424 = phi_3174_; + if _e424 { continue; } - let _e423 = candidate[2u]; - if (_e423 > 1f) { - let _e425 = which; - let _e426 = cascadeCount; - if (_e425 < (_e426 - 1i)) { + let _e426 = candidate[2u]; + if (_e426 > 1f) { + let _e428 = which; + let _e429 = cascadeCount; + if (_e428 < (_e429 - 1i)) { continue; } candidate[2u] = 1f; } - let _e431 = inTile[0u]; - let _e432 = which; - let _e435 = cascadeCount; - let _e439 = inTile[1u]; - uv_4 = vec2(((_e431 + f32(_e432)) / f32(_e435)), _e439); - let _e441 = candidate; - projected = _e441; - let _e442 = which; - cascade = _e442; - let _e443 = texelMetres; - cascadeTexel = _e443; - let _e446 = matrix[0][2u]; - let _e449 = matrix[1][2u]; - let _e452 = matrix[2][2u]; - cascadeDepth = (1f / max(length(vec3(_e446, _e449, _e452)), 0.000001f)); - let _e458 = (*s_2).n; - let _e461 = matrix[0][2u]; - let _e464 = matrix[1][2u]; - let _e467 = matrix[2][2u]; - let _e471 = cascadeDepth; - cosSlope = clamp((abs(dot(_e458, vec3(_e461, _e464, _e467))) * _e471), 0.1f, 1f); - let _e474 = texelMetres; - let _e475 = cosSlope; - let _e476 = cosSlope; - let _e482 = cosSlope; - let _e484 = cascadeDepth; - receiverSlope = (((_e474 * sqrt(max((1f - (_e475 * _e476)), 0f))) / _e482) / _e484); + let _e434 = inTile[0u]; + let _e435 = which; + let _e438 = cascadeCount; + let _e442 = inTile[1u]; + uv_4 = vec2(((_e434 + f32(_e435)) / f32(_e438)), _e442); + let _e444 = candidate; + projected = _e444; + let _e445 = which; + cascade = _e445; + let _e446 = texelMetres; + cascadeTexel = _e446; + let _e449 = matrix[0][2u]; + let _e452 = matrix[1][2u]; + let _e455 = matrix[2][2u]; + cascadeDepth = (1f / max(length(vec3(_e449, _e452, _e455)), 0.000001f)); + let _e461 = (*s_2).n; + let _e464 = matrix[0][2u]; + let _e467 = matrix[1][2u]; + let _e470 = matrix[2][2u]; + let _e474 = cascadeDepth; + cosSlope = clamp((abs(dot(_e461, vec3(_e464, _e467, _e470))) * _e474), 0.1f, 1f); + let _e477 = texelMetres; + let _e478 = cosSlope; + let _e479 = cosSlope; + let _e485 = cosSlope; + let _e487 = cascadeDepth; + receiverSlope = (((_e477 * sqrt(max((1f - (_e478 * _e479)), 0f))) / _e485) / _e487); found = true; break; } else { break; } continuing { - let _e486 = attempt; - attempt = (_e486 + 1i); + let _e489 = attempt; + attempt = (_e489 + 1i); } } - let _e488 = found; - if !(_e488) { + let _e491 = found; + if !(_e491) { return 1f; } - let _e490 = cascade; - if (_e490 == 0i) { - let _e494 = frag_info.shadow_bias[0u]; - local_10 = _e494; + let _e493 = cascade; + if (_e493 == 0i) { + let _e497 = frag_info.shadow_bias[0u]; + local_10 = _e497; } else { - let _e495 = cascade; - if (_e495 == 1i) { - let _e499 = frag_info.shadow_bias[1u]; - local_11 = _e499; - } else { - let _e502 = frag_info.shadow_bias[2u]; + let _e498 = cascade; + if (_e498 == 1i) { + let _e502 = frag_info.shadow_bias[1u]; local_11 = _e502; - } - let _e503 = local_11; - local_10 = _e503; - } - let _e504 = local_10; - bias = _e504; - let _e507 = frag_info.shadow_params[0u]; - let _e510 = frag_info.shadow_cascades[3u]; - texel_1 = vec2(_e507, _e510); - let _e512 = cascade; - let _e514 = cascadeCount; - let _e518 = texel_1; - tileLo = (vec2((f32(_e512) / f32(_e514)), 0f) + (_e518 * 0.5f)); - let _e521 = cascade; - let _e524 = cascadeCount; - let _e528 = texel_1; - tileHi = (vec2((f32((_e521 + 1i)) / f32(_e524)), 1f) - (_e528 * 0.5f)); - let _e533 = frag_info.shadow_bias[3u]; - if (_e533 > 0.5f) { - let _e535 = uv_4; - let _e536 = tileLo; - let _e537 = tileHi; - let _e539 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e535, _e536, _e537), 0f); - stored_2 = _e539; - let _e541 = stored_2[1u]; - let _e544 = stored_2[2u]; - let _e547 = stored_2[3u]; - through = clamp(vec3((1f - _e541), (1f - _e544), _e547), vec3(0f), vec3(4f)); - let _e552 = through; - let _e553 = strength_1; - light_transmittance = mix(vec3(1f, 1f, 1f), _e552, vec3(clamp(_e553, 0f, 1f))); - } - let _e559 = frag_info.ambient_ground[3u]; - softness = _e559; + } else { + let _e505 = frag_info.shadow_bias[2u]; + local_11 = _e505; + } + let _e506 = local_11; + local_10 = _e506; + } + let _e507 = local_10; + bias = _e507; + let _e510 = frag_info.shadow_params[0u]; + let _e513 = frag_info.shadow_cascades[3u]; + texel_1 = vec2(_e510, _e513); + let _e515 = cascade; + let _e517 = cascadeCount; + let _e521 = texel_1; + tileLo = (vec2((f32(_e515) / f32(_e517)), 0f) + (_e521 * 0.5f)); + let _e524 = cascade; + let _e527 = cascadeCount; + let _e531 = texel_1; + tileHi = (vec2((f32((_e524 + 1i)) / f32(_e527)), 1f) - (_e531 * 0.5f)); + let _e536 = frag_info.shadow_bias[3u]; + if (_e536 > 0.5f) { + let _e538 = uv_4; + let _e539 = tileLo; + let _e540 = tileHi; + let _e542 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e538, _e539, _e540), 0f); + stored_2 = _e542; + let _e544 = stored_2[1u]; + let _e547 = stored_2[2u]; + let _e550 = stored_2[3u]; + through = clamp(vec3((1f - _e544), (1f - _e547), _e550), vec3(0f), vec3(4f)); + let _e555 = through; + let _e556 = strength_1; + light_transmittance = mix(vec3(1f, 1f, 1f), _e555, vec3(clamp(_e556, 0f, 1f))); + } + let _e562 = frag_info.ambient_ground[3u]; + softness = _e562; lit_2 = 0f; - let _e560 = softness; - if (_e560 < 0f) { - let _e562 = uv_4; - let _e563 = tileLo; - let _e564 = tileHi; - let _e566 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e562, _e563, _e564), 0f); - moments_2 = _e566; - let _e568 = projected[2u]; - let _e569 = bias; - let _e571 = softness; - let _e575 = moments_2; - param_61 = _e575; - param_62 = (_e568 - _e569); - param_63 = clamp((-(_e571) - 1f), 0f, 0.95f); - let _e576 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); - lit_2 = _e576; - } else { - let _e577 = softness; - if (_e577 <= 0f) { + let _e563 = softness; + if (_e563 < 0f) { + let _e565 = uv_4; + let _e566 = tileLo; + let _e567 = tileHi; + let _e569 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp(_e565, _e566, _e567), 0f); + moments_2 = _e569; + let _e571 = projected[2u]; + let _e572 = bias; + let _e574 = softness; + let _e578 = moments_2; + param_61 = _e578; + param_62 = (_e571 - _e572); + param_63 = clamp((-(_e574) - 1f), 0f, 0.95f); + let _e579 = EvsmVisibility_u0028_vf4_u003b_f1_u003b_f1_u003b((¶m_61), (¶m_62), (¶m_63)); + lit_2 = _e579; + } else { + let _e580 = softness; + if (_e580 <= 0f) { y = -1i; loop { - let _e579 = y; - if (_e579 <= 1i) { + let _e582 = y; + if (_e582 <= 1i) { x = -1i; loop { - let _e581 = x; - if (_e581 <= 1i) { - let _e583 = uv_4; - let _e584 = x; - let _e586 = y; - let _e589 = texel_1; - let _e592 = tileLo; - let _e593 = tileHi; - let _e595 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e583 + (vec2(f32(_e584), f32(_e586)) * _e589)), _e592, _e593), 0f); - occluder = _e595.x; - let _e598 = projected[2u]; - let _e599 = bias; - let _e601 = occluder; - let _e604 = lit_2; - lit_2 = (_e604 + select(1f, 0f, ((_e598 - _e599) > _e601))); + let _e584 = x; + if (_e584 <= 1i) { + let _e586 = uv_4; + let _e587 = x; + let _e589 = y; + let _e592 = texel_1; + let _e595 = tileLo; + let _e596 = tileHi; + let _e598 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e586 + (vec2(f32(_e587), f32(_e589)) * _e592)), _e595, _e596), 0f); + occluder = _e598.x; + let _e601 = projected[2u]; + let _e602 = bias; + let _e604 = occluder; + let _e607 = lit_2; + lit_2 = (_e607 + select(1f, 0f, ((_e601 - _e602) > _e604))); continue; } else { break; } continuing { - let _e606 = x; - x = (_e606 + 1i); + let _e609 = x; + x = (_e609 + 1i); } } continue; @@ -47565,125 +47587,125 @@ fn ShadowFactor_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf break; } continuing { - let _e608 = y; - y = (_e608 + 1i); + let _e611 = y; + y = (_e611 + 1i); } } - let _e610 = lit_2; - lit_2 = (_e610 * 0.11111111f); + let _e613 = lit_2; + lit_2 = (_e613 * 0.11111111f); } else { - let _e612 = softness; - spread = tan(min(_e612, 0.5f)); - let _e615 = ShadowNoise_u0028_(); - turn_1 = (_e615 * 6.2831855f); - let _e617 = spread; - let _e619 = projected[2u]; - let _e621 = cascadeDepth; - let _e623 = cascadeTexel; - searchRadius = clamp((((_e617 * _e619) * _e621) / _e623), 1f, 16f); + let _e615 = softness; + spread = tan(min(_e615, 0.5f)); + let _e618 = ShadowNoise_u0028_(); + turn_1 = (_e618 * 6.2831855f); + let _e620 = spread; + let _e622 = projected[2u]; + let _e624 = cascadeDepth; + let _e626 = cascadeTexel; + searchRadius = clamp((((_e620 * _e622) * _e624) / _e626), 1f, 16f); blockerSum = 0f; blockerCount = 0f; i_5 = 0i; loop { - let _e626 = i_5; - if (_e626 < 16i) { - let _e628 = i_5; - param_64 = _e628; + let _e629 = i_5; + if (_e629 < 16i) { + let _e631 = i_5; + param_64 = _e631; param_65 = 16i; - let _e629 = turn_1; - param_66 = _e629; - let _e630 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); - disc = _e630; - let _e631 = bias; - let _e632 = disc; - let _e634 = searchRadius; - let _e638 = receiverSlope; - tapBias = (_e631 + (max(((length(_e632) * _e634) - 1.5f), 0f) * _e638)); - let _e641 = uv_4; - let _e642 = disc; - let _e643 = texel_1; - let _e645 = searchRadius; - let _e648 = tileLo; - let _e649 = tileHi; - let _e651 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e641 + ((_e642 * _e643) * _e645)), _e648, _e649), 0f); - occluder_1 = _e651.x; - let _e654 = projected[2u]; - let _e655 = tapBias; - let _e657 = occluder_1; - if ((_e654 - _e655) > _e657) { - let _e659 = occluder_1; - let _e660 = blockerSum; - blockerSum = (_e660 + _e659); - let _e662 = blockerCount; - blockerCount = (_e662 + 1f); + let _e632 = turn_1; + param_66 = _e632; + let _e633 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_64), (¶m_65), (¶m_66)); + disc = _e633; + let _e634 = bias; + let _e635 = disc; + let _e637 = searchRadius; + let _e641 = receiverSlope; + tapBias = (_e634 + (max(((length(_e635) * _e637) - 1.5f), 0f) * _e641)); + let _e644 = uv_4; + let _e645 = disc; + let _e646 = texel_1; + let _e648 = searchRadius; + let _e651 = tileLo; + let _e652 = tileHi; + let _e654 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e644 + ((_e645 * _e646) * _e648)), _e651, _e652), 0f); + occluder_1 = _e654.x; + let _e657 = projected[2u]; + let _e658 = tapBias; + let _e660 = occluder_1; + if ((_e657 - _e658) > _e660) { + let _e662 = occluder_1; + let _e663 = blockerSum; + blockerSum = (_e663 + _e662); + let _e665 = blockerCount; + blockerCount = (_e665 + 1f); } continue; } else { break; } continuing { - let _e664 = i_5; - i_5 = (_e664 + 1i); + let _e667 = i_5; + i_5 = (_e667 + 1i); } } - let _e666 = blockerCount; - if (_e666 <= 0f) { + let _e669 = blockerCount; + if (_e669 <= 0f) { return 1f; } - let _e669 = projected[2u]; - let _e670 = blockerSum; - let _e671 = blockerCount; - let _e675 = cascadeDepth; - gap = (max((_e669 - (_e670 / _e671)), 0f) * _e675); - let _e677 = spread; - let _e678 = gap; - let _e680 = cascadeTexel; - radius_2 = clamp(((_e677 * _e678) / _e680), 1f, 16f); + let _e672 = projected[2u]; + let _e673 = blockerSum; + let _e674 = blockerCount; + let _e678 = cascadeDepth; + gap = (max((_e672 - (_e673 / _e674)), 0f) * _e678); + let _e680 = spread; + let _e681 = gap; + let _e683 = cascadeTexel; + radius_2 = clamp(((_e680 * _e681) / _e683), 1f, 16f); i_6 = 0i; loop { - let _e683 = i_6; - if (_e683 < 16i) { - let _e685 = turn_1; - let _e687 = i_6; - param_67 = _e687; + let _e686 = i_6; + if (_e686 < 16i) { + let _e688 = turn_1; + let _e690 = i_6; + param_67 = _e690; param_68 = 16i; - param_69 = (_e685 + 1f); - let _e688 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); - disc_1 = _e688; - let _e689 = bias; - let _e690 = disc_1; - let _e692 = radius_2; - let _e696 = receiverSlope; - tapBias_1 = (_e689 + (max(((length(_e690) * _e692) - 1.5f), 0f) * _e696)); - let _e699 = uv_4; - let _e700 = disc_1; - let _e701 = texel_1; - let _e703 = radius_2; - let _e706 = tileLo; - let _e707 = tileHi; - let _e709 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e699 + ((_e700 * _e701) * _e703)), _e706, _e707), 0f); - occluder_2 = _e709.x; - let _e712 = projected[2u]; - let _e713 = tapBias_1; - let _e715 = occluder_2; - let _e718 = lit_2; - lit_2 = (_e718 + select(1f, 0f, ((_e712 - _e713) > _e715))); + param_69 = (_e688 + 1f); + let _e691 = VogelDisc_u0028_i1_u003b_i1_u003b_f1_u003b((¶m_67), (¶m_68), (¶m_69)); + disc_1 = _e691; + let _e692 = bias; + let _e693 = disc_1; + let _e695 = radius_2; + let _e699 = receiverSlope; + tapBias_1 = (_e692 + (max(((length(_e693) * _e695) - 1.5f), 0f) * _e699)); + let _e702 = uv_4; + let _e703 = disc_1; + let _e704 = texel_1; + let _e706 = radius_2; + let _e709 = tileLo; + let _e710 = tileHi; + let _e712 = textureSampleLevel(shadow_texture_tex, shadow_texture_smp, clamp((_e702 + ((_e703 * _e704) * _e706)), _e709, _e710), 0f); + occluder_2 = _e712.x; + let _e715 = projected[2u]; + let _e716 = tapBias_1; + let _e718 = occluder_2; + let _e721 = lit_2; + lit_2 = (_e721 + select(1f, 0f, ((_e715 - _e716) > _e718))); continue; } else { break; } continuing { - let _e720 = i_6; - i_6 = (_e720 + 1i); + let _e723 = i_6; + i_6 = (_e723 + 1i); } } - let _e722 = lit_2; - lit_2 = (_e722 * 0.0625f); + let _e725 = lit_2; + lit_2 = (_e725 * 0.0625f); } } - let _e724 = lit_2; - let _e725 = strength_1; - return mix(1f, _e724, clamp(_e725, 0f, 1f)); + let _e727 = lit_2; + let _e728 = strength_1; + return mix(1f, _e727, clamp(_e728, 0f, 1f)); } fn LightVisibility_u0028_struct_u002d_Surface_u002d_vf3_u002d_f1_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf3_u002d_vf3_u002d_f11_u003b_struct_u002d_LightSample_u002d_vf3_u002d_vf3_u002d_vf3_u002d_f1_u002d_f1_u002d_f1_u002d_f1_u002d_vf31_u003b_i1_u003b(s_3: ptr, light_2: ptr, index: ptr) -> f32 { diff --git a/packages/flutter3d_webgpu/test/cross_backend_test.dart b/packages/flutter3d_webgpu/test/cross_backend_test.dart index efbf68e98..848e99654 100644 --- a/packages/flutter3d_webgpu/test/cross_backend_test.dart +++ b/packages/flutter3d_webgpu/test/cross_backend_test.dart @@ -132,6 +132,7 @@ const Map _budgets = { // the hard cutoff on Impeller — `Material.alphaToCoverage`, two of four. 'alpha-to-coverage': 0.65, 'orthographic-metal': 0.01, + 'orthographic-shadow': 0.01, 'debug-view-split': 0.01, 'teapot-generated-normals': 0.01, 'shadow-teapot': 0.01, diff --git a/packages/flutter3d_webgpu/test/goldens/orthographic-shadow.png b/packages/flutter3d_webgpu/test/goldens/orthographic-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..c45047006ccc2b30dc5b010fd8d0ac3b2b6f855c GIT binary patch literal 24153 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 S_JF)EMDvcWDq7h({C@!XHOtQc literal 0 HcmV?d00001 diff --git a/site/content/core/architecture.md b/site/content/core/architecture.md index 4e8601c29..6ef25b3f2 100644 --- a/site/content/core/architecture.md +++ b/site/content/core/architecture.md @@ -53,7 +53,7 @@ Each exists for a different reason, and none of them is a fallback for another. Its shaders are GLSL ES 3.00 generated from `flutter3d_shaders`. Nothing checked that the generated file was current for a long time, and it cost two failures that reported nothing: a uniform member the browser's copy had never heard of, and a sky shader from before the sky was rewritten. `tool/ci.sh` regenerates it and fails on the diff now, and the compiled Impeller bundle, which cannot be diffed, is held by a freshness rule instead. -**`flutter3d_cpu`** is the one that makes the agreement mean something. Two hardware backends agreeing proves less than it looks like: both are driven by a C API and both rasterise on a GPU, so an assumption shared by graphics hardware would be invisible to the pair of them. This one shares nothing with either, no driver, no shading language, no command buffer. It is also what makes 90 golden scenes checkable in a headless run, and what caught three bugs that every simulation test passed. +**`flutter3d_cpu`** is the one that makes the agreement mean something. Two hardware backends agreeing proves less than it looks like: both are driven by a C API and both rasterise on a GPU, so an assumption shared by graphics hardware would be invisible to the pair of them. This one shares nothing with either, no driver, no shading language, no command buffer. It is also what makes 91 golden scenes checkable in a headless run, and what caught three bugs that every simulation test passed. **`flutter3d_webgpu`** asks a question none of the other three could: whether the *shader* half of the contract is a seam. The first three all read one text (`impellerc` compiles the GLSL, the WebGL generator translates it, the software rasteriser transcribes it into Dart by hand), and WebGPU cannot, because WGSL is a different language and a browser will not take SPIR-V. So its table is generated down a second toolchain, the same manifest through `glslangValidator` and then `naga`, and all thirty-nine stages come out. What it declines it declines by name: no blend constant (WebGPU has no colour/alpha pair for one) and no wireframe (no polygon fill mode in the API at all). Two more used to be on that list. Rendering into a mip is on now and cost no code: a reflection probe convolves its own chain, and a face and a level are `baseArrayLayer` and `baseMipLevel` on an ordinary view here. Block compression is on too, and what it cost was the asking: a WebGPU device gets exactly the features it requested, and requesting one the adapter does not carry rejects the device instead of handing back a lesser one, so the adapter is asked, the intersection is requested, and the capability answers from what was granted. diff --git a/site/content/core/index.md b/site/content/core/index.md index cfee5c2f3..9dd7e27b0 100644 --- a/site/content/core/index.md +++ b/site/content/core/index.md @@ -25,7 +25,7 @@ Nothing in this section is genre knowledge. That property is what made the secon |---|---| | `flutter3d_impeller` | `flutter_gpu`, over Metal and Vulkan. The production one | | `flutter3d_webgl` | WebGL2 in the browser. Runs all three games at a fixed resolution and a lower frame rate, and is what an ordinary web build opens | -| `flutter3d_cpu` | Nothing: it rasterises in Dart, so 90 golden scenes stay checkable with no GPU in the room | +| `flutter3d_cpu` | Nothing: it rasterises in Dart, so 91 golden scenes stay checkable with no GPU in the room | | `flutter3d_webgpu` | WebGPU in a browser that has an adapter. Reached by `--dart-define=FLUTTER3D_WEBGPU=true`, because a build that can try it ships it | `flutter3d_conformance` is the suite each of them passed before it belonged in that table, and [Writing a HAL backend](/core/backends/) is the guide for writing the fifth. diff --git a/site/content/core/rendering.md b/site/content/core/rendering.md index 72227a656..ab338a571 100644 --- a/site/content/core/rendering.md +++ b/site/content/core/rendering.md @@ -487,7 +487,7 @@ It is marched at half resolution and brought up by depth, so a halo behind a pil ```dart SkySettings( - enabled: true, // off by default: ninety goldens are recorded against none + enabled: true, // off by default: ninety-one goldens are recorded against none zenith: Vector3(0.10, 0.22, 0.52), horizon: Vector3(0.42, 0.50, 0.62), nadir: Vector3(0.06, 0.06, 0.07), // what fills the frame looking down at nothing diff --git a/site/content/quickstart.md b/site/content/quickstart.md index 9dac63652..f1503133b 100644 --- a/site/content/quickstart.md +++ b/site/content/quickstart.md @@ -91,7 +91,7 @@ tool/ci.sh # shaders, analyze, every test (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11340 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety scenes stay checkable in a headless run. +There are 11345 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. ## Your own application diff --git a/site/content/reference/packages.md b/site/content/reference/packages.md index 985442fb5..e65f1c6ee 100644 --- a/site/content/reference/packages.md +++ b/site/content/reference/packages.md @@ -60,7 +60,7 @@ A rasteriser written in Dart. `CpuDevice` implements the same HAL, plus PNG outp Not a fallback. Two hardware backends agreeing proves less than it looks like: both are driven by a C API and both rasterise on a GPU, so an assumption shared by graphics hardware would be invisible to the pair of them. This one shares nothing with either, no driver, no shading language, no command buffer. -It is how ninety golden scenes are checkable with no GPU in the room, and it is a dev dependency of every game because three shipped bugs would have been caught by rendering a single frame in a test. It stopped being *only* a dev dependency once `flutter3d_app` started reaching for it as the runtime fallback when Impeller will not start, which makes it a real production path now, if a last-resort one. +It is how ninety-one golden scenes are checkable with no GPU in the room, and it is a dev dependency of every game because three shipped bugs would have been caught by rendering a single frame in a test. It stopped being *only* a dev dependency once `flutter3d_app` started reaching for it as the runtime fallback when Impeller will not start, which makes it a real production path now, if a last-resort one. ### `flutter3d_conformance` The suite any fourth backend would have to pass before it counted as one, plus the cross-backend comparison with per-scene budgets. diff --git a/site/content/reference/testing.md b/site/content/reference/testing.md index d89d101c1..4e7c2a53f 100644 --- a/site/content/reference/testing.md +++ b/site/content/reference/testing.md @@ -1,14 +1,14 @@ --- -description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11340 tests need a GPU. +description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11345 tests need a GPU. --- # Testing -11340 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. +11345 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. | Package | Tests | | Package | Tests | |---|---|---|---|---| -| `flutter3d` | 1659 | | | | +| `flutter3d` | 1662 | | | | | | | | `flutter3d_mesh` | 599 | | | | | `apps/flutter3d_modeler` | 1750 | | `flutter3d_sim` | 691 | | `pad_input` | 67 | @@ -20,7 +20,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flutter3d_game_racing` | 223 | | `flutter3d_webgl` | 62 | | `flutter3d_game_platformer` | 221 | | `flutter3d_hardware` | 70 | | `apps/flutter3d_demo_platformer` | 208 | | `flutter3d_impeller` | 64 | -| `flutter3d_cpu` | 380 | | `apps/flutter3d_demo_strategy` | 50 | +| `flutter3d_cpu` | 382 | | `apps/flutter3d_demo_strategy` | 50 | | `apps/flutter3d_editor` | 234 | | `apps/flutter3d_demo_arcade` | 20 | | `apps/flutter3d_demo_racing` | 162 | | `pointer_lock` | 28 | | `flutter3d_physics` | 269 | | `flutter3d_webgpu` | 193 | @@ -40,7 +40,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flame_flutter3d` | 140 | | `apps/flutter3d_showcase` | 88 | | `flame_multiplayer` | 9 | | `flame_multiplayer_dashwire` | 3 | -The rows sum to 11313 rather than 11340: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. +The rows sum to 11318 rather than 11345: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. `flutter3d_samples` is not in the table and has no `test/` at all: it is test data with two path constants over it, and other packages' decoder tests are what exercise it. `flutter3d_conformance` is missing for a different reason: it is invoked as a script harness rather than through `flutter test`, so it does not surface in a grep of `test(` calls either. See below for what that cost once. @@ -52,7 +52,7 @@ tool/ci.sh # shaders, analyze, every test ## Four independent golden sets, not one -Ninety scenes are rendered four times: through Impeller, through the software rasteriser in `flutter3d_cpu`, through WebGL2, and through WebGPU, the last two in a driven browser. Each backend is held to zero differing pixels against its own set, with a per-channel tolerance of 8. +Ninety-one scenes are rendered four times: through Impeller, through the software rasteriser in `flutter3d_cpu`, through WebGL2, and through WebGPU, the last two in a driven browser. Each backend is held to zero differing pixels against its own set, with a per-channel tolerance of 8. The browser sets are recorded when a branch lands, not beside it (`golden_web.sh` holds one fixed port for the whole of its run), so a new scene is in the other sets for as long as that takes. Which scenes, and what they are waiting for, is `_provisional` in `flutter3d_webgl/test/cross_backend_test.dart`: the comparison is skipped with the reason printed instead of quietly missing, and the check beside it fails the moment a reference lands and the name is still there. @@ -65,7 +65,7 @@ The browser sets are recorded when a branch lands, not beside it (`golden_web.sh {{golden3 shadow-teapot | One scene, three sets: a GPU through Metal, a rasteriser written in Dart, and a browser. The pictures on this site are the Impeller set.}}

-

Independently written implementations agreeing is evidence; one implementation agreeing with itself is not. The software set also keeps ninety scenes checkable in a headless run: recording the other two takes a GPU or a browser, but comparing the committed sets takes neither.

+

Independently written implementations agreeing is evidence; one implementation agreeing with itself is not. The software set also keeps ninety-one scenes checkable in a headless run: recording the other two takes a GPU or a browser, but comparing the committed sets takes neither.

`cross_backend_test.dart` compares them with per-scene budgets, and any new backend has to pass `flutter3d_conformance` before it counts as one. diff --git a/tool/ci.sh b/tool/ci.sh index 1b9a7b7dd..3224d2641 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -9,7 +9,7 @@ # # What it does NOT do, stated so the gap is not mistaken for coverage: # -# * The Impeller half of the golden set. Those ninety scenes render through +# * The Impeller half of the golden set. Those ninety-one scenes render through # flutter_gpu and need a real device, so they run from # packages/flutter3d/tool/golden.sh on a machine with a GPU. The software # half runs here, and cross_backend_test.dart compares the two committed From 6039614abb2e6150d16bf24b0be7e4234132ecdc Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 19:38:35 +0300 Subject: [PATCH 2/6] A level can put its brushes in a draw order MeshNode.drawOrder had no word in the level format, so a stripe painted on a floor of the same stone fought the floor pixel by pixel and the only fix was to patch the node after loading. Brush.drawOrder is that word: optional, left out of the document at nought so every level keeps its bytes and its digest, and refused by the validator outside -128 to 127, the range the renderer's sort key holds, rather than clamped into a tie. Brush surfaces are batched by it, since a batch is drawn as one node with one place in the order, and LevelScene sets it on the node. A cut brush's pieces and a duplicated brush keep it. Nothing the simulation reads depends on it, so an edit of the order alone is look-only: LevelDiff.brushOrder names the brushes, from diffLevel and from a LevelPatch alike, and simulation stays empty. Any other change to the same brush still goes to simulation. --- ARCHITECTURE.md | 2 +- README.md | 2 +- packages/flutter3d_app/CHANGELOG.md | 3 + .../flutter3d_app/test/level_loader_test.dart | 37 ++++ packages/flutter3d_editor_core/CHANGELOG.md | 5 + .../lib/src/editing.dart | 1 + .../lib/src/level_scene.dart | 5 +- packages/flutter3d_sim/CHANGELOG.md | 10 + .../flutter3d_sim/lib/src/level/breaches.dart | 2 + .../flutter3d_sim/lib/src/level/brush.dart | 26 +++ .../lib/src/level/brush_geometry.dart | 7 +- .../lib/src/level/brush_surface.dart | 6 + .../lib/src/level/level_diff.dart | 37 +++- .../lib/src/level/level_patch.dart | 29 ++- .../lib/src/level/level_validator.dart | 15 ++ .../lib/src/level/surface_builder.dart | 5 + .../test/brush_draw_order_test.dart | 178 ++++++++++++++++++ site/content/core/simulation.md | 2 +- site/content/quickstart.md | 2 +- site/content/reference/testing.md | 10 +- 20 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 packages/flutter3d_sim/test/brush_draw_order_test.dart diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0cec52f94..e9a8da34a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2691,7 +2691,7 @@ storage. |---|---| | Style | `dart format` | | Analysis | `flutter analyze` clean across the workspace, no warnings | -| Unit tests | **11345 tests** across 43 packages and 10 applications | +| Unit tests | **11355 tests** across 43 packages and 10 applications | | Structure rules | 35, `dart run tool/structure.dart`, the first CI step | | CI | GitHub Actions over `tool/ci.sh`, on `ubuntu-latest`, with no graphics card | diff --git a/README.md b/README.md index e9fa9ee9d..bbdd17f30 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Or one package at a time: (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11345 tests across forty-three packages and nine applications. The +There are 11355 tests across forty-three packages and nine applications. The only ones that need a GPU are the Impeller half of the golden set. The other half is rendered by the software backend, which is what makes 91 scenes checkable in a headless run. diff --git a/packages/flutter3d_app/CHANGELOG.md b/packages/flutter3d_app/CHANGELOG.md index 17b8a4fa0..041170702 100644 --- a/packages/flutter3d_app/CHANGELOG.md +++ b/packages/flutter3d_app/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- A level's `Brush.drawOrder` reaches the nodes `LevelLoader` builds + (`P7`). + - **`HotSwap.loadMaterial`** loads the bundle the build hook compiled a `.f3dmat` into and watches it — `P8`: the library for the renderer, the materials' lighting models and parameters, and an edit drawn by the next diff --git a/packages/flutter3d_app/test/level_loader_test.dart b/packages/flutter3d_app/test/level_loader_test.dart index c86eb8c72..e680d962a 100644 --- a/packages/flutter3d_app/test/level_loader_test.dart +++ b/packages/flutter3d_app/test/level_loader_test.dart @@ -442,6 +442,43 @@ void main() { ); }); }); + + group('the draw order a document asks for', () { + test('reaches the node, and splits a batch of one material', () async { + // `P7`: a stripe painted on a floor of the same stone is drawn after + // it. The two share a material, so without the order in the batch key + // they would be one node with one place in the order. + // + // Mutation: drop `..drawOrder = surface.drawOrder` from `LevelScene` — + // both nodes load at nought and the stripe fights the floor. + final loaded = await const LevelLoader().build( + Level.fromJson({ + ..._levelJson(), + 'brushes': [ + { + 'material': 'wall', + 'at': [0.0, -0.5, 0.0], + 'size': [8.0, 1.0, 8.0], + }, + { + 'material': 'wall', + 'at': [0.0, 0.0, 0.0], + 'size': [1.0, 0.02, 8.0], + 'solid': false, + 'drawOrder': 2, + }, + ], + }), + device: device, + registry: registry, + ); + + expect( + [for (final node in loaded.brushNodes) node.drawOrder]..sort(), + [0, 2], + ); + }); + }); } /// Two walls twelve metres apart: one for a blast to cut, one for it to miss. diff --git a/packages/flutter3d_editor_core/CHANGELOG.md b/packages/flutter3d_editor_core/CHANGELOG.md index 0ed464793..581b329ef 100644 --- a/packages/flutter3d_editor_core/CHANGELOG.md +++ b/packages/flutter3d_editor_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased + +- `LevelScene` sets each brush batch's `MeshNode.drawOrder` from the level's + `Brush.drawOrder` (`P7`), and a duplicated brush keeps its order. + ## 0.8.0+1 **Resolves on Flutter 3.44 and Dart 3.12.0.** The constraints asked for Dart diff --git a/packages/flutter3d_editor_core/lib/src/editing.dart b/packages/flutter3d_editor_core/lib/src/editing.dart index bb285f1e6..17173b395 100644 --- a/packages/flutter3d_editor_core/lib/src/editing.dart +++ b/packages/flutter3d_editor_core/lib/src/editing.dart @@ -253,6 +253,7 @@ final class Editing { shadowCasting: brush.shadowCasting, layer: brush.layer, ramp: brush.ramp, + drawOrder: brush.drawOrder, ), ); selected = level.brushes.length - 1; diff --git a/packages/flutter3d_editor_core/lib/src/level_scene.dart b/packages/flutter3d_editor_core/lib/src/level_scene.dart index c30c7c038..4fe32eb4b 100644 --- a/packages/flutter3d_editor_core/lib/src/level_scene.dart +++ b/packages/flutter3d_editor_core/lib/src/level_scene.dart @@ -218,7 +218,10 @@ final class LevelScene { // from both faces. See `Brush.shadowCasting` — and note that this // is why surfaces are batched by that answer as well as by // material: a batch is the smallest thing that can answer it. - ..shadowCasting = shadowModeOf(surface.shadowCasting); + ..shadowCasting = shadowModeOf(surface.shadowCasting) + // `P7`: a stripe painted on a floor of the same stone is drawn + // after it, in the order the document gave. + ..drawOrder = surface.drawOrder; return ( node: node, mesh: mesh, diff --git a/packages/flutter3d_sim/CHANGELOG.md b/packages/flutter3d_sim/CHANGELOG.md index ef36a34c6..6d8a3838d 100644 --- a/packages/flutter3d_sim/CHANGELOG.md +++ b/packages/flutter3d_sim/CHANGELOG.md @@ -1,5 +1,15 @@ ## Unreleased +- **`Brush.drawOrder`**, the engine's `MeshNode.drawOrder` in the level + format — `P7`: where a brush is drawn among the others, for a stripe laid + on a floor of the same stone. Optional and left out of the document at + nought, so every level keeps its bytes and its digest; the validator + refuses a number outside −128 to 127, the range the renderer can order by. + Brush surfaces are batched by it, `Breaches` keeps it on the pieces of a + cut brush, and an edit of it alone is look-only: `LevelDiff.brushOrder` + names the brushes, from `diffLevel` and from a `LevelPatch` alike, and + `simulation` stays empty. + - **A `.f3drun` carries the levels edited under the run.** `Demo.levelSwaps` holds each one as a `DemoLevelSwap`: the step it took effect before and the whole document, since the edited level exists in no diff --git a/packages/flutter3d_sim/lib/src/level/breaches.dart b/packages/flutter3d_sim/lib/src/level/breaches.dart index 39d0300e7..cd4ec6b94 100644 --- a/packages/flutter3d_sim/lib/src/level/breaches.dart +++ b/packages/flutter3d_sim/lib/src/level/breaches.dart @@ -48,6 +48,8 @@ List subtractBox(Brush brush, Aabb3 hole) { shadowCasting: brush.shadowCasting, surface: brush.surface, layer: brush.layer, + // A stripe cut in half is still drawn over the floor it lies on. + drawOrder: brush.drawOrder, ), ); } diff --git a/packages/flutter3d_sim/lib/src/level/brush.dart b/packages/flutter3d_sim/lib/src/level/brush.dart index d8b91d64e..ade552ce9 100644 --- a/packages/flutter3d_sim/lib/src/level/brush.dart +++ b/packages/flutter3d_sim/lib/src/level/brush.dart @@ -62,6 +62,7 @@ final class Brush { String? surface, this.layer, this.ramp, + this.drawOrder = 0, Map source = const {}, }) : centre = centre.clone(), size = size.clone(), @@ -164,6 +165,29 @@ final class Brush { /// Whether this brush is a ramp rather than a block. bool get isRamp => ramp != null; + /// Where this brush is drawn among the others — `P7`, the engine's + /// `MeshNode.drawOrder`, which the loader sets on the draws built from it. + /// + /// **What a level needs when two surfaces sit in one plane.** A painted + /// stripe laid on a floor of the same stone, a puddle's sheet on the + /// cobbles: drawn in whatever order the batching gives, the two fight + /// pixel by pixel, and the only fix from Dart was to patch the node after + /// loading. Lower first. Nought, the default, is left out of the document, + /// so every level ever saved keeps its bytes and its digest. + /// + /// **Between [minDrawOrder] and [maxDrawOrder]**, the range the engine's + /// sort key holds — the validator refuses a number beyond it rather than + /// let the engine clamp it into a tie nobody asked for. Only the picture + /// reads it: a change to it alone is a look-only edit (`LevelDiff`). + final int drawOrder; + + /// The lowest [drawOrder] a document may give: the engine's sort key keeps + /// the order in a signed byte. + static const int minDrawOrder = -128; + + /// The highest [drawOrder] a document may give. See [minDrawOrder]. + static const int maxDrawOrder = 127; + Vector3 get halfExtents => size / 2.0; Vector3 get min => centre - halfExtents; Vector3 get max => centre + halfExtents; @@ -203,6 +227,7 @@ final class Brush { surface: json.textOrNull('surface'), layer: json.integerOrNull('layer'), ramp: _rampFromName(json.textOrNull('ramp')), + drawOrder: json.integerOrNull('drawOrder') ?? 0, source: json, ); @@ -264,6 +289,7 @@ final class Brush { ramp == null ? null : _rampName(ramp!), whenAbsent: ramp != null, ), + WriteThroughField('drawOrder', drawOrder, whenAbsent: drawOrder != 0), ]); } } diff --git a/packages/flutter3d_sim/lib/src/level/brush_geometry.dart b/packages/flutter3d_sim/lib/src/level/brush_geometry.dart index 42c7f01bc..46ba174f7 100644 --- a/packages/flutter3d_sim/lib/src/level/brush_geometry.dart +++ b/packages/flutter3d_sim/lib/src/level/brush_geometry.dart @@ -120,12 +120,17 @@ final class BrushGeometry { final brush = level.brushes[index]; final slot = visibility == null ? -1 : visibility.slotOf(x, y, z); final own = perBrush ? '|#$index' : ''; - final key = '${brush.material}|${brush.shadowCasting.name}|$slot$own'; + // `P7`: and by the draw order, since a batch is drawn as one node and + // a node has one place in the order. + final key = + '${brush.material}|${brush.shadowCasting.name}|' + '${brush.drawOrder}|$slot$own'; return builders.putIfAbsent( key, () => SurfaceBuilder( brush.material, shadowCasting: brush.shadowCasting, + drawOrder: brush.drawOrder, lightmapped: lightmap != null, brush: perBrush ? index : null, ), diff --git a/packages/flutter3d_sim/lib/src/level/brush_surface.dart b/packages/flutter3d_sim/lib/src/level/brush_surface.dart index 83b967500..a8eec65e0 100644 --- a/packages/flutter3d_sim/lib/src/level/brush_surface.dart +++ b/packages/flutter3d_sim/lib/src/level/brush_surface.dart @@ -16,6 +16,7 @@ final class BrushSurface { BrushSurface({ required this.material, required this.shadowCasting, + this.drawOrder = 0, required this.positions, required this.normals, required this.texcoords, @@ -59,6 +60,11 @@ final class BrushSurface { /// [shadowCasting], as on the brushes this came from. bool get castsShadow => shadowCasting.casts; + /// Where the draw built from this goes among the others — `Brush.drawOrder` + /// of the brushes it came from, which share it: surfaces are keyed by it + /// too, because the node a batch becomes has one place in the order. + final int drawOrder; + /// Three floats per vertex. final Float32List positions; diff --git a/packages/flutter3d_sim/lib/src/level/level_diff.dart b/packages/flutter3d_sim/lib/src/level/level_diff.dart index b1d29b7ae..115e93b25 100644 --- a/packages/flutter3d_sim/lib/src/level/level_diff.dart +++ b/packages/flutter3d_sim/lib/src/level/level_diff.dart @@ -28,6 +28,7 @@ final class LevelDiff { this.materials = const [], this.fog = false, this.music = false, + this.brushOrder = const [], this.simulation = const [], }); @@ -47,6 +48,16 @@ final class LevelDiff { /// The music changed. final bool music; + /// Brushes whose draw order changed and nothing else, by index — `P7`. + /// + /// **The one change to a brush the simulation cannot feel.** Nothing that + /// collides, walks or steps reads `Brush.drawOrder`; it only says which of + /// two surfaces in one plane is drawn over the other. So a brush edited in + /// that alone is patched into the running scene like a material, and stays + /// out of [simulation] — any other change to the same brush puts the whole + /// list back there, and this is empty. + final List brushOrder; + /// Which parts the simulation reads changed: `brushes`, `entities`, /// `heightfield`, `recipes`, `next`. Empty when the change can be patched /// into a running scene without touching the run. @@ -59,6 +70,7 @@ final class LevelDiff { materials.isEmpty && !fog && !music && + brushOrder.isEmpty && simulation.isEmpty; /// Whether the change can be patched in without a timeline branch. @@ -70,6 +82,7 @@ final class LevelDiff { 'materials': materials, 'fog': fog, 'music': music, + 'brushOrder': brushOrder, 'simulation': simulation, }; } @@ -90,6 +103,12 @@ LevelDiff diffLevel(Level before, Level after) { : newLights.length; final names = {...before.materials.keys, ...after.materials.keys}; + // `P7`: the brushes as the simulation sees them, without the one field it + // never reads. Equal so, and any difference left is the draw order alone. + final brushesMoved = differs( + [for (final b in before.brushes) brushWithoutOrder(b)], + [for (final b in after.brushes) brushWithoutOrder(b)], + ); return LevelDiff( lights: [ for (var i = 0; i < shared; i++) @@ -108,12 +127,14 @@ LevelDiff diffLevel(Level before, Level after) { before.fogColor != after.fogColor || before.fogDensity != after.fogDensity, music: before.music != after.music, + brushOrder: brushesMoved + ? const [] + : [ + for (var i = 0; i < after.brushes.length; i++) + if (before.brushes[i].drawOrder != after.brushes[i].drawOrder) i, + ], simulation: [ - if (differs( - [for (final b in before.brushes) b.toJson()], - [for (final b in after.brushes) b.toJson()], - )) - 'brushes', + if (brushesMoved) 'brushes', if (differs( [for (final e in before.entities) e.toJson()], [for (final e in after.entities) e.toJson()], @@ -130,3 +151,9 @@ LevelDiff diffLevel(Level before, Level after) { ], ); } + +/// [brush]'s document without its draw order — what the simulation reads of +/// it, for telling an edit of the order alone from one it would feel. See +/// [LevelDiff.brushOrder]. +Map brushWithoutOrder(Brush brush) => + brush.toJson()..remove('drawOrder'); diff --git a/packages/flutter3d_sim/lib/src/level/level_patch.dart b/packages/flutter3d_sim/lib/src/level/level_patch.dart index e91a5a0f8..b12a683b0 100644 --- a/packages/flutter3d_sim/lib/src/level/level_patch.dart +++ b/packages/flutter3d_sim/lib/src/level/level_patch.dart @@ -266,18 +266,39 @@ final class LevelPatch { final whole = fields.keys.any( (String key) => patchedLists.contains(key) || key == 'materials', ); - return LevelPatched(next, whole ? diffLevel(level, next) : _diff()); + return LevelPatched( + next, + whole ? diffLevel(level, next) : _diff(level, next), + ); } - /// What the edits change, read off the edits themselves. - LevelDiff _diff() { + /// What the edits change, read off the edits themselves — and, for a brush + /// replaced in place, off the two rows, which is the one place the edit + /// cannot say whether the simulation would feel it. + LevelDiff _diff(Level before, Level after) { Iterable of(String list) => rows.where((RowEdit edit) => edit.list == list); final lights = of('lights'); final lightCountChanged = lights.any( (RowEdit edit) => edit.inserts || edit.removes, ); + // `P7`: brushes replaced in place whose rows differ only in the draw + // order are a look-only edit. With no row put in or taken out, an edit's + // place is the brush's place in both levels. + final brushes = of('brushes'); + final orderOnly = + brushes.isNotEmpty && + brushes.every( + (RowEdit edit) => + !edit.inserts && + !edit.removes && + _digest(brushWithoutOrder(before.brushes[edit.at])) == + _digest(brushWithoutOrder(after.brushes[edit.at])), + ); return LevelDiff( + brushOrder: orderOnly + ? [for (final edit in brushes) edit.at] + : const [], // With no row put in or taken out, a row's place in the patch is its // place in both levels. lights: lightCountChanged @@ -288,7 +309,7 @@ final class LevelPatch { fog: fields.containsKey('fogColor') || fields.containsKey('fogDensity'), music: fields.containsKey('music'), simulation: [ - if (of('brushes').isNotEmpty) 'brushes', + if (brushes.isNotEmpty && !orderOnly) 'brushes', if (of('entities').isNotEmpty) 'entities', for (final key in const ['heightfield', 'recipes', 'next']) if (fields.containsKey(key)) key, diff --git a/packages/flutter3d_sim/lib/src/level/level_validator.dart b/packages/flutter3d_sim/lib/src/level/level_validator.dart index 36f4d087f..4def135ca 100644 --- a/packages/flutter3d_sim/lib/src/level/level_validator.dart +++ b/packages/flutter3d_sim/lib/src/level/level_validator.dart @@ -165,6 +165,21 @@ final class LevelValidator { ); } + // `P7`: the engine keeps the order in a signed byte and clamps past + // it, so 200 and 127 would draw as one — a tie the author never wrote. + if (brush.drawOrder < Brush.minDrawOrder || + brush.drawOrder > Brush.maxDrawOrder) { + issues.add( + LevelIssue( + LevelIssueSeverity.error, + 'drawOrder ${brush.drawOrder} is outside ' + '${Brush.minDrawOrder} to ${Brush.maxDrawOrder}, the range the ' + 'renderer can order by', + where: where, + ), + ); + } + _checkRamp(brush, where, issues); } diff --git a/packages/flutter3d_sim/lib/src/level/surface_builder.dart b/packages/flutter3d_sim/lib/src/level/surface_builder.dart index b3889a10b..ac51b3c10 100644 --- a/packages/flutter3d_sim/lib/src/level/surface_builder.dart +++ b/packages/flutter3d_sim/lib/src/level/surface_builder.dart @@ -12,6 +12,7 @@ final class SurfaceBuilder { SurfaceBuilder( this.material, { required this.shadowCasting, + this.drawOrder = 0, this.lightmapped = false, this.brush, }); @@ -19,6 +20,9 @@ final class SurfaceBuilder { final String material; final ShadowCasting shadowCasting; + /// See [BrushSurface.drawOrder]. + final int drawOrder; + /// The one brush these triangles belong to, when every brush is its own /// surface. See [BrushSurface.brush]. final int? brush; @@ -79,6 +83,7 @@ final class SurfaceBuilder { BrushSurface finish() => BrushSurface( material: material, shadowCasting: shadowCasting, + drawOrder: drawOrder, positions: Float32List.fromList(_positions), normals: Float32List.fromList(_normals), texcoords: Float32List.fromList(_texcoords), diff --git a/packages/flutter3d_sim/test/brush_draw_order_test.dart b/packages/flutter3d_sim/test/brush_draw_order_test.dart new file mode 100644 index 000000000..b754ff5ff --- /dev/null +++ b/packages/flutter3d_sim/test/brush_draw_order_test.dart @@ -0,0 +1,178 @@ +/// A brush's place in the draw order, as a level document says it — `P7`. +/// +/// dart test test/brush_draw_order_test.dart +/// +/// The engine has had `MeshNode.drawOrder` since the order between nodes was +/// written, and the level format had no word for it: a stripe painted on a +/// floor of the same stone fought the floor pixel by pixel, and the only fix +/// was to patch the node after loading. What is pinned here: the word is read +/// and written back, a document without it keeps its bytes, a number the +/// renderer cannot order by is refused, a batch is split by it, and an edit +/// of it alone is a look-only edit, patched into a running game without a +/// timeline branch. +library; + +import 'package:flutter3d_sim/flutter3d_sim.dart'; +import 'package:test/test.dart'; +import 'package:vector_math/vector_math.dart'; + +Map _floor() => { + 'at': [0, -0.5, 0], + 'size': [20, 1, 20], + 'material': 'stone', +}; + +Map _stripe([Map extra = const {}]) => + { + 'at': [0, 0.01, 0], + 'size': [1, 0.02, 20], + 'material': 'stone', + 'solid': false, + ...extra, + }; + +Map _document(Map stripe) => + { + 'version': 1, + 'name': 'yard', + 'materials': { + 'stone': { + 'color': [0.5, 0.5, 0.5, 1.0], + }, + }, + 'brushes': [_floor(), stripe], + 'lights': [ + { + 'type': 'point', + 'at': [0, 3, 0], + 'color': [1, 1, 1], + }, + ], + }; + +void main() { + group('the document', () { + test('a brush with an order keeps it through a round trip', () { + // Mutation: leave `drawOrder` out of `Brush.toJson` — the stripe comes + // back at nought and is drawn under the floor half the time. + final brush = Brush.fromJson(_stripe(const {'drawOrder': 3})); + expect(brush.drawOrder, 3); + + final again = Brush.fromJson(brush.toJson()); + expect(again.drawOrder, 3); + expect(brush.toJson()['drawOrder'], 3); + }); + + test('a brush built in code with an order writes it', () { + final brush = Brush( + centre: Vector3.zero(), + size: Vector3.all(1.0), + drawOrder: -4, + ); + expect(brush.toJson()['drawOrder'], -4); + }); + + test('a document without it keeps its bytes and its digest', () { + // Mutation: write `drawOrder` whether or not it is nought — every level + // ever saved grows a key per brush and digests to something new, so + // every recorded run of it stops replaying. + final level = Level.fromJson(_document(_stripe())); + final brushes = level.toJson()['brushes']! as List; + for (final brush in brushes) { + expect( + (brush! as Map).containsKey('drawOrder'), + isFalse, + ); + } + expect( + Level.fromJson(level.toJson()).digestHex, + Level.fromJson(_document(_stripe())).digestHex, + ); + expect( + Level.fromJson(_document(_stripe(const {'drawOrder': 0}))).digestHex, + isNot( + Level.fromJson(_document(_stripe(const {'drawOrder': 1}))).digestHex, + ), + ); + }); + }); + + group('the validator', () { + List orderIssues(int order) => + LevelValidator(registry: EntityRegistry(const [])) + .validate(Level.fromJson(_document(_stripe({'drawOrder': order})))) + .where((LevelIssue issue) => issue.message.contains('drawOrder')) + .toList(); + + test('refuses an order the renderer cannot hold', () { + // Mutation: drop the range check — 200 loads, the engine clamps it to + // 127, and two brushes the author put in an order draw as a tie. + expect(orderIssues(200), hasLength(1)); + expect(orderIssues(200).single.severity, LevelIssueSeverity.error); + expect(orderIssues(-129), hasLength(1)); + }); + + test('takes both ends of the range', () { + expect(orderIssues(Brush.maxDrawOrder), isEmpty); + expect(orderIssues(Brush.minDrawOrder), isEmpty); + }); + }); + + test('a batch is split by the order, and carries it', () { + // Mutation: drop the order from `BrushGeometry`'s batch key. Floor and + // stripe share a material and become one surface, one node, one place + // in the order. + final level = Level.fromJson(_document(_stripe(const {'drawOrder': 2}))); + final surfaces = const BrushGeometry(cullHiddenFaces: false).build(level); + + expect( + [for (final surface in surfaces) surface.drawOrder]..sort(), + [0, 2], + ); + }); + + group('an edit of the order alone', () { + final before = Level.fromJson(_document(_stripe())); + final after = Level.fromJson(_document(_stripe(const {'drawOrder': 2}))); + + test('is look-only to the diff', () { + // Mutation: compare brushes whole in `diffLevel` — the edit lands in + // `simulation`, and the running game branches its timeline to redraw + // a stripe. + final diff = diffLevel(before, after); + expect(diff.simulation, isEmpty); + expect(diff.presentationOnly, isTrue); + expect(diff.brushOrder, [1]); + expect(diff.isEmpty, isFalse); + }); + + test('and to a patch', () { + // Mutation: count any brush row in `LevelPatch._diff` as simulation. + final patch = LevelPatch.between(before, after); + final result = patch.applyTo(before); + expect(result, isA()); + final diff = (result as LevelPatched).diff; + expect(diff.simulation, isEmpty); + expect(diff.brushOrder, [1]); + }); + + test('but not with anything else changed on the same brush', () { + final moved = Level.fromJson( + _document( + _stripe(const { + 'drawOrder': 2, + 'at': [3, 0.01, 0], + }), + ), + ); + final diff = diffLevel(before, moved); + expect(diff.simulation, ['brushes']); + expect(diff.brushOrder, isEmpty); + + final patched = + LevelPatch.between(before, moved).applyTo(before) as LevelPatched; + expect(patched.diff.simulation, ['brushes']); + expect(patched.diff.brushOrder, isEmpty); + }); + }); +} diff --git a/site/content/core/simulation.md b/site/content/core/simulation.md index b21b6a008..7f30da4b9 100644 --- a/site/content/core/simulation.md +++ b/site/content/core/simulation.md @@ -106,7 +106,7 @@ A level is JSON: brushes, materials, lights, entities, fog, and where to go next | Concept | What it is | |---|---| -| `Brush` | An axis-aligned box: centre, size, material, optional `surface` name, optional layer, `solid` | +| `Brush` | An axis-aligned box: centre, size, material, optional `surface` name, optional layer, `solid`, an optional `drawOrder` from −128 to 127 for two surfaces in one plane | | `LevelMaterial` | Base colour, roughness, metallic, emissive, `texelsPerMetre`, and albedo/normal/ORM paths | | `LevelLight` | Type, position, direction, colour, intensity, range, `castsShadow` | | `EntityDef` | A `type`, a position, a yaw, a name, and free-form `properties` | diff --git a/site/content/quickstart.md b/site/content/quickstart.md index f1503133b..6673fc961 100644 --- a/site/content/quickstart.md +++ b/site/content/quickstart.md @@ -91,7 +91,7 @@ tool/ci.sh # shaders, analyze, every test (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11345 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. +There are 11355 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. ## Your own application diff --git a/site/content/reference/testing.md b/site/content/reference/testing.md index 4e7c2a53f..31f00ef27 100644 --- a/site/content/reference/testing.md +++ b/site/content/reference/testing.md @@ -1,17 +1,17 @@ --- -description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11345 tests need a GPU. +description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11355 tests need a GPU. --- # Testing -11345 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. +11355 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. | Package | Tests | | Package | Tests | |---|---|---|---|---| | `flutter3d` | 1662 | | | | | | | | `flutter3d_mesh` | 599 | | | | | `apps/flutter3d_modeler` | 1750 | -| `flutter3d_sim` | 691 | | `pad_input` | 67 | +| `flutter3d_sim` | 700 | | `pad_input` | 67 | | `flutter3d_lab` | 15 | | `flutter3d_core` | 771 | | `flutter3d_lti` | 26 | | `apps/flutter3d_lab_pendulum` | 7 | | | | | `apps/flutter3d_lab_incident` | 13 | @@ -27,7 +27,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flutter3d_game_strategy` | 133 | | `flutter3d_editor_mcp` | 36 | | `apps/flutter3d_demo_river` | 54 | | `flutter3d_testing` | 45 | | `flutter3d_editor_core` | 141 | | `flutter3d_editor_widgets` | 91 | -| `apps/flutter3d_demo_dungeon` | 115 | | `flutter3d_app` | 169 | +| `apps/flutter3d_demo_dungeon` | 115 | | `flutter3d_app` | 170 | | `flutter3d_game` | 325 | | `flutter3d_shaders` | 5 | | `flutter3d_particles` | 97 | | `flutter3d_stereo` | 50 | | `flutter3d_model_core` | 1172 | | `flutter3d_model_mcp` | 196 | @@ -40,7 +40,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flame_flutter3d` | 140 | | `apps/flutter3d_showcase` | 88 | | `flame_multiplayer` | 9 | | `flame_multiplayer_dashwire` | 3 | -The rows sum to 11318 rather than 11345: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. +The rows sum to 11328 rather than 11355: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. `flutter3d_samples` is not in the table and has no `test/` at all: it is test data with two path constants over it, and other packages' decoder tests are what exercise it. `flutter3d_conformance` is missing for a different reason: it is invoked as a script harness rather than through `flutter test`, so it does not surface in a grep of `test(` calls either. See below for what that cost once. From 9cd8dfad7b52cdc4684ecbe9cdebc5538314532c Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 20:02:59 +0300 Subject: [PATCH 3/6] Particles and splats fog as the lit scene does Their stages declared the fog block with two of its four members, the colour and the eye. They could not tell an orthographic camera from a perspective one, so they fogged in rings round the eye's point and faced a mesh particle towards it, and they could not read the height fog's falloff, so a puff of smoke fogged at the camera's density whatever its height. The block is now declared whole in lib/particle_fog.glsl, shared by every particle and splat stage: depth from the eye's plane through an orthographic lens, the view axis for a mesh particle's facing there, and the height fog integrated along the ray as ApplyFog does. Both particle contributors and the splat contributor bind the axis, the lens and the falloff, and the software stages mirror it. Through a perspective lens with a flat fog nothing moves: every particle and splat golden holds in all four sets. Splats under an orthographic camera are also sorted by depth along the view axis rather than by distance from the eye's point, and resorted when the axis turns. --- ARCHITECTURE.md | 2 +- README.md | 2 +- packages/flutter3d_core/CHANGELOG.md | 11 + .../src/engine/render/splat_contributor.dart | 53 +- .../lib/src/engine/render/splat_sort.dart | 17 +- .../test/engine/splat_sort_test.dart | 57 + packages/flutter3d_cpu/CHANGELOG.md | 4 + .../lib/src/cpu_shaders_particles.dart | 87 +- .../test/particle_mesh_facing_test.dart | 53 + .../flutter3d_cpu/test/splat_fog_test.dart | 62 +- packages/flutter3d_particles/CHANGELOG.md | 6 + .../lib/src/mesh_particle_contributor.dart | 17 +- .../lib/src/particle_contributor.dart | 17 +- .../test/particle_fog_test.dart | 142 + packages/flutter3d_shaders/CHANGELOG.md | 8 + .../flutter3d_shaders/lib/uniform_blocks.dart | 18 + .../shaders/lib/particle.glsl | 27 +- .../shaders/lib/particle_fog.glsl | 74 + .../shaders/lib/particle_six_way.glsl | 20 +- .../shaders/lib/particle_textured.glsl | 20 +- .../shaders/lighting/particle_mesh.frag | 29 +- .../shaders/lighting/splat.frag | 20 +- .../shaders/lighting/splat_hashed.frag | 17 +- packages/flutter3d_webgl/CHANGELOG.md | 3 + .../flutter3d_webgl/lib/engine_shaders.dart | 765 ++++- packages/flutter3d_webgpu/CHANGELOG.md | 3 + .../flutter3d_webgpu/lib/engine_shaders.dart | 3039 ++++++++++------- site/content/quickstart.md | 2 +- site/content/reference/testing.md | 12 +- 29 files changed, 3092 insertions(+), 1495 deletions(-) create mode 100644 packages/flutter3d_cpu/test/particle_mesh_facing_test.dart create mode 100644 packages/flutter3d_particles/test/particle_fog_test.dart create mode 100644 packages/flutter3d_shaders/shaders/lib/particle_fog.glsl diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e9a8da34a..cb5267022 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2691,7 +2691,7 @@ storage. |---|---| | Style | `dart format` | | Analysis | `flutter analyze` clean across the workspace, no warnings | -| Unit tests | **11355 tests** across 43 packages and 10 applications | +| Unit tests | **11362 tests** across 43 packages and 10 applications | | Structure rules | 35, `dart run tool/structure.dart`, the first CI step | | CI | GitHub Actions over `tool/ci.sh`, on `ubuntu-latest`, with no graphics card | diff --git a/README.md b/README.md index bbdd17f30..7ff696bc3 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Or one package at a time: (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11355 tests across forty-three packages and nine applications. The +There are 11362 tests across forty-three packages and nine applications. The only ones that need a GPU are the Impeller half of the golden set. The other half is rendered by the software backend, which is what makes 91 scenes checkable in a headless run. diff --git a/packages/flutter3d_core/CHANGELOG.md b/packages/flutter3d_core/CHANGELOG.md index 4c438fc1e..213d39402 100644 --- a/packages/flutter3d_core/CHANGELOG.md +++ b/packages/flutter3d_core/CHANGELOG.md @@ -1,5 +1,16 @@ ## Unreleased +- **Splats under an orthographic camera are sorted by depth along the view + axis** (`P7`): `SplatSorter.sort` and `SplatQuads.build` take an `axis`, + and `SplatContributor` passes the camera's under an orthographic lens. By + distance from the eye's point, two splats side by side at one depth were + ordered by how far each stood from the axis. A turn of the axis sorts + again. + +- `SplatContributor` binds the fog's whole block, the view axis, the lens + and the height fog's falloff with it, so a cloud fogs by depth under an + orthographic camera and thins upwards in a height fog (`P7`, `P5`). + - **Shadow cascades under an orthographic camera split by depth across what the view sees** — `P7`. They were split by distance from the eye, which an orthographic camera puts tens of metres back, so the near cascades covered diff --git a/packages/flutter3d_core/lib/src/engine/render/splat_contributor.dart b/packages/flutter3d_core/lib/src/engine/render/splat_contributor.dart index 4e399e5d3..195a41dbc 100644 --- a/packages/flutter3d_core/lib/src/engine/render/splat_contributor.dart +++ b/packages/flutter3d_core/lib/src/engine/render/splat_contributor.dart @@ -34,6 +34,7 @@ import 'package:flutter3d_shaders/typed_blocks.dart'; import 'package:vector_math/vector_math.dart'; import '../../formats/splat/splat_cloud.dart'; +import '../scene/projection.dart'; import '../scene/scene_node.dart'; import 'engine_tables.dart'; import 'identity_indices.dart'; @@ -177,6 +178,10 @@ final class SplatQuads { final Float64List _sortedModel = Float64List(16); bool _sortedWithModel = false; + /// The view axis the last sort ordered by, or null when it ordered by + /// distance — `P7`, see [build]'s `axis`. + Vector3? _sortedAxis; + /// How many times [build] has sorted. For the tests that hold it to not /// sorting on every frame. int get sorts => _sorts; @@ -217,6 +222,11 @@ final class SplatQuads { /// projected onto [right] and [up] alone and nothing is added — the exact /// ellipse on the view axis, and a quad whose size is the splat's own, for /// a caller with no projection to hand. + /// + /// [axis], the view axis, orders by depth along it instead of by distance + /// from [eye] — `P7`, for an orthographic camera, whose rays are parallel. + /// A turn then reorders the cloud, so the sort also runs when the axis has + /// turned further than [resortFraction]. void build({ required Vector3 eye, required Vector3 right, @@ -224,10 +234,11 @@ final class SplatQuads { Matrix4? model, bool sorted = true, SplatLens? lens, + Vector3? axis, }) { // A tree's cut is chosen where the sort runs, so a hashed build still // sorts when the cut moves: the order and the cut have to agree. - if ((sorted || lod != null) && _needsSort(eye, model)) { + if ((sorted || lod != null) && _needsSort(eye, model, axis)) { final lod = this.lod; if (lod != null) { // The cut is chosen by distance in the tree's own space, so the eye @@ -238,9 +249,10 @@ final class SplatQuads { _sortedPageVersion = lod.tree.pageVersion; _sortedBudget = lod.budget; } - _sorter.sort(cloud, eye, model: model); + _sorter.sort(cloud, eye, model: model, axis: axis); _sorts++; _sortedEye = eye.clone(); + _sortedAxis = axis?.clone(); _sortedWithModel = model != null; if (model != null) _sortedModel.setAll(0, model.storage); } @@ -410,9 +422,15 @@ final class SplatQuads { vertexCount = count * kSplatVerticesPerSplat; } - bool _needsSort(Vector3 eye, Matrix4? model) { + bool _needsSort(Vector3 eye, Matrix4? model, Vector3? axis) { final last = _sortedEye; if (last == null) return true; + // `P7`: by depth, a turn reorders what a move of the eye does not. + final sortedAxis = _sortedAxis; + if ((axis == null) != (sortedAxis == null)) return true; + if (axis != null && axis.distanceTo(sortedAxis!) > resortFraction) { + return true; + } final lod = this.lod; if (lod != null && (lod.tree.pageVersion != _sortedPageVersion || @@ -456,11 +474,14 @@ final class SplatContributor extends PassContributor { final ParticleInfoBlock _particleInfo = ParticleInfoBlock(); final SplatHashInfoBlock _hashInfo = SplatHashInfoBlock(); - /// `FogInfo`'s two members as both splat stages declare them: the fog's - /// colour and density, and the eye. Not `FogInfoBlock`, which is the lit - /// stages' three-member layout. + /// `FogInfo`'s four members, the lit stages' layout, which both splat + /// stages declare whole since `P7` — `lib/particle_fog.glsl`: the fog's + /// colour and density, the eye and the height fog's falloff, the view + /// axis, and whether the lens is orthographic. final Float32List _fog = Float32List(4); final Float32List _eye = Float32List(4); + final Float32List _forward = Float32List(4); + final Float32List _projection = Float32List(4); SplatContributor( SplatCloud cloud, { @@ -573,6 +594,8 @@ final class SplatContributor extends PassContributor { model: node?.worldMatrix, sorted: !hashed, lens: lens, + // `P7`: through an orthographic lens, back to front is by depth. + axis: isOrthographic(viewProjection) ? forward : null, ); if (quads.vertexCount == 0) return; @@ -619,14 +642,28 @@ final class SplatContributor extends PassContributor { ..[1] = colour.y ..[2] = colour.z ..[3] = fog.densityAt(eye.y); + // `P5`, `P7`: the height fog's falloff, integrated per fragment as the + // lit stages do, and the view axis and lens, so an orthographic camera + // fogs a cloud by depth from its plane rather than in rings. _eye ..[0] = eye.x ..[1] = eye.y - ..[2] = eye.z; + ..[2] = eye.z + ..[3] = fog.resolvedHeightFalloff; + _forward + ..[0] = forward.x + ..[1] = forward.y + ..[2] = forward.z; + _projection[0] = isOrthographic(viewProjection) ? 1.0 : 0.0; frame.encoder.bindUniformBlock( fragmentShader, 'FogInfo', - {'fog': _fog, 'eye': _eye}, + { + 'fog': _fog, + 'eye': _eye, + 'forward': _forward, + 'projection': _projection, + }, ); if (hashed) { // The frame's slice of the engine's blue noise, the same slice the diff --git a/packages/flutter3d_core/lib/src/engine/render/splat_sort.dart b/packages/flutter3d_core/lib/src/engine/render/splat_sort.dart index a961fd410..8e9f6fffc 100644 --- a/packages/flutter3d_core/lib/src/engine/render/splat_sort.dart +++ b/packages/flutter3d_core/lib/src/engine/render/splat_sort.dart @@ -112,12 +112,19 @@ final class SplatSorter { double _lastRange = 0.0; /// Orders [cloud] far to near by distance from [eye], with the cloud placed - /// in the world by [model] when it is given. + /// in the world by [model] when it is given — or, given [axis], by depth + /// along it. + /// + /// **[axis] is for an orthographic camera — `P7`.** Its rays are parallel, + /// so what is in front of what along a ray is depth along the view axis; + /// a distance from the eye's point, which is only where the camera was put, + /// orders two splats side by side at one depth by how far each is from + /// the axis, and the nearer one off-axis can be drawn under the other. /// /// The distances are quantised across the range this cloud spans from /// this eye, not across a fixed one: sixteen bits over the cloud's own /// depth is what keeps a small cloud and a large one equally well ordered. - void sort(SplatCloud cloud, Vector3 eye, {Matrix4? model}) { + void sort(SplatCloud cloud, Vector3 eye, {Matrix4? model, Vector3? axis}) { final count = cloud.count; if (_keys.length < count) { _keys = Uint32List(count); @@ -137,6 +144,8 @@ final class SplatSorter { final centres = cloud.centres; final m = model?.storage; final ex = eye.x, ey = eye.y, ez = eye.z; + final along = axis != null; + final ax = axis?.x ?? 0.0, ay = axis?.y ?? 0.0, az = axis?.z ?? 0.0; var near = double.infinity; var far = double.negativeInfinity; for (var i = 0; i < count; i++) { @@ -150,7 +159,9 @@ final class SplatSorter { (m == null ? ly : m[1] * lx + m[5] * ly + m[9] * lz + m[13]) - ey; final dz = (m == null ? lz : m[2] * lx + m[6] * ly + m[10] * lz + m[14]) - ez; - final d = math.sqrt(dx * dx + dy * dy + dz * dz); + final d = along + ? dx * ax + dy * ay + dz * az + : math.sqrt(dx * dx + dy * dy + dz * dz); distances[i] = d; if (d < near) near = d; if (d > far) far = d; diff --git a/packages/flutter3d_core/test/engine/splat_sort_test.dart b/packages/flutter3d_core/test/engine/splat_sort_test.dart index 409f6f0b2..7e8cb8dc0 100644 --- a/packages/flutter3d_core/test/engine/splat_sort_test.dart +++ b/packages/flutter3d_core/test/engine/splat_sort_test.dart @@ -177,4 +177,61 @@ void main() { sorter.sort(_randomCloud(1, 1), Vector3.zero()); expect(sorter.order[0], 0); }); + + group('through an orthographic lens — P7', () { + // Two splats at one depth along the axis, one on it and one far to its + // side, and a third half a metre nearer, on the side. Looking down −z + // from an eye ten metres back. + SplatCloud cloud() => SplatCloud( + centres: Float32List.fromList([ + 0.0, 0.0, 0.0, // + 8.0, 0.0, 0.0, + 8.0, 0.0, 0.5, + ]), + colours: Float32List(12), + scales: Float32List(9), + rotations: Float32List.fromList([ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + ]), + ); + final eye = Vector3(0.0, 0.0, 10.0); + final axis = Vector3(0.0, 0.0, -1.0); + + test('back to front is by depth along the axis', () { + // Mutation: order by distance from the eye whatever `axis` says. The + // splat on the side half a metre nearer is further from the eye's + // point than the one on the axis, and is drawn under it. + final sorter = SplatSorter()..sort(cloud(), eye, axis: axis); + final order = sorter.order.sublist(0, 3); + expect(order.indexOf(2), greaterThan(order.indexOf(0))); + expect(order.indexOf(2), greaterThan(order.indexOf(1))); + }); + + test('a turn of the axis sorts again, the eye standing still', () { + // Mutation: drop the axis from `SplatQuads._needsSort` — a turned + // orthographic camera keeps the order of the old axis. + final quads = SplatQuads(cloud()); + void build(Vector3 at, Vector3 towards) => quads.build( + eye: at, + right: Vector3(1.0, 0.0, 0.0), + up: Vector3(0.0, 1.0, 0.0), + axis: towards, + ); + build(eye, axis); + final sorts = quads.sorts; + build(eye, Vector3(0.6, 0.0, -0.8)); + expect(quads.sorts, sorts + 1); + }); + }); } diff --git a/packages/flutter3d_cpu/CHANGELOG.md b/packages/flutter3d_cpu/CHANGELOG.md index 93d5c55cf..56a4e615f 100644 --- a/packages/flutter3d_cpu/CHANGELOG.md +++ b/packages/flutter3d_cpu/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +- The software particle and splat stages fog as `lib/particle_fog.glsl` + does — by depth through an orthographic lens, height fog integrated — and + the mesh particle faces the view axis there (`P7`, `P5`). + - The software `ShadowFactor`, light shafts and volumetric fog pick a cascade as the GLSL now does under an orthographic camera (`P7`); `orthographic-shadow` is in the software reference set. diff --git a/packages/flutter3d_cpu/lib/src/cpu_shaders_particles.dart b/packages/flutter3d_cpu/lib/src/cpu_shaders_particles.dart index eba9d57c4..d94bb7e39 100644 --- a/packages/flutter3d_cpu/lib/src/cpu_shaders_particles.dart +++ b/packages/flutter3d_cpu/lib/src/cpu_shaders_particles.dart @@ -42,6 +42,44 @@ double softParticleFade( return stored > 0.0 ? fade : 1.0; } +/// `lib/particle_fog.glsl`'s `ParticleEyeDistance` — `P7`: the air between +/// the eye and world `(x, y, z)`, the distance through a perspective lens and +/// the depth from the eye's plane through an orthographic one. +double particleEyeDistance(ShaderBindings b, double x, double y, double z) { + final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); + final dx = x - eye.x; + final dy = y - eye.y; + final dz = z - eye.z; + if (orthographic(b)) { + final forward = b.vec4('FogInfo', 'forward', Vector4.zero()); + return math.max(dx * forward.x + dy * forward.y + dz * forward.z, 0.0); + } + return math.sqrt(dx * dx + dy * dy + dz * dz); +} + +/// `lib/particle_fog.glsl`'s `ParticleFogTransmittance` — `P5`, `P7`: how +/// much of a fragment at world `(x, y, z)` the fog leaves, height fog and all, +/// as [applyFog] has it for the lit stages. One with no fog. +double particleFogTransmittance( + ShaderBindings b, + double x, + double y, + double z, +) { + final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); + if (fog.w <= 0.0) return 1.0; + final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); + final falloff = eye.w; + final k = falloff * (y - eye.y); + final density = falloff > 0.0 + ? fog.w * (k.abs() > 1e-3 ? (1.0 - math.exp(-k)) / k : 1.0 - 0.5 * k) + : fog.w; + return math + .exp(-density * particleEyeDistance(b, x, y, z)) + .clamp(0.0, 1.0) + .toDouble(); +} + /// `particle.vert`: a billboard corner, and the world position for the fog. final class ParticleVertexShader implements CpuVertexShader { const ParticleVertexShader(); @@ -97,14 +135,7 @@ final class ParticleTexturedShader implements CpuFragmentShader { texel = texture.sample(v[4], v[5], du: du, dv: dv); } - var fogged = 1.0; - final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); - if (fog.w > 0.0) { - final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final d = - (Vector3(v[6], v[7], v[8]) - Vector3(eye.x, eye.y, eye.z)).length; - fogged = math.exp(-fog.w * d).clamp(0.0, 1.0); - } + final fogged = particleFogTransmittance(b, v[6], v[7], v[8]); // The texture's alpha is coverage and the particle's is brightness, so the // two multiply. @@ -192,9 +223,7 @@ final class ParticleSixWayShader implements CpuFragmentShader { // covers what is behind it. final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); if (fog.w > 0.0) { - final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final d = (world - Vector3(eye.x, eye.y, eye.z)).length; - final fogged = math.exp(-fog.w * d).clamp(0.0, 1.0); + final fogged = particleFogTransmittance(b, v[6], v[7], v[8]); colour ..x = fog.x + (colour.x - fog.x) * fogged ..y = fog.y + (colour.y - fog.y) * fogged @@ -260,12 +289,15 @@ final class ParticleMeshShader implements CpuFragmentShader { @override Vector4? run(Float32List v, ShaderBindings b, FragmentContext c) { - final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final tx = eye.x - v[4]; - final ty = eye.y - v[5]; - final tz = eye.z - v[6]; + // `ParticleTowardsEye` — `P7`: against the view axis through an + // orthographic lens, to the eye's point through a perspective one. + final forward = b.vec4('FogInfo', 'forward', Vector4.zero()); + final ortho = orthographic(b); + final tx = ortho ? -forward.x : eye.x - v[4]; + final ty = ortho ? -forward.y : eye.y - v[5]; + final tz = ortho ? -forward.z : eye.z - v[6]; final distance = math.sqrt(tx * tx + ty * ty + tz * tz); var facing = 1.0; @@ -282,10 +314,7 @@ final class ParticleMeshShader implements CpuFragmentShader { // a face that vanished there would carve a dark seam along it. final intensity = 0.35 + 0.65 * facing; - var fogged = 1.0; - if (fog.w > 0.0) { - fogged = math.exp(-fog.w * distance).clamp(0.0, 1.0); - } + final fogged = particleFogTransmittance(b, v[4], v[5], v[6]); final scale = v[3] * intensity * fogged; return Vector4(v[0] * scale, v[1] * scale, v[2] * scale, 1.0); @@ -310,14 +339,7 @@ final class ParticleShader implements CpuFragmentShader { falloff * (soft ? softParticleFade(b, c, v[6], v[7], v[8]) : 1.0); - var fogged = 1.0; - final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); - if (fog.w > 0.0) { - final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final d = - (Vector3(v[6], v[7], v[8]) - Vector3(eye.x, eye.y, eye.z)).length; - fogged = math.exp(-fog.w * d).clamp(0.0, 1.0); - } + final fogged = particleFogTransmittance(b, v[6], v[7], v[8]); final scale = v[3] * intensity * fogged; // Alpha one with the colour premultiplied: these draw additively, so the @@ -352,10 +374,7 @@ final class SplatShader implements CpuFragmentShader { var bl = v[2]; final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); if (fog.w > 0.0) { - final eye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final d = - (Vector3(v[6], v[7], v[8]) - Vector3(eye.x, eye.y, eye.z)).length; - final visibility = math.exp(-fog.w * d).clamp(0.0, 1.0); + final visibility = particleFogTransmittance(b, v[6], v[7], v[8]); // A mix rather than an attenuation, which is the opposite of the particle // stage and for the opposite reason: this is blended, so a splat that // faded toward black would put black into the wall behind it. @@ -429,11 +448,7 @@ final class SplatHashedShader implements CpuFragmentShader { final fog = b.vec4('FogInfo', 'fog', Vector4.zero()); if (fog.w <= 0.0) return Vector4(v[0], v[1], v[2], 1.0); - final fogEye = b.vec4('FogInfo', 'eye', Vector4.zero()); - final d = - (Vector3(v[6], v[7], v[8]) - Vector3(fogEye.x, fogEye.y, fogEye.z)) - .length; - final visibility = math.exp(-fog.w * d).clamp(0.0, 1.0); + final visibility = particleFogTransmittance(b, v[6], v[7], v[8]); return Vector4( fog.x + (v[0] - fog.x) * visibility, fog.y + (v[1] - fog.y) * visibility, diff --git a/packages/flutter3d_cpu/test/particle_mesh_facing_test.dart b/packages/flutter3d_cpu/test/particle_mesh_facing_test.dart new file mode 100644 index 000000000..123c62219 --- /dev/null +++ b/packages/flutter3d_cpu/test/particle_mesh_facing_test.dart @@ -0,0 +1,53 @@ +/// A mesh particle faces the view axis through an orthographic lens — `P7`. +/// +/// dart test test/particle_mesh_facing_test.dart +/// +/// `particle_mesh.frag` shades a shard by how squarely it faces the eye. It +/// measured that towards the eye's point, which through an orthographic lens +/// is only where the camera was put: a shard square to the view came out dim +/// because the camera stood off to one side of it. This pins the Dart +/// transcription to `ParticleTowardsEye` in `lib/particle_fog.glsl`. +library; + +import 'dart:typed_data'; + +import 'package:flutter3d_cpu/flutter3d_cpu.dart'; +import 'package:test/test.dart'; + +/// What a white shard at the origin facing +z gives, the eye ten metres off +/// to its side and the view looking down −z. +double _brightness({required bool orthographic}) { + final varyings = Float32List.fromList([ + 1.0, 1.0, 1.0, 1.0, // colour + 0.0, 0.0, 0.0, // world + 0.0, 0.0, 1.0, // normal + ]); + final bindings = ShaderBindings(>{ + 'FogInfo': { + 'fog': Float32List(4), + 'eye': Float32List.fromList([10.0, 0.0, 0.0, 0.0]), + 'forward': Float32List.fromList([0.0, 0.0, -1.0, 0.0]), + 'projection': Float32List.fromList([ + if (orthographic) 1.0 else 0.0, + 0.0, + 0.0, + 0.0, + ]), + }, + }, const {}); + return const ParticleMeshShader() + .run(varyings, bindings, FragmentContext())! + .x; +} + +void main() { + test('square to an orthographic view, a shard is at full brightness', () { + // Mutation: face the eye's point whatever the lens, as before `P7` — + // edge-on to it, the shard drops to its floor of 0.35. + expect(_brightness(orthographic: true), closeTo(1.0, 1e-6)); + }); + + test('through a perspective lens it still faces the eye', () { + expect(_brightness(orthographic: false), closeTo(0.35, 1e-6)); + }); +} diff --git a/packages/flutter3d_cpu/test/splat_fog_test.dart b/packages/flutter3d_cpu/test/splat_fog_test.dart index 94445c1e8..21fa8a4cc 100644 --- a/packages/flutter3d_cpu/test/splat_fog_test.dart +++ b/packages/flutter3d_cpu/test/splat_fog_test.dart @@ -8,6 +8,7 @@ /// at the draw; the others drew the unfogged cloud without a word. library; +import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter3d_core/flutter3d_core.dart'; @@ -20,20 +21,33 @@ const int _height = 24; /// One opaque red splat at the origin, seen from five metres, as the centre /// pixel's colour. -List _centre(SplatComposite composite, FogSettings fog) { +List _centre(SplatComposite composite, FogSettings fog) => + _seen(composite, fog); + +/// One opaque red splat at [at], seen by [camera] (from five metres along +z +/// by default), as the colour of [pixel] (the centre by default). +List _seen( + SplatComposite composite, + FogSettings fog, { + CameraNode? camera, + Vector3? at, + (int, int) pixel = (_width ~/ 2, _height ~/ 2), +}) { final device = CpuDevice( width: _width, height: _height, shaders: CpuShaderLibrary(builtinCpuShaders()), ); - final camera = CameraNode() - ..setPosition(0.0, 0.0, 5.0) - ..lookAt(Vector3.zero()); + final view = + camera ?? + (CameraNode() + ..setPosition(0.0, 0.0, 5.0) + ..lookAt(Vector3.zero())); final scene = Scene() ..ambientIntensity = 0.0 - ..add(camera); + ..add(view); final cloud = SplatCloud( - centres: Float32List(3), + centres: Float32List.fromList([at?.x ?? 0, at?.y ?? 0, at?.z ?? 0]), colours: Float32List.fromList([1.0, 0.0, 0.0, 1.0]), scales: Float32List.fromList([1.0, 1.0, 1.0]), rotations: Float32List.fromList([0.0, 0.0, 0.0, 1.0]), @@ -45,7 +59,7 @@ List _centre(SplatComposite composite, FogSettings fog) { height: _height, scene: scene, views: [ - RenderView(camera: camera, clearColor: Vector4(0.0, 0.0, 0.0, 1.0)), + RenderView(camera: view, clearColor: Vector4(0.0, 0.0, 0.0, 1.0)), ], settings: RenderSettings( tonemap: false, @@ -54,8 +68,8 @@ List _centre(SplatComposite composite, FogSettings fog) { ), ); final pixels = device.readHdrPixels(result.frame); - final at = ((_height ~/ 2) * _width + _width ~/ 2) * 4; - return pixels.sublist(at, at + 3); + final index = (pixel.$2 * _width + pixel.$1) * 4; + return pixels.sublist(index, index + 3); } void main() { @@ -78,4 +92,34 @@ void main() { expect(fogged[2], greaterThan(0.5)); }); } + + test('through an orthographic lens a splat fogs by its depth', () { + // `P7`: three metres to the side of an orthographic camera five metres + // back, the splat is five metres deep and 5.8 from the eye's point. With + // blue fog over black, red over red and blue is the share the fog left. + // + // Mutation: fog by distance from the eye whatever the lens, as before, + // or leave `projection` unbound in `SplatContributor` — e^(−0.3 × 5.8) + // is 0.17, not e^(−1.5), 0.22. + final camera = CameraNode() + ..projection = const OrthographicProjection(height: 10.0, far: 100.0) + ..setPosition(0.0, 0.0, 5.0) + ..lookAt(Vector3.zero()); + // 10 m over 24 rows is 2.4 rows a metre, the same across: three metres + // right of the middle is column 16 + 7. + final seen = _seen( + SplatComposite.sorted, + FogSettings(color: Vector3(0.0, 0.0, 1.0), density: 0.3), + camera: camera, + at: Vector3(3.0, 0.0, 0.0), + pixel: (_width ~/ 2 + 7, _height ~/ 2), + ); + // The composite encodes; the share is one of light. + double linear(double encoded) => encoded <= 0.04045 + ? encoded / 12.92 + : math.pow((encoded + 0.055) / 1.055, 2.4).toDouble(); + final (red, blue) = (linear(seen[0]), linear(seen[2])); + expect(red + blue, greaterThan(0.1), reason: 'no splat there'); + expect(red / (red + blue), closeTo(math.exp(-1.5), 0.02)); + }); } diff --git a/packages/flutter3d_particles/CHANGELOG.md b/packages/flutter3d_particles/CHANGELOG.md index 5f814277f..15e14d033 100644 --- a/packages/flutter3d_particles/CHANGELOG.md +++ b/packages/flutter3d_particles/CHANGELOG.md @@ -1,5 +1,11 @@ ## Unreleased +* **Particles fog as the lit scene does** — `P7`, `P5`. Both contributors + bind the fog's whole block: the height fog's falloff, integrated along the + ray rather than read at the camera, and the view axis and the lens, so an + orthographic camera fogs by depth from its plane and a mesh particle + faces its axis. + * **Height fog, as thick as it is at the camera.** Both contributors hand their stages `FogSettings.densityAt` the camera's height rather than the density at the fog's base, so a height fog does not leave particles fogged diff --git a/packages/flutter3d_particles/lib/src/mesh_particle_contributor.dart b/packages/flutter3d_particles/lib/src/mesh_particle_contributor.dart index 3b3409621..af837e5aa 100644 --- a/packages/flutter3d_particles/lib/src/mesh_particle_contributor.dart +++ b/packages/flutter3d_particles/lib/src/mesh_particle_contributor.dart @@ -206,15 +206,25 @@ final class MeshParticleContributor extends PassContributor { _fog[1] = colour.y; _fog[2] = colour.z; view.camera.readWorldPosition(_eye); - // The air as thick as it is at the camera: a height fog's falloff is not - // integrated here — `FogSettings.densityAt` says what that costs. + // The density at the camera and the height fog's falloff, which the + // stage integrates along the ray as the lit stages do — `P5`; the view + // axis and the lens, so an orthographic camera fogs by depth from its + // plane — `P7`. The lit layout, whole: `lib/particle_fog.glsl`. _fog[3] = fog.densityAt(_eye.y); _eyeData[0] = _eye.x; _eyeData[1] = _eye.y; _eyeData[2] = _eye.z; + _eyeData[3] = fog.resolvedHeightFalloff; + view.camera.readForward(_fogAxis); + _fogForward[0] = _fogAxis.x; + _fogForward[1] = _fogAxis.y; + _fogForward[2] = _fogAxis.z; + _fogProjection[0] = isOrthographic(viewProjection) ? 1.0 : 0.0; encoder.bindUniformBlock(fragmentShader, 'FogInfo', { 'fog': _fog, 'eye': _eyeData, + 'forward': _fogForward, + 'projection': _fogProjection, }); encoder.draw(instanceCount: written); @@ -246,6 +256,9 @@ final class MeshParticleContributor extends PassContributor { final Set _missing = {}; final Float32List _fog = Float32List(4); final Float32List _eyeData = Float32List(4); + final Float32List _fogForward = Float32List(4); + final Float32List _fogProjection = Float32List(4); + final vm.Vector3 _fogAxis = vm.Vector3.zero(); final vm.Vector3 _eye = vm.Vector3.zero(); PipelineHandle? _pipeline; Float32List? _instances; diff --git a/packages/flutter3d_particles/lib/src/particle_contributor.dart b/packages/flutter3d_particles/lib/src/particle_contributor.dart index 1c887b571..8918915d8 100644 --- a/packages/flutter3d_particles/lib/src/particle_contributor.dart +++ b/packages/flutter3d_particles/lib/src/particle_contributor.dart @@ -187,15 +187,25 @@ final class ParticleContributor extends PassContributor { _fog[1] = colour.y; _fog[2] = colour.z; view.camera.readWorldPosition(_eye); - // The air as thick as it is at the camera: a height fog's falloff is not - // integrated here — `FogSettings.densityAt` says what that costs. + // The density at the camera and the height fog's falloff, which the + // stage integrates along the ray as the lit stages do — `P5`; the view + // axis and the lens, so an orthographic camera fogs by depth from its + // plane — `P7`. The lit layout, whole: `lib/particle_fog.glsl`. _fog[3] = fog.densityAt(_eye.y); _eyeData[0] = _eye.x; _eyeData[1] = _eye.y; _eyeData[2] = _eye.z; + _eyeData[3] = fog.resolvedHeightFalloff; + view.camera.readForward(_fogAxis); + _fogForward[0] = _fogAxis.x; + _fogForward[1] = _fogAxis.y; + _fogForward[2] = _fogAxis.z; + _fogProjection[0] = isOrthographic(viewProjection) ? 1.0 : 0.0; encoder.bindUniformBlock(fragmentShader, 'FogInfo', { 'fog': _fog, 'eye': _eyeData, + 'forward': _fogForward, + 'projection': _fogProjection, }); if (sheet != null && lights != null) { @@ -480,6 +490,9 @@ final class ParticleContributor extends PassContributor { final Float32List _fog = Float32List(4); final Float32List _eyeData = Float32List(4); + final Float32List _fogForward = Float32List(4); + final Float32List _fogProjection = Float32List(4); + final vm.Vector3 _fogAxis = vm.Vector3.zero(); final vm.Vector3 _eye = vm.Vector3.zero(); Float32List? _vertices; Uint32List? _indices; diff --git a/packages/flutter3d_particles/test/particle_fog_test.dart b/packages/flutter3d_particles/test/particle_fog_test.dart new file mode 100644 index 000000000..a778e04d6 --- /dev/null +++ b/packages/flutter3d_particles/test/particle_fog_test.dart @@ -0,0 +1,142 @@ +/// Particles fog as the lit scene does — `P7`, `P5`: by depth through an +/// orthographic lens, and thinning upwards in a height fog. +/// +/// dart test test/particle_fog_test.dart +/// +/// The particle stages declared the fog's block with two of its four members, +/// the colour and the eye. They could not tell an orthographic camera from a +/// perspective one, so they fogged in rings round the eye's point, which +/// through an orthographic lens is only where the camera was put; and they +/// could not read the height fog's falloff, so a puff of smoke fogged at the +/// camera's density whatever its height. Drawn by the software backend +/// through a real renderer. +library; + +import 'dart:math' as math; + +import 'package:flutter3d_core/flutter3d_core.dart'; +import 'package:flutter3d_cpu/flutter3d_cpu.dart'; +import 'package:flutter3d_particles/flutter3d_particles.dart'; +import 'package:test/test.dart'; +import 'package:vector_math/vector_math.dart'; + +const int _size = 48; + +/// The red, linear, of the pixels at [pixels] when discs two metres across +/// stand at [at], seen by [camera] under [fog]. +List _red({ + required CameraNode camera, + required List at, + required List<(int, int)> pixels, + FogSettings fog = const FogSettings(), +}) { + final device = CpuDevice( + width: _size, + height: _size, + shaders: CpuShaderLibrary(builtinCpuShaders()), + ); + final particles = ParticleSystem(capacity: at.length); + for (final where in at) { + particles.burst( + ParticleEffect( + count: 1, + emitter: const SphereEmitter(speed: Range.exact(0.0)), + lifetime: const Range.exact(5.0), + size: const Range.exact(2.0), + color: Vector4(0.8, 0.5, 0.2, 1.0), + ), + where, + ); + } + final scene = Scene()..add(camera); + final renderer = Renderer.create(device: device) + ..addContributor(ParticleContributor(particles)); + final result = renderer.render( + width: _size, + height: _size, + scene: scene, + views: [ + RenderView(camera: camera, clearColor: Vector4(0.0, 0.0, 0.0, 1.0)), + ], + settings: RenderSettings( + tonemap: false, + bloom: const BloomSettings(enabled: false), + fog: fog, + ), + ); + final frame = device.readHdrPixels(result.frame); + return [ + for (final (x, y) in pixels) + switch (frame[(y * _size + x) * 4]) { + final encoded when encoded <= 0.04045 => encoded / 12.92, + final encoded => math.pow((encoded + 0.055) / 1.055, 2.4).toDouble(), + }, + ]; +} + +void main() { + test('through an orthographic lens two discs at one depth fog alike', () { + // One on the axis and one three metres to the side, both five metres + // deep: the same air between them and the eye's plane. + // + // Mutation: fog by distance from the eye whatever the lens, as before + // `P7`. The one to the side is 5.8 m from the eye's point and comes out + // a quarter darker. + CameraNode camera() => + CameraNode() + ..projection = const OrthographicProjection(height: 8.0, far: 100.0); + final at = [Vector3(0.0, 0.0, -5.0), Vector3(3.0, 0.0, -5.0)]; + // Six pixels a metre, so three metres right of the middle is pixel 42. + const pixels = <(int, int)>[(24, 24), (42, 24)]; + final clear = _red(camera: camera(), at: at, pixels: pixels); + final fogged = _red( + camera: camera(), + at: at, + pixels: pixels, + fog: FogSettings(color: Vector3.zero(), density: 0.4), + ); + + expect(clear[0], greaterThan(0.05), reason: 'the middle disc is not there'); + expect(clear[1], greaterThan(0.05), reason: 'the side disc is not there'); + final middle = fogged[0] / clear[0]; + final side = fogged[1] / clear[1]; + expect(middle, closeTo(math.exp(-0.4 * 5.0), 0.02)); + expect(side, closeTo(middle, 0.02)); + }); + + test('in a height fog a disc above the eye fogs as thin air does', () { + // The camera at the fog's base looking up at a disc four metres higher + // and five ahead: the air along the way thins towards it, and the share + // the disc keeps is the closed form `ApplyFog` integrates for the lit + // stages. + // + // Mutation: drop the falloff from `ParticleFogTransmittance`. The disc + // fogs at the camera's density the whole way and comes out darker. + final target = Vector3(0.0, 4.0, -5.0); + CameraNode camera() => CameraNode()..lookAt(target); + final fog = FogSettings( + color: Vector3.zero(), + density: 0.4, + heightFalloff: 0.5, + ); + const pixels = <(int, int)>[(24, 24)]; + final clear = _red(camera: camera(), at: [target], pixels: pixels); + final fogged = _red( + camera: camera(), + at: [target], + pixels: pixels, + fog: fog, + ); + + final k = 0.5 * 4.0; + final mean = (1.0 - math.exp(-k)) / k; + final expected = math.exp(-fog.densityAt(0.0) * mean * target.length); + expect(clear.single, greaterThan(0.05), reason: 'the disc is not there'); + expect(fogged.single / clear.single, closeTo(expected, 0.02)); + expect( + fogged.single / clear.single, + greaterThan(math.exp(-fog.densityAt(0.0) * target.length) + 0.05), + reason: 'fogged at the camera\'s density the whole way', + ); + }); +} diff --git a/packages/flutter3d_shaders/CHANGELOG.md b/packages/flutter3d_shaders/CHANGELOG.md index 45e46828b..c38600742 100644 --- a/packages/flutter3d_shaders/CHANGELOG.md +++ b/packages/flutter3d_shaders/CHANGELOG.md @@ -1,5 +1,13 @@ ## Unreleased +- **Particles and splats declare the fog's block whole**, in the new + `lib/particle_fog.glsl` — `P7`, `P5`. They declared its first two members, + so they fogged in rings round the eye's point under an orthographic + camera, faced a mesh particle towards that point, and fogged at the + camera's density in a height fog. Now they fog by depth from the eye's + plane through an orthographic lens, a mesh particle faces the view axis + there, and the height fog is integrated along the ray as `ApplyFog` does. + - `ShadowFactor` picks a cascade by depth along the view axis through an orthographic lens (`P7`); the light shafts and the volumetric fog pick by the way along the ray from where depth is nought, the same distance through diff --git a/packages/flutter3d_shaders/lib/uniform_blocks.dart b/packages/flutter3d_shaders/lib/uniform_blocks.dart index 8e7ba4d94..2a3dd3158 100644 --- a/packages/flutter3d_shaders/lib/uniform_blocks.dart +++ b/packages/flutter3d_shaders/lib/uniform_blocks.dart @@ -877,12 +877,16 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, }, 'ParticleMesh': >{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, }, 'ParticleMeshVertex': >{ @@ -926,6 +930,8 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, 'LightListInfo': { 'list': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), @@ -990,6 +996,8 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, 'LightListInfo': { 'list': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), @@ -1032,6 +1040,8 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, 'SoftParticleInfo': { 'target': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), @@ -1043,12 +1053,16 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, }, 'ParticleTexturedSoft': >{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, 'SoftParticleInfo': { 'target': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), @@ -1539,12 +1553,16 @@ uniformBlocks = >>{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, }, 'SplatHashed': >{ 'FogInfo': { 'fog': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), 'eye': (offset: 16, byteLength: 16, elements: 1, type: 'Vector4'), + 'forward': (offset: 32, byteLength: 16, elements: 1, type: 'Vector4'), + 'projection': (offset: 48, byteLength: 16, elements: 1, type: 'Vector4'), }, 'SplatHashInfo': { 'frame': (offset: 0, byteLength: 16, elements: 1, type: 'Vector4'), diff --git a/packages/flutter3d_shaders/shaders/lib/particle.glsl b/packages/flutter3d_shaders/shaders/lib/particle.glsl index 7227e080a..16ce951c4 100644 --- a/packages/flutter3d_shaders/shaders/lib/particle.glsl +++ b/packages/flutter3d_shaders/shaders/lib/particle.glsl @@ -28,23 +28,8 @@ in vec3 v_world_position; out vec4 frag_color; -/// The lit shaders' block, declared again because this shader shares none of -/// their headers — it has a different vertex layout and none of their varyings. -/// -/// **The first two members of it, not all three.** `color.glsl` carries a -/// `forward` beside these, for the view axis the surface buffer measures its -/// depths along; a particle writes no surface buffer, so it neither declares -/// that member nor is bound one. The two blocks share a name and not a shape, -/// which is fine — they belong to different programs — and a check that binds -/// this one has to bind what it declares. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include void main() { // Distance from the middle of the quad, where the corners sit at 1. @@ -62,13 +47,7 @@ void main() { // and come out brighter than the wall it is supposed to be fading into; // multiplying toward zero is what "further away contributes less" means when // the destination is only ever added to. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); #ifdef F3D_SOFT_PARTICLE intensity *= SoftParticleFade(v_world_position); diff --git a/packages/flutter3d_shaders/shaders/lib/particle_fog.glsl b/packages/flutter3d_shaders/shaders/lib/particle_fog.glsl new file mode 100644 index 000000000..7ff772f1a --- /dev/null +++ b/packages/flutter3d_shaders/shaders/lib/particle_fog.glsl @@ -0,0 +1,74 @@ +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + +uniform FogInfo { + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. + vec4 fog; + + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. + vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; +} +fog_info; + +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ diff --git a/packages/flutter3d_shaders/shaders/lib/particle_six_way.glsl b/packages/flutter3d_shaders/shaders/lib/particle_six_way.glsl index 49dcc3047..2b984ac5f 100644 --- a/packages/flutter3d_shaders/shaders/lib/particle_six_way.glsl +++ b/packages/flutter3d_shaders/shaders/lib/particle_six_way.glsl @@ -48,16 +48,8 @@ out vec4 frag_color; uniform sampler2D six_way_positive; uniform sampler2D six_way_negative; -/// Declared again, as in the other particle stages, since this shares none of -/// the lit path's headers. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include uniform SixWayInfo { /// xyz: the quad's right, which is the camera's, in world space. @@ -117,13 +109,7 @@ void main() { // A mix toward the fog, unlike the additive stages: this one covers what is // behind it, and covered smoke far away should read as the fog does. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); color = mix(fog_info.fog.rgb, color, fogged); float coverage = clamp(v_color.a * positive.a, 0.0, 1.0); diff --git a/packages/flutter3d_shaders/shaders/lib/particle_textured.glsl b/packages/flutter3d_shaders/shaders/lib/particle_textured.glsl index aef661d5e..89a1cf3a2 100644 --- a/packages/flutter3d_shaders/shaders/lib/particle_textured.glsl +++ b/packages/flutter3d_shaders/shaders/lib/particle_textured.glsl @@ -27,16 +27,8 @@ out vec4 frag_color; uniform sampler2D particle_texture; -/// Declared again for the same reason the other particle stages declare it: -/// this shader shares none of the lit path's headers. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include void main() { // `texture`, not `textureLod`. The level is chosen from the derivative the @@ -49,13 +41,7 @@ void main() { // Attenuation rather than a mix. Blending an additive particle toward the // fog colour makes a distant one *add* fog to the wall behind it — the same // note as the other two particle stages, kept because each is read alone. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); // The texture's alpha is coverage and the particle's is brightness, so the // two multiply rather than one replacing the other: a faded spark of a diff --git a/packages/flutter3d_shaders/shaders/lighting/particle_mesh.frag b/packages/flutter3d_shaders/shaders/lighting/particle_mesh.frag index 1d4bcccfb..44dda3d67 100644 --- a/packages/flutter3d_shaders/shaders/lighting/particle_mesh.frag +++ b/packages/flutter3d_shaders/shaders/lighting/particle_mesh.frag @@ -27,28 +27,20 @@ in vec3 v_normal; out vec4 frag_color; -/// The same block the other particle stage declares, for the same reason: this -/// shader shares none of the lit shaders' headers. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include void main() { - vec3 to_eye = fog_info.eye.xyz - v_world_position; - float distance_to_eye = length(to_eye); - // `abs`, not `max(dot, 0)`. Nothing here is culled — a particle mesh is seen // from every side as it tumbles — so a back face is as visible as a front // one, and clamping would make half of every shard go black rather than dim. + // + // `P7`: faced against the view axis through an orthographic lens, where + // the eye's point is only where the camera was put — a shard turning past + // it would otherwise brighten and dim with where the camera stands. vec3 n = normalize(v_normal); - float facing = distance_to_eye > 0.0 - ? abs(dot(n, to_eye / distance_to_eye)) - : 1.0; + vec3 towards = ParticleTowardsEye(); + float facing = dot(towards, towards) > 0.0 ? abs(dot(n, towards)) : 1.0; // Never all the way to zero. A silhouette edge is exactly perpendicular to // the eye, and a face that vanished there would carve a dark seam across the @@ -58,10 +50,7 @@ void main() { // Attenuation rather than a mix, for the reason spelled out in // lighting/particle.frag: blending toward the fog colour makes a distant // additive particle *add* fog to the wall behind it. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp(exp(-fog_info.fog.w * distance_to_eye), 0.0, 1.0); - } + float fogged = ParticleFogTransmittance(); frag_color = vec4(v_color.rgb * v_color.a * intensity * fogged, 1.0); } diff --git a/packages/flutter3d_shaders/shaders/lighting/splat.frag b/packages/flutter3d_shaders/shaders/lighting/splat.frag index 33da889c4..5918aba99 100644 --- a/packages/flutter3d_shaders/shaders/lighting/splat.frag +++ b/packages/flutter3d_shaders/shaders/lighting/splat.frag @@ -30,18 +30,8 @@ in vec3 v_world_position; out vec4 frag_color; -/// The same block the particle stage declares, for the same reason it declares -/// it: a different vertex layout and none of the lit shaders' varyings, so none -/// of their headers apply. Two members, not three — nothing here writes a -/// surface buffer, so there is no view axis to measure a depth along. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include void main() { // `exp(-½ dᵀd)` with d already in standard deviations, which is what the @@ -67,11 +57,7 @@ void main() { // fading into the air. vec3 colour = v_color.rgb; if (fog_info.fog.w > 0.0) { - float visibility = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - colour = mix(fog_info.fog.rgb, colour, visibility); + colour = mix(fog_info.fog.rgb, colour, ParticleFogTransmittance()); } // Premultiplied, because that is what the blend state this is drawn under diff --git a/packages/flutter3d_shaders/shaders/lighting/splat_hashed.frag b/packages/flutter3d_shaders/shaders/lighting/splat_hashed.frag index 5da5b3a7b..8dfc24659 100644 --- a/packages/flutter3d_shaders/shaders/lighting/splat_hashed.frag +++ b/packages/flutter3d_shaders/shaders/lighting/splat_hashed.frag @@ -52,15 +52,8 @@ in vec3 v_world_position; out vec4 frag_color; -/// The block `splat.frag` declares, for the fog mix it makes. -uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. - vec4 fog; - - /// xyz: camera position in world space. - vec4 eye; -} -fog_info; +// The fog's block, whole — see `lib/particle_fog.glsl`. +#include /// `EngineTables.blueNoise`: 32 slices of 64 × 64 in an 8 × 4 atlas. uniform sampler2D blue_noise_texture; @@ -130,11 +123,7 @@ void main() { vec3 colour = v_color.rgb; if (fog_info.fog.w > 0.0) { - float visibility = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - colour = mix(fog_info.fog.rgb, colour, visibility); + colour = mix(fog_info.fog.rgb, colour, ParticleFogTransmittance()); } // Opaque: whether the splat is here was decided above, and how much of it diff --git a/packages/flutter3d_webgl/CHANGELOG.md b/packages/flutter3d_webgl/CHANGELOG.md index 07d141a07..a6b83cba5 100644 --- a/packages/flutter3d_webgl/CHANGELOG.md +++ b/packages/flutter3d_webgl/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- The shader table regenerated for the particles' and splats' whole fog + block. + - The shader table regenerated for the orthographic camera's cascade pick; `orthographic-shadow` is in the browser reference set. diff --git a/packages/flutter3d_webgl/lib/engine_shaders.dart b/packages/flutter3d_webgl/lib/engine_shaders.dart index 045d5dbee..502f57b95 100644 --- a/packages/flutter3d_webgl/lib/engine_shaders.dart +++ b/packages/flutter3d_webgl/lib/engine_shaders.dart @@ -25202,24 +25202,84 @@ in vec3 v_world_position; layout(location = 0) out vec4 frag_color; -/// The lit shaders' block, declared again because this shader shares none of -/// their headers — it has a different vertex layout and none of their varyings. -/// -/// **The first two members of it, not all three.** `color.glsl` carries a -/// `forward` beside these, for the view axis the surface buffer measures its -/// depths along; a particle writes no surface buffer, so it neither declares -/// that member nor is bound one. The two blocks share a name and not a shape, -/// which is fine — they belong to different programs — and a check that binds -/// this one has to bind what it declares. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + void main() { // Distance from the middle of the quad, where the corners sit at 1. vec2 centred = v_uv * 2.0 - 1.0; @@ -25236,13 +25296,7 @@ void main() { // and come out brighter than the wall it is supposed to be fading into; // multiplying toward zero is what "further away contributes less" means when // the destination is only ever added to. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); #ifdef F3D_SOFT_PARTICLE intensity *= SoftParticleFade(v_world_position); @@ -25388,17 +25442,84 @@ layout(location = 0) out vec4 frag_color; uniform sampler2D particle_texture; -/// Declared again for the same reason the other particle stages declare it: -/// this shader shares none of the lit path's headers. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + void main() { // `texture`, not `textureLod`. The level is chosen from the derivative the // hardware computes for this fragment, which is the whole point of building @@ -25410,13 +25531,7 @@ void main() { // Attenuation rather than a mix. Blending an additive particle toward the // fog colour makes a distant one *add* fog to the wall behind it — the same // note as the other two particle stages, kept because each is read alone. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); // The texture's alpha is coverage and the particle's is brightness, so the // two multiply rather than one replacing the other: a faded spark of a @@ -25866,17 +25981,84 @@ layout(location = 0) out vec4 frag_color; uniform sampler2D six_way_positive; uniform sampler2D six_way_negative; -/// Declared again, as in the other particle stages, since this shares none of -/// the lit path's headers. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + layout(std140) uniform SixWayInfo { /// xyz: the quad's right, which is the camera's, in world space. vec4 right; @@ -25935,13 +26117,7 @@ void main() { // A mix toward the fog, unlike the additive stages: this one covers what is // behind it, and covered smoke far away should read as the fog does. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); color = mix(fog_info.fog.rgb, color, fogged); float coverage = clamp(v_color.a * positive.a, 0.0, 1.0); @@ -26091,24 +26267,84 @@ in vec3 v_world_position; layout(location = 0) out vec4 frag_color; -/// The lit shaders' block, declared again because this shader shares none of -/// their headers — it has a different vertex layout and none of their varyings. -/// -/// **The first two members of it, not all three.** `color.glsl` carries a -/// `forward` beside these, for the view axis the surface buffer measures its -/// depths along; a particle writes no surface buffer, so it neither declares -/// that member nor is bound one. The two blocks share a name and not a shape, -/// which is fine — they belong to different programs — and a check that binds -/// this one has to bind what it declares. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + void main() { // Distance from the middle of the quad, where the corners sit at 1. vec2 centred = v_uv * 2.0 - 1.0; @@ -26125,13 +26361,7 @@ void main() { // and come out brighter than the wall it is supposed to be fading into; // multiplying toward zero is what "further away contributes less" means when // the destination is only ever added to. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); #ifdef F3D_SOFT_PARTICLE intensity *= SoftParticleFade(v_world_position); @@ -26277,17 +26507,84 @@ layout(location = 0) out vec4 frag_color; uniform sampler2D particle_texture; -/// Declared again for the same reason the other particle stages declare it: -/// this shader shares none of the lit path's headers. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + void main() { // `texture`, not `textureLod`. The level is chosen from the derivative the // hardware computes for this fragment, which is the whole point of building @@ -26299,13 +26596,7 @@ void main() { // Attenuation rather than a mix. Blending an additive particle toward the // fog colour makes a distant one *add* fog to the wall behind it — the same // note as the other two particle stages, kept because each is read alone. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); // The texture's alpha is coverage and the particle's is brightness, so the // two multiply rather than one replacing the other: a faded spark of a @@ -26756,17 +27047,84 @@ layout(location = 0) out vec4 frag_color; uniform sampler2D six_way_positive; uniform sampler2D six_way_negative; -/// Declared again, as in the other particle stages, since this shares none of -/// the lit path's headers. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + layout(std140) uniform SixWayInfo { /// xyz: the quad's right, which is the camera's, in world space. vec4 right; @@ -26825,13 +27183,7 @@ void main() { // A mix toward the fog, unlike the additive stages: this one covers what is // behind it, and covered smoke far away should read as the fog does. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - } + float fogged = ParticleFogTransmittance(); color = mix(fog_info.fog.rgb, color, fogged); float coverage = clamp(v_color.a * positive.a, 0.0, 1.0); @@ -26881,19 +27233,84 @@ in vec3 v_world_position; layout(location = 0) out vec4 frag_color; -/// The same block the particle stage declares, for the same reason it declares -/// it: a different vertex layout and none of the lit shaders' varyings, so none -/// of their headers apply. Two members, not three — nothing here writes a -/// surface buffer, so there is no view axis to measure a depth along. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + void main() { // `exp(-½ dᵀd)` with d already in standard deviations, which is what the // quad's own coordinates are. No matrix here: the inverse covariance was @@ -26918,11 +27335,7 @@ void main() { // fading into the air. vec3 colour = v_color.rgb; if (fog_info.fog.w > 0.0) { - float visibility = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - colour = mix(fog_info.fog.rgb, colour, visibility); + colour = mix(fog_info.fog.rgb, colour, ParticleFogTransmittance()); } // Premultiplied, because that is what the blend state this is drawn under @@ -27019,16 +27432,84 @@ in vec3 v_world_position; layout(location = 0) out vec4 frag_color; -/// The block `splat.frag` declares, for the fog mix it makes. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { + vec3 to_eye = fog_info.eye.xyz - v_world_position; + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + + /// `EngineTables.blueNoise`: 32 slices of 64 × 64 in an 8 × 4 atlas. uniform sampler2D blue_noise_texture; @@ -27097,11 +27578,7 @@ void main() { vec3 colour = v_color.rgb; if (fog_info.fog.w > 0.0) { - float visibility = clamp( - exp(-fog_info.fog.w * distance(v_world_position, fog_info.eye.xyz)), - 0.0, - 1.0); - colour = mix(fog_info.fog.rgb, colour, visibility); + colour = mix(fog_info.fog.rgb, colour, ParticleFogTransmittance()); } // Opaque: whether the splat is here was decided above, and how much of it @@ -27143,28 +27620,95 @@ in vec3 v_normal; layout(location = 0) out vec4 frag_color; -/// The same block the other particle stage declares, for the same reason: this -/// shader shares none of the lit shaders' headers. +// The fog's block, whole — see `lib/particle_fog.glsl`. +// --- lib/particle_fog.glsl --- +// The fog, for the stages that share none of the lit shaders' headers: +// particles and splats — `P7`, `P5`. +// +// **The lit block's whole shape, where these stages used to declare its first +// two members.** With only the fog and the eye, a particle could not tell an +// orthographic camera from a perspective one, so it fogged in rings round a +// point nobody sees and faced a mesh particle towards it; and with the eye's +// `w` undeclared it could not thin upwards with a height fog, so it fogged at +// the camera's density whatever its height. The four members and their +// meanings are `color.glsl`'s, and the engine fills them the same way. +// +// Include after declaring `v_world_position`. + +#ifndef PARTICLE_FOG_GLSL_ +#define PARTICLE_FOG_GLSL_ + layout(std140) uniform FogInfo { - /// rgb: linear fog colour. w: density per metre, zero for no fog. + /// rgb: linear fog colour. w: density per metre at the eye, zero for no + /// fog. vec4 fog; - /// xyz: camera position in world space. + /// xyz: camera position in world space. w: how fast the fog thins + /// upwards, per metre; nought is flat fog. vec4 eye; + + /// xyz: the direction the camera looks, a unit vector in world space. + /// w: unused here. + vec4 forward; + + /// x: one when the camera's projection is orthographic, nought when it is + /// perspective. y, z, w unused. + vec4 projection; } fog_info; -void main() { +/// Whether the camera is orthographic. See `Orthographic` in `color.glsl`. +bool ParticleOrthographic() { return fog_info.projection.x > 0.5; } + +/// The air between the eye and this fragment, in metres: the distance from +/// the eye through a perspective lens, the depth from the eye's plane +/// through an orthographic one — `EyeDistance` in `color.glsl`. +float ParticleEyeDistance() { + return ParticleOrthographic() + ? max(dot(v_world_position - fog_info.eye.xyz, + fog_info.forward.xyz), + 0.0) + : distance(v_world_position, fog_info.eye.xyz); +} + +/// The unit direction back along the ray that reached this fragment: to the +/// eye through a perspective lens, against the view axis through an +/// orthographic one. `TowardsEye` in `color.glsl`. +vec3 ParticleTowardsEye() { vec3 to_eye = fog_info.eye.xyz - v_world_position; - float distance_to_eye = length(to_eye); + float length_to_eye = length(to_eye); + if (ParticleOrthographic()) return -fog_info.forward.xyz; + return length_to_eye > 0.0 ? to_eye / length_to_eye : vec3(0.0); +} + +/// How much of this fragment the fog leaves, from nought to one: `ApplyFog`'s +/// share in `color.glsl`, height fog and all, so a puff of smoke in a valley +/// fogs as the ground beside it does. One with no fog. +float ParticleFogTransmittance() { + float density = fog_info.fog.w; + if (density <= 0.0) return 1.0; + float falloff = fog_info.eye.w; + if (falloff > 0.0) { + float k = falloff * (v_world_position.y - fog_info.eye.y); + density *= abs(k) > 1e-3 ? (1.0 - exp(-k)) / k : 1.0 - 0.5 * k; + } + return clamp(exp(-density * ParticleEyeDistance()), 0.0, 1.0); +} + +#endif // PARTICLE_FOG_GLSL_ + +void main() { // `abs`, not `max(dot, 0)`. Nothing here is culled — a particle mesh is seen // from every side as it tumbles — so a back face is as visible as a front // one, and clamping would make half of every shard go black rather than dim. + // + // `P7`: faced against the view axis through an orthographic lens, where + // the eye's point is only where the camera was put — a shard turning past + // it would otherwise brighten and dim with where the camera stands. vec3 n = normalize(v_normal); - float facing = distance_to_eye > 0.0 - ? abs(dot(n, to_eye / distance_to_eye)) - : 1.0; + vec3 towards = ParticleTowardsEye(); + float facing = dot(towards, towards) > 0.0 ? abs(dot(n, towards)) : 1.0; // Never all the way to zero. A silhouette edge is exactly perpendicular to // the eye, and a face that vanished there would carve a dark seam across the @@ -27174,10 +27718,7 @@ void main() { // Attenuation rather than a mix, for the reason spelled out in // lighting/particle.frag: blending toward the fog colour makes a distant // additive particle *add* fog to the wall behind it. - float fogged = 1.0; - if (fog_info.fog.w > 0.0) { - fogged = clamp(exp(-fog_info.fog.w * distance_to_eye), 0.0, 1.0); - } + float fogged = ParticleFogTransmittance(); frag_color = vec4(v_color.rgb * v_color.a * intensity * fogged, 1.0); } diff --git a/packages/flutter3d_webgpu/CHANGELOG.md b/packages/flutter3d_webgpu/CHANGELOG.md index 3b9221387..704172085 100644 --- a/packages/flutter3d_webgpu/CHANGELOG.md +++ b/packages/flutter3d_webgpu/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- The shader table regenerated for the particles' and splats' whole fog + block. + - The shader table regenerated for the orthographic camera's cascade pick; `orthographic-shadow` is in the WebGPU reference set. diff --git a/packages/flutter3d_webgpu/lib/engine_shaders.dart b/packages/flutter3d_webgpu/lib/engine_shaders.dart index 8da505f7a..691794d84 100644 --- a/packages/flutter3d_webgpu/lib/engine_shaders.dart +++ b/packages/flutter3d_webgpu/lib/engine_shaders.dart @@ -31271,52 +31271,109 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(6) v_world_positio struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } -var v_uv_1: vec2; @group(1) @binding(0) var fog_info: FogInfo; var v_world_position_1: vec3; +var v_uv_1: vec2; var frag_color: vec4; var v_color_1: vec4; +fn ParticleOrthographic_u0028_() -> bool { + let _e19 = fog_info.projection[0u]; + return (_e19 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e18 = ParticleOrthographic_u0028_(); + if _e18 { + let _e19 = v_world_position_1; + let _e21 = fog_info.eye; + let _e25 = fog_info.forward; + local = max(dot((_e19 - _e21.xyz), _e25.xyz), 0f); + } else { + let _e29 = v_world_position_1; + let _e31 = fog_info.eye; + local = distance(_e29, _e31.xyz); + } + let _e34 = local; + return _e34; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e23 = fog_info.fog[3u]; + density = _e23; + let _e24 = density; + if (_e24 <= 0f) { + return 1f; + } + let _e28 = fog_info.eye[3u]; + falloff = _e28; + let _e29 = falloff; + if (_e29 > 0f) { + let _e31 = falloff; + let _e33 = v_world_position_1[1u]; + let _e36 = fog_info.eye[1u]; + k = (_e31 * (_e33 - _e36)); + let _e39 = k; + if (abs(_e39) > 0.001f) { + let _e42 = k; + let _e46 = k; + local_1 = ((1f - exp(-(_e42))) / _e46); + } else { + let _e48 = k; + local_1 = (1f - (0.5f * _e48)); + } + let _e51 = local_1; + let _e52 = density; + density = (_e52 * _e51); + } + let _e54 = density; + let _e56 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e54) * _e56)), 0f, 1f); +} + fn main_1() { var centred: vec2; var radius: f32; - var falloff: f32; + var falloff_1: f32; var intensity: f32; var fogged: f32; - let _e16 = v_uv_1; - centred = ((_e16 * 2f) - vec2(1f)); - let _e20 = centred; - radius = length(_e20); - let _e22 = radius; - falloff = (1f - smoothstep(0f, 1f, _e22)); - let _e25 = falloff; - let _e26 = falloff; - intensity = (_e25 * _e26); - fogged = 1f; - let _e30 = fog_info.fog[3u]; - if (_e30 > 0f) { - let _e34 = fog_info.fog[3u]; - let _e36 = v_world_position_1; - let _e38 = fog_info.eye; - fogged = clamp(exp((-(_e34) * distance(_e36, _e38.xyz))), 0f, 1f); - } - let _e44 = v_color_1; - let _e47 = v_color_1[3u]; - let _e49 = intensity; - let _e51 = fogged; - let _e52 = (((_e44.xyz * _e47) * _e49) * _e51); - frag_color = vec4(_e52.x, _e52.y, _e52.z, 1f); + let _e22 = v_uv_1; + centred = ((_e22 * 2f) - vec2(1f)); + let _e26 = centred; + radius = length(_e26); + let _e28 = radius; + falloff_1 = (1f - smoothstep(0f, 1f, _e28)); + let _e31 = falloff_1; + let _e32 = falloff_1; + intensity = (_e31 * _e32); + let _e34 = ParticleFogTransmittance_u0028_(); + fogged = _e34; + let _e35 = v_color_1; + let _e38 = v_color_1[3u]; + let _e40 = intensity; + let _e42 = fogged; + let _e43 = (((_e35.xyz * _e38) * _e40) * _e42); + frag_color = vec4(_e43.x, _e43.y, _e43.z, 1f); return; } @fragment -fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { - v_uv_1 = v_uv; +fn main(@location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e7 = frag_color; @@ -31329,10 +31386,20 @@ fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), ], @@ -31343,51 +31410,108 @@ fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } +@group(1) @binding(0) +var fog_info: FogInfo; +var v_world_position_1: vec3; @group(1) @binding(1) var particle_texture_tex: texture_2d; @group(1) @binding(2) var particle_texture_smp: sampler; var v_uv_1: vec2; -@group(1) @binding(0) -var fog_info: FogInfo; -var v_world_position_1: vec3; var v_color_1: vec4; var frag_color: vec4; +fn ParticleOrthographic_u0028_() -> bool { + let _e20 = fog_info.projection[0u]; + return (_e20 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e19 = ParticleOrthographic_u0028_(); + if _e19 { + let _e20 = v_world_position_1; + let _e22 = fog_info.eye; + let _e26 = fog_info.forward; + local = max(dot((_e20 - _e22.xyz), _e26.xyz), 0f); + } else { + let _e30 = v_world_position_1; + let _e32 = fog_info.eye; + local = distance(_e30, _e32.xyz); + } + let _e35 = local; + return _e35; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e24 = fog_info.fog[3u]; + density = _e24; + let _e25 = density; + if (_e25 <= 0f) { + return 1f; + } + let _e29 = fog_info.eye[3u]; + falloff = _e29; + let _e30 = falloff; + if (_e30 > 0f) { + let _e32 = falloff; + let _e34 = v_world_position_1[1u]; + let _e37 = fog_info.eye[1u]; + k = (_e32 * (_e34 - _e37)); + let _e40 = k; + if (abs(_e40) > 0.001f) { + let _e43 = k; + let _e47 = k; + local_1 = ((1f - exp(-(_e43))) / _e47); + } else { + let _e49 = k; + local_1 = (1f - (0.5f * _e49)); + } + let _e52 = local_1; + let _e53 = density; + density = (_e53 * _e52); + } + let _e55 = density; + let _e57 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e55) * _e57)), 0f, 1f); +} + fn main_1() { var texel: vec4; var fogged: f32; var scale: f32; - let _e15 = v_uv_1; - let _e16 = textureSample(particle_texture_tex, particle_texture_smp, _e15); - texel = _e16; - fogged = 1f; - let _e19 = fog_info.fog[3u]; - if (_e19 > 0f) { - let _e23 = fog_info.fog[3u]; - let _e25 = v_world_position_1; - let _e27 = fog_info.eye; - fogged = clamp(exp((-(_e23) * distance(_e25, _e27.xyz))), 0f, 1f); - } - let _e34 = v_color_1[3u]; - let _e36 = texel[3u]; - let _e38 = fogged; - scale = ((_e34 * _e36) * _e38); - let _e40 = v_color_1; - let _e42 = texel; - let _e45 = scale; - let _e46 = ((_e40.xyz * _e42.xyz) * _e45); - frag_color = vec4(_e46.x, _e46.y, _e46.z, 1f); + let _e21 = v_uv_1; + let _e22 = textureSample(particle_texture_tex, particle_texture_smp, _e21); + texel = _e22; + let _e23 = ParticleFogTransmittance_u0028_(); + fogged = _e23; + let _e25 = v_color_1[3u]; + let _e27 = texel[3u]; + let _e29 = fogged; + scale = ((_e25 * _e27) * _e29); + let _e31 = v_color_1; + let _e33 = texel; + let _e36 = scale; + let _e37 = ((_e31.xyz * _e33.xyz) * _e36); + frag_color = vec4(_e37.x, _e37.y, _e37.z, 1f); return; } @fragment -fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { - v_uv_1 = v_uv; +fn main(@location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e7 = frag_color; @@ -31400,10 +31524,20 @@ fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), ], @@ -31437,6 +31571,13 @@ struct ContributorLightInfo { slots: vec4, } +struct FogInfo { + fog: vec4, + eye: vec4, + forward: vec4, + projection: vec4, +} + struct SixWayInfo { right: vec4, up: vec4, @@ -31445,11 +31586,6 @@ struct SixWayInfo { ambient: vec4, } -struct FogInfo { - fog: vec4, - eye: vec4, -} - var g_cluster_offset: f32; var g_cluster_count: f32; @group(1) @binding(2) @@ -31460,6 +31596,9 @@ var light_list_texture_tex: texture_2d; var light_list_texture_smp: sampler; @group(1) @binding(0) var contributor_light_info: ContributorLightInfo; +@group(1) @binding(1) +var fog_info: FogInfo; +var v_world_position_1: vec3; @group(1) @binding(3) var six_way_info: SixWayInfo; @group(1) @binding(8) @@ -31471,156 +31610,214 @@ var v_uv_1: vec2; var six_way_negative_tex: texture_2d; @group(1) @binding(7) var six_way_negative_smp: sampler; -var v_world_position_1: vec3; var v_color_1: vec4; -@group(1) @binding(1) -var fog_info: FogInfo; var frag_color: vec4; +fn ParticleOrthographic_u0028_() -> bool { + let _e47 = fog_info.projection[0u]; + return (_e47 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e46 = ParticleOrthographic_u0028_(); + if _e46 { + let _e47 = v_world_position_1; + let _e49 = fog_info.eye; + let _e53 = fog_info.forward; + local = max(dot((_e47 - _e49.xyz), _e53.xyz), 0f); + } else { + let _e57 = v_world_position_1; + let _e59 = fog_info.eye; + local = distance(_e57, _e59.xyz); + } + let _e62 = local; + return _e62; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e51 = fog_info.fog[3u]; + density = _e51; + let _e52 = density; + if (_e52 <= 0f) { + return 1f; + } + let _e56 = fog_info.eye[3u]; + falloff = _e56; + let _e57 = falloff; + if (_e57 > 0f) { + let _e59 = falloff; + let _e61 = v_world_position_1[1u]; + let _e64 = fog_info.eye[1u]; + k = (_e59 * (_e61 - _e64)); + let _e67 = k; + if (abs(_e67) > 0.001f) { + let _e70 = k; + let _e74 = k; + local_1 = ((1f - exp(-(_e70))) / _e74); + } else { + let _e76 = k; + local_1 = (1f - (0.5f * _e76)); + } + let _e79 = local_1; + let _e80 = density; + density = (_e80 * _e79); + } + let _e82 = density; + let _e84 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e82) * _e84)), 0f, 1f); +} + fn SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b(l: ptr>, positive: ptr>, negative: ptr>) -> f32 { var x: f32; var y: f32; var z: f32; - var local: f32; - var local_1: f32; var local_2: f32; + var local_3: f32; + var local_4: f32; - let _e53 = (*l); - let _e55 = six_way_info.right; - x = dot(_e53, _e55.xyz); - let _e58 = (*l); - let _e60 = six_way_info.up; - y = dot(_e58, _e60.xyz); - let _e63 = (*l); - let _e65 = six_way_info.forward; - z = dot(_e63, _e65.xyz); - let _e68 = x; + let _e54 = (*l); + let _e56 = six_way_info.right; + x = dot(_e54, _e56.xyz); + let _e59 = (*l); + let _e61 = six_way_info.up; + y = dot(_e59, _e61.xyz); + let _e64 = (*l); + let _e66 = six_way_info.forward; + z = dot(_e64, _e66.xyz); let _e69 = x; - let _e71 = x; - if (_e71 > 0f) { - let _e74 = (*positive)[0u]; - local = _e74; + let _e70 = x; + let _e72 = x; + if (_e72 > 0f) { + let _e75 = (*positive)[0u]; + local_2 = _e75; } else { - let _e76 = (*negative)[0u]; - local = _e76; + let _e77 = (*negative)[0u]; + local_2 = _e77; } - let _e77 = local; - let _e79 = y; + let _e78 = local_2; let _e80 = y; - let _e82 = y; - if (_e82 > 0f) { - let _e85 = (*positive)[1u]; - local_1 = _e85; + let _e81 = y; + let _e83 = y; + if (_e83 > 0f) { + let _e86 = (*positive)[1u]; + local_3 = _e86; } else { - let _e87 = (*negative)[1u]; - local_1 = _e87; + let _e88 = (*negative)[1u]; + local_3 = _e88; } - let _e88 = local_1; - let _e91 = z; + let _e89 = local_3; let _e92 = z; - let _e94 = z; - if (_e94 > 0f) { - let _e97 = (*positive)[2u]; - local_2 = _e97; + let _e93 = z; + let _e95 = z; + if (_e95 > 0f) { + let _e98 = (*positive)[2u]; + local_4 = _e98; } else { - let _e99 = (*negative)[2u]; - local_2 = _e99; + let _e100 = (*negative)[2u]; + local_4 = _e100; } - let _e100 = local_2; - return ((((_e68 * _e69) * _e77) + ((_e79 * _e80) * _e88)) + ((_e91 * _e92) * _e100)); + let _e101 = local_4; + return ((((_e69 * _e70) * _e78) + ((_e80 * _e81) * _e89)) + ((_e92 * _e93) * _e101)); } fn LightListLane_u0028_vf4_u003b_i1_u003b(four: ptr>, slot: ptr) -> f32 { var lane: i32; - var local_3: f32; - var local_4: f32; var local_5: f32; + var local_6: f32; + var local_7: f32; - let _e50 = (*slot); let _e51 = (*slot); - lane = (_e50 - ((_e51 / 4i) * 4i)); - let _e55 = lane; - if (_e55 == 0i) { - let _e58 = (*four)[0u]; - local_3 = _e58; - } else { - let _e59 = lane; - if (_e59 == 1i) { - let _e62 = (*four)[1u]; - local_4 = _e62; + let _e52 = (*slot); + lane = (_e51 - ((_e52 / 4i) * 4i)); + let _e56 = lane; + if (_e56 == 0i) { + let _e59 = (*four)[0u]; + local_5 = _e59; + } else { + let _e60 = lane; + if (_e60 == 1i) { + let _e63 = (*four)[1u]; + local_6 = _e63; } else { - let _e63 = lane; - if (_e63 == 2i) { - let _e66 = (*four)[2u]; - local_5 = _e66; + let _e64 = lane; + if (_e64 == 2i) { + let _e67 = (*four)[2u]; + local_7 = _e67; } else { - let _e68 = (*four)[3u]; - local_5 = _e68; + let _e69 = (*four)[3u]; + local_7 = _e69; } - let _e69 = local_5; - local_4 = _e69; + let _e70 = local_7; + local_6 = _e70; } - let _e70 = local_4; - local_3 = _e70; + let _e71 = local_6; + local_5 = _e71; } - let _e71 = local_3; - return _e71; + let _e72 = local_5; + return _e72; } fn LightListScale_u0028_i1_u003b(slot_1: ptr) -> f32 { var param: vec4; var param_1: i32; - let _e47 = (*slot_1); - let _e51 = light_list_info.scales[(_e47 / 4i)]; - param = _e51; - let _e52 = (*slot_1); - param_1 = _e52; - let _e53 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m), (¶m_1)); - return _e53; + let _e48 = (*slot_1); + let _e52 = light_list_info.scales[(_e48 / 4i)]; + param = _e52; + let _e53 = (*slot_1); + param_1 = _e53; + let _e54 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m), (¶m_1)); + return _e54; } fn InSlots_u0028_f1_u003b(row: ptr) -> bool { var a: vec4; var b: vec4; - let _e49 = light_list_info.slot_rows[0i]; - let _e50 = (*row); - a = abs((_e49 - vec4(_e50))); - let _e56 = light_list_info.slot_rows[1i]; - let _e57 = (*row); - b = abs((_e56 - vec4(_e57))); - let _e62 = a[0u]; - let _e64 = a[1u]; - let _e67 = a[2u]; - let _e69 = a[3u]; - let _e73 = b[0u]; - let _e75 = b[1u]; - let _e78 = b[2u]; - let _e80 = b[3u]; - return (min(min(min(_e62, _e64), min(_e67, _e69)), min(min(_e73, _e75), min(_e78, _e80))) < 0.5f); + let _e50 = light_list_info.slot_rows[0i]; + let _e51 = (*row); + a = abs((_e50 - vec4(_e51))); + let _e57 = light_list_info.slot_rows[1i]; + let _e58 = (*row); + b = abs((_e57 - vec4(_e58))); + let _e63 = a[0u]; + let _e65 = a[1u]; + let _e68 = a[2u]; + let _e70 = a[3u]; + let _e74 = b[0u]; + let _e76 = b[1u]; + let _e79 = b[2u]; + let _e81 = b[3u]; + return (min(min(min(_e63, _e65), min(_e68, _e70)), min(min(_e74, _e76), min(_e79, _e81))) < 0.5f); } fn LightListRow_u0028_i1_u003b(slot_2: ptr) -> f32 { var param_2: vec4; var param_3: i32; - let _e47 = (*slot_2); - let _e51 = light_list_info.indices[(_e47 / 4i)]; - param_2 = _e51; - let _e52 = (*slot_2); - param_3 = _e52; - let _e53 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_2), (¶m_3)); - return _e53; + let _e48 = (*slot_2); + let _e52 = light_list_info.indices[(_e48 / 4i)]; + param_2 = _e52; + let _e53 = (*slot_2); + param_3 = _e53; + let _e54 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_2), (¶m_3)); + return _e54; } fn LightListTexel_u0028_f1_u003b_f1_u003b(texel: ptr, row_1: ptr) -> vec4 { - let _e46 = (*texel); - let _e50 = light_list_info.list[1u]; - let _e52 = (*row_1); - let _e56 = light_list_info.list[2u]; - let _e59 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2(((_e46 + 0.5f) * _e50), ((_e52 + 0.5f) * _e56)), 0f); - return _e59; + let _e47 = (*texel); + let _e51 = light_list_info.list[1u]; + let _e53 = (*row_1); + let _e57 = light_list_info.list[2u]; + let _e60 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2(((_e47 + 0.5f) * _e51), ((_e53 + 0.5f) * _e57)), 0f); + return _e60; } fn ClusterRow_u0028_i1_u003b(slot_3: ptr) -> f32 { @@ -31634,35 +31831,35 @@ fn ClusterRow_u0028_i1_u003b(slot_3: ptr) -> f32 { var param_6: vec4; var param_7: i32; - let _e54 = g_cluster_offset; - let _e55 = (*slot_3); - entry = (_e54 + f32(_e55)); - let _e58 = entry; - row_2 = floor((_e58 / 16f)); - let _e61 = entry; - let _e62 = row_2; - within = (_e61 - (_e62 * 16f)); - let _e65 = within; - texel_1 = floor((_e65 / 4f)); - let _e70 = light_list_info.cluster_depth[3u]; - let _e71 = row_2; - let _e73 = texel_1; - param_4 = _e73; - param_5 = (_e70 + _e71); - let _e74 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_4), (¶m_5)); - four_1 = _e74; - let _e75 = within; - let _e76 = texel_1; - let _e81 = four_1; - param_6 = _e81; - param_7 = i32(((_e75 - (_e76 * 4f)) + 0.5f)); - let _e82 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_6), (¶m_7)); - return _e82; + let _e55 = g_cluster_offset; + let _e56 = (*slot_3); + entry = (_e55 + f32(_e56)); + let _e59 = entry; + row_2 = floor((_e59 / 16f)); + let _e62 = entry; + let _e63 = row_2; + within = (_e62 - (_e63 * 16f)); + let _e66 = within; + texel_1 = floor((_e66 / 4f)); + let _e71 = light_list_info.cluster_depth[3u]; + let _e72 = row_2; + let _e74 = texel_1; + param_4 = _e74; + param_5 = (_e71 + _e72); + let _e75 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_4), (¶m_5)); + four_1 = _e75; + let _e76 = within; + let _e77 = texel_1; + let _e82 = four_1; + param_6 = _e82; + param_7 = i32(((_e76 - (_e77 * 4f)) + 0.5f)); + let _e83 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_6), (¶m_7)); + return _e83; } fn Clustered_u0028_() -> bool { - let _e46 = light_list_info.cluster_grid[3u]; - return (_e46 > 0.5f); + let _e47 = light_list_info.cluster_grid[3u]; + return (_e47 > 0.5f); } fn ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b(index: ptr, world: ptr>, toLight: ptr>, radiance: ptr>) { @@ -31673,12 +31870,12 @@ fn ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b(index: ptr; var degenerate: bool; - var local_8: vec3; + var local_10: vec3; var ratio: f32; - var local_9: f32; + var local_11: f32; var window: f32; - var falloff: f32; + var falloff_1: f32; var spot: bool; var ramp: f32; - var local_10: f32; - var attenuation: f32; - var local_11: f32; var local_12: f32; + var attenuation: f32; + var local_13: f32; + var local_14: f32; - let _e80 = (*index); - if (_e80 < 8i) { - let _e82 = (*index); - let _e85 = contributor_light_info.light_position[_e82]; - position = _e85; - let _e86 = (*index); - let _e89 = contributor_light_info.light_color[_e86]; - color = _e89; - let _e90 = (*index); - let _e93 = contributor_light_info.light_direction[_e90]; - direction = _e93; - let _e94 = (*index); - let _e97 = contributor_light_info.light_cone[_e94]; - cone = _e97; - } else { - let _e98 = (*index); - slot_4 = (_e98 - 8i); - let _e100 = Clustered_u0028_(); - clustered = _e100; - let _e101 = clustered; - if _e101 { - let _e102 = slot_4; - param_8 = _e102; - let _e103 = ClusterRow_u0028_i1_u003b((¶m_8)); - local_6 = _e103; + let _e81 = (*index); + if (_e81 < 8i) { + let _e83 = (*index); + let _e86 = contributor_light_info.light_position[_e83]; + position = _e86; + let _e87 = (*index); + let _e90 = contributor_light_info.light_color[_e87]; + color = _e90; + let _e91 = (*index); + let _e94 = contributor_light_info.light_direction[_e91]; + direction = _e94; + let _e95 = (*index); + let _e98 = contributor_light_info.light_cone[_e95]; + cone = _e98; + } else { + let _e99 = (*index); + slot_4 = (_e99 - 8i); + let _e101 = Clustered_u0028_(); + clustered = _e101; + let _e102 = clustered; + if _e102 { + let _e103 = slot_4; + param_8 = _e103; + let _e104 = ClusterRow_u0028_i1_u003b((¶m_8)); + local_8 = _e104; } else { - let _e104 = slot_4; - param_9 = _e104; - let _e105 = LightListRow_u0028_i1_u003b((¶m_9)); - local_6 = _e105; - } - let _e106 = local_6; - listRow = _e106; - let _e107 = listRow; - let _e111 = light_list_info.list[2u]; - v = ((_e107 + 0.5f) * _e111); - let _e115 = light_list_info.list[1u]; - u = _e115; - let _e116 = u; - let _e118 = v; - let _e120 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((0.5f * _e116), _e118), 0f); - position = _e120; - let _e121 = u; - let _e123 = v; - let _e125 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((1.5f * _e121), _e123), 0f); - color = _e125; - let _e126 = u; - let _e128 = v; - let _e130 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((2.5f * _e126), _e128), 0f); - direction = _e130; - let _e131 = u; - let _e133 = v; - let _e135 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((3.5f * _e131), _e133), 0f); - cone = _e135; - let _e136 = clustered; - if _e136 { - let _e137 = listRow; - param_10 = _e137; - let _e138 = InSlots_u0028_f1_u003b((¶m_10)); - local_7 = select(1f, 0f, _e138); + let _e105 = slot_4; + param_9 = _e105; + let _e106 = LightListRow_u0028_i1_u003b((¶m_9)); + local_8 = _e106; + } + let _e107 = local_8; + listRow = _e107; + let _e108 = listRow; + let _e112 = light_list_info.list[2u]; + v = ((_e108 + 0.5f) * _e112); + let _e116 = light_list_info.list[1u]; + u = _e116; + let _e117 = u; + let _e119 = v; + let _e121 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((0.5f * _e117), _e119), 0f); + position = _e121; + let _e122 = u; + let _e124 = v; + let _e126 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((1.5f * _e122), _e124), 0f); + color = _e126; + let _e127 = u; + let _e129 = v; + let _e131 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((2.5f * _e127), _e129), 0f); + direction = _e131; + let _e132 = u; + let _e134 = v; + let _e136 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((3.5f * _e132), _e134), 0f); + cone = _e136; + let _e137 = clustered; + if _e137 { + let _e138 = listRow; + param_10 = _e138; + let _e139 = InSlots_u0028_f1_u003b((¶m_10)); + local_9 = select(1f, 0f, _e139); } else { - let _e140 = slot_4; - param_11 = _e140; - let _e141 = LightListScale_u0028_i1_u003b((¶m_11)); - local_7 = _e141; - } - let _e142 = local_7; - let _e144 = color[3u]; - color[3u] = (_e144 * _e142); - } - let _e148 = position[3u]; - type_30 = _e148; - let _e149 = type_30; - directional = (_e149 < 0.5f); - let _e151 = position; - let _e153 = (*world); - offset = (_e151.xyz - _e153); - let _e155 = offset; - distance_ = length(_e155); - let _e157 = direction; - aim = normalize(_e157.xyz); - let _e160 = directional; - let _e162 = distance_; - degenerate = (!(_e160) && (_e162 < 0.000001f)); - let _e165 = directional; - if _e165 { - let _e166 = aim; - local_8 = -(_e166); - } else { - let _e168 = offset; - let _e169 = distance_; - local_8 = (_e168 / vec3(max(_e169, 0.000001f))); - } - let _e173 = local_8; - (*toLight) = _e173; - let _e175 = direction[3u]; - if (_e175 > 0f) { - let _e177 = distance_; - let _e179 = direction[3u]; - local_9 = (_e177 / _e179); - } else { - local_9 = 0f; - } - let _e181 = local_9; - ratio = _e181; - let _e182 = ratio; + let _e141 = slot_4; + param_11 = _e141; + let _e142 = LightListScale_u0028_i1_u003b((¶m_11)); + local_9 = _e142; + } + let _e143 = local_9; + let _e145 = color[3u]; + color[3u] = (_e145 * _e143); + } + let _e149 = position[3u]; + type_30 = _e149; + let _e150 = type_30; + directional = (_e150 < 0.5f); + let _e152 = position; + let _e154 = (*world); + offset = (_e152.xyz - _e154); + let _e156 = offset; + distance_ = length(_e156); + let _e158 = direction; + aim = normalize(_e158.xyz); + let _e161 = directional; + let _e163 = distance_; + degenerate = (!(_e161) && (_e163 < 0.000001f)); + let _e166 = directional; + if _e166 { + let _e167 = aim; + local_10 = -(_e167); + } else { + let _e169 = offset; + let _e170 = distance_; + local_10 = (_e169 / vec3(max(_e170, 0.000001f))); + } + let _e174 = local_10; + (*toLight) = _e174; + let _e176 = direction[3u]; + if (_e176 > 0f) { + let _e178 = distance_; + let _e180 = direction[3u]; + local_11 = (_e178 / _e180); + } else { + local_11 = 0f; + } + let _e182 = local_11; + ratio = _e182; let _e183 = ratio; - let _e185 = ratio; - let _e187 = ratio; - window = clamp((1f - (((_e182 * _e183) * _e185) * _e187)), 0f, 1f); - let _e191 = window; + let _e184 = ratio; + let _e186 = ratio; + let _e188 = ratio; + window = clamp((1f - (((_e183 * _e184) * _e186) * _e188)), 0f, 1f); let _e192 = window; - let _e194 = distance_; + let _e193 = window; let _e195 = distance_; - falloff = ((_e191 * _e192) / max((_e194 * _e195), 0.0001f)); - let _e199 = type_30; - let _e201 = type_30; - spot = ((_e199 > 1.5f) && (_e201 < 2.5f)); - let _e204 = spot; - if _e204 { - let _e205 = aim; - let _e206 = (*toLight); - let _e210 = cone[1u]; - let _e213 = cone[0u]; - let _e215 = cone[1u]; - local_10 = clamp(((dot(_e205, -(_e206)) - _e210) / max((_e213 - _e215), 0.0001f)), 0f, 1f); + let _e196 = distance_; + falloff_1 = ((_e192 * _e193) / max((_e195 * _e196), 0.0001f)); + let _e200 = type_30; + let _e202 = type_30; + spot = ((_e200 > 1.5f) && (_e202 < 2.5f)); + let _e205 = spot; + if _e205 { + let _e206 = aim; + let _e207 = (*toLight); + let _e211 = cone[1u]; + let _e214 = cone[0u]; + let _e216 = cone[1u]; + local_12 = clamp(((dot(_e206, -(_e207)) - _e211) / max((_e214 - _e216), 0.0001f)), 0f, 1f); } else { - local_10 = 1f; + local_12 = 1f; } - let _e220 = local_10; - ramp = _e220; - let _e221 = directional; - if _e221 { - local_11 = 1f; + let _e221 = local_12; + ramp = _e221; + let _e222 = directional; + if _e222 { + local_13 = 1f; } else { - let _e222 = degenerate; - if _e222 { - local_12 = 0f; + let _e223 = degenerate; + if _e223 { + local_14 = 0f; } else { - let _e223 = falloff; - let _e224 = ramp; - local_12 = (_e223 * _e224); - } - let _e226 = local_12; - local_11 = _e226; - } - let _e227 = local_11; - attenuation = _e227; - let _e228 = color; - let _e231 = color[3u]; - let _e233 = attenuation; - (*radiance) = ((_e228.xyz * _e231) * _e233); + let _e224 = falloff_1; + let _e225 = ramp; + local_14 = (_e224 * _e225); + } + let _e227 = local_14; + local_13 = _e227; + } + let _e228 = local_13; + attenuation = _e228; + let _e229 = color; + let _e232 = color[3u]; + let _e234 = attenuation; + (*radiance) = ((_e229.xyz * _e232) * _e234); return; } @@ -31862,65 +32059,65 @@ fn FindCluster_u0028_vf3_u003b(world_1: ptr>) { var tx: f32; var ty: f32; var tz: f32; - var local_13: f32; + var local_15: f32; var cell: f32; var row_3: f32; var header: vec4; var param_12: f32; var param_13: f32; - let _e59 = light_list_info.cluster_view_projection; - let _e60 = (*world_1); - clip = (_e59 * vec4(_e60.x, _e60.y, _e60.z, 1f)); - let _e66 = clip; - let _e69 = clip[3u]; - ndc = (_e66.xy / vec2(max(_e69, 0.000001f))); - let _e74 = light_list_info.cluster_grid; - grid = _e74.xyz; - let _e78 = light_list_info.cluster_depth[0u]; - near = _e78; - let _e80 = ndc[0u]; - let _e84 = grid[0u]; - let _e88 = grid[0u]; - tx = clamp(floor((((_e80 * 0.5f) + 0.5f) * _e84)), 0f, (_e88 - 1f)); - let _e92 = ndc[1u]; - let _e96 = grid[1u]; - let _e100 = grid[1u]; - ty = clamp(floor((((_e92 * 0.5f) + 0.5f) * _e96)), 0f, (_e100 - 1f)); - let _e104 = clip[3u]; - let _e105 = near; - if (_e104 <= _e105) { - local_13 = 0f; + let _e60 = light_list_info.cluster_view_projection; + let _e61 = (*world_1); + clip = (_e60 * vec4(_e61.x, _e61.y, _e61.z, 1f)); + let _e67 = clip; + let _e70 = clip[3u]; + ndc = (_e67.xy / vec2(max(_e70, 0.000001f))); + let _e75 = light_list_info.cluster_grid; + grid = _e75.xyz; + let _e79 = light_list_info.cluster_depth[0u]; + near = _e79; + let _e81 = ndc[0u]; + let _e85 = grid[0u]; + let _e89 = grid[0u]; + tx = clamp(floor((((_e81 * 0.5f) + 0.5f) * _e85)), 0f, (_e89 - 1f)); + let _e93 = ndc[1u]; + let _e97 = grid[1u]; + let _e101 = grid[1u]; + ty = clamp(floor((((_e93 * 0.5f) + 0.5f) * _e97)), 0f, (_e101 - 1f)); + let _e105 = clip[3u]; + let _e106 = near; + if (_e105 <= _e106) { + local_15 = 0f; } else { - let _e108 = clip[3u]; - let _e109 = near; - let _e114 = light_list_info.cluster_depth[1u]; - let _e118 = grid[2u]; - local_13 = clamp(floor((log((_e108 / _e109)) * _e114)), 0f, (_e118 - 1f)); - } - let _e121 = local_13; - tz = _e121; - let _e122 = tx; - let _e123 = ty; - let _e125 = grid[0u]; - let _e128 = tz; - let _e130 = grid[0u]; - let _e133 = grid[1u]; - cell = ((_e122 + (_e123 * _e125)) + ((_e128 * _e130) * _e133)); - let _e136 = cell; - row_3 = floor((_e136 / 4f)); - let _e139 = cell; - let _e140 = row_3; - let _e145 = light_list_info.cluster_depth[2u]; - let _e146 = row_3; - param_12 = (_e139 - (_e140 * 4f)); - param_13 = (_e145 + _e146); - let _e148 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_12), (¶m_13)); - header = _e148; - let _e150 = header[0u]; - g_cluster_offset = _e150; - let _e152 = header[1u]; - g_cluster_count = _e152; + let _e109 = clip[3u]; + let _e110 = near; + let _e115 = light_list_info.cluster_depth[1u]; + let _e119 = grid[2u]; + local_15 = clamp(floor((log((_e109 / _e110)) * _e115)), 0f, (_e119 - 1f)); + } + let _e122 = local_15; + tz = _e122; + let _e123 = tx; + let _e124 = ty; + let _e126 = grid[0u]; + let _e129 = tz; + let _e131 = grid[0u]; + let _e134 = grid[1u]; + cell = ((_e123 + (_e124 * _e126)) + ((_e129 * _e131) * _e134)); + let _e137 = cell; + row_3 = floor((_e137 / 4f)); + let _e140 = cell; + let _e141 = row_3; + let _e146 = light_list_info.cluster_depth[2u]; + let _e147 = row_3; + param_12 = (_e140 - (_e141 * 4f)); + param_13 = (_e146 + _e147); + let _e149 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_12), (¶m_13)); + header = _e149; + let _e151 = header[0u]; + g_cluster_offset = _e151; + let _e153 = header[1u]; + g_cluster_count = _e153; return; } @@ -31928,19 +32125,19 @@ fn ContributorLightCount_u0028_vf3_u003b(world_2: ptr>) -> i var tail: f32; var param_14: vec3; - let _e49 = light_list_info.list[0u]; - tail = _e49; - let _e50 = Clustered_u0028_(); - if _e50 { - let _e51 = (*world_2); - param_14 = _e51; + let _e50 = light_list_info.list[0u]; + tail = _e50; + let _e51 = Clustered_u0028_(); + if _e51 { + let _e52 = (*world_2); + param_14 = _e52; FindCluster_u0028_vf3_u003b((¶m_14)); - let _e52 = g_cluster_count; - tail = _e52; + let _e53 = g_cluster_count; + tail = _e53; } - let _e55 = contributor_light_info.slots[0u]; - let _e59 = tail; - return (clamp(i32((_e55 + 0.5f)), 0i, 8i) + clamp(i32((_e59 + 0.5f)), 0i, 24i)); + let _e56 = contributor_light_info.slots[0u]; + let _e60 = tail; + return (clamp(i32((_e56 + 0.5f)), 0i, 8i) + clamp(i32((_e60 + 0.5f)), 0i, 24i)); } fn main_1() { @@ -31966,95 +32163,89 @@ fn main_1() { g_cluster_offset = 0f; g_cluster_count = 0f; - let _e63 = v_uv_1; - let _e64 = textureSample(six_way_positive_tex, six_way_positive_smp, _e63); - positive_1 = _e64; - let _e65 = v_uv_1; - let _e66 = textureSample(six_way_negative_tex, six_way_negative_smp, _e65); - negative_1 = _e66; + let _e64 = v_uv_1; + let _e65 = textureSample(six_way_positive_tex, six_way_positive_smp, _e64); + positive_1 = _e65; + let _e66 = v_uv_1; + let _e67 = textureSample(six_way_negative_tex, six_way_negative_smp, _e66); + negative_1 = _e67; lit = vec3(0f, 0f, 0f); - let _e67 = v_world_position_1; - param_15 = _e67; - let _e68 = ContributorLightCount_u0028_vf3_u003b((¶m_15)); - count = _e68; + let _e68 = v_world_position_1; + param_15 = _e68; + let _e69 = ContributorLightCount_u0028_vf3_u003b((¶m_15)); + count = _e69; i = 0i; loop { - let _e69 = i; - if (_e69 < 32i) { - let _e71 = i; - let _e72 = count; - if (_e71 >= _e72) { + let _e70 = i; + if (_e70 < 32i) { + let _e72 = i; + let _e73 = count; + if (_e72 >= _e73) { break; } - let _e74 = i; - param_16 = _e74; - let _e75 = v_world_position_1; - param_17 = _e75; + let _e75 = i; + param_16 = _e75; + let _e76 = v_world_position_1; + param_17 = _e76; ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b((¶m_16), (¶m_17), (¶m_18), (¶m_19)); - let _e76 = param_18; - l_1 = _e76; - let _e77 = param_19; - radiance_1 = _e77; - let _e78 = radiance_1; - let _e79 = l_1; - param_20 = _e79; - let _e80 = positive_1; - param_21 = _e80.xyz; - let _e82 = negative_1; - param_22 = _e82.xyz; - let _e84 = SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b((¶m_20), (¶m_21), (¶m_22)); - let _e86 = lit; - lit = (_e86 + (_e78 * _e84)); + let _e77 = param_18; + l_1 = _e77; + let _e78 = param_19; + radiance_1 = _e78; + let _e79 = radiance_1; + let _e80 = l_1; + param_20 = _e80; + let _e81 = positive_1; + param_21 = _e81.xyz; + let _e83 = negative_1; + param_22 = _e83.xyz; + let _e85 = SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b((¶m_20), (¶m_21), (¶m_22)); + let _e87 = lit; + lit = (_e87 + (_e79 * _e85)); continue; } else { break; } continuing { - let _e88 = i; - i = (_e88 + 1i); - } - } - let _e91 = positive_1[0u]; - let _e93 = positive_1[1u]; - let _e96 = positive_1[2u]; - let _e99 = negative_1[0u]; - let _e102 = negative_1[1u]; - let _e105 = negative_1[2u]; - mean = ((((((_e91 + _e93) + _e96) + _e99) + _e102) + _e105) / 6f); - let _e108 = v_color_1; - let _e110 = lit; - let _e112 = six_way_info.ambient; - let _e114 = mean; - let _e119 = six_way_info.emission; - let _e122 = negative_1[3u]; - color_1 = ((_e108.xyz * (_e110 + (_e112.xyz * _e114))) + (_e119.xyz * _e122)); - fogged = 1f; - let _e127 = fog_info.fog[3u]; - if (_e127 > 0f) { - let _e131 = fog_info.fog[3u]; - let _e133 = v_world_position_1; - let _e135 = fog_info.eye; - fogged = clamp(exp((-(_e131) * distance(_e133, _e135.xyz))), 0f, 1f); - } - let _e142 = fog_info.fog; - let _e144 = color_1; - let _e145 = fogged; - color_1 = mix(_e142.xyz, _e144, vec3(_e145)); - let _e149 = v_color_1[3u]; - let _e151 = positive_1[3u]; - coverage = clamp((_e149 * _e151), 0f, 1f); - let _e154 = color_1; - let _e155 = coverage; - let _e156 = (_e154 * _e155); - let _e157 = coverage; - frag_color = vec4(_e156.x, _e156.y, _e156.z, _e157); + let _e89 = i; + i = (_e89 + 1i); + } + } + let _e92 = positive_1[0u]; + let _e94 = positive_1[1u]; + let _e97 = positive_1[2u]; + let _e100 = negative_1[0u]; + let _e103 = negative_1[1u]; + let _e106 = negative_1[2u]; + mean = ((((((_e92 + _e94) + _e97) + _e100) + _e103) + _e106) / 6f); + let _e109 = v_color_1; + let _e111 = lit; + let _e113 = six_way_info.ambient; + let _e115 = mean; + let _e120 = six_way_info.emission; + let _e123 = negative_1[3u]; + color_1 = ((_e109.xyz * (_e111 + (_e113.xyz * _e115))) + (_e120.xyz * _e123)); + let _e126 = ParticleFogTransmittance_u0028_(); + fogged = _e126; + let _e128 = fog_info.fog; + let _e130 = color_1; + let _e131 = fogged; + color_1 = mix(_e128.xyz, _e130, vec3(_e131)); + let _e135 = v_color_1[3u]; + let _e137 = positive_1[3u]; + coverage = clamp((_e135 * _e137), 0f, 1f); + let _e140 = color_1; + let _e141 = coverage; + let _e142 = (_e140 * _e141); + let _e143 = coverage; + frag_color = vec4(_e142.x, _e142.y, _e142.z, _e143); return; } @fragment -fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { - v_uv_1 = v_uv; +fn main(@location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e7 = frag_color; @@ -32100,10 +32291,20 @@ fn main(@location(5) v_uv: vec2, @location(6) v_world_position: vec3, name: 'FogInfo', group: 1, binding: 1, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), WebGpuBlock( @@ -32206,6 +32407,8 @@ struct SoftParticleInfo { struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } var gl_FragCoord_1: vec4; @@ -32215,28 +32418,28 @@ var soft_particle_info: SoftParticleInfo; var scene_depth_texture_tex: texture_2d; @group(1) @binding(3) var scene_depth_texture_smp: sampler; -var v_uv_1: vec2; @group(1) @binding(0) var fog_info: FogInfo; var v_world_position_1: vec3; +var v_uv_1: vec2; var frag_color: vec4; var v_color_1: vec4; fn FragCoordFromTop_u0028_f1_u003b(rows: ptr) -> vec2 { var local: vec2; - let _e21 = (*rows); - if (_e21 > 0f) { - let _e24 = gl_FragCoord_1[0u]; - let _e25 = (*rows); - let _e27 = gl_FragCoord_1[1u]; - local = vec2(_e24, (_e25 - _e27)); + let _e24 = (*rows); + if (_e24 > 0f) { + let _e27 = gl_FragCoord_1[0u]; + let _e28 = (*rows); + let _e30 = gl_FragCoord_1[1u]; + local = vec2(_e27, (_e28 - _e30)); } else { - let _e30 = gl_FragCoord_1; - local = _e30.xy; + let _e33 = gl_FragCoord_1; + local = _e33.xy; } - let _e32 = local; - return _e32; + let _e35 = local; + return _e35; } fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { @@ -32246,71 +32449,126 @@ fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { var depth: f32; var fade: f32; - let _e27 = soft_particle_info.target_[2u]; - param = _e27; - let _e28 = FragCoordFromTop_u0028_f1_u003b((¶m)); - let _e30 = soft_particle_info.target_; - uv = (_e28 * _e30.xy); - let _e33 = uv; - let _e34 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e33, 0f); - stored = _e34.w; - let _e36 = (*world); - let _e38 = soft_particle_info.eye; - let _e42 = soft_particle_info.forward; - depth = dot((_e36 - _e38.xyz), _e42.xyz); - let _e45 = stored; - let _e46 = depth; - let _e50 = soft_particle_info.target_[3u]; - fade = clamp(((_e45 - _e46) * _e50), 0f, 1f); - let _e53 = stored; - let _e55 = fade; - return select(1f, _e55, (_e53 > 0f)); + let _e30 = soft_particle_info.target_[2u]; + param = _e30; + let _e31 = FragCoordFromTop_u0028_f1_u003b((¶m)); + let _e33 = soft_particle_info.target_; + uv = (_e31 * _e33.xy); + let _e36 = uv; + let _e37 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e36, 0f); + stored = _e37.w; + let _e39 = (*world); + let _e41 = soft_particle_info.eye; + let _e45 = soft_particle_info.forward; + depth = dot((_e39 - _e41.xyz), _e45.xyz); + let _e48 = stored; + let _e49 = depth; + let _e53 = soft_particle_info.target_[3u]; + fade = clamp(((_e48 - _e49) * _e53), 0f, 1f); + let _e56 = stored; + let _e58 = fade; + return select(1f, _e58, (_e56 > 0f)); +} + +fn ParticleOrthographic_u0028_() -> bool { + let _e24 = fog_info.projection[0u]; + return (_e24 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local_1: f32; + + let _e23 = ParticleOrthographic_u0028_(); + if _e23 { + let _e24 = v_world_position_1; + let _e26 = fog_info.eye; + let _e30 = fog_info.forward; + local_1 = max(dot((_e24 - _e26.xyz), _e30.xyz), 0f); + } else { + let _e34 = v_world_position_1; + let _e36 = fog_info.eye; + local_1 = distance(_e34, _e36.xyz); + } + let _e39 = local_1; + return _e39; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_2: f32; + + let _e28 = fog_info.fog[3u]; + density = _e28; + let _e29 = density; + if (_e29 <= 0f) { + return 1f; + } + let _e33 = fog_info.eye[3u]; + falloff = _e33; + let _e34 = falloff; + if (_e34 > 0f) { + let _e36 = falloff; + let _e38 = v_world_position_1[1u]; + let _e41 = fog_info.eye[1u]; + k = (_e36 * (_e38 - _e41)); + let _e44 = k; + if (abs(_e44) > 0.001f) { + let _e47 = k; + let _e51 = k; + local_2 = ((1f - exp(-(_e47))) / _e51); + } else { + let _e53 = k; + local_2 = (1f - (0.5f * _e53)); + } + let _e56 = local_2; + let _e57 = density; + density = (_e57 * _e56); + } + let _e59 = density; + let _e61 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e59) * _e61)), 0f, 1f); } fn main_1() { var centred: vec2; var radius: f32; - var falloff: f32; + var falloff_1: f32; var intensity: f32; var fogged: f32; var param_1: vec3; - let _e25 = v_uv_1; - centred = ((_e25 * 2f) - vec2(1f)); - let _e29 = centred; - radius = length(_e29); - let _e31 = radius; - falloff = (1f - smoothstep(0f, 1f, _e31)); - let _e34 = falloff; - let _e35 = falloff; - intensity = (_e34 * _e35); - fogged = 1f; - let _e39 = fog_info.fog[3u]; - if (_e39 > 0f) { - let _e43 = fog_info.fog[3u]; - let _e45 = v_world_position_1; - let _e47 = fog_info.eye; - fogged = clamp(exp((-(_e43) * distance(_e45, _e47.xyz))), 0f, 1f); - } - let _e53 = v_world_position_1; - param_1 = _e53; - let _e54 = SoftParticleFade_u0028_vf3_u003b((¶m_1)); - let _e55 = intensity; - intensity = (_e55 * _e54); - let _e57 = v_color_1; - let _e60 = v_color_1[3u]; - let _e62 = intensity; - let _e64 = fogged; - let _e65 = (((_e57.xyz * _e60) * _e62) * _e64); - frag_color = vec4(_e65.x, _e65.y, _e65.z, 1f); + let _e28 = v_uv_1; + centred = ((_e28 * 2f) - vec2(1f)); + let _e32 = centred; + radius = length(_e32); + let _e34 = radius; + falloff_1 = (1f - smoothstep(0f, 1f, _e34)); + let _e37 = falloff_1; + let _e38 = falloff_1; + intensity = (_e37 * _e38); + let _e40 = ParticleFogTransmittance_u0028_(); + fogged = _e40; + let _e41 = v_world_position_1; + param_1 = _e41; + let _e42 = SoftParticleFade_u0028_vf3_u003b((¶m_1)); + let _e43 = intensity; + intensity = (_e43 * _e42); + let _e45 = v_color_1; + let _e48 = v_color_1[3u]; + let _e50 = intensity; + let _e52 = fogged; + let _e53 = (((_e45.xyz * _e48) * _e50) * _e52); + frag_color = vec4(_e53.x, _e53.y, _e53.z, 1f); return; } @fragment -fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { +fn main(@builtin(position) gl_FragCoord: vec4, @location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { gl_FragCoord_1 = gl_FragCoord; - v_uv_1 = v_uv; v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e9 = frag_color; @@ -32323,10 +32581,20 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), WebGpuBlock( @@ -32370,6 +32638,8 @@ struct SoftParticleInfo { struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } var gl_FragCoord_1: vec4; @@ -32379,32 +32649,32 @@ var soft_particle_info: SoftParticleInfo; var scene_depth_texture_tex: texture_2d; @group(1) @binding(5) var scene_depth_texture_smp: sampler; +@group(1) @binding(0) +var fog_info: FogInfo; +var v_world_position_1: vec3; @group(1) @binding(2) var particle_texture_tex: texture_2d; @group(1) @binding(3) var particle_texture_smp: sampler; var v_uv_1: vec2; -@group(1) @binding(0) -var fog_info: FogInfo; -var v_world_position_1: vec3; var v_color_1: vec4; var frag_color: vec4; fn FragCoordFromTop_u0028_f1_u003b(rows: ptr) -> vec2 { var local: vec2; - let _e22 = (*rows); - if (_e22 > 0f) { - let _e25 = gl_FragCoord_1[0u]; - let _e26 = (*rows); - let _e28 = gl_FragCoord_1[1u]; - local = vec2(_e25, (_e26 - _e28)); + let _e25 = (*rows); + if (_e25 > 0f) { + let _e28 = gl_FragCoord_1[0u]; + let _e29 = (*rows); + let _e31 = gl_FragCoord_1[1u]; + local = vec2(_e28, (_e29 - _e31)); } else { - let _e31 = gl_FragCoord_1; - local = _e31.xy; + let _e34 = gl_FragCoord_1; + local = _e34.xy; } - let _e33 = local; - return _e33; + let _e36 = local; + return _e36; } fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { @@ -32414,25 +32684,86 @@ fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { var depth: f32; var fade: f32; - let _e28 = soft_particle_info.target_[2u]; - param = _e28; - let _e29 = FragCoordFromTop_u0028_f1_u003b((¶m)); - let _e31 = soft_particle_info.target_; - uv = (_e29 * _e31.xy); - let _e34 = uv; - let _e35 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e34, 0f); - stored = _e35.w; - let _e37 = (*world); - let _e39 = soft_particle_info.eye; - let _e43 = soft_particle_info.forward; - depth = dot((_e37 - _e39.xyz), _e43.xyz); - let _e46 = stored; - let _e47 = depth; - let _e51 = soft_particle_info.target_[3u]; - fade = clamp(((_e46 - _e47) * _e51), 0f, 1f); - let _e54 = stored; - let _e56 = fade; - return select(1f, _e56, (_e54 > 0f)); + let _e31 = soft_particle_info.target_[2u]; + param = _e31; + let _e32 = FragCoordFromTop_u0028_f1_u003b((¶m)); + let _e34 = soft_particle_info.target_; + uv = (_e32 * _e34.xy); + let _e37 = uv; + let _e38 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e37, 0f); + stored = _e38.w; + let _e40 = (*world); + let _e42 = soft_particle_info.eye; + let _e46 = soft_particle_info.forward; + depth = dot((_e40 - _e42.xyz), _e46.xyz); + let _e49 = stored; + let _e50 = depth; + let _e54 = soft_particle_info.target_[3u]; + fade = clamp(((_e49 - _e50) * _e54), 0f, 1f); + let _e57 = stored; + let _e59 = fade; + return select(1f, _e59, (_e57 > 0f)); +} + +fn ParticleOrthographic_u0028_() -> bool { + let _e25 = fog_info.projection[0u]; + return (_e25 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local_1: f32; + + let _e24 = ParticleOrthographic_u0028_(); + if _e24 { + let _e25 = v_world_position_1; + let _e27 = fog_info.eye; + let _e31 = fog_info.forward; + local_1 = max(dot((_e25 - _e27.xyz), _e31.xyz), 0f); + } else { + let _e35 = v_world_position_1; + let _e37 = fog_info.eye; + local_1 = distance(_e35, _e37.xyz); + } + let _e40 = local_1; + return _e40; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_2: f32; + + let _e29 = fog_info.fog[3u]; + density = _e29; + let _e30 = density; + if (_e30 <= 0f) { + return 1f; + } + let _e34 = fog_info.eye[3u]; + falloff = _e34; + let _e35 = falloff; + if (_e35 > 0f) { + let _e37 = falloff; + let _e39 = v_world_position_1[1u]; + let _e42 = fog_info.eye[1u]; + k = (_e37 * (_e39 - _e42)); + let _e45 = k; + if (abs(_e45) > 0.001f) { + let _e48 = k; + let _e52 = k; + local_2 = ((1f - exp(-(_e48))) / _e52); + } else { + let _e54 = k; + local_2 = (1f - (0.5f * _e54)); + } + let _e57 = local_2; + let _e58 = density; + density = (_e58 * _e57); + } + let _e60 = density; + let _e62 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e60) * _e62)), 0f, 1f); } fn main_1() { @@ -32441,39 +32772,33 @@ fn main_1() { var scale: f32; var param_1: vec3; - let _e24 = v_uv_1; - let _e25 = textureSample(particle_texture_tex, particle_texture_smp, _e24); - texel = _e25; - fogged = 1f; - let _e28 = fog_info.fog[3u]; - if (_e28 > 0f) { - let _e32 = fog_info.fog[3u]; - let _e34 = v_world_position_1; - let _e36 = fog_info.eye; - fogged = clamp(exp((-(_e32) * distance(_e34, _e36.xyz))), 0f, 1f); - } - let _e43 = v_color_1[3u]; - let _e45 = texel[3u]; - let _e47 = fogged; - scale = ((_e43 * _e45) * _e47); - let _e49 = v_world_position_1; - param_1 = _e49; - let _e50 = SoftParticleFade_u0028_vf3_u003b((¶m_1)); - let _e51 = scale; - scale = (_e51 * _e50); - let _e53 = v_color_1; - let _e55 = texel; - let _e58 = scale; - let _e59 = ((_e53.xyz * _e55.xyz) * _e58); - frag_color = vec4(_e59.x, _e59.y, _e59.z, 1f); + let _e27 = v_uv_1; + let _e28 = textureSample(particle_texture_tex, particle_texture_smp, _e27); + texel = _e28; + let _e29 = ParticleFogTransmittance_u0028_(); + fogged = _e29; + let _e31 = v_color_1[3u]; + let _e33 = texel[3u]; + let _e35 = fogged; + scale = ((_e31 * _e33) * _e35); + let _e37 = v_world_position_1; + param_1 = _e37; + let _e38 = SoftParticleFade_u0028_vf3_u003b((¶m_1)); + let _e39 = scale; + scale = (_e39 * _e38); + let _e41 = v_color_1; + let _e43 = texel; + let _e46 = scale; + let _e47 = ((_e41.xyz * _e43.xyz) * _e46); + frag_color = vec4(_e47.x, _e47.y, _e47.z, 1f); return; } @fragment -fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { +fn main(@builtin(position) gl_FragCoord: vec4, @location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { gl_FragCoord_1 = gl_FragCoord; - v_uv_1 = v_uv; v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e9 = frag_color; @@ -32486,10 +32811,20 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), WebGpuBlock( @@ -32555,6 +32890,13 @@ struct SoftParticleInfo { forward: vec4, } +struct FogInfo { + fog: vec4, + eye: vec4, + forward: vec4, + projection: vec4, +} + struct SixWayInfo { right: vec4, up: vec4, @@ -32563,11 +32905,6 @@ struct SixWayInfo { ambient: vec4, } -struct FogInfo { - fog: vec4, - eye: vec4, -} - var g_cluster_offset: f32; var g_cluster_count: f32; @group(1) @binding(2) @@ -32585,6 +32922,9 @@ var soft_particle_info: SoftParticleInfo; var scene_depth_texture_tex: texture_2d; @group(1) @binding(8) var scene_depth_texture_smp: sampler; +@group(1) @binding(1) +var fog_info: FogInfo; +var v_world_position_1: vec3; @group(1) @binding(3) var six_way_info: SixWayInfo; @group(1) @binding(11) @@ -32596,27 +32936,24 @@ var v_uv_1: vec2; var six_way_negative_tex: texture_2d; @group(1) @binding(10) var six_way_negative_smp: sampler; -var v_world_position_1: vec3; var v_color_1: vec4; -@group(1) @binding(1) -var fog_info: FogInfo; var frag_color: vec4; fn FragCoordFromTop_u0028_f1_u003b(rows: ptr) -> vec2 { var local: vec2; - let _e50 = (*rows); - if (_e50 > 0f) { - let _e53 = gl_FragCoord_1[0u]; - let _e54 = (*rows); - let _e56 = gl_FragCoord_1[1u]; - local = vec2(_e53, (_e54 - _e56)); + let _e51 = (*rows); + if (_e51 > 0f) { + let _e54 = gl_FragCoord_1[0u]; + let _e55 = (*rows); + let _e57 = gl_FragCoord_1[1u]; + local = vec2(_e54, (_e55 - _e57)); } else { - let _e59 = gl_FragCoord_1; - local = _e59.xy; + let _e60 = gl_FragCoord_1; + local = _e60.xy; } - let _e61 = local; - return _e61; + let _e62 = local; + return _e62; } fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { @@ -32626,171 +32963,232 @@ fn SoftParticleFade_u0028_vf3_u003b(world: ptr>) -> f32 { var depth: f32; var fade: f32; - let _e56 = soft_particle_info.target_[2u]; - param = _e56; - let _e57 = FragCoordFromTop_u0028_f1_u003b((¶m)); - let _e59 = soft_particle_info.target_; - uv = (_e57 * _e59.xy); - let _e62 = uv; - let _e63 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e62, 0f); - stored = _e63.w; - let _e65 = (*world); - let _e67 = soft_particle_info.eye; - let _e71 = soft_particle_info.forward; - depth = dot((_e65 - _e67.xyz), _e71.xyz); - let _e74 = stored; - let _e75 = depth; - let _e79 = soft_particle_info.target_[3u]; - fade = clamp(((_e74 - _e75) * _e79), 0f, 1f); - let _e82 = stored; - let _e84 = fade; - return select(1f, _e84, (_e82 > 0f)); + let _e57 = soft_particle_info.target_[2u]; + param = _e57; + let _e58 = FragCoordFromTop_u0028_f1_u003b((¶m)); + let _e60 = soft_particle_info.target_; + uv = (_e58 * _e60.xy); + let _e63 = uv; + let _e64 = textureSampleLevel(scene_depth_texture_tex, scene_depth_texture_smp, _e63, 0f); + stored = _e64.w; + let _e66 = (*world); + let _e68 = soft_particle_info.eye; + let _e72 = soft_particle_info.forward; + depth = dot((_e66 - _e68.xyz), _e72.xyz); + let _e75 = stored; + let _e76 = depth; + let _e80 = soft_particle_info.target_[3u]; + fade = clamp(((_e75 - _e76) * _e80), 0f, 1f); + let _e83 = stored; + let _e85 = fade; + return select(1f, _e85, (_e83 > 0f)); +} + +fn ParticleOrthographic_u0028_() -> bool { + let _e51 = fog_info.projection[0u]; + return (_e51 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local_1: f32; + + let _e50 = ParticleOrthographic_u0028_(); + if _e50 { + let _e51 = v_world_position_1; + let _e53 = fog_info.eye; + let _e57 = fog_info.forward; + local_1 = max(dot((_e51 - _e53.xyz), _e57.xyz), 0f); + } else { + let _e61 = v_world_position_1; + let _e63 = fog_info.eye; + local_1 = distance(_e61, _e63.xyz); + } + let _e66 = local_1; + return _e66; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_2: f32; + + let _e55 = fog_info.fog[3u]; + density = _e55; + let _e56 = density; + if (_e56 <= 0f) { + return 1f; + } + let _e60 = fog_info.eye[3u]; + falloff = _e60; + let _e61 = falloff; + if (_e61 > 0f) { + let _e63 = falloff; + let _e65 = v_world_position_1[1u]; + let _e68 = fog_info.eye[1u]; + k = (_e63 * (_e65 - _e68)); + let _e71 = k; + if (abs(_e71) > 0.001f) { + let _e74 = k; + let _e78 = k; + local_2 = ((1f - exp(-(_e74))) / _e78); + } else { + let _e80 = k; + local_2 = (1f - (0.5f * _e80)); + } + let _e83 = local_2; + let _e84 = density; + density = (_e84 * _e83); + } + let _e86 = density; + let _e88 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e86) * _e88)), 0f, 1f); } fn SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b(l: ptr>, positive: ptr>, negative: ptr>) -> f32 { var x: f32; var y: f32; var z: f32; - var local_1: f32; - var local_2: f32; var local_3: f32; + var local_4: f32; + var local_5: f32; - let _e57 = (*l); - let _e59 = six_way_info.right; - x = dot(_e57, _e59.xyz); - let _e62 = (*l); - let _e64 = six_way_info.up; - y = dot(_e62, _e64.xyz); - let _e67 = (*l); - let _e69 = six_way_info.forward; - z = dot(_e67, _e69.xyz); - let _e72 = x; + let _e58 = (*l); + let _e60 = six_way_info.right; + x = dot(_e58, _e60.xyz); + let _e63 = (*l); + let _e65 = six_way_info.up; + y = dot(_e63, _e65.xyz); + let _e68 = (*l); + let _e70 = six_way_info.forward; + z = dot(_e68, _e70.xyz); let _e73 = x; - let _e75 = x; - if (_e75 > 0f) { - let _e78 = (*positive)[0u]; - local_1 = _e78; + let _e74 = x; + let _e76 = x; + if (_e76 > 0f) { + let _e79 = (*positive)[0u]; + local_3 = _e79; } else { - let _e80 = (*negative)[0u]; - local_1 = _e80; + let _e81 = (*negative)[0u]; + local_3 = _e81; } - let _e81 = local_1; - let _e83 = y; + let _e82 = local_3; let _e84 = y; - let _e86 = y; - if (_e86 > 0f) { - let _e89 = (*positive)[1u]; - local_2 = _e89; + let _e85 = y; + let _e87 = y; + if (_e87 > 0f) { + let _e90 = (*positive)[1u]; + local_4 = _e90; } else { - let _e91 = (*negative)[1u]; - local_2 = _e91; + let _e92 = (*negative)[1u]; + local_4 = _e92; } - let _e92 = local_2; - let _e95 = z; + let _e93 = local_4; let _e96 = z; - let _e98 = z; - if (_e98 > 0f) { - let _e101 = (*positive)[2u]; - local_3 = _e101; + let _e97 = z; + let _e99 = z; + if (_e99 > 0f) { + let _e102 = (*positive)[2u]; + local_5 = _e102; } else { - let _e103 = (*negative)[2u]; - local_3 = _e103; + let _e104 = (*negative)[2u]; + local_5 = _e104; } - let _e104 = local_3; - return ((((_e72 * _e73) * _e81) + ((_e83 * _e84) * _e92)) + ((_e95 * _e96) * _e104)); + let _e105 = local_5; + return ((((_e73 * _e74) * _e82) + ((_e84 * _e85) * _e93)) + ((_e96 * _e97) * _e105)); } fn LightListLane_u0028_vf4_u003b_i1_u003b(four: ptr>, slot: ptr) -> f32 { var lane: i32; - var local_4: f32; - var local_5: f32; var local_6: f32; + var local_7: f32; + var local_8: f32; - let _e54 = (*slot); let _e55 = (*slot); - lane = (_e54 - ((_e55 / 4i) * 4i)); - let _e59 = lane; - if (_e59 == 0i) { - let _e62 = (*four)[0u]; - local_4 = _e62; - } else { - let _e63 = lane; - if (_e63 == 1i) { - let _e66 = (*four)[1u]; - local_5 = _e66; + let _e56 = (*slot); + lane = (_e55 - ((_e56 / 4i) * 4i)); + let _e60 = lane; + if (_e60 == 0i) { + let _e63 = (*four)[0u]; + local_6 = _e63; + } else { + let _e64 = lane; + if (_e64 == 1i) { + let _e67 = (*four)[1u]; + local_7 = _e67; } else { - let _e67 = lane; - if (_e67 == 2i) { - let _e70 = (*four)[2u]; - local_6 = _e70; + let _e68 = lane; + if (_e68 == 2i) { + let _e71 = (*four)[2u]; + local_8 = _e71; } else { - let _e72 = (*four)[3u]; - local_6 = _e72; + let _e73 = (*four)[3u]; + local_8 = _e73; } - let _e73 = local_6; - local_5 = _e73; + let _e74 = local_8; + local_7 = _e74; } - let _e74 = local_5; - local_4 = _e74; + let _e75 = local_7; + local_6 = _e75; } - let _e75 = local_4; - return _e75; + let _e76 = local_6; + return _e76; } fn LightListScale_u0028_i1_u003b(slot_1: ptr) -> f32 { var param_1: vec4; var param_2: i32; - let _e51 = (*slot_1); - let _e55 = light_list_info.scales[(_e51 / 4i)]; - param_1 = _e55; - let _e56 = (*slot_1); - param_2 = _e56; - let _e57 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_1), (¶m_2)); - return _e57; + let _e52 = (*slot_1); + let _e56 = light_list_info.scales[(_e52 / 4i)]; + param_1 = _e56; + let _e57 = (*slot_1); + param_2 = _e57; + let _e58 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_1), (¶m_2)); + return _e58; } fn InSlots_u0028_f1_u003b(row: ptr) -> bool { var a: vec4; var b: vec4; - let _e53 = light_list_info.slot_rows[0i]; - let _e54 = (*row); - a = abs((_e53 - vec4(_e54))); - let _e60 = light_list_info.slot_rows[1i]; - let _e61 = (*row); - b = abs((_e60 - vec4(_e61))); - let _e66 = a[0u]; - let _e68 = a[1u]; - let _e71 = a[2u]; - let _e73 = a[3u]; - let _e77 = b[0u]; - let _e79 = b[1u]; - let _e82 = b[2u]; - let _e84 = b[3u]; - return (min(min(min(_e66, _e68), min(_e71, _e73)), min(min(_e77, _e79), min(_e82, _e84))) < 0.5f); + let _e54 = light_list_info.slot_rows[0i]; + let _e55 = (*row); + a = abs((_e54 - vec4(_e55))); + let _e61 = light_list_info.slot_rows[1i]; + let _e62 = (*row); + b = abs((_e61 - vec4(_e62))); + let _e67 = a[0u]; + let _e69 = a[1u]; + let _e72 = a[2u]; + let _e74 = a[3u]; + let _e78 = b[0u]; + let _e80 = b[1u]; + let _e83 = b[2u]; + let _e85 = b[3u]; + return (min(min(min(_e67, _e69), min(_e72, _e74)), min(min(_e78, _e80), min(_e83, _e85))) < 0.5f); } fn LightListRow_u0028_i1_u003b(slot_2: ptr) -> f32 { var param_3: vec4; var param_4: i32; - let _e51 = (*slot_2); - let _e55 = light_list_info.indices[(_e51 / 4i)]; - param_3 = _e55; - let _e56 = (*slot_2); - param_4 = _e56; - let _e57 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_3), (¶m_4)); - return _e57; + let _e52 = (*slot_2); + let _e56 = light_list_info.indices[(_e52 / 4i)]; + param_3 = _e56; + let _e57 = (*slot_2); + param_4 = _e57; + let _e58 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_3), (¶m_4)); + return _e58; } fn LightListTexel_u0028_f1_u003b_f1_u003b(texel: ptr, row_1: ptr) -> vec4 { - let _e50 = (*texel); - let _e54 = light_list_info.list[1u]; - let _e56 = (*row_1); - let _e60 = light_list_info.list[2u]; - let _e63 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2(((_e50 + 0.5f) * _e54), ((_e56 + 0.5f) * _e60)), 0f); - return _e63; + let _e51 = (*texel); + let _e55 = light_list_info.list[1u]; + let _e57 = (*row_1); + let _e61 = light_list_info.list[2u]; + let _e64 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2(((_e51 + 0.5f) * _e55), ((_e57 + 0.5f) * _e61)), 0f); + return _e64; } fn ClusterRow_u0028_i1_u003b(slot_3: ptr) -> f32 { @@ -32804,35 +33202,35 @@ fn ClusterRow_u0028_i1_u003b(slot_3: ptr) -> f32 { var param_7: vec4; var param_8: i32; - let _e58 = g_cluster_offset; - let _e59 = (*slot_3); - entry = (_e58 + f32(_e59)); - let _e62 = entry; - row_2 = floor((_e62 / 16f)); - let _e65 = entry; - let _e66 = row_2; - within = (_e65 - (_e66 * 16f)); - let _e69 = within; - texel_1 = floor((_e69 / 4f)); - let _e74 = light_list_info.cluster_depth[3u]; - let _e75 = row_2; - let _e77 = texel_1; - param_5 = _e77; - param_6 = (_e74 + _e75); - let _e78 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_5), (¶m_6)); - four_1 = _e78; - let _e79 = within; - let _e80 = texel_1; - let _e85 = four_1; - param_7 = _e85; - param_8 = i32(((_e79 - (_e80 * 4f)) + 0.5f)); - let _e86 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_7), (¶m_8)); - return _e86; + let _e59 = g_cluster_offset; + let _e60 = (*slot_3); + entry = (_e59 + f32(_e60)); + let _e63 = entry; + row_2 = floor((_e63 / 16f)); + let _e66 = entry; + let _e67 = row_2; + within = (_e66 - (_e67 * 16f)); + let _e70 = within; + texel_1 = floor((_e70 / 4f)); + let _e75 = light_list_info.cluster_depth[3u]; + let _e76 = row_2; + let _e78 = texel_1; + param_5 = _e78; + param_6 = (_e75 + _e76); + let _e79 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_5), (¶m_6)); + four_1 = _e79; + let _e80 = within; + let _e81 = texel_1; + let _e86 = four_1; + param_7 = _e86; + param_8 = i32(((_e80 - (_e81 * 4f)) + 0.5f)); + let _e87 = LightListLane_u0028_vf4_u003b_i1_u003b((¶m_7), (¶m_8)); + return _e87; } fn Clustered_u0028_() -> bool { - let _e50 = light_list_info.cluster_grid[3u]; - return (_e50 > 0.5f); + let _e51 = light_list_info.cluster_grid[3u]; + return (_e51 > 0.5f); } fn ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b(index: ptr, world_1: ptr>, toLight: ptr>, radiance: ptr>) { @@ -32843,12 +33241,12 @@ fn ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b(index: ptr; var degenerate: bool; - var local_9: vec3; + var local_11: vec3; var ratio: f32; - var local_10: f32; + var local_12: f32; var window: f32; - var falloff: f32; + var falloff_1: f32; var spot: bool; var ramp: f32; - var local_11: f32; - var attenuation: f32; - var local_12: f32; var local_13: f32; + var attenuation: f32; + var local_14: f32; + var local_15: f32; - let _e84 = (*index); - if (_e84 < 8i) { - let _e86 = (*index); - let _e89 = contributor_light_info.light_position[_e86]; - position = _e89; - let _e90 = (*index); - let _e93 = contributor_light_info.light_color[_e90]; - color = _e93; - let _e94 = (*index); - let _e97 = contributor_light_info.light_direction[_e94]; - direction = _e97; - let _e98 = (*index); - let _e101 = contributor_light_info.light_cone[_e98]; - cone = _e101; - } else { - let _e102 = (*index); - slot_4 = (_e102 - 8i); - let _e104 = Clustered_u0028_(); - clustered = _e104; - let _e105 = clustered; - if _e105 { - let _e106 = slot_4; - param_9 = _e106; - let _e107 = ClusterRow_u0028_i1_u003b((¶m_9)); - local_7 = _e107; + let _e85 = (*index); + if (_e85 < 8i) { + let _e87 = (*index); + let _e90 = contributor_light_info.light_position[_e87]; + position = _e90; + let _e91 = (*index); + let _e94 = contributor_light_info.light_color[_e91]; + color = _e94; + let _e95 = (*index); + let _e98 = contributor_light_info.light_direction[_e95]; + direction = _e98; + let _e99 = (*index); + let _e102 = contributor_light_info.light_cone[_e99]; + cone = _e102; + } else { + let _e103 = (*index); + slot_4 = (_e103 - 8i); + let _e105 = Clustered_u0028_(); + clustered = _e105; + let _e106 = clustered; + if _e106 { + let _e107 = slot_4; + param_9 = _e107; + let _e108 = ClusterRow_u0028_i1_u003b((¶m_9)); + local_9 = _e108; } else { - let _e108 = slot_4; - param_10 = _e108; - let _e109 = LightListRow_u0028_i1_u003b((¶m_10)); - local_7 = _e109; - } - let _e110 = local_7; - listRow = _e110; - let _e111 = listRow; - let _e115 = light_list_info.list[2u]; - v = ((_e111 + 0.5f) * _e115); - let _e119 = light_list_info.list[1u]; - u = _e119; - let _e120 = u; - let _e122 = v; - let _e124 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((0.5f * _e120), _e122), 0f); - position = _e124; - let _e125 = u; - let _e127 = v; - let _e129 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((1.5f * _e125), _e127), 0f); - color = _e129; - let _e130 = u; - let _e132 = v; - let _e134 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((2.5f * _e130), _e132), 0f); - direction = _e134; - let _e135 = u; - let _e137 = v; - let _e139 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((3.5f * _e135), _e137), 0f); - cone = _e139; - let _e140 = clustered; - if _e140 { - let _e141 = listRow; - param_11 = _e141; - let _e142 = InSlots_u0028_f1_u003b((¶m_11)); - local_8 = select(1f, 0f, _e142); + let _e109 = slot_4; + param_10 = _e109; + let _e110 = LightListRow_u0028_i1_u003b((¶m_10)); + local_9 = _e110; + } + let _e111 = local_9; + listRow = _e111; + let _e112 = listRow; + let _e116 = light_list_info.list[2u]; + v = ((_e112 + 0.5f) * _e116); + let _e120 = light_list_info.list[1u]; + u = _e120; + let _e121 = u; + let _e123 = v; + let _e125 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((0.5f * _e121), _e123), 0f); + position = _e125; + let _e126 = u; + let _e128 = v; + let _e130 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((1.5f * _e126), _e128), 0f); + color = _e130; + let _e131 = u; + let _e133 = v; + let _e135 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((2.5f * _e131), _e133), 0f); + direction = _e135; + let _e136 = u; + let _e138 = v; + let _e140 = textureSampleLevel(light_list_texture_tex, light_list_texture_smp, vec2((3.5f * _e136), _e138), 0f); + cone = _e140; + let _e141 = clustered; + if _e141 { + let _e142 = listRow; + param_11 = _e142; + let _e143 = InSlots_u0028_f1_u003b((¶m_11)); + local_10 = select(1f, 0f, _e143); } else { - let _e144 = slot_4; - param_12 = _e144; - let _e145 = LightListScale_u0028_i1_u003b((¶m_12)); - local_8 = _e145; - } - let _e146 = local_8; - let _e148 = color[3u]; - color[3u] = (_e148 * _e146); - } - let _e152 = position[3u]; - type_31 = _e152; - let _e153 = type_31; - directional = (_e153 < 0.5f); - let _e155 = position; - let _e157 = (*world_1); - offset = (_e155.xyz - _e157); - let _e159 = offset; - distance_ = length(_e159); - let _e161 = direction; - aim = normalize(_e161.xyz); - let _e164 = directional; - let _e166 = distance_; - degenerate = (!(_e164) && (_e166 < 0.000001f)); - let _e169 = directional; - if _e169 { - let _e170 = aim; - local_9 = -(_e170); - } else { - let _e172 = offset; - let _e173 = distance_; - local_9 = (_e172 / vec3(max(_e173, 0.000001f))); - } - let _e177 = local_9; - (*toLight) = _e177; - let _e179 = direction[3u]; - if (_e179 > 0f) { - let _e181 = distance_; - let _e183 = direction[3u]; - local_10 = (_e181 / _e183); - } else { - local_10 = 0f; - } - let _e185 = local_10; - ratio = _e185; - let _e186 = ratio; + let _e145 = slot_4; + param_12 = _e145; + let _e146 = LightListScale_u0028_i1_u003b((¶m_12)); + local_10 = _e146; + } + let _e147 = local_10; + let _e149 = color[3u]; + color[3u] = (_e149 * _e147); + } + let _e153 = position[3u]; + type_31 = _e153; + let _e154 = type_31; + directional = (_e154 < 0.5f); + let _e156 = position; + let _e158 = (*world_1); + offset = (_e156.xyz - _e158); + let _e160 = offset; + distance_ = length(_e160); + let _e162 = direction; + aim = normalize(_e162.xyz); + let _e165 = directional; + let _e167 = distance_; + degenerate = (!(_e165) && (_e167 < 0.000001f)); + let _e170 = directional; + if _e170 { + let _e171 = aim; + local_11 = -(_e171); + } else { + let _e173 = offset; + let _e174 = distance_; + local_11 = (_e173 / vec3(max(_e174, 0.000001f))); + } + let _e178 = local_11; + (*toLight) = _e178; + let _e180 = direction[3u]; + if (_e180 > 0f) { + let _e182 = distance_; + let _e184 = direction[3u]; + local_12 = (_e182 / _e184); + } else { + local_12 = 0f; + } + let _e186 = local_12; + ratio = _e186; let _e187 = ratio; - let _e189 = ratio; - let _e191 = ratio; - window = clamp((1f - (((_e186 * _e187) * _e189) * _e191)), 0f, 1f); - let _e195 = window; + let _e188 = ratio; + let _e190 = ratio; + let _e192 = ratio; + window = clamp((1f - (((_e187 * _e188) * _e190) * _e192)), 0f, 1f); let _e196 = window; - let _e198 = distance_; + let _e197 = window; let _e199 = distance_; - falloff = ((_e195 * _e196) / max((_e198 * _e199), 0.0001f)); - let _e203 = type_31; - let _e205 = type_31; - spot = ((_e203 > 1.5f) && (_e205 < 2.5f)); - let _e208 = spot; - if _e208 { - let _e209 = aim; - let _e210 = (*toLight); - let _e214 = cone[1u]; - let _e217 = cone[0u]; - let _e219 = cone[1u]; - local_11 = clamp(((dot(_e209, -(_e210)) - _e214) / max((_e217 - _e219), 0.0001f)), 0f, 1f); - } else { - local_11 = 1f; - } - let _e224 = local_11; - ramp = _e224; - let _e225 = directional; - if _e225 { - local_12 = 1f; + let _e200 = distance_; + falloff_1 = ((_e196 * _e197) / max((_e199 * _e200), 0.0001f)); + let _e204 = type_31; + let _e206 = type_31; + spot = ((_e204 > 1.5f) && (_e206 < 2.5f)); + let _e209 = spot; + if _e209 { + let _e210 = aim; + let _e211 = (*toLight); + let _e215 = cone[1u]; + let _e218 = cone[0u]; + let _e220 = cone[1u]; + local_13 = clamp(((dot(_e210, -(_e211)) - _e215) / max((_e218 - _e220), 0.0001f)), 0f, 1f); + } else { + local_13 = 1f; + } + let _e225 = local_13; + ramp = _e225; + let _e226 = directional; + if _e226 { + local_14 = 1f; } else { - let _e226 = degenerate; - if _e226 { - local_13 = 0f; + let _e227 = degenerate; + if _e227 { + local_15 = 0f; } else { - let _e227 = falloff; - let _e228 = ramp; - local_13 = (_e227 * _e228); - } - let _e230 = local_13; - local_12 = _e230; - } - let _e231 = local_12; - attenuation = _e231; - let _e232 = color; - let _e235 = color[3u]; - let _e237 = attenuation; - (*radiance) = ((_e232.xyz * _e235) * _e237); + let _e228 = falloff_1; + let _e229 = ramp; + local_15 = (_e228 * _e229); + } + let _e231 = local_15; + local_14 = _e231; + } + let _e232 = local_14; + attenuation = _e232; + let _e233 = color; + let _e236 = color[3u]; + let _e238 = attenuation; + (*radiance) = ((_e233.xyz * _e236) * _e238); return; } @@ -33032,65 +33430,65 @@ fn FindCluster_u0028_vf3_u003b(world_2: ptr>) { var tx: f32; var ty: f32; var tz: f32; - var local_14: f32; + var local_16: f32; var cell: f32; var row_3: f32; var header: vec4; var param_13: f32; var param_14: f32; - let _e63 = light_list_info.cluster_view_projection; - let _e64 = (*world_2); - clip = (_e63 * vec4(_e64.x, _e64.y, _e64.z, 1f)); - let _e70 = clip; - let _e73 = clip[3u]; - ndc = (_e70.xy / vec2(max(_e73, 0.000001f))); - let _e78 = light_list_info.cluster_grid; - grid = _e78.xyz; - let _e82 = light_list_info.cluster_depth[0u]; - near = _e82; - let _e84 = ndc[0u]; - let _e88 = grid[0u]; - let _e92 = grid[0u]; - tx = clamp(floor((((_e84 * 0.5f) + 0.5f) * _e88)), 0f, (_e92 - 1f)); - let _e96 = ndc[1u]; - let _e100 = grid[1u]; - let _e104 = grid[1u]; - ty = clamp(floor((((_e96 * 0.5f) + 0.5f) * _e100)), 0f, (_e104 - 1f)); - let _e108 = clip[3u]; - let _e109 = near; - if (_e108 <= _e109) { - local_14 = 0f; - } else { - let _e112 = clip[3u]; - let _e113 = near; - let _e118 = light_list_info.cluster_depth[1u]; - let _e122 = grid[2u]; - local_14 = clamp(floor((log((_e112 / _e113)) * _e118)), 0f, (_e122 - 1f)); - } - let _e125 = local_14; - tz = _e125; - let _e126 = tx; - let _e127 = ty; - let _e129 = grid[0u]; - let _e132 = tz; - let _e134 = grid[0u]; - let _e137 = grid[1u]; - cell = ((_e126 + (_e127 * _e129)) + ((_e132 * _e134) * _e137)); - let _e140 = cell; - row_3 = floor((_e140 / 4f)); - let _e143 = cell; - let _e144 = row_3; - let _e149 = light_list_info.cluster_depth[2u]; - let _e150 = row_3; - param_13 = (_e143 - (_e144 * 4f)); - param_14 = (_e149 + _e150); - let _e152 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_13), (¶m_14)); - header = _e152; - let _e154 = header[0u]; - g_cluster_offset = _e154; - let _e156 = header[1u]; - g_cluster_count = _e156; + let _e64 = light_list_info.cluster_view_projection; + let _e65 = (*world_2); + clip = (_e64 * vec4(_e65.x, _e65.y, _e65.z, 1f)); + let _e71 = clip; + let _e74 = clip[3u]; + ndc = (_e71.xy / vec2(max(_e74, 0.000001f))); + let _e79 = light_list_info.cluster_grid; + grid = _e79.xyz; + let _e83 = light_list_info.cluster_depth[0u]; + near = _e83; + let _e85 = ndc[0u]; + let _e89 = grid[0u]; + let _e93 = grid[0u]; + tx = clamp(floor((((_e85 * 0.5f) + 0.5f) * _e89)), 0f, (_e93 - 1f)); + let _e97 = ndc[1u]; + let _e101 = grid[1u]; + let _e105 = grid[1u]; + ty = clamp(floor((((_e97 * 0.5f) + 0.5f) * _e101)), 0f, (_e105 - 1f)); + let _e109 = clip[3u]; + let _e110 = near; + if (_e109 <= _e110) { + local_16 = 0f; + } else { + let _e113 = clip[3u]; + let _e114 = near; + let _e119 = light_list_info.cluster_depth[1u]; + let _e123 = grid[2u]; + local_16 = clamp(floor((log((_e113 / _e114)) * _e119)), 0f, (_e123 - 1f)); + } + let _e126 = local_16; + tz = _e126; + let _e127 = tx; + let _e128 = ty; + let _e130 = grid[0u]; + let _e133 = tz; + let _e135 = grid[0u]; + let _e138 = grid[1u]; + cell = ((_e127 + (_e128 * _e130)) + ((_e133 * _e135) * _e138)); + let _e141 = cell; + row_3 = floor((_e141 / 4f)); + let _e144 = cell; + let _e145 = row_3; + let _e150 = light_list_info.cluster_depth[2u]; + let _e151 = row_3; + param_13 = (_e144 - (_e145 * 4f)); + param_14 = (_e150 + _e151); + let _e153 = LightListTexel_u0028_f1_u003b_f1_u003b((¶m_13), (¶m_14)); + header = _e153; + let _e155 = header[0u]; + g_cluster_offset = _e155; + let _e157 = header[1u]; + g_cluster_count = _e157; return; } @@ -33098,19 +33496,19 @@ fn ContributorLightCount_u0028_vf3_u003b(world_3: ptr>) -> i var tail: f32; var param_15: vec3; - let _e53 = light_list_info.list[0u]; - tail = _e53; - let _e54 = Clustered_u0028_(); - if _e54 { - let _e55 = (*world_3); - param_15 = _e55; + let _e54 = light_list_info.list[0u]; + tail = _e54; + let _e55 = Clustered_u0028_(); + if _e55 { + let _e56 = (*world_3); + param_15 = _e56; FindCluster_u0028_vf3_u003b((¶m_15)); - let _e56 = g_cluster_count; - tail = _e56; + let _e57 = g_cluster_count; + tail = _e57; } - let _e59 = contributor_light_info.slots[0u]; - let _e63 = tail; - return (clamp(i32((_e59 + 0.5f)), 0i, 8i) + clamp(i32((_e63 + 0.5f)), 0i, 24i)); + let _e60 = contributor_light_info.slots[0u]; + let _e64 = tail; + return (clamp(i32((_e60 + 0.5f)), 0i, 8i) + clamp(i32((_e64 + 0.5f)), 0i, 24i)); } fn main_1() { @@ -33137,101 +33535,95 @@ fn main_1() { g_cluster_offset = 0f; g_cluster_count = 0f; - let _e68 = v_uv_1; - let _e69 = textureSample(six_way_positive_tex, six_way_positive_smp, _e68); - positive_1 = _e69; - let _e70 = v_uv_1; - let _e71 = textureSample(six_way_negative_tex, six_way_negative_smp, _e70); - negative_1 = _e71; + let _e69 = v_uv_1; + let _e70 = textureSample(six_way_positive_tex, six_way_positive_smp, _e69); + positive_1 = _e70; + let _e71 = v_uv_1; + let _e72 = textureSample(six_way_negative_tex, six_way_negative_smp, _e71); + negative_1 = _e72; lit = vec3(0f, 0f, 0f); - let _e72 = v_world_position_1; - param_16 = _e72; - let _e73 = ContributorLightCount_u0028_vf3_u003b((¶m_16)); - count = _e73; + let _e73 = v_world_position_1; + param_16 = _e73; + let _e74 = ContributorLightCount_u0028_vf3_u003b((¶m_16)); + count = _e74; i = 0i; loop { - let _e74 = i; - if (_e74 < 32i) { - let _e76 = i; - let _e77 = count; - if (_e76 >= _e77) { + let _e75 = i; + if (_e75 < 32i) { + let _e77 = i; + let _e78 = count; + if (_e77 >= _e78) { break; } - let _e79 = i; - param_17 = _e79; - let _e80 = v_world_position_1; - param_18 = _e80; + let _e80 = i; + param_17 = _e80; + let _e81 = v_world_position_1; + param_18 = _e81; ContributorLight_u0028_i1_u003b_vf3_u003b_vf3_u003b_vf3_u003b((¶m_17), (¶m_18), (¶m_19), (¶m_20)); - let _e81 = param_19; - l_1 = _e81; - let _e82 = param_20; - radiance_1 = _e82; - let _e83 = radiance_1; - let _e84 = l_1; - param_21 = _e84; - let _e85 = positive_1; - param_22 = _e85.xyz; - let _e87 = negative_1; - param_23 = _e87.xyz; - let _e89 = SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b((¶m_21), (¶m_22), (¶m_23)); - let _e91 = lit; - lit = (_e91 + (_e83 * _e89)); + let _e82 = param_19; + l_1 = _e82; + let _e83 = param_20; + radiance_1 = _e83; + let _e84 = radiance_1; + let _e85 = l_1; + param_21 = _e85; + let _e86 = positive_1; + param_22 = _e86.xyz; + let _e88 = negative_1; + param_23 = _e88.xyz; + let _e90 = SixWayResponse_u0028_vf3_u003b_vf3_u003b_vf3_u003b((¶m_21), (¶m_22), (¶m_23)); + let _e92 = lit; + lit = (_e92 + (_e84 * _e90)); continue; } else { break; } continuing { - let _e93 = i; - i = (_e93 + 1i); + let _e94 = i; + i = (_e94 + 1i); } } - let _e96 = positive_1[0u]; - let _e98 = positive_1[1u]; - let _e101 = positive_1[2u]; - let _e104 = negative_1[0u]; - let _e107 = negative_1[1u]; - let _e110 = negative_1[2u]; - mean = ((((((_e96 + _e98) + _e101) + _e104) + _e107) + _e110) / 6f); - let _e113 = v_color_1; - let _e115 = lit; - let _e117 = six_way_info.ambient; - let _e119 = mean; - let _e124 = six_way_info.emission; - let _e127 = negative_1[3u]; - color_1 = ((_e113.xyz * (_e115 + (_e117.xyz * _e119))) + (_e124.xyz * _e127)); - fogged = 1f; - let _e132 = fog_info.fog[3u]; - if (_e132 > 0f) { - let _e136 = fog_info.fog[3u]; - let _e138 = v_world_position_1; - let _e140 = fog_info.eye; - fogged = clamp(exp((-(_e136) * distance(_e138, _e140.xyz))), 0f, 1f); - } - let _e147 = fog_info.fog; + let _e97 = positive_1[0u]; + let _e99 = positive_1[1u]; + let _e102 = positive_1[2u]; + let _e105 = negative_1[0u]; + let _e108 = negative_1[1u]; + let _e111 = negative_1[2u]; + mean = ((((((_e97 + _e99) + _e102) + _e105) + _e108) + _e111) / 6f); + let _e114 = v_color_1; + let _e116 = lit; + let _e118 = six_way_info.ambient; + let _e120 = mean; + let _e125 = six_way_info.emission; + let _e128 = negative_1[3u]; + color_1 = ((_e114.xyz * (_e116 + (_e118.xyz * _e120))) + (_e125.xyz * _e128)); + let _e131 = ParticleFogTransmittance_u0028_(); + fogged = _e131; + let _e133 = fog_info.fog; + let _e135 = color_1; + let _e136 = fogged; + color_1 = mix(_e133.xyz, _e135, vec3(_e136)); + let _e140 = v_color_1[3u]; + let _e142 = positive_1[3u]; + coverage = clamp((_e140 * _e142), 0f, 1f); + let _e145 = v_world_position_1; + param_24 = _e145; + let _e146 = SoftParticleFade_u0028_vf3_u003b((¶m_24)); + let _e147 = coverage; + coverage = (_e147 * _e146); let _e149 = color_1; - let _e150 = fogged; - color_1 = mix(_e147.xyz, _e149, vec3(_e150)); - let _e154 = v_color_1[3u]; - let _e156 = positive_1[3u]; - coverage = clamp((_e154 * _e156), 0f, 1f); - let _e159 = v_world_position_1; - param_24 = _e159; - let _e160 = SoftParticleFade_u0028_vf3_u003b((¶m_24)); - let _e161 = coverage; - coverage = (_e161 * _e160); - let _e163 = color_1; - let _e164 = coverage; - let _e165 = (_e163 * _e164); - let _e166 = coverage; - frag_color = vec4(_e165.x, _e165.y, _e165.z, _e166); + let _e150 = coverage; + let _e151 = (_e149 * _e150); + let _e152 = coverage; + frag_color = vec4(_e151.x, _e151.y, _e151.z, _e152); return; } @fragment -fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2, @location(6) v_world_position: vec3, @location(0) v_color: vec4) -> @location(0) vec4 { +fn main(@builtin(position) gl_FragCoord: vec4, @location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { gl_FragCoord_1 = gl_FragCoord; - v_uv_1 = v_uv; v_world_position_1 = v_world_position; + v_uv_1 = v_uv; v_color_1 = v_color; main_1(); let _e9 = frag_color; @@ -33277,10 +33669,20 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 name: 'FogInfo', group: 1, binding: 1, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), WebGpuBlock( @@ -33403,61 +33805,119 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } -var v_uv_1: vec2; -var v_color_1: vec4; @group(1) @binding(0) var fog_info: FogInfo; var v_world_position_1: vec3; +var v_uv_1: vec2; +var v_color_1: vec4; var frag_color: vec4; +fn ParticleOrthographic_u0028_() -> bool { + let _e21 = fog_info.projection[0u]; + return (_e21 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e20 = ParticleOrthographic_u0028_(); + if _e20 { + let _e21 = v_world_position_1; + let _e23 = fog_info.eye; + let _e27 = fog_info.forward; + local = max(dot((_e21 - _e23.xyz), _e27.xyz), 0f); + } else { + let _e31 = v_world_position_1; + let _e33 = fog_info.eye; + local = distance(_e31, _e33.xyz); + } + let _e36 = local; + return _e36; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e25 = fog_info.fog[3u]; + density = _e25; + let _e26 = density; + if (_e26 <= 0f) { + return 1f; + } + let _e30 = fog_info.eye[3u]; + falloff = _e30; + let _e31 = falloff; + if (_e31 > 0f) { + let _e33 = falloff; + let _e35 = v_world_position_1[1u]; + let _e38 = fog_info.eye[1u]; + k = (_e33 * (_e35 - _e38)); + let _e41 = k; + if (abs(_e41) > 0.001f) { + let _e44 = k; + let _e48 = k; + local_1 = ((1f - exp(-(_e44))) / _e48); + } else { + let _e50 = k; + local_1 = (1f - (0.5f * _e50)); + } + let _e53 = local_1; + let _e54 = density; + density = (_e54 * _e53); + } + let _e56 = density; + let _e58 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e56) * _e58)), 0f, 1f); +} + fn main_1() { var power: f32; var alpha: f32; var colour: vec3; - var visibility: f32; - let _e17 = v_uv_1; - let _e18 = v_uv_1; - power = (-0.5f * dot(_e17, _e18)); - let _e21 = power; - if (_e21 < -4.5f) { + let _e22 = v_uv_1; + let _e23 = v_uv_1; + power = (-0.5f * dot(_e22, _e23)); + let _e26 = power; + if (_e26 < -4.5f) { discard; } - let _e24 = v_color_1[3u]; - let _e25 = power; - alpha = (_e24 * exp(_e25)); - let _e28 = alpha; - if (_e28 < 0.003921569f) { + let _e29 = v_color_1[3u]; + let _e30 = power; + alpha = (_e29 * exp(_e30)); + let _e33 = alpha; + if (_e33 < 0.003921569f) { discard; } - let _e30 = v_color_1; - colour = _e30.xyz; - let _e34 = fog_info.fog[3u]; - if (_e34 > 0f) { - let _e38 = fog_info.fog[3u]; - let _e40 = v_world_position_1; - let _e42 = fog_info.eye; - visibility = clamp(exp((-(_e38) * distance(_e40, _e42.xyz))), 0f, 1f); - let _e49 = fog_info.fog; - let _e51 = colour; - let _e52 = visibility; - colour = mix(_e49.xyz, _e51, vec3(_e52)); - } - let _e55 = colour; - let _e56 = alpha; - let _e57 = (_e55 * _e56); - let _e58 = alpha; - frag_color = vec4(_e57.x, _e57.y, _e57.z, _e58); + let _e35 = v_color_1; + colour = _e35.xyz; + let _e39 = fog_info.fog[3u]; + if (_e39 > 0f) { + let _e42 = fog_info.fog; + let _e44 = colour; + let _e45 = ParticleFogTransmittance_u0028_(); + colour = mix(_e42.xyz, _e44, vec3(_e45)); + } + let _e48 = colour; + let _e49 = alpha; + let _e50 = (_e48 * _e49); + let _e51 = alpha; + frag_color = vec4(_e50.x, _e50.y, _e50.z, _e51); return; } @fragment -fn main(@location(5) v_uv: vec2, @location(0) v_color: vec4, @location(6) v_world_position: vec3) -> @location(0) vec4 { +fn main(@location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { + v_world_position_1 = v_world_position; v_uv_1 = v_uv; v_color_1 = v_color; - v_world_position_1 = v_world_position; main_1(); let _e7 = frag_color; return _e7; @@ -33469,10 +33929,20 @@ fn main(@location(5) v_uv: vec2, @location(0) v_color: vec4, @location name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), ], @@ -33480,46 +33950,109 @@ fn main(@location(5) v_uv: vec2, @location(0) v_color: vec4, @location ), 'SplatHashed': WebGpuStage( wgsl: r''' -struct SplatHashInfo { - frame: vec4, +struct FogInfo { + fog: vec4, eye: vec4, forward: vec4, + projection: vec4, } -struct FogInfo { - fog: vec4, +struct SplatHashInfo { + frame: vec4, eye: vec4, + forward: vec4, } var gl_FragCoord_1: vec4; +@group(1) @binding(0) +var fog_info: FogInfo; +var v_world_position_1: vec3; var v_uv_1: vec2; var v_color_1: vec4; -var v_world_position_1: vec3; @group(1) @binding(1) var splat_hash_info: SplatHashInfo; @group(1) @binding(2) var blue_noise_texture_tex: texture_2d; @group(1) @binding(3) var blue_noise_texture_smp: sampler; -@group(1) @binding(0) -var fog_info: FogInfo; var frag_color: vec4; +fn ParticleOrthographic_u0028_() -> bool { + let _e42 = fog_info.projection[0u]; + return (_e42 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e41 = ParticleOrthographic_u0028_(); + if _e41 { + let _e42 = v_world_position_1; + let _e44 = fog_info.eye; + let _e48 = fog_info.forward; + local = max(dot((_e42 - _e44.xyz), _e48.xyz), 0f); + } else { + let _e52 = v_world_position_1; + let _e54 = fog_info.eye; + local = distance(_e52, _e54.xyz); + } + let _e57 = local; + return _e57; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e46 = fog_info.fog[3u]; + density = _e46; + let _e47 = density; + if (_e47 <= 0f) { + return 1f; + } + let _e51 = fog_info.eye[3u]; + falloff = _e51; + let _e52 = falloff; + if (_e52 > 0f) { + let _e54 = falloff; + let _e56 = v_world_position_1[1u]; + let _e59 = fog_info.eye[1u]; + k = (_e54 * (_e56 - _e59)); + let _e62 = k; + if (abs(_e62) > 0.001f) { + let _e65 = k; + let _e69 = k; + local_1 = ((1f - exp(-(_e65))) / _e69); + } else { + let _e71 = k; + local_1 = (1f - (0.5f * _e71)); + } + let _e74 = local_1; + let _e75 = density; + density = (_e75 * _e74); + } + let _e77 = density; + let _e79 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e77) * _e79)), 0f, 1f); +} + fn FragCoordFromTop_u0028_f1_u003b(rows: ptr) -> vec2 { - var local: vec2; + var local_2: vec2; - let _e40 = (*rows); - if (_e40 > 0f) { - let _e43 = gl_FragCoord_1[0u]; - let _e44 = (*rows); - let _e46 = gl_FragCoord_1[1u]; - local = vec2(_e43, (_e44 - _e46)); + let _e42 = (*rows); + if (_e42 > 0f) { + let _e45 = gl_FragCoord_1[0u]; + let _e46 = (*rows); + let _e48 = gl_FragCoord_1[1u]; + local_2 = vec2(_e45, (_e46 - _e48)); } else { - let _e49 = gl_FragCoord_1; - local = _e49.xy; + let _e51 = gl_FragCoord_1; + local_2 = _e51.xy; } - let _e51 = local; - return _e51; + let _e53 = local_2; + return _e53; } fn main_1() { @@ -33536,91 +34069,86 @@ fn main_1() { var corner: vec2; var threshold: f32; var colour: vec3; - var visibility: f32; - let _e52 = v_uv_1; let _e53 = v_uv_1; - power = (-0.5f * dot(_e52, _e53)); - let _e56 = power; - if (_e56 < -4.5f) { + let _e54 = v_uv_1; + power = (-0.5f * dot(_e53, _e54)); + let _e57 = power; + if (_e57 < -4.5f) { discard; } - let _e59 = v_color_1[3u]; - let _e60 = power; - alpha = (_e59 * exp(_e60)); - let _e63 = alpha; - if (_e63 < 0.003921569f) { + let _e60 = v_color_1[3u]; + let _e61 = power; + alpha = (_e60 * exp(_e61)); + let _e64 = alpha; + if (_e64 < 0.003921569f) { discard; } - let _e65 = v_world_position_1; - let _e67 = splat_hash_info.eye; - let _e71 = splat_hash_info.forward; - along = dot((_e65 - _e67.xyz), _e71.xyz); - let _e74 = along; - let _e77 = v_color_1; - identity = (floor((_e74 * 1000f)) + floor(dot(_e77, vec4(255f, 1785f, 7905f, 32385f)))); - let _e81 = identity; - identity = (_e81 - (floor((_e81 / 4096f)) * 4096f)); - let _e86 = identity; - let _e88 = ((_e86 * 1597f) + 2531f); - mixed = (_e88 - (floor((_e88 / 4096f)) * 4096f)); - let _e93 = mixed; + let _e66 = v_world_position_1; + let _e68 = splat_hash_info.eye; + let _e72 = splat_hash_info.forward; + along = dot((_e66 - _e68.xyz), _e72.xyz); + let _e75 = along; + let _e78 = v_color_1; + identity = (floor((_e75 * 1000f)) + floor(dot(_e78, vec4(255f, 1785f, 7905f, 32385f)))); + let _e82 = identity; + identity = (_e82 - (floor((_e82 / 4096f)) * 4096f)); + let _e87 = identity; + let _e89 = ((_e87 * 1597f) + 2531f); + mixed = (_e89 - (floor((_e89 / 4096f)) * 4096f)); let _e94 = mixed; - let _e96 = ((2f * _e94) + 1f); - let _e101 = (_e93 * (_e96 - (floor((_e96 / 4096f)) * 4096f))); - mixed = (_e101 - (floor((_e101 / 4096f)) * 4096f)); - let _e106 = mixed; - row = floor((_e106 / 64f)); - let _e109 = mixed; - let _e110 = row; - let _e112 = (_e109 + (_e110 * 37f)); - let _e117 = row; - offset = vec2((_e112 - (floor((_e112 / 64f)) * 64f)), _e117); - let _e121 = splat_hash_info.frame[0u]; - slice = _e121; - let _e124 = splat_hash_info.frame[1u]; - param = _e124; - let _e125 = FragCoordFromTop_u0028_f1_u003b((¶m)); - let _e127 = offset; - let _e128 = (floor(_e125) + _e127); - let _e129 = vec2(64f); - cell = (_e128 - (floor((_e128 / _e129)) * _e129)); - let _e134 = slice; - let _e139 = slice; - corner = (vec2((_e134 - (floor((_e134 / 8f)) * 8f)), floor((_e139 / 8f))) * 64f); - let _e144 = corner; - let _e145 = cell; - let _e150 = textureSampleLevel(blue_noise_texture_tex, blue_noise_texture_smp, (((_e144 + _e145) + vec2(0.5f)) / vec2(512f, 256f)), 0f); - threshold = (_e150.x * 0.99609375f); - let _e153 = alpha; - let _e154 = threshold; - if (_e153 <= _e154) { + let _e95 = mixed; + let _e97 = ((2f * _e95) + 1f); + let _e102 = (_e94 * (_e97 - (floor((_e97 / 4096f)) * 4096f))); + mixed = (_e102 - (floor((_e102 / 4096f)) * 4096f)); + let _e107 = mixed; + row = floor((_e107 / 64f)); + let _e110 = mixed; + let _e111 = row; + let _e113 = (_e110 + (_e111 * 37f)); + let _e118 = row; + offset = vec2((_e113 - (floor((_e113 / 64f)) * 64f)), _e118); + let _e122 = splat_hash_info.frame[0u]; + slice = _e122; + let _e125 = splat_hash_info.frame[1u]; + param = _e125; + let _e126 = FragCoordFromTop_u0028_f1_u003b((¶m)); + let _e128 = offset; + let _e129 = (floor(_e126) + _e128); + let _e130 = vec2(64f); + cell = (_e129 - (floor((_e129 / _e130)) * _e130)); + let _e135 = slice; + let _e140 = slice; + corner = (vec2((_e135 - (floor((_e135 / 8f)) * 8f)), floor((_e140 / 8f))) * 64f); + let _e145 = corner; + let _e146 = cell; + let _e151 = textureSampleLevel(blue_noise_texture_tex, blue_noise_texture_smp, (((_e145 + _e146) + vec2(0.5f)) / vec2(512f, 256f)), 0f); + threshold = (_e151.x * 0.99609375f); + let _e154 = alpha; + let _e155 = threshold; + if (_e154 <= _e155) { discard; } - let _e156 = v_color_1; - colour = _e156.xyz; - let _e160 = fog_info.fog[3u]; - if (_e160 > 0f) { - let _e164 = fog_info.fog[3u]; - let _e166 = v_world_position_1; - let _e168 = fog_info.eye; - visibility = clamp(exp((-(_e164) * distance(_e166, _e168.xyz))), 0f, 1f); - let _e175 = fog_info.fog; - let _e177 = colour; - let _e178 = visibility; - colour = mix(_e175.xyz, _e177, vec3(_e178)); - } - let _e181 = colour; - frag_color = vec4(_e181.x, _e181.y, _e181.z, 1f); + let _e157 = v_color_1; + colour = _e157.xyz; + let _e161 = fog_info.fog[3u]; + if (_e161 > 0f) { + let _e164 = fog_info.fog; + let _e166 = colour; + let _e167 = ParticleFogTransmittance_u0028_(); + colour = mix(_e164.xyz, _e166, vec3(_e167)); + } + let _e170 = colour; + frag_color = vec4(_e170.x, _e170.y, _e170.z, 1f); return; } @fragment -fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2, @location(0) v_color: vec4, @location(6) v_world_position: vec3) -> @location(0) vec4 { +fn main(@builtin(position) gl_FragCoord: vec4, @location(6) v_world_position: vec3, @location(5) v_uv: vec2, @location(0) v_color: vec4) -> @location(0) vec4 { gl_FragCoord_1 = gl_FragCoord; + v_world_position_1 = v_world_position; v_uv_1 = v_uv; v_color_1 = v_color; - v_world_position_1 = v_world_position; main_1(); let _e9 = frag_color; return _e9; @@ -33632,10 +34160,20 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 name: 'FogInfo', group: 1, binding: 0, - sizeInBytes: 32, + sizeInBytes: 64, members: [ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), WebGpuBlock( @@ -33669,6 +34207,8 @@ fn main(@builtin(position) gl_FragCoord: vec4, @location(5) v_uv: vec2 struct FogInfo { fog: vec4, eye: vec4, + forward: vec4, + projection: vec4, } @group(1) @binding(0) @@ -33678,48 +34218,127 @@ var v_normal_1: vec3; var frag_color: vec4; var v_color_1: vec4; -fn main_1() { +fn ParticleOrthographic_u0028_() -> bool { + let _e20 = fog_info.projection[0u]; + return (_e20 > 0.5f); +} + +fn ParticleEyeDistance_u0028_() -> f32 { + var local: f32; + + let _e19 = ParticleOrthographic_u0028_(); + if _e19 { + let _e20 = v_world_position_1; + let _e22 = fog_info.eye; + let _e26 = fog_info.forward; + local = max(dot((_e20 - _e22.xyz), _e26.xyz), 0f); + } else { + let _e30 = v_world_position_1; + let _e32 = fog_info.eye; + local = distance(_e30, _e32.xyz); + } + let _e35 = local; + return _e35; +} + +fn ParticleFogTransmittance_u0028_() -> f32 { + var density: f32; + var falloff: f32; + var k: f32; + var local_1: f32; + + let _e24 = fog_info.fog[3u]; + density = _e24; + let _e25 = density; + if (_e25 <= 0f) { + return 1f; + } + let _e29 = fog_info.eye[3u]; + falloff = _e29; + let _e30 = falloff; + if (_e30 > 0f) { + let _e32 = falloff; + let _e34 = v_world_position_1[1u]; + let _e37 = fog_info.eye[1u]; + k = (_e32 * (_e34 - _e37)); + let _e40 = k; + if (abs(_e40) > 0.001f) { + let _e43 = k; + let _e47 = k; + local_1 = ((1f - exp(-(_e43))) / _e47); + } else { + let _e49 = k; + local_1 = (1f - (0.5f * _e49)); + } + let _e52 = local_1; + let _e53 = density; + density = (_e53 * _e52); + } + let _e55 = density; + let _e57 = ParticleEyeDistance_u0028_(); + return clamp(exp((-(_e55) * _e57)), 0f, 1f); +} + +fn ParticleTowardsEye_u0028_() -> vec3 { var to_eye: vec3; - var distance_to_eye: f32; + var length_to_eye: f32; + var local_2: vec3; + + let _e22 = fog_info.eye; + let _e24 = v_world_position_1; + to_eye = (_e22.xyz - _e24); + let _e26 = to_eye; + length_to_eye = length(_e26); + let _e28 = ParticleOrthographic_u0028_(); + if _e28 { + let _e30 = fog_info.forward; + return -(_e30.xyz); + } + let _e33 = length_to_eye; + if (_e33 > 0f) { + let _e35 = to_eye; + let _e36 = length_to_eye; + local_2 = (_e35 / vec3(_e36)); + } else { + local_2 = vec3(0f, 0f, 0f); + } + let _e39 = local_2; + return _e39; +} + +fn main_1() { var n: vec3; + var towards: vec3; var facing: f32; - var local: f32; + var local_3: f32; var intensity: f32; var fogged: f32; - let _e19 = fog_info.eye; - let _e21 = v_world_position_1; - to_eye = (_e19.xyz - _e21); - let _e23 = to_eye; - distance_to_eye = length(_e23); - let _e25 = v_normal_1; - n = normalize(_e25); - let _e27 = distance_to_eye; - if (_e27 > 0f) { - let _e29 = n; - let _e30 = to_eye; - let _e31 = distance_to_eye; - local = abs(dot(_e29, (_e30 / vec3(_e31)))); - } else { - local = 1f; - } - let _e36 = local; - facing = _e36; - let _e37 = facing; - intensity = mix(0.35f, 1f, _e37); - fogged = 1f; - let _e41 = fog_info.fog[3u]; - if (_e41 > 0f) { - let _e45 = fog_info.fog[3u]; - let _e47 = distance_to_eye; - fogged = clamp(exp((-(_e45) * _e47)), 0f, 1f); - } - let _e51 = v_color_1; - let _e54 = v_color_1[3u]; - let _e56 = intensity; - let _e58 = fogged; - let _e59 = (((_e51.xyz * _e54) * _e56) * _e58); - frag_color = vec4(_e59.x, _e59.y, _e59.z, 1f); + let _e24 = v_normal_1; + n = normalize(_e24); + let _e26 = ParticleTowardsEye_u0028_(); + towards = _e26; + let _e27 = towards; + let _e28 = towards; + if (dot(_e27, _e28) > 0f) { + let _e31 = n; + let _e32 = towards; + local_3 = abs(dot(_e31, _e32)); + } else { + local_3 = 1f; + } + let _e35 = local_3; + facing = _e35; + let _e36 = facing; + intensity = mix(0.35f, 1f, _e36); + let _e38 = ParticleFogTransmittance_u0028_(); + fogged = _e38; + let _e39 = v_color_1; + let _e42 = v_color_1[3u]; + let _e44 = intensity; + let _e46 = fogged; + let _e47 = (((_e39.xyz * _e42) * _e44) * _e46); + frag_color = vec4(_e47.x, _e47.y, _e47.z, 1f); return; } @@ -33739,10 +34358,20 @@ fn main(@location(6) v_world_position: vec3, @location(2) v_normal: vec3[ WebGpuBlockMember(name: 'fog', offsetInBytes: 0, sizeInBytes: 16), WebGpuBlockMember(name: 'eye', offsetInBytes: 16, sizeInBytes: 16), + WebGpuBlockMember( + name: 'forward', + offsetInBytes: 32, + sizeInBytes: 16, + ), + WebGpuBlockMember( + name: 'projection', + offsetInBytes: 48, + sizeInBytes: 16, + ), ], ), ], diff --git a/site/content/quickstart.md b/site/content/quickstart.md index 6673fc961..2238aa60f 100644 --- a/site/content/quickstart.md +++ b/site/content/quickstart.md @@ -91,7 +91,7 @@ tool/ci.sh # shaders, analyze, every test (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11355 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. +There are 11362 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. ## Your own application diff --git a/site/content/reference/testing.md b/site/content/reference/testing.md index 31f00ef27..a1f34898a 100644 --- a/site/content/reference/testing.md +++ b/site/content/reference/testing.md @@ -1,10 +1,10 @@ --- -description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11355 tests need a GPU. +description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11362 tests need a GPU. --- # Testing -11355 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. +11362 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. | Package | Tests | | Package | Tests | |---|---|---|---|---| @@ -12,7 +12,7 @@ description: Four independent golden sets, mutation-checking every new test, det | | | | `flutter3d_mesh` | 599 | | | | | `apps/flutter3d_modeler` | 1750 | | `flutter3d_sim` | 700 | | `pad_input` | 67 | -| `flutter3d_lab` | 15 | | `flutter3d_core` | 771 | +| `flutter3d_lab` | 15 | | `flutter3d_core` | 773 | | `flutter3d_lti` | 26 | | `apps/flutter3d_lab_pendulum` | 7 | | | | | `apps/flutter3d_lab_incident` | 13 | | `flutter3d_game_shooter` | 340 | | `flutter3d_audio_core` | 55 | @@ -20,7 +20,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flutter3d_game_racing` | 223 | | `flutter3d_webgl` | 62 | | `flutter3d_game_platformer` | 221 | | `flutter3d_hardware` | 70 | | `apps/flutter3d_demo_platformer` | 208 | | `flutter3d_impeller` | 64 | -| `flutter3d_cpu` | 382 | | `apps/flutter3d_demo_strategy` | 50 | +| `flutter3d_cpu` | 385 | | `apps/flutter3d_demo_strategy` | 50 | | `apps/flutter3d_editor` | 234 | | `apps/flutter3d_demo_arcade` | 20 | | `apps/flutter3d_demo_racing` | 162 | | `pointer_lock` | 28 | | `flutter3d_physics` | 269 | | `flutter3d_webgpu` | 193 | @@ -29,7 +29,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flutter3d_editor_core` | 141 | | `flutter3d_editor_widgets` | 91 | | `apps/flutter3d_demo_dungeon` | 115 | | `flutter3d_app` | 170 | | `flutter3d_game` | 325 | | `flutter3d_shaders` | 5 | -| `flutter3d_particles` | 97 | | `flutter3d_stereo` | 50 | +| `flutter3d_particles` | 99 | | `flutter3d_stereo` | 50 | | `flutter3d_model_core` | 1172 | | `flutter3d_model_mcp` | 196 | | `flutter3d_editor_play` | 28 | | `flutter3d_net` | 15 | | | | | `flutter3d_net_webrtc` | 2 | @@ -40,7 +40,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flame_flutter3d` | 140 | | `apps/flutter3d_showcase` | 88 | | `flame_multiplayer` | 9 | | `flame_multiplayer_dashwire` | 3 | -The rows sum to 11328 rather than 11355: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. +The rows sum to 11335 rather than 11362: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. `flutter3d_samples` is not in the table and has no `test/` at all: it is test data with two path constants over it, and other packages' decoder tests are what exercise it. `flutter3d_conformance` is missing for a different reason: it is invoked as a script harness rather than through `flutter test`, so it does not surface in a grep of `test(` calls either. See below for what that cost once. From 803385801be64065bfef307a10550e2c09fd1d7c Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 20:02:59 +0300 Subject: [PATCH 4/6] Roadmap: say what of P7 is done and what is left The section 4.1 row still said orthography, alpha to coverage and the order were left. All three landed; this records the level format's draw order, the orthographic shadow cascades, the particles' and splats' fog and facing, and the splats' depth sort, with their tests and golden budgets. Left: a near plane at or behind the eye, the order in the editor's inspector, the example overlay's made-up perspective, the MCP tools, and goldens for particles and splats under an orthographic camera or in a height fog. P5 no longer lists height fog on particles and splats. --- tasks/0.9-engine-roadmap.md | 63 ++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/tasks/0.9-engine-roadmap.md b/tasks/0.9-engine-roadmap.md index 3dc054372..2dcf226eb 100644 --- a/tasks/0.9-engine-roadmap.md +++ b/tasks/0.9-engine-roadmap.md @@ -322,7 +322,7 @@ and the published web editor needs it checked from its own address. | P4 | Planar reflections; public render-to-texture camera | `components/planar_reflector_component.dart`, `render_texture.dart` | done, reflections off by default; see below | M | | P5 | Physical sky (atmospheric scattering), stars, height fog | `sky_sources.dart` `PhysicalSkySource` | done, off by default; see below | M | | P6 | Debug views (material channels, UV, NaN, split wipe) and render stats | `render/debug_view.dart`, `render_stats.dart` (0.24) | done, off by default; see below | S | -| P7 | Orthographic everywhere, per-draw ranges, alpha to coverage, render order | 0.24 commits | per-draw ranges done; orthography, alpha to coverage and order left | S | +| P7 | Orthographic everywhere, per-draw ranges, alpha to coverage, render order | 0.24 commits | done; a near plane at the eye, the editor's order and the overlay left, see below | S | **P1 done.** `AntiAliasSettings(method: EdgeSmoothing.smaa)`: edges, area weights and neighbourhood blend as three stages on all four backends, the @@ -459,7 +459,8 @@ density law, the closed form against a numerical integral, and nought being flat); the `sky-physical-dusk` and `sky-physical-night` goldens in all four sets, WebGPU at 0 of 172800 pixels from Impeller, WebGL at 11 and 0, the software set at 0.451% (silhouettes and the floor's far edge, which Impeller -multisamples) and 0.028%. +multisamples) and 0.028%. Particles and splats integrate the height fog the +same way since `P7` gave their stages the whole fog block (see there). *Left:* switching the physical sky and the height fog on by default: the goldens now exist in all four sets, which decision 18 asks for first, but `SkySettings.enabled`, `physical` and `heightFalloff` all still default to @@ -468,8 +469,7 @@ it is its own change; a precomputed scattering table (`PhysicalSky`'s docstring says "not done yet"), which would make the sky a texture read on phones instead of a 16 × 8 march per pixel; multiple scattering (the noon horizon is pale cyan, not white); a moon; ozone; the camera's height moving -the sky; height fog on particles and splats, which fog as a flat fog at the -camera's density; an HDR environment from the sky (`fromSky` is eight bits, +the sky; an HDR environment from the sky (`fromSky` is eight bits, so a bright horizon clips); the sky and fog in the level format, the editor and the MCP tools; timing on the Galaxy A55; the comparison page row, with the 0.9.0 revision (decision 19). Two claims @@ -551,14 +551,53 @@ reading the frame as premultiplied lit every leaf towards its rim. Tests: encoding only when the call was made; the refusal; nothing emitted for a scene that does not ask; mutation-checked twice) and `alpha-to-coverage` in all four sets, the browsers at 0.6% from Impeller on the discs' rims. -*Left:* the order in the level format and the editor; -under an orthographic -camera, the shadow cascades (split by distance from the eye, so the near -ones cover air and everything falls to the last and softest), particles' -and splats' fog and facing (their stages declare a shortened `FogInfo` -without the axis), a near plane at or behind the eye (the surface buffer -reads depth nought or less as sky), splat sorting by distance rather than -depth, and the example's overlay passing a perspective to `lookFrom`. +The order in the level format: `Brush.drawOrder`, optional and left out +at nought so every document keeps its bytes and its digest, refused by the +validator outside −128 to 127 (the sort key's signed byte, which the engine +would otherwise clamp into a tie). Brush surfaces are batched by it, since a +batch is one node with one place in the order; `LevelScene` sets it on the +node; a cut brush's pieces and a duplicate keep it. An edit of it alone is +look-only — `LevelDiff.brushOrder`, from `diffLevel` and from a `LevelPatch` +alike — and any other change to the same brush still goes to `simulation`. +Tests: `brush_draw_order_test.dart` (round trip, no key and the same digest +at nought, the range refused at both ends and taken at both, the batch split, +look-only to the diff and to a patch, and not with the brush moved; each +mutation-checked) and `level_loader_test.dart` (the order reaches the node). +Under an orthographic camera, the shadow cascades: the depths where the view +box meets the casters' bounds, capped at `viewDistance`, are cut into equal +slabs, one per near cascade, each fitted to the corners of its own share of +the box and the bounds (`orthographic_cascades.dart`, every triple of +fourteen planes rather than a clipped polygon); the last cascade is still +the whole scene, the reach back to the light is kept, and `ShadowFactor` +picks by depth through that lens while the shafts and the volumetric fog +pick by the way along the ray from where depth is nought, the same distance +as before through a perspective lens. Tests: `orthographic_cascade_test.dart` +(the near cascade holds casters and is under half the last; canopies at the +bottom and top of an isometric frame both land in near cascades within a +factor of two; three cascades shadow what one does — and it says that no +mutation of the fit makes this one fail, since a miss falls through to the +whole-level map), `orthographic_cascade_pick_test.dart` (the software pick, +both lenses), and `orthographic-shadow` in all four sets, WebGPU at 0 of +172800 pixels from Impeller, WebGL at 0.394% and the software set at 1.043%, +on the edges of posts, slabs and shadows; the other ninety Impeller +references held unchanged. Particles' and splats' fog and facing: the stages +declare the fog's block whole (`lib/particle_fog.glsl`), so they fog by depth +from the eye's plane through an orthographic lens, a mesh particle faces the +view axis there, and — `P5`'s tail — the height fog is integrated along the +ray as `ApplyFog` does, where they fogged at the camera's density; both +particle contributors and the splat contributor bind the axis, the lens and +the falloff (`particle_fog_test.dart`, `splat_fog_test.dart`, +`particle_mesh_facing_test.dart`, each mutation-checked). Splats sorted by +depth along the axis through an orthographic lens, resorted when the axis +turns (`splat_sort_test.dart`). +*Left:* a near plane at or behind the eye (the surface buffer reads depth +nought or less as sky, and every pass that reads it would have to learn +otherwise); the draw order in the editor's inspector, which waits for the +inspector's own rework; the example's mesh overlay golden passing a made-up +perspective pixel size to `MeshOverlay.lookFrom`; the order and the +orthographic camera in the MCP tools; no golden draws particles or splats +under an orthographic camera or in a height fog, so those paths are held by +the software tests alone. Already ours and not theirs, to keep: volumetric fog, weighted blended OIT, motion blur, local exposure and HDR out, EVSM, impostors, Hi-Z and software From 1b509d24869b9303dfc915a59e2de0b4e7a59441 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 20:28:58 +0300 Subject: [PATCH 5/6] Round orthographic cascade radii and slab ends onto a grid The near cascades under an orthographic camera were fitted to the corners of each slab of the view box and the casters' bounds, so their radii and slab ends moved with every pan and every caster that rose or fell. The shadow pass snaps each map to whole texels of its radius, so that grid rescaled every frame and shadow edges crawled, and a still tile was redrawn because its scale had changed instead of being scrolled. The slab ends and radii now round onto steps a sixteenth of their power of two; orthographic-shadow is re-recorded in all four sets (shadow outlines move by a texel). Tests: a centimetre's pan and a caster rising five centimetres leave the near radii and slab ends unchanged; the software shafts and volumetric fog at the edge of an orthographic frame read the near tile; a breach and an editor duplicate keep a brush's drawOrder. The shadow pass's aspect falls back to one for a viewport with no height. --- ARCHITECTURE.md | 2 +- README.md | 2 +- packages/flutter3d/README.md | 4 +- .../test/goldens/orthographic-shadow.png | Bin 24140 -> 24158 bytes .../test/orthographic_cascade_test.dart | 44 ++++++ packages/flutter3d_core/CHANGELOG.md | 5 +- .../engine/render/orthographic_cascades.dart | 37 ++++- .../lib/src/engine/render/renderer.dart | 5 +- .../test/cross_backend_test.dart | 2 +- .../test/goldens/orthographic-shadow.png | Bin 18744 -> 18840 bytes .../test/orthographic_march_cascade_test.dart | 137 ++++++++++++++++++ .../test/editing_test.dart | 26 ++++ .../test/brush_draw_order_test.dart | 20 +++ .../test/goldens/orthographic-shadow.png | Bin 24846 -> 24879 bytes .../test/goldens/orthographic-shadow.png | Bin 24153 -> 24171 bytes site/content/quickstart.md | 2 +- site/content/reference/testing.md | 14 +- tasks/0.9-engine-roadmap.md | 15 +- 18 files changed, 292 insertions(+), 23 deletions(-) create mode 100644 packages/flutter3d_cpu/test/orthographic_march_cascade_test.dart diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cb5267022..cea21d8c5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2691,7 +2691,7 @@ storage. |---|---| | Style | `dart format` | | Analysis | `flutter analyze` clean across the workspace, no warnings | -| Unit tests | **11362 tests** across 43 packages and 10 applications | +| Unit tests | **11368 tests** across 43 packages and 10 applications | | Structure rules | 35, `dart run tool/structure.dart`, the first CI step | | CI | GitHub Actions over `tool/ci.sh`, on `ubuntu-latest`, with no graphics card | diff --git a/README.md b/README.md index 7ff696bc3..521a67507 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Or one package at a time: (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11362 tests across forty-three packages and nine applications. The +There are 11368 tests across forty-three packages and nine applications. The only ones that need a GPU are the Impeller half of the golden set. The other half is rendered by the software backend, which is what makes 91 scenes checkable in a headless run. diff --git a/packages/flutter3d/README.md b/packages/flutter3d/README.md index 497762afa..ecb36c69b 100644 --- a/packages/flutter3d/README.md +++ b/packages/flutter3d/README.md @@ -104,7 +104,7 @@ What works today: `CUBICSPLINE` with authored tangents), slerped rotations, an `AnimationPlayer` with play/pause/seek/speed and once/loop/ping-pong, and the decoded node hierarchy rebuilt on instantiation so an animated parent carries its subtree; -- 1662 tests covering projection, scene, sorting, debug draw, raycasting, +- 1664 tests covering projection, scene, sorting, debug draw, raycasting, animation, skinning, lighting, render targets, BVH, LOD, glTF, OBJ and `.f3d`, plus a real frame drawn through `flutter3d_cpu`'s software rasteriser for the ones that need one, all without a GPU. The geometry the engine is written in @@ -322,7 +322,7 @@ lib/src/engine/assets/ glTF, OBJ and .f3d decoders, isolate loading, ca example/lib/ the demo, and the frame capture hook skills/ the conventions, as agent skills — see below bin/skills.dart what copies them into a project that uses this -test/ 1662 tests, all runnable without a GPU +test/ 1664 tests, all runnable without a GPU ``` The GLSL is not here. Every shader this package draws with lives in diff --git a/packages/flutter3d/test/goldens/orthographic-shadow.png b/packages/flutter3d/test/goldens/orthographic-shadow.png index e911a446911c39f0733a8b926c896ad20d3a990b..e0c7d75ee026f570ffd212c212515407247c9fb8 100644 GIT binary patch literal 24158 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 HRoMRkqP55W diff --git a/packages/flutter3d/test/orthographic_cascade_test.dart b/packages/flutter3d/test/orthographic_cascade_test.dart index ca6deb344..c3cc912bf 100644 --- a/packages/flutter3d/test/orthographic_cascade_test.dart +++ b/packages/flutter3d/test/orthographic_cascade_test.dart @@ -190,6 +190,50 @@ void main() { }, ); + group('the near cascades hold still', () { + // The fit follows where the view box meets the casters' bounds, which + // slides with every pan and every caster that moves. The shadow pass + // snaps each map to whole texels of its radius and scrolls a still tile + // only while its scale holds, so a radius or a slab end that follows the + // fit continuously is a texel grid that rescales every frame. + // + // Mutation: drop the rounding in `orthographicCascades`. A centimetre's + // pan moves the near radii by a couple of millimetres and a canopy rising + // five centimetres moves them and the slab ends by centimetres. + Future<({List radii, List splits})> cascades( + ({Scene scene, CameraNode camera}) room, + ) async { + final engine = _engine(); + await _grid(engine, room, const ShadowSettings(cascades: 3)); + final radii = engine.renderer.debugCascadeRadii; + final splits = engine.renderer.debugCascadeSplits; + return ( + radii: [radii[0], radii[1]], + splits: [splits[0], splits[1]], + ); + } + + test('under a pan of a centimetre', () async { + final room = _isometricRoom(); + final before = await cascades(room); + room.camera.setPosition(0.01, 40.0, -40.0); + final after = await cascades(room); + expect(after.radii, before.radii, reason: 'a near radius moved'); + expect(after.splits, before.splits, reason: 'a slab end moved'); + }); + + test('under a caster rising five centimetres', () async { + final room = _isometricRoom(); + final before = await cascades(room); + room.scene.root + .findByName('near')! + .setPosition(_nearCanopy.x, _nearCanopy.y + 0.05, _nearCanopy.z); + final after = await cascades(room); + expect(after.radii, before.radii, reason: 'a near radius moved'); + expect(after.splits, before.splits, reason: 'a slab end moved'); + }); + }); + test('three cascades shadow the same things as one', () async { // A fitted cascade must move nothing, only sharpen it. // diff --git a/packages/flutter3d_core/CHANGELOG.md b/packages/flutter3d_core/CHANGELOG.md index 213d39402..937eb9496 100644 --- a/packages/flutter3d_core/CHANGELOG.md +++ b/packages/flutter3d_core/CHANGELOG.md @@ -18,7 +18,10 @@ map. Now the depths where the view box meets the casters' bounds, capped at `ShadowSettings.viewDistance`, are cut into equal slabs, one per near cascade, each fitted to its own share of the box; the last cascade is still - the whole scene, and the reach back towards the light is kept. + the whole scene, and the reach back towards the light is kept. The slab + ends and the radii are rounded onto a grid a sixteenth of their power of + two, so a pan or a caster that moves leaves the maps' texels, and the + scroll of a still tile, where they were. `Renderer.debugCascadeSplits` reports where they ended. `orthographic-shadow` is in all four golden sets. diff --git a/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart index 007b2dd42..fe6999d57 100644 --- a/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart +++ b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart @@ -34,6 +34,10 @@ typedef OrthographicCascade = ({double end, Vector3 centre, double radius}); /// convention, depth nought to one; the view box is read off its rows, so a /// projection with an offset or a tilt is handled the same as a centred one. /// +/// The slab ends and every radius are rounded onto a grid of their own size, +/// so that a camera panning by less than a step, or a caster that moves, +/// leaves the maps' texels where they were — see [_quantum]. +/// /// When the view sees none of the bounds, the slabs are cut from the view box /// alone: nothing in it casts, and any fit is as good as another. List orthographicCascades({ @@ -65,6 +69,11 @@ List orthographicCascades({ // A sliver at least, so a view that meets the bounds in a plane still has // slabs with an inside. final end = math.max(math.min(far, start + reach), start + 1e-3); + // Both ends on a grid, so a pan or a caster that moves a little leaves the + // slabs where they were — see [_quantum]. + final step = _quantum(end - start); + final from = (start / step).floorToDouble() * step; + final to = (end / step).ceilToDouble() * step; return [ for (var i = 0; i < slabs; i++) @@ -72,12 +81,34 @@ List orthographicCascades({ volume, eye: eye, forward: forward, - from: start + (end - start) * i / slabs, - to: start + (end - start) * (i + 1) / slabs, + from: from + (to - from) * i / slabs, + to: from + (to - from) * (i + 1) / slabs, ), ]; } +/// A step for rounding a length of about [length]: a sixteenth of the power of +/// two at or above it. +/// +/// **Rounded, because the fit moves with everything.** The corners a slab is +/// fitted to are where the view box meets the casters' bounds, so they slide +/// with every pan of the camera and every caster that rises or falls — a +/// character jumping is enough. The shadow pass snaps each map to whole +/// texels of its radius, and a radius that changes every frame is a texel +/// grid that rescales every frame: shadow edges crawl, and the scroll that +/// keeps a still tile from being redrawn refuses a map whose scale changed. +/// On steps a sixteenth of the length's power of two the radius and the slab +/// ends stay put until the fit has moved by a whole step, and a radius +/// rounded up wastes at most an eighth of the map. +double _quantum(double length) => + math.pow(2.0, (math.log(math.max(length, 1e-3)) / math.ln2).ceil()) / 16.0; + +/// [length] rounded up to its [_quantum]. +double _roundUp(double length) { + final step = _quantum(length); + return (length / step).ceilToDouble() * step; +} + /// `normal · p + offset ≥ 0` for every point p inside. typedef _HalfSpace = ({Vector3 normal, double offset}); @@ -140,7 +171,7 @@ OrthographicCascade _fit( 0.0, (r, corner) => math.max(r, corner.distanceTo(centre)), ); - return (end: to, centre: centre, radius: math.max(radius, 1e-3)); + return (end: to, centre: centre, radius: _roundUp(math.max(radius, 1e-3))); } /// The corners of the convex volume [planes] bound: every point where three diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer.dart b/packages/flutter3d_core/lib/src/engine/render/renderer.dart index 2258757d6..565cbfd6f 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer.dart @@ -4302,7 +4302,10 @@ final class Renderer implements RenderServices { width, height, )) { - final rect => rect.width / rect.height, + // A viewport with no height has no aspect; a square one + // keeps the projection and the corner solver finite. + final rect when rect.height > 0 => rect.width / rect.height, + _ => 1.0, }, ); // One node per probe the scene holds, in scene order, so the name the diff --git a/packages/flutter3d_cpu/test/cross_backend_test.dart b/packages/flutter3d_cpu/test/cross_backend_test.dart index 08cede48f..86a0a7e7b 100644 --- a/packages/flutter3d_cpu/test/cross_backend_test.dart +++ b/packages/flutter3d_cpu/test/cross_backend_test.dart @@ -63,7 +63,7 @@ const Map _budgets = { // multisamples and this set does not; the highlights, fog and sky agree. // Recorded first from an Impeller build by mistake, which read 0%. 'orthographic-metal': 0.65, - // 1.043% measured, on the edges of the posts, the slabs, their shadows + // 1.028% measured, on the edges of the posts, the slabs, their shadows // and the floor, which Impeller multisamples and this set does not; the // shadows themselves sit where Impeller's do, near and far. 'orthographic-shadow': 1.15, diff --git a/packages/flutter3d_cpu/test/goldens/orthographic-shadow.png b/packages/flutter3d_cpu/test/goldens/orthographic-shadow.png index bba6e5a8a185dbf68306d5e4af365a1b64ded128..ce9845fb4a164beab23933f02f5abef11e724ef6 100644 GIT binary patch literal 18840 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 SodF)T#Ng@b=d#Wzp$PyOz93ov literal 18744 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 BoundTexture( + CpuTexture(1, 1, TextureFormat.r16g16b16a16Float), + SamplerOptions.nearestClamp, +); + +Float32List _vec4(double x, double y, double z, double w) => + Float32List.fromList([x, y, z, w]); + +/// What both passes share: an orthographic view a hundred metres wide looking +/// along +z from the origin, thirty metres of march, two cascades split at +/// forty metres, and a light space a hundred and twenty metres across that +/// holds the whole march. +Map _march() => { + 'forward': _vec4(0.0, 0.0, 1.0, 16.0), + // Clip to world: x and y fifty metres each way, depth nought to a hundred, + // and no perspective divide — the rays are parallel. + 'inverse_view_projection': Float32List.fromList( + Matrix4.diagonal3Values(50.0, 50.0, 100.0).storage, + ), + 'camera': _vec4(0.0, 0.0, 0.0, 30.0), + 'cascades': _vec4(40.0, 200.0, 2.0, 0.0), + 'bias': _vec4(0.001, 0.001, 0.001, 0.0), + for (final name in [ + 'shadow_matrix', + 'shadow_matrix_far', + 'shadow_matrix_farthest', + ]) + name: Float32List.fromList( + Matrix4.diagonal3Values(1.0 / 60.0, 1.0 / 60.0, 1.0 / 100.0).storage, + ), + 'sun': _vec4(0.0, -1.0, 0.0, 0.0), +}; + +/// The pixel nine tenths of the way to the right edge: forty-five metres off +/// the axis, so every step of the march is past the split by distance from +/// the eye and inside it by depth. +final Float32List _edge = Float32List.fromList([0.95, 0.5]); + +double _shafts({required bool farLit}) { + final bindings = ShaderBindings( + >{ + 'ShaftInfo': { + ..._march(), + 'scatter': _vec4(1.0, 1.0, 1.0, 0.05), + }, + }, + { + 'scene_texture': _black(), + 'surface_texture': _black(), + 'shadow_texture': _atlas(farLit: farLit), + }, + ); + return builtinCpuShaders()['LightShafts']!.fragment! + .run(_edge, bindings, FragmentContext())! + .x; +} + +double _fog({required bool farLit}) { + final bindings = ShaderBindings( + >{ + 'VolumeFogInfo': { + ..._march(), + 'medium': _vec4(0.05, 0.0, 0.0, 0.0), + 'sun_radiance': _vec4(1.0, 1.0, 1.0, 0.0), + 'ambient': _vec4(0.0, 0.0, 0.0, 0.0), + 'albedo': _vec4(0.0, 0.0, 0.0, 0.0), + }, + }, + { + 'surface_texture': _black(), + 'shadow_texture': _atlas(farLit: farLit), + }, + ); + return builtinCpuShaders()['VolumetricFog']!.fragment! + .run(_edge, bindings, FragmentContext())! + .x; +} + +void main() { + test('a light shaft at the edge of an orthographic frame reads the near ' + 'cascade', () { + // Mutation: pick by `(at - eye).length` in `LightShaftsShader`, as before + // `P7`. Every step is forty-five metres and more from the eye, past the + // split, and reads the far tile's occluder: the shaft goes dark. + final lit = _shafts(farLit: true); + expect(lit, greaterThan(0.0), reason: 'the shaft scatters nothing'); + expect(_shafts(farLit: false), closeTo(lit, 1e-9)); + }); + + test('the volumetric fog at the edge of an orthographic frame reads the ' + 'near cascade', () { + // Mutation: pick by `(at - eye).length` in `VolumetricFogShader`, as + // before `P7`. The air reads the far tile's occluder and scatters only + // the ambient, which is black here. + final lit = _fog(farLit: true); + expect(lit, greaterThan(0.0), reason: 'the fog scatters nothing'); + expect(_fog(farLit: false), closeTo(lit, 1e-9)); + }); +} diff --git a/packages/flutter3d_editor_core/test/editing_test.dart b/packages/flutter3d_editor_core/test/editing_test.dart index cd0242e4b..78831b725 100644 --- a/packages/flutter3d_editor_core/test/editing_test.dart +++ b/packages/flutter3d_editor_core/test/editing_test.dart @@ -214,6 +214,32 @@ void main() { expect(editing.brush!.centre.x, from.centre.x + from.size.x); expect(editing.brush!.material, from.material); }); + + test('and a duplicate keeps its place in the draw order', () { + // Mutation: leave `drawOrder` out of the copy `duplicate` builds. A + // second stripe copied off the first is drawn at nought and fights the + // floor the first one sits on cleanly. + final editing = Editing.parse( + jsonEncode({ + 'version': 1, + 'name': 'test', + 'brushes': [ + { + 'at': [0.0, 0.01, 0.0], + 'size': [1.0, 0.02, 20.0], + 'solid': false, + 'drawOrder': 3, + }, + ], + }), + path: '/levels/test.json', + )..select(Piece.brush, 0); + + editing.duplicate(); + + expect(editing.level.brushes, hasLength(2)); + expect(editing.brush!.drawOrder, 3); + }); }); group('undo', () { diff --git a/packages/flutter3d_sim/test/brush_draw_order_test.dart b/packages/flutter3d_sim/test/brush_draw_order_test.dart index b754ff5ff..ebf066125 100644 --- a/packages/flutter3d_sim/test/brush_draw_order_test.dart +++ b/packages/flutter3d_sim/test/brush_draw_order_test.dart @@ -131,6 +131,26 @@ void main() { ); }); + test('a brush cut by a breach keeps its order in every piece', () { + // Mutation: leave `drawOrder` out of the pieces `subtractBox` builds. A + // stripe with a hole knocked through it comes back at nought and fights + // the floor it lies on. + final stripe = Brush( + centre: Vector3(0.0, 0.01, 0.0), + size: Vector3(1.0, 0.02, 20.0), + solid: false, + drawOrder: 3, + ); + final hole = Aabb3.minMax( + Vector3(-2.0, -1.0, -1.0), + Vector3(2.0, 1.0, 1.0), + ); + final pieces = subtractBox(stripe, hole); + + expect(pieces, hasLength(2)); + expect([for (final piece in pieces) piece.drawOrder], [3, 3]); + }); + group('an edit of the order alone', () { final before = Level.fromJson(_document(_stripe())); final after = Level.fromJson(_document(_stripe(const {'drawOrder': 2}))); diff --git a/packages/flutter3d_webgl/test/goldens/orthographic-shadow.png b/packages/flutter3d_webgl/test/goldens/orthographic-shadow.png index a377c598c44e16aacbb85392d8210b6c8d95405c..e88590904c5e6ac683fc28fa43acd580ed78e301 100644 GIT binary patch literal 24879 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 GU;H2MCZ;d| literal 24846 zcmYJb1yqyo|354uq97tlr%JbUi$NpZEe+B=8UtyNmKr6}-8e!zM>9%#OuDlU7@jD+f~TNn%AuhqS~;D4e7n4W@v?zp^> zRlf)R_}nuK20vrCs7k-NRRX76yLId7Em?`z>X6jUMsHXBE??AH{tQ0sbr{_(RXGW{ zs>dOZVgdr#thIjL3rIDX31!#Y>FS^Oh7G*mM{*DTpcMP>L^)yV=z?jC(<3=S@CB46JQ#pyGXESSHllICFGDTo#gY&_6RoYSM6KMn6X=*Tc>T(w8qPcUjxr=AB@v~e*laB1 z0msy{N(Kgo%ed$lw*Px!09_>%yrKgW+z?lk(dGePlZB@$_M)XT3PqcU-I&-hFy>U#C~Zd$lW*Ruw=;wFPAKbw8YTGGesDQ zzB9PGTZ;;_!XV;B{LKBH3U%jXilh{gMO>X!STEyAWQ}L=^;U#;m*(YVSCCYdIS$vK zEchL7Z9!{Eigu^Tie>4RE>pzC79zR0Ws#1S-V`*~%J^z(qr7~41UP}O^IHx$_3PPa z-^<3xeS6%7`Sq}_PyTgm@ZjEvK+{0WTBTw3#vLJFgKb-fY@3rmy!`yi8X5{rlo;v@ zC+45H2|p*Swj8L;u~3)gS*eT*A(>(tQ}caVZC0z&mLf*QmxjVhi_XHP(`gMe7 zW+p2y?2}WHl@kKZ18{2^EE!3tlK95brg6{CK+c6+5!q-NOb!cHAA_FgBm;y8vZH1i>i!C}(Gz(ARY z(!v=VJ`ujQb{*5K_upII-p8a!p?KVxX@>al18M(?c?7RnCus)~e$4Ma>%}8*)2qpE z5Bn%Xhtu0y!DiZP$!<$^>Cn`rW0?rA6Fr@8ZijRFcYVJsu}mN+qmz}C6Oxa-ysX6_(T^xgr? z%slf?W7InqC2=L+4J<p2H`3D7TAb3!z}n}t z+4+q*#@FpNZ(_}oWM*r7J9OoYI20S6ij1Iv9@JGf#+O{|dXQtz z>+fdV@X1L*-!8+q^%2n6+313a&fOL=OU*r6nyaUaK7*4P`)<_B`e{^7<<36;Xg`G zGu>qrU%s44Bo*ZO8~zUSIGE?MTUuO(P8FFVX(#1rA>>U}ocLaC|5uZa=zEpn+txdo znDz3J-ME^q9p>f)IFk7`!OmKi*dA_G2a~ik&(h?$xrIB2O><`noL@lqc+xK(WIEE# zoZdAs4C6YLYSHPm6TK(wZ{E(0s~hvU3TH?M?+52kL+hiOh*Tky}OI|RuG_R{;<{=UhBw6wNjE!ZyGBy`cG4nBG)eLpap z2`fr`|7n!?rEyFTovEqmA(eN;jJxgfP*@|pTf?%G(QABmYEDX!@-?%X*!+ff%M?Kw zJTf4nXXo5}FCZ`w(cOrBav=pO&AV3l%4K>zb&a5@iX(Y*H<)3_&x^PhIWvESnuPyrC*!T%#E{0Y~9tAFv9Vw6)D~83D%eA^YyyDBtkvz~oOtkAOj-`%eAfYkwpQy-|p9he^}Nvr9D_=hNOtADF(r6bQ!p zyUM^ucn(j4-ymiwH?Qucv?ZY!qX_Wp7TCOBhlQ3XcX1wLHOwVul4{Nm&)PCqb4TKd zOp_dxY^++a1&%Uh?@RS&#b?6B6I7>g8yx>VFwSZG=ZpM8n?sGP5pS7^soDhbeS`a5 zCJ*t2Xl)8}ra%?6`tZii`r$0k6EI4t=!aV*a#TXt=a%n(QVtRO;o@K}W~hDE?LjZP za!ut?^&siK0r|K)W5xjk1WTE^6>D|VtsRpXUt4{Qkg9ns2x;UjVfccDzsm$3ll>;j zN2}F7yNIWsMObV<%?Cg}&JyQtZx%U(V1C(u`Y@~(XAmv2QEmex1>>1h1dE0-{EaXEud zj$P~H83hNKh-!k3NF6J%#%$-lPCS}wz#Pg;K3|aP^o#6}NfnQ9ySWJ_!l6Lc-w(s4 z+9RJS4+g_FTxb(t|DOG8?(jvm(UZdJ-h5qK{w3d$XKi*U z{CJ$e3wk3geZ9r=(c89l2_vQRs!IvNCxw0xde_U=#myMn{ZC3EWS0n(w=2$fs`|~{ zzvesqANU>GK1_yd)M9*CuMju-5xpB_CML#iF|f;m^7TZ`59|-+@gETk5JY4H)5TCytjE;d3wvnhXSTY z-f|$C`DDp(DI6 zHS=$N*wDOF%KSLi!aS7R-efM6Ln%KD_6y$kFwR${>16bum1EbqwC+tI`MnSss0Xa`AEIH-M z9kVwh;>O*^liZ5mA6A?M6$m&;OBSwqUT@qIJwP^R{=62mm~H;ca(F)Y5z@jsN+q&$ zbi4nrt03cCFg6?8=JLPu_!67_`|>lQFB==>%1u(9obQxOqaMUIx&BUCi08Sjs|#+L zi(6>b^Yh7xE0Wk|=!BJx+A(0(ZwJ+S_ajT1?rN48&s)YVHIo{kL# zR9|Uk8AK))1*~l3zMm`F*xWInWR~iP4ny_V(Jb=qUha4p^V2u4I=lA}=%>lWFbMBN zq(EA-vP!l$)_RNIDM#D#@n-Nz{5sBZ8iS@vL`4Wcff-)+ibn0ue9qmUqx|k7-pXqB z2pFNfn?p8=Y2OiTB;({ool>rEmtpq}*FLIUuCt5W z<7?^)_6aS~Nb?WIhVhHJU;ZF5V)&UxFOAzlgNdu5p; z(s3u#@<35Y)sxoPcu~&eWGhck$yspu%qDiNf{uDsh3jS3P6k$KZd92gR;Ai&;<0d& zaM?4yAWI8|v(wxJ=uv7Z*k@XFCuj(Nb(dBYe0c0+J_9Z|pu?OkCMqYAAWobL_L2DDzwVWc75Q6(uwv|Woe3ncCpUnxr@;R+$7`_>tvTbgI0}K`~@Eh?(4uU zh;>;pd;1nm=S9}Ok|8(UtNS5z8v&B6id~%bmCzns27vlY);;@iL>F9z18eLqtsxM> zT;Z1&vm{yu(IV4&o|~ETQaZ$$%7R1t!d_MBjOpiZUpblcVz4x9&P2PlWD(^w8i{=a za9CZPw-9}-L>x;MT}QK6L9YglT#eL3ieQL|K%@Zt!G33wi|7@0`un~F3QSf`yIjbA z{@|NAX6e2NX+RPjC7n*LPLPHZ(|L`ifq|bUH6k`Vagr z7qKTHZQieDtUM&`nEUz^8SAT$sMX=j(yU?96%V!ROwbh?US!D~AJT%r!@*7Iu%B6& zNTHl)a%(X(-MH4jb*X13l3!4QWsaq42x@ZO*{2Ze#)4Bi>M|=ytDWCh|3=uEG~qCy z&|x|hUidy|1z_@*mzmV9(N^z~d_2D69z$7|SjrP+OYEmVFg2CYpZQM^T zeVnvVejhibXZU7oP=<2jZTgx=p;8Ig>i2NV6E8z^RhHR2 zl$(NtWZx@qe5&^jN?J9K&0hklOKkXTW{plncL(b!R7QxxzKBb)$8adiyif?6JrCAP z&j#T6?QL}^{jZp5R-Ha4cNxSY* zYSK|rwS^ViYh7ua$EEF<3F_|ofIKE0>C=t7Cr{b%T=t~jXuwAvAvuWFY#o+|gQ#0- zM!&QX#1Y+AUpNC8s`#hcwbr1uRPI}>FmQ^6C8WU(g+hoz%{VPtVt12;>~e$g@SGOv zIoURF*Q^|nHB>WGo)_Nv#l_Ou<=hb!7$>xBK4MMCW5pfMYowL&ay;OD@ftBYgB@xj zX(kJYQG$m{tcHX-Yr=zY1G|h&n-ekHX$a<`6=C2T9nFOklDPfVryn4sz9_|4fI5bE zCo)lPiFd}PU%tKdRqDwPjjD`2xqQdZ-ks_{Bh9WG%6899EGG`2$YNSTLxTUu2Wei; zmRwE?R#py=jqxfdD13QiJb(PjV%C57RPI6HPTr2wL|y_h>c~%?ry>9T_=v5dA8W9s zB}097HZGF7sw}w?TG|EV0Cj$5YKnWXF8zcLGjy>|hL`8H|M-gSK@Bz=@BeYb(aFhI z8XA0=j(2*8=07A^x91M*gp~@bSd4~ zl)B;A=;2c2b;#$J#heH`3)>p+4Cs!Ndt4W+!c;Fbs zhR(~4B(2Hn{`pfvw5Tk!5kz|FRc2@wo3Tz6}e zbQ<(9V8%}+Gh65v;{5JbQ(L|~ky3xARpVt?;UZuWmz!?K>yn)~46q1>%D}3fHeO2k zI{%Bw8x0f(iGCuv&vjpX+GpYh3U9^ShA|YWnM!D zirC&{GP&WYY_D%Fltj8y`~Iyh-{PgsHl-AE5BFJ3;HYD0`m9=HD8(Qc z7(;(mFwo_hDnhz$sAWP9v8Px-`n`qRhzAaN90Ns6y?-yXD z!B;laVEneX&o|==F)Vz7+GSV35pFi`_mw_xy!C`a9wR!YgJ&;55)uDTh-8HPpZUjK;3fkRc!QuAR=lZC z#M}G!1<`Frj=YD{u^c7Z{Ul0M<~di_f1=4UD2jhRKkbh6spg5<1VwT&%J0!?gb!YQ zvnVmXAOv02m&=(wdQ?J|0TOK8_ympy$4JstL@axnWxGd|NrwrVtL6-OY`e0!z}NVG zd+Q=w5`JZCXt=TK)lBRU()i!Ao0;3#0MG?RqRh)@*IDr<56h#Kd)aTb{376@l$P4D z2jXrK6D5A66X!kjskmT*FkQ0`RCMxEuwD!vp0ldmPZGdZDgPc@JXtpCLhU?xJ6EOC zE{^E$>guZly@Vn*-baxVQx(Y0O;Z!OYt5D=4D7C&G&4(Sl8j)+S$b)V4goN65ePuV zZmWDX$#eN!KjrLQLEZJ0YAehW;pW1lDId%8#@_lN+f5cjLGMdxAT_98M5XP1*lw?S z$lC;F$d)(CNbnsKVUOX%7M_ZSs5C^fCp$zevSaA&TWUS_0XWL8IODaQHGl7uCmd9O zPd*rYXEXr!g35iHP^n$I`(z$0O#TFKeClovp?$BioTKofrfb+{{&=PLA!SfhjBKy} zUoV$fF6t!5=qcYfmk2=j^EN4~hA>|}w{~H#A1XqPbc&##V;m3vpL+{~Yk^ zALYhz{y)}_^)4MLBiKyNhgwkMdbA8pE*lT?hKTq<#-s-PV(g2zCMRm7=RL z2zo?S_2`@BVKz@Rq=^%gu`kl2lCWo+hDbf5mO)4OVx7vH_E%wtIA z=sfonHm9r)H{bDJY!71p{&lld`hbaZaa`1BY9p)+(qdzR%D~E{WBj?*vaHtxboCG6 z9HJXSOatLE&TjUbR48nz`8~f(uhD@oE%<-@dzRCfW>e`^zL`{$#S z>G;@%eXS$4)BdIPOV3lg?)xu)#-;D70{Z4OrQA>FW))PW57zSak68RW)gj+q>&g zM)>ZHRexDD%$v3L0&oFXDX+|?0DPJB5sFw1tTRmH_Qg(Lqit|dm2F8WqxyV6^%ilK zfp>B26}?+*NoPRT6spd?X~3LUi^&L&j2q` zp7wSWcXNxVk7v%!HF}Ar+Zz=3?0yBxAPl?YCz8in@B_jVVO2q?i~WHP_|RSv`rgzF zqNbN%ziPw#z3v91`n~vR=h%OO@_9GwkHN14uVbPgWSsRj*LO+}TbQf9wzI+&_0Q&> z%I)T#Gi|3?m6{-h}j64M3sLg#*@q``*!K8r{Q?+zSo2^av99pT4U&xBUnbF>w{?^t&BTXP3~x%(@JW`TjMNohDSjZOCwhcFd$ zAODECUzOHbW7pQp+6RE$?(P0lb>ZF98sd1swk5}bSO(~6-d|BS78&>B10VKw;rAkG zu7)!WqN3bOmu=SY(=%~{Mc{BjHv73JT>mqEMk~y5*VL!Wqm(};&TLl8LiL>|4$f6| zu-Wu^x5ZoorW?M__T4R_AS8a6jHq}hm?pQR)YjHEwT>+cqyQfk!wWxWX7}r${%&u8 z=I$z{AH3YLYL3OGnUr=@V1LF+DRVZpM(44){^kt8Hm14aub^}|4%c}P`hF~ZLP*RX z^9N_SITG6Z@U%O6$H{wR3w_-`A$p40`O5My5NAF_MTC5O)!Lbk?~w>FCtCK|ql=&t zKhr+`M9dx&!1_JU{@1ZK8hF|(HKuZxWAP*uI) zO}InF^&DQTfv#S^tAX2UIl*%cWdl+NwFuFwS7>j5r~XzGF6g zOC>n8L=ix&@6Cux@$P%LxZdG4pmfPXZ=_W}TP_A&8M>YSJ>eXks6J^RD$Q<;-~My5 zqHkyap*6iViJ-{%#9s`WmUMXR1^q|r`RV3}-o%IUexQ?Oc@^4qH{q{PSZJuB($OKI zyRf7vrHQk1=<2Eks=DaJrI#v4S8AnJ`@;;g`z8#Vuzfjyvk1j4k0$f41-*w_FWthx z*!G!f7%4&8@PMHVuzE__8{^hRCQT+e!N(mqfd>|}0FJ+Lx+&lJwI~S`GHmMk&<-@! zTjljzEORKelmKqERx!_LY*j-kv{bPpNmdh!YAH0qjMHkE&l;3hX`AlH|H}fbXfQ1D zaZNL%DSCPz8hW0jpCUPLLV&h6=QdLl(NkKst1FZy*AP+40F~j2&EtL!Cz3b+cOfOj z_N*M8?hA$XW!Pzi1CQf*k$}tF{C-seNlWj-3*4dV3u=BlV7hAXO;4Eb5QHY=QeVu; za<;sCH}$cr)}#=a|7rQ{^#+fP7juA7l>Q4*dDH!ZUH_Ki4D+f*>0Z6%b&RcQ6UX7w zrLEL4i3wlXQJo z1%+X6EhWGwG(Br3am}JrowhWQc5>^HQru1v>JlyaY2{SzKwXc$I<=CxX>dzJM)U8K zFB(IAx5FeaZUdpO-mc5teeT+@pwI?P9h*6s(@lrT#;%Gh+v-CJZt3fKam4JG#w#sl z5qlnn=J@s*|Hif}hJ{4a-(9t;`9Q>Fo!ATWg`Z{IJxuLWs8)12?@yuhTGRW6IwOvm z7v$-tu!MWg))(Sc{oJ_ro~qir3zi3y2ESp2oBgBNPLl5J`b}<>f9vMObUqJVbNml9 zyK*qGV!Ll%^&d!BB-#Un{b10m!B;-*wQ z+iQ6s<0%ZOnMXbkkzYjyiqly26R3VhXAf%^x${jmehUipaU*Lz2lTqbh zgrU2$*pd~{8-7?YKniv~3G50yYmRTdntP4=f*(|&_pee}a{zR0U_+a1M4_a^E*#Q$ zfvsZ`69=3Ar>3escY4fXe%Jc-o`vnOH9VZW^YZF#m;80{{A7r#MdTNZdF!`2X%zMS z-DBTR>Sr|3+R4yVk}N9R2~Zm;@6N`LN*9tSzfBBKLp(PaQD;*#iI6sd+iv7Ct{-JOwx^YuD|4H(6zXB|2^EZfT%UWJX1aH{40}( zo4cbHio*{z9Az@YY*KCv(kDcDmS}exRWjd-6Bxl%8u&dm+F{NuwdsrV=dAl@>>loh zVj>t8Cc2>Zd@=)cDRB&#DwXn)msEm_?@0qI2opjp+%^<|f&s*U@~9X^`u`J2+$hX% zNOT@3fqX&)zJDIJd0;a8lDm;ucGCBE-zGGq+UqmW zz*Z7{{sRpNpR!rK ze~nM-A(47EYgdRl=urPbIQ~cVC%fVikiwr;-KqNZPcx)NYCm$qd$`1x zi<<~Xv?&SjVqDJdutBefb8jCTQke$k6_l9wP3XQl*fhG-iz8Z^J|z4seny6xZ@ROs zn!VxK66JIFS>m|{?q-}h{K|rKF8~vjebqJlBAb+4As`fNrcWkdgi+|Y1CZkVgA~+M zR|j58gN%({vi$W)Upi3&QvIgktCZy@EoqT8*U%vGbBwA9*dhMBj-$c;GQRjizn5mh z>yt`BAOmFB&d5S9S1fOJw40??6@Yr4lmT!od=zlU9IZJ@i8PA#boH>Ad$rZKh5Im8 zD>n)I4r1azF;ZWUHXodJm5nl2)s(+c;n}YrLEUf1d?-_$X3_=s#*h?U*;`C!^9&kEG!;LkHoZQbo?=YpL3yxTHUl!Jx(f;_7H^y0uX zrFrSRd0`)x4g4rT05U+z!#E01dF_R`>=7@ii3L5+DYdWz%{aYTZI^nUV_n@{h-(EaK35WnzhaAu8dZqvkhDe#?Y z%|fav$(AF?lq)F6WNNB@KtWs1xOUt1TG7SD@r04`9$GH}$KJ^FBAZYs<48YUmQ=gY zC|HFmw;F!AUl{V-%_uh>n=0A?*w}-flHzU)d8lVP2nG}-Fj#(JGjyIRJP`$UJNN%Q zHlQay2zL1is(Wj=k?<$;z(2iD!Y$huY0IXgcRonsV=}1Ml`$)V7OIg-#>t+lGS()4c%JQ~>s$l16R+lJ=QW^PFPG_!Z%_I^^z1u?Rjq zd}1m``L3sJYoAY3J0=I&Kf4V!`499D0TLW&1COBv*%k>~B?aSj;dfSj#c-LrpYW^t z$KhWRmssCMCi;N5*8hbgDmXmTxPVX-e&J#vHuJGdU{phN0SBBUda(j-(Ygoq_h_hRsevB&!rl? z2$fkr1~d}MD;985&cG`g8+W)j%TwWIv9cI0V*yO#Da!gyCgn;`3f;G68xMKO3xB@+ z7LDfhio9Fdthp!}7>WTr(Gc3_7^Zt{JtAzP4&#aMz${VT-k#oiiF+3!Q&uOP`~H6% z{`VCOaQLFH3fgWIz)Lf`PEVa-f50OocXD8~oOH98j*N(Vz$Cs%PX=xjj&@^C;=z~{=Wu-fiEk*jzGGS=e@`qBPG&z7+tPcBl}dmSc`-NB$aqrmj5=oGJz%`m z4Aa#lc#7GgLLsW?2*O9zrs#bWsG%lGic_REO+fXb!(_NKBgDXAB}LFbIrp3+;H#GZ zC8j#l&A>+?BqQua<%-LjW0$x4zYHgVZ)mDT(%Zb!T?Vlf1~mD#=*70Isc1H`Vtg`q z;?dSz(8QxP7zN(nVS*8oIflJn+_|0d;Gj*^>7@Cp6sNcT{~{mvIg)e`0lsc=P*n3> za_HIqyt3L2_56*c)Y&~C_Yt1Zh%d5H-2wVi+tME|u(??!x;QD}B1opvw>5@!O}~X4 zGn+mR)#wGgZUwF}S55r5>vzlQF{eMW0Ubo(sVtUuu`~d>%2^M^{Oe!Hlk1Bep5^Pn ze-q;fO=?;fv3jPct3^hDnn(ddi81ZHd&>=vV&AL6{D*xu<=S+Z>Z7%3>`}EzTH49P zId?-nZ`zCv8w-Pd#tV*&xFvvA?w!GVhzyKyz*N6-OQ1Zu^-bm{fZ*s$W}`3pj}Xyw zcfDm{K?pqhVAV3+l-3PxoE(j1(&r?TyhLtz_uNXq`PKZhW>M_-eAI415`D8}MLI={ z(P;h6>3xTL0{lth8$zd$TBx?@0yk<;Ub^fJ0XWy=g4E?|$ekNktZMxyj%@P!?{LoV zU~ED?5$Dy1Pf0L?Bt)rC6=~Zf`Q~GPxTK zPMyig_XZZ*k_=s8EV>P%({C1HsFO{=>BD-$Av}m@eS*JVbYb$d-Ml{N@BdM| zV&%*d?a`~UyJs&}8E=;)*(Z6jwdKyDq^P8+UB^v^z`T3omR8IkIy;Eh=l?BL2zifp zPa-r)LStdCICTt24Oz$xm$#+ntSbNaj@^iW1HOxT<}<#KDBqnQB<~r1ZUWw1pI?N@ zK}!u_;6JHfTY!m;lJ4#5`5U?8gWFe)t}L>Cp?LTr{)5FeXdmT&7HFZ(Q@a`M-qy4E zex_!C{H~FV;-8%pDRbgCkMyk)+*-_A4-zOnKTHU}FCA{ zdkvLCG5wEVT{x)2qMXwR9+dN`k?;A*t&acb@^2h0O~3!7Ii$GN?+l0*2harjK-{O> zu%opVm#a@7`!d#FbTA>MW>viubI{jVgGjaX6Taw)Zn%RFmD{?rR zX}g+~7!=-Zpr5w@JTE3y^CSvr=qF}?OvdXetww#}Nxr(|ypFnQubFZ1FwzrIQT2RM zC)ZW%F)O9vW{39j#n!;?@t>%)dZsafQR?D{T<}|8C-;r-4tM+9+WNCh&5l@&7`7ep zPQE*B%g*O|-ZwcpdAg;S)nmdXIZ2`&P&Rw;5D1|YwknX!C@bswi!8?UQ*sH@Dvujw zi%+$&*-+6GEZY}$zm#AYdLc{&5LdiN7&a^t2<|iq%@wPMwt<0x)iVwH|5FGkU^q48 z_#Rwnr9r&(oS%M2UYD#jo?)Ahy3gnlZ+LZUMdF&h`*l)zA9+o-6NOy%Dp(EUDQWXW+gm zE*HLET7Q#E8AALv9!H?g<$z~UdsUBsKM5qz*Jd`RZpdXpCYDQ4Nil1ScTdY(_HrNf zF7<*sTgewnabO{ClOf~~M;RYbd8;d%|t`;t@`naF_XJrMBxU%#_itJ$Fg-9evuPF~L7B9tYVl_rW zj(dBGf|h|=@WxR%&BXqgJl%Vh(|7#s@bH}6$+kBHMY7|Jf*WNQ6eK$el0*DJhP4-+ z(i7#6ze~i;GN*C7?;a%W2Vl0`8}%|E@7?beQTl0P6FDu<%^a~!%#$u*MHYX<4dm~o}t9h&e{*mYN zgH!&Ilc=~9J>8+22qGMj+!<2$W_q9z6uCHL?#!+^x>V|%6~3?8LSMGUf9Mm2Q^{iu zsg{kWJw>P`Yi_Q-Fr%BAnG9lghk5w1X2W)K;?IGU`+>9>ZXJ*w&3P5lP^4%VfS%(c zU+nEaC0Nkz>x8gWLI-3k9!@$^H#>fBILCX)60R+v%~7<)izrbzVD!J%N?KoVS765R z_|N?o;}jyq`3y+4^zYMIJxBK;-&8mEUvCqSR!EO`Zm2W44YX{y|3|4GMC8b9l&S2M zjA%=>+|8Z)j6GRf%eqVLBE(`(-qTL!|ioVAZXI%1IW zhMckihLWVU5IB3`Q4fntggGtafxtwUJRDZT|Kl>jS5v_k-mS$fxpZj_ zUH{dhQe6H3INVM@_pnHl9$-<=O8}+F^xxJDe-xS(a!SeRyDah`^(95OXQ}+&8qLG~ zuca#6v*UNk8{FsHegYjZQfj!S6DJUpvn!C7s;17pWbl`Aqo+vbSB1QDyvaQskOOVF zNWCzM4JFo#q83~2NX!HY7{KJ@v~;j-pPo>AP1LS*r@DPcwk|q;33|DYG3UL(?{|Dm zU;Eod`ls$MzIJq|f;a$35sgx3Yf3jS2iV!c!_oHgq=R$j<5aYKUbMW`0``u+^GlC( z7qxx8Z67;}P>|MokO87@j0*8!GGeBL`Z#jUfyDbS28-?S<%=T*8S6U@x9`7%Md}J& zNR&kP2~_jxvOzFF$Q@@0=44T}0^M~+^WKC6;`w`mr~8LIbKRO}02=-Rx`yjB-eJgmUn0uB%-Pf9N_5}QNHsLm&qa4BD<#y$%RW8 z6hD?p@cX=sXw?ZKB|v6<_Nqy74pAT0MRRs9-|W-yo>Syuvu2!I8)7-i$BMK12=DzN zn>Feq$f;k2{-WzS!}4gcm1*izvi!*i5_qXcOrUXZJZ4doRe}AHAGAdtbemtf%BRCTMg)g5X9R(LS;*V}BQ58;aju zO&%k7JfR`X=%InV2*b7wh%{Pnt7xsKqKnvVFbTX3LLYh#unCGZ^bMB6sY?uhD_|P#cZ$chw?E zLo>}B=2}^a7lU0~7b65fzLVO2>?lUM%i1;%MKOPFzbzv)h}i^G#1A$bh+*mx_-A%% z#}@@#allG30gOpO6^I0@|4cZ2lfhtM+oSFdqd9D5O7bk;K?-J%Qaz8hL^c?2-OsAK z?b$TQ5NT>N3yaWNKWAuKZF6FNzBO-IX8FhDX)kqnzMRFMC`gON-f^18auchut1uP~Ib zeESewDgGZKLOUp_LxrFQKMCxu-KBd6KCT)~iUQdB3fL%K(hm&NLSxZ_kkD`rprHX( zVDd#&y9?FnRX1jzwwp}Z){9kcINOq%v`uX!N!1k-4K2}Y!3s7Ho>M!@^4`Q{Cz%2C zr{L-dDCaFvZzLbubt4^Q;tw}mf=v+Z#Xk$e?}7~$ClK?Jk58;${z3OA488*>`v{Ic z%ib20_weR@Q`u95H~|N~9rPYe0f9|!hs^w;yhlaueudsGcz6*$&4A%aa}8)pIJ^!F zL&xq_fn(roBqkys`Hzq>f;WGe_Wt{7`!(+MLzR&40@!H;ySma%g=Eb59Yd(QR6C`> z>+ViQ`-FQ|Ic8(a^b< ze71lJ2pBD!Zr_`tU}goB`kmF=Cu96sAJ&~65L;73pFjd$s>X>wV^%^di{;|~k!m2s zWDeh{2}Rd9tz1x5qk)l>QB(5l#5sUk^>2b+-c{wzCu$0M-$l`o#O8_&t=jMK2t%nT z!%n-t{3#e-@v{my>h>>eRfyJ8`>QObf!PoT9 zAxznw24ntw?KH6M%NtK@_LcB2ctGYrIi7)-!mA%PAR}*#&d5VPqT3ak1?ETff^VS9zHjHW$T!y_hH|WP>j9F4E&4(B6?%sTGDk)TGuF5p z>}bAulQ3PCvK|5tcYb6L$g<;|dLs(D2UuJ!yhcEl_!F-R?NaLBX;2`Z?>0d>jL=kD zmmRpqL4P$)MtNpCS&2hnEf698?6HIILVlv9Q2hEM*G7&cB1I-U8dr(U{>?yl`N%3T(WJ0^7;${S8UYr7RYw3Xcwbh% zZTr|@`XD<37`vy~)FrgB%+b7DO@UZ>9<>4vI;1!gXoH$(O=ryxX<1Hj+ygVJ;p`6N zH)wQO8FTnYYDbp%S2NaCcG!k5!}f|cE?CfgFJO+)g>I>=2-7fUduy$CqX+a$?DfN@ z1|i`H84(yD_4z+U-ngpSg5-ZPHDnuCh^QvJktoolt5{%k_?iMhII9RvoW6?N?BX+u z6~Lka5?x7-S))Tv9=Iqv1U!QHOhiO9P@tv~=lx8}N!ka_xe|FNO=_HsOr$i|w0vkWN*IjVf7_2FincY zht{}alW!zxkZ7(uZe+jYSW{SJIz+h?%r>{##LhW?!;4P+)Mo#e1we%d#pnO0 z87!k^0N`s0yY#B?0#Hl|Le@iJ|Dtk5mORIy?5q>~(~7Zj3^KW~u#0VRCp(Lg8AD~8 znW9S&%Z^6tXA~BMPt@GO(whW6JOl$IoSy=RznS}fY0{{BX#JFp&vsI?{vqyP&^r$H zM|PYE!l9Y~M;aC_0sndb?rapgL(tt~B;`nyi!8AbDk(;U5BW$Kg?H*z2HgA^hNS;m zy}fb9xe-pl&_}mre^%wSn|lY^*MZ_m&(aY5L8PNtWXEYq?sI)4I1g;=BOB7d0qolW^=l8d}T2i`1{zCBK|nNOtn z`6wp5+dafB!)6gU>(pMmm=e!bY()&NX_1pJ)_0>N)2)B4-JSf!dFrE?0dkKc)5Cb( zMV7L-jC%nFTVi$TzG8nKTCk|Bnu4%~)#E)K!}Fk>7mlm8Jx!C~g1pI~x7Y)=qD-nc zqpjI(9f!x9uHnLV7Z~{;R1gmRg+eP$J z_S(PlWY-e9&^+eSWH~9<3~fIf^(okSe)V;wHs)}0FV@f}>AT`*93Xv545;R`;%!H+ zB#|a~hOz{8aZ)9L{0)K>w={5cjnEX_k;L2KGY$vfftWT^?Cdl> z&plW1{SylmW|VPS_3YiYa~p_r2tiw@`%Q<@$FIxcH~9o0AJD|lBUvc2fmZZDo;K8@ zb=9$B2P@5c=(f}*k)C2v^+4%dRkan=;n^wZ!v?vl+40V78t)cQ1G~U*W+idV_AU+) z(eua)Oyx!=3h}Ecin(Q|9c6#7zfgr;uED$(fzprEPDBQT(Uw>X2J&3nvL0#qI~8y0 zUfJ4ERDlH`>?BHMS+nIenLN2{kgKTUKU&7LoSyVZI?HWtR!*s6E=!9H4u--9ZxQVJ&jNt z)ZV8UAWRf2K?^%Q0-2jm9TRoAC_W*8v8d+giem{Mo>M^{340^W7&MWZ491_e z0Z@EgY`hU6uy;y}|7k+>e8}NR)ZoBDjN2SZBZH=3M5zjrEB&|sAAu1!vbKG_5}(b} z`#VD9=|GDodjtjp5lVppzSg5J<%q4pe!K<*6-JAu@fk>*Vr?Mm5YLcEU zjX@8U^$_0>v3zYH5P+peoS-^3&X@IZ-t+Pqy2}IfGnlls*;?5y%<73F3ptN0q#J}s z=k>q0Ext$~;$OZlX)761%mH9NL7pM4*|>DMY0$Zd}o7E*#>`*N#5iO zUcPz?Wa9CT7EVyBialr}sdAX8M$yxVba*>7CY3fF7N;WMLX}Xi4L@RE=>C4l1hu7K zOX0@dmD<@lU@gT_SpA0j>_WX{WQ6I(ZPLC}&mzW0K6MH;O4%s0 z19Dz!J5@o+>G@yKnKioTtPtlYf+Hd?Fn6STj%py;)ewf#yQANsdfr|6fzr z9S?Q<|L>41>qwDJ_KFA@hwO}sgp5PR3E7*_ppcc-87Ioh9^uSFnOVu+TgQ>Tec$K% z{XHJPf8FQ2KcCm@{dv7#BCm_D&LfnB~Le=JFnm_|t0)?W|7lj>nWtKy;o-z@HX;;?vlH>rX=r1_8>vn5=ywx@0 zUXmKSu6bK>b>jgAr4eB18z;N-`dn~n1VS}xs}5PkZShp6Jq92apG6l zYL{O(=y2#yGJDt}n*XXb2+j1r-2kSM|SdU za8T5JX|kW3dI` zFy5cpoG#lA_J`y5(%i1RLw)W!z3AXI+HRziCgLw3Qp-J`v08xB))W4zIT3DpxSk%045Y*Fs$o~?Pdm) zVuzH9o!3@I(+XAg{mZfzwD0qZ6GcYXH+O|dePEUA7{H0d)p#V%he%mEks@Sihuc21 z>(->7Fl~p^D=Z=`2tu#k6j9DF$vo7bGbRL4 z1IQ0lKr%%}MIxmtLqeqz5AnFG)&a2iFH-t|GJ?-S-N2T_*#WtHBQ6rK1rL(ak15Ci z#~37<xsZ2I;h|Fv}5g}CR-iWa_;V!^jI2SyK*VA{dW4Ln8dy`W1 zG`pkT6tIo{u4}HbcPst?T3o~wGquz5|EGSurmUuttS)<)qfNg< zl8(Vj69STb!_4~=p@{ykuz-TpRC(tTwy(2$Ez)$?smkxS>zUnm?hP>ndJNTbCM@*n#s|*d*vL)SjLq z*5P%jDuzXqdo{ybvw^H0@t>t$w?&{z~w`fl_VAP8A%zsAw5X*&u+V)Nc7c5}pV;l8DzMNHXZ~U6z;yFoN!R#NO!= z#}+ZtOp3h`_X02%*<&jh(x`+*-EIn=UMhA*4zX^4dS6H|wNP6capKv%LusT4v>@V} z9OBnvE&G#h+>YElmrm%%Y_7I(Uvc@n$z7#8=&`f?!M(9b&^B?u;Ri8NnoWB2K2M}h z_MH$yg}jiD8ze>s47xKoJSWZwB`sL1%DnP=y#{uH%Ut0C+2ziS_u`0>Pf4U(YHFD{ zoiwVQ%TrZHzP$&ga2eUT!v->*qMxN;FfC*pU{=MtF^RcRwF#Q)3GdS`A(H_fop1o$WUD4KxL2V^k0hD(N2 z#j@aaBuIMzZGifXCxEAMySTV&Lh1GN7N_YoFx>waY}h=)t)3j#PV!XP9}$swZ|D!Q zw;l$6FjT@}5_TLJMj##h&M+8y^l;OGG3WPPW7%4$2AAVGndn}H+dzj`)&!hlk)Sa# zOCTGt%KlLn$=xU563!Jy79=DS&Tod*WTdF#h&Xf;xXyd_!D$`>^dQ>Ne`iWMEH{T= zA1Akf5)%#JHdiGCCNCM1nv5iln64p~Z%B8n5Sj#i6sI^pS4pnM?2>1Mv4Ayz_Oo|~ z8DNUY;RIwq`Sv^jI;6Z%L`>@oyhywEUX!Qf_a)nGUaZ?fQkV99Fe)}43zp2T6RWdY zsz(Nc9YrXE1q~z3!HG3fvib<{!y(F{OAd=Lx?M&3v$KcoB&S!HSiJ(L3NbLu(aZIR z3%LHCrT^i2&5wWAuGRr_8EzKVSs+@miJ;WAQE4y97Y{s~)+3#BQ13UQM^M9kTb? z4KP7SbdiAOu|3<_;sIzLJObf)xZqZiL(uX-5n3L1SM}-ZKiUSegF4^w!o!gst+v|H zpjT&&_3jaob2UBt-nOWuM9-(IZwG!kQS|W}_uH$+R&ui^{E8sslFZ~OvXXcnHgQd_ z4`xD5vdzsOe&u-#MMdC>mdUoRm?P(nC!`mi=yKV_`Nb4`Ote;U$sE?5IIa za^T|ri%qiFTJhQ=mT&vpV`*Qzqjryb&bqvBtaOoH)QMOK{B_mc=U~_s0)_&TVqxLl z^Mea5zTcWrQ*Tq<;+cCQweMXvOuoP1DKrf|b3LPeznyP5BkcIiRKPun zg0DW+yyi!i%YUMFy+|U8P)*U#1=Eq?xPZo=KNU9Z1!IM8m$pg?NaQMguF~N8+%+!u zCNd?n9FwR2*Uf&()?%_6dh|R0TY_%gqv2o&`1+>WYbOVVgHiJ%wv9|hb@ex6zJ@Y8 z9y~NtIgG|a^W*gxZqXE}2e-ss-`u84d_bc#wfeO|Go|1-9M{>5x;N9?8{Fu0-=^DI z3|%Kv`HVJg0>;i^c}+5V)34xPq&D&`Z8`n6;@O>Ir}kk%@KjPaqJwU)gfPtDy{@_k zaK_Qks;pipv_-&ZNiMlv9TB4(PK1tCLNsni#pNExw*Q&!>$_POFp#!$ybw?0AMi90;!k#3firT0)EL{ZXk7lkM<;VNmtLzA3zYWxxTts3ncG087;3+6-49=`8CV+zpoE;FX?wqm2w_8Ee7gPpBu~f69A_-1AZ6DGs$V zgPGPygvGPU*+IjMQ6p_aEKJ#!vzrMHlWTNZMRUjd9TLT#cBj_YR({Sb!7~ciptqwa z&F(8}t%U6dmj3&LQ}nMr@>BOe-r*2rtrdmNTu64jQzR=N+y2#-V_R5{^7Y@`bDK{q zvd2-@=^*K~T7Qn4LKRM1rScEqLqp}T%a_g911OGm@pl9kZg36>L>JR;o;dvU#zU6W z>QxNB+`Bd=!#*}Pfs>JW|8RZih@fA2KSB7QDHnY=|*#U9&B zT!|5lJlbD4Gi@F5p4?Nrc-rVEfi2EqiuRTd?Fl@QK7`|2E1$7Y8}&Z`H&cN$HiTkTbcNIae;;L|GxXa zmGwS}Ek2a4bqv_O$s>D8(Zlt*p|Hh@o@Zd)T?R#b{m~G?EeSYzA`DjgG8i%%3wt_} zXZ14&ZpQ^72e%5P(gRK&RX9(4sk6?=g1l83ud-QpkcY3Wc|-U)Bps9YQoL-}XV5`@ zv}YUXXW^$DQNg&ptS3}vlQ&0 zS|uMXcis#8-ZF7v? z?7Ty9X8m)A>wDL|AQ#s!aFF1qj+m*57WfULqmD=Yg@kE!rwsZ^$!P^X9F${l^gX!) zJN6TD)99&G0BZZO&LAO(TcNF|2*DYyJX)C)1ZUusUI$cvh%qs6r#w2BUPdZ#K zn{(*(Lw=UzY^$Yiv*FU`u*2P*Z?2B9<@W9vVSBg2*2U+3FInTOl{yUr{qRf&5}^TvE$UV>l)3!Vm( zcnF8YW4=GUzmrky%ocV0!&33$_~|B{+D{R}E4U0^x&^_Zzl$_l8lT1Sp9@;Xi}~KD z)on2lk-r-tSljaHcs$oCFGG>B4<5e(S%9i|V7I zBJ&~c{v8mN?CgILnuS?|1H@h^gfmrRDm6T;4&uOgi=)JKj!pwIrLaka^!rz``98?G z)3$E!RHRN=u%4zMep1AK=XjY@Hm3hKb;d}~r5d*IBm?$+EZ0DlG*-WB2BsdZtjL}s z*i85AQKX@@TRk?Lb)$D0pxb_|bHo~U-w}DNnH#$a;>A$W)VEL3t1Sor+I^!7leU6` zHb2b`m&|+*9X3g3bvYJdAvdchrUvvH`+8@}IeN6ILlK$~+ZY%QUXBYDJoRyzqm$dQ zW@BEN7BN4(5f!j5%OT>MzQmm$>pTtX2d=Ead##4;%XhlkwaBJ(lzeLwzO&;@B(HEv zBfxDEQ|j-nZ!b~7BWw9aUiVr&=E=^^hF+|wat@(xIw}6{ z)s%d6)M&Q-(z)R|4be+k%Rd?sA zD#|=Mm$m8~Lp7{Muq zl@uE2QRsLu1Xd=EeixMoaYv0Bdk0*Pi|fGc1}npaUyD5!!`{925oXF;DNI|;EDKev zX()cz?U${WqCT_+h3~R$GwJfhamS^+hWdT;v>21@J7|G^lU=auwl{G%&D3MMGqgZB zMQ^x->xjv5iD7Gm>eJ6=HQ_$b<-9DFCVE!t>i_zv@JKNhkAT47KMK&_fjw`vixA{H zThU?zp4-7g9U!EFwKJ%UT?Y8o9U|^qyK!zL%T3RS=Kv;#5?ZKje$hIZ)rE4zRoi1{ zm-wQPw6QW_ZJnWcLb{XzvLz=y(q-sSOzTfx{or=G`7d)c38T3+_TUwkT8h!kI#G*f zhlgor!^VwFg}M&QFM%v`P(a5ql0@YgL%QhP0o&fIgpBbeOqi=v`Fb3#x!-{*LJ%>X zP-+FUMI;rr0g13yu;gPORo0TAUZTqj9I@@&!A#MzwQvYEA5$XWpo;x_qwQpX93-X! zG87%w?elb&z)}f5^T!{^L`Pnh+xNWFe8ywePi#<*5sc)EZ-3t|+yp^P3%HbiXci1| zCxU?NZbH3BtA$PA>xk!F_eS3XsEotx3l_P zOC*qqUnhF)^br{FD2|kXI@95-M&JJ77unxr-$25=CjoK_eftOpACkIYzxC*M@8(c! z0n3O)FB7!v^#T!vhNC@86P}89tYgpJy=&d_B_3&HTt?;Ue<^1Cyp2NSYs(LRXtUnb zl$&mMU%HwfYfmvgR7=0o6*`O`I_SHpuU#av&JegWZKlB!2ZbDNj7Sl|gxP5@Oz2cD zWFBl)^2Xi;8Xs*_5imU`wyX3zCC+a+ZH=qR6GXD+2h(VcFM0#0)Yw2ti?u# zIyAiHo0$#}l~wfiT#PsU|9(G#?O8RyO8JXCWh~1sJ0|~UFmGLhA}A6d)9r`*Ngh}?Ufe>lIiyw zc9ltL^gSx7IE_3Iv`2ep!t@d4%XpU8uUj0uzom}QF2OTmhDmu?re|-{+EekEnTL4;2?7WUfrJ_XN&xEk z#l)A)0KobM4ZXzRQbF6stpp%N_*CkBZWr=>`}|cgga!C-PrQG?Si$WU&?a);u3L$G#4g8@qAQRD*@;-mAf$D z6N42fxZcLs@t6~G14SM+Cfi&~4fc(LMfb~Wy*w@Z(ir8cy=7lXbG5#CCqp5jKYXSz zZD2^7F|qn?!%RmbK1SqHzL+0)0E4XaPZs{cdF$VD#Qc&i^|#fx7w12}V|H*$$J5KM zkZ(y3NgZQdrxZowc)>b0e&6&3JTaQBP57ddVDH|Sqd0U(NS1vFt0-C<(^^}LF}U;O z%SXdsTbX)!4gCb8aIi`%WNUvJ`<>0iPm-%|pO8Sf)&LwFemTr{t-;aJ@4sCj=6#nZ zF27|vI8R?dWj>y>xx!T>ors)VYmJO>LlpckfDed7)5<3la=X$rvkXk6iNF(|-XSMy z>rOnW$2>MNvNu=;<`M3WdX|}A#RhqB!{n4-ed7j6o3$I|ho$q6@SjjnNE=;U8+%
?!tO$6aIXMm4yigxp;9LBSwVA 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 literal 24153 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 S_JF)EMDvcWDq7h({C@!XHOtQc diff --git a/site/content/quickstart.md b/site/content/quickstart.md index 2238aa60f..1ce1ccf80 100644 --- a/site/content/quickstart.md +++ b/site/content/quickstart.md @@ -91,7 +91,7 @@ tool/ci.sh # shaders, analyze, every test (cd packages/flutter3d_physics && dart test) # plain Dart, no Flutter needed ``` -There are 11362 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. +There are 11368 tests across 43 packages and twelve applications, and only about thirty need a GPU: the Impeller half of the golden set. The other half renders through the software backend, so ninety-one scenes stay checkable in a headless run. ## Your own application diff --git a/site/content/reference/testing.md b/site/content/reference/testing.md index a1f34898a..ff39e76c2 100644 --- a/site/content/reference/testing.md +++ b/site/content/reference/testing.md @@ -1,17 +1,17 @@ --- -description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11362 tests need a GPU. +description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11368 tests need a GPU. --- # Testing -11362 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. +11368 tests across 43 packages and nine applications, counted the same way the `the document says how many tests there are` rule does: a scan of every `test(`/`testWidgets(` call. The rule holds `ARCHITECTURE.md` §13, the README and this page to the answer. The README went on saying 1242 across thirteen packages for as long as nothing compared it with anything. About thirty need a GPU; the [architecture](/core/architecture/) is what keeps the number that low. | Package | Tests | | Package | Tests | |---|---|---|---|---| -| `flutter3d` | 1662 | | | | +| `flutter3d` | 1664 | | | | | | | | `flutter3d_mesh` | 599 | | | | | `apps/flutter3d_modeler` | 1750 | -| `flutter3d_sim` | 700 | | `pad_input` | 67 | +| `flutter3d_sim` | 701 | | `pad_input` | 67 | | `flutter3d_lab` | 15 | | `flutter3d_core` | 773 | | `flutter3d_lti` | 26 | | `apps/flutter3d_lab_pendulum` | 7 | | | | | `apps/flutter3d_lab_incident` | 13 | @@ -20,13 +20,13 @@ description: Four independent golden sets, mutation-checking every new test, det | `flutter3d_game_racing` | 223 | | `flutter3d_webgl` | 62 | | `flutter3d_game_platformer` | 221 | | `flutter3d_hardware` | 70 | | `apps/flutter3d_demo_platformer` | 208 | | `flutter3d_impeller` | 64 | -| `flutter3d_cpu` | 385 | | `apps/flutter3d_demo_strategy` | 50 | +| `flutter3d_cpu` | 387 | | `apps/flutter3d_demo_strategy` | 50 | | `apps/flutter3d_editor` | 234 | | `apps/flutter3d_demo_arcade` | 20 | | `apps/flutter3d_demo_racing` | 162 | | `pointer_lock` | 28 | | `flutter3d_physics` | 269 | | `flutter3d_webgpu` | 193 | | `flutter3d_game_strategy` | 133 | | `flutter3d_editor_mcp` | 36 | | `apps/flutter3d_demo_river` | 54 | | `flutter3d_testing` | 45 | -| `flutter3d_editor_core` | 141 | | `flutter3d_editor_widgets` | 91 | +| `flutter3d_editor_core` | 142 | | `flutter3d_editor_widgets` | 91 | | `apps/flutter3d_demo_dungeon` | 115 | | `flutter3d_app` | 170 | | `flutter3d_game` | 325 | | `flutter3d_shaders` | 5 | | `flutter3d_particles` | 99 | | `flutter3d_stereo` | 50 | @@ -40,7 +40,7 @@ description: Four independent golden sets, mutation-checking every new test, det | `flame_flutter3d` | 140 | | `apps/flutter3d_showcase` | 88 | | `flame_multiplayer` | 9 | | `flame_multiplayer_dashwire` | 3 | -The rows sum to 11335 rather than 11362: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. +The rows sum to 11341 rather than 11368: the remaining 26 live in `packages/*/example/test`, which the count includes and this table does not, among them the two seeds a new project starts from, `packages/flutter3d_app/example` and `packages/flutter3d_game/example`. `flutter3d_samples` is not in the table and has no `test/` at all: it is test data with two path constants over it, and other packages' decoder tests are what exercise it. `flutter3d_conformance` is missing for a different reason: it is invoked as a script harness rather than through `flutter test`, so it does not surface in a grep of `test(` calls either. See below for what that cost once. diff --git a/tasks/0.9-engine-roadmap.md b/tasks/0.9-engine-roadmap.md index 2dcf226eb..2d234d05f 100644 --- a/tasks/0.9-engine-roadmap.md +++ b/tasks/0.9-engine-roadmap.md @@ -567,8 +567,10 @@ Under an orthographic camera, the shadow cascades: the depths where the view box meets the casters' bounds, capped at `viewDistance`, are cut into equal slabs, one per near cascade, each fitted to the corners of its own share of the box and the bounds (`orthographic_cascades.dart`, every triple of -fourteen planes rather than a clipped polygon); the last cascade is still -the whole scene, the reach back to the light is kept, and `ShadowFactor` +fourteen planes rather than a clipped polygon), with the slab ends and the +radii rounded onto a grid a sixteenth of their power of two so a pan or a +moving caster leaves the texel grid and a still tile's scroll alone; the +last cascade is still the whole scene, the reach back to the light is kept, and `ShadowFactor` picks by depth through that lens while the shafts and the volumetric fog pick by the way along the ray from where depth is nought, the same distance as before through a perspective lens. Tests: `orthographic_cascade_test.dart` @@ -576,9 +578,12 @@ as before through a perspective lens. Tests: `orthographic_cascade_test.dart` bottom and top of an isometric frame both land in near cascades within a factor of two; three cascades shadow what one does — and it says that no mutation of the fit makes this one fail, since a miss falls through to the -whole-level map), `orthographic_cascade_pick_test.dart` (the software pick, -both lenses), and `orthographic-shadow` in all four sets, WebGPU at 0 of -172800 pixels from Impeller, WebGL at 0.394% and the software set at 1.043%, +whole-level map; a centimetre's pan and a canopy rising five centimetres +move neither the near radii nor the slab ends), +`orthographic_cascade_pick_test.dart` (the software pick, both lenses), `orthographic_march_cascade_test.dart` (the software shafts +and fog at the edge of an orthographic frame read the near tile), and +`orthographic-shadow` in all four sets, WebGPU at 0 of +172800 pixels from Impeller, WebGL at 0.394% and the software set at 1.028%, on the edges of posts, slabs and shadows; the other ninety Impeller references held unchanged. Particles' and splats' fog and facing: the stages declare the fog's block whole (`lib/particle_fog.glsl`), so they fog by depth From 28462c3faba163b656589baf6d9789e753041000 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 20:45:24 +0300 Subject: [PATCH 6/6] fix: Take the cascade rounding step from the depth range's length The step was taken from end - start, which in doubles can come back a bit either side of the reach as start moves; at a reach that is a power of two that doubles the step and moves every slab end and near radius. The step now comes from min(far - start, reach) itself, and the power of two is found by doubling and halving instead of a logarithm, so it is exact by construction. Vector3's float corners keep the sum exact in practice, so no test reaches the old case through the engine. --- .../engine/render/orthographic_cascades.dart | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart index fe6999d57..1b04ec60c 100644 --- a/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart +++ b/packages/flutter3d_core/lib/src/engine/render/orthographic_cascades.dart @@ -68,10 +68,16 @@ List orthographicCascades({ final far = depths.fold(double.negativeInfinity, math.max); // A sliver at least, so a view that meets the bounds in a plane still has // slabs with an inside. - final end = math.max(math.min(far, start + reach), start + 1e-3); + final length = math.max(math.min(far - start, reach), 1e-3); + final end = start + length; // Both ends on a grid, so a pan or a caster that moves a little leaves the - // slabs where they were — see [_quantum]. - final step = _quantum(end - start); + // slabs where they were — see [_quantum]. The step is taken from the length + // itself, not from `end - start`, which in doubles can come back a bit + // either side of [reach] as `start` moves; at a reach that is a power of two + // that is a step twice as long. The corners come from `Vector3`'s floats, + // whose sums are exact in a double, so nothing measured here did it; taken + // from the length, it cannot. + final step = _quantum(length); final from = (start / step).floorToDouble() * step; final to = (end / step).ceilToDouble() * step; @@ -99,9 +105,21 @@ List orthographicCascades({ /// keeps a still tile from being redrawn refuses a map whose scale changed. /// On steps a sixteenth of the length's power of two the radius and the slab /// ends stay put until the fit has moved by a whole step, and a radius -/// rounded up wastes at most an eighth of the map. -double _quantum(double length) => - math.pow(2.0, (math.log(math.max(length, 1e-3)) / math.ln2).ceil()) / 16.0; +/// rounded up is at most an eighth longer than it needs to be. +/// +/// The power of two found by doubling and halving, exactly, rather than by a +/// logarithm, whose last bit at a power of two is the platform's to choose. +double _quantum(double length) { + final target = math.max(length, 1e-3); + var power = 1.0; + while (power < target) { + power *= 2.0; + } + while (power * 0.5 >= target) { + power *= 0.5; + } + return power / 16.0; +} /// [length] rounded up to its [_quantum]. double _roundUp(double length) {