use std::collections::HashMap;
use rayon::prelude::*;
use crate::camera_math::{self, CameraState};
use crate::grid_view::GridView;
use crate::opticast::OpticastSettings;
use crate::raster_target::RasterTarget;
use crate::sky::Sky;
use crate::Camera;
use roxlap_formats::material::{material_for_color, Material, MaterialTable};
#[derive(Clone, Copy)]
pub struct DdaEnv<'a> {
pub sky: Option<&'a Sky>,
pub fog_color: u32,
pub fog_max_dist: f32,
pub side_shades: [i8; 6],
pub materials: Option<&'a MaterialTable>,
pub terrain_materials: &'a [(u32, u8)],
}
impl Default for DdaEnv<'_> {
fn default() -> Self {
Self {
sky: None,
fog_color: 0,
fog_max_dist: 0.0,
side_shades: [0; 6],
materials: None,
terrain_materials: &[],
}
}
}
pub trait PixelSink {
fn put(&mut self, idx: usize, color: u32, dist: f32);
}
pub struct RasterSink<'a> {
target: RasterTarget<'a>,
len: usize,
}
impl<'a> RasterSink<'a> {
#[must_use]
pub fn new(framebuffer: &'a mut [u32], zbuffer: &'a mut [f32]) -> Self {
debug_assert_eq!(framebuffer.len(), zbuffer.len());
let len = framebuffer.len();
Self {
target: RasterTarget::new(framebuffer, zbuffer),
len,
}
}
}
impl PixelSink for RasterSink<'_> {
fn put(&mut self, idx: usize, color: u32, dist: f32) {
if idx < self.len {
unsafe {
self.target.write_color(idx, color);
self.target.write_depth(idx, dist);
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct Hit {
color: u32,
dist: f32,
}
#[cfg(test)]
pub(crate) mod prof {
use std::cell::Cell;
thread_local! {
pub static CELLS: Cell<u64> = const { Cell::new(0) };
pub static BRICKS: Cell<u64> = const { Cell::new(0) };
pub static SURF: Cell<u64> = const { Cell::new(0) };
}
pub fn reset() {
CELLS.with(|x| x.set(0));
BRICKS.with(|x| x.set(0));
SURF.with(|x| x.set(0));
}
pub fn read() -> (u64, u64, u64) {
(
CELLS.with(Cell::get),
BRICKS.with(Cell::get),
SURF.with(Cell::get),
)
}
}
#[inline]
pub(crate) fn shade(color: u32, bright_sub: u32) -> u32 {
let a = ((color >> 24) & 0xff).saturating_sub(bright_sub);
let ch = |shift: u32| -> u32 { ((((color >> shift) & 0xff) * a) >> 7).min(255) };
0x8000_0000 | (ch(16) << 16) | (ch(8) << 8) | ch(0)
}
#[inline]
fn apply_fog(color: u32, depth: f32, env: &DdaEnv<'_>) -> u32 {
if env.fog_max_dist <= 0.0 {
return color;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let f = ((depth / env.fog_max_dist).clamp(0.0, 1.0) * 256.0) as u32; let g = 256 - f;
let fog = env.fog_color;
let mix = |shift: u32| -> u32 {
let src = (color >> shift) & 0xff;
let dst = (fog >> shift) & 0xff;
((src * g + dst * f) >> 8).min(255)
};
0x8000_0000 | (mix(16) << 16) | (mix(8) << 8) | mix(0)
}
#[inline]
fn terrain_material(env: &DdaEnv<'_>, color: u32) -> Material {
match env.materials {
Some(table) if !env.terrain_materials.is_empty() => {
table.get(material_for_color(env.terrain_materials, color))
}
_ => Material::OPAQUE,
}
}
#[inline]
fn composite_over(accum: [f32; 3], trans: f32, bg: u32) -> u32 {
let b = rgb_to_f32(bg);
f32_to_rgb([
accum[0] + trans * b[0],
accum[1] + trans * b[1],
accum[2] + trans * b[2],
])
}
#[inline]
fn finalize_exit(
touched: bool,
accum: [f32; 3],
trans: f32,
env: &DdaEnv<'_>,
dir: [f32; 3],
dist: f32,
) -> Option<Hit> {
if !touched {
return None;
}
let bg = match env.sky {
Some(s) => sample_sky(s, dir),
None => 0x8000_0000 | (env.fog_color & 0x00ff_ffff),
};
Some(Hit {
color: composite_over(accum, trans, bg),
dist,
})
}
#[inline]
#[allow(clippy::cast_precision_loss)]
fn rgb_to_f32(c: u32) -> [f32; 3] {
[
((c >> 16) & 0xff) as f32 / 255.0,
((c >> 8) & 0xff) as f32 / 255.0,
(c & 0xff) as f32 / 255.0,
]
}
#[inline]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn f32_to_rgb(c: [f32; 3]) -> u32 {
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
0x8000_0000 | (q(c[0]) << 16) | (q(c[1]) << 8) | q(c[2])
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn sample_sky(sky: &Sky, dir: [f32; 3]) -> u32 {
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len < 1e-9 {
return 0x8000_0000;
}
let d = [dir[0] / len, dir[1] / len, dir[2] / len];
let xsiz_full = sky.lat.len().max(1) as i32; let pi = std::f32::consts::PI;
let elev01 = (-d[2]).clamp(-1.0, 1.0).acos() / pi; let x = (elev01 * xsiz_full as f32) as i32;
let x = x.clamp(0, xsiz_full - 1);
let y = if sky.ysiz <= 1 {
0
} else {
let az = d[1].atan2(d[0]); let yf = ((az / (pi * 2.0)) + 0.5) * sky.ysiz as f32;
(yf as i32).rem_euclid(sky.ysiz)
};
let idx = (y * xsiz_full + x) as usize;
let px = sky.pixels.get(idx).copied().unwrap_or(0) as u32;
0x8000_0000 | (px & 0x00ff_ffff)
}
#[allow(clippy::cast_possible_truncation)]
pub fn render_sky_fill(
fb: &mut [u32],
zb: &[f32],
pitch_pixels: usize,
width: u32,
height: u32,
cam: &CameraState,
settings: &OpticastSettings,
sky: &Sky,
) {
for py in 0..height {
let row = py as usize * pitch_pixels;
for px in 0..width {
let idx = row + px as usize;
if zb[idx].is_finite() {
continue; }
let (_origin, dir) = pixel_ray(cam, settings, px, py);
fb[idx] = sample_sky(sky, dir);
}
}
}
#[must_use]
pub fn pixel_ray(
cs: &CameraState,
settings: &OpticastSettings,
px: u32,
py: u32,
) -> ([f32; 3], [f32; 3]) {
#[allow(clippy::cast_precision_loss)]
let sx = px as f32 - settings.hx;
#[allow(clippy::cast_precision_loss)]
let sy = py as f32 - settings.hy;
let dir = [
sx * cs.right[0] + sy * cs.down[0] + settings.hz * cs.forward[0],
sx * cs.right[1] + sy * cs.down[1] + settings.hz * cs.forward[1],
sx * cs.right[2] + sy * cs.down[2] + settings.hz * cs.forward[2],
];
(cs.pos, dir)
}
pub(crate) fn intersect_aabb(
o: [f32; 3],
dir: [f32; 3],
lo: [f32; 3],
hi: [f32; 3],
) -> Option<(f32, f32)> {
let mut t0 = 0.0f32;
let mut t1 = f32::INFINITY;
for a in 0..3 {
if dir[a].abs() < 1e-9 {
if o[a] < lo[a] || o[a] > hi[a] {
return None;
}
} else {
let inv = 1.0 / dir[a];
let mut ta = (lo[a] - o[a]) * inv;
let mut tb = (hi[a] - o[a]) * inv;
if ta > tb {
core::mem::swap(&mut ta, &mut tb);
}
t0 = t0.max(ta);
t1 = t1.min(tb);
if t0 > t1 {
return None;
}
}
}
Some((t0, t1))
}
const BRICK: i32 = 8;
#[derive(Debug)]
pub(crate) struct BrickMap {
nb: [i32; 3],
bits: Vec<u64>,
ns: [i32; 3],
super_bits: Vec<u64>,
}
const SUPER: i32 = BRICK * BRICK;
impl BrickMap {
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
fn build(grid: &GridView<'_>, mip: u32) -> Self {
let vsid_m = (grid.vsid >> mip).max(1) as i32;
let z_m = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1) as i32;
let nb = [
(vsid_m + BRICK - 1) / BRICK,
(vsid_m + BRICK - 1) / BRICK,
(z_m + BRICK - 1) / BRICK,
];
let ns = [
(nb[0] + BRICK - 1) / BRICK,
(nb[1] + BRICK - 1) / BRICK,
(nb[2] + BRICK - 1) / BRICK,
];
let count = (nb[0] * nb[1] * nb[2]) as usize;
let scount = (ns[0] * ns[1] * ns[2]) as usize;
let mut bits = vec![0u64; count.div_ceil(64)];
let mut super_bits = vec![0u64; scount.div_ceil(64)];
for y in 0..vsid_m {
for x in 0..vsid_m {
let (bx, by) = (x / BRICK, y / BRICK);
grid.for_each_run_mip(x as u32, y as u32, mip, |top, bot| {
for bz in (top / BRICK)..=((bot - 1) / BRICK) {
let idx = ((bz * nb[1] + by) * nb[0] + bx) as usize;
bits[idx / 64] |= 1u64 << (idx % 64);
let sidx =
(((bz / BRICK) * ns[1] + by / BRICK) * ns[0] + bx / BRICK) as usize;
super_bits[sidx / 64] |= 1u64 << (sidx % 64);
}
});
}
}
Self {
nb,
bits,
ns,
super_bits,
}
}
#[inline]
#[allow(clippy::cast_sign_loss)]
fn occupied(&self, b: [i32; 3]) -> bool {
if b[0] < 0
|| b[0] >= self.nb[0]
|| b[1] < 0
|| b[1] >= self.nb[1]
|| b[2] < 0
|| b[2] >= self.nb[2]
{
return false;
}
let idx = ((b[2] * self.nb[1] + b[1]) * self.nb[0] + b[0]) as usize;
(self.bits[idx / 64] >> (idx % 64)) & 1 != 0
}
#[inline]
#[allow(clippy::cast_sign_loss)]
fn occupied_super(&self, s: [i32; 3]) -> bool {
if s[0] < 0
|| s[0] >= self.ns[0]
|| s[1] < 0
|| s[1] >= self.ns[1]
|| s[2] < 0
|| s[2] >= self.ns[2]
{
return false;
}
let idx = ((s[2] * self.ns[1] + s[1]) * self.ns[0] + s[0]) as usize;
(self.super_bits[idx / 64] >> (idx % 64)) & 1 != 0
}
}
pub(crate) fn dda_setup(
origin: [f32; 3],
dir: [f32; 3],
cell: [i32; 3],
cell_size: f32,
) -> ([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 dir[a] > 1e-9 {
step[a] = 1;
#[allow(clippy::cast_precision_loss)]
let boundary = (cell[a] + 1) as f32 * cell_size;
t_max[a] = (boundary - origin[a]) / dir[a];
t_delta[a] = cell_size / dir[a];
} else if dir[a] < -1e-9 {
step[a] = -1;
#[allow(clippy::cast_precision_loss)]
let boundary = cell[a] as f32 * cell_size;
t_max[a] = (boundary - origin[a]) / dir[a];
t_delta[a] = -cell_size / dir[a];
}
}
(step, t_max, t_delta)
}
#[inline]
pub(crate) fn min_axis(t_max: [f32; 3]) -> usize {
if t_max[0] <= t_max[1] && t_max[0] <= t_max[2] {
0
} else if t_max[1] <= t_max[2] {
1
} else {
2
}
}
#[derive(Debug, Default)]
pub struct BrickCache {
maps: HashMap<(i32, i32, i32, u32), (u64, BrickMap)>,
}
impl BrickCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn ensure(&mut self, chunk: [i32; 3], mip: u32, version: u64, view: &GridView<'_>) {
let key = (chunk[0], chunk[1], chunk[2], mip);
let stale = self.maps.get(&key).map_or(true, |(v, _)| *v != version);
if stale {
self.maps.insert(key, (version, BrickMap::build(view, mip)));
}
}
#[inline]
fn get(&self, chunk: [i32; 3], mip: u32) -> Option<&BrickMap> {
self.maps
.get(&(chunk[0], chunk[1], chunk[2], mip))
.map(|(_, m)| m)
}
pub fn retain_chunks(&mut self, keep: impl Fn([i32; 3]) -> bool) {
self.maps.retain(|k, _| keep([k.0, k.1, k.2]));
}
}
#[allow(clippy::cast_possible_wrap)]
fn local_cache(grid: &GridView<'_>, requested_mip: u32) -> (BrickCache, u32) {
let mip = effective_mip(grid, requested_mip);
let mut cache = BrickCache::new();
if let Some(cg) = grid.chunk_grid {
for dz in 0..cg.chunks_z as i32 {
for dy in 0..cg.chunks_y as i32 {
for dx in 0..cg.chunks_x as i32 {
let slot = ((dz * cg.chunks_y as i32 + dy) * cg.chunks_x as i32 + dx) as usize;
if let Some(Some(view)) = cg.chunks.get(slot) {
let ch = [
cg.origin_chunk_xy[0] + dx,
cg.origin_chunk_xy[1] + dy,
cg.origin_chunk_z + dz,
];
cache.ensure(ch, mip, 0, view);
}
}
}
}
} else {
cache.ensure([0, 0, 0], mip, 0, grid);
}
(cache, mip)
}
#[must_use]
pub fn effective_mip(grid: &GridView<'_>, requested: u32) -> u32 {
if requested == 0 {
return 0;
}
let mut m = requested;
if let Some(cg) = grid.chunk_grid {
for c in cg.chunks.iter().flatten() {
m = m.min(c.mip_count().saturating_sub(1));
}
} else {
m = m.min(grid.mip_count().saturating_sub(1));
}
m
}
struct Sampler<'a> {
grid: GridView<'a>,
bricks: &'a BrickCache,
mip: u32,
xy_shift: u32,
xy_mask: i32,
z_shift: u32,
z_mask: i32,
cur_ch: [i32; 3],
cur_view: Option<GridView<'a>>,
cur_brick: Option<&'a BrickMap>,
has_cur: bool,
}
impl<'a> Sampler<'a> {
fn new(grid: GridView<'a>, bricks: &'a BrickCache, mip: u32) -> Self {
let cs_xy = (grid.chunk_size_xy >> mip).max(1);
let cs_z = (crate::grid_view::CHUNK_SIZE_Z >> mip).max(1);
debug_assert!(
cs_xy.is_power_of_two() && cs_z.is_power_of_two(),
"chunk dims must be powers of two for the shift/mask split"
);
#[allow(clippy::cast_possible_wrap)]
Self {
grid,
bricks,
mip,
xy_shift: cs_xy.trailing_zeros(),
xy_mask: cs_xy as i32 - 1,
z_shift: cs_z.trailing_zeros(),
z_mask: cs_z as i32 - 1,
cur_ch: [0; 3],
cur_view: None,
cur_brick: None,
has_cur: false,
}
}
fn select_chunk(&mut self, ch: [i32; 3]) {
if self.has_cur && self.cur_ch == ch {
return;
}
self.cur_view = self.grid.chunk_at_xyz(ch);
self.cur_brick = self.bricks.get(ch, self.mip);
self.cur_ch = ch;
self.has_cur = true;
}
#[allow(clippy::cast_sign_loss)]
fn locate(&self, c: [i32; 3]) -> ([i32; 3], [u32; 3]) {
let ch = [
c[0] >> self.xy_shift,
c[1] >> self.xy_shift,
c[2] >> self.z_shift,
];
let loc = [
(c[0] & self.xy_mask) as u32,
(c[1] & self.xy_mask) as u32,
(c[2] & self.z_mask) as u32,
];
(ch, loc)
}
#[allow(clippy::cast_possible_wrap)]
fn hit(&mut self, c: [i32; 3]) -> Option<u32> {
#[cfg(test)]
prof::SURF.with(|x| x.set(x.get() + 1));
let (ch, loc) = self.locate(c);
self.select_chunk(ch);
let occupied = self.cur_brick.is_some_and(|bm| {
bm.occupied([
loc[0] as i32 / BRICK,
loc[1] as i32 / BRICK,
loc[2] as i32 / BRICK,
])
});
if !occupied {
return None;
}
self.cur_view?
.surface_color_mip(loc[0], loc[1], loc[2], self.mip)
}
#[inline]
fn cells_per_chunk_xy(&self) -> i32 {
1 << self.xy_shift
}
#[inline]
fn cells_per_chunk_z(&self) -> i32 {
1 << self.z_shift
}
#[allow(clippy::cast_sign_loss)]
fn brick_occupied(&mut self, brick: [i32; 3]) -> bool {
let c0 = [brick[0] << 3, brick[1] << 3, brick[2] << 3];
let ch = [
c0[0] >> self.xy_shift,
c0[1] >> self.xy_shift,
c0[2] >> self.z_shift,
];
self.select_chunk(ch);
self.cur_brick.is_some_and(|bm| {
bm.occupied([
(c0[0] & self.xy_mask) >> 3,
(c0[1] & self.xy_mask) >> 3,
(c0[2] & self.z_mask) >> 3,
])
})
}
#[allow(clippy::cast_sign_loss)]
fn super_occupied(&mut self, s: [i32; 3]) -> bool {
let c0 = [s[0] << 6, s[1] << 6, s[2] << 6];
let ch = [
c0[0] >> self.xy_shift,
c0[1] >> self.xy_shift,
c0[2] >> self.z_shift,
];
self.select_chunk(ch);
self.cur_brick.is_some_and(|bm| {
bm.occupied_super([
(c0[0] & self.xy_mask) >> 6,
(c0[1] & self.xy_mask) >> 6,
(c0[2] & self.z_mask) >> 6,
])
})
}
}
#[allow(
clippy::too_many_arguments,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn cell_walk_skip(
origin: [f32; 3],
dir: [f32; 3],
fwd_dot: f32,
sampler: &mut Sampler<'_>,
lo_c: [i32; 3],
hi_c: [i32; 3],
cell_size: f32,
t_enter: f32,
t_exit: f32,
max_dist: f32,
env: &DdaEnv<'_>,
) -> Option<Hit> {
let has_super = sampler.cells_per_chunk_xy() >= SUPER && sampler.cells_per_chunk_z() >= SUPER;
let has_brick = sampler.cells_per_chunk_xy() >= BRICK && sampler.cells_per_chunk_z() >= BRICK;
let start = t_enter + 1e-4;
let p = [
origin[0] + dir[0] * start,
origin[1] + dir[1] * start,
origin[2] + dir[2] * start,
];
let mut cellc = [
((p[0] / cell_size).floor() as i32).clamp(lo_c[0], hi_c[0] - 1),
((p[1] / cell_size).floor() as i32).clamp(lo_c[1], hi_c[1] - 1),
((p[2] / cell_size).floor() as i32).clamp(lo_c[2], hi_c[2] - 1),
];
let (step, mut t_max, t_delta) = dda_setup(origin, dir, cellc, cell_size);
let inv = [
if step[0] != 0 { 1.0 / dir[0] } else { 0.0 },
if step[1] != 0 { 1.0 / dir[1] } else { 0.0 },
if step[2] != 0 { 1.0 / dir[2] } else { 0.0 },
];
let mut t_curr = t_enter;
let mut last_axis = 3usize;
let mut accum = [0.0f32; 3];
let mut trans = 1.0f32;
let mut touched = false;
let mut prev_solid = false;
let mut prev_mat = 0u8;
let span = (hi_c[0] - lo_c[0]) + (hi_c[1] - lo_c[1]) + (hi_c[2] - lo_c[2]);
let max_steps = span.max(0) as usize + 16;
for _ in 0..max_steps {
if cellc[0] < lo_c[0]
|| cellc[0] >= hi_c[0]
|| cellc[1] < lo_c[1]
|| cellc[1] >= hi_c[1]
|| cellc[2] < lo_c[2]
|| cellc[2] >= hi_c[2]
{
return finalize_exit(touched, accum, trans, env, dir, max_dist);
}
let depth = t_curr * fwd_dot;
if depth > max_dist || t_curr > t_exit {
return finalize_exit(touched, accum, trans, env, dir, max_dist);
}
if env.fog_max_dist > 0.0 && depth >= env.fog_max_dist {
let fog = 0x8000_0000 | (env.fog_color & 0x00ff_ffff);
let color = if touched {
composite_over(accum, trans, fog)
} else {
fog
};
return Some(Hit {
color,
dist: env.fog_max_dist,
});
}
let skip_shift = if has_super
&& !sampler.super_occupied([cellc[0] >> 6, cellc[1] >> 6, cellc[2] >> 6])
{
Some(6u32)
} else if has_brick
&& !sampler.brick_occupied([cellc[0] >> 3, cellc[1] >> 3, cellc[2] >> 3])
{
Some(3u32)
} else {
None
};
if let Some(sh) = skip_shift {
#[cfg(test)]
prof::BRICKS.with(|x| x.set(x.get() + 1));
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;
}
let idx = cellc[a] >> sh;
plane[a] = if step[a] > 0 {
(idx + 1) << sh
} else {
idx << sh
};
let tb = (plane[a] as f32 * cell_size - origin[a]) * inv[a];
if tb < best_t {
best_t = tb;
best_axis = a;
}
}
if best_axis == 3 {
return finalize_exit(touched, accum, trans, env, dir, max_dist);
}
let pb = [
origin[0] + dir[0] * (best_t + 1e-4),
origin[1] + dir[1] * (best_t + 1e-4),
origin[2] + dir[2] * (best_t + 1e-4),
];
let mut nc = [
(pb[0] / cell_size).floor() as i32,
(pb[1] / cell_size).floor() as i32,
(pb[2] / cell_size).floor() as i32,
];
nc[best_axis] = if step[best_axis] > 0 {
plane[best_axis]
} else {
plane[best_axis] - 1
};
if nc[0] < lo_c[0]
|| nc[0] >= hi_c[0]
|| nc[1] < lo_c[1]
|| nc[1] >= hi_c[1]
|| nc[2] < lo_c[2]
|| nc[2] >= hi_c[2]
{
return finalize_exit(touched, accum, trans, env, dir, max_dist);
}
cellc = nc;
for a in 0..3 {
if step[a] > 0 {
t_max[a] = ((cellc[a] + 1) as f32 * cell_size - origin[a]) * inv[a];
} else if step[a] < 0 {
t_max[a] = (cellc[a] as f32 * cell_size - origin[a]) * inv[a];
}
}
t_curr = best_t.max(t_curr);
last_axis = best_axis;
prev_solid = false; continue;
}
#[cfg(test)]
prof::CELLS.with(|x| x.set(x.get() + 1));
if let Some(color) = sampler.hit(cellc) {
let bright_sub = side_shade_sub(env, last_axis, step);
let lit = apply_fog(shade(color, bright_sub), depth.max(0.0), env);
let m = terrain_material(env, color);
if m.is_opaque() {
let color = if touched {
composite_over(accum, trans, lit)
} else {
lit
};
return Some(Hit {
color,
dist: depth.max(0.0),
});
}
let mat_id = material_for_color(env.terrain_materials, color);
if !prev_solid || mat_id != prev_mat {
let a = f32::from(m.alpha) / 255.0;
let c = rgb_to_f32(lit);
accum[0] += trans * a * c[0];
accum[1] += trans * a * c[1];
accum[2] += trans * a * c[2];
if !matches!(m.mode, roxlap_formats::material::BlendMode::Additive) {
trans *= 1.0 - a; }
touched = true;
prev_mat = mat_id;
if trans < 1.0 / 256.0 {
return Some(Hit {
color: f32_to_rgb(accum),
dist: depth.max(0.0),
});
}
}
prev_solid = true;
} else {
prev_solid = false;
}
let axis = min_axis(t_max);
last_axis = axis;
t_curr = t_max[axis];
cellc[axis] += step[axis];
t_max[axis] += t_delta[axis];
}
None
}
#[inline]
fn side_shade_sub(env: &DdaEnv<'_>, axis: usize, step: [i32; 3]) -> u32 {
if axis >= 3 {
return 0;
}
let face = axis * 2 + usize::from(step[axis] < 0);
env.side_shades[face].max(0) as u32
}
fn cast_ray(
origin: [f32; 3],
dir: [f32; 3],
forward: [f32; 3],
sampler: &mut Sampler<'_>,
settings: &OpticastSettings,
env: &DdaEnv<'_>,
) -> Option<Hit> {
let (lo_i, hi_i) = sampler.grid.voxel_bounds();
#[allow(clippy::cast_precision_loss)]
let lo_f = [lo_i[0] as f32, lo_i[1] as f32, lo_i[2] as f32];
#[allow(clippy::cast_precision_loss)]
let hi_f = [hi_i[0] as f32, hi_i[1] as f32, hi_i[2] as f32];
let (t_enter, t_exit) = intersect_aabb(origin, dir, lo_f, hi_f)?;
let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
#[allow(clippy::cast_precision_loss)]
let max_dist = settings.max_scan_dist.max(1) as f32;
let cell = 1i32 << sampler.mip;
let cell_size = cell as f32;
let lo_c = [
lo_i[0].div_euclid(cell),
lo_i[1].div_euclid(cell),
lo_i[2].div_euclid(cell),
];
let hi_c = [
hi_i[0].div_euclid(cell),
hi_i[1].div_euclid(cell),
hi_i[2].div_euclid(cell),
];
cell_walk_skip(
origin, dir, fwd_dot, sampler, lo_c, hi_c, cell_size, t_enter, t_exit, max_dist, env,
)
}
pub fn render_dda(
camera: &Camera,
settings: &OpticastSettings,
grid: GridView<'_>,
pitch_pixels: usize,
env: &DdaEnv<'_>,
mip: u32,
sink: &mut impl PixelSink,
) {
let cs = camera_math::derive(
camera,
settings.xres,
settings.yres,
settings.hx,
settings.hy,
settings.hz,
);
let (cache, mip) = local_cache(&grid, mip);
let mut sampler = Sampler::new(grid, &cache, mip);
for py in settings.y_start..settings.y_end {
let row = py as usize * pitch_pixels;
for px in 0..settings.xres {
if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py) {
sink.put(row + px as usize, color, dist);
}
}
}
}
#[inline]
fn pixel_result(
cs: &CameraState,
settings: &OpticastSettings,
sampler: &mut Sampler<'_>,
env: &DdaEnv<'_>,
px: u32,
py: u32,
) -> Option<(u32, f32)> {
let (origin, dir) = pixel_ray(cs, settings, px, py);
if let Some(hit) = cast_ray(origin, dir, cs.forward, sampler, settings, env) {
Some((hit.color, hit.dist))
} else {
env.sky.map(|sky| (sample_sky(sky, dir), f32::INFINITY))
}
}
#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
pub fn render_dda_parallel(
camera: &Camera,
settings: &OpticastSettings,
grid: GridView<'_>,
fb: &mut [u32],
zb: &mut [f32],
pitch_pixels: usize,
env: &DdaEnv<'_>,
cache: &BrickCache,
mip: u32,
) {
debug_assert_eq!(fb.len(), zb.len());
let (y0, y1) = (settings.y_start, settings.y_end);
if y1 <= y0 {
return;
}
let cs = camera_math::derive(
camera,
settings.xres,
settings.yres,
settings.hx,
settings.hy,
settings.hz,
);
let target = RasterTarget::new(fb, zb);
let nthreads = rayon::current_num_threads().max(1);
let rows = (y1 - y0) as usize;
let band = rows.div_ceil(nthreads).max(1) as u32;
let bands: Vec<(u32, u32)> = (y0..y1)
.step_by(band as usize)
.map(|s| (s, (s + band).min(y1)))
.collect();
bands.par_iter().for_each(|&(by0, by1)| {
let mut sampler = Sampler::new(grid, cache, mip);
for py in by0..by1 {
let row = py as usize * pitch_pixels;
for px in 0..settings.xres {
if let Some((color, dist)) = pixel_result(&cs, settings, &mut sampler, env, px, py)
{
let idx = row + px as usize;
unsafe {
target.write_color(idx, color);
target.write_depth(idx, dist);
}
}
}
}
});
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn cast_ray_reference(
origin: [f32; 3],
dir: [f32; 3],
forward: [f32; 3],
grid: &GridView<'_>,
settings: &OpticastSettings,
) -> Option<Hit> {
let nx = grid.vsid as f32;
let nz = f32::from(u16::try_from(crate::grid_view::CHUNK_SIZE_Z).unwrap_or(256));
#[allow(clippy::cast_possible_wrap)]
let n_i = [
grid.vsid as i32,
grid.vsid as i32,
crate::grid_view::CHUNK_SIZE_Z as i32,
];
let (t_enter, t_exit) = intersect_aabb(origin, dir, [0.0; 3], [nx, nx, nz])?;
let fwd_dot = dir[0] * forward[0] + dir[1] * forward[1] + dir[2] * forward[2];
let max_dist = settings.max_scan_dist.max(1) as f32;
let start = t_enter + 1e-4;
let p = [
origin[0] + dir[0] * start,
origin[1] + dir[1] * start,
origin[2] + dir[2] * start,
];
let mut voxel = [
(p[0].floor() as i32).clamp(0, n_i[0] - 1),
(p[1].floor() as i32).clamp(0, n_i[1] - 1),
(p[2].floor() as i32).clamp(0, n_i[2] - 1),
];
let (step, mut t_max, t_delta) = dda_setup(origin, dir, voxel, 1.0);
let mut t_curr = t_enter;
let max_steps = (n_i[0] + n_i[1] + n_i[2]) as usize + 8;
for _ in 0..max_steps {
if voxel[0] < 0
|| voxel[0] >= n_i[0]
|| voxel[1] < 0
|| voxel[1] >= n_i[1]
|| voxel[2] < 0
|| voxel[2] >= n_i[2]
{
return None;
}
let depth = t_curr * fwd_dot;
if depth > max_dist || t_curr > t_exit {
return None;
}
#[allow(clippy::cast_sign_loss)]
if let Some(color) = grid.surface_color(voxel[0] as u32, voxel[1] as u32, voxel[2] as u32) {
return Some(Hit {
color: shade(color, 0),
dist: depth.max(0.0),
});
}
let axis = min_axis(t_max);
t_curr = t_max[axis];
voxel[axis] += step[axis];
t_max[axis] += t_delta[axis];
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct Recorder {
puts: Vec<(usize, u32, f32)>,
}
impl PixelSink for Recorder {
fn put(&mut self, idx: usize, color: u32, dist: f32) {
self.puts.push((idx, color, dist));
}
}
fn oracle_camera() -> Camera {
Camera {
pos: [0.0, 0.0, 0.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
}
}
fn render_mask(grid: GridView<'_>, camera: &Camera, w: u32, h: u32) -> Vec<bool> {
let n = (w as usize) * (h as usize);
let mut fb = vec![0u32; n]; let mut zb = vec![f32::INFINITY; n];
let settings = OpticastSettings::for_oracle_framebuffer(w, h);
{
let mut sink = RasterSink::new(&mut fb, &mut zb);
render_dda(
camera,
&settings,
grid,
w as usize,
&DdaEnv::default(),
0,
&mut sink,
);
}
fb.iter().map(|&c| c != 0).collect()
}
fn rows_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
let w = w as usize;
for y in 0..h as usize {
let row = &mask[y * w..(y + 1) * w];
let first = row.iter().position(|&b| b);
let last = row.iter().rposition(|&b| b);
if let (Some(f), Some(l)) = (first, last) {
if row[f..=l].iter().any(|&b| !b) {
return false;
}
}
}
true
}
fn cols_have_no_holes(mask: &[bool], w: u32, h: u32) -> bool {
let w = w as usize;
let h = h as usize;
for x in 0..w {
let col: Vec<bool> = (0..h).map(|y| mask[y * w + x]).collect();
let first = col.iter().position(|&b| b);
let last = col.iter().rposition(|&b| b);
if let (Some(f), Some(l)) = (first, last) {
if col[f..=l].iter().any(|&b| !b) {
return false;
}
}
}
true
}
#[test]
fn center_pixel_ray_is_forward() {
let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let (origin, dir) = pixel_ray(&cs, &settings, settings.hx as u32, settings.hy as u32);
assert_eq!(origin, [0.0, 0.0, 0.0]);
assert_eq!(
dir.map(f32::to_bits),
[0.0f32, 320.0, 0.0].map(f32::to_bits)
);
}
#[test]
fn corner_pixel_ray_matches_camera_corn0() {
let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
let cs = camera_math::derive(&oracle_camera(), 640, 480, 320.0, 240.0, 320.0);
let (_origin, dir) = pixel_ray(&cs, &settings, 0, 0);
assert_eq!(dir.map(f32::to_bits), cs.corn[0].map(f32::to_bits));
}
#[test]
fn gridview_voxel_color_matches_reference() {
let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |x, _, z| {
let lo = (10..=12).contains(&z);
let hi = (40..=42).contains(&z);
(lo || hi).then_some(0x80_10_20_30 + x)
});
let grid = GridView::from_single_vxl(&vxl);
for x in 0..8 {
for y in 0..8 {
for z in 0..64 {
assert_eq!(
grid.voxel_color(x, y, z),
vxl.voxel_color(x, y, z),
"mismatch at ({x},{y},{z})"
);
}
}
}
}
#[test]
fn empty_grid_no_hits() {
let vxl = roxlap_formats::vxl::Vxl::empty(64);
let grid = GridView::from_single_vxl(&vxl);
let settings = OpticastSettings::for_oracle_framebuffer(64, 48);
let mut rec = Recorder::default();
render_dda(
&oracle_camera(),
&settings,
grid,
64,
&DdaEnv::default(),
0,
&mut rec,
);
assert!(rec.puts.is_empty(), "all-air grid must produce no hits");
}
#[test]
fn floor_seen_from_above() {
const FLOOR_Z: u32 = 40;
const FLOOR_COL: u32 = 0x80_30_60_90;
let vxl =
roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= FLOOR_Z).then_some(FLOOR_COL));
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [16.0, 16.0, 10.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let settings = OpticastSettings::for_oracle_framebuffer(48, 48);
let mut rec = Recorder::default();
render_dda(&cam, &settings, grid, 48, &DdaEnv::default(), 0, &mut rec);
assert!(!rec.puts.is_empty(), "floor must be visible");
let centre = 24usize * 48 + 24;
let hit = rec
.puts
.iter()
.find(|(idx, _, _)| *idx == centre)
.expect("centre ray must hit the floor");
assert_eq!(hit.1 & 0x00ff_ffff, FLOOR_COL & 0x00ff_ffff);
let expected = (FLOOR_Z as f32) - 10.0;
assert!(
(hit.2 - expected).abs() < 1.5,
"centre depth {} not ≈ {}",
hit.2,
expected
);
}
#[test]
fn horizon_splits_sky_and_floor() {
const FLOOR_Z: u32 = 40;
let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| {
(z >= FLOOR_Z).then_some(0x80_44_66_88)
});
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [32.0, 4.0, 30.0],
right: [-1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
};
let (w, h) = (64u32, 64u32);
let mask = render_mask(grid, &cam, w, h);
let count_band = |y0: usize, y1: usize| -> usize {
(y0 * w as usize..y1 * w as usize)
.filter(|&i| mask[i])
.count()
};
let top = count_band(0, h as usize / 4);
let bottom = count_band(3 * h as usize / 4, h as usize);
assert!(mask.iter().any(|&b| b), "floor must be visible");
assert!(mask.iter().any(|&b| !b), "sky must be visible");
assert!(
bottom > top,
"bottom band ({bottom}) should hit more floor than top band ({top})"
);
}
fn render_reference(
grid: GridView<'_>,
camera: &Camera,
w: u32,
h: u32,
) -> (Vec<u32>, Vec<f32>) {
let n = (w as usize) * (h as usize);
let mut fb = vec![0u32; n];
let mut zb = vec![f32::INFINITY; n];
let settings = OpticastSettings::for_oracle_framebuffer(w, h);
let cs = camera_math::derive(camera, w, h, settings.hx, settings.hy, settings.hz);
for py in 0..h {
for px in 0..w {
let (o, d) = pixel_ray(&cs, &settings, px, py);
if let Some(hit) = cast_ray_reference(o, d, cs.forward, &grid, &settings) {
let i = (py * w + px) as usize;
fb[i] = hit.color;
zb[i] = hit.dist;
}
}
}
(fb, zb)
}
fn render_brickmap(
grid: GridView<'_>,
camera: &Camera,
w: u32,
h: u32,
) -> (Vec<u32>, Vec<f32>) {
render_brickmap_env(grid, camera, w, h, &DdaEnv::default())
}
fn render_brickmap_env(
grid: GridView<'_>,
camera: &Camera,
w: u32,
h: u32,
env: &DdaEnv<'_>,
) -> (Vec<u32>, Vec<f32>) {
let n = (w as usize) * (h as usize);
let mut fb = vec![0u32; n];
let mut zb = vec![f32::INFINITY; n];
let settings = OpticastSettings::for_oracle_framebuffer(w, h);
{
let mut sink = RasterSink::new(&mut fb, &mut zb);
render_dda(camera, &settings, grid, w as usize, env, 0, &mut sink);
}
(fb, zb)
}
#[test]
fn no_sky_leak_through_diagonal_wall() {
let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
((x + y == 64) && (2..62).contains(&z)).then_some(0x80_40_80_60)
});
let grid = GridView::from_single_vxl(&vxl);
let (w, h) = (160u32, 160u32);
let c = [10.0, 10.0, 32.0];
let poses = [
Camera::from_yaw_pitch(c, 0.785, 0.0),
Camera::from_yaw_pitch(c, 0.6, 0.1),
Camera::from_yaw_pitch(c, 0.95, -0.1),
Camera::from_yaw_pitch(c, 0.785, 0.3),
Camera::from_yaw_pitch(c, 0.5, 0.0),
];
for (i, cam) in poses.iter().enumerate() {
let (fb_b, _) = render_brickmap(grid, cam, w, h);
let (fb_r, _) = render_reference(grid, cam, w, h);
let leak = (0..(w * h) as usize)
.filter(|&k| (fb_b[k] != 0) != (fb_r[k] != 0))
.count();
assert_eq!(leak, 0, "pose {i}: {leak} px diverge from dense reference");
}
}
#[test]
fn terrain_glass_tints_floor_behind() {
let glass = 0x80_40_C0_E0; let floor = 0x80_C0_40_40; let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| {
if z == 4 {
Some(glass)
} else if z >= 10 {
Some(floor)
} else {
None
}
});
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [8.0, 8.0, 0.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let (w, h) = (32u32, 32u32);
let centre = (h / 2 * w + w / 2) as usize;
let (fb_op, _) = render_brickmap(grid, &cam, w, h);
assert_eq!(
fb_op[centre] & 0x00ff_ffff,
0x0040_C0E0,
"opaque glass first-hit"
);
let mut table = MaterialTable::new();
table.set(1, Material::alpha_blend(128));
let env = DdaEnv {
materials: Some(&table),
terrain_materials: &[(glass & 0x00ff_ffff, 1)],
..DdaEnv::default()
};
let (fb_tr, _) = render_brickmap_env(grid, &cam, w, h, &env);
assert_ne!(
fb_tr[centre], fb_op[centre],
"glass should composite over the floor, not stay opaque"
);
let r_op = (fb_op[centre] >> 16) & 0xff; let r_tr = (fb_tr[centre] >> 16) & 0xff; assert!(
r_tr > r_op,
"floor red tints through the glass (op={r_op:02x} tr={r_tr:02x})"
);
}
#[test]
fn distance_fog_blends_toward_fog_color() {
let vxl =
roxlap_formats::vxl::Vxl::from_dense(64, |_, _, z| (z >= 40).then_some(0x80_FF_FF_FF));
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [32.0, 2.0, 38.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
};
let env = DdaEnv {
sky: None,
fog_color: 0x00_00_00_00, fog_max_dist: 64.0,
side_shades: [0; 6],
materials: None,
terrain_materials: &[],
};
let (w, h) = (64u32, 64u32);
let (fog, _) = render_brickmap_env(grid, &cam, w, h, &env);
let (nofog, zb) = render_brickmap(grid, &cam, w, h);
let (idx, depth) = zb.iter().enumerate().filter(|(_, z)| z.is_finite()).fold(
(0usize, 0.0f32),
|acc, (i, &z)| {
if z > acc.1 {
(i, z)
} else {
acc
}
},
);
assert!(depth > 20.0, "need a deep pixel to test fog (got {depth})");
let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
assert!(
lum(fog[idx]) < lum(nofog[idx]),
"fogged pixel {:08x} not darker than {:08x}",
fog[idx],
nofog[idx]
);
}
#[test]
fn textured_sky_fills_misses() {
let sky = crate::sky::Sky::blue_gradient();
let vxl = roxlap_formats::vxl::Vxl::empty(32); let grid = GridView::from_single_vxl(&vxl);
let env = DdaEnv {
sky: Some(&sky),
fog_color: 0,
fog_max_dist: 0.0,
side_shades: [0; 6],
materials: None,
terrain_materials: &[],
};
let cam = Camera::from_yaw_pitch([16.0, 16.0, 128.0], 0.3, -0.4);
let (w, h) = (48u32, 48u32);
let (fb, _) = render_brickmap_env(grid, &cam, w, h, &env);
assert!(fb.iter().all(|&c| c >> 24 == 0x80), "all misses sky-filled");
let top = fb[0];
let bottom = fb[(h - 1) as usize * w as usize];
assert_ne!(top, bottom, "sky gradient should vary with elevation");
}
#[test]
fn sky_elevation_zenith_at_column_zero() {
let mut pixels = vec![0i32; 8];
pixels[0] = 0x0011_1111; pixels[7] = 0x0099_9999; let sky = crate::sky::Sky::from_pixels(pixels, 8, 1);
let up = sample_sky(&sky, [0.0, 0.0, -1.0]); let down = sample_sky(&sky, [0.0, 0.0, 1.0]); assert_eq!(
up & 0x00ff_ffff,
0x0011_1111,
"looking up → column 0 (zenith)"
);
assert_eq!(
down & 0x00ff_ffff,
0x0099_9999,
"looking down → last column (nadir)"
);
}
#[test]
fn sky_fill_paints_panorama_gridless() {
let sky = crate::sky::Sky::blue_gradient();
let cam = Camera::from_yaw_pitch([0.0, 0.0, 0.0], 0.3, -0.4);
let (w, h) = (48u32, 48u32);
let cs = crate::camera_math::derive(&cam, w, h, 24.0, 24.0, 24.0);
let settings = crate::opticast::OpticastSettings::for_oracle_framebuffer(w, h);
let mut fb = vec![0u32; (w * h) as usize];
let zb = vec![f32::INFINITY; (w * h) as usize];
render_sky_fill(&mut fb, &zb, w as usize, w, h, &cs, &settings, &sky);
assert!(
fb.iter().all(|&c| c >> 24 == 0x80),
"every pixel sky-filled with the brightness byte set"
);
let top = fb[0];
let bottom = fb[(h - 1) as usize * w as usize];
assert_ne!(top, bottom, "sky gradient should vary with elevation");
let mut fb2 = vec![0x1234_5678u32; (w * h) as usize];
let mut zb2 = vec![f32::INFINITY; (w * h) as usize];
zb2[0] = 10.0; render_sky_fill(&mut fb2, &zb2, w as usize, w, h, &cs, &settings, &sky);
assert_eq!(fb2[0], 0x1234_5678, "finite-z pixel is not overwritten");
}
#[test]
fn side_shades_darken_hit_face() {
let vxl =
roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x80_FF_FF_FF));
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [8.0, 8.0, 2.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let centre = 16 * 32 + 16;
let (plain, _) = render_brickmap(grid, &cam, 32, 32);
let env = DdaEnv {
sky: None,
fog_color: 0,
fog_max_dist: 0.0,
side_shades: [0, 0, 0, 0, 0x40, 0],
materials: None,
terrain_materials: &[],
};
let (shaded, _) = render_brickmap_env(grid, &cam, 32, 32, &env);
let lum = |c: u32| (c & 0xff) + ((c >> 8) & 0xff) + ((c >> 16) & 0xff);
assert!(
lum(shaded[centre]) < lum(plain[centre]),
"side-shaded face {:08x} not darker than {:08x}",
shaded[centre],
plain[centre]
);
}
#[test]
fn brickmap_approximates_dense_reference() {
let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
let surf = 30 + ((x / 5 + y / 7) % 11);
let ground = z >= surf;
let block = (20..=24).contains(&z) && (10..20).contains(&x) && (40..50).contains(&y);
(ground || block).then_some(0x80_30_50_70 + (x ^ y) % 0x40)
});
let grid = GridView::from_single_vxl(&vxl);
let (w, h) = (80u32, 80u32);
let poses = [
Camera::orbit(0.6, 0.5, 90.0, [32.0, 32.0, 40.0]),
Camera::orbit(2.1, 0.2, 70.0, [32.0, 32.0, 35.0]),
Camera::orbit(-1.0, 0.9, 120.0, [32.0, 32.0, 45.0]),
];
let n = (w * h) as usize;
for (i, cam) in poses.iter().enumerate() {
let (fb_b, zb_b) = render_brickmap(grid, cam, w, h);
let (fb_r, _zb_r) = render_reference(grid, cam, w, h);
let cov_b = fb_b.iter().filter(|&&c| c != 0).count();
let cov_r = fb_r.iter().filter(|&&c| c != 0).count();
assert!(cov_b > 200, "pose {i} rendered ~empty (cov {cov_b})");
let cov_diff = cov_b.abs_diff(cov_r);
assert!(
cov_diff * 100 <= n, "pose {i} coverage diverged: brick {cov_b} vs dense {cov_r}"
);
let diffs = fb_b.iter().zip(&fb_r).filter(|(a, b)| a != b).count();
assert!(
diffs * 100 <= n * 3, "pose {i} too many pixel diffs vs dense: {diffs}/{n}"
);
for k in 0..n {
if fb_b[k] != 0 {
assert!(zb_b[k].is_finite(), "pose {i} px {k} non-finite depth");
}
}
}
}
#[test]
fn baked_brightness_darkens_color() {
let dim =
roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x40_FF_FF_FF));
let grid = GridView::from_single_vxl(&dim);
let cam = Camera {
pos: [8.0, 8.0, 2.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let (fb, _) = render_brickmap(grid, &cam, 32, 32);
let centre = 16 * 32 + 16;
assert_eq!(fb[centre], 0x80_7F_7F_7F, "got {:08x}", fb[centre]);
let full =
roxlap_formats::vxl::Vxl::from_dense(16, |_, _, z| (z >= 8).then_some(0x80_FF_FF_FF));
let gridf = GridView::from_single_vxl(&full);
let (fbf, _) = render_brickmap(gridf, &cam, 32, 32);
assert_eq!(fbf[centre], 0x80_FF_FF_FF, "got {:08x}", fbf[centre]);
}
#[test]
fn cross_chunk_lookdown_sees_lower_stacked_floor() {
const FLOOR_LOCAL_Z: u32 = 40;
const FLOOR_COL: u32 = 0x80_22_88_44;
let upper = roxlap_formats::vxl::Vxl::empty(32); let lower = roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| {
(z >= FLOOR_LOCAL_Z).then_some(FLOOR_COL)
});
let v_up = GridView::from_single_vxl(&upper);
let v_lo = GridView::from_single_vxl(&lower);
let chunks = [Some(v_up), Some(v_lo)];
let cg = crate::ChunkGrid {
chunks: &chunks,
origin_chunk_xy: [0, 0],
origin_chunk_z: 0,
chunks_x: 1,
chunks_y: 1,
chunks_z: 2,
};
let grid = GridView::from_chunk_grid(&cg, 32);
let cam = Camera {
pos: [16.0, 16.0, 100.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let (w, h) = (48u32, 48u32);
let (fb, zb) = render_brickmap(grid, &cam, w, h);
let centre = 24 * 48 + 24;
assert!(
fb[centre] & 0x00ff_ffff == FLOOR_COL & 0x00ff_ffff,
"centre ray must reach the lower-chunk floor (got {:08x})",
fb[centre]
);
let expected = 296.0 - 100.0;
assert!(
(zb[centre] - expected).abs() < 2.0,
"look-down depth {} not ≈ {expected}",
zb[centre]
);
}
#[test]
fn cross_chunk_xy_floor_is_seamless() {
let mk = || {
roxlap_formats::vxl::Vxl::from_dense(32, |_, _, z| (z >= 20).then_some(0x80_50_50_50))
};
let (c0, c1) = (mk(), mk());
let v0 = GridView::from_single_vxl(&c0);
let v1 = GridView::from_single_vxl(&c1);
let chunks = [Some(v0), Some(v1)];
let cg = crate::ChunkGrid {
chunks: &chunks,
origin_chunk_xy: [0, 0],
origin_chunk_z: 0,
chunks_x: 2,
chunks_y: 1,
chunks_z: 1,
};
let grid = GridView::from_chunk_grid(&cg, 32);
let cam = Camera {
pos: [32.0, 16.0, 4.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let (w, h) = (64u32, 64u32);
let mask = render_mask(grid, &cam, w, h);
let row = (h / 2) as usize * w as usize;
let left = (0..w as usize / 2).filter(|&x| mask[row + x]).count();
let right = (w as usize / 2..w as usize)
.filter(|&x| mask[row + x])
.count();
assert!(
left > 5 && right > 5,
"seam not continuous: left={left} right={right}"
);
}
fn render_mask_mip(grid: GridView<'_>, camera: &Camera, w: u32, h: u32, mip: u32) -> Vec<bool> {
let n = (w as usize) * (h as usize);
let mut fb = vec![0u32; n];
let mut zb = vec![f32::INFINITY; n];
let settings = OpticastSettings::for_oracle_framebuffer(w, h);
{
let mut sink = RasterSink::new(&mut fb, &mut zb);
render_dda(
camera,
&settings,
grid,
w as usize,
&DdaEnv::default(),
mip,
&mut sink,
);
}
fb.iter().map(|&c| c != 0).collect()
}
#[test]
fn mip_render_is_coarse_but_complete() {
let mut vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
let surf = 24 + ((x / 3 + y / 5) % 17);
(z >= surf).then_some(0x80_50_70_90)
});
vxl.generate_mips(4);
assert!(vxl.mip_count() >= 3, "need mips built for this test");
let grid = GridView::from_single_vxl(&vxl);
let (w, h) = (96u32, 96u32);
let cam = Camera::orbit(0.7, 0.6, 110.0, [32.0, 32.0, 36.0]);
let m0 = render_mask_mip(grid, &cam, w, h, 0);
let m2 = render_mask_mip(grid, &cam, w, h, 2);
let c0 = m0.iter().filter(|&&b| b).count();
let c2 = m2.iter().filter(|&&b| b).count();
assert!(c0 > 200 && c2 > 200, "both mips visible (c0={c0} c2={c2})");
let ratio = c2 as f32 / c0 as f32;
assert!(
(0.7..1.4).contains(&ratio),
"mip-2 coverage {c2} vs mip-0 {c0} (ratio {ratio:.2}) diverged"
);
}
#[test]
#[ignore = "perf benchmark — run explicitly with --ignored"]
fn bench_terrain() {
use std::time::Instant;
const NC: i32 = 6;
let cs = crate::grid_view::CHUNK_SIZE_Z; let _ = cs;
let mut vxls: Vec<roxlap_formats::vxl::Vxl> = Vec::new();
for cy in 0..NC {
for cx in 0..NC {
let (ox, oy) = (cx * 128, cy * 128);
let mut v = roxlap_formats::vxl::Vxl::from_dense(128, |x, y, z| {
let (gx, gy) = (ox + x as i32, oy + y as i32);
let surf = 90 + ((gx / 7 + gy / 9).rem_euclid(40)) + ((gx / 23).rem_euclid(20));
(z as i32 >= surf).then_some(0x80_50_70_90 + (x ^ y) % 0x30)
});
v.generate_mips(4);
vxls.push(v);
}
}
let views: Vec<Option<GridView>> = vxls
.iter()
.map(|v| Some(GridView::from_single_vxl(v)))
.collect();
let cg = crate::ChunkGrid {
chunks: &views,
origin_chunk_xy: [0, 0],
origin_chunk_z: 0,
chunks_x: NC as u32,
chunks_y: NC as u32,
chunks_z: 1,
};
let grid = GridView::from_chunk_grid(&cg, 128);
let (w, h) = (960u32, 600u32);
let mut settings = OpticastSettings::for_oracle_framebuffer(w, h);
settings.max_scan_dist = 512;
let n = (w * h) as usize;
let mut fb = vec![0u32; n];
let mut zb = vec![f32::INFINITY; n];
let centre = [f64::from(NC * 128) / 2.0, f64::from(NC * 128) / 2.0, 60.0];
let poses = [
(
"horizon",
Camera::from_yaw_pitch([20.0, 20.0, 40.0], 0.6, 0.15),
),
("down", Camera::orbit(0.7, 1.0, 130.0, centre)),
];
for (name, cam) in poses {
{
let mut sink = RasterSink::new(&mut fb, &mut zb);
prof::reset();
render_dda(
&cam,
&settings,
grid,
w as usize,
&DdaEnv::default(),
0,
&mut sink,
);
}
let (cells, bricks, surf) = prof::read();
let iters = 6;
let t0 = Instant::now();
for _ in 0..iters {
let mut sink = RasterSink::new(&mut fb, &mut zb);
render_dda(
&cam,
&settings,
grid,
w as usize,
&DdaEnv::default(),
0,
&mut sink,
);
}
let ms = t0.elapsed().as_secs_f64() * 1000.0 / f64::from(iters);
let hits = fb.iter().filter(|&&c| c != 0).count();
eprintln!(
"[{name}] {w}x{h} 1-thread: {ms:.1} ms | hits={hits}/{n} | per-px: cells={:.1} bricks={:.1} surf={:.1}",
cells as f64 / n as f64,
bricks as f64 / n as f64,
surf as f64 / n as f64,
);
}
}
#[test]
fn parallel_matches_sequential() {
let vxl = roxlap_formats::vxl::Vxl::from_dense(64, |x, y, z| {
let surf = 28 + ((x / 4 + y / 6) % 13);
(z >= surf).then_some(0x80_40_60_80 + (x ^ y) % 0x30)
});
let grid = GridView::from_single_vxl(&vxl);
let (w, h) = (96u32, 96u32);
let cam = Camera::orbit(0.8, 0.55, 100.0, [32.0, 32.0, 40.0]);
let env = DdaEnv {
sky: None,
fog_color: 0x00_20_30_40,
fog_max_dist: 120.0,
side_shades: [0, 0, 0, 0, 0x30, 0x10],
materials: None,
terrain_materials: &[],
};
let (seq_fb, seq_zb) = render_brickmap_env(grid, &cam, w, h, &env);
let n = (w * h) as usize;
let mut par_fb = vec![0u32; n];
let mut par_zb = vec![f32::INFINITY; n];
let settings = OpticastSettings::for_oracle_framebuffer(w, h);
let (cache, mip) = local_cache(&grid, 0);
render_dda_parallel(
&cam,
&settings,
grid,
&mut par_fb,
&mut par_zb,
w as usize,
&env,
&cache,
mip,
);
assert!(par_fb == seq_fb, "parallel colour differs from sequential");
assert!(
par_zb
.iter()
.zip(&seq_zb)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"parallel depth differs from sequential"
);
}
#[test]
fn cliff_side_is_solid_not_see_through() {
const TOP_Z: u32 = 50;
const COL: u32 = 0x80_77_88_99;
let vxl = roxlap_formats::vxl::Vxl::from_dense(8, |_, _, z| (z >= TOP_Z).then_some(COL));
let grid = GridView::from_single_vxl(&vxl);
assert_eq!(grid.voxel_color(4, 4, TOP_Z), Some(COL));
assert_eq!(grid.voxel_color(4, 4, 150), None);
assert_eq!(grid.surface_color(4, 4, 150), Some(COL));
assert_eq!(grid.surface_color(4, 4, 10), None);
}
#[test]
fn camera_inside_solid_hits_everywhere() {
let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |_, _, _| Some(0x80_55_55_55));
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera {
pos: [8.0, 8.0, 128.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
};
let (w, h) = (32u32, 32u32);
let mask = render_mask(grid, &cam, w, h);
assert!(
mask.iter().all(|&b| b),
"every ray must hit when the camera is inside solid"
);
}
#[test]
fn single_voxel_silhouette_has_no_notch() {
const C: u32 = 0x80_FF_80_40;
let vxl = roxlap_formats::vxl::Vxl::from_dense(16, |x, y, z| {
(x == 8 && y == 8 && z == 8).then_some(C)
});
let grid = GridView::from_single_vxl(&vxl);
let cam = Camera::orbit(0.7, 0.6, 4.0, [8.5, 8.5, 8.5]);
let (w, h) = (96u32, 96u32);
let mask = render_mask(grid, &cam, w, h);
let hits = mask.iter().filter(|&&b| b).count();
assert!(
hits > 30,
"silhouette too small to be meaningful: {hits} px"
);
assert!(
rows_have_no_holes(&mask, w, h),
"row-interior gap in single-voxel silhouette (notch)"
);
assert!(
cols_have_no_holes(&mask, w, h),
"column-interior gap in single-voxel silhouette (notch)"
);
}
}