use crate::base::Rgba;
use crate::gfx::bitmap::Bitmap;
pub struct Framebuffer {
color: Bitmap,
depth: Vec<f32>,
w: u32,
h: u32,
}
impl Framebuffer {
pub fn new(w: u32, h: u32) -> Framebuffer {
Framebuffer {
color: Bitmap::new(w, h, Rgba::TRANSPARENT),
depth: vec![f32::INFINITY; (w as usize) * (h as usize)],
w,
h,
}
}
pub fn width(&self) -> u32 {
self.w
}
pub fn height(&self) -> u32 {
self.h
}
pub fn clear(&mut self, background: Rgba) {
self.color.fill(background);
self.depth.fill(f32::INFINITY);
}
pub fn bitmap(&self) -> &Bitmap {
&self.color
}
pub fn depth_at(&self, x: u32, y: u32) -> Option<f32> {
if x < self.w && y < self.h {
Some(self.depth[(y * self.w + x) as usize])
} else {
None
}
}
pub fn depths(&self) -> &[f32] {
&self.depth
}
pub fn bitmap_mut(&mut self) -> &mut Bitmap {
&mut self.color
}
pub fn depth_fog(&mut self, ground: Rgba, max_mix: f32) {
let max_mix = max_mix.clamp(0.0, 1.0);
if max_mix == 0.0 {
return;
}
for i in 0..self.depth.len() {
let d = self.depth[i];
if !d.is_finite() {
continue;
}
let k = ((d + 1.0) * 0.5).clamp(0.0, 1.0) * max_mix;
if k > 0.0 {
let p = self.color.pixels()[i];
self.color.pixels_mut()[i] = p.lerp(ground, k);
}
}
}
pub fn coverage(&self) -> f32 {
if self.depth.is_empty() {
return 0.0;
}
let covered = self.depth.iter().filter(|d| d.is_finite()).count();
covered as f32 / self.depth.len() as f32
}
}
#[derive(Copy, Clone, Debug)]
pub struct RasterVertex {
pub x: f32,
pub y: f32,
pub ndc_z: f32,
pub rgb: [f32; 3],
pub uw: f32,
pub vw: f32,
pub inv_w: f32,
}
impl RasterVertex {
pub fn flat(x: f32, y: f32, ndc_z: f32, rgb: [f32; 3]) -> RasterVertex {
RasterVertex {
x,
y,
ndc_z,
rgb,
uw: 0.0,
vw: 0.0,
inv_w: 1.0,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct ClipVertex {
pub pos: [f32; 3],
pub rgb: [f32; 3],
pub uv: [f32; 2],
}
pub fn clip_near(tri: &[ClipVertex; 3], near: f32, out: &mut [ClipVertex; 4]) -> usize {
let inside = |v: &ClipVertex| v.pos[2] <= -near;
let mut n = 0usize;
for i in 0..3 {
let cur = &tri[i];
let next = &tri[(i + 1) % 3];
let (ci, ni) = (inside(cur), inside(next));
if ci {
out[n] = *cur;
n += 1;
}
if ci != ni {
let dz = next.pos[2] - cur.pos[2];
let t = (-near - cur.pos[2]) / dz;
let lerp3 = |a: [f32; 3], b: [f32; 3]| {
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]
};
let lerp2 =
|a: [f32; 2], b: [f32; 2]| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
out[n] = ClipVertex {
pos: lerp3(cur.pos, next.pos),
rgb: lerp3(cur.rgb, next.rgb),
uv: lerp2(cur.uv, next.uv),
};
n += 1;
}
debug_assert!(n <= 4);
}
n
}
pub fn clip_screen_rect(
poly: &[RasterVertex],
fb_w: f32,
fb_h: f32,
band: f32,
out: &mut [RasterVertex; 12],
) -> usize {
let mut a = [poly[0]; 12];
let mut b = [poly[0]; 12];
let mut n = poly.len().min(12);
a[..n].copy_from_slice(&poly[..n]);
let planes: [(bool, bool, f32); 4] = [
(true, true, -band), (true, false, fb_w + band), (false, true, -band), (false, false, fb_h + band),
];
let (mut src, mut dst) = (&mut a, &mut b);
for (is_x, keep_ge, bound) in planes {
let coord = |v: &RasterVertex| if is_x { v.x } else { v.y };
let inside = |v: &RasterVertex| {
if keep_ge {
coord(v) >= bound
} else {
coord(v) <= bound
}
};
let mut m = 0usize;
for i in 0..n {
let cur = src[i];
let nxt = src[(i + 1) % n];
let (ci, ni) = (inside(&cur), inside(&nxt));
if ci {
dst[m] = cur;
m += 1;
}
if ci != ni {
let denom = coord(&nxt) - coord(&cur);
let t = (bound - coord(&cur)) / denom;
let l = |a: f32, b: f32| a + (b - a) * t;
dst[m] = RasterVertex {
x: l(cur.x, nxt.x),
y: l(cur.y, nxt.y),
ndc_z: l(cur.ndc_z, nxt.ndc_z),
rgb: [
l(cur.rgb[0], nxt.rgb[0]),
l(cur.rgb[1], nxt.rgb[1]),
l(cur.rgb[2], nxt.rgb[2]),
],
uw: l(cur.uw, nxt.uw),
vw: l(cur.vw, nxt.vw),
inv_w: l(cur.inv_w, nxt.inv_w),
};
m += 1;
}
debug_assert!(m <= 12);
}
n = m;
if n == 0 {
return 0;
}
std::mem::swap(&mut src, &mut dst);
}
out[..n].copy_from_slice(&src[..n]);
n
}
const SUB_BITS: i64 = 4;
const SUB: f32 = (1 << SUB_BITS) as f32;
const HALF: i64 = 1 << (SUB_BITS - 1);
const COORD_CLAMP: i64 = 1 << 29;
#[inline]
fn orient2d(ax: i64, ay: i64, bx: i64, by: i64, cx: i64, cy: i64) -> i64 {
(bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
}
#[inline]
fn is_top_left(ax: i64, ay: i64, bx: i64, by: i64) -> bool {
let dy = by - ay;
let dx = bx - ax;
dy < 0 || (dy == 0 && dx > 0)
}
pub fn fill_triangle(
fb: &mut Framebuffer,
v: &[RasterVertex; 3],
texture: Option<&crate::three::texture::TextureSampler<'_>>,
) {
for p in v {
if !(p.x.is_finite() && p.y.is_finite() && p.ndc_z.is_finite()) {
return;
}
}
let snap = |c: f32| ((c * SUB).round() as i64).clamp(-COORD_CLAMP, COORD_CLAMP);
let xs: [i64; 3] = [snap(v[0].x), snap(v[1].x), snap(v[2].x)];
let ys: [i64; 3] = [snap(v[0].y), snap(v[1].y), snap(v[2].y)];
let area = orient2d(xs[0], ys[0], xs[1], ys[1], xs[2], ys[2]);
if area <= 0 {
return; }
let min_x = ((xs.iter().min().unwrap() - HALF) >> SUB_BITS).max(0);
let max_x = ((xs.iter().max().unwrap() + HALF) >> SUB_BITS).min(fb.w as i64 - 1);
let min_y = ((ys.iter().min().unwrap() - HALF) >> SUB_BITS).max(0);
let max_y = ((ys.iter().max().unwrap() + HALF) >> SUB_BITS).min(fb.h as i64 - 1);
if min_x > max_x || min_y > max_y {
return;
}
let edge = |j: usize, k: usize, px: i64, py: i64| orient2d(xs[j], ys[j], xs[k], ys[k], px, py);
let step_x = [
(ys[1] - ys[2]) << SUB_BITS,
(ys[2] - ys[0]) << SUB_BITS,
(ys[0] - ys[1]) << SUB_BITS,
];
let step_y = [
(xs[2] - xs[1]) << SUB_BITS,
(xs[0] - xs[2]) << SUB_BITS,
(xs[1] - xs[0]) << SUB_BITS,
];
let bias = [
if is_top_left(xs[1], ys[1], xs[2], ys[2]) {
0
} else {
-1
},
if is_top_left(xs[2], ys[2], xs[0], ys[0]) {
0
} else {
-1
},
if is_top_left(xs[0], ys[0], xs[1], ys[1]) {
0
} else {
-1
},
];
let px0 = (min_x << SUB_BITS) + HALF;
let py0 = (min_y << SUB_BITS) + HALF;
let mut row_w = [
edge(1, 2, px0, py0) + bias[0],
edge(2, 0, px0, py0) + bias[1],
edge(0, 1, px0, py0) + bias[2],
];
let inv_area = 1.0 / area as f32;
let z = [v[0].ndc_z, v[1].ndc_z, v[2].ndc_z];
let c = [v[0].rgb, v[1].rgb, v[2].rgb];
for y in min_y..=max_y {
let mut w = row_w;
let row_base = (y as usize) * (fb.w as usize);
for x in min_x..=max_x {
if w[0] >= 0 && w[1] >= 0 && w[2] >= 0 {
let l0 = (w[0] - bias[0]) as f32 * inv_area;
let l1 = (w[1] - bias[1]) as f32 * inv_area;
let l2 = (w[2] - bias[2]) as f32 * inv_area;
let depth = l0 * z[0] + l1 * z[1] + l2 * z[2];
let idx = row_base + x as usize;
if depth < fb.depth[idx] && (-1.0..=1.0).contains(&depth) {
fb.depth[idx] = depth;
let lin = |k: usize| l0 * c[0][k] + l1 * c[1][k] + l2 * c[2][k];
let mut rgb = [lin(0), lin(1), lin(2)];
if let Some(tex) = texture {
let iw = l0 * v[0].inv_w + l1 * v[1].inv_w + l2 * v[2].inv_w;
if iw > 1e-12 {
let u = (l0 * v[0].uw + l1 * v[1].uw + l2 * v[2].uw) / iw;
let tv = (l0 * v[0].vw + l1 * v[1].vw + l2 * v[2].vw) / iw;
let t = tex.sample(u, tv);
rgb = [rgb[0] * t[0], rgb[1] * t[1], rgb[2] * t[2]];
}
}
let to_srgb = |l: f32| (l.clamp(0.0, 1.0).sqrt() * 255.0 + 0.5) as u8;
fb.color.pixels_mut()[idx] =
Rgba::rgb(to_srgb(rgb[0]), to_srgb(rgb[1]), to_srgb(rgb[2]));
}
}
w[0] += step_x[0];
w[1] += step_x[1];
w[2] += step_x[2];
}
row_w[0] += step_y[0];
row_w[1] += step_y[1];
row_w[2] += step_y[2];
}
}
#[cfg(test)]
mod tests {
use super::*;
const WHITE_LIN: [f32; 3] = [1.0, 1.0, 1.0];
fn vtx(x: f32, y: f32, z: f32, rgb: [f32; 3]) -> RasterVertex {
RasterVertex::flat(x, y, z, rgb)
}
#[test]
fn fills_a_simple_triangle() {
let mut fb = Framebuffer::new(16, 16);
fb.clear(Rgba::TRANSPARENT);
fill_triangle(
&mut fb,
&[
vtx(1.0, 1.0, 0.0, WHITE_LIN),
vtx(14.0, 1.0, 0.0, WHITE_LIN),
vtx(1.0, 14.0, 0.0, WHITE_LIN),
],
None,
);
assert!(fb.coverage() > 0.2, "coverage {}", fb.coverage());
assert!(fb.depth_at(4, 4).unwrap().is_finite());
assert!(
fb.depth_at(15, 15).unwrap().is_infinite(),
"outside stays empty"
);
let mut fb2 = Framebuffer::new(16, 16);
fill_triangle(
&mut fb2,
&[
vtx(1.0, 1.0, 0.0, WHITE_LIN),
vtx(1.0, 14.0, 0.0, WHITE_LIN),
vtx(14.0, 1.0, 0.0, WHITE_LIN),
],
None,
);
assert_eq!(fb2.coverage(), 0.0);
}
#[test]
fn shared_edge_paints_each_pixel_exactly_once() {
let quad = |fb: &mut Framebuffer, z: f32, rgb: [f32; 3]| {
fill_triangle(
fb,
&[
vtx(2.0, 2.0, z, rgb),
vtx(12.0, 2.0, z, rgb),
vtx(2.0, 12.0, z, rgb),
],
None,
);
fill_triangle(
fb,
&[
vtx(12.0, 2.0, z, rgb),
vtx(12.0, 12.0, z, rgb),
vtx(2.0, 12.0, z, rgb),
],
None,
);
};
let mut fb = Framebuffer::new(16, 16);
fb.clear(Rgba::TRANSPARENT);
quad(&mut fb, 0.5, [1.0, 0.0, 0.0]);
let covered_far: Vec<usize> = (0..256).filter(|&i| fb.depth[i].is_finite()).collect();
assert_eq!(covered_far.len(), 100, "quad interior coverage");
quad(&mut fb, -0.5, [0.0, 1.0, 0.0]);
for &i in &covered_far {
assert!(
(fb.depth[i] + 0.5).abs() < 1e-4,
"pixel {i} not repainted by nearer quad: {}",
fb.depth[i]
);
assert_eq!(fb.color.pixels()[i].g, 255, "pixel {i} color stale");
}
let covered_near = (0..256).filter(|&i| fb.depth[i].is_finite()).count();
assert_eq!(covered_near, 100, "no seam gaps or double-cover growth");
}
#[test]
fn z_test_orders_triangles() {
let mut fb = Framebuffer::new(8, 8);
fb.clear(Rgba::TRANSPARENT);
fill_triangle(
&mut fb,
&[
vtx(0.0, 0.0, 0.9, [1.0, 0.0, 0.0]),
vtx(16.0, 0.0, 0.9, [1.0, 0.0, 0.0]),
vtx(0.0, 16.0, 0.9, [1.0, 0.0, 0.0]),
],
None,
);
fill_triangle(
&mut fb,
&[
vtx(0.0, 0.0, -0.5, [0.0, 1.0, 0.0]),
vtx(4.0, 0.0, -0.5, [0.0, 1.0, 0.0]),
vtx(0.0, 16.0, -0.5, [0.0, 1.0, 0.0]),
],
None,
);
fill_triangle(
&mut fb,
&[
vtx(0.0, 0.0, 0.99, [0.0, 0.0, 1.0]),
vtx(16.0, 0.0, 0.99, [0.0, 0.0, 1.0]),
vtx(0.0, 16.0, 0.99, [0.0, 0.0, 1.0]),
],
None,
);
let left = fb.bitmap().get(1, 2).unwrap();
assert_eq!((left.r, left.g), (0, 255), "near triangle wins: {left:?}");
let right = fb.bitmap().get(6, 1).unwrap();
assert_eq!(
(right.r, right.g, right.b),
(255, 0, 0),
"far behind loses: {right:?}"
);
}
#[test]
fn ndc_depth_outside_unit_range_is_rejected() {
let mut fb = Framebuffer::new(8, 8);
fb.clear(Rgba::TRANSPARENT);
fill_triangle(
&mut fb,
&[
vtx(0.0, 0.0, 1.5, WHITE_LIN),
vtx(16.0, 0.0, 1.5, WHITE_LIN),
vtx(0.0, 16.0, 1.5, WHITE_LIN),
],
None,
);
assert_eq!(fb.coverage(), 0.0, "beyond-far geometry must not paint");
}
#[test]
fn nan_vertices_are_skipped() {
let mut fb = Framebuffer::new(8, 8);
fb.clear(Rgba::TRANSPARENT);
fill_triangle(
&mut fb,
&[
vtx(f32::NAN, 0.0, 0.0, WHITE_LIN),
vtx(8.0, 0.0, 0.0, WHITE_LIN),
vtx(0.0, 8.0, 0.0, WHITE_LIN),
],
None,
);
assert_eq!(fb.coverage(), 0.0);
}
#[test]
fn color_interpolates_across_the_face() {
let mut fb = Framebuffer::new(17, 17);
fb.clear(Rgba::TRANSPARENT);
fill_triangle(
&mut fb,
&[
vtx(0.0, 0.0, 0.0, [0.0, 0.0, 0.0]),
vtx(17.0, 0.0, 0.0, [1.0, 0.0, 0.0]),
vtx(0.0, 17.0, 0.0, [0.0, 0.0, 0.0]),
],
None,
);
let mid = fb.bitmap().get(8, 1).unwrap();
assert!(
mid.r > 130 && mid.r < 210,
"sqrt(≈0.5)*255 ≈ 180, got {}",
mid.r
);
let near_left = fb.bitmap().get(1, 1).unwrap();
assert!(near_left.r < 100, "{near_left:?}");
}
#[test]
fn textured_fill_is_perspective_correct() {
use crate::three::texture::{TextureSampler, Wrap};
let tex_bmp =
crate::gfx::Bitmap::from_fn(4, 1, |x, _| if x < 2 { Rgba::WHITE } else { Rgba::BLACK });
let tex = TextureSampler::new(&tex_bmp, Wrap::Clamp, Wrap::Clamp).unwrap();
let mut fb = Framebuffer::new(16, 16);
fb.clear(Rgba::TRANSPARENT);
let w = [1.0f32, 0.25, 0.25]; let mk = |x: f32, y: f32, u: f32, iw: f32| RasterVertex {
x,
y,
ndc_z: 0.0,
rgb: [1.0, 1.0, 1.0],
uw: u * iw,
vw: 0.5 * iw,
inv_w: iw,
};
fill_triangle(
&mut fb,
&[
mk(0.0, 0.0, 0.0, w[0]),
mk(16.0, 0.0, 1.0, w[1]),
mk(0.0, 16.0, 0.0, w[2]),
],
Some(&tex),
);
let mid = fb.bitmap().get(8, 1).unwrap();
assert!(mid.r > 200, "perspective correction missing: {mid:?}");
let far = fb.bitmap().get(14, 1).unwrap();
assert!(far.r < 80, "far end should sample the black half: {far:?}");
}
#[test]
fn clip_near_cases() {
let v = |z: f32| ClipVertex {
pos: [0.0, 0.0, z],
rgb: [1.0, 1.0, 1.0],
uv: [0.0, 0.0],
};
let mut out = [v(0.0); 4];
let n = clip_near(&[v(-2.0), v(-3.0), v(-4.0)], 1.0, &mut out);
assert_eq!(n, 3);
let n = clip_near(&[v(-0.1), v(-0.5), v(0.2)], 1.0, &mut out);
assert_eq!(n, 0);
let tri = [
ClipVertex {
pos: [0.0, 0.0, -0.5],
rgb: [1.0, 0.0, 0.0],
uv: [0.0, 0.0],
},
ClipVertex {
pos: [1.0, 0.0, -2.0],
rgb: [0.0, 1.0, 0.0],
uv: [1.0, 0.0],
},
ClipVertex {
pos: [-1.0, 0.0, -2.0],
rgb: [0.0, 0.0, 1.0],
uv: [0.0, 1.0],
},
];
let n = clip_near(&tri, 1.0, &mut out);
assert_eq!(n, 4);
for cv in &out[..n] {
assert!(cv.pos[2] <= -1.0 + 1e-6, "{:?}", cv.pos);
}
let tri = [
ClipVertex {
pos: [0.0, 0.0, -2.0],
rgb: [1.0, 0.0, 0.0],
uv: [0.0, 0.0],
},
ClipVertex {
pos: [1.0, 0.0, -0.2],
rgb: [0.0, 1.0, 0.0],
uv: [1.0, 0.0],
},
ClipVertex {
pos: [-1.0, 0.0, -0.2],
rgb: [0.0, 0.0, 1.0],
uv: [0.0, 1.0],
},
];
let n = clip_near(&tri, 1.0, &mut out);
assert_eq!(n, 3);
assert!(out[..n].iter().any(|cv| cv.rgb[0] > 0.0 && cv.rgb[1] > 0.0));
}
}