use glam::{DQuat, DVec3, IVec3, Vec3};
use crate::{Grid, Scene, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
const SHADOW_MAX_STEPS: u32 = 4096;
struct GridOcc<'a> {
grid: &'a Grid,
origin: DVec3,
rot_inv: DQuat,
lo: [f32; 3],
hi: [f32; 3],
}
pub struct SceneOccluder<'a> {
grids: Vec<GridOcc<'a>>,
}
impl<'a> SceneOccluder<'a> {
#[must_use]
pub fn build(scene: &'a Scene) -> Self {
let mut grids = Vec::new();
for (_id, grid) in scene.grids() {
if let Some((lo, hi)) = grid_voxel_aabb(grid) {
grids.push(GridOcc {
grid,
origin: grid.transform.origin,
rot_inv: grid.transform.rotation.inverse(),
lo,
hi,
});
}
}
Self { grids }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.grids.is_empty()
}
}
impl roxlap_core::WorldOccluder for SceneOccluder<'_> {
fn occluded_world(&self, origin: [f32; 3], dir: [f32; 3], max_t: f32) -> bool {
let ow = DVec3::new(
f64::from(origin[0]),
f64::from(origin[1]),
f64::from(origin[2]),
);
let dw = DVec3::new(f64::from(dir[0]), f64::from(dir[1]), f64::from(dir[2]));
for g in &self.grids {
if occluded_in_grid(g, ow, dw, max_t) {
return true;
}
}
false
}
}
fn grid_voxel_aabb(grid: &Grid) -> Option<([f32; 3], [f32; 3])> {
let mut min = IVec3::splat(i32::MAX);
let mut max = IVec3::splat(i32::MIN);
let mut any = false;
for idx in grid.chunks.keys() {
any = true;
min = min.min(*idx);
max = max.max(*idx);
}
if !any {
return None;
}
#[allow(clippy::cast_precision_loss)]
let cs_xy = CHUNK_SIZE_XY as i32;
#[allow(clippy::cast_precision_loss)]
let cs_z = CHUNK_SIZE_Z as i32;
let lo = [
(min.x * cs_xy) as f32,
(min.y * cs_xy) as f32,
(min.z * cs_z) as f32,
];
let hi = [
((max.x + 1) * cs_xy) as f32,
((max.y + 1) * cs_xy) as f32,
((max.z + 1) * cs_z) as f32,
];
Some((lo, hi))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn occluded_in_grid(g: &GridOcc<'_>, ow: DVec3, dw: DVec3, max_t: f32) -> bool {
let o: Vec3 = (g.rot_inv * (ow - g.origin)).as_vec3();
let d: Vec3 = (g.rot_inv * dw).as_vec3();
let o = [o.x, o.y, o.z];
let d = [d.x, d.y, d.z];
let Some((t0, t1)) = intersect_aabb(o, d, g.lo, g.hi) else {
return false;
};
let t_enter = t0.max(0.0);
let t_exit = t1.min(max_t);
if t_enter > t_exit {
return false;
}
let start = t_enter + 1e-4;
let p = [
o[0] + d[0] * start,
o[1] + d[1] * start,
o[2] + d[2] * start,
];
let mut cell = [
(p[0].floor() as i32).clamp(g.lo[0] as i32, g.hi[0] as i32 - 1),
(p[1].floor() as i32).clamp(g.lo[1] as i32, g.hi[1] as i32 - 1),
(p[2].floor() as i32).clamp(g.lo[2] as i32, g.hi[2] as i32 - 1),
];
let (step, mut t_max, t_delta) = dda_setup(o, d, cell);
let inv = [
if step[0] != 0 { 1.0 / d[0] } else { 0.0 },
if step[1] != 0 { 1.0 / d[1] } else { 0.0 },
if step[2] != 0 { 1.0 / d[2] } else { 0.0 },
];
let mut t_curr = t_enter;
let lo_i = [g.lo[0] as i32, g.lo[1] as i32, g.lo[2] as i32];
let hi_i = [g.hi[0] as i32, g.hi[1] as i32, g.hi[2] as i32];
let (cs_xy, cs_z) = (CHUNK_SIZE_XY as i32, CHUNK_SIZE_Z as i32);
let mut sampler = g.grid.solid_sampler();
let cache = &g.grid.dda_brick_cache;
let mut used = 0u32;
while used < SHADOW_MAX_STEPS {
if cell[0] < lo_i[0]
|| cell[0] >= hi_i[0]
|| cell[1] < lo_i[1]
|| cell[1] >= hi_i[1]
|| cell[2] < lo_i[2]
|| cell[2] >= hi_i[2]
|| t_curr > t_exit
{
return false;
}
let (chunk_idx, in_chunk) = crate::voxel_split(IVec3::new(cell[0], cell[1], cell[2]));
let vxl = sampler.chunk_at(chunk_idx);
let skip_box: Option<([i32; 3], [i32; 3])> = if vxl.is_none() {
let lo = [chunk_idx.x * cs_xy, chunk_idx.y * cs_xy, chunk_idx.z * cs_z];
Some((lo, [lo[0] + cs_xy, lo[1] + cs_xy, lo[2] + cs_z]))
} else {
let ch = [chunk_idx.x, chunk_idx.y, chunk_idx.z];
let in_c = [in_chunk.x as i32, in_chunk.y as i32, in_chunk.z as i32];
if cache.super_occupied_at(ch, 0, in_c) == Some(false) {
let lo = [
(cell[0] >> 6) << 6,
(cell[1] >> 6) << 6,
(cell[2] >> 6) << 6,
];
Some((lo, [lo[0] + 64, lo[1] + 64, lo[2] + 64]))
} else if cache.brick_occupied_at(ch, 0, in_c) == Some(false) {
let lo = [
(cell[0] >> 3) << 3,
(cell[1] >> 3) << 3,
(cell[2] >> 3) << 3,
];
Some((lo, [lo[0] + 8, lo[1] + 8, lo[2] + 8]))
} else {
None
}
};
if let Some((blo, bhi)) = skip_box {
let mut best_t = f32::INFINITY;
let mut best_axis = 3usize;
let mut plane = [0i32; 3];
for a in 0..3 {
if step[a] == 0 {
continue;
}
plane[a] = if step[a] > 0 { bhi[a] } else { blo[a] };
let tb = (plane[a] as f32 - o[a]) * inv[a];
if tb < best_t {
best_t = tb;
best_axis = a;
}
}
if best_axis == 3 {
return false;
}
let pb = [
o[0] + d[0] * (best_t + 1e-4),
o[1] + d[1] * (best_t + 1e-4),
o[2] + d[2] * (best_t + 1e-4),
];
let mut nc = [
pb[0].floor() as i32,
pb[1].floor() as i32,
pb[2].floor() as i32,
];
nc[best_axis] = if step[best_axis] > 0 {
plane[best_axis]
} else {
plane[best_axis] - 1
};
let crossed =
cell[0].abs_diff(nc[0]) + cell[1].abs_diff(nc[1]) + cell[2].abs_diff(nc[2]);
if used.saturating_add(crossed) >= SHADOW_MAX_STEPS {
return false;
}
used += crossed;
cell = nc;
for a in 0..3 {
if step[a] > 0 {
t_max[a] = ((cell[a] + 1) as f32 - o[a]) * inv[a];
} else if step[a] < 0 {
t_max[a] = (cell[a] as f32 - o[a]) * inv[a];
}
}
t_curr = best_t.max(t_curr);
continue;
}
if let Some(vxl) = vxl {
if crate::chunks::vxl_voxel_solid(vxl, in_chunk.x, in_chunk.y, in_chunk.z) {
return true;
}
}
let a = min_axis(t_max);
t_curr = t_max[a];
cell[a] += step[a];
t_max[a] += t_delta[a];
used += 1;
}
false
}
fn intersect_aabb(o: [f32; 3], d: [f32; 3], lo: [f32; 3], hi: [f32; 3]) -> Option<(f32, f32)> {
let mut tmin = f32::NEG_INFINITY;
let mut tmax = f32::INFINITY;
for a in 0..3 {
if d[a].abs() < 1e-9 {
if o[a] < lo[a] || o[a] > hi[a] {
return None;
}
} else {
let inv = 1.0 / d[a];
let mut t0 = (lo[a] - o[a]) * inv;
let mut t1 = (hi[a] - o[a]) * inv;
if t0 > t1 {
std::mem::swap(&mut t0, &mut t1);
}
tmin = tmin.max(t0);
tmax = tmax.min(t1);
if tmin > tmax {
return None;
}
}
}
Some((tmin, tmax))
}
fn dda_setup(o: [f32; 3], d: [f32; 3], cell: [i32; 3]) -> ([i32; 3], [f32; 3], [f32; 3]) {
let mut step = [0i32; 3];
let mut t_max = [f32::INFINITY; 3];
let mut t_delta = [f32::INFINITY; 3];
for a in 0..3 {
if d[a] > 1e-9 {
step[a] = 1;
#[allow(clippy::cast_precision_loss)]
let boundary = (cell[a] + 1) as f32;
t_max[a] = (boundary - o[a]) / d[a];
t_delta[a] = 1.0 / d[a];
} else if d[a] < -1e-9 {
step[a] = -1;
#[allow(clippy::cast_precision_loss)]
let boundary = cell[a] as f32;
t_max[a] = (boundary - o[a]) / d[a];
t_delta[a] = -1.0 / d[a];
}
}
(step, t_max, t_delta)
}
#[inline]
fn min_axis(t: [f32; 3]) -> usize {
if t[0] < t[1] && t[0] < t[2] {
0
} else if t[1] < t[2] {
1
} else {
2
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GridTransform, Scene};
use roxlap_formats::color::VoxColor;
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn occluded_dense(g: &GridOcc<'_>, ow: DVec3, dw: DVec3, max_t: f32) -> bool {
let o: Vec3 = (g.rot_inv * (ow - g.origin)).as_vec3();
let d: Vec3 = (g.rot_inv * dw).as_vec3();
let o = [o.x, o.y, o.z];
let d = [d.x, d.y, d.z];
let Some((t0, t1)) = intersect_aabb(o, d, g.lo, g.hi) else {
return false;
};
let t_enter = t0.max(0.0);
let t_exit = t1.min(max_t);
if t_enter > t_exit {
return false;
}
let start = t_enter + 1e-4;
let p = [
o[0] + d[0] * start,
o[1] + d[1] * start,
o[2] + d[2] * start,
];
let mut cell = [
(p[0].floor() as i32).clamp(g.lo[0] as i32, g.hi[0] as i32 - 1),
(p[1].floor() as i32).clamp(g.lo[1] as i32, g.hi[1] as i32 - 1),
(p[2].floor() as i32).clamp(g.lo[2] as i32, g.hi[2] as i32 - 1),
];
let (step, mut t_max, t_delta) = dda_setup(o, d, cell);
let mut t_curr = t_enter;
let lo_i = [g.lo[0] as i32, g.lo[1] as i32, g.lo[2] as i32];
let hi_i = [g.hi[0] as i32, g.hi[1] as i32, g.hi[2] as i32];
for _ in 0..SHADOW_MAX_STEPS {
if cell[0] < lo_i[0]
|| cell[0] >= hi_i[0]
|| cell[1] < lo_i[1]
|| cell[1] >= hi_i[1]
|| cell[2] < lo_i[2]
|| cell[2] >= hi_i[2]
|| t_curr > t_exit
{
return false;
}
if g.grid.voxel_solid(IVec3::new(cell[0], cell[1], cell[2])) {
return true;
}
let a = min_axis(t_max);
t_curr = t_max[a];
cell[a] += step[a];
t_max[a] += t_delta[a];
}
false
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn skip_march_matches_dense_reference() {
let mut scene = Scene::new();
let gid = scene.add_grid(GridTransform::identity());
let grid = scene.grid_mut(gid).unwrap();
grid.set_rect(
IVec3::new(0, 0, 160),
IVec3::new(255, 127, 255),
Some(VoxColor(0x80_55_66_77)),
);
grid.set_rect(
IVec3::new(384, 0, 160),
IVec3::new(511, 127, 255),
Some(VoxColor(0x80_55_66_77)),
);
for i in 0..14 {
let (x, y) = (29 * i % 220 + 10, 41 * i % 100 + 10);
grid.set_sphere(IVec3::new(x, y, 170), 7, None);
grid.set_rect(
IVec3::new(x + 3, y + 3, 120),
IVec3::new(x + 4, y + 4, 159),
Some(VoxColor(0x80_99_88_77)),
);
}
let run = |scene: &Scene| {
let occ = SceneOccluder::build(scene);
let g = &occ.grids[0];
let mut seed = 0x1234_5678u64;
#[allow(clippy::cast_possible_truncation)]
let mut rng = move || {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((seed >> 40) as u32) as f32 / 16_777_216.0
};
let mut hits = 0u32;
for i in 0..4000u32 {
let ox = rng() * 560.0 - 20.0;
let oy = rng() * 160.0 - 16.0;
let oz = rng() * 240.0;
let dx = rng() * 2.0 - 1.0;
let dy = rng() * 2.0 - 1.0;
let dz = match i % 5 {
0 => 0.0,
1 => 1e-6,
_ => rng() * 2.0 - 1.0,
};
let ow = DVec3::new(f64::from(ox), f64::from(oy), f64::from(oz));
let dw = DVec3::new(f64::from(dx), f64::from(dy), f64::from(dz));
if dw.length() < 1e-9 {
continue;
}
let max_t = if i % 3 == 0 { 96.0 } else { 512.0 };
let dense = occluded_dense(g, ow, dw, max_t);
let skip = occluded_in_grid(g, ow, dw, max_t);
assert_eq!(
skip, dense,
"ray {i}: o={ow:?} d={dw:?} max_t={max_t} skip={skip} dense={dense}",
);
hits += u32::from(dense);
}
hits
};
let hits_no_maps = run(&scene);
scene.grid_mut(gid).unwrap().ensure_dda_bricks(0);
let hits_with_maps = run(&scene);
assert_eq!(hits_no_maps, hits_with_maps, "map presence changed results");
assert!(hits_with_maps > 200, "sweep should hit terrain often");
}
}