use crate::dsp::bands::NUM_BANDS;
use crate::effects::direct::DirectApplyFlags;
use crate::models::air_absorption::AirAbsorptionModel;
use crate::models::directivity::Directivity;
use crate::models::distance_attenuation::DistanceAttenuationModel;
use crate::models::propagation_medium::SPEED_OF_SOUND;
use crate::scene::coordinate_space::CoordinateSpace3f;
use crate::scene::ray::Ray;
use crate::scene::sampling::{generate_sphere_volume_sample, transform_sphere_volume_sample};
use crate::scene::sphere::Sphere;
use crate::scene::Scene;
use glam::Vec3;
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, Copy)]
pub enum OcclusionType {
Raycast,
Volumetric,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct DirectSoundPath {
pub distance_attenuation: f32,
pub air_absorption: [f32; NUM_BANDS],
pub delay: f32,
pub occlusion: f32,
pub transmission: [f32; NUM_BANDS],
pub directivity: f32,
}
impl Default for DirectSoundPath {
fn default() -> Self {
Self {
distance_attenuation: 1.0,
air_absorption: [1.0, 1.0, 1.0],
delay: 0.0,
occlusion: 1.0,
transmission: [0.1, 0.1, 0.1],
directivity: 0.0,
}
}
}
pub struct DirectSimulator {
sphere_volume_samples: Vec<Vec3>,
}
impl DirectSimulator {
pub fn new(max_occlusion_samples: usize) -> Self {
let mut sphere_volume_samples = Vec::new();
for i in 0..max_occlusion_samples {
sphere_volume_samples.push(generate_sphere_volume_sample(i));
}
Self {
sphere_volume_samples,
}
}
pub fn simulate(
&self,
scene: &Scene,
flags: DirectApplyFlags,
source: &CoordinateSpace3f,
listener: &CoordinateSpace3f,
distance_attenuation_model: &impl DistanceAttenuationModel,
air_absorption_model: &impl AirAbsorptionModel,
directivity: Directivity,
occlusion_type: OcclusionType,
occlusion_radius: f32,
num_occlusion_samples: usize,
num_transmission_rays: usize,
direct_sound_path: &mut DirectSoundPath,
) {
let distance = (source.origin - listener.origin).length();
if flags.contains(DirectApplyFlags::DistanceAttenuation) {
direct_sound_path.distance_attenuation = distance_attenuation_model.evaluate(distance);
} else {
direct_sound_path.distance_attenuation = 1.0
}
if flags.contains(DirectApplyFlags::AirAbsorption) {
for i in 0..NUM_BANDS {
direct_sound_path.air_absorption[i] = air_absorption_model.evaluate(distance, i);
}
} else {
direct_sound_path.air_absorption.fill(1.0);
}
if flags.contains(DirectApplyFlags::Delay) {
direct_sound_path.delay = Self::direct_path_delay(listener.origin, source.origin);
} else {
direct_sound_path.delay = 0.0;
}
if flags.contains(DirectApplyFlags::Directivity) {
direct_sound_path.directivity = directivity.evaluate_at(listener.origin, source);
} else {
direct_sound_path.directivity = 1.0;
}
if flags.contains(DirectApplyFlags::Occlusion) {
match occlusion_type {
OcclusionType::Raycast => {
direct_sound_path.occlusion =
Self::raycast_occlusion(scene, listener.origin, source.origin);
}
OcclusionType::Volumetric => {
direct_sound_path.occlusion = self.raycast_volumetric(
scene,
listener.origin,
source.origin,
occlusion_radius,
num_occlusion_samples,
);
}
}
} else {
direct_sound_path.occlusion = 1.0
}
if flags.contains(DirectApplyFlags::Transmission) {
self.transmission(
scene,
listener.origin,
source.origin,
&mut direct_sound_path.transmission,
num_transmission_rays,
);
} else {
direct_sound_path.transmission.fill(1.0);
}
}
fn direct_path_delay(listener: Vec3, source: Vec3) -> f32 {
(source - listener).length() / SPEED_OF_SOUND
}
fn raycast_occlusion(scene: &Scene, listener_position: Vec3, source_position: Vec3) -> f32 {
match scene.is_occluded(listener_position, source_position) {
false => 1.0,
true => 0.0,
}
}
fn raycast_volumetric(
&self,
scene: &Scene,
listener_position: Vec3,
source_position: Vec3,
source_radius: f32,
num_samples: usize,
) -> f32 {
let mut occlusion: f32 = 0.0;
let mut num_valid_samples = 0;
let num_samples = self.sphere_volume_samples.len().min(num_samples);
for i in 0..num_samples {
let sphere = Sphere::new(source_position, source_radius);
let sample = transform_sphere_volume_sample(self.sphere_volume_samples[i], sphere);
if scene.is_occluded(source_position, sample) {
continue;
}
num_valid_samples += 1;
if !scene.is_occluded(listener_position, sample) {
occlusion += 1.0;
}
}
if num_valid_samples == 0 {
return 0.0;
}
occlusion / num_valid_samples as f32
}
fn transmission(
&self,
scene: &Scene,
listener_position: Vec3,
source_position: Vec3,
transmission_factors: &mut [f32],
num_transmission_rays: usize,
) {
if num_transmission_rays == 0 {
return;
}
let ray_offset = 1e-2f32;
let rays = [
Ray::new(
listener_position,
(source_position - listener_position).normalize(),
),
Ray::new(
source_position,
(listener_position - source_position).normalize(),
),
];
let mut current_ray_index = 0;
let mut hit_count = 0;
let mut min_distances: [f32; 2] = [0.0, 0.0];
let max_distance = (source_position - listener_position).length();
let mut accumulated_transmission: [f32; NUM_BANDS] = [1.0, 1.0, 1.0];
for _ in 0..num_transmission_rays {
let ray = &rays[current_ray_index];
let min_distance = &mut min_distances[current_ray_index];
let hit = scene.closest_hit(ray, *min_distance, max_distance);
if hit.is_none() {
break;
}
let hit = hit.unwrap();
hit_count += 1;
for j in 0..NUM_BANDS {
accumulated_transmission[j] *= hit.material.transmission[j];
}
*min_distance = hit.distance + ray_offset;
if *min_distance >= max_distance {
break;
}
if (min_distances[0] + min_distances[1]) >= max_distance {
break;
}
current_ray_index = 1 - current_ray_index;
}
if hit_count <= 1 {
transmission_factors.copy_from_slice(accumulated_transmission.as_slice());
} else {
for i in 0..NUM_BANDS {
transmission_factors[i] = accumulated_transmission[i].sqrt();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use plotters::prelude::*;
const OUT_FILE_NAME: &str = "figures/sphere_volume_samples.gif";
#[ignore = "visual check only."]
#[test]
fn direct_simulator_samples() {
let num_frames_gif = 30;
let max_occlusion_samples = 100;
let num_samples_source = 10;
let simulator = DirectSimulator::new(max_occlusion_samples);
let mut points_source = simulator.sphere_volume_samples;
let points_remaining = points_source.split_off(num_samples_source);
let drawing_area = BitMapBackend::gif(OUT_FILE_NAME, (1024, 760), 100)
.unwrap()
.into_drawing_area();
let x_axis = (-1.5..1.5).step(0.1);
let z_axis = (-1.5..1.5).step(0.1);
for yaw in 0..num_frames_gif {
drawing_area.fill(&WHITE).unwrap();
let mut chart = ChartBuilder::on(&drawing_area)
.caption(
"Volumetric Occlusion Sampling Sphere".to_string(),
("sans", 20),
)
.build_cartesian_3d(x_axis.clone(), -1.5..1.5, z_axis.clone())
.unwrap();
chart.with_projection(|mut pb| {
pb.yaw = 1.00 - ((num_frames_gif as f64 / 100.0) - yaw as f64 / 50.0).abs();
pb.scale = 0.9;
pb.into_matrix()
});
chart
.configure_axes()
.light_grid_style(BLACK.mix(0.15))
.max_light_lines(3)
.draw()
.unwrap();
chart
.draw_series(points_source.iter().map(|point| {
Circle::new(
(point.x as f64, point.y as f64, point.z as f64),
2,
RED.filled(),
)
}))
.unwrap();
chart
.draw_series(points_remaining.iter().map(|point| {
Circle::new(
(point.x as f64, point.y as f64, point.z as f64),
2,
BLUE.filled(),
)
}))
.unwrap();
drawing_area.present().unwrap();
}
drawing_area.present().expect(
"Unable to write result to file, please make sure 'figures' dir exists in crate dir",
);
println!("Result has been saved to {}", OUT_FILE_NAME);
}
}