Skip to main content

agg_rust/
span_gouraud_rgba.rs

1//! RGBA Gouraud shading span generator.
2//!
3//! Port of `agg_span_gouraud_rgba.h` — interpolates RGBA colors across a
4//! triangle using DDA-based scanline interpolation.
5
6use crate::basics::{iround, VertexSource};
7use crate::color::Rgba8;
8use crate::dda_line::DdaLineInterpolator;
9use crate::math::cross_product;
10use crate::renderer_scanline::SpanGenerator;
11use crate::span_gouraud::{CoordType, SpanGouraud};
12
13const SUBPIXEL_SHIFT: i32 = 4;
14const SUBPIXEL_SCALE: i32 = 1 << SUBPIXEL_SHIFT;
15
16// ============================================================================
17// RgbaCalc — per-edge color/position interpolator
18// ============================================================================
19
20/// Per-edge interpolation state for Gouraud shading.
21///
22/// Computes color and x-position at a given scanline y by linearly
23/// interpolating between two triangle vertices.
24///
25/// Port of C++ `span_gouraud_rgba::rgba_calc`.
26struct RgbaCalc {
27    x1: f64,
28    y1: f64,
29    dx: f64,
30    inv_dy: f64,
31    r1: i32,
32    g1: i32,
33    b1: i32,
34    a1: i32,
35    dr: i32,
36    dg: i32,
37    db: i32,
38    da: i32,
39    r: i32,
40    g: i32,
41    b: i32,
42    a: i32,
43    x: i32,
44}
45
46impl RgbaCalc {
47    fn new() -> Self {
48        Self {
49            x1: 0.0,
50            y1: 0.0,
51            dx: 0.0,
52            inv_dy: 0.0,
53            r1: 0,
54            g1: 0,
55            b1: 0,
56            a1: 0,
57            dr: 0,
58            dg: 0,
59            db: 0,
60            da: 0,
61            r: 0,
62            g: 0,
63            b: 0,
64            a: 0,
65            x: 0,
66        }
67    }
68
69    fn init(&mut self, c1: &CoordType<Rgba8>, c2: &CoordType<Rgba8>) {
70        self.x1 = c1.x - 0.5;
71        self.y1 = c1.y - 0.5;
72        self.dx = c2.x - c1.x;
73        let dy = c2.y - c1.y;
74        self.inv_dy = if dy < 1e-5 { 1e5 } else { 1.0 / dy };
75        self.r1 = c1.color.r as i32;
76        self.g1 = c1.color.g as i32;
77        self.b1 = c1.color.b as i32;
78        self.a1 = c1.color.a as i32;
79        self.dr = c2.color.r as i32 - self.r1;
80        self.dg = c2.color.g as i32 - self.g1;
81        self.db = c2.color.b as i32 - self.b1;
82        self.da = c2.color.a as i32 - self.a1;
83    }
84
85    fn calc(&mut self, y: f64) {
86        let k = ((y - self.y1) * self.inv_dy).clamp(0.0, 1.0);
87        self.r = self.r1 + iround(self.dr as f64 * k);
88        self.g = self.g1 + iround(self.dg as f64 * k);
89        self.b = self.b1 + iround(self.db as f64 * k);
90        self.a = self.a1 + iround(self.da as f64 * k);
91        self.x = iround((self.x1 + self.dx * k) * SUBPIXEL_SCALE as f64);
92    }
93}
94
95/// Adjust DDA by a signed step count (equivalent to C++ `r -= start`).
96fn dda_sub(dda: &mut DdaLineInterpolator<14, 0>, n: i32) {
97    if n >= 0 {
98        dda.dec_by(n as u32);
99    } else {
100        dda.inc_by((-n) as u32);
101    }
102}
103
104// ============================================================================
105// SpanGouraudRgba
106// ============================================================================
107
108/// RGBA Gouraud shading span generator.
109///
110/// Composes `SpanGouraud<Rgba8>` for triangle storage and provides the
111/// `SpanGenerator` implementation that interpolates RGBA colors across
112/// scanlines using DDA.
113///
114/// Port of C++ `span_gouraud_rgba<ColorT>`.
115pub struct SpanGouraudRgba {
116    base: SpanGouraud<Rgba8>,
117    swap: bool,
118    y2: i32,
119    rgba1: RgbaCalc,
120    rgba2: RgbaCalc,
121    rgba3: RgbaCalc,
122}
123
124impl SpanGouraudRgba {
125    pub fn new() -> Self {
126        Self {
127            base: SpanGouraud::new(),
128            swap: false,
129            y2: 0,
130            rgba1: RgbaCalc::new(),
131            rgba2: RgbaCalc::new(),
132            rgba3: RgbaCalc::new(),
133        }
134    }
135
136    #[allow(clippy::too_many_arguments)]
137    pub fn new_with_triangle(
138        c1: Rgba8,
139        c2: Rgba8,
140        c3: Rgba8,
141        x1: f64,
142        y1: f64,
143        x2: f64,
144        y2: f64,
145        x3: f64,
146        y3: f64,
147        d: f64,
148    ) -> Self {
149        Self {
150            base: SpanGouraud::new_with_triangle(c1, c2, c3, x1, y1, x2, y2, x3, y3, d),
151            swap: false,
152            y2: 0,
153            rgba1: RgbaCalc::new(),
154            rgba2: RgbaCalc::new(),
155            rgba3: RgbaCalc::new(),
156        }
157    }
158
159    /// Delegate to base: set vertex colors.
160    pub fn colors(&mut self, c1: Rgba8, c2: Rgba8, c3: Rgba8) {
161        self.base.colors(c1, c2, c3);
162    }
163
164    /// Delegate to base: set triangle geometry.
165    #[allow(clippy::too_many_arguments)]
166    pub fn triangle(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, d: f64) {
167        self.base.triangle(x1, y1, x2, y2, x3, y3, d);
168    }
169}
170
171impl Default for SpanGouraudRgba {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl VertexSource for SpanGouraudRgba {
178    fn rewind(&mut self, path_id: u32) {
179        self.base.rewind(path_id);
180    }
181
182    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
183        self.base.vertex(x, y)
184    }
185}
186
187impl SpanGenerator for SpanGouraudRgba {
188    type Color = Rgba8;
189
190    fn prepare(&mut self) {
191        let coord = self.base.arrange_vertices();
192
193        self.y2 = coord[1].y as i32;
194
195        self.swap = cross_product(
196            coord[0].x, coord[0].y, coord[2].x, coord[2].y, coord[1].x, coord[1].y,
197        ) < 0.0;
198
199        self.rgba1.init(&coord[0], &coord[2]);
200        self.rgba2.init(&coord[0], &coord[1]);
201        self.rgba3.init(&coord[1], &coord[2]);
202    }
203
204    fn generate(&mut self, span: &mut [Rgba8], x: i32, y: i32, len: u32) {
205        self.rgba1.calc(y as f64);
206
207        let (pc1_r, pc1_g, pc1_b, pc1_a, pc1_x, pc2_r, pc2_g, pc2_b, pc2_a, pc2_x);
208
209        if y <= self.y2 {
210            // Bottom part of the triangle (first sub-triangle)
211            self.rgba2.calc(y as f64 + self.rgba2.inv_dy);
212            if self.swap {
213                pc1_r = self.rgba2.r;
214                pc1_g = self.rgba2.g;
215                pc1_b = self.rgba2.b;
216                pc1_a = self.rgba2.a;
217                pc1_x = self.rgba2.x;
218                pc2_r = self.rgba1.r;
219                pc2_g = self.rgba1.g;
220                pc2_b = self.rgba1.b;
221                pc2_a = self.rgba1.a;
222                pc2_x = self.rgba1.x;
223            } else {
224                pc1_r = self.rgba1.r;
225                pc1_g = self.rgba1.g;
226                pc1_b = self.rgba1.b;
227                pc1_a = self.rgba1.a;
228                pc1_x = self.rgba1.x;
229                pc2_r = self.rgba2.r;
230                pc2_g = self.rgba2.g;
231                pc2_b = self.rgba2.b;
232                pc2_a = self.rgba2.a;
233                pc2_x = self.rgba2.x;
234            }
235        } else {
236            // Upper part (second sub-triangle)
237            self.rgba3.calc(y as f64 - self.rgba3.inv_dy);
238            if self.swap {
239                pc1_r = self.rgba3.r;
240                pc1_g = self.rgba3.g;
241                pc1_b = self.rgba3.b;
242                pc1_a = self.rgba3.a;
243                pc1_x = self.rgba3.x;
244                pc2_r = self.rgba1.r;
245                pc2_g = self.rgba1.g;
246                pc2_b = self.rgba1.b;
247                pc2_a = self.rgba1.a;
248                pc2_x = self.rgba1.x;
249            } else {
250                pc1_r = self.rgba1.r;
251                pc1_g = self.rgba1.g;
252                pc1_b = self.rgba1.b;
253                pc1_a = self.rgba1.a;
254                pc1_x = self.rgba1.x;
255                pc2_r = self.rgba3.r;
256                pc2_g = self.rgba3.g;
257                pc2_b = self.rgba3.b;
258                pc2_a = self.rgba3.a;
259                pc2_x = self.rgba3.x;
260            }
261        }
262
263        // Horizontal interpolation length with subpixel accuracy
264        let mut nlen = (pc2_x - pc1_x).abs();
265        if nlen <= 0 {
266            nlen = 1;
267        }
268
269        let mut r = DdaLineInterpolator::<14, 0>::new(pc1_r, pc2_r, nlen as u32);
270        let mut g = DdaLineInterpolator::<14, 0>::new(pc1_g, pc2_g, nlen as u32);
271        let mut b = DdaLineInterpolator::<14, 0>::new(pc1_b, pc2_b, nlen as u32);
272        let mut a = DdaLineInterpolator::<14, 0>::new(pc1_a, pc2_a, nlen as u32);
273
274        // Roll back interpolators to span start
275        let mut start = pc1_x - (x << SUBPIXEL_SHIFT);
276        dda_sub(&mut r, start);
277        dda_sub(&mut g, start);
278        dda_sub(&mut b, start);
279        dda_sub(&mut a, start);
280        nlen += start;
281
282        let lim = Rgba8::BASE_MASK as i32;
283        let mut idx = 0usize;
284        let mut remaining = len as i32;
285
286        // Beginning part — check for overflow (typically 1-2 pixels)
287        while remaining > 0 && start > 0 {
288            let vr = r.y().clamp(0, lim);
289            let vg = g.y().clamp(0, lim);
290            let vb = b.y().clamp(0, lim);
291            let va = a.y().clamp(0, lim);
292            span[idx].r = vr as u8;
293            span[idx].g = vg as u8;
294            span[idx].b = vb as u8;
295            span[idx].a = va as u8;
296            r.inc_by(SUBPIXEL_SCALE as u32);
297            g.inc_by(SUBPIXEL_SCALE as u32);
298            b.inc_by(SUBPIXEL_SCALE as u32);
299            a.inc_by(SUBPIXEL_SCALE as u32);
300            nlen -= SUBPIXEL_SCALE;
301            start -= SUBPIXEL_SCALE;
302            idx += 1;
303            remaining -= 1;
304        }
305
306        // Middle part — no overflow checking needed
307        while remaining > 0 && nlen > 0 {
308            span[idx].r = r.y() as u8;
309            span[idx].g = g.y() as u8;
310            span[idx].b = b.y() as u8;
311            span[idx].a = a.y() as u8;
312            r.inc_by(SUBPIXEL_SCALE as u32);
313            g.inc_by(SUBPIXEL_SCALE as u32);
314            b.inc_by(SUBPIXEL_SCALE as u32);
315            a.inc_by(SUBPIXEL_SCALE as u32);
316            nlen -= SUBPIXEL_SCALE;
317            idx += 1;
318            remaining -= 1;
319        }
320
321        // Ending part — check for overflow again
322        while remaining > 0 {
323            let vr = r.y().clamp(0, lim);
324            let vg = g.y().clamp(0, lim);
325            let vb = b.y().clamp(0, lim);
326            let va = a.y().clamp(0, lim);
327            span[idx].r = vr as u8;
328            span[idx].g = vg as u8;
329            span[idx].b = vb as u8;
330            span[idx].a = va as u8;
331            r.inc_by(SUBPIXEL_SCALE as u32);
332            g.inc_by(SUBPIXEL_SCALE as u32);
333            b.inc_by(SUBPIXEL_SCALE as u32);
334            a.inc_by(SUBPIXEL_SCALE as u32);
335            idx += 1;
336            remaining -= 1;
337        }
338    }
339}
340
341// ============================================================================
342// Tests
343// ============================================================================
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_new_default() {
351        let sg = SpanGouraudRgba::new();
352        assert_eq!(sg.y2, 0);
353        assert!(!sg.swap);
354    }
355
356    #[test]
357    fn test_prepare_simple_triangle() {
358        let mut sg = SpanGouraudRgba::new();
359        let red = Rgba8::new(255, 0, 0, 255);
360        let green = Rgba8::new(0, 255, 0, 255);
361        let blue = Rgba8::new(0, 0, 255, 255);
362        sg.colors(red, green, blue);
363        sg.triangle(0.0, 0.0, 100.0, 50.0, 50.0, 100.0, 0.0);
364        sg.prepare();
365        // y2 should be the middle vertex Y
366        assert!(sg.y2 >= 0);
367    }
368
369    #[test]
370    fn test_generate_horizontal_gradient() {
371        // Triangle spanning the x-axis with red on left, green on right
372        let mut sg = SpanGouraudRgba::new();
373        let red = Rgba8::new(255, 0, 0, 255);
374        let green = Rgba8::new(0, 255, 0, 255);
375        let blue_ish = Rgba8::new(128, 128, 0, 255);
376        sg.colors(red, green, blue_ish);
377        sg.triangle(0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 0.0);
378        sg.prepare();
379
380        let mut span = vec![Rgba8::default(); 10];
381        sg.generate(&mut span, 0, 50, 10);
382
383        // Alpha should be non-zero for valid pixels
384        // (Exact values depend on triangle geometry)
385        let has_nonzero = span.iter().any(|c| c.a > 0);
386        assert!(has_nonzero, "Expected some visible pixels");
387    }
388
389    #[test]
390    fn test_generate_single_color() {
391        // All three vertices same color — result should be uniform
392        let c = Rgba8::new(100, 150, 200, 255);
393        let mut sg = SpanGouraudRgba::new();
394        sg.colors(c, c, c);
395        sg.triangle(0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 0.0);
396        sg.prepare();
397
398        let mut span = vec![Rgba8::default(); 5];
399        sg.generate(&mut span, 20, 25, 5);
400
401        // All pixels in the span should have similar color values
402        for pixel in &span {
403            assert!(
404                (pixel.r as i32 - 100).abs() <= 2,
405                "r={} expected ~100",
406                pixel.r
407            );
408            assert!(
409                (pixel.g as i32 - 150).abs() <= 2,
410                "g={} expected ~150",
411                pixel.g
412            );
413            assert!(
414                (pixel.b as i32 - 200).abs() <= 2,
415                "b={} expected ~200",
416                pixel.b
417            );
418        }
419    }
420
421    #[test]
422    fn test_vertex_source_delegation() {
423        let mut sg = SpanGouraudRgba::new();
424        let c = Rgba8::new(128, 128, 128, 255);
425        sg.colors(c, c, c);
426        sg.triangle(10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 0.0);
427
428        sg.rewind(0);
429        let mut x = 0.0;
430        let mut y = 0.0;
431        let cmd = sg.vertex(&mut x, &mut y);
432        assert_eq!(cmd, 1); // PATH_CMD_MOVE_TO
433        assert_eq!(x, 10.0);
434        assert_eq!(y, 20.0);
435    }
436
437    #[test]
438    fn test_new_with_triangle() {
439        let red = Rgba8::new(255, 0, 0, 255);
440        let green = Rgba8::new(0, 255, 0, 255);
441        let blue = Rgba8::new(0, 0, 255, 255);
442        let mut sg = SpanGouraudRgba::new_with_triangle(
443            red, green, blue, 0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 0.0,
444        );
445        sg.prepare();
446        // Should not panic
447        let mut span = vec![Rgba8::default(); 3];
448        sg.generate(&mut span, 40, 50, 3);
449    }
450
451    #[test]
452    fn test_rgba_calc_init_and_calc() {
453        let c1 = CoordType {
454            x: 0.0,
455            y: 0.0,
456            color: Rgba8::new(0, 0, 0, 255),
457        };
458        let c2 = CoordType {
459            x: 100.0,
460            y: 100.0,
461            color: Rgba8::new(255, 255, 255, 255),
462        };
463
464        let mut calc = RgbaCalc::new();
465        calc.init(&c1, &c2);
466
467        // At y=0 (start), should be near c1's color
468        // (y1 is stored as c1.y - 0.5, so k is slightly > 0)
469        calc.calc(0.0);
470        assert!(calc.r <= 2, "r={}", calc.r);
471        assert!(calc.g <= 2, "g={}", calc.g);
472
473        // At y=100 (end), should be near c2's color
474        calc.calc(100.0);
475        assert!(calc.r >= 253, "r={}", calc.r);
476        assert!(calc.g >= 253, "g={}", calc.g);
477
478        // At y=50 (middle), should be halfway
479        calc.calc(50.0);
480        assert!(calc.r > 100 && calc.r < 160, "r={}", calc.r);
481    }
482
483    #[test]
484    fn test_rgba_calc_zero_height() {
485        // Degenerate case: zero height triangle edge
486        let c1 = CoordType {
487            x: 0.0,
488            y: 50.0,
489            color: Rgba8::new(100, 100, 100, 255),
490        };
491        let c2 = CoordType {
492            x: 100.0,
493            y: 50.0,
494            color: Rgba8::new(200, 200, 200, 255),
495        };
496
497        let mut calc = RgbaCalc::new();
498        calc.init(&c1, &c2);
499        // Should not panic, inv_dy should be large
500        calc.calc(50.0);
501    }
502
503    #[test]
504    fn test_dda_sub_positive() {
505        let mut dda = DdaLineInterpolator::<14, 0>::new(0, 255, 100);
506        let y_before = dda.y();
507        dda_sub(&mut dda, 10);
508        // After subtracting 10 steps, y should decrease
509        assert!(dda.y() <= y_before);
510    }
511
512    #[test]
513    fn test_dda_sub_negative() {
514        let mut dda = DdaLineInterpolator::<14, 0>::new(0, 255, 100);
515        let y_before = dda.y();
516        dda_sub(&mut dda, -10);
517        // After subtracting negative steps (= adding), y should increase
518        assert!(dda.y() >= y_before);
519    }
520}