flutter3d
Showcase Changelog 38 packages API reference

Distance rolloff: the source

since 0.1.0 Simulation, audio and XR

Regions the guide quotes: curves (line 34), sample (line 41), compare (line 87).

1/// Three curves for how a sound gets quieter with distance, and what each
2/// one answers at the same handful of distances.
3///
4/// Quoted by `audio_rolloff.md` and shown whole in the Source tab.
5library;
6
7import 'package:flutter/widgets.dart';
8import 'package:flutter3d/flutter3d.dart';
9import 'package:flutter3d_audio/flutter3d_audio.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class AudioRolloffDemo extends ShowcaseDemo {
14 @override
15 Scene build(DemoContext context) {
16 // The printed answer, for the second step of the guide.
17 _run();
18 final material = Material(
19 name: 'speaker',
20 baseColor: Vector4(0.4, 0.6, 0.9, 1.0),
21 );
22 final node = MeshNode(
23 DeviceMesh.upload(context.device, SphereShape(segments: 16).build()),
24 material,
25 );
26 return Scene()
27 ..add(node)
28 ..add(
29 LightNode(name: 'sun', intensity: 3.0)
30 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
31 );
32 }
34 static const curves = <String, Attenuation>{
35 'inverse': InverseRolloff(),
36 'linear': LinearRolloff(),
37 'exponential': ExponentialRolloff(),
38 };
40 static String _run() {
41 const distances = <double>[1.0, 5.0, 10.0, 20.0];
42 final lines = <String>[
43 for (final entry in curves.entries)
44 '${entry.key}: ${[for (final d in distances) entry.value.gainAt(d).toStringAsFixed(2)].join(', ')}',
45 ];
46 return 'gain at ${distances.join(', ')} metres\n${lines.join('\n')}';
47 }
49 double distance = 6.0;
51 static const List<Color> _inks = <Color>[
52 Color(0xFF7FB0FF),
53 Color(0xFF6CE07C),
54 Color(0xFFE8A33D),
55 ];
57 @override
58 List<DemoControl> controls(DemoContext context) => <DemoControl>[
59 SliderControl(
60 'Distance',
61 min: 0.0,
62 max: 30.0,
63 value: () => distance,
64 onChanged: (double v) => distance = v,
65 format: (double v) => '${v.toStringAsFixed(1)} m',
66 ),
67 ];
69 @override
70 Widget? customBody(BuildContext buildContext, DemoContext context) =>
71 ColoredBox(
72 color: const Color(0xFF14161A),
73 child: Padding(
74 padding: const EdgeInsets.all(24),
75 child: CustomPaint(
76 painter: _CurvePainter(distance, _inks),
77 child: const SizedBox.expand(),
78 ),
79 ),
80 );
82 @override
83 void verify(Scene scene, FrameResult frame) {
84 if (frame.drawCalls < 1) {
85 throw StateError('the speaker marker was not drawn');
86 }
87 const inverse = InverseRolloff();
88 const linear = LinearRolloff();
89 final closer = inverse.gainAt(5.0) > inverse.gainAt(10.0);
90 final silentPastMaximum = linear.gainAt(linear.maximum + 1.0) <= 0.0;
91 if (!closer || !silentPastMaximum) {
92 throw StateError(
93 'every curve should get quieter with distance and go silent past '
94 'its maximum',
95 );
96 }
97 }
100/// The three curves against distance, with a marker where the slider is and
101/// what each curve answers there.
102final class _CurvePainter extends CustomPainter {
103 const _CurvePainter(this.distance, this.inks);
105 final double distance;
106 final List<Color> inks;
108 static const double _reach = 30.0;
110 @override
111 void paint(Canvas canvas, Size size) {
112 final Rect plot = Rect.fromLTWH(
113 48.0,
114 8.0,
115 size.width - 64.0,
116 size.height - 56.0,
117 );
118 final Paint axis = Paint()
119 ..color = const Color(0x55FFFFFF)
120 ..style = PaintingStyle.stroke;
121 canvas.drawRect(plot, axis);
122 Offset at(double d, double gain) => Offset(
123 plot.left + d / _reach * plot.width,
124 plot.bottom - gain.clamp(0.0, 1.0) * plot.height,
125 );
126 void label(String text, Offset where, Color colour) {
127 final TextPainter painter = TextPainter(
128 text: TextSpan(
129 text: text,
130 style: TextStyle(color: colour, fontSize: 13),
131 ),
132 textDirection: TextDirection.ltr,
133 )..layout();
134 painter.paint(canvas, where);
135 }
137 for (var m = 0; m <= 30; m += 5) {
138 final Offset x = at(m.toDouble(), 0.0);
139 canvas.drawLine(
140 x,
141 Offset(x.dx, plot.top),
142 axis..color = const Color(0x22FFFFFF),
143 );
144 label(
145 '$m m',
146 Offset(x.dx - 10, plot.bottom + 4),
147 const Color(0xAAFFFFFF),
148 );
149 }
150 for (var g = 0; g <= 4; g++) {
151 label(
152 '${g * 25}%',
153 Offset(6, plot.bottom - g / 4 * plot.height - 8),
154 const Color(0xAAFFFFFF),
155 );
156 }
158 var i = 0;
159 for (final MapEntry<String, Attenuation> entry
160 in AudioRolloffDemo.curves.entries) {
161 final Path path = Path();
162 for (var step = 0; step <= 150; step++) {
163 final double d = step / 150 * _reach;
164 // A gain never runs off the top: at the source itself it is 1.
165 final Offset p = at(d, entry.value.gainAt(d));
166 step == 0 ? path.moveTo(p.dx, p.dy) : path.lineTo(p.dx, p.dy);
167 }
168 canvas.drawPath(
169 path,
170 Paint()
171 ..color = inks[i]
172 ..style = PaintingStyle.stroke
173 // Widest first: two curves that coincide stay both visible.
174 ..strokeWidth = 7.0 - 2.0 * i,
175 );
176 final double here = entry.value.gainAt(distance);
177 canvas.drawCircle(at(distance, here), 5.0, Paint()..color = inks[i]);
178 label(
179 '${entry.key}: ${(here * 100).round()}%',
180 Offset(plot.right - 170, plot.top + 8 + 18.0 * i),
181 inks[i],
182 );
183 i++;
184 }
185 canvas.drawLine(
186 at(distance, 0.0),
187 at(distance, 1.0),
188 Paint()
189 ..color = const Color(0x88FFFFFF)
190 ..strokeWidth = 1.5,
191 );
192 }
194 @override
195 bool shouldRepaint(covariant _CurvePainter oldDelegate) => true;