use crate::scene::aabb::Aabb;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ScatterVolume {
pub shape: ScatterShape,
pub density: f32,
pub colour: ColourSource,
pub anisotropy: f32,
pub emission: Emission,
pub density_remap: DensityRemap,
pub noise: Option<NoiseDriver>,
pub step_budget: Option<u32>,
pub density_texture: Option<crate::resources::VolumeId>,
pub refraction: Option<RefractionParams>,
}
impl Default for ScatterVolume {
fn default() -> Self {
Self {
shape: ScatterShape::Box(Aabb {
min: glam::Vec3::splat(-0.5),
max: glam::Vec3::splat(0.5),
}),
density: 0.0,
colour: ColourSource::Flat([0.8, 0.85, 0.9]),
anisotropy: 0.0,
emission: Emission::None,
density_remap: DensityRemap::Identity,
noise: None,
step_budget: None,
density_texture: None,
refraction: None,
}
}
}
impl ScatterVolume {
pub fn box_uniform(aabb: Aabb, density: f32, colour: [f32; 3]) -> Self {
Self {
shape: ScatterShape::Box(aabb),
density,
colour: ColourSource::Flat(colour),
..Default::default()
}
}
pub fn sphere_uniform(center: [f32; 3], radius: f32, density: f32, colour: [f32; 3]) -> Self {
Self {
shape: ScatterShape::Sphere { center, radius },
density,
colour: ColourSource::Flat(colour),
..Default::default()
}
}
pub fn world_aabb(&self) -> Aabb {
match self.shape {
ScatterShape::Box(b) => b,
ScatterShape::Sphere { center, radius } => {
let c = glam::Vec3::from(center);
let r = glam::Vec3::splat(radius);
Aabb {
min: c - r,
max: c + r,
}
}
}
}
pub fn shape_centre(&self) -> [f32; 3] {
match self.shape {
ScatterShape::Box(b) => {
let c = (b.min + b.max) * 0.5;
[c.x, c.y, c.z]
}
ScatterShape::Sphere { center, .. } => center,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum ScatterShape {
Box(Aabb),
Sphere {
center: [f32; 3],
radius: f32,
},
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum ColourSource {
Flat([f32; 3]),
Ramp(crate::resources::ColourmapId),
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum Emission {
None,
Strength {
strength: f32,
curve: EmissionCurve,
},
}
#[derive(Debug, Clone, Copy)]
pub enum EmissionCurve {
Linear,
Power(f32),
Threshold(f32),
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum DensityRemap {
Identity,
Smoothstep {
lo: f32,
hi: f32,
},
ExpFalloff {
center: [f32; 3],
falloff: f32,
},
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct NoiseDriver {
pub scale: f32,
pub octaves: u32,
pub scroll_velocity: [f32; 3],
pub time_scale: f32,
pub lacunarity: f32,
}
impl Default for NoiseDriver {
fn default() -> Self {
Self {
scale: 1.0,
octaves: 3,
scroll_velocity: [0.0; 3],
time_scale: 0.0,
lacunarity: 2.0,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct RefractionParams {
pub strength: f32,
pub density_threshold: f32,
pub noise_scale: f32,
}
impl Default for RefractionParams {
fn default() -> Self {
Self {
strength: 0.015,
density_threshold: 0.05,
noise_scale: 1.5,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuRefractionVolume {
pub shape_kind: u32,
pub _pad0: u32,
pub _pad1: u32,
pub _pad2: u32,
pub p0: [f32; 4],
pub p1: [f32; 4],
pub params: [f32; 4],
}
impl GpuRefractionVolume {
pub fn pack(volume: &ScatterVolume, time_seconds: f32) -> Option<Self> {
let r = volume.refraction?;
if !(r.strength > 0.0) {
return None;
}
let (shape_kind, p0, p1) = match volume.shape {
ScatterShape::Box(b) => (
0u32,
[b.min.x, b.min.y, b.min.z, 0.0],
[b.max.x, b.max.y, b.max.z, 0.0],
),
ScatterShape::Sphere { center, radius } => {
(1u32, [center[0], center[1], center[2], radius], [0.0; 4])
}
};
Some(Self {
shape_kind,
_pad0: 0,
_pad1: 0,
_pad2: 0,
p0,
p1,
params: [
r.strength,
r.density_threshold.max(0.0),
r.noise_scale.max(1e-4),
time_seconds,
],
})
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuScatterVolume {
pub shape_kind: u32,
pub flags: u32,
pub remap_kind: u32,
pub emission_kind: u32,
pub p0: [f32; 4],
pub p1: [f32; 4],
pub colour_density: [f32; 4],
pub params: [f32; 4],
pub remap_data: [f32; 4],
pub remap_data2: [f32; 4],
pub noise_pack: [f32; 4],
pub noise_vel: [f32; 4],
}
pub const SCATTER_FLAG_UNLIT: u32 = 1;
pub const SCATTER_FLAG_RECEIVE_SHADOWS: u32 = 2;
pub const SCATTER_FLAG_USE_RAMP: u32 = 4;
pub const SCATTER_FLAG_USE_NOISE: u32 = 8;
pub const SCATTER_FLAG_USE_DENSITY_TEXTURE: u32 = 16;
impl GpuScatterVolume {
pub fn pack(volume: &ScatterVolume, density_multiplier: f32, flags: u32) -> Option<Self> {
let density = volume.density * density_multiplier;
if !(density > 0.0) {
return None;
}
let mut effective_flags = flags;
let colour = match volume.colour {
ColourSource::Flat(rgb) => rgb,
ColourSource::Ramp(_) => {
effective_flags |= SCATTER_FLAG_USE_RAMP;
[1.0, 1.0, 1.0]
}
};
let (shape_kind, p0, p1) = match volume.shape {
ScatterShape::Box(b) => (
0u32,
[b.min.x, b.min.y, b.min.z, 0.0],
[b.max.x, b.max.y, b.max.z, 0.0],
),
ScatterShape::Sphere { center, radius } => {
(1u32, [center[0], center[1], center[2], radius], [0.0; 4])
}
};
let anisotropy = volume.anisotropy.clamp(-0.95, 0.95);
let centre = match volume.shape {
ScatterShape::Box(b) => {
let c = (b.min + b.max) * 0.5;
[c.x, c.y, c.z]
}
ScatterShape::Sphere { center, .. } => center,
};
let (remap_kind, remap_data, remap_data2) = match volume.density_remap {
DensityRemap::Identity => (0u32, [0.0; 4], [0.0; 4]),
DensityRemap::Smoothstep { lo, hi } => (
1u32,
[centre[0], centre[1], centre[2], lo],
[hi, 0.0, 0.0, 0.0],
),
DensityRemap::ExpFalloff { center, falloff } => {
(2u32, [center[0], center[1], center[2], falloff], [0.0; 4])
}
};
let (emission_kind, emission_strength, emission_param) = match volume.emission {
Emission::None => (0u32, 0.0, 0.0),
Emission::Strength { strength, curve } => match curve {
EmissionCurve::Linear => (1u32, strength, 0.0),
EmissionCurve::Power(exponent) => (2u32, strength, exponent),
EmissionCurve::Threshold(min_density) => (3u32, strength, min_density),
},
};
let (noise_pack, noise_vel) = match volume.noise {
None => ([0.0; 4], [0.0; 4]),
Some(n) => {
effective_flags |= SCATTER_FLAG_USE_NOISE;
(
[
n.scale.max(1e-4),
n.octaves.clamp(1, 6) as f32,
n.time_scale,
n.lacunarity.clamp(1.1, 4.0),
],
[
n.scroll_velocity[0],
n.scroll_velocity[1],
n.scroll_velocity[2],
0.0,
],
)
}
};
if volume.density_texture.is_some() {
effective_flags |= SCATTER_FLAG_USE_DENSITY_TEXTURE;
effective_flags &= !SCATTER_FLAG_USE_NOISE;
}
let step_budget_f = volume
.step_budget
.map(|b| b.clamp(1, 128) as f32)
.unwrap_or(0.0);
Some(Self {
shape_kind,
flags: effective_flags,
remap_kind,
emission_kind,
p0,
p1,
colour_density: [colour[0], colour[1], colour[2], density],
params: [anisotropy, emission_strength, emission_param, step_budget_f],
remap_data,
remap_data2,
noise_pack,
noise_vel,
})
}
}
pub fn ray_intersect(
shape: &ScatterShape,
origin: glam::Vec3,
dir: glam::Vec3,
) -> Option<(f32, f32)> {
match shape {
ScatterShape::Box(b) => ray_box(b, origin, dir),
ScatterShape::Sphere { center, radius } => {
ray_sphere(glam::Vec3::from(*center), *radius, origin, dir)
}
}
}
fn ray_box(b: &Aabb, o: glam::Vec3, d: glam::Vec3) -> Option<(f32, f32)> {
let inv = glam::Vec3::new(
if d.x.abs() > 1e-8 {
1.0 / d.x
} else {
f32::INFINITY
},
if d.y.abs() > 1e-8 {
1.0 / d.y
} else {
f32::INFINITY
},
if d.z.abs() > 1e-8 {
1.0 / d.z
} else {
f32::INFINITY
},
);
let t0 = (b.min - o) * inv;
let t1 = (b.max - o) * inv;
let t_min = t0.min(t1);
let t_max = t0.max(t1);
let t_enter = t_min.x.max(t_min.y).max(t_min.z).max(0.0);
let t_exit = t_max.x.min(t_max.y).min(t_max.z);
if t_enter >= t_exit || t_exit <= 0.0 {
None
} else {
Some((t_enter, t_exit))
}
}
fn ray_sphere(c: glam::Vec3, r: f32, o: glam::Vec3, d: glam::Vec3) -> Option<(f32, f32)> {
let oc = o - c;
let a = d.dot(d);
let b = 2.0 * oc.dot(d);
let cc = oc.dot(oc) - r * r;
let disc = b * b - 4.0 * a * cc;
if disc < 0.0 {
return None;
}
let sq = disc.sqrt();
let t0 = (-b - sq) / (2.0 * a);
let t1 = (-b + sq) / (2.0 * a);
let t_enter = t0.max(0.0);
let t_exit = t1;
if t_enter >= t_exit || t_exit <= 0.0 {
None
} else {
Some((t_enter, t_exit))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_volume_has_zero_density() {
let v = ScatterVolume::default();
assert_eq!(v.density, 0.0);
assert!(matches!(v.colour, ColourSource::Flat(_)));
assert!(matches!(v.emission, Emission::None));
assert!(matches!(v.density_remap, DensityRemap::Identity));
assert!(v.noise.is_none());
}
#[test]
fn pack_zero_density_returns_none() {
let v = ScatterVolume::default();
assert!(GpuScatterVolume::pack(&v, 1.0, 0).is_none());
}
#[test]
fn pack_box_round_trips() {
let v = ScatterVolume::box_uniform(
Aabb {
min: glam::Vec3::new(-1.0, -2.0, -3.0),
max: glam::Vec3::new(4.0, 5.0, 6.0),
},
0.2,
[0.1, 0.2, 0.3],
);
let g = GpuScatterVolume::pack(&v, 1.0, 0).unwrap();
assert_eq!(g.shape_kind, 0);
assert_eq!(&g.p0[..3], &[-1.0, -2.0, -3.0]);
assert_eq!(&g.p1[..3], &[4.0, 5.0, 6.0]);
assert_eq!(g.colour_density, [0.1, 0.2, 0.3, 0.2]);
}
#[test]
fn pack_sphere_round_trips() {
let v = ScatterVolume::sphere_uniform([1.0, 2.0, 3.0], 4.0, 0.5, [0.4, 0.5, 0.6]);
let g = GpuScatterVolume::pack(&v, 1.0, 0).unwrap();
assert_eq!(g.shape_kind, 1);
assert_eq!(g.p0, [1.0, 2.0, 3.0, 4.0]);
assert_eq!(g.colour_density, [0.4, 0.5, 0.6, 0.5]);
}
#[test]
fn opacity_multiplier_scales_density() {
let v = ScatterVolume::box_uniform(
Aabb {
min: glam::Vec3::ZERO,
max: glam::Vec3::ONE,
},
0.4,
[1.0; 3],
);
let g = GpuScatterVolume::pack(&v, 0.5, 0).unwrap();
assert!((g.colour_density[3] - 0.2).abs() < 1e-6);
}
#[test]
fn ray_box_hits_from_outside() {
let b = Aabb {
min: glam::Vec3::new(-1.0, -1.0, -1.0),
max: glam::Vec3::new(1.0, 1.0, 1.0),
};
let hit = ray_intersect(
&ScatterShape::Box(b),
glam::Vec3::new(0.0, 0.0, -5.0),
glam::Vec3::Z,
);
let (enter, exit) = hit.unwrap();
assert!((enter - 4.0).abs() < 1e-4);
assert!((exit - 6.0).abs() < 1e-4);
}
#[test]
fn ray_box_camera_inside_starts_at_zero() {
let b = Aabb {
min: glam::Vec3::new(-1.0, -1.0, -1.0),
max: glam::Vec3::new(1.0, 1.0, 1.0),
};
let hit = ray_intersect(&ScatterShape::Box(b), glam::Vec3::ZERO, glam::Vec3::Z);
let (enter, exit) = hit.unwrap();
assert_eq!(enter, 0.0);
assert!((exit - 1.0).abs() < 1e-4);
}
#[test]
fn ray_sphere_misses() {
let hit = ray_intersect(
&ScatterShape::Sphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
},
glam::Vec3::new(2.0, 0.0, -5.0),
glam::Vec3::Z,
);
assert!(hit.is_none());
}
#[test]
fn ray_sphere_camera_inside_starts_at_zero() {
let hit = ray_intersect(
&ScatterShape::Sphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
},
glam::Vec3::ZERO,
glam::Vec3::Z,
);
let (enter, exit) = hit.unwrap();
assert_eq!(enter, 0.0);
assert!((exit - 1.0).abs() < 1e-4);
}
#[test]
fn world_aabb_sphere_matches_bounds() {
let v = ScatterVolume::sphere_uniform([0.0, 0.0, 0.0], 2.0, 0.1, [1.0; 3]);
let b = v.world_aabb();
assert_eq!(b.min, glam::Vec3::splat(-2.0));
assert_eq!(b.max, glam::Vec3::splat(2.0));
}
}