From 5ac35aaf11d756c33ea8f898cda6fe49ceb7c4c9 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 19:23:38 +0300 Subject: [PATCH 1/3] feat: Split shadow cascades through an orthographic camera's box The cascades were split by distance from the eye, mostly logarithmically, which is what a perspective camera wants. Through an orthographic lens the eye is only where the camera was put along its axis, often tens of metres short of anything, so the near cascades covered air in front of the board and every shadow fell to the last cascade, the whole level. When isOrthographic reads the camera's matrix, the near cascades now share evenly the depth the camera's box and the casters' bounds have in common, since a pixel needs the same texel at every depth. The shading still picks a cascade by distance from the eye, so the thresholds are distances and each sphere is fitted to the slab of the box its threshold can send to it. Radii are rounded up an eighth of an octave at a time. The last cascade stays the whole scene and the caster reach-back is kept. The shadow node is handed the first view's aspect to build the box. Renderer.debugCascadeSplits exposes the thresholds for tests. orthographic_test.dart: the first split lies inside the casters' depth in the box, near and far posts land in near cascades of similar radius, and the eye's place along the axis moves a radius by one step at most. --- .../lib/src/engine/render/renderer.dart | 22 ++ .../engine/render/renderer_frame_nodes.dart | 6 + .../engine/render/renderer_shadow_pass.dart | 231 +++++++++++++++++- .../src/engine/render/shadow_settings.dart | 9 + .../flutter3d_cpu/test/orthographic_test.dart | 147 +++++++++++ 5 files changed, 414 insertions(+), 1 deletion(-) diff --git a/packages/flutter3d_core/lib/src/engine/render/renderer.dart b/packages/flutter3d_core/lib/src/engine/render/renderer.dart index 68a6e4cff..e075a88ec 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer.dart @@ -1256,6 +1256,18 @@ final class Renderer implements RenderServices { /// snapping's whole job is that this value *quantises* as the camera creeps, /// and a picture at any single moment cannot show the difference between a /// number that jumps and one that slides. + /// Where the near cascades end, in metres from the camera, as of the last + /// shadow pass: the distances the shading picks a cascade by. Nought past + /// the last cascade there is. + /// + /// For tests: under an orthographic camera the question worth asking is + /// whether these land inside the box the camera sees or in the air in front + /// of it, and the picture cannot say which cascade drew a shadow. + List get debugCascadeSplits => List.unmodifiable([ + _shadowCascades[0], + _shadowCascades[1], + ]); + List get debugCascadeCentres => List.unmodifiable(_shadowCascadeCentres); final List _shadowCascadeCentres = []; @@ -4282,6 +4294,16 @@ 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: switch (ordered) { + [final first, ...] => switch (_viewportPixels( + first.viewportFraction, + width, + height, + )) { + final rect => rect.width / rect.height, + }, + _ => 1.0, + }, ); // 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..0b61a518c 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: what an orthographic camera's + /// box is fitted with — `P7`. + 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_shadow_pass.dart b/packages/flutter3d_core/lib/src/engine/render/renderer_shadow_pass.dart index fa4061e20..2360be66a 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,31 @@ extension _ShadowPasses on Renderer { final radii = []; final splits = [0.0, 0.0]; - if (count > 1 && camera != null) { + final cameraViewProjection = camera?.viewProjection(aspect); + final orthographic = + count > 1 && + cameraViewProjection != null && + isOrthographic(cameraViewProjection) + ? _orthographicCascades( + viewProjection: cameraViewProjection, + eye: camera!.readWorldPosition(), + bounds: bounds, + count: count, + viewDistance: math.max(settings.viewDistance, 1.0), + ) + : null; + if (orthographic != null) { + splits + ..[0] = orthographic.splits[0] + ..[1] = orthographic.splits[1]; + for (var i = 0; i < orthographic.radii.length; i++) { + // Never wider than the whole scene: a small board in a big box is + // better served by the map that fits the board. + final fits = orthographic.radii[i] < sceneRadius; + centres.add(fits ? orthographic.centres[i] : sceneCentre.clone()); + radii.add(fits ? orthographic.radii[i] : sceneRadius); + } + } else if (count > 1 && camera != null) { final eyeAt = camera.readWorldPosition(); final forward = camera.readForward(); final near = 1.0; @@ -1995,6 +2020,210 @@ extension _ShadowPasses on Renderer { } } +/// The near cascades under an orthographic camera — `P7`: where they end and +/// the spheres they cover, or null when the camera's box holds no caster. +/// +/// [viewProjection] is the camera's own, in the engine's `[0, 1]` depth range +/// and before any backend adjustment; [eye] is the camera's position, which is +/// what the shading measures a fragment's distance from; [bounds] are the +/// casters'. [count] cascades in all, of which the last stays the whole scene +/// and is not returned. +/// +/// **Why perspective's split is wrong here.** It divides a distance from the +/// eye between the cascades, mostly logarithmically, because through a +/// perspective lens a texel covers more world the further away it is. Through +/// an orthographic lens it does not — every pixel is the same size at every +/// depth — and the eye is only where the camera was put along its axis, often +/// tens of metres short of anything. Split from there, the near cascades +/// covered air in front of the board and every fragment fell through to the +/// last, which is the whole level and the softest map there is. +/// +/// So the box is split instead, evenly, and only the part of it the casters +/// occupy: the depth range of the box clipped to their bounds, which on an +/// isometric board is where the box's four long edges meet the ground rather +/// than wherever its near and far planes were left. The near cascades share +/// that range between them and the last is left as a net for what a mirror +/// or a probe sees outside the box. +/// +/// The shading still picks a cascade by distance from [eye], which through an +/// orthographic lens is depth plus a sideways offset. So the thresholds are +/// distances and each sphere is fitted to every point of the box that its +/// threshold can send to it: the last near one ends at the farthest corner's +/// distance, and a cascade past the first starts no nearer than the depth at +/// which a point at the box's rim reaches the previous threshold. +({List splits, List centres, List radii})? +_orthographicCascades({ + required vm.Matrix4 viewProjection, + required vm.Vector3 eye, + required vm.Aabb3 bounds, + required int count, + required double viewDistance, +}) { + final near = count - 1; + if (near < 1) return null; + final inverse = vm.Matrix4.copy(viewProjection); + if (inverse.invert() == 0.0) return null; + final forward = viewAxisOf(viewProjection); + + vm.Vector3 unproject(double x, double y, double z) { + final p = inverse * vm.Vector4(x, y, z, 1.0) as vm.Vector4; + return vm.Vector3(p.x / p.w, p.y / p.w, p.z / p.w); + } + + // The box's corners: index bit 0 is x, bit 1 is y, bit 2 is depth. + final corners = [ + for (var i = 0; i < 8; i++) + unproject( + i & 1 == 0 ? -1.0 : 1.0, + i & 2 == 0 ? -1.0 : 1.0, + i & 4 == 0 ? 0.0 : 1.0, + ), + ]; + double depthOf(vm.Vector3 p) => (p - eye).dot(forward); + final boxNear = depthOf(corners[0]); + final boxFar = depthOf(corners[4]); + if (!(boxFar > boxNear)) return null; + + // The box and the bounds as half-spaces, each a function that is not + // negative inside. The box's are linear in the point because an + // orthographic matrix leaves w alone. + final m = viewProjection.storage; + double row(int r, vm.Vector3 p) => + (m[r] * p.x + m[4 + r] * p.y + m[8 + r] * p.z + m[12 + r]) / m[15]; + final inBox = [ + (p) => 1.0 - row(0, p), + (p) => 1.0 + row(0, p), + (p) => 1.0 - row(1, p), + (p) => 1.0 + row(1, p), + (p) => row(2, p), + (p) => 1.0 - row(2, p), + ]; + final inBounds = [ + (p) => p.x - bounds.min.x, + (p) => bounds.max.x - p.x, + (p) => p.y - bounds.min.y, + (p) => bounds.max.y - p.y, + (p) => p.z - bounds.min.z, + (p) => bounds.max.z - p.z, + ]; + final boundsCorners = [ + for (var i = 0; i < 8; i++) + vm.Vector3( + i & 1 == 0 ? bounds.min.x : bounds.max.x, + i & 2 == 0 ? bounds.min.y : bounds.max.y, + i & 4 == 0 ? bounds.min.z : bounds.max.z, + ), + ]; + const edges = <(int, int)>[ + (0, 1), (2, 3), (4, 5), (6, 7), // + (0, 2), (1, 3), (4, 6), (5, 7), // + (0, 4), (1, 5), (2, 6), (3, 7), + ]; + + // The depths the box and the bounds share. A vertex of their intersection + // is a corner of one inside the other or where an edge of one crosses a + // face of the other, and clipping every edge of each by the other's + // half-spaces finds all of those as the clipped segments' ends. + var from = double.infinity; + var to = double.negativeInfinity; + void clip(List points, List planes) { + for (final (i, j) in edges) { + final a = points[i]; + final b = points[j]; + var t0 = 0.0; + var t1 = 1.0; + for (final plane in planes) { + final fa = plane(a); + final fb = plane(b); + if (fa < 0.0 && fb < 0.0) { + t0 = 1.0; + t1 = 0.0; + break; + } + if (fa < 0.0) t0 = math.max(t0, fa / (fa - fb)); + if (fb < 0.0) t1 = math.min(t1, fa / (fa - fb)); + } + if (t0 > t1) continue; + final da = depthOf(a); + final db = depthOf(b); + final d0 = da + (db - da) * t0; + final d1 = da + (db - da) * t1; + from = math.min(from, math.min(d0, d1)); + to = math.max(to, math.max(d0, d1)); + } + } + + clip(boundsCorners, inBox); + clip(corners, inBounds); + if (!(to >= from)) return null; + from = math.max(from, boxNear); + to = math.max(from + 1e-3, math.min(to, from + viewDistance)); + + // How far a point at the box's rim lies off the axis through the eye: what + // the distance the shading measures adds to the depth. + var offAxis = 0.0; + for (var i = 0; i < 4; i++) { + final across = corners[i] - eye; + across.sub(forward.scaled(across.dot(forward))); + offAxis = math.max(offAxis, across.length); + } + + // Even in depth, because the texel a pixel needs is the same at every depth. + final depthSplits = [ + for (var k = 1; k <= near; k++) from + (to - from) * k / near, + ]; + final thresholds = [ + for (var k = 0; k < near; k++) + k == near - 1 + ? math.sqrt(math.max(to * to, from * from) + offAxis * offAxis) + : depthSplits[k], + ]; + + vm.Vector3 cornerAt(int i, double depth) { + final t = (depth - boxNear) / (boxFar - boxNear); + return corners[i & 3] + (corners[(i & 3) + 4] - corners[i & 3]).scaled(t); + } + + final centres = []; + final radii = []; + for (var k = 0; k < near; k++) { + final reach = thresholds[k]; + final zHigh = math.min(to, reach); + final lowest = math.max(from, -reach); + final previous = k > 0 ? thresholds[k - 1] : 0.0; + final square = previous * previous - offAxis * offAxis; + final zLow = math.min( + zHigh, + lowest >= 0.0 && square > 0.0 + ? math.max(lowest, math.sqrt(square)) + : lowest, + ); + final slab = [ + for (var i = 0; i < 8; i++) cornerAt(i, i & 4 == 0 ? zLow : zHigh), + ]; + final centre = slab.fold(vm.Vector3.zero(), (sum, p) => sum..add(p)) + ..scale(1.0 / 8.0); + final fitted = slab.fold( + 1e-3, + (widest, p) => math.max(widest, (p - centre).length), + ); + // Rounded up an eighth of an octave at a time, so a camera panning over + // uneven ground does not change a texel's size, and with it every shadow + // edge, a little each frame. + centres.add(centre); + radii.add( + math + .pow(2.0, (math.log(fitted) / math.ln2 * 8.0).ceil() / 8.0) + .toDouble(), + ); + } + return ( + splits: [thresholds[0], near > 1 ? thresholds[1] : 0.0], + centres: centres, + radii: radii, + ); +} + /// What a static cascade tile does in a frame — `S1`. enum _StaticTile { keep, scroll, redraw } diff --git a/packages/flutter3d_core/lib/src/engine/render/shadow_settings.dart b/packages/flutter3d_core/lib/src/engine/render/shadow_settings.dart index 9dc72ca40..93c71dec3 100644 --- a/packages/flutter3d_core/lib/src/engine/render/shadow_settings.dart +++ b/packages/flutter3d_core/lib/src/engine/render/shadow_settings.dart @@ -247,6 +247,10 @@ final class ShadowSettings { /// which is what perspective actually wants, since a texel's world size grows /// with distance. The usual practical answer is most of the way towards /// logarithmic, and that is the default. + /// + /// **Not read under an orthographic camera** — `P7`. There a texel covers + /// the same world at every depth, so the near cascades split the camera's + /// box evenly whatever this says. final double cascadeSplit; /// How far from the camera the cascades are fitted for, in metres. @@ -265,6 +269,11 @@ final class ShadowSettings { /// unshadowed past it** — the last cascade is still fitted to the whole scene /// and every fragment falls through to it, so this trades sharpness near the /// camera against sharpness far from it, and never against coverage. + /// + /// **Under an orthographic camera it is measured from the nearest point of + /// the casters' bounds inside the box** rather than from the eye, which + /// there is only where the camera was put along its axis — `P7`. The near cascades share the depth + /// the box and the casters have in common, up to this much of it. final double viewDistance; /// Distance bias for a point light's cube map, in **metres**. diff --git a/packages/flutter3d_cpu/test/orthographic_test.dart b/packages/flutter3d_cpu/test/orthographic_test.dart index 74d564973..15785c57b 100644 --- a/packages/flutter3d_cpu/test/orthographic_test.dart +++ b/packages/flutter3d_cpu/test/orthographic_test.dart @@ -10,6 +10,7 @@ /// parallel rays instead. library; +import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter3d_core/flutter3d_core.dart'; @@ -93,6 +94,77 @@ double _spread(Float32List frame) { reds.reduce((a, b) => a < b ? a : b); } +/// An isometric board, the way a strategy game looks at one: a floor sixty +/// metres on a side with a post at each of [posts], the sun low across it, +/// and the camera [back] metres up its axis from the board's middle, looking +/// down at thirty-five degrees through a box [height] metres tall. +/// +/// Returns the renderer after one frame with three cascades, and the camera. +({Renderer renderer, CameraNode camera, Scene scene}) _board({ + List posts = const [], + double back = 80.0, + double height = 20.0, +}) { + final device = CpuDevice( + width: _width, + height: _height, + shaders: CpuShaderLibrary(builtinCpuShaders()), + ); + final scene = Scene(); + MeshNode block(Vector3 size, Vector3 at) => MeshNode( + DeviceMesh.upload(device, CuboidShape(size: size).build()), + Material(baseColor: Vector4(0.7, 0.7, 0.7, 1.0)), + )..setPosition(at.x, at.y, at.z); + scene.add(block(Vector3(60.0, 1.0, 60.0), Vector3(0.0, -0.5, 0.0))); + for (final post in posts) { + scene.add(block(Vector3(0.6, 3.0, 0.6), post + Vector3(0.0, 1.5, 0.0))); + } + scene.add( + LightNode(type: LightType.directional, intensity: 2.0, castsShadow: true) + ..setLocalForward(Vector3(-0.4, -0.8, 0.3)), + ); + final pitch = 35.0 * math.pi / 180.0; + final axis = Vector3(0.0, -math.sin(pitch), -math.cos(pitch)); + final camera = CameraNode(projection: OrthographicProjection(height: height)) + ..setPosition(-axis.x * back, -axis.y * back, -axis.z * back) + ..lookAt(Vector3.zero()); + scene.add(camera); + final renderer = Renderer.create(device: device) + ..render( + width: _width, + height: _height, + scene: scene, + views: [RenderView(camera: camera)], + settings: const RenderSettings( + bloom: BloomSettings(enabled: false), + shadows: ShadowSettings(cascades: 3), + ), + ); + return (renderer: renderer, camera: camera, scene: scene); +} + +/// Which cascade the shading reads at [point] and how wide it is: the first +/// whose split [point]'s distance from the eye has not passed and whose +/// sphere holds it, which is the fall-through `shadow.glsl` makes. +({int index, double radius}) _cascadeAt( + Renderer renderer, + CameraNode camera, + Vector3 point, +) { + final radii = renderer.debugCascadeRadii; + final centres = renderer.debugCascadeCentres; + final splits = renderer.debugCascadeSplits; + final distance = (point - camera.readWorldPosition()).length; + var index = 0; + if (radii.length > 1 && distance > splits[0]) index = 1; + if (radii.length > 2 && distance > splits[1]) index = 2; + while (index < radii.length - 1 && + (point - centres[index]).length > radii[index]) { + index++; + } + return (index: index, radius: radii[index]); +} + const OrthographicProjection _ortho = OrthographicProjection(height: 6.0); const PerspectiveProjection _perspective = PerspectiveProjection( fovYRadians: 1.2, @@ -176,4 +248,79 @@ void main() { final bottom = _at(sky, _width ~/ 2, _height - 2); expect((top - bottom).length, greaterThan(0.05)); }); + + test('the first cascade ends inside the box, not in the air before it', () { + // Mutation: drop the orthographic branch in `_renderShadowMap`, and the + // split is perspective's — about nine metres from an eye eighty metres + // short of the board, in front of everything the box holds. + final board = _board(); + final eye = board.camera.readWorldPosition(); + final forward = board.camera.readForward(); + // The nearest and farthest of the floor the camera sees, along its axis. + final bounds = board.scene.computeBounds(castersOnly: true); + final depths = [ + for (var i = 0; i < 8; i++) + (Vector3( + i & 1 == 0 ? bounds.min.x : bounds.max.x, + i & 2 == 0 ? bounds.min.y : bounds.max.y, + i & 4 == 0 ? bounds.min.z : bounds.max.z, + ) - + eye) + .dot(forward), + ]; + final nearest = depths.reduce(math.min); + final split = board.renderer.debugCascadeSplits.first; + expect(split, greaterThan(nearest)); + expect(split, lessThan(depths.reduce(math.max))); + // And the box, not the whole floor: the floor reaches forty metres + // either side of the board's middle along the axis, and the box sees + // about twenty of them. + expect(split, lessThan(80.0 + 5.0)); + }); + + test('near and far casters get the same texel', () { + // Mutation: as above, and both posts fall through to the last cascade, + // the whole sixty-metre floor; or put the first split well short of the + // box's middle, and both land in the second cascade. + const nearPost = [0.0, 0.0, 8.0]; + const farPost = [0.0, 0.0, -8.0]; + final board = _board( + posts: [Vector3.array(nearPost), Vector3.array(farPost)], + ); + final near = _cascadeAt( + board.renderer, + board.camera, + Vector3.array(nearPost), + ); + final far = _cascadeAt( + board.renderer, + board.camera, + Vector3.array(farPost), + ); + final whole = board.renderer.debugCascadeRadii.last; + expect(near.index, lessThan(2), reason: 'the near post in the last'); + expect(far.index, lessThan(2), reason: 'the far post in the last'); + expect(near.index, lessThan(far.index)); + expect(near.radius, lessThan(whole * 0.7)); + expect(far.radius, lessThan(whole * 0.7)); + final ratio = + math.max(near.radius, far.radius) / math.min(near.radius, far.radius); + expect(ratio, lessThan(1.5)); + }); + + test('where the camera stands along its axis barely moves a cascade', () { + // The eye's place along the axis changes nothing in the picture, so it + // should not change which world a texel covers either. It does a little: + // the shading picks a cascade by distance from the eye, which adds the + // box's sideways reach to the depth, and an eye close to the board has + // to fit the second cascade a little wider for it. One rounding step of + // the radius, an eighth of an octave, and no more. + final close = _board(back: 40.0).renderer; + final distant = _board(back: 120.0).renderer; + for (var i = 0; i < 2; i++) { + final a = close.debugCascadeRadii[i]; + final b = distant.debugCascadeRadii[i]; + expect(math.max(a, b) / math.min(a, b), lessThan(1.1), reason: '$i'); + } + }); } From 0abc173af1eac7f54b52ec655b45335b1288a958 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 19:24:06 +0300 Subject: [PATCH 2/3] test: Record orthographic-shadow in all four golden sets A strategy board through an isometric orthographic camera: a floor far larger than the box the camera sees, blocks for buildings and thin posts for units from the near edge of the view to the far one. With the cascades split from the eye the posts cast no shadow that could be seen; split through the box they are as sharp at the top of the frame as at the bottom. WebGPU lands at 0 pixels from Impeller, WebGL at 0.433% and the software set at 1.117%, all on edges; the budgets are set just above. The golden scene and test counts in the documents follow. --- ARCHITECTURE.md | 8 +-- README.md | 4 +- packages/flutter3d/CHANGELOG.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 | 67 ++++++++++++++++-- .../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 -> 25958 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/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 | 12 ++++ .../src/engine/render/render_settings.dart | 4 +- .../lib/src/engine/render/renderer.dart | 2 +- .../src/engine/render/renderer_post_pass.dart | 4 +- .../engine/render/renderer_shadow_pass.dart | 2 +- .../formats/ktx2_supercompression_test.dart | 2 +- packages/flutter3d_cpu/CHANGELOG.md | 3 + .../lib/src/cpu_shaders_post.dart | 2 +- .../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 -> 21311 bytes .../test/gpu_formats_test.dart | 2 +- packages/flutter3d_webgl/CHANGELOG.md | 3 + .../test/cross_backend_test.dart | 3 + .../test/goldens/orthographic-shadow.png | Bin 0 -> 26175 bytes .../test/lighting_models_test.dart | 2 +- packages/flutter3d_webgl/tool/golden_web.sh | 2 +- packages/flutter3d_webgpu/CHANGELOG.md | 3 + .../test/cross_backend_test.dart | 1 + .../test/goldens/orthographic-shadow.png | Bin 0 -> 25971 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 | 12 ++-- tool/ci.sh | 2 +- 51 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 packages/flutter3d/test/goldens/orthographic-shadow.png create mode 100644 packages/flutter3d_cpu/test/goldens/orthographic-shadow.png 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..30e74d293 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 | **11343 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..57b33150f 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 11343 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..b18d6476e 100644 --- a/packages/flutter3d/CHANGELOG.md +++ b/packages/flutter3d/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +- `orthographic-shadow` in the reference set: a strategy board through an + isometric orthographic camera, its posts' shadows as sharp at the top of + the frame as at the bottom. + - **`generatedMaterialPathFor`**: where the build hook writes a `.f3dmat`'s compiled bundle, `flutter3d_generated/.f3dshaders`. 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..734ead07a 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. A strategy board through an isometric orthographic camera, its + // shadows from cascades split through the box rather than from the eye. + 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..f0339e530 100644 --- a/packages/flutter3d/example/lib/src/spike/golden_stages.dart +++ b/packages/flutter3d/example/lib/src/spike/golden_stages.dart @@ -1269,6 +1269,68 @@ abstract final class GoldenStages { fog: FogSettings(color: Vector3(0.7, 0.75, 0.8), density: 0.03), ); + /// `orthographic-shadow`: a strategy board seen isometrically — a floor a + /// hundred and twenty metres on a side, a level far larger than the box + /// the camera sees, with blocks for buildings and thin posts for units in + /// rows from the near edge of the view to the far one, under the sun, + /// through an orthographic camera standing sixty metres back along its + /// axis. + /// + /// What it holds is `P7`'s cascade split: the near cascades fitted to the + /// box and shared evenly through its depth, so the posts at the top of the + /// frame cast as sharp a shadow as the ones at the bottom. Split from the + /// eye as a perspective camera's are, every shadow here fell to the last + /// cascade, the whole floor, and the posts cast none that could be seen. + static Future orthographicShadow(GoldenStage stage) async { + final device = stage.device; + stage.sun.setLocalForward(Vector3(-1.0, -1.1, -0.45).normalized()); + final stone = Material(baseColor: Vector4(0.78, 0.74, 0.68, 1.0)); + final roof = Material(baseColor: Vector4(0.62, 0.34, 0.26, 1.0)); + final unit = Material(baseColor: Vector4(0.25, 0.4, 0.7, 1.0)); + return GoldenStaged( + nodes: [ + _slab( + device, + Vector3(120.0, 0.2, 120.0), + Vector3(0.0, -0.1, 0.0), + Material(baseColor: Vector4(0.5, 0.6, 0.42, 1.0), roughness: 0.95), + ), + for (final (x, z, height) in <(double, double, double)>[ + (-4.0, -4.0, 2.5), + (3.0, -5.0, 1.5), + (-5.0, 3.0, 1.8), + (4.5, 4.0, 3.0), + (0.0, 0.0, 1.2), + ]) ...[ + _slab( + device, + Vector3(1.8, height, 1.8), + Vector3(x, height * 0.5, z), + stone, + ), + _slab( + device, + Vector3(2.0, 0.3, 2.0), + Vector3(x, height + 0.15, z), + roof, + ), + ], + for (var row = -3; row <= 3; row++) + for (var column = -1; column <= 1; column++) + _slab( + device, + Vector3(0.12, 1.0, 0.12), + Vector3(column * 2.4 + row * 0.6, 0.5, row * 2.6), + unit, + ), + ], + everyFrame: (_, _) => stage.camera + ..projection = const OrthographicProjection(height: 14.0, far: 200.0) + ..setPosition(34.6, 34.6, 34.6) + ..lookAt(Vector3.zero()), + ); + } + // ------------------------------------------------------------------ P6 /// `debug-view-split`: the lit teapot left of the middle and its shading @@ -1276,10 +1338,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..b95f9ff8787f1f879d2afe736118b32d5be2acb2 GIT binary patch literal 25958 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 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/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..147167cd9 100644 --- a/packages/flutter3d_core/CHANGELOG.md +++ b/packages/flutter3d_core/CHANGELOG.md @@ -1,5 +1,17 @@ ## Unreleased +- **Shadow cascades under an orthographic camera.** They were split by + distance from the eye, mostly logarithmically, as a perspective camera + wants; through an orthographic lens the eye is only where the camera was + put along its axis, so the near cascades covered the air in front of the + board and every shadow fell to the last, the whole level. Now the near + cascades share the depth the camera's box and the casters have in common, + evenly, since a pixel needs the same texel at every depth, and each is a + sphere fitted to the slab of the box its threshold can send to it. The + last stays the whole scene, for mirrors and probes. Perspective cameras + split as before. `Renderer.debugCascadeSplits` reads where the near + cascades end. `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/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 e075a88ec..4b1f269fc 100644 --- a/packages/flutter3d_core/lib/src/engine/render/renderer.dart +++ b/packages/flutter3d_core/lib/src/engine/render/renderer.dart @@ -1607,7 +1607,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; 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 2360be66a..a213739e9 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 @@ -1276,7 +1276,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..b02ba72a1 100644 --- a/packages/flutter3d_cpu/CHANGELOG.md +++ b/packages/flutter3d_cpu/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- `orthographic-shadow` in the software reference set, at 1.117% from + Impeller on the edges. + - 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_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..81810f6e2 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.117% measured, on the edges and nowhere in a shadow: the posts are a + // tenth of a metre wide and almost all edge, which Impeller multisamples + // and this set does not. The shadows, near and far, agree. + 'orthographic-shadow': 1.2, // 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..33d4356c531927711c0f5a7198feebc830963584 GIT binary patch literal 21311 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 _budgets = { // does not multisample as Impeller does; the highlights, the fog and the // sky agree. 'orthographic-metal': 0.5, + // 0.433% measured, on the posts' and the blocks' edges, which WebGL2 + // does not multisample as Impeller does; the shadows agree. + '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..11e7f4568429eabfaa0c373e8cb706ee38c49360 GIT binary patch literal 26175 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 _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..98e4033639fb0dc4b7cc10d6afe35fd03315757a GIT binary patch literal 25971 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 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..94d3fa0e5 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 11343 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..81912c748 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 11340 tests need a GPU. +description: Four independent golden sets, mutation-checking every new test, determinism and snapshots, and why only about thirty of 11343 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. +11343 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 | |---|---|---|---|---| @@ -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` | 383 | | `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 11316 rather than 11343: 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 3dc4d9539dc60e0f1aad5f7e5ce6eb1bffb66193 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotov Date: Sat, 3 Oct 2026 19:24:13 +0300 Subject: [PATCH 3/3] docs: Move orthographic shadow cascades to Done in P7 What was done and how it was tested, and what stays left: the shading still picks a cascade by distance from the eye rather than by depth along the view axis, which costs an eye close to the board up to one rounding step of the second cascade's radius. --- tasks/0.9-engine-roadmap.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tasks/0.9-engine-roadmap.md b/tasks/0.9-engine-roadmap.md index 3dc054372..896246098 100644 --- a/tasks/0.9-engine-roadmap.md +++ b/tasks/0.9-engine-roadmap.md @@ -551,10 +551,29 @@ 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. +The shadow cascades under an orthographic camera: split by distance from +the eye, the near ones covered the air in front of the board and every +shadow fell to the last, the whole level. When `isOrthographic` reads the +view, the near cascades share evenly the depth the camera's box and the +casters' bounds have in common (their intersection, found by clipping each +one's edges by the other), since a pixel needs the same texel at every +depth; each is a sphere fitted to the slab of the box its distance +threshold can send to it, radius rounded up an eighth of an octave at a +time; the last stays the whole scene, and the caster reach-back is kept. +Tests: `orthographic_test.dart` (the first split inside the casters' depth +in the box, not in front of it; a near and a far post in near cascades of +radii within 1.5 of each other; the eye's place along the axis moving a +radius by one rounding step at most; mutation-checked by removing the +branch and by an uneven split) and `orthographic-shadow` in all four sets, +WebGPU at 0 pixels from Impeller, WebGL at 0.433% and the software set at +1.117%, on the edges. `orthographic-metal` draws no shadow and did not move. *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' +under an orthographic camera, the shading still picks a cascade by distance +from the eye rather than by depth along the axis, so the second cascade is +fitted a little wider to hold the box's rim and an eye close to the board +costs up to one rounding step of its radius (picking by `ViewDepth` in +`shadow.glsl`, the volumetric fog, the shafts and the reflections, with +their mirrors, would remove it); 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