flutter3d_testing

Pixel regression tests for a game, on a machine with no GPU.

import 'package:flutter3d_testing/flutter3d_testing.dart';

test('the crypt still looks like the crypt', () async {
  final frame = await renderFrame(
    width: 320,
    height: 180,
    build: (device) {
      final scene = Scene();
      // ... put the level in it, uploading meshes to `device`
      return (scene: scene, camera: camera);
    },
  );
  await expectMatchesGolden(frame, 'test/goldens/crypt.png');
});

The first run records the reference and says so. Later runs fail when the picture changes, and the message says by how much and how to re-record.

Why this can exist here and nowhere else

flutter3d_cpu is a full backend: a software rasteriser that passes the same conformance suite as Impeller and WebGL. A frame can therefore be drawn with no driver, no display and no graphics hardware, and a continuous integration runner can answer "does it still look right".

Every other 3D engine on this platform needs a real device for that check. In practice that means either a machine nobody wants to pay for or a check nobody runs.

Why it is its own package

flutter3d_cpu must not depend on flutter3d. A backend that could not compile without the engine would be part of the engine, not an implementation of an interface. And flutter3d must not depend on any backend; a scan in tool/structure.dart enforces that.

So neither of them can hold code that needs both, and this package needs both.

What a match means

tolerance is the share of pixels allowed to differ, and it is zero by default. What counts as a differing pixel is a separate setting, and its default is not byte for byte. A pixel differs when red, green or blue is more than channel steps off (8 unless you say otherwise), and alpha is not compared unless you pass alpha: true. This README used to say the tolerance was zero and stop there, which anyone keeping byte-exact references read as "exact". It was not exact, and a reference that moved by eight steps everywhere still passed.

For an exact comparison:

await expectMatchesGolden(frame, path, channel: 0, alpha: true);

The frame comes from a software rasteriser, so the same scene drawn twice is the same bytes twice. There is no driver, clock or thread to disagree, which means any difference is a change and not noise. Raise tolerance only when something has been measured to move, and say why in the call.

What it does not do

It does not compare a software frame against a GPU one. This repository keeps two separate golden sets on purpose: the same scene differs by a fraction of a percent between a rasteriser and a driver, and one shared set would need a tolerance. See the note at the top of packages/flutter3d_cpu/test/cross_backend_test.dart.

References a game records here show what the software backend draws. That is still the right thing to regress against, because a change that alters the picture alters it on both.


Part of flutter3d, an independent implementation of a 3D engine for Flutter. It is not a fork or a binding of another engine, and it is not affiliated with the Flutter team. It has four switchable rendering backends: Impeller via Flutter GPU, WebGL2, WebGPU and a software rasteriser. It loads glTF, OBJ and .f3d, and has six lighting models, shadows, bloom, skinning, animation, BVH culling and picking, plus a deterministic fixed-step game layer with collision, navigation, positional audio, and gamepad and touch input. Four example games (shooter, platformer, racing, strategy) are each built on a genre package: flutter3d_game_shooter, flutter3d_game_platformer, flutter3d_game_racing, flutter3d_game_strategy. A new game starts from the editor's scaffold, which writes one from a template: flutter3d.pleion.dev/first-project/. Documentation: flutter3d.pleion.dev.

Libraries

flutter3d_testing
Pixel regression tests for a game, on a machine with no GPU.