Reconciling an orthographic camera and a Flame viewfinder
Open the live demo · Read the source · View on GitHub
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.