flutter3d
Showcase Changelog 38 packages API reference

Reconciling an orthographic camera and a Flame viewfinder

since 0.7.0 The Flame bridge

CameraSyncController keeps a flutter3d CameraNode and a Flame Viewfinder describing the same view. Position goes through the same BridgePlane/SyncDirection every other bridge in this category shares; zoom and an OrthographicProjection's height are reconciled by zoom = 1 / height, the one correspondence that moves the two the same way with no further data.

Step 1: Two cameras, one plane #

final camera = CameraNode(
  name: 'bridged-camera',
  projection: const OrthographicProjection(height: 4.0),
);
final viewfinder = Viewfinder();
final plane = BridgePlane.ground();

Step 2: flutter3d leads #

The flutter3d camera moves; the controller carries its position and its lens height onto the Flame viewfinder.

// The flutter3d camera is authoritative; `advance` copies its position
// and its orthographic height onto the Flame viewfinder.
final sceneToFlame = CameraSyncController(
  camera: camera,
  viewfinder: viewfinder,
  plane: plane,
);
camera.setPosition(3.0, 0.0, 2.0);
sceneToFlame.advance(1 / 60);
final double viewfinderX = viewfinder.position.x;
final double viewfinderZoom = viewfinder.zoom;

Step 3: Flame leads #

Reversed, with SyncDirection.flameToScene: the viewfinder moves and zooms, and the controller carries that onto the flutter3d camera's own projection.

// The Flame viewfinder is authoritative instead; `advance` copies its
// position and its zoom onto the flutter3d camera's projection.
final flameToScene = CameraSyncController(
  camera: camera,
  viewfinder: viewfinder,
  plane: plane,
  direction: SyncDirection.flameToScene,
);
viewfinder.position = Vector2(1.0, -1.0);
viewfinder.zoom = 0.5;
flameToScene.advance(1 / 60);
final double cameraX = camera.readPosition().x;
final double orthoHeight =
    (camera.projection as OrthographicProjection).height;

The flutter3d camera's move landed on the viewfinder as position and as 1 / height; the viewfinder's own move and zoom landed back on the camera as position and as 1 / zoom — both directions of the same reconciliation, over the same plane.

Step 4: See both lenses agree #

The picture is the same ground twice. flutter3d looks straight down through an orthographic camera at nine pillars; Flame draws a yellow ring round each one in its own world, through its own viewfinder. When the two lenses agree, every ring sits on its pillar, whatever the camera does.

The plane stands at the camera's own height: when Flame leads, the controller writes the camera's position through it, and a plane at zero would put the lens on the floor.

// One plane for both, at the camera's own height: the controller writes
// the camera's position through it when Flame leads, and a plane at zero
// would put the lens on the floor.
final BridgePlane plane = BridgePlane.ground(height: _eyeHeight);
_game = TransparentFlameGame();
final Viewfinder viewfinder = _game.camera.viewfinder;
_sceneLeads = CameraSyncController(
  camera: _lens,
  viewfinder: viewfinder,
  plane: plane,
);
_flameLeads = CameraSyncController(
  camera: _lens,
  viewfinder: viewfinder,
  plane: plane,
  direction: SyncDirection.flameToScene,
);
// Flame's own picture of the same ground: a ring round every pillar, in
// metres, seen through the viewfinder the controller is keeping in step.
for (final (double, double) at in _pillars) {
  _game.world.add(
    CircleComponent(
      radius: 0.55,
      position: Vector2(at.$1, at.$2),
      anchor: Anchor.center,
      paint: Paint()
        ..color = const Color(0xFFFFD866)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 0.05,
    ),
  );
}

Step 5: Move one, and the other follows #

Pick who leads with Who leads. The camera drifts and its lens height breathes in and out. Either the flutter3d camera moves and the viewfinder is told, or the viewfinder moves and the camera is told; the rings stay on the pillars either way.

if (_leader == 0) {
  // flutter3d leads: the camera moves and the viewfinder is told.
  _lens
    ..setPosition(x, _eyeHeight, z)
    ..projection = OrthographicProjection(height: height);
  _sceneLeads.advance(dt);
} else {
  // Flame leads: the viewfinder moves and the camera is told.
  viewfinder
    ..position = Vector2(x, z)
    ..zoom = 1.0 / height;
  _flameLeads.advance(dt);
}

The controller's zoom is one over the height, which is a number and not a size on screen. Turning it into pixels to the metre is the page's own step, after advance: the game is as many pixels high as the picture under it.