flutter3d
Showcase Changelog 38 packages API reference

Screen-space bounds: the source

since 0.7.0 Views, picking and input

Regions the guide quotes: box (line 117), bounds (line 134).

1/// Where a box in the world lands on the glass, as a rectangle a focus ring
2/// or a label can use. Three things move about the scene and the rectangle
3/// `screenBoundsOfBox` returns for each is drawn over the picture.
4///
5/// Quoted by `screen_bounds.md` and shown whole in the Source tab.
6library;
7
8import 'dart:math' as math;
9
10import 'package:flutter/gestures.dart';
11import 'package:flutter/widgets.dart';
12import 'package:flutter3d/flutter3d.dart' hide Material;
13import 'package:flutter3d/flutter3d.dart' as f3d show Material;
14import 'package:flutter3d_app/flutter3d_app.dart';
15import 'package:flutter3d_showcase/src/demo/demo.dart';
16import 'package:vector_math/vector_math.dart' hide Colors;
18final class ScreenBoundsDemo extends ShowcaseDemo {
19 int ringed = 3;
21 late final CameraNode _camera;
22 late final Scene _scene;
23 late final MeshNode _cube;
24 late final MeshNode _sphere;
25 late final MeshNode _pillar;
26 double _clock = 0.0;
28 static const List<String> _names = <String>['cube', 'sphere', 'pillar'];
29 static const List<Color> _colours = <Color>[
30 Color(0xFFFFB03B),
31 Color(0xFF5CD6FF),
32 Color(0xFFFF6B9A),
33 ];
35 @override
36 void configureView(DemoContext context) {
37 context.orbit
38 ..distance = 9.0
39 ..pitch = 0.45
40 ..yaw = 0.4;
41 }
43 @override
44 Scene build(DemoContext context) {
45 _camera = context.camera;
46 f3d.Material colour(String name, double r, double g, double b) =>
47 f3d.Material(
48 name: name,
49 baseColor: Vector4(r, g, b, 1.0),
50 roughness: 0.6,
51 );
52 _cube = MeshNode(
53 DeviceMesh.upload(
54 context.device,
55 CuboidShape(size: Vector3.all(1.2)).build(),
56 ),
57 colour('cube', 0.8, 0.55, 0.3),
58 name: 'cube',
59 );
60 _sphere = MeshNode(
61 DeviceMesh.upload(context.device, SphereShape(radius: 0.7).build()),
62 colour('sphere', 0.35, 0.65, 0.8),
63 name: 'sphere',
64 );
65 _pillar = MeshNode(
66 DeviceMesh.upload(
67 context.device,
68 CuboidShape(size: Vector3(0.6, 2.4, 0.6)).build(),
69 ),
70 colour('pillar', 0.8, 0.4, 0.55),
71 name: 'pillar',
72 )..setPosition(0.0, 1.2, -1.6);
74 _scene = Scene()
75 ..ambientColor = Vector3(0.5, 0.55, 0.65)
76 ..ambientIntensity = 0.25
77 ..add(
78 MeshNode(
79 DeviceMesh.upload(
80 context.device,
81 const PlaneShape(width: 12.0, depth: 12.0).build(),
82 ),
83 colour('floor', 0.36, 0.4, 0.38),
84 name: 'floor',
85 ),
86 )
87 ..add(_cube)
88 ..add(_sphere)
89 ..add(_pillar)
90 ..add(
91 LightNode(name: 'sun', intensity: 3.0)
92 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
93 );
94 _place();
95 return _scene;
96 }
98 void _place() {
99 _cube.setPosition(
100 2.4 * math.cos(_clock * 0.6),
101 0.6,
102 2.4 * math.sin(_clock * 0.6),
103 );
104 _sphere.setPosition(
105 1.4 * math.cos(-_clock * 0.9 + 2.0),
106 0.7 + 0.5 * math.sin(_clock * 1.7).abs(),
107 1.4 * math.sin(-_clock * 0.9 + 2.0),
108 );
109 }
111 @override
112 void update(DemoContext context, double dt) {
113 _clock += dt;
114 _place();
115 }
117 /// What each thing occupies in the world, as the box `screenBoundsOfBox`
118 /// takes — the box a culling or picking system would already keep.
119 Aabb3 _boxOf(int which) => switch (which) {
120 0 => Aabb3.centerAndHalfExtents(
121 _cube.readPosition(),
122 Vector3(0.6, 0.6, 0.6),
123 ),
124 1 => Aabb3.centerAndHalfExtents(
125 _sphere.readPosition(),
126 Vector3(0.7, 0.7, 0.7),
127 ),
128 _ => Aabb3.centerAndHalfExtents(
129 _pillar.readPosition(),
130 Vector3(0.3, 1.2, 0.3),
131 ),
132 };
134 ScreenBounds? _measure(int which, double width, double height) =>
135 screenBoundsOfBox(
136 _camera.viewProjection(width / height),
137 _boxOf(which),
138 width: width,
139 height: height,
140 );
142 @override
143 Widget? customBody(BuildContext buildContext, DemoContext context) =>
144 Listener(
145 onPointerMove: (PointerMoveEvent event) =>
146 context.orbit.rotate(event.delta.dx, event.delta.dy),
147 onPointerSignal: (PointerSignalEvent event) {
148 if (event is PointerScrollEvent) {
149 context.orbit.zoom(event.scrollDelta.dy > 0.0 ? 1.1 : 1.0 / 1.1);
150 }
151 },
152 child: Stack(
153 fit: StackFit.expand,
154 children: <Widget>[
155 SceneSurface(
156 renderer: context.renderer,
157 scene: _scene,
158 view: context.view,
159 settings: () => const RenderSettings(),
160 onBeforeFrame: () =>
161 context.orbit.syncProjectionDepth(context.camera),
162 presentFrame: presentFrame,
163 ),
164 IgnorePointer(child: CustomPaint(painter: _RingPainter(this))),
165 ],
166 ),
167 );
169 @override
170 List<DemoControl> controls(DemoContext context) => <DemoControl>[
171 ChoiceControl(
172 'Ring around',
173 options: const <String>['cube', 'sphere', 'pillar', 'all three'],
174 index: () => ringed,
175 onChanged: (int i) => ringed = i,
176 ),
177 ];
179 @override
180 void verify(Scene scene, FrameResult frame) {
181 if (frame.drawCalls < 1) {
182 throw StateError('the scene was not drawn');
183 }
184 const double width = 320.0;
185 const double height = 180.0;
186 final viewProjection = _camera.viewProjection(width / height);
187 for (var which = 0; which < 3; which++) {
188 final ScreenBounds? bounds = _measure(which, width, height);
189 if (bounds == null) {
190 throw StateError('${_names[which]} is behind the camera');
191 }
192 // A real rectangle, and one that holds the point its own centre
193 // projects to.
194 if (bounds.left >= bounds.right || bounds.top >= bounds.bottom) {
195 throw StateError('${_names[which]} got a degenerate rectangle');
196 }
197 final Vector3 centre = _boxOf(which).center;
198 final Vector4 clip = viewProjection.transformed(
199 Vector4(centre.x, centre.y, centre.z, 1.0),
200 );
201 final double x = (clip.x / clip.w * 0.5 + 0.5) * width;
202 final double y = (0.5 - clip.y / clip.w * 0.5) * height;
203 if (x < bounds.left ||
204 x > bounds.right ||
205 y < bounds.top ||
206 y > bounds.bottom) {
207 throw StateError(
208 '${_names[which]} is not inside the rectangle its own box gave',
209 );
210 }
211 }
212 }
215/// Draws the rectangles, at whatever size the frame is being shown.
216final class _RingPainter extends CustomPainter {
217 _RingPainter(this._demo);
219 final ScreenBoundsDemo _demo;
221 @override
222 void paint(Canvas canvas, Size size) {
223 final List<int> which = _demo.ringed == 3
224 ? const <int>[0, 1, 2]
225 : <int>[_demo.ringed];
226 for (final int i in which) {
227 final ScreenBounds? bounds = _demo._measure(i, size.width, size.height);
228 if (bounds == null) continue;
229 final Rect rect = Rect.fromLTRB(
230 bounds.left,
231 bounds.top,
232 bounds.right,
233 bounds.bottom,
234 );
235 canvas.drawRRect(
236 RRect.fromRectAndRadius(rect.inflate(3.0), const Radius.circular(4.0)),
237 Paint()
238 ..style = PaintingStyle.stroke
239 ..strokeWidth = 2.0
240 ..color = ScreenBoundsDemo._colours[i],
241 );
242 final TextPainter label = TextPainter(
243 text: TextSpan(
244 text:
245 '${ScreenBoundsDemo._names[i]} '
246 '${bounds.left.round()},${bounds.top.round()} '
247 '${rect.width.round()}x${rect.height.round()}',
248 style: TextStyle(
249 color: ScreenBoundsDemo._colours[i],
250 fontSize: 11.0,
251 fontFamily: 'RobotoMono',
252 ),
253 ),
254 textDirection: TextDirection.ltr,
255 )..layout();
256 label.paint(canvas, Offset(rect.left, math.max(rect.top - 20.0, 2.0)));
257 }
258 }
260 @override
261 bool shouldRepaint(_RingPainter oldDelegate) => true;