flutter3d
Showcase Changelog 38 packages API reference

Occlusion: the source

since 0.1.0 or earlier Simulation, audio and XR

Regions the guide quotes: live (line 70), wall (line 132), scene (line 141), listen (line 145).

1/// What the world does to a sound between there and here, as a callback the
2/// scene asks and never a wall test of its own.
3///
4/// Quoted by `audio_occlusion.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_audio/flutter3d_audio.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
13import 'package:vector_math/vector_math.dart';
15final class AudioOcclusionDemo extends ShowcaseDemo {
16 late final String _report;
18 double listenerX = 0.0;
19 bool walking = true;
21 late final AudioScene _audio;
22 late final SoundEmitter _bell;
23 late final AudioListener _listener;
24 late final MeshNode _source;
25 late final MeshNode _head;
26 late final BarGauge _gain;
27 late final BarGauge _muffle;
28 double _clock = 0.0;
30 @override
31 void configureView(DemoContext context) {
32 context.orbit
33 ..distance = 12.0
34 ..pitch = 0.8
35 ..yaw = 0.0;
36 context.orbit.target.setValues(1.0, 0.0, 1.0);
37 }
39 @override
40 Scene build(DemoContext context) {
41 _report = _run();
42 _source = ballNode(
43 context,
44 'bell',
45 0.5,
46 Vector4(0.9, 0.75, 0.3, 1.0),
47 at: Vector3(4.0, 0.5, 0.0),
48 );
49 _head = blockNode(
50 context,
51 'listener',
52 Vector3(0.7, 0.7, 0.7),
53 Vector4(0.45, 0.65, 0.95, 1.0),
54 );
55 _gain = BarGauge(
56 context,
57 'gain',
58 Vector4(0.9, 0.75, 0.3, 1.0),
59 Vector3(-4.0, 0.0, 4.5),
60 height: 5.0,
61 );
62 _muffle = BarGauge(
63 context,
64 'muffle',
65 Vector4(0.7, 0.4, 0.85, 1.0),
66 Vector3(-4.0, 0.0, 5.5),
67 height: 5.0,
68 );
70 // The same scene and the same question, asked every frame while the
71 // listener walks: the wall is only ever the callback.
72 _audio = AudioScene(backend: SilentBackend(), occlusion: _occlusion);
73 _bell = _audio.play(
74 const SoundDef(name: 'bell', asset: 'bell.wav', loop: true),
75 Vector3(4.0, 0.0, 0.0),
76 );
77 _listener = AudioListener(position: Vector3.zero());
78 return sceneOf(<SceneNode>[
79 floorNode(context, width: 18.0, depth: 14.0),
80 blockNode(
81 context,
82 'wall',
83 Vector3(0.3, 2.0, 6.0),
84 Vector4(0.6, 0.5, 0.4, 1.0),
85 at: Vector3(2.0, 1.0, 0.0),
86 ),
87 _source,
88 _head,
89 ..._gain.nodes,
90 ..._muffle.nodes,
91 ]);
92 }
94 @override
95 void update(DemoContext context, double dt) {
96 _clock += dt;
97 if (walking) listenerX = 2.0 + 5.5 * math.sin(_clock * 0.45);
98 _listener.position.setValues(listenerX, 0.0, 0.0);
99 _audio.update(_listener);
100 _head.setPosition(listenerX, 0.35, 0.0);
101 _gain.set(_bell.audibleGain);
102 _muffle.set(_bell.muffle);
103 // A muffled bell goes dull.
104 _source.material.baseColor.setValues(
105 0.9 - 0.45 * _bell.muffle,
106 0.75 - 0.3 * _bell.muffle,
107 0.3 + 0.2 * _bell.muffle,
108 1.0,
109 );
110 }
112 @override
113 List<DemoControl> controls(DemoContext context) => <DemoControl>[
114 SliderControl(
115 'Listener at',
116 min: -6.0,
117 max: 8.0,
118 value: () => listenerX,
119 onChanged: (double v) {
120 walking = false;
121 listenerX = v;
122 },
123 format: (double v) => '${v.toStringAsFixed(1)} m',
124 ),
125 ToggleControl(
126 'Walk about',
127 value: () => walking,
128 onChanged: (bool v) => walking = v,
129 ),
130 ];
132 /// A wall at x = 2: a straight line that crosses it is half heard, and one
133 /// that does not is heard clearly. The scene never learns what a wall is;
134 /// it only asks this question.
135 static double _occlusion(Vector3 from, Vector3 to) {
136 final crossesWall = (from.x - 2.0).sign != (to.x - 2.0).sign;
137 return crossesWall ? 0.5 : 1.0;
138 }
140 static String _run() {
141 final scene = AudioScene(backend: SilentBackend(), occlusion: _occlusion);
142 const bell = SoundDef(name: 'bell', asset: 'bell.wav', loop: true);
143 final behindTheWall = scene.play(bell, Vector3(4.0, 0.0, 0.0));
145 final listener = AudioListener(position: Vector3(0.0, 0.0, 0.0));
146 scene.update(listener);
147 final gainBehindWall = behindTheWall.audibleGain;
148 final muffleBehindWall = behindTheWall.muffle;
150 listener.position.setValues(5.0, 0.0, 0.0);
151 scene.update(listener);
152 final gainSameSide = behindTheWall.audibleGain;
154 return 'listener at 0, sound past the wall at 4: gain '
155 '${gainBehindWall.toStringAsFixed(3)}, muffle '
156 '${muffleBehindWall.toStringAsFixed(2)}\n'
157 'listener moved to 5, same side as the sound now: gain '
158 '${gainSameSide.toStringAsFixed(3)}';
159 }
161 @override
162 void verify(Scene scene, FrameResult frame) {
163 if (frame.drawCalls < 1) {
164 throw StateError('the wall marker was not drawn');
165 }
166 if (!_report.contains('muffle 0.50')) {
167 throw StateError(
168 'a sound heard through the wall should be muffled '
169 'by half',
170 );
171 }
172 }