Skip to main content

embedded_3dgfx/draw/
effects.rs

1//! Post-processing and environment effect configurations (fog and dithering).
2
3use core::fmt::Debug;
4use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
5
6/// Configuration for depth-based fog effect.
7#[derive(Debug, Clone, Copy)]
8pub struct FogConfig {
9    /// Fog color to blend towards.
10    pub color: Rgb565,
11    /// Near plane distance (fixed-point 16.16 format).
12    pub near: u32,
13    /// Far plane distance (fixed-point 16.16 format).
14    pub far: u32,
15}
16
17impl FogConfig {
18    /// Create a new fog configuration.
19    pub fn new(color: Rgb565, near: f32, far: f32) -> Self {
20        Self {
21            color,
22            near: (near * 65536.0) as u32,
23            far: (far * 65536.0) as u32,
24        }
25    }
26
27    /// Apply fog effect to a color based on depth.
28    #[inline]
29    pub fn apply(&self, base_color: Rgb565, depth: u32) -> Rgb565 {
30        let fog_factor = if depth <= self.near {
31            0u32
32        } else if depth >= self.far {
33            65536u32
34        } else {
35            let numerator = (depth - self.near) as u64;
36            let denominator = (self.far - self.near) as u64;
37            ((numerator * 65536) / denominator) as u32
38        };
39
40        let base_r = base_color.r() as u32;
41        let base_g = base_color.g() as u32;
42        let base_b = base_color.b() as u32;
43
44        let fog_r = self.color.r() as u32;
45        let fog_g = self.color.g() as u32;
46        let fog_b = self.color.b() as u32;
47
48        let r = ((base_r * (65536 - fog_factor) + fog_r * fog_factor) / 65536) as u8;
49        let g = ((base_g * (65536 - fog_factor) + fog_g * fog_factor) / 65536) as u8;
50        let b = ((base_b * (65536 - fog_factor) + fog_b * fog_factor) / 65536) as u8;
51
52        Rgb565::new(r, g, b)
53    }
54}
55
56/// Configuration for ordered dithering effect.
57#[derive(Debug, Clone, Copy)]
58pub struct DitherConfig {
59    /// Dithering intensity (0-255, where 0 is no dithering).
60    pub intensity: u8,
61}
62
63impl DitherConfig {
64    /// 4x4 Bayer matrix for ordered dithering.
65    const BAYER_MATRIX: [[u8; 4]; 4] =
66        [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
67
68    /// Create a new dither configuration.
69    pub fn new(intensity: u8) -> Self {
70        Self { intensity }
71    }
72
73    /// Apply dithering effect to a color based on screen position.
74    #[inline]
75    pub fn apply(&self, color: Rgb565, x: i32, y: i32) -> Rgb565 {
76        if self.intensity == 0 {
77            return color;
78        }
79
80        let matrix_x = (x & 3) as usize;
81        let matrix_y = (y & 3) as usize;
82        let threshold = Self::BAYER_MATRIX[matrix_y][matrix_x];
83
84        let scaled_threshold = ((threshold as u16 * self.intensity as u16) / 15) as u8;
85
86        let r = color.r();
87        let g = color.g();
88        let b = color.b();
89
90        let r = if r > scaled_threshold {
91            r.saturating_sub(scaled_threshold / 2)
92        } else {
93            r.saturating_add(scaled_threshold / 2)
94        };
95
96        let g = if g > scaled_threshold {
97            g.saturating_sub(scaled_threshold / 2)
98        } else {
99            g.saturating_add(scaled_threshold / 2)
100        };
101
102        let b = if b > scaled_threshold {
103            b.saturating_sub(scaled_threshold / 2)
104        } else {
105            b.saturating_add(scaled_threshold / 2)
106        };
107
108        Rgb565::new(r, g, b)
109    }
110}
111
112/// Strategy for depth interpolation during triangle rasterization.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114pub enum DepthInterpolationMode {
115    /// Full per-pixel linear depth interpolation along scanlines (default).
116    #[default]
117    Exact,
118    /// Uniform per-triangle depth using the arithmetic average of vertex depths.
119    FastAverage,
120    /// Uniform per-triangle depth using the maximum (farthest) vertex depth.
121    FastMax,
122}
123
124impl DepthInterpolationMode {
125    /// Pre-process vertex depths according to the interpolation mode.
126    #[inline(always)]
127    pub fn process_depths(&self, z1: f32, z2: f32, z3: f32) -> (f32, f32, f32) {
128        match self {
129            Self::Exact => (z1, z2, z3),
130            Self::FastAverage => {
131                let avg = (z1 + z2 + z3) * (1.0 / 3.0);
132                (avg, avg, avg)
133            }
134            Self::FastMax => {
135                let max_z = if z1 >= z2 && z1 >= z3 {
136                    z1
137                } else if z2 >= z3 {
138                    z2
139                } else {
140                    z3
141                };
142                (max_z, max_z, max_z)
143            }
144        }
145    }
146}
147
148/// Field interlace mode for temporal scanline-interlaced rendering.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub enum InterlaceField {
151    /// Progressive rendering: all scanlines drawn (default).
152    #[default]
153    Progressive,
154    /// Even field: draws only scanlines with even y (0, 2, 4, ...).
155    Even,
156    /// Odd field: draws only scanlines with odd y (1, 3, 5, ...).
157    Odd,
158}
159
160impl InterlaceField {
161    /// Returns true if scanline `y` should be rendered in this field.
162    #[inline(always)]
163    pub fn includes_scanline(&self, y: i32) -> bool {
164        match self {
165            Self::Progressive => true,
166            Self::Even => (y & 1) == 0,
167            Self::Odd => (y & 1) != 0,
168        }
169    }
170
171    /// Toggles between Even and Odd field for successive frames.
172    #[inline]
173    pub fn toggle(&self) -> Self {
174        match self {
175            Self::Progressive => Self::Progressive,
176            Self::Even => Self::Odd,
177            Self::Odd => Self::Even,
178        }
179    }
180}
181
182/// Configuration for screen-door (dithered stipple) transparency.
183///
184/// Discards fragments deterministically against a 4x4 Bayer threshold matrix,
185/// providing order-independent, zero-allocation transparency that writes directly
186/// to the depth buffer without requiring sorting or frame readbacks.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub struct ScreenDoorConfig {
189    /// Opacity level (0 = fully transparent/discarded, 255 = fully opaque).
190    pub alpha: u8,
191}
192
193impl ScreenDoorConfig {
194    const BAYER_THRESHOLD: [[u8; 4]; 4] = [
195        [0, 128, 32, 160],
196        [192, 64, 224, 96],
197        [48, 176, 16, 144],
198        [240, 112, 208, 80],
199    ];
200
201    #[inline(always)]
202    pub const fn new(alpha: u8) -> Self {
203        Self { alpha }
204    }
205
206    /// Evaluates whether a fragment at `(x, y)` passes the screen-door threshold test.
207    #[inline(always)]
208    pub fn test(&self, x: i32, y: i32) -> bool {
209        if self.alpha == 255 {
210            return true;
211        }
212        if self.alpha == 0 {
213            return false;
214        }
215        let mx = (x & 3) as usize;
216        let my = (y & 3) as usize;
217        self.alpha > Self::BAYER_THRESHOLD[my][mx]
218    }
219}
220
221/// Checkerboard pixel parity mode for 50% fill-rate rendering.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum CheckerboardField {
224    /// Draw all pixels (standard).
225    #[default]
226    Disabled,
227    /// Draw pixels where `(x ^ y) & 1 == 0`.
228    Even,
229    /// Draw pixels where `(x ^ y) & 1 == 1`.
230    Odd,
231}
232
233impl CheckerboardField {
234    /// Returns true if pixel `(x, y)` should be rendered in this field.
235    #[inline(always)]
236    pub fn includes_pixel(&self, x: i32, y: i32) -> bool {
237        match self {
238            Self::Disabled => true,
239            Self::Even => ((x ^ y) & 1) == 0,
240            Self::Odd => ((x ^ y) & 1) != 0,
241        }
242    }
243
244    /// Toggles between Even and Odd field for alternating frames.
245    #[inline]
246    pub fn toggle(&self) -> Self {
247        match self {
248            Self::Disabled => Self::Disabled,
249            Self::Even => Self::Odd,
250            Self::Odd => Self::Even,
251        }
252    }
253}
254
255/// Depth bias (polygon offset) configuration to prevent Z-fighting on decals, shadows, and coplanar geometry.
256///
257/// In 3D rendering (similar to OpenGL `glPolygonOffset` or Vulkan depth bias), depth bias offsets
258/// fragment depths before depth comparison and writing.
259///
260/// Depth offset calculation:
261/// `offset = constant + slope_scale * max_slope`
262/// where `max_slope = max(|dz/dx|, |dz/dy|)`.
263///
264/// Negative values bring the primitive closer to the camera (ideal for decals and overlays),
265/// while positive values push it further back (ideal for shadow geometry or outlines).
266#[derive(Debug, Clone, Copy, PartialEq, Default)]
267pub struct DepthBias {
268    /// Constant depth offset applied to fragments (negative pulls closer to camera).
269    pub constant: f32,
270    /// Factor scaling with the maximum depth slope (gradient) of the primitive.
271    pub slope_scale: f32,
272}
273
274impl DepthBias {
275    /// Zero depth bias (disabled).
276    pub const ZERO: Self = Self {
277        constant: 0.0,
278        slope_scale: 0.0,
279    };
280
281    /// Create a new depth bias configuration.
282    #[inline(always)]
283    pub const fn new(constant: f32, slope_scale: f32) -> Self {
284        Self {
285            constant,
286            slope_scale,
287        }
288    }
289
290    /// Preconfigured bias for decals (bullet marks, footsteps, blood splatters)
291    /// shifting geometry slightly closer to the viewer.
292    #[inline(always)]
293    pub const fn decal() -> Self {
294        Self {
295            constant: -0.005,
296            slope_scale: -0.01,
297        }
298    }
299
300    /// Preconfigured bias for planar drop/blob shadows.
301    #[inline(always)]
302    pub const fn shadow() -> Self {
303        Self {
304            constant: -0.002,
305            slope_scale: -0.005,
306        }
307    }
308
309    /// Calculate the depth offset in world/clip depth units.
310    #[inline]
311    pub fn compute_offset(
312        &self,
313        p1: nalgebra::Point2<i32>,
314        p2: nalgebra::Point2<i32>,
315        p3: nalgebra::Point2<i32>,
316        z1: f32,
317        z2: f32,
318        z3: f32,
319    ) -> f32 {
320        if self.constant == 0.0 && self.slope_scale == 0.0 {
321            return 0.0;
322        }
323        let dx1 = (p2.x - p1.x) as f32;
324        let dy1 = (p2.y - p1.y) as f32;
325        let dz1 = z2 - z1;
326
327        let dx2 = (p3.x - p1.x) as f32;
328        let dy2 = (p3.y - p1.y) as f32;
329        let dz2 = z3 - z1;
330
331        let det = dx1 * dy2 - dx2 * dy1;
332        let slope = if det.abs() > 1e-6 {
333            let dzdx = ((dz1 * dy2 - dz2 * dy1) / det).abs();
334            let dzdy = ((dx1 * dz2 - dx2 * dz1) / det).abs();
335            if dzdx > dzdy { dzdx } else { dzdy }
336        } else {
337            0.0
338        };
339
340        self.constant + self.slope_scale * slope
341    }
342
343    /// Apply depth bias to three vertex depths.
344    #[inline]
345    pub fn apply(
346        &self,
347        p1: nalgebra::Point2<i32>,
348        p2: nalgebra::Point2<i32>,
349        p3: nalgebra::Point2<i32>,
350        z1: f32,
351        z2: f32,
352        z3: f32,
353    ) -> (f32, f32, f32) {
354        let offset = self.compute_offset(p1, p2, p3, z1, z2, z3);
355        (
356            (z1 + offset).max(0.0),
357            (z2 + offset).max(0.0),
358            (z3 + offset).max(0.0),
359        )
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_screen_door_alpha() {
369        let full = ScreenDoorConfig::new(255);
370        assert!(full.test(0, 0));
371        assert!(full.test(1, 2));
372
373        let none = ScreenDoorConfig::new(0);
374        assert!(!none.test(0, 0));
375        assert!(!none.test(1, 2));
376
377        let half = ScreenDoorConfig::new(128);
378        // Half of pixels should pass, half should fail across 4x4 matrix
379        let mut passed = 0;
380        for y in 0..4 {
381            for x in 0..4 {
382                if half.test(x, y) {
383                    passed += 1;
384                }
385            }
386        }
387        assert_eq!(passed, 8);
388    }
389
390    #[test]
391    fn test_checkerboard_field() {
392        let even = CheckerboardField::Even;
393        assert!(even.includes_pixel(0, 0));
394        assert!(!even.includes_pixel(1, 0));
395        assert!(even.includes_pixel(1, 1));
396        assert_eq!(even.toggle(), CheckerboardField::Odd);
397    }
398
399    #[test]
400    fn test_depth_bias() {
401        let p1 = nalgebra::Point2::new(0, 0);
402        let p2 = nalgebra::Point2::new(10, 0);
403        let p3 = nalgebra::Point2::new(0, 10);
404
405        // Coplanar flat triangle (slope = 0)
406        let bias = DepthBias::new(-0.01, -0.05);
407        let (z1, z2, z3) = bias.apply(p1, p2, p3, 5.0, 5.0, 5.0);
408        assert!((z1 - 4.99).abs() < 1e-4);
409        assert!((z2 - 4.99).abs() < 1e-4);
410        assert!((z3 - 4.99).abs() < 1e-4);
411
412        // Sloped triangle: dz/dx = (10 - 0) / 10 = 1.0
413        let offset = bias.compute_offset(p1, p2, p3, 0.0, 10.0, 0.0);
414        // offset = -0.01 + (-0.05 * 1.0) = -0.06
415        assert!((offset - (-0.06)).abs() < 1e-4);
416    }
417
418    #[test]
419    fn test_fog_and_dither_configs() {
420        let fog = FogConfig::new(Rgb565::WHITE, 1.0, 3.0);
421        assert_eq!(fog.apply(Rgb565::BLACK, 0), Rgb565::BLACK);
422        assert_eq!(fog.apply(Rgb565::BLACK, fog.far), Rgb565::WHITE);
423        let mid = fog.apply(Rgb565::BLACK, (fog.near + fog.far) / 2);
424        assert!(mid.r() > 5 && mid.r() < 31);
425
426        let zero = DitherConfig::new(0);
427        let color = Rgb565::new(20, 30, 20);
428        assert_eq!(zero.apply(color, 0, 0), color);
429        let heavy = DitherConfig::new(255);
430        assert_ne!(heavy.apply(color, 3, 3), color);
431    }
432
433    #[test]
434    fn test_depth_interpolation_and_interlace() {
435        let (a, b, c) = DepthInterpolationMode::Exact.process_depths(1.0, 2.0, 3.0);
436        assert_eq!((a, b, c), (1.0, 2.0, 3.0));
437        let (a, b, c) = DepthInterpolationMode::FastAverage.process_depths(1.0, 2.0, 3.0);
438        assert!((a - 2.0).abs() < 1e-5);
439        assert_eq!(a, b);
440        assert_eq!(b, c);
441        let (a, b, c) = DepthInterpolationMode::FastMax.process_depths(1.0, 3.0, 2.0);
442        assert_eq!((a, b, c), (3.0, 3.0, 3.0));
443
444        assert!(InterlaceField::Progressive.includes_scanline(0));
445        assert!(InterlaceField::Even.includes_scanline(2));
446        assert!(!InterlaceField::Even.includes_scanline(1));
447        assert!(InterlaceField::Odd.includes_scanline(1));
448        assert_eq!(InterlaceField::Even.toggle(), InterlaceField::Odd);
449        assert_eq!(
450            InterlaceField::Progressive.toggle(),
451            InterlaceField::Progressive
452        );
453    }
454}