use core::fmt::Debug;
use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
#[derive(Debug, Clone, Copy)]
pub struct FogConfig {
pub color: Rgb565,
pub near: u32,
pub far: u32,
}
impl FogConfig {
pub fn new(color: Rgb565, near: f32, far: f32) -> Self {
Self {
color,
near: (near * 65536.0) as u32,
far: (far * 65536.0) as u32,
}
}
#[inline]
pub fn apply(&self, base_color: Rgb565, depth: u32) -> Rgb565 {
let fog_factor = if depth <= self.near {
0u32
} else if depth >= self.far {
65536u32
} else {
let numerator = (depth - self.near) as u64;
let denominator = (self.far - self.near) as u64;
((numerator * 65536) / denominator) as u32
};
let base_r = base_color.r() as u32;
let base_g = base_color.g() as u32;
let base_b = base_color.b() as u32;
let fog_r = self.color.r() as u32;
let fog_g = self.color.g() as u32;
let fog_b = self.color.b() as u32;
let r = ((base_r * (65536 - fog_factor) + fog_r * fog_factor) / 65536) as u8;
let g = ((base_g * (65536 - fog_factor) + fog_g * fog_factor) / 65536) as u8;
let b = ((base_b * (65536 - fog_factor) + fog_b * fog_factor) / 65536) as u8;
Rgb565::new(r, g, b)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DitherConfig {
pub intensity: u8,
}
impl DitherConfig {
const BAYER_MATRIX: [[u8; 4]; 4] =
[[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
pub fn new(intensity: u8) -> Self {
Self { intensity }
}
#[inline]
pub fn apply(&self, color: Rgb565, x: i32, y: i32) -> Rgb565 {
if self.intensity == 0 {
return color;
}
let matrix_x = (x & 3) as usize;
let matrix_y = (y & 3) as usize;
let threshold = Self::BAYER_MATRIX[matrix_y][matrix_x];
let scaled_threshold = ((threshold as u16 * self.intensity as u16) / 15) as u8;
let r = color.r();
let g = color.g();
let b = color.b();
let r = if r > scaled_threshold {
r.saturating_sub(scaled_threshold / 2)
} else {
r.saturating_add(scaled_threshold / 2)
};
let g = if g > scaled_threshold {
g.saturating_sub(scaled_threshold / 2)
} else {
g.saturating_add(scaled_threshold / 2)
};
let b = if b > scaled_threshold {
b.saturating_sub(scaled_threshold / 2)
} else {
b.saturating_add(scaled_threshold / 2)
};
Rgb565::new(r, g, b)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DepthInterpolationMode {
#[default]
Exact,
FastAverage,
FastMax,
}
impl DepthInterpolationMode {
#[inline(always)]
pub fn process_depths(&self, z1: f32, z2: f32, z3: f32) -> (f32, f32, f32) {
match self {
Self::Exact => (z1, z2, z3),
Self::FastAverage => {
let avg = (z1 + z2 + z3) * (1.0 / 3.0);
(avg, avg, avg)
}
Self::FastMax => {
let max_z = if z1 >= z2 && z1 >= z3 {
z1
} else if z2 >= z3 {
z2
} else {
z3
};
(max_z, max_z, max_z)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InterlaceField {
#[default]
Progressive,
Even,
Odd,
}
impl InterlaceField {
#[inline(always)]
pub fn includes_scanline(&self, y: i32) -> bool {
match self {
Self::Progressive => true,
Self::Even => (y & 1) == 0,
Self::Odd => (y & 1) != 0,
}
}
#[inline]
pub fn toggle(&self) -> Self {
match self {
Self::Progressive => Self::Progressive,
Self::Even => Self::Odd,
Self::Odd => Self::Even,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ScreenDoorConfig {
pub alpha: u8,
}
impl ScreenDoorConfig {
const BAYER_THRESHOLD: [[u8; 4]; 4] = [
[0, 128, 32, 160],
[192, 64, 224, 96],
[48, 176, 16, 144],
[240, 112, 208, 80],
];
#[inline(always)]
pub const fn new(alpha: u8) -> Self {
Self { alpha }
}
#[inline(always)]
pub fn test(&self, x: i32, y: i32) -> bool {
if self.alpha == 255 {
return true;
}
if self.alpha == 0 {
return false;
}
let mx = (x & 3) as usize;
let my = (y & 3) as usize;
self.alpha > Self::BAYER_THRESHOLD[my][mx]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CheckerboardField {
#[default]
Disabled,
Even,
Odd,
}
impl CheckerboardField {
#[inline(always)]
pub fn includes_pixel(&self, x: i32, y: i32) -> bool {
match self {
Self::Disabled => true,
Self::Even => ((x ^ y) & 1) == 0,
Self::Odd => ((x ^ y) & 1) != 0,
}
}
#[inline]
pub fn toggle(&self) -> Self {
match self {
Self::Disabled => Self::Disabled,
Self::Even => Self::Odd,
Self::Odd => Self::Even,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct DepthBias {
pub constant: f32,
pub slope_scale: f32,
}
impl DepthBias {
pub const ZERO: Self = Self {
constant: 0.0,
slope_scale: 0.0,
};
#[inline(always)]
pub const fn new(constant: f32, slope_scale: f32) -> Self {
Self {
constant,
slope_scale,
}
}
#[inline(always)]
pub const fn decal() -> Self {
Self {
constant: -0.005,
slope_scale: -0.01,
}
}
#[inline(always)]
pub const fn shadow() -> Self {
Self {
constant: -0.002,
slope_scale: -0.005,
}
}
#[inline]
pub fn compute_offset(
&self,
p1: nalgebra::Point2<i32>,
p2: nalgebra::Point2<i32>,
p3: nalgebra::Point2<i32>,
z1: f32,
z2: f32,
z3: f32,
) -> f32 {
if self.constant == 0.0 && self.slope_scale == 0.0 {
return 0.0;
}
let dx1 = (p2.x - p1.x) as f32;
let dy1 = (p2.y - p1.y) as f32;
let dz1 = z2 - z1;
let dx2 = (p3.x - p1.x) as f32;
let dy2 = (p3.y - p1.y) as f32;
let dz2 = z3 - z1;
let det = dx1 * dy2 - dx2 * dy1;
let slope = if det.abs() > 1e-6 {
let dzdx = ((dz1 * dy2 - dz2 * dy1) / det).abs();
let dzdy = ((dx1 * dz2 - dx2 * dz1) / det).abs();
if dzdx > dzdy { dzdx } else { dzdy }
} else {
0.0
};
self.constant + self.slope_scale * slope
}
#[inline]
pub fn apply(
&self,
p1: nalgebra::Point2<i32>,
p2: nalgebra::Point2<i32>,
p3: nalgebra::Point2<i32>,
z1: f32,
z2: f32,
z3: f32,
) -> (f32, f32, f32) {
let offset = self.compute_offset(p1, p2, p3, z1, z2, z3);
(
(z1 + offset).max(0.0),
(z2 + offset).max(0.0),
(z3 + offset).max(0.0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_screen_door_alpha() {
let full = ScreenDoorConfig::new(255);
assert!(full.test(0, 0));
assert!(full.test(1, 2));
let none = ScreenDoorConfig::new(0);
assert!(!none.test(0, 0));
assert!(!none.test(1, 2));
let half = ScreenDoorConfig::new(128);
let mut passed = 0;
for y in 0..4 {
for x in 0..4 {
if half.test(x, y) {
passed += 1;
}
}
}
assert_eq!(passed, 8);
}
#[test]
fn test_checkerboard_field() {
let even = CheckerboardField::Even;
assert!(even.includes_pixel(0, 0));
assert!(!even.includes_pixel(1, 0));
assert!(even.includes_pixel(1, 1));
assert_eq!(even.toggle(), CheckerboardField::Odd);
}
#[test]
fn test_depth_bias() {
let p1 = nalgebra::Point2::new(0, 0);
let p2 = nalgebra::Point2::new(10, 0);
let p3 = nalgebra::Point2::new(0, 10);
let bias = DepthBias::new(-0.01, -0.05);
let (z1, z2, z3) = bias.apply(p1, p2, p3, 5.0, 5.0, 5.0);
assert!((z1 - 4.99).abs() < 1e-4);
assert!((z2 - 4.99).abs() < 1e-4);
assert!((z3 - 4.99).abs() < 1e-4);
let offset = bias.compute_offset(p1, p2, p3, 0.0, 10.0, 0.0);
assert!((offset - (-0.06)).abs() < 1e-4);
}
#[test]
fn test_fog_and_dither_configs() {
let fog = FogConfig::new(Rgb565::WHITE, 1.0, 3.0);
assert_eq!(fog.apply(Rgb565::BLACK, 0), Rgb565::BLACK);
assert_eq!(fog.apply(Rgb565::BLACK, fog.far), Rgb565::WHITE);
let mid = fog.apply(Rgb565::BLACK, (fog.near + fog.far) / 2);
assert!(mid.r() > 5 && mid.r() < 31);
let zero = DitherConfig::new(0);
let color = Rgb565::new(20, 30, 20);
assert_eq!(zero.apply(color, 0, 0), color);
let heavy = DitherConfig::new(255);
assert_ne!(heavy.apply(color, 3, 3), color);
}
#[test]
fn test_depth_interpolation_and_interlace() {
let (a, b, c) = DepthInterpolationMode::Exact.process_depths(1.0, 2.0, 3.0);
assert_eq!((a, b, c), (1.0, 2.0, 3.0));
let (a, b, c) = DepthInterpolationMode::FastAverage.process_depths(1.0, 2.0, 3.0);
assert!((a - 2.0).abs() < 1e-5);
assert_eq!(a, b);
assert_eq!(b, c);
let (a, b, c) = DepthInterpolationMode::FastMax.process_depths(1.0, 3.0, 2.0);
assert_eq!((a, b, c), (3.0, 3.0, 3.0));
assert!(InterlaceField::Progressive.includes_scanline(0));
assert!(InterlaceField::Even.includes_scanline(2));
assert!(!InterlaceField::Even.includes_scanline(1));
assert!(InterlaceField::Odd.includes_scanline(1));
assert_eq!(InterlaceField::Even.toggle(), InterlaceField::Odd);
assert_eq!(
InterlaceField::Progressive.toggle(),
InterlaceField::Progressive
);
}
}