use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_math::prelude::*;
use bevy_transform::prelude::*;
use firewheel::{nodes::spatial_basic::SpatialBasicNode, vector};
use crate::{SeedlingSystems, nodes::itd::ItdNode, pool::sample_effects::EffectOf};
pub(crate) struct SpatialPlugin;
impl Plugin for SpatialPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DefaultSpatialScale>().add_systems(
Last,
(
update_2d_emitters,
update_2d_emitters_effects,
update_3d_emitters,
update_3d_emitters_effects,
update_itd_effects,
#[cfg(feature = "hrtf")]
spatial_hrtf::update_hrtf_effects,
)
.after(SeedlingSystems::Pool)
.before(SeedlingSystems::Queue),
);
}
}
#[derive(Component, Debug, Clone, Copy)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SpatialScale(pub Vec3);
impl Default for SpatialScale {
fn default() -> Self {
Self(Vec3::ONE)
}
}
#[derive(Resource, Debug, Clone, Copy)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct DefaultSpatialScale(pub Vec3);
impl Default for DefaultSpatialScale {
fn default() -> Self {
Self(Vec3::ONE)
}
}
#[derive(Debug, Default, Component)]
#[require(Transform)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SpatialListener2D;
#[derive(Debug, Default, Component)]
#[require(Transform)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SpatialListener3D;
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);
let mut world_offset = emitter_pos - listener.translation;
world_offset.z = 0.0;
let local_offset = (listener.rotation.inverse() * world_offset) * scale;
spatial.offset = vector::Vec3::new(local_offset.x, 0.0, local_offset.y);
}
}
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);
let mut world_offset = emitter_pos - listener.translation;
world_offset.z = 0.0;
let local_offset = (listener.rotation.inverse() * world_offset) * scale;
spatial.offset = vector::Vec3::new(local_offset.x, 0.0, local_offset.y);
}
}
fn update_itd_effects(
listeners: Query<&GlobalTransform, Or<(With<SpatialListener2D>, With<SpatialListener3D>)>>,
mut emitters: Query<(&mut ItdNode, &EffectOf)>,
effect_parents: Query<&GlobalTransform>,
) {
for (mut spatial, 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 world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
spatial.direction = local_offset;
}
}
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);
let world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
spatial.offset = (local_offset * scale).into();
}
}
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);
let world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
spatial.offset = (local_offset * scale).into();
}
}
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(feature = "hrtf")]
mod spatial_hrtf {
use super::*;
use crate::prelude::hrtf::HrtfNode;
pub(super) fn update_hrtf_effects(
listeners: Query<&GlobalTransform, Or<(With<SpatialListener2D>, With<SpatialListener3D>)>>,
mut emitters: Query<(&mut HrtfNode, 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);
let world_offset = emitter_pos - listener.translation;
let local_offset = listener.rotation.inverse() * world_offset;
let local_offset = local_offset * scale;
spatial.offset = local_offset * scale;
}
}
}
#[cfg(test)]
mod test {
use bevy_asset::AssetServer;
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: Vec3 = effect.single().unwrap().offset.into();
assert_eq!(effect, position);
true
} else {
false
}
},
);
if complete {
break;
}
app.update();
}
}
}