#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::similar_names,
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::many_single_char_names,
clippy::must_use_candidate,
clippy::unnecessary_cast,
clippy::cast_lossless,
clippy::needless_bool_assign,
clippy::needless_range_loop,
clippy::no_effect,
clippy::identity_op,
clippy::if_not_else
)]
use rayon::prelude::*;
use crate::engine::LightSrc;
pub(crate) const MAXZDIM: i32 = 256;
pub(crate) const ESTNORMRAD: i32 = 2;
pub(crate) const AO_RAD: i32 = 1;
const _: () = assert!(AO_RAD <= ESTNORMRAD);
pub(crate) const AO_STRENGTH: f32 = 0.8;
#[derive(Clone, Copy, Debug)]
pub struct AoParams {
pub strength: f32,
pub radius: i32,
pub min_floor: f32,
}
impl Default for AoParams {
fn default() -> Self {
Self {
strength: AO_STRENGTH,
radius: AO_RAD,
min_floor: 0.0,
}
}
}
pub(crate) const fn xbsflor(k: usize) -> u32 {
if k >= 32 {
0
} else {
(-1i32 << k) as u32
}
}
pub(crate) const fn xbsceil(k: usize) -> u32 {
!xbsflor(k)
}
pub(crate) fn expandbit256(column: &[u8], bits: &mut [u32; 8]) {
let mut src_idx: usize = 0;
let mut dst_idx: usize = 0;
let mut bitpos: i32 = 32;
let mut word: u32 = 0;
let nbits: i32 = (bits.len() as i32) * 32;
let mut next_len: i32;
let mut delta: i32;
let mut go_to_v3 = false;
'outer: loop {
if go_to_v3 {
if src_idx + 3 >= column.len() {
break;
}
delta = i32::from(column[src_idx + 3]) - bitpos;
while delta >= 0 {
if dst_idx >= bits.len() {
break 'outer;
}
bits[dst_idx] = word;
dst_idx += 1;
word = u32::MAX;
bitpos += 32;
delta -= 32;
}
word &= xbsceil((delta + 32) as usize);
}
go_to_v3 = true;
if src_idx + 1 >= column.len() {
break;
}
delta = i32::from(column[src_idx + 1]) - bitpos;
while delta >= 0 {
if dst_idx >= bits.len() {
break 'outer;
}
bits[dst_idx] = word;
dst_idx += 1;
word = 0;
bitpos += 32;
delta -= 32;
}
word |= xbsflor((delta + 32) as usize);
next_len = i32::from(column[src_idx]);
if next_len == 0 {
break;
}
src_idx += (next_len as usize) * 4;
}
if bitpos <= nbits {
while dst_idx < bits.len() {
bits[dst_idx] = word;
dst_idx += 1;
word = u32::MAX;
}
}
}
#[inline]
pub(crate) fn bit256(bits: &[u32; 8], z: usize) -> bool {
(bits[z >> 5] >> (z & 31)) & 1 != 0
}
#[allow(dead_code)] pub struct EstNormCache {
bits: Vec<[u32; 8]>,
origin_x: i32,
origin_y: i32,
width: usize,
#[allow(dead_code)]
height: usize,
vsid: i32,
z_below: Vec<u8>,
z_above: Vec<u8>,
}
impl EstNormCache {
#[must_use]
pub fn build(
world_data: &[u8],
column_offsets: &[u32],
vsid: u32,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> Self {
let vsid_i = vsid as i32;
let reader = |x: i32, y: i32| -> Option<&[u8]> {
if (x | y) < 0 || x >= vsid_i || y >= vsid_i {
return None;
}
let col_idx = (y as u32) * vsid + (x as u32);
let off_start = column_offsets[col_idx as usize] as usize;
Some(&world_data[off_start..])
};
let mut cache = Self::build_with_reader(reader, x0, y0, x1, y1);
cache.vsid = vsid_i;
cache
}
#[must_use]
pub fn build_with_reader_z<'r>(
column_reader: impl Fn(i32, i32, i32) -> Option<&'r [u8]>,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> Self {
let mut cache = Self::build_with_reader(|x, y| column_reader(x, y, 0), x0, y0, x1, y1);
let n = cache.bits.len();
let mut z_below = vec![0u8; n];
let mut z_above = vec![0u8; n];
let pad = ESTNORMRAD as usize;
for yi in 0..cache.height {
let y = cache.origin_y + yi as i32;
for xi in 0..cache.width {
let x = cache.origin_x + xi as i32;
let col = yi * cache.width + xi;
if let Some(column) = column_reader(x, y, -1) {
let mut tmp = [0u32; 8];
expandbit256(column, &mut tmp);
for i in 0..pad {
if bit256(&tmp, (MAXZDIM as usize) - 1 - i) {
z_below[col] |= 1 << i;
}
}
}
if let Some(column) = column_reader(x, y, 1) {
let mut tmp = [0u32; 8];
expandbit256(column, &mut tmp);
for i in 0..pad {
if bit256(&tmp, i) {
z_above[col] |= 1 << i;
}
}
} else {
z_above[col] = ((1u32 << pad) - 1) as u8;
}
}
}
cache.z_below = z_below;
cache.z_above = z_above;
cache
}
#[must_use]
pub fn build_with_reader<'r>(
column_reader: impl Fn(i32, i32) -> Option<&'r [u8]>,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> Self {
let rad = ESTNORMRAD;
let pad_x0 = x0 - rad;
let pad_y0 = y0 - rad;
let pad_x1 = x1 + rad;
let pad_y1 = y1 + rad;
let width = (pad_x1 - pad_x0) as usize;
let height = (pad_y1 - pad_y0) as usize;
let mut bits = vec![[0u32; 8]; width * height];
for yi in 0..height {
let y = pad_y0 + yi as i32;
for xi in 0..width {
let x = pad_x0 + xi as i32;
if let Some(column) = column_reader(x, y) {
expandbit256(column, &mut bits[yi * width + xi]);
}
}
}
Self {
bits,
origin_x: pad_x0,
origin_y: pad_y0,
width,
height,
vsid: 0,
z_below: Vec::new(),
z_above: Vec::new(),
}
}
#[inline]
fn solid(&self, xi: usize, yi: usize, z: i32) -> bool {
if z < 0 {
let i = (-1 - z) as usize;
if !self.z_below.is_empty() && i < ESTNORMRAD as usize {
return (self.z_below[yi * self.width + xi] >> i) & 1 != 0;
}
return false;
}
if z >= MAXZDIM {
let i = (z - MAXZDIM) as usize;
if !self.z_above.is_empty() && i < ESTNORMRAD as usize {
return (self.z_above[yi * self.width + xi] >> i) & 1 != 0;
}
return true;
}
let col = &self.bits[yi * self.width + xi];
let z = z as usize;
(col[z >> 5] >> (z & 31)) & 1 != 0
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn estnorm(&self, x: i32, y: i32, z: i32) -> [f32; 3] {
let cx = (x - self.origin_x) as i32;
let cy = (y - self.origin_y) as i32;
let mut nx = 0i32;
let mut ny = 0i32;
let mut nz = 0i32;
for dy in -ESTNORMRAD..=ESTNORMRAD {
let yi = (cy + dy) as usize;
for dx in -ESTNORMRAD..=ESTNORMRAD {
let xi = (cx + dx) as usize;
for dz in -ESTNORMRAD..=ESTNORMRAD {
if self.solid(xi, yi, z + dz) {
nx += dx;
ny += dy;
nz += dz;
}
}
}
}
let len_sq = nx * nx + ny * ny + nz * nz;
if len_sq == 0 {
return [0.0, 0.0, 0.0];
}
let inv = 1.0 / (len_sq as f32).sqrt();
[nx as f32 * inv, ny as f32 * inv, nz as f32 * inv]
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn ambient_occlusion(&self, x: i32, y: i32, z: i32, radius: i32) -> f32 {
const FACES: [[i32; 3]; 6] = [
[-1, 0, 0],
[1, 0, 0],
[0, -1, 0],
[0, 1, 0],
[0, 0, -1],
[0, 0, 1],
];
let r = radius.clamp(1, ESTNORMRAD);
let cx = (x - self.origin_x) as i32;
let cy = (y - self.origin_y) as i32;
let mut occ = 0.0f32;
let mut total = 0.0f32;
for f in FACES {
if self.solid((cx + f[0]) as usize, (cy + f[1]) as usize, z + f[2]) {
continue;
}
for dy in -r..=r {
for dx in -r..=r {
for dz in -r..=r {
if dx * f[0] + dy * f[1] + dz * f[2] <= 0 {
continue;
}
let d = [dx as f32, dy as f32, dz as f32];
let w = 1.0 / (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
total += w;
if self.solid((cx + dx) as usize, (cy + dy) as usize, z + dz) {
occ += w;
}
}
}
}
}
if total <= 0.0 {
0.0
} else {
occ / total
}
}
#[must_use]
#[allow(dead_code)]
pub(crate) fn vsid(&self) -> i32 {
self.vsid
}
}
pub fn update_lighting(
world_data: &mut [u8],
column_offsets: &[u32],
vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
lightmode: u32,
lights: &[LightSrc],
) {
if lightmode == 0 {
return;
}
let vsid_i = vsid as i32;
let x0p = (x0 - ESTNORMRAD).max(0);
let y0p = (y0 - ESTNORMRAD).max(0);
let z0p = (z0 - ESTNORMRAD).max(0);
let x1p = (x1 + ESTNORMRAD).min(vsid_i);
let y1p = (y1 + ESTNORMRAD).min(vsid_i);
let z1p = (z1 + ESTNORMRAD).min(MAXZDIM);
if x0p >= x1p || y0p >= y1p || z0p >= z1p {
return;
}
let cache = EstNormCache::build(world_data, column_offsets, vsid, x0p, y0p, x1p, y1p);
let lightsub: Vec<f32> = lights.iter().map(|l| 1.0 / (l.r2.sqrt() * l.r2)).collect();
#[allow(clippy::cast_sign_loss)]
let region_w = (x1p - x0p) as usize;
#[allow(clippy::cast_sign_loss)]
let region_h = (y1p - y0p) as usize;
let mut column_extents: Vec<(usize, usize)> = Vec::with_capacity(region_w * region_h);
for yi in 0..region_h {
#[allow(clippy::cast_possible_wrap)]
let y = y0p + yi as i32;
for xi in 0..region_w {
#[allow(clippy::cast_possible_wrap)]
let x = x0p + xi as i32;
#[allow(clippy::cast_sign_loss)]
let col_idx = (y as u32) * vsid + (x as u32);
let start = column_offsets[col_idx as usize] as usize;
let end = start + roxlap_formats::vxl::slng(&world_data[start..]);
column_extents.push((start, end));
}
}
let world_view = WorldDataMutView::new(world_data);
let row_body = |y: i32| {
#[allow(clippy::cast_sign_loss)]
let yi = (y - y0p) as usize;
for x in x0p..x1p {
#[allow(clippy::cast_sign_loss)]
let xi = (x - x0p) as usize;
let (off_start, off_end) = column_extents[yi * region_w + xi];
let column = unsafe { world_view.column_slice(off_start, off_end) };
shade_column(
column,
x,
y,
z0p,
z1p,
lightmode,
lights,
&lightsub,
&cache,
AoParams::default(),
);
}
};
(y0p..y1p).into_par_iter().for_each(row_body);
}
#[allow(clippy::too_many_arguments)]
pub fn update_lighting_chunk<'r>(
target_data: &mut [u8],
target_column_offsets: &[u32],
target_vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
column_reader: impl Fn(i32, i32) -> Option<&'r [u8]>,
lightmode: u32,
lights: &[LightSrc],
) {
if lightmode == 0 {
return;
}
let target_vsid_i = target_vsid as i32;
let z0p = (z0 - ESTNORMRAD).max(0);
let z1p = (z1 + ESTNORMRAD).min(MAXZDIM);
let wx0 = x0.max(0);
let wy0 = y0.max(0);
let wx1 = x1.min(target_vsid_i);
let wy1 = y1.min(target_vsid_i);
if wx0 >= wx1 || wy0 >= wy1 || z0p >= z1p {
return;
}
let cache = EstNormCache::build_with_reader(column_reader, x0, y0, x1, y1);
apply_lighting_with_cache(
target_data,
target_column_offsets,
target_vsid,
wx0,
wy0,
z0p,
wx1,
wy1,
z1p,
&cache,
lightmode,
lights,
AoParams::default(),
);
}
#[allow(clippy::too_many_arguments)]
pub fn apply_lighting_with_cache(
target_data: &mut [u8],
target_column_offsets: &[u32],
target_vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
cache: &EstNormCache,
lightmode: u32,
lights: &[LightSrc],
ao: AoParams,
) {
if lightmode == 0 || x0 >= x1 || y0 >= y1 || z0 >= z1 {
return;
}
let lightsub: Vec<f32> = lights.iter().map(|l| 1.0 / (l.r2.sqrt() * l.r2)).collect();
let region_w = (x1 - x0) as usize;
let region_h = (y1 - y0) as usize;
let mut column_extents: Vec<(usize, usize)> = Vec::with_capacity(region_w * region_h);
for yi in 0..region_h {
let y = y0 + yi as i32;
for xi in 0..region_w {
let x = x0 + xi as i32;
let col_idx = (y as u32) * target_vsid + (x as u32);
let start = target_column_offsets[col_idx as usize] as usize;
let end = start + roxlap_formats::vxl::slng(&target_data[start..]);
column_extents.push((start, end));
}
}
let world_view = WorldDataMutView::new(target_data);
let row_body = |y: i32| {
let yi = (y - y0) as usize;
for x in x0..x1 {
let xi = (x - x0) as usize;
let (off_start, off_end) = column_extents[yi * region_w + xi];
let column = unsafe { world_view.column_slice(off_start, off_end) };
shade_column(
column, x, y, z0, z1, lightmode, lights, &lightsub, cache, ao,
);
}
};
(y0..y1).into_par_iter().for_each(row_body);
}
struct WorldDataMutView<'a> {
ptr: *mut u8,
len: usize,
_marker: std::marker::PhantomData<&'a mut [u8]>,
}
unsafe impl Send for WorldDataMutView<'_> {}
unsafe impl Sync for WorldDataMutView<'_> {}
impl<'a> WorldDataMutView<'a> {
fn new(buf: &'a mut [u8]) -> Self {
Self {
ptr: buf.as_mut_ptr(),
len: buf.len(),
_marker: std::marker::PhantomData,
}
}
unsafe fn column_slice(&self, off_start: usize, off_end: usize) -> &'a mut [u8] {
debug_assert!(off_start <= off_end, "column slice: start > end");
debug_assert!(off_end <= self.len, "column slice: end past buffer");
unsafe { std::slice::from_raw_parts_mut(self.ptr.add(off_start), off_end - off_start) }
}
}
#[allow(clippy::cast_lossless)]
fn shade_column(
column: &mut [u8],
x: i32,
y: i32,
z_lo: i32,
z_hi: i32,
lightmode: u32,
lights: &[LightSrc],
lightsub: &[f32],
cache: &EstNormCache,
ao: AoParams,
) {
let mut v_off: usize = 0;
let mut cstat = false;
loop {
let (sz0, sz1, voxel_byte_offset_signed): (i32, i32, isize);
if !cstat {
if v_off + 2 >= column.len() {
break;
}
let v1 = i32::from(column[v_off + 1]);
let v2 = i32::from(column[v_off + 2]);
sz0 = v1;
sz1 = v2 + 1;
voxel_byte_offset_signed = (v_off as isize) + 7 - ((sz0 as isize) << 2);
cstat = true;
} else {
if v_off + 2 >= column.len() {
break;
}
let v0 = i32::from(column[v_off]);
let v1 = i32::from(column[v_off + 1]);
let v2 = i32::from(column[v_off + 2]);
let prev_offset = v2 - v1 - v0 + 2; if v0 == 0 {
break;
}
v_off += (v0 as usize) * 4;
if v_off + 3 >= column.len() {
break;
}
let v3 = i32::from(column[v_off + 3]);
sz1 = v3;
sz0 = prev_offset + sz1;
voxel_byte_offset_signed = (v_off as isize) + 3 - ((sz1 as isize) << 2);
cstat = false;
}
let lo = sz0.max(z_lo);
let hi = sz1.min(z_hi);
for z in lo..hi {
let brightness = if lightmode == 3 {
ao_byte(cache, x, y, z, ao)
} else {
let normal = cache.estnorm(x, y, z);
compute_brightness(x, y, z, normal, lightmode, lights, lightsub)
};
let byte_off = voxel_byte_offset_signed + ((z as isize) << 2);
if byte_off >= 0 && (byte_off as usize) < column.len() {
column[byte_off as usize] = brightness;
}
}
}
}
fn ao_byte(cache: &EstNormCache, x: i32, y: i32, z: i32, params: AoParams) -> u8 {
let ao = cache.ambient_occlusion(x, y, z, params.radius);
let factor = (1.0 - params.strength * ao).max(params.min_floor);
clamp_to_byte(128.0 * factor)
}
fn compute_brightness(
x: i32,
y: i32,
z: i32,
tp: [f32; 3],
lightmode: u32,
lights: &[LightSrc],
lightsub: &[f32],
) -> u8 {
if lightmode < 2 {
let f = (tp[1] * 0.5 + tp[2]) * 64.0 + 103.5;
clamp_to_byte(f)
} else {
let mut f = (tp[1] * 0.5 + tp[2]) * 16.0 + 47.5;
let xf = x as f32;
let yf = y as f32;
let zf = z as f32;
for (i, light) in lights.iter().enumerate() {
let fx = light.pos[0] - xf;
let fy = light.pos[1] - yf;
let fz = light.pos[2] - zf;
let h = tp[0] * fx + tp[1] * fy + tp[2] * fz;
if h >= 0.0 {
continue;
}
let g_sq = fx * fx + fy * fy + fz * fz;
if g_sq >= light.r2 {
continue;
}
let g = 1.0 / (g_sq * g_sq.sqrt()) - lightsub[i];
f -= g * h * light.sc;
}
clamp_to_byte(f)
}
}
#[inline]
fn clamp_to_byte(f: f32) -> u8 {
if f >= 255.0 {
255
} else if f <= 0.0 {
0
} else {
f as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ao_only_darkens_concave_not_convex() {
let vsid: u32 = 10;
let column = |z1: u8, z2: u8| -> Vec<u8> {
let mut c = vec![0u8, z1, z2, 0];
for _ in z1..=z2 {
c.extend([0x20, 0x20, 0x20, 0x80]);
}
c
};
let floor = column(20, 20); let block = column(15, 15); let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
let x = i % vsid;
let y = i / vsid;
let raised = (3..=5).contains(&x) && (3..=5).contains(&y);
data.extend_from_slice(if raised { &block } else { &floor });
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
let cache = EstNormCache::build(&data, &offsets, vsid, 0, 0, vsid as i32, vsid as i32);
let ao = |x, y, z| cache.ambient_occlusion(x, y, z, AO_RAD);
let top_center = ao(4, 4, 15); let top_edge = ao(3, 4, 15); let base = ao(2, 4, 20); let flat = ao(0, 0, 20); assert!(flat < 0.01, "open flat floor must not occlude: {flat}");
assert!(
top_center < 0.01,
"convex flat top must not occlude: {top_center}"
);
assert!(top_edge < 0.01, "convex edge must not occlude: {top_edge}");
assert!(base > 0.1, "concave base must occlude: {base}");
let p = |strength, min_floor| AoParams {
strength,
radius: AO_RAD,
min_floor,
};
let off = ao_byte(&cache, 2, 4, 20, p(0.0, 0.0));
assert_eq!(off, 128, "strength 0 ⇒ no darkening (full ambient)");
let full = ao_byte(&cache, 2, 4, 20, p(1.0, 0.0));
assert!(full < 128, "strength 1 darkens the concave voxel: {full}");
let floored = ao_byte(&cache, 2, 4, 20, p(1.0, 0.8));
assert!(
floored > full && floored >= 100,
"min_floor 0.8 clamps darkening to ≥ ~102: floored={floored} full={full}",
);
}
#[test]
fn ao_z_seam_reads_stacked_neighbour() {
let mk = |z1: u8, z2: u8| -> Vec<u8> {
let mut c = vec![0u8, z1, z2, 0];
for _ in z1..=z2 {
c.extend([0x20, 0x20, 0x20, 0x80]);
}
c
};
let floor = mk(0, 0);
let pit = mk(1, 255);
let above = mk(255, 255);
let reader = |x: i32, y: i32, dz: i32| -> Option<&[u8]> {
match (x, y, dz) {
(1, 1, 0) => Some(&floor),
(2, 1, 0) => Some(&pit),
(2, 1, -1) => Some(&above),
_ => None,
}
};
let plain = EstNormCache::build_with_reader(|x, y| reader(x, y, 0), 0, 0, 3, 3);
let zaware = EstNormCache::build_with_reader_z(reader, 0, 0, 3, 3);
let ao_plain = plain.ambient_occlusion(1, 1, 0, AO_RAD);
let ao_z = zaware.ambient_occlusion(1, 1, 0, AO_RAD);
assert!(
ao_plain > 0.0,
"the in-layer pit wall should already occlude a little: {ao_plain}"
);
assert!(
ao_z > ao_plain + 0.01,
"the solid across the z-seam must add occlusion: z-aware={ao_z} plain={ao_plain}"
);
}
#[test]
fn ambient_occlusion_darkens_next_to_a_wall() {
let vsid: u32 = 8;
let column = |z1: u8, z2: u8| -> Vec<u8> {
let mut c = vec![0u8, z1, z2, 0];
for _ in z1..=z2 {
c.extend([0x20, 0x20, 0x20, 0x80]);
}
c
};
let floor = column(20, 20); let wall = column(10, 20); let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
let col = if i == 3 * vsid + 5 { &wall } else { &floor };
data.extend_from_slice(col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
let cache = EstNormCache::build(&data, &offsets, vsid, 0, 0, vsid as i32, vsid as i32);
let near = cache.ambient_occlusion(4, 3, 20, AO_RAD);
let open = cache.ambient_occlusion(2, 3, 20, AO_RAD);
assert!(
open < 0.05,
"open floor voxel should be ~unoccluded: {open}"
);
assert!(
near > open + 0.1,
"voxel beside the wall must be more occluded: near={near} open={open}",
);
}
#[test]
fn lightmode3_bakes_ambient_occlusion() {
let vsid: u32 = 8;
let column = |z1: u8, z2: u8| -> Vec<u8> {
let mut c = vec![0u8, z1, z2, 0];
for _ in z1..=z2 {
c.extend([0x20, 0x20, 0x20, 0xab]); }
c
};
let floor = column(20, 20);
let wall = column(10, 20);
let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
let col = if i == 3 * vsid + 5 { &wall } else { &floor };
data.extend_from_slice(col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
update_lighting(&mut data, &offsets, vsid, 0, 0, 0, 8, 8, 30, 3, &[]);
let alpha = |x: u32, y: u32| data[offsets[(y * vsid + x) as usize] as usize + 7];
let near = alpha(4, 3);
let open = alpha(2, 3);
assert_ne!(open, 0xab, "open voxel alpha rewritten by the AO bake");
assert_eq!(open, 128, "open floor voxel keeps full ambient (128)");
assert!(
near < open,
"voxel beside the wall is darker: near={near} open={open}"
);
}
#[test]
fn xbsflor_xbsceil_known_values() {
assert_eq!(xbsflor(0), 0xffff_ffff);
assert_eq!(xbsflor(1), 0xffff_fffe);
assert_eq!(xbsflor(5), 0xffff_ffe0);
assert_eq!(xbsflor(31), 0x8000_0000);
assert_eq!(xbsflor(32), 0);
assert_eq!(xbsceil(0), 0);
assert_eq!(xbsceil(5), 0x1f);
assert_eq!(xbsceil(31), 0x7fff_ffff);
assert_eq!(xbsceil(32), 0xffff_ffff);
}
#[test]
fn single_slab_z10_to_14_sets_correct_bits() {
let mut col = vec![0u8, 10, 14, 0]; col.extend(vec![0u8; 5 * 4]);
let mut bits = [0u32; 8];
expandbit256(&col, &mut bits);
assert_eq!(
bits[0], 0xffff_fc00,
"word 0 want 0xffff_fc00 got 0x{:08x}",
bits[0]
);
for (i, w) in bits.iter().enumerate().skip(1) {
assert_eq!(*w, 0xffff_ffff, "word {i} want -1 got 0x{:08x}", *w);
}
}
#[test]
fn lightmode1_bakes_brightness_into_visible_voxels() {
let vsid: u32 = 4;
let mut col = vec![0u8, 20, 24, 0]; for _ in 20..=24 {
col.extend([0x10, 0x20, 0x30, 0xab]);
}
let col_len = col.len() as u32;
let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
data.extend_from_slice(&col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
assert_eq!(col_len as usize * (vsid * vsid) as usize, data.len());
update_lighting(
&mut data,
&offsets,
vsid,
1,
1,
0,
3,
3,
30, 1, &[],
);
let off1 = offsets[(1 * vsid + 1) as usize] as usize;
let alphas: Vec<u8> = (0..5).map(|i| data[off1 + 4 + i * 4 + 3]).collect();
for (i, &a) in alphas.iter().enumerate() {
assert_ne!(a, 0xab, "alpha[{i}] not rewritten");
}
for (i, &a) in alphas.iter().enumerate() {
assert!(
a > 100,
"alpha[{i}]={a} should be on the bright side for top-of-floor voxels"
);
}
}
#[test]
fn lightmode2_with_light_produces_per_column_variation() {
let vsid: u32 = 5;
let mut col = vec![0u8, 20, 24, 0];
for _ in 20..=24 {
col.extend([0x10, 0x20, 0x30, 0]);
}
let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
data.extend_from_slice(&col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
let lights = [LightSrc {
pos: [4.0, 2.0, 20.0],
r2: 50.0 * 50.0,
sc: 64.0,
}];
update_lighting(&mut data, &offsets, vsid, 0, 0, 0, 5, 5, 30, 2, &lights);
let alpha_at = |x: u32, z_idx: usize| {
let off = offsets[(2 * vsid + x) as usize] as usize;
data[off + 4 + z_idx * 4 + 3]
};
let close = alpha_at(4, 0); let far = alpha_at(0, 0); assert!(
close >= far,
"column nearer the light should be ≥ as bright as the far one (close={close} far={far})"
);
}
#[test]
fn empty_column_all_air() {
let col = vec![0u8, 0, 0, 0]; let mut bits = [0u32; 8];
expandbit256(&col, &mut bits);
assert_eq!(
bits[0], 0xffff_ffff,
"empty column word 0 want all-1 got 0x{:08x}",
bits[0]
);
}
}