#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScreenAabb {
pub x_min: i32,
pub y_min: i32,
pub x_max: i32,
pub y_max: i32,
pub depth: f32,
}
impl ScreenAabb {
#[inline]
pub fn new(x_min: i32, y_min: i32, x_max: i32, y_max: i32, depth: f32) -> Self {
Self {
x_min,
y_min,
x_max,
y_max,
depth,
}
}
#[inline]
pub fn clamp(self, width: i32, height: i32) -> Self {
Self {
x_min: self.x_min.max(0),
y_min: self.y_min.max(0),
x_max: self.x_max.min(width - 1),
y_max: self.y_max.min(height - 1),
depth: self.depth,
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.x_min > self.x_max || self.y_min > self.y_max
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcclusionMode {
Conservative,
Accurate,
}
#[derive(Debug, Clone, Copy)]
pub struct OcclusionQuery {
pub mode: OcclusionMode,
}
#[inline(always)]
fn depth_to_fixed(d: f32) -> u32 {
(d * 65536.0) as u32
}
impl OcclusionQuery {
#[inline]
pub fn new(mode: OcclusionMode) -> Self {
Self { mode }
}
pub fn is_visible(&self, aabb: &ScreenAabb, zbuffer: &[crate::ZDepth], width: usize) -> bool {
if aabb.is_empty() {
return false;
}
let aabb_zdepth = crate::to_zdepth(depth_to_fixed(aabb.depth));
match self.mode {
OcclusionMode::Conservative => {
let corners = [
(aabb.x_min, aabb.y_min),
(aabb.x_max, aabb.y_min),
(aabb.x_min, aabb.y_max),
(aabb.x_max, aabb.y_max),
];
for (cx, cy) in corners {
if Self::pixel_visible(zbuffer, width, cx, cy, aabb_zdepth) {
return true;
}
}
false
}
OcclusionMode::Accurate => {
for py in aabb.y_min..=aabb.y_max {
for px in aabb.x_min..=aabb.x_max {
if Self::pixel_visible(zbuffer, width, px, py, aabb_zdepth) {
return true;
}
}
}
false
}
}
}
#[inline(always)]
fn pixel_visible(
zbuffer: &[crate::ZDepth],
width: usize,
px: i32,
py: i32,
aabb_zdepth: crate::ZDepth,
) -> bool {
if px < 0 || py < 0 {
return false;
}
let idx = py as usize * width + px as usize;
if idx >= zbuffer.len() {
return false;
}
let stored = zbuffer[idx];
stored == crate::Z_MAX_VALUE || stored > aabb_zdepth
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OcclusionStats {
pub queries: u32,
pub passed: u32,
pub culled: u32,
}
impl OcclusionStats {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn record(&mut self, visible: bool) {
self.queries += 1;
if visible {
self.passed += 1;
} else {
self.culled += 1;
}
}
#[inline]
pub fn cull_ratio(&self) -> f32 {
self.culled as f32 / self.queries.max(1) as f32
}
#[inline]
pub fn reset(&mut self) {
self.queries = 0;
self.passed = 0;
self.culled = 0;
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use crate::{Z_MAX_VALUE, ZDepth};
#[test]
fn test_screen_aabb_clamp() {
let aabb = ScreenAabb::new(-5, -10, 50, 60, 1.0);
let clamped = aabb.clamp(40, 30);
assert_eq!(clamped.x_min, 0);
assert_eq!(clamped.y_min, 0);
assert_eq!(clamped.x_max, 39); assert_eq!(clamped.y_max, 29); assert!(!clamped.is_empty());
let off_screen = ScreenAabb::new(100, 100, 200, 200, 1.0).clamp(80, 60);
assert!(off_screen.is_empty());
}
#[test]
fn test_occlusion_empty_zbuffer() {
const W: usize = 64;
const H: usize = 64;
let zbuf: std::vec::Vec<ZDepth> = std::vec![Z_MAX_VALUE; W * H];
let aabb = ScreenAabb::new(10, 10, 20, 20, 5.0);
let conservative = OcclusionQuery::new(OcclusionMode::Conservative);
assert!(
conservative.is_visible(&aabb, &zbuf, W),
"Conservative: empty z-buffer must be visible"
);
let accurate = OcclusionQuery::new(OcclusionMode::Accurate);
assert!(
accurate.is_visible(&aabb, &zbuf, W),
"Accurate: empty z-buffer must be visible"
);
}
#[test]
fn test_occlusion_fully_occluded() {
const W: usize = 64;
const H: usize = 64;
let zbuf: std::vec::Vec<ZDepth> = std::vec![0; W * H];
let aabb = ScreenAabb::new(10, 10, 20, 20, 10.0);
let conservative = OcclusionQuery::new(OcclusionMode::Conservative);
assert!(
!conservative.is_visible(&aabb, &zbuf, W),
"Conservative: should be fully occluded"
);
let accurate = OcclusionQuery::new(OcclusionMode::Accurate);
assert!(
!accurate.is_visible(&aabb, &zbuf, W),
"Accurate: should be fully occluded"
);
}
#[test]
fn test_occlusion_stats() {
let mut stats = OcclusionStats::new();
assert_eq!(stats.cull_ratio(), 0.0);
stats.record(true); stats.record(true); stats.record(false); stats.record(false);
assert_eq!(stats.queries, 4);
assert_eq!(stats.passed, 2);
assert_eq!(stats.culled, 2);
assert!((stats.cull_ratio() - 0.5).abs() < 1e-6);
stats.reset();
assert_eq!(stats.queries, 0);
assert_eq!(stats.passed, 0);
assert_eq!(stats.culled, 0);
assert_eq!(stats.cull_ratio(), 0.0);
}
}