flutter3d
Showcase Changelog 38 packages API reference

Portable determinism: the source

since 0.5.1 Simulation, audio and XR

Regions the guide quotes: live (line 107), trig (line 165), roll (line 170), resume (line 175).

1/// Determinism in a step: transcendental functions computed the same way on
2/// every platform, and a random generator whose state can be written down and
3/// restored to continue the same sequence.
4///
5/// Quoted by `portable_math.md` and shown whole in the Source tab.
6library;
7
8import 'dart:math' as math;
9
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
13import 'package:flutter3d_sim/flutter3d_sim.dart';
14import 'package:vector_math/vector_math.dart';
16final class PortableMathDemo extends ShowcaseDemo {
17 late final String _report;
19 bool rolling = true;
20 bool _rollAsked = false;
22 late GameRandom _dice;
23 late GameRandom _resumed;
24 final List<int> _seen = List<int>.filled(6, 0);
25 final List<int> _seenAgain = List<int>.filled(6, 0);
26 late final List<BarGauge> _bars;
27 late final List<BarGauge> _barsAgain;
28 late final MeshNode _hand;
29 late final MeshNode _lamp;
30 double _clock = 0.0;
31 double _owed = 0.0;
32 bool _agree = true;
34 static const int _dots = 16;
36 @override
37 void configureView(DemoContext context) {
38 context.orbit
39 ..distance = 13.0
40 ..pitch = 0.6
41 ..yaw = 0.0;
42 context.orbit.target.setValues(0.0, 0.5, 1.0);
43 }
45 @override
46 Scene build(DemoContext context) {
47 _report = _run();
48 _reseed();
49 BarGauge bar(String name, Vector4 ink, double x, double z) => BarGauge(
50 context,
51 name,
52 ink,
53 Vector3(x, 0.0, z),
54 height: 3.0,
55 width: 0.6,
56 vertical: true,
57 );
58 _bars = <BarGauge>[
59 for (var i = 0; i < 6; i++)
60 bar(
61 'face ${i + 1}',
62 Vector4(0.45, 0.65, 0.95, 1.0),
63 -2.5 + i * 1.0,
64 3.2,
65 ),
66 ];
67 _barsAgain = <BarGauge>[
68 for (var i = 0; i < 6; i++)
69 bar(
70 'again ${i + 1}',
71 Vector4(0.95, 0.65, 0.3, 1.0),
72 -2.5 + i * 1.0 + 0.35,
73 3.2,
74 ),
75 ];
76 _hand = ballNode(context, 'hand', 0.3, Vector4(0.95, 0.75, 0.3, 1.0));
77 _lamp = ballNode(
78 context,
79 'lamp',
80 0.3,
81 Vector4(0.35, 0.85, 0.4, 1.0),
82 at: Vector3(0.0, 0.3, 5.5),
83 );
84 return sceneOf(<SceneNode>[
85 floorNode(context, width: 12.0, depth: 12.0),
86 // A ring of points placed by `Portable`, and a hand that goes round it.
87 for (var i = 0; i < _dots; i++)
88 ballNode(
89 context,
90 'dot $i',
91 0.12,
92 Vector4(0.6, 0.65, 0.75, 1.0),
93 at: Vector3(
94 2.5 * Portable.sinCos(i * 2 * math.pi / _dots).cos,
95 0.12,
96 -1.5 + 2.5 * Portable.sinCos(i * 2 * math.pi / _dots).sin,
97 ),
98 ),
99 _hand,
100 _lamp,
101 for (final BarGauge g in _bars) ...g.nodes,
102 for (final BarGauge g in _barsAgain) ...g.nodes,
103 ]);
104 }
106 void _reseed() {
107 // One generator, and a second that is handed the first's state: from
108 // here the two must roll the same numbers for as long as anyone asks.
109 _dice = GameRandom(1234);
110 _resumed = GameRandom(1)..state = _dice.state;
111 _seen.fillRange(0, 6, 0);
112 _seenAgain.fillRange(0, 6, 0);
113 _agree = true;
114 }
116 @override
117 void update(DemoContext context, double dt) {
118 _clock += dt;
119 if (rolling) _owed += dt * 24.0;
120 if (_rollAsked) {
121 _rollAsked = false;
122 _owed += 30.0;
123 }
124 while (_owed >= 1.0) {
125 _owed -= 1.0;
126 final int a = _dice.nextInt(6);
127 final int b = _resumed.nextInt(6);
128 _seen[a]++;
129 _seenAgain[b]++;
130 if (a != b) _agree = false;
131 if (_seen.reduce(math.max) > 40) _reseed();
132 }
133 // The hand goes round the ring through `Portable`, not `dart:math`.
134 final ({double sin, double cos}) at = Portable.sinCos(_clock);
135 _hand.setPosition(2.5 * at.cos, 0.3, -1.5 + 2.5 * at.sin);
136 for (var i = 0; i < 6; i++) {
137 _bars[i].set(_seen[i] / 40.0);
138 _barsAgain[i].set(_seenAgain[i] / 40.0);
139 }
140 _lamp.material.baseColor.setValues(
141 _agree ? 0.35 : 0.9,
142 _agree ? 0.85 : 0.3,
143 _agree ? 0.4 : 0.3,
144 1.0,
145 );
146 }
148 @override
149 List<DemoControl> controls(DemoContext context) => <DemoControl>[
150 ToggleControl(
151 'Keep rolling',
152 value: () => rolling,
153 onChanged: (bool v) => rolling = v,
154 ),
155 ToggleControl(
156 'Roll thirty more',
157 value: () => false,
158 onChanged: (bool v) {
159 if (v) _rollAsked = true;
160 },
161 ),
162 ];
164 static String _run() {
165 // `Portable.sinCos` places a point on a circle without ever calling
166 // `dart:math`, whose trig is a platform's own library and disagrees
167 // between the Dart VM and a browser in the last few bits.
168 final direction = Portable.sinCos(0.7853981633974483); // pi / 4
170 final dice = GameRandom(1234);
171 final firstRoll = dice.nextInt(6) + 1;
172 final secondRoll = dice.nextInt(6) + 1;
173 final savedState = dice.state;
175 // A fresh generator given the saved state continues the exact same
176 // sequence, which is what lets a snapshot carry a simulation's dice
177 // forward across a save and a load.
178 final resumed = GameRandom(1)..state = savedState;
179 final thirdRoll = dice.nextInt(6) + 1;
180 final resumedRoll = resumed.nextInt(6) + 1;
182 return 'sin/cos of pi/4: ${direction.sin.toStringAsFixed(4)}, '
183 '${direction.cos.toStringAsFixed(4)}\n'
184 'rolls: $firstRoll, $secondRoll, then $thirdRoll\n'
185 'a generator resumed from the saved state rolls $resumedRoll next, '
186 'matching the original\'s $thirdRoll';
187 }
189 @override
190 void verify(Scene scene, FrameResult frame) {
191 if (frame.drawCalls < 1) {
192 throw StateError('the die was not drawn');
193 }
194 final dice = GameRandom(1234);
195 final expectedThird = () {
196 dice.nextInt(6);
197 dice.nextInt(6);
198 return dice.nextInt(6) + 1;
199 }();
200 if (!_report.contains('then $expectedThird')) {
201 throw StateError(
202 'a GameRandom seeded the same way should roll the '
203 'same sequence',
204 );
205 }
206 }