use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam};
use bevy_math::prelude::*;
use bevy_transform::prelude::*;
use firewheel::nodes::spatial_basic::SpatialBasicNode;
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_basic,
update_itd,
#[cfg(feature = "hrtf")]
spatial_hrtf::update_hrtf,
)
.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;
#[derive(SystemParam)]
struct SpatialListeners<'w, 's> {
listeners: Query<
'w,
's,
(
&'static GlobalTransform,
AnyOf<(&'static SpatialListener2D, &'static SpatialListener3D)>,
),
>,
}
enum SpatialKind {
Listener2D,
Listener3D,
}
impl From<(Option<&'_ SpatialListener2D>, Option<&'_ SpatialListener3D>)> for SpatialKind {
fn from(value: (Option<&'_ SpatialListener2D>, Option<&'_ SpatialListener3D>)) -> Self {
match value {
(Some(_), None) => Self::Listener2D,
(None, Some(_)) => Self::Listener3D,
_ => unreachable!(),
}
}
}
impl SpatialListeners<'_, '_> {
fn nearest_listener(&self, emitter: Vec3) -> Option<(Transform, SpatialKind)> {
self.listeners
.iter()
.map(|(transform, kind)| {
let transform = transform.compute_transform();
let kind = SpatialKind::from(kind);
let distance = match kind {
SpatialKind::Listener2D => {
emitter.xy().distance_squared(transform.translation.xy())
}
SpatialKind::Listener3D => emitter.distance_squared(transform.translation),
};
(transform, kind, distance)
})
.min_by(|(.., a), (.., b)| a.total_cmp(b))
.map(|(transform, kind, ..)| (transform, kind))
}
fn calculate_offset(&self, emitter: Vec3) -> Option<Vec3> {
let (listener, kind) = self.nearest_listener(emitter)?;
let mut world_offset = emitter - listener.translation;
match kind {
SpatialKind::Listener2D => {
world_offset.z = 0.0;
let local_offset = listener.rotation.inverse() * world_offset;
Some(Vec3::new(local_offset.x, 0.0, local_offset.y))
}
SpatialKind::Listener3D => {
let local_offset = listener.rotation.inverse() * world_offset;
Some(local_offset)
}
}
}
}
type EffectTransform = AnyOf<(&'static GlobalTransform, &'static EffectOf)>;
fn extract_effect_transform(
effect_transform: <EffectTransform as QueryData>::Item<'_, '_>,
transforms: &Query<&GlobalTransform>,
) -> Option<Vec3> {
match effect_transform {
(Some(global), _) => Some(global.translation()),
(_, Some(parent)) => match transforms.get(parent.0) {
Ok(global) => Some(global.translation()),
Err(_) => None,
},
_ => unreachable!(),
}
}
fn update_basic(
listeners: SpatialListeners,
mut emitters: Query<(
&mut SpatialBasicNode,
Option<&SpatialScale>,
EffectTransform,
)>,
transforms: Query<&GlobalTransform>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, transform) in emitters.iter_mut() {
if let Some(emitter_pos) = extract_effect_transform(transform, &transforms)
&& let Some(offset) = listeners.calculate_offset(emitter_pos)
{
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0);
spatial.offset = (offset * scale).into();
}
}
}
fn update_itd(
listeners: SpatialListeners,
mut emitters: Query<(&mut ItdNode, EffectTransform)>,
transforms: Query<&GlobalTransform>,
) {
for (mut spatial, transform) in emitters.iter_mut() {
if let Some(emitter_pos) = extract_effect_transform(transform, &transforms)
&& let Some(offset) = listeners.calculate_offset(emitter_pos)
{
spatial.direction = offset;
}
}
}
#[cfg(feature = "hrtf")]
mod spatial_hrtf {
use super::*;
use crate::prelude::hrtf::HrtfNode;
pub(super) fn update_hrtf(
listeners: SpatialListeners,
mut emitters: Query<(&mut HrtfNode, Option<&SpatialScale>, EffectTransform)>,
transforms: Query<&GlobalTransform>,
default_scale: Res<DefaultSpatialScale>,
) {
for (mut spatial, scale, transform) in emitters.iter_mut() {
if let Some(emitter_pos) = extract_effect_transform(transform, &transforms)
&& let Some(offset) = listeners.calculate_offset(emitter_pos)
{
let scale = scale.map(|s| s.0).unwrap_or(default_scale.0);
spatial.offset = 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 mut app = prepare_app({
let positions = positions.clone();
move |mut commands: Commands| {
for position in &positions {
commands.spawn((SpatialListener3D, *position));
}
}
});
let closest = run(&mut app, |listeners: SpatialListeners| {
let emitter = Vec3::splat(0.0);
listeners.nearest_listener(emitter).unwrap()
});
assert_eq!(closest.0, positions[1]);
}
#[test]
fn test_empty() {
let positions = []
.into_iter()
.map(Transform::from_translation)
.collect::<Vec<_>>();
let mut app = prepare_app({
let positions = positions.clone();
move |mut commands: Commands| {
for position in &positions {
commands.spawn((SpatialListener3D, *position));
}
}
});
let closest = run(&mut app, |listeners: SpatialListeners| {
let emitter = Vec3::splat(0.0);
listeners.nearest_listener(emitter)
});
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();
}
}
}