use bevy::prelude::*;
use firewheel::nodes::spatial_basic::SpatialBasicNode;
use crate::pool::sample_effects::EffectOf;
#[derive(Component, Debug, Clone, Copy)]
pub struct SpatialScale(pub Vec3);
impl Default for SpatialScale {
fn default() -> Self {
Self(Vec3::ONE)
}
}
#[derive(Resource, Debug, Default, Clone)]
pub struct DefaultSpatialScale(SpatialScale);
impl core::ops::Deref for DefaultSpatialScale {
type Target = SpatialScale;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl core::ops::DerefMut for DefaultSpatialScale {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Default, Component)]
#[require(Transform)]
pub struct SpatialListener2D;
#[derive(Debug, Default, Component)]
#[require(Transform)]
pub struct SpatialListener3D;
pub(crate) fn update_2d_emitters(
listeners: Query<&GlobalTransform, With<SpatialListener2D>>,
mut emitters: Query<(
&mut SpatialBasicNode,
Option<&SpatialScale>,
&GlobalTransform,
)>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, transform) in emitters.iter_mut() {
let emitter_pos = transform.translation();
let closest_listener = find_closest_listener(
emitter_pos,
listeners.iter().map(GlobalTransform::compute_transform),
);
let Some(listener) = closest_listener else {
continue;
};
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0.0);
let mut world_offset = emitter_pos - listener.translation;
world_offset.z = 0.0;
let local_offset = (listener.rotation.inverse() * world_offset) * scale;
spatial.offset = Vec3::new(local_offset.x, 0.0, local_offset.y);
}
}
pub(crate) fn update_2d_emitters_effects(
listeners: Query<&GlobalTransform, With<SpatialListener2D>>,
mut emitters: Query<(&mut SpatialBasicNode, Option<&SpatialScale>, &EffectOf)>,
effect_parents: Query<&GlobalTransform>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, effect_of) in emitters.iter_mut() {
let Ok(transform) = effect_parents.get(effect_of.0) else {
continue;
};
let emitter_pos = transform.translation();
let closest_listener = find_closest_listener(
emitter_pos,
listeners.iter().map(GlobalTransform::compute_transform),
);
let Some(listener) = closest_listener else {
continue;
};
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0.0);
let mut world_offset = emitter_pos - listener.translation;
world_offset.z = 0.0;
let local_offset = (listener.rotation.inverse() * world_offset) * scale;
spatial.offset = Vec3::new(local_offset.x, 0.0, local_offset.y);
}
}
pub(crate) fn update_3d_emitters(
listeners: Query<&GlobalTransform, With<SpatialListener3D>>,
mut emitters: Query<(
&mut SpatialBasicNode,
Option<&SpatialScale>,
&GlobalTransform,
)>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, transform) in emitters.iter_mut() {
let emitter_pos = transform.translation();
let closest_listener = find_closest_listener(
emitter_pos,
listeners.iter().map(GlobalTransform::compute_transform),
);
let Some(listener) = closest_listener else {
continue;
};
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0.0);
let world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
spatial.offset = local_offset * scale;
}
}
pub(crate) fn update_3d_emitters_effects(
listeners: Query<&GlobalTransform, With<SpatialListener3D>>,
mut emitters: Query<(&mut SpatialBasicNode, Option<&SpatialScale>, &EffectOf)>,
effect_parents: Query<&GlobalTransform>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, effect_of) in emitters.iter_mut() {
let Ok(transform) = effect_parents.get(effect_of.0) else {
continue;
};
let emitter_pos = transform.translation();
let closest_listener = find_closest_listener(
emitter_pos,
listeners.iter().map(GlobalTransform::compute_transform),
);
let Some(listener) = closest_listener else {
continue;
};
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0.0);
let world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
spatial.offset = local_offset * scale;
}
}
fn find_closest_listener(
emitter_pos: Vec3,
listeners: impl Iterator<Item = Transform>,
) -> Option<Transform> {
let mut closest_listener: Option<(f32, Transform)> = None;
for listener in listeners {
let listener_pos = listener.translation;
let distance = emitter_pos.distance_squared(listener_pos);
match &mut closest_listener {
None => closest_listener = Some((distance, listener)),
Some((old_distance, old_transform)) => {
if distance < *old_distance {
*old_distance = distance;
*old_transform = listener;
}
}
}
}
closest_listener.map(|l| l.1)
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
node::follower::FollowerOf,
pool::Sampler,
prelude::*,
test::{prepare_app, run},
};
#[test]
fn test_closest() {
let positions = [Vec3::splat(5.0), Vec3::splat(4.0), Vec3::splat(6.0)]
.into_iter()
.map(Transform::from_translation)
.collect::<Vec<_>>();
let emitter = Vec3::splat(0.0);
let closest = find_closest_listener(emitter, positions.iter().copied()).unwrap();
assert_eq!(closest, positions[1]);
}
#[test]
fn test_empty() {
let positions = [];
let emitter = Vec3::splat(0.0);
let closest = find_closest_listener(emitter, positions.iter().copied());
assert!(closest.is_none());
}
#[derive(PoolLabel, PartialEq, Eq, Hash, Clone, Debug)]
struct TestPool;
#[test]
fn test_immediate_positioning() {
let position = Vec3::splat(3.0);
let mut app = prepare_app(move |mut commands: Commands, server: Res<AssetServer>| {
commands.spawn((
SamplerPool(TestPool),
sample_effects![SpatialBasicNode::default()],
));
commands.spawn((SpatialListener3D, Transform::default()));
commands.spawn((
TestPool,
Transform::from_translation(position),
SamplePlayer::new(server.load("sine_440hz_1ms.wav")).looping(),
));
});
loop {
let complete = run(
&mut app,
move |player: Query<&Sampler>,
effect: Query<&SpatialBasicNode, With<FollowerOf>>| {
if player.iter().len() == 1 {
let effect = effect.single().unwrap();
assert_eq!(effect.offset, position);
true
} else {
false
}
},
);
if complete {
break;
}
app.update();
}
}
}