use glam::IVec3;
use roxlap_formats::edit::{set_spans, Vspan};
use roxlap_formats::vxl::Vxl;
use crate::{Grid, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
const CHUNK_EDIT_HEADROOM_PER_COLUMN: usize = 256;
pub(crate) fn empty_chunk_vxl() -> Vxl {
let vsid = CHUNK_SIZE_XY;
let n_cols = (vsid as usize) * (vsid as usize);
let mut data: Vec<u8> = Vec::with_capacity(n_cols * 8);
let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
for _ in 0..n_cols {
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
data.extend_from_slice(&[0, 0, 0, 0]); data.extend_from_slice(&[0, 0, 0, 0]); }
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
let mut vxl = Vxl {
vsid,
ipo: [0.0; 3],
ist: [1.0, 0.0, 0.0],
ihe: [0.0, 0.0, 1.0],
ifo: [0.0, 1.0, 0.0],
data: data.into_boxed_slice(),
column_offset: column_offset.into_boxed_slice(),
mip_base_offsets: Box::new([0, n_cols + 1]),
vbit: Box::new([]),
vbiti: 0,
};
vxl.reserve_edit_capacity(n_cols * CHUNK_EDIT_HEADROOM_PER_COLUMN);
let mut spans: Vec<Vspan> = Vec::with_capacity(n_cols);
for y in 0..vsid {
for x in 0..vsid {
spans.push(Vspan {
x,
y,
z0: 0,
z1: u8::MAX,
});
}
}
set_spans(&mut vxl, &spans, None);
vxl
}
#[allow(clippy::cast_possible_wrap)]
pub(crate) fn vxl_voxel_solid(vxl: &Vxl, x: u32, y: u32, z: u32) -> bool {
let idx = (y * vxl.vsid + x) as usize;
let slab = vxl.column_data(idx);
let z = z as i32;
let mut top = i32::from(slab[1]);
let mut v = 0usize;
while slab[v] != 0 {
v += usize::from(slab[v]) * 4;
if slab[v + 3] >= slab[v + 1] {
continue;
}
let bot = i32::from(slab[v + 3]);
if z < bot {
return z >= top;
}
top = i32::from(slab[v + 1]);
}
z >= top
}
pub(crate) struct SolidSampler<'a> {
grid: &'a Grid,
cached_idx: IVec3,
cached: Option<&'a Vxl>,
primed: bool,
}
impl<'a> SolidSampler<'a> {
pub(crate) fn chunk_at(&mut self, chunk_idx: IVec3) -> Option<&'a Vxl> {
if !self.primed || chunk_idx != self.cached_idx {
self.cached = self.grid.chunk(chunk_idx);
self.cached_idx = chunk_idx;
self.primed = true;
}
self.cached
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BakeMode {
Directional,
AmbientOcclusion(roxlap_core::AoParams),
PointLights,
}
impl BakeMode {
fn lightmode(self) -> u32 {
match self {
Self::Directional => 1,
Self::AmbientOcclusion(_) => 3,
Self::PointLights => 2,
}
}
fn ao(self) -> roxlap_core::AoParams {
match self {
Self::Directional | Self::PointLights => roxlap_core::AoParams::default(),
Self::AmbientOcclusion(ao) => ao,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BakeLight {
pub pos: glam::Vec3,
pub radius: f32,
pub strength: f32,
}
impl BakeLight {
#[allow(clippy::cast_possible_wrap, clippy::cast_precision_loss)]
fn chunk_local(&self, chunk_idx: IVec3) -> Option<roxlap_core::LightSrc> {
let base = glam::Vec3::new(
(chunk_idx.x * CHUNK_SIZE_XY as i32) as f32,
(chunk_idx.y * CHUNK_SIZE_XY as i32) as f32,
(chunk_idx.z * CHUNK_SIZE_Z as i32) as f32,
);
let local = self.pos - base;
let ext = glam::Vec3::new(
CHUNK_SIZE_XY as f32,
CHUNK_SIZE_XY as f32,
CHUNK_SIZE_Z as f32,
);
let nearest = local.clamp(glam::Vec3::ZERO, ext);
if (nearest - local).length_squared() > self.radius * self.radius {
return None;
}
Some(roxlap_core::LightSrc {
pos: [local.x, local.y, local.z],
r2: self.radius * self.radius,
sc: self.strength,
})
}
}
impl Grid {
#[must_use]
pub fn voxel_solid(&self, voxel: IVec3) -> bool {
let (chunk_idx, in_chunk) = crate::voxel_split(voxel);
match self.chunk(chunk_idx) {
Some(vxl) => vxl_voxel_solid(vxl, in_chunk.x, in_chunk.y, in_chunk.z),
None => false,
}
}
#[must_use]
pub fn chunk_voxel_solid(vxl: &Vxl, in_chunk: glam::UVec3) -> bool {
vxl_voxel_solid(vxl, in_chunk.x, in_chunk.y, in_chunk.z)
}
pub(crate) fn solid_sampler(&self) -> SolidSampler<'_> {
SolidSampler {
grid: self,
cached_idx: IVec3::ZERO,
cached: None,
primed: false,
}
}
#[must_use]
pub fn voxel_color(&self, voxel: IVec3) -> Option<roxlap_formats::color::VoxColor> {
let (chunk_idx, in_chunk) = crate::voxel_split(voxel);
self.chunk(chunk_idx)?
.voxel_color(in_chunk.x, in_chunk.y, in_chunk.z)
}
#[must_use]
pub fn chunk(&self, chunk_idx: IVec3) -> Option<&Vxl> {
self.chunks.get(&chunk_idx)
}
pub fn bake(&mut self, mode: BakeMode) {
self.bake_u32(mode.lightmode(), mode.ao());
}
#[deprecated(since = "0.23.0", note = "use `bake(BakeMode::..)`")]
pub fn bake_lightmode(&mut self, lightmode: u32) {
self.bake_u32(lightmode, roxlap_core::AoParams::default());
}
#[deprecated(since = "0.23.0", note = "use `bake(BakeMode::AmbientOcclusion(ao))`")]
pub fn bake_lightmode_with_ao(&mut self, lightmode: u32, ao: roxlap_core::AoParams) {
self.bake_u32(lightmode, ao);
}
fn bake_u32(&mut self, lightmode: u32, ao: roxlap_core::AoParams) {
if lightmode == 0 {
return;
}
#[allow(clippy::cast_possible_wrap)]
let cs_xy = CHUNK_SIZE_XY as i32;
#[allow(clippy::cast_possible_wrap)]
let cs_z = CHUNK_SIZE_Z as i32;
let chunk_idxs: Vec<IVec3> = self.chunks.keys().copied().collect();
use rayon::prelude::*;
let wave = rayon::current_num_threads().max(1);
for batch in chunk_idxs.chunks(wave) {
let caches: Vec<(IVec3, roxlap_core::EstNormCache)> = batch
.par_iter()
.map(|&chunk_idx| {
(
chunk_idx,
self.build_chunk_estnorm_cache(chunk_idx, 0, 0, cs_xy, cs_xy),
)
})
.collect();
for (chunk_idx, cache) in caches {
let lights = self.chunk_bake_lights(chunk_idx, lightmode);
let target = self.chunks.get_mut(&chunk_idx).expect("populated chunk");
roxlap_core::apply_lighting_with_cache(
&mut target.data,
&target.column_offset,
CHUNK_SIZE_XY,
0,
0,
0,
cs_xy,
cs_xy,
cs_z,
&cache,
lightmode,
&lights,
ao,
);
}
}
}
#[allow(clippy::cast_possible_wrap)]
pub fn bake_bbox(&mut self, lo: IVec3, hi: IVec3, mode: BakeMode) {
self.bake_bbox_u32(lo, hi, mode.lightmode(), mode.ao());
}
#[deprecated(since = "0.23.0", note = "use `bake_bbox(lo, hi, BakeMode::..)`")]
pub fn bake_lightmode_bbox(&mut self, lo: IVec3, hi: IVec3, lightmode: u32) {
self.bake_bbox_u32(lo, hi, lightmode, roxlap_core::AoParams::default());
}
#[allow(clippy::cast_possible_wrap)]
fn bake_bbox_u32(&mut self, lo: IVec3, hi: IVec3, lightmode: u32, ao: roxlap_core::AoParams) {
if lightmode == 0 {
return;
}
let cs_xy = CHUNK_SIZE_XY as i32;
let cs_z = CHUNK_SIZE_Z as i32;
let pad = roxlap_core::ESTNORMRAD;
let a_lo = IVec3::new(lo.x - pad, lo.y - pad, lo.z - pad);
let a_hi = IVec3::new(hi.x + pad + 1, hi.y + pad + 1, hi.z + pad + 1);
if a_lo.x >= a_hi.x || a_lo.y >= a_hi.y || a_lo.z >= a_hi.z {
return;
}
let c_lo = IVec3::new(
a_lo.x.div_euclid(cs_xy),
a_lo.y.div_euclid(cs_xy),
a_lo.z.div_euclid(cs_z),
);
let c_hi = IVec3::new(
(a_hi.x - 1).div_euclid(cs_xy),
(a_hi.y - 1).div_euclid(cs_xy),
(a_hi.z - 1).div_euclid(cs_z),
);
for chz in c_lo.z..=c_hi.z {
for chy in c_lo.y..=c_hi.y {
for chx in c_lo.x..=c_hi.x {
let chunk_idx = IVec3::new(chx, chy, chz);
if !self.chunks.contains_key(&chunk_idx) {
continue;
}
let base = IVec3::new(chx * cs_xy, chy * cs_xy, chz * cs_z);
let lx0 = (a_lo.x - base.x).max(0);
let ly0 = (a_lo.y - base.y).max(0);
let lz0 = (a_lo.z - base.z).max(0);
let lx1 = (a_hi.x - base.x).min(cs_xy);
let ly1 = (a_hi.y - base.y).min(cs_xy);
let lz1 = (a_hi.z - base.z).min(cs_z);
if lx0 >= lx1 || ly0 >= ly1 || lz0 >= lz1 {
continue;
}
let cache = self.build_chunk_estnorm_cache(chunk_idx, lx0, ly0, lx1, ly1);
let lights = self.chunk_bake_lights(chunk_idx, lightmode);
let target = self.chunks.get_mut(&chunk_idx).expect("populated chunk");
roxlap_core::apply_lighting_with_cache(
&mut target.data,
&target.column_offset,
CHUNK_SIZE_XY,
lx0,
ly0,
lz0,
lx1,
ly1,
lz1,
&cache,
lightmode,
&lights,
ao,
);
self.bump_chunk_version_bbox(
chunk_idx,
IVec3::new(lx0, ly0, lz0),
IVec3::new(lx1 - 1, ly1 - 1, lz1 - 1),
);
}
}
}
}
fn chunk_bake_lights(&self, chunk_idx: IVec3, lightmode: u32) -> Vec<roxlap_core::LightSrc> {
if lightmode != 2 || self.bake_lights.is_empty() {
return Vec::new();
}
self.bake_lights
.iter()
.filter_map(|l| l.chunk_local(chunk_idx))
.collect()
}
#[allow(clippy::cast_possible_wrap)]
fn build_chunk_estnorm_cache(
&self,
chunk_idx: IVec3,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> roxlap_core::EstNormCache {
let cs_xy = CHUNK_SIZE_XY as i32;
let reader = |px: i32, py: i32, chz_delta: i32| -> Option<&[u8]> {
let nb_chx = chunk_idx.x + px.div_euclid(cs_xy);
let nb_chy = chunk_idx.y + py.div_euclid(cs_xy);
let in_x = px.rem_euclid(cs_xy);
let in_y = py.rem_euclid(cs_xy);
let chunk = self.chunk(IVec3::new(nb_chx, nb_chy, chunk_idx.z + chz_delta))?;
let col_idx = (in_y as u32) * CHUNK_SIZE_XY + (in_x as u32);
let off = chunk.column_offset[col_idx as usize] as usize;
Some(&chunk.data[off..])
};
roxlap_core::EstNormCache::build_with_reader_z(reader, x0, y0, x1, y1)
}
pub fn chunk_mut(&mut self, chunk_idx: IVec3) -> Option<&mut Vxl> {
self.chunks.get_mut(&chunk_idx)
}
pub fn ensure_chunk(&mut self, chunk_idx: IVec3) -> &mut Vxl {
if !self.chunks.contains_key(&chunk_idx) {
self.note_chunk_set_changed();
}
self.chunks.entry(chunk_idx).or_insert_with(empty_chunk_vxl)
}
#[must_use]
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
#[must_use]
pub fn chunk_xy_backing(&self) -> Option<ChunkXyBacking<'_>> {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut any = false;
for chunk_idx in self.chunks.keys() {
if chunk_idx.z != 0 {
continue;
}
min_x = min_x.min(chunk_idx.x);
min_y = min_y.min(chunk_idx.y);
max_x = max_x.max(chunk_idx.x);
max_y = max_y.max(chunk_idx.y);
any = true;
}
if !any {
return None;
}
#[allow(clippy::cast_sign_loss)]
let chunks_x = (max_x - min_x + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_y = (max_y - min_y + 1) as u32;
let mut table: Vec<Option<roxlap_core::GridView<'_>>> =
vec![None; (chunks_x * chunks_y) as usize];
for (chunk_idx, vxl) in &self.chunks {
if chunk_idx.z != 0 {
continue;
}
let dx = chunk_idx.x - min_x;
let dy = chunk_idx.y - min_y;
#[allow(clippy::cast_sign_loss)]
let i = (dy as u32 * chunks_x + dx as u32) as usize;
table[i] = Some(roxlap_core::GridView::from_single_vxl(vxl));
}
Some(ChunkXyBacking {
chunks: table,
origin_chunk_xy: [min_x, min_y],
origin_chunk_z: 0,
chunks_x,
chunks_y,
chunks_z: 1,
})
}
#[must_use]
pub fn chunk_xyz_backing(&self) -> Option<ChunkXyBacking<'_>> {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut min_z = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut max_z = i32::MIN;
let mut any = false;
for chunk_idx in self.chunks.keys() {
min_x = min_x.min(chunk_idx.x);
min_y = min_y.min(chunk_idx.y);
min_z = min_z.min(chunk_idx.z);
max_x = max_x.max(chunk_idx.x);
max_y = max_y.max(chunk_idx.y);
max_z = max_z.max(chunk_idx.z);
any = true;
}
if !any {
return None;
}
#[allow(clippy::cast_sign_loss)]
let chunks_x = (max_x - min_x + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_y = (max_y - min_y + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_z = (max_z - min_z + 1) as u32;
let mut table: Vec<Option<roxlap_core::GridView<'_>>> =
vec![None; (chunks_x * chunks_y * chunks_z) as usize];
for (chunk_idx, vxl) in &self.chunks {
let dx = chunk_idx.x - min_x;
let dy = chunk_idx.y - min_y;
let dz = chunk_idx.z - min_z;
#[allow(clippy::cast_sign_loss)]
let (dx, dy, dz) = (dx as u32, dy as u32, dz as u32);
let i = ((dz * chunks_y + dy) * chunks_x + dx) as usize;
table[i] = Some(roxlap_core::GridView::from_single_vxl(vxl));
}
Some(ChunkXyBacking {
chunks: table,
origin_chunk_xy: [min_x, min_y],
origin_chunk_z: min_z,
chunks_x,
chunks_y,
chunks_z,
})
}
}
pub struct ChunkXyBacking<'a> {
pub chunks: Vec<Option<roxlap_core::GridView<'a>>>,
pub origin_chunk_xy: [i32; 2],
pub origin_chunk_z: i32,
pub chunks_x: u32,
pub chunks_y: u32,
pub chunks_z: u32,
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{GridTransform, CHUNK_SIZE_Z};
use roxlap_formats::color::VoxColor;
pub(crate) fn voxel_is_solid(vxl: &Vxl, x: u32, y: u32, z: u32) -> bool {
super::vxl_voxel_solid(vxl, x, y, z)
}
#[test]
fn voxel_solid_reflects_set_voxel() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(5, 6, 7), Some(VoxColor(0x80_aa_bb_cc)));
assert!(g.voxel_solid(IVec3::new(5, 6, 7)), "set voxel is solid");
assert!(!g.voxel_solid(IVec3::new(5, 6, 8)), "neighbour is air");
assert!(
!g.voxel_solid(IVec3::new(900, 900, 7)),
"absent chunk reads as air",
);
}
#[test]
fn voxel_solid_handles_negative_coords() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(-1, -1, 10), Some(VoxColor(0x80_11_22_33)));
assert!(g.voxel_solid(IVec3::new(-1, -1, 10)));
assert!(!g.voxel_solid(IVec3::new(-1, -1, 11)));
}
#[test]
fn empty_chunk_has_correct_vsid() {
let vxl = empty_chunk_vxl();
assert_eq!(vxl.vsid, CHUNK_SIZE_XY);
}
#[test]
fn empty_chunk_is_all_air() {
let vxl = empty_chunk_vxl();
for &(x, y, z) in &[
(0u32, 0u32, 0u32),
(0, 0, 100),
(0, 0, 200),
(CHUNK_SIZE_XY - 1, CHUNK_SIZE_XY - 1, 0),
(64, 64, 128),
] {
assert!(
!voxel_is_solid(&vxl, x, y, z),
"voxel ({x}, {y}, {z}) should be air"
);
}
}
#[test]
fn empty_chunk_air_above_bedrock_on_grid_sample() {
let vxl = empty_chunk_vxl();
let bedrock_z = CHUNK_SIZE_Z - 1;
for y in (0..CHUNK_SIZE_XY).step_by(16) {
for x in (0..CHUNK_SIZE_XY).step_by(16) {
for z in (0..bedrock_z).step_by(16) {
assert!(
!voxel_is_solid(&vxl, x, y, z),
"voxel ({x}, {y}, {z}) leaked solid in empty chunk"
);
}
assert!(voxel_is_solid(&vxl, x, y, bedrock_z));
}
}
}
#[test]
fn empty_chunk_keeps_bedrock_placeholder() {
let vxl = empty_chunk_vxl();
assert!(voxel_is_solid(&vxl, 0, 0, CHUNK_SIZE_Z - 1));
assert!(voxel_is_solid(&vxl, 64, 64, CHUNK_SIZE_Z - 1));
}
#[test]
fn ensure_chunk_creates_when_missing() {
let mut g = Grid::new(GridTransform::identity());
assert_eq!(g.chunk_count(), 0);
assert!(g.chunk(IVec3::ZERO).is_none());
let _ = g.ensure_chunk(IVec3::ZERO);
assert_eq!(g.chunk_count(), 1);
assert!(g.chunk(IVec3::ZERO).is_some());
}
#[test]
fn ensure_chunk_returns_existing() {
let mut g = Grid::new(GridTransform::identity());
let chunk = IVec3::new(2, -1, 0);
g.ensure_chunk(chunk);
g.set_voxel(IVec3::new(261, -122, 7), Some(VoxColor(0x80_aa_bb_cc)));
let vxl = g.ensure_chunk(chunk);
assert!(voxel_is_solid(vxl, 5, 6, 7));
assert_eq!(g.chunk_count(), 1);
}
#[test]
fn chunk_mut_returns_none_for_missing() {
let mut g = Grid::new(GridTransform::identity());
assert!(g.chunk_mut(IVec3::ZERO).is_none());
}
#[test]
fn chunk_xy_backing_returns_chunks_z_one() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, 0));
g.ensure_chunk(IVec3::new(1, 0, 0));
g.ensure_chunk(IVec3::new(0, 0, 1));
let backing = g.chunk_xy_backing().expect("two chz=0 chunks present");
assert_eq!(backing.chunks_z, 1);
assert_eq!(backing.origin_chunk_z, 0);
assert_eq!(backing.chunks_x, 2);
assert_eq!(backing.chunks_y, 1);
assert_eq!(backing.chunks.len(), 2);
}
#[test]
fn chunk_xyz_backing_with_stacked_chunks_enumerates_all_z() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, 0));
g.ensure_chunk(IVec3::new(1, 0, 0));
g.ensure_chunk(IVec3::new(0, 0, 1));
let backing = g.chunk_xyz_backing().expect("at least one chunk");
assert_eq!(backing.chunks_x, 2);
assert_eq!(backing.chunks_y, 1);
assert_eq!(backing.chunks_z, 2);
assert_eq!(backing.origin_chunk_xy, [0, 0]);
assert_eq!(backing.origin_chunk_z, 0);
assert_eq!(backing.chunks.len(), 4); assert!(backing.chunks[0].is_some(), "(0, 0, 0) present");
assert!(backing.chunks[1].is_some(), "(1, 0, 0) present");
assert!(backing.chunks[2].is_some(), "(0, 0, 1) present");
assert!(backing.chunks[3].is_none(), "(1, 0, 1) implicit-air");
}
#[test]
fn chunk_xyz_backing_with_negative_origin_chunk_z() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, -2));
g.ensure_chunk(IVec3::new(0, 0, 0));
let backing = g.chunk_xyz_backing().expect("at least one chunk");
assert_eq!(backing.chunks_z, 3); assert_eq!(backing.origin_chunk_z, -2);
assert!(backing.chunks[0].is_some(), "chz=-2 → dz=0");
assert!(backing.chunks[1].is_none(), "chz=-1 → dz=1 implicit-air");
assert!(backing.chunks[2].is_some(), "chz=0 → dz=2");
}
#[test]
fn bbox_rebake_matches_full_rebake() {
let build = || {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(
IVec3::new(0, 0, 160),
IVec3::new(255, 255, 255),
Some(VoxColor(0x80_66_77_88)),
);
for i in 0..10 {
let (x, y) = (23 * i % 240 + 8, 37 * i % 240 + 8);
g.set_sphere(IVec3::new(x, y, 165), 6, None);
}
g.bake(BakeMode::Directional);
g
};
let mut full = build();
let mut bbox = build();
let (lo, hi) = (IVec3::new(120, 60, 158), IVec3::new(136, 76, 200));
full.set_rect(lo, hi, None);
bbox.set_rect(lo, hi, None);
full.bake(BakeMode::Directional);
bbox.bake_bbox(lo, hi, BakeMode::Directional);
for (idx, a) in &full.chunks {
let b = bbox.chunks.get(idx).expect("same chunk set");
assert_eq!(
a.data, b.data,
"chunk {idx:?} bytes diverge between full and bbox rebake",
);
}
}
#[test]
fn point_light_bake_brightens_near_light() {
let floor = |g: &mut Grid| {
g.set_rect(
IVec3::new(0, 0, 200),
IVec3::new(127, 127, 255),
Some(VoxColor(0x80_66_77_88)),
);
};
let byte_at = |g: &Grid, x: u32, y: u32| {
(g.chunk(IVec3::ZERO)
.expect("chunk")
.voxel_color(x, y, 200)
.expect("floor voxel")
.0
>> 24)
& 0xff
};
let mut g = Grid::new(GridTransform::identity());
floor(&mut g);
g.bake_lights.push(BakeLight {
pos: glam::Vec3::new(64.0, 64.0, 192.0),
radius: 40.0,
strength: 4000.0,
});
g.bake(BakeMode::PointLights);
let near = byte_at(&g, 64, 64);
let far = byte_at(&g, 4, 4); assert!(
near > far + 20,
"light pool must brighten the floor under it (near={near} far={far})"
);
let mut dir = Grid::new(GridTransform::identity());
floor(&mut dir);
dir.bake(BakeMode::Directional);
let dir_far = byte_at(&dir, 4, 4);
assert!(
far < dir_far,
"PointLights base ({far}) must be dimmer than Directional ({dir_far})"
);
}
#[test]
fn point_light_bbox_rebake_matches_full_rebake() {
let build = || {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(
IVec3::new(0, 0, 160),
IVec3::new(255, 255, 255),
Some(VoxColor(0x80_66_77_88)),
);
g.bake_lights.push(BakeLight {
pos: glam::Vec3::new(126.0, 70.0, 152.0),
radius: 48.0,
strength: 4000.0,
});
g.bake(BakeMode::PointLights);
g
};
let mut full = build();
let mut bbox = build();
let (lo, hi) = (IVec3::new(120, 60, 158), IVec3::new(136, 76, 200));
full.set_rect(lo, hi, None);
bbox.set_rect(lo, hi, None);
full.bake(BakeMode::PointLights);
bbox.bake_bbox(lo, hi, BakeMode::PointLights);
for (idx, a) in &full.chunks {
let b = bbox.chunks.get(idx).expect("same chunk set");
assert_eq!(
a.data, b.data,
"chunk {idx:?} bytes diverge between full and bbox point-light rebake",
);
}
}
#[test]
fn remip_bbox_matches_generate_mips() {
use roxlap_formats::edit::{set_sphere_with_colfunc, SpanOp};
let mut g = Grid::new(GridTransform::identity());
g.set_rect(
IVec3::new(0, 0, 150),
IVec3::new(127, 127, 255),
Some(VoxColor(0x80_66_77_88)),
);
for i in 0..8 {
let (x, y) = (29 * i % 100 + 12, 41 * i % 100 + 12);
g.set_sphere(IVec3::new(x, y, 158), 5, None);
}
let base = g.chunks.get_mut(&IVec3::ZERO).expect("chunk");
base.generate_mips(6);
let mut full = base.clone();
let mut inc = base.clone();
let edits: [(IVec3, u32); 3] = [
(IVec3::new(63, 64, 170), 7),
(IVec3::new(0, 0, 155), 4),
(IVec3::new(126, 100, 165), 5),
];
for round in 0..2 {
let mut lo = IVec3::splat(i32::MAX);
let mut hi = IVec3::splat(i32::MIN);
for (k, &(c, r)) in edits.iter().enumerate() {
if k % 2 != round % 2 {
continue; }
#[allow(clippy::cast_possible_wrap)]
let ri = r as i32;
for v in [&mut full, &mut inc] {
set_sphere_with_colfunc(v, c.into(), r, SpanOp::Carve, |_, _, _| {
VoxColor(0x80_31_41_59)
});
}
lo = lo.min(c - IVec3::splat(ri));
hi = hi.max(c + IVec3::splat(ri));
}
full.generate_mips(6);
inc.remip_bbox(lo.x, lo.y, hi.x, hi.y, 6);
assert_eq!(
full.mip_base_offsets, inc.mip_base_offsets,
"round {round}: mip tables diverge",
);
assert_eq!(
full.column_offset, inc.column_offset,
"round {round}: column offsets diverge",
);
assert_eq!(full.data, inc.data, "round {round}: data bytes diverge");
}
}
#[test]
fn vxl_voxel_solid_matches_expandrle_reference() {
use roxlap_formats::edit::expandrle;
let mut g = Grid::new(GridTransform::identity());
g.set_rect(IVec3::new(3, 4, 40), IVec3::new(4, 5, 60), None);
g.set_rect(IVec3::new(3, 4, 100), IVec3::new(4, 5, 140), None);
g.set_voxel(IVec3::new(3, 4, 120), Some(VoxColor(0x80_11_22_33))); g.set_sphere(IVec3::new(8, 8, 80), 6, None);
let vxl = g.chunk(IVec3::ZERO).expect("chunk materialised");
let maxzdim = CHUNK_SIZE_Z as i32;
for (x, y) in [(3u32, 4u32), (8, 8), (0, 0), (8, 2), (12, 8)] {
let column = vxl.column_data((y * vxl.vsid + x) as usize);
let mut b2 = vec![maxzdim; 2 * (CHUNK_SIZE_Z as usize) + 4];
expandrle(column, &mut b2);
for z in 0..CHUNK_SIZE_Z {
let zi = z as i32;
let mut reference = false;
let mut i = 0;
while b2[i] < maxzdim {
if zi >= b2[i] && zi < b2[i + 1] {
reference = true;
break;
}
i += 2;
}
assert_eq!(
vxl_voxel_solid(vxl, x, y, z),
reference,
"column ({x}, {y}) z={z} disagrees with expandrle",
);
}
}
}
}