Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/flutter3d/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/<same path>.f3dshaders`.

Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/example/lib/src/spike/backend_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ??
Expand Down
10 changes: 10 additions & 0 deletions packages/flutter3d/example/lib/src/spike/golden_scenes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,16 @@ final List<GoldenScene> kGoldenScenes = <GoldenScene>[
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(
Expand Down
67 changes: 63 additions & 4 deletions packages/flutter3d/example/lib/src/spike/golden_stages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1269,17 +1269,76 @@ 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<GoldenStaged> 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: <SceneNode>[
_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),
]) ...<SceneNode>[
_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
/// normal right of it, in one draw — the wipe `DebugViewSettings.split`
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/ambient_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter3d/test/anisotropy_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter3d/test/auto_batch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/fog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/flutter3d/test/grade_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Future<List<int>> _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());
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/halation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/irradiance_bounce_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/masked_shadow_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/material_vertex_stage_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/per_view_exposure_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/render_scale_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/sharpen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ int _steepest(List<int> 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);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/sky_procedural_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter3d/test/ssao_blur_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
12 changes: 12 additions & 0 deletions packages/flutter3d_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading