flutter3d
Showcase Changelog 38 packages API reference

Reconciling an orthographic camera and a Flame viewfinder: the source

since 0.7.0 The Flame bridge

Regions the guide quotes: live (line 132), drive (line 182), cameras (line 213), scene-to-flame (line 220), flame-to-scene (line 232).

1/// `CameraSyncController` reconciling an orthographic flutter3d `CameraNode`
2/// and a Flame `Viewfinder`, in both `SyncDirection`s.
3///
4/// Quoted by `flame_camera_bridge.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flame/camera.dart';
10import 'package:flame/components.dart' show Anchor, CircleComponent, Component;
11import 'package:flame_flutter3d/flame_flutter3d.dart';
12import 'package:flutter/widgets.dart';
13import 'package:flutter3d/flutter3d.dart';
14import 'package:flutter3d_showcase/src/demo/demo.dart';
15import 'package:flutter3d_showcase/src/demo/flame_layer.dart';
16import 'package:vector_math/vector_math.dart';
18final class FlameCameraBridgeDemo extends ShowcaseDemo {
19 late final String _report;
20 late final double _viewfinderX;
21 late final double _viewfinderZoom;
22 late final double _cameraX;
23 late final double _orthoHeight;
25 /// Which side leads on the page: 0 the flutter3d camera, 1 the viewfinder.
26 int _leader = 0;
28 late final CameraNode _lens;
29 late final RenderView _view;
30 late final Scene _scene;
31 late final TransparentFlameGame _game;
32 late final CameraSyncController _sceneLeads;
33 late final CameraSyncController _flameLeads;
34 late final DemoContext _context;
35 late final Widget _body = Flutter3dFlameWidget(
36 game: _game,
37 camera: _lens,
38 existing: (device: _context.device, renderer: _context.renderer),
39 buildScene: (GraphicsDevice device) => _scene,
40 );
41 double _clock = 0.0;
43 /// Where the pillars stand, on the ground, in metres.
44 static const List<(double, double)> _pillars = <(double, double)>[
45 (-3.0, -3.0),
46 (0.0, -3.0),
47 (3.0, -3.0),
48 (-3.0, 0.0),
49 (0.0, 0.0),
50 (3.0, 0.0),
51 (-3.0, 3.0),
52 (0.0, 3.0),
53 (3.0, 3.0),
54 ];
56 /// Where the camera stands above the ground.
57 static const double _eyeHeight = 6.0;
59 @override
60 List<RenderView> views(DemoContext context) => <RenderView>[_view];
62 @override
63 Scene build(DemoContext context) {
64 _context = context;
65 final (
66 String report,
67 double viewfinderX,
68 double viewfinderZoom,
69 double cameraX,
70 double orthoHeight,
71 ) = _run();
72 _report = report;
73 _viewfinderX = viewfinderX;
74 _viewfinderZoom = viewfinderZoom;
75 _cameraX = cameraX;
76 _orthoHeight = orthoHeight;
78 _lens =
79 CameraNode(
80 name: 'bridged-lens',
81 projection: const OrthographicProjection(height: 9.0),
82 )
83 ..setPosition(0.0, _eyeHeight, 0.0)
84 // Straight down, with -z at the top of the picture: Flame's y grows
85 // downwards and the plane maps it to z, so the two agree on which way
86 // is up.
87 ..lookAt(Vector3.zero(), up: Vector3(0.0, 0.0, -1.0));
88 _view = RenderView(
89 camera: _lens,
90 clearColor: Vector4(0.05, 0.05, 0.07, 1.0),
91 );
93 _scene = Scene()
94 ..ambientColor = Vector3(0.5, 0.55, 0.65)
95 ..ambientIntensity = 0.35
96 ..add(_lens)
97 ..add(
98 MeshNode(
99 DeviceMesh.upload(
100 context.device,
101 CuboidShape(size: Vector3(40.0, 0.1, 40.0)).build(),
102 ),
103 Material(name: 'floor', baseColor: Vector4(0.3, 0.33, 0.32, 1.0)),
104 name: 'floor',
105 )..setPosition(0.0, -0.05, 0.0),
106 )
107 ..add(
108 LightNode(name: 'sun', intensity: 2.5)
109 ..setLocalForward(Vector3(-0.3, -1.0, -0.4)),
110 );
111 for (final (int i, (double, double) at) in _pillars.indexed) {
112 _scene.add(
113 MeshNode(
114 DeviceMesh.upload(
115 context.device,
116 CuboidShape(size: Vector3(0.6, 0.4 + 0.15 * (i % 4), 0.6)).build(),
117 ),
118 Material(
119 name: 'pillar $i',
120 baseColor: Vector4(
121 0.55 + 0.4 * math.sin(i * 0.9),
122 0.5 + 0.3 * math.sin(i * 1.7 + 1.0),
123 0.4 + 0.3 * math.sin(i * 2.3 + 2.0),
124 1.0,
125 ),
126 ),
127 name: 'pillar $i',
128 )..setPosition(at.$1, 0.2 + 0.075 * (i % 4), at.$2),
129 );
130 }
132 // One plane for both, at the camera's own height: the controller writes
133 // the camera's position through it when Flame leads, and a plane at zero
134 // would put the lens on the floor.
135 final BridgePlane plane = BridgePlane.ground(height: _eyeHeight);
136 _game = TransparentFlameGame();
137 final Viewfinder viewfinder = _game.camera.viewfinder;
138 _sceneLeads = CameraSyncController(
139 camera: _lens,
140 viewfinder: viewfinder,
141 plane: plane,
142 );
143 _flameLeads = CameraSyncController(
144 camera: _lens,
145 viewfinder: viewfinder,
146 plane: plane,
147 direction: SyncDirection.flameToScene,
148 );
149 // Flame's own picture of the same ground: a ring round every pillar, in
150 // metres, seen through the viewfinder the controller is keeping in step.
151 for (final (double, double) at in _pillars) {
152 _game.world.add(
153 CircleComponent(
154 radius: 0.55,
155 position: Vector2(at.$1, at.$2),
156 anchor: Anchor.center,
157 paint: Paint()
158 ..color = const Color(0xFFFFD866)
159 ..style = PaintingStyle.stroke
160 ..strokeWidth = 0.05,
161 ),
162 );
163 }
164 _game
165 ..add(_Drive(this))
166 ..add(
167 flameCaption('yellow rings: Flame\'s world, through its viewfinder'),
168 )
169 ..add(flameCaption(_report, at: Vector2(16.0, 40.0)));
170 return _scene;
171 }
173 /// The half of the frame a page's own logic owns: whichever side leads
174 /// moves, then the controller carries it over, then the Flame zoom is
175 /// scaled from "one over the height" to pixels to the metre.
176 void _drive(double dt) {
177 _clock += dt;
178 final double x = 1.8 * math.sin(_clock * 0.5);
179 final double z = 1.2 * math.sin(_clock * 0.9);
180 final double height = 9.0 + 2.0 * math.sin(_clock * 0.7);
181 final Viewfinder viewfinder = _game.camera.viewfinder;
182 if (_leader == 0) {
183 // flutter3d leads: the camera moves and the viewfinder is told.
184 _lens
185 ..setPosition(x, _eyeHeight, z)
186 ..projection = OrthographicProjection(height: height);
187 _sceneLeads.advance(dt);
188 } else {
189 // Flame leads: the viewfinder moves and the camera is told.
190 viewfinder
191 ..position = Vector2(x, z)
192 ..zoom = 1.0 / height;
193 _flameLeads.advance(dt);
194 }
195 // The controller's zoom is one over the height, which is not a size on
196 // screen. Turning it into pixels to the metre is the caller's step: the
197 // game is as many pixels high as the picture under it.
198 final double pixelsHigh = _game.size.y;
199 if (pixelsHigh > 0.0) viewfinder.zoom = pixelsHigh * viewfinder.zoom;
200 }
202 @override
203 List<DemoControl> controls(DemoContext context) => <DemoControl>[
204 ChoiceControl(
205 'Who leads',
206 options: const <String>['flutter3d camera', 'Flame viewfinder'],
207 index: () => _leader,
208 onChanged: (int i) => _leader = i,
209 ),
210 ];
212 static (String, double, double, double, double) _run() {
213 final camera = CameraNode(
214 name: 'bridged-camera',
215 projection: const OrthographicProjection(height: 4.0),
216 );
217 final viewfinder = Viewfinder();
218 final plane = BridgePlane.ground();
220 // The flutter3d camera is authoritative; `advance` copies its position
221 // and its orthographic height onto the Flame viewfinder.
222 final sceneToFlame = CameraSyncController(
223 camera: camera,
224 viewfinder: viewfinder,
225 plane: plane,
226 );
227 camera.setPosition(3.0, 0.0, 2.0);
228 sceneToFlame.advance(1 / 60);
229 final double viewfinderX = viewfinder.position.x;
230 final double viewfinderZoom = viewfinder.zoom;
232 // The Flame viewfinder is authoritative instead; `advance` copies its
233 // position and its zoom onto the flutter3d camera's projection.
234 final flameToScene = CameraSyncController(
235 camera: camera,
236 viewfinder: viewfinder,
237 plane: plane,
238 direction: SyncDirection.flameToScene,
239 );
240 viewfinder.position = Vector2(1.0, -1.0);
241 viewfinder.zoom = 0.5;
242 flameToScene.advance(1 / 60);
243 final double cameraX = camera.readPosition().x;
244 final double orthoHeight =
245 (camera.projection as OrthographicProjection).height;
247 return (
248 'flutter3d led: Flame position=($viewfinderX, _), zoom=$viewfinderZoom; '
249 'Flame led: flutter3d x=$cameraX, height=$orthoHeight',
250 viewfinderX,
251 viewfinderZoom,
252 cameraX,
253 orthoHeight,
254 );
255 }
257 /// The Flame game the page runs, for a test that steps it without a window.
258 @visibleForTesting
259 TransparentFlameGame get game => _game;
261 @override
262 Widget? customBody(BuildContext buildContext, DemoContext context) => _body;
264 @override
265 void verify(Scene scene, FrameResult frame) {
266 if (frame.drawCalls < 1) {
267 throw StateError('the marker was not drawn');
268 }
269 // Compared as numbers, not read back out of `_report`: see
270 // `sim_audio_xr/actors.dart` for why.
271 if (_viewfinderX != 3.0) {
272 throw StateError(
273 'the viewfinder should have followed the camera to x=3, got '
274 '$_viewfinderX',
275 );
276 }
277 if (_viewfinderZoom != 0.25) {
278 throw StateError(
279 'zoom = 1 / height should read 0.25 for a height-4 lens, got '
280 '$_viewfinderZoom',
281 );
282 }
283 if (_cameraX != 1.0) {
284 throw StateError(
285 'the flutter3d camera should have followed the viewfinder to x=1, '
286 'got $_cameraX',
287 );
288 }
289 if (_orthoHeight != 2.0) {
290 throw StateError(
291 'height = 1 / zoom should read 2.0 for zoom 0.5, got $_orthoHeight',
292 );
293 }
294 }
297/// Calls the page's own step once a Flame frame, before the bridge clock.
298final class _Drive extends Component {
299 _Drive(this._page);
301 final FlameCameraBridgeDemo _page;
303 @override
304 void update(double dt) {
305 super.update(dt);
306 _page._drive(dt);
307 }