use alloc::vec::Vec;
use core::{iter::zip, mem::swap};
use view_frustum::{outcode, status};
use crate::geom::{Edge, Tri, Vertex, vertex};
use crate::math::{Lerp, vec::ProjVec3};
pub trait Clip {
type Item;
fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>);
}
pub type ClipVec = ProjVec3;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ClipVert<A> {
pub pos: ClipVec,
pub attrib: A,
outcode: u8,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Status {
Visible,
Clipped,
Hidden,
}
#[derive(Debug, Copy, Clone)]
pub struct ClipPlane(ClipVec, u8);
impl ClipPlane {
const fn new(x: f32, y: f32, z: f32, off: f32, bit: u8) -> Self {
Self(ClipVec::new([x, y, z, -off]), bit)
}
#[inline]
pub fn signed_dist(&self, pt: &ClipVec) -> f32 {
self.0.dot(pt)
}
#[inline]
pub fn outcode(&self, pt: &ClipVec) -> u8 {
(self.signed_dist(pt) > 0.0) as u8 * self.1
}
#[inline]
pub fn is_inside<A>(&self, v: &ClipVert<A>) -> bool {
self.1 & v.outcode == 0
}
pub fn intersect<A: Lerp>(
&self,
[v0, v1]: [&ClipVert<A>; 2],
) -> Option<ClipVert<A>> {
let d0 = self.signed_dist(&v0.pos);
let d1 = self.signed_dist(&v1.pos);
(d0 * d1 < 0.0).then(|| {
let t = -d0 / (d1 - d0);
ClipVert::new(vertex(
v0.pos.lerp(&v1.pos, t),
v0.attrib.lerp(&v1.attrib, t),
))
})
}
pub fn clip_simple_polygon<A: Lerp + Clone>(
&self,
verts_in: &[ClipVert<A>],
verts_out: &mut Vec<ClipVert<A>>,
) {
let mut verts = verts_in.iter().chain(&verts_in[..1]);
let Some(mut v0) = verts.next() else {
return;
};
for v1 in verts {
if self.is_inside(v0) {
verts_out.push((*v0).clone());
} else {
}
if let Some(v) = self.intersect([v0, v1]) {
verts_out.push(v);
}
v0 = v1;
}
}
}
pub mod view_frustum {
use super::*;
#[rustfmt::skip]
pub const PLANES: [ClipPlane; 6] = [
ClipPlane::new( 0.0, 0.0, -1.0, 1.0, 0x01), ClipPlane::new( 0.0, 0.0, 1.0, 1.0, 0x02), ClipPlane::new(-1.0, 0.0, 0.0, 1.0, 0x04), ClipPlane::new( 1.0, 0.0, 0.0, 1.0, 0x08), ClipPlane::new( 0.0, -1.0, 0.0, 1.0, 0x10), ClipPlane::new( 0.0, 1.0, 0.0, 1.0, 0x20), ];
pub fn clip<G: Clip + ?Sized>(geom: &G, out: &mut Vec<G::Item>) {
geom.clip(&PLANES, out);
}
pub fn outcode(pt: &ClipVec) -> u8 {
PLANES.iter().map(|p| p.outcode(pt)).sum()
}
pub fn status<V>(vs: &[ClipVert<V>]) -> Status {
let all_outside = vs.iter().fold(!0, |code, v| code & v.outcode);
let any_outside = vs.iter().fold(0, |code, v| code | v.outcode);
if all_outside != 0 {
Status::Hidden
} else if any_outside == 0 {
Status::Visible
} else {
Status::Clipped
}
}
}
pub fn clip_simple_polygon<'a, A: Lerp + Clone>(
planes: &[ClipPlane],
verts_in: &'a mut Vec<ClipVert<A>>,
verts_out: &'a mut Vec<ClipVert<A>>,
) {
debug_assert!(verts_out.is_empty());
for (p, i) in zip(planes, 0..) {
p.clip_simple_polygon(verts_in, verts_out);
verts_in.clear();
if verts_out.is_empty() {
break;
} else if i < planes.len() - 1 {
swap(verts_in, verts_out);
}
}
}
impl<V> ClipVert<V> {
pub fn new(Vertex { pos, attrib }: Vertex<ClipVec, V>) -> Self {
let outcode = outcode(&pos);
Self { pos, attrib, outcode }
}
}
impl<A: Lerp + Clone> Clip for [Edge<ClipVert<A>>] {
type Item = Edge<ClipVert<A>>;
fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>) {
'lines: for Edge(a, b) in self {
let both_outside = a.outcode & b.outcode != 0;
let neither_outside = a.outcode | b.outcode == 0;
let mut a = a.clone();
let mut b = b.clone();
if both_outside {
continue;
}
if neither_outside {
out.push(Edge(a, b));
continue;
}
for p in planes {
let a_in = p.is_inside(&a);
let b_in = p.is_inside(&b);
if !a_in && !b_in {
continue 'lines;
}
if let Some(v) = p.intersect([&a, &b]) {
if a_in {
b = v;
} else if b_in {
a = v;
}
}
}
out.push(Edge(a, b));
}
}
}
impl<A: Lerp + Clone> Clip for [Tri<ClipVert<A>>] {
type Item = Tri<ClipVert<A>>;
fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>) {
debug_assert!(out.is_empty());
let mut verts_in = Vec::with_capacity(10);
let mut verts_out = Vec::with_capacity(10);
for tri @ Tri(vs) in self {
match status(vs) {
Status::Visible => {
out.push(tri.clone());
continue;
}
Status::Hidden => continue,
Status::Clipped => { }
}
verts_in.extend(vs.clone());
clip_simple_polygon(planes, &mut verts_in, &mut verts_out);
if let [p, rest @ ..] = &verts_out[..] {
out.extend(
rest.windows(2)
.map(|e| Tri([p.clone(), e[0].clone(), e[1].clone()])),
);
}
verts_out.clear();
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use crate::{geom::vertex, math::Vary};
use super::{view_frustum::*, *};
const FAR_PLANE: ClipPlane = PLANES[1];
fn vec(x: f32, y: f32, z: f32) -> ClipVec {
[x, y, z, 1.0].into()
}
fn vtx(pos: ClipVec) -> ClipVert<f32> {
ClipVert::new(vertex(pos, 0.0))
}
fn tri(a: ClipVec, b: ClipVec, c: ClipVec) -> Tri<ClipVert<f32>> {
Tri([a, b, c].map(vtx))
}
#[test]
fn signed_distance() {
assert_eq!(FAR_PLANE.signed_dist(&vec(0.0, 0.0, -1.0)), -2.0);
assert_eq!(FAR_PLANE.signed_dist(&vec(1.0, 0.0, 0.0)), -1.0);
assert_eq!(FAR_PLANE.signed_dist(&vec(0.0, 2.0, 1.0)), 0.0);
assert_eq!(FAR_PLANE.signed_dist(&vec(-3.0, 0.0, 2.0)), 1.0);
}
#[test]
fn outcode_inside() {
assert_eq!(outcode(&vec(0.0, 0.0, 0.0)), 0);
assert_eq!(outcode(&vec(1.0, 0.0, 0.0)), 0);
assert_eq!(outcode(&vec(0.0, -1.0, 0.0)), 0);
assert_eq!(outcode(&vec(0.0, 1.0, 1.0)), 0);
}
#[test]
fn outcode_outside() {
assert_eq!(outcode(&vec(0.0, 0.0, -1.5)), 0b00_0_01);
assert_eq!(outcode(&vec(2.0, 0.0, 0.0)), 0b00_10_00);
assert_eq!(outcode(&vec(0.0, -1.01, 0.0)), 0b01_00_00);
assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)), 0b00_01_10);
}
#[test]
fn edge_clip_inside() {
let e = [vec(2.0, 0.0, -1.0), vec(-1.0, 1.0, 1.0)].map(vtx);
let mut res = vec![];
FAR_PLANE.clip_simple_polygon(&e, &mut res);
assert_eq!(res, e);
}
#[test]
fn edge_clip_outside() {
let e = [vec(2.0, 0.0, 1.5), vec(-1.0, 1.0, 2.0)].map(vtx);
let mut res = vec![];
FAR_PLANE.clip_simple_polygon(&e, &mut res);
assert_eq!(res, []);
}
#[test]
fn edge_clip_in_out() {
let e = [vec(2.0, 0.0, 0.0), vec(-1.0, 1.0, 2.0)].map(vtx);
let mut res = vec![];
FAR_PLANE.clip_simple_polygon(&e, &mut res);
assert_eq!(res[..2], [e[0], vtx(vec(0.5, 0.5, 1.0))]);
}
#[test]
fn edge_clip_out_in() {
let e = [vec(2.0, 0.0, 4.0), vec(-1.0, 1.0, 0.0)].map(vtx);
let mut res = vec![];
FAR_PLANE.clip_simple_polygon(&e, &mut res);
assert_eq!(res[..2], [vtx(vec(-0.25, 0.75, 1.0)), e[1]]);
}
#[test]
fn tri_clip_fully_inside() {
let tri =
tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 0.5), vec(-1.0, 1.5, 0.0));
let res = &mut vec![];
[tri].clip(&[FAR_PLANE], res);
assert_eq!(res, &[tri]);
}
#[test]
fn tri_clip_fully_outside() {
let tri =
tri(vec(0.0, -1.0, 1.5), vec(2.0, 0.0, 1.5), vec(-1.0, 1.5, 2.0));
let res = &mut vec![];
[tri].clip(&[FAR_PLANE], res);
assert_eq!(res, &[]);
}
#[test]
fn tri_clip_inside_on_on() {
let tri =
tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 1.0), vec(-1.0, 1.5, 1.0));
let res = &mut vec![];
[tri].clip(&[FAR_PLANE], res);
assert_eq!(res, &[tri]);
}
#[test]
fn tri_clip_outside_inside_inside() {
let out = vec(0.0, 0.0, 2.0);
let in1 = vec(0.0, 1.0, 0.0);
let in2 = vec(1.0, 0.0, 0.0);
let tr = tri(out, in1, in2);
let res = &mut vec![];
[tr].clip(&[FAR_PLANE], res);
assert_eq!(
res,
&[
tri(vec(0.0, 0.5, 1.0), in1, in2),
tri(vec(0.0, 0.5, 1.0), in2, vec(0.5, 0.0, 1.0))
]
);
}
#[test]
fn tri_clip_outside_on_inside() {
let out = vec(0.0, 0.0, 2.0);
let on = vec(1.0, 0.0, 1.0);
let ins = vec(0.0, -1.0, 0.0);
let tr = tri(out, on, ins);
let res = &mut vec![];
[tr].clip(&[FAR_PLANE], res);
assert_eq!(res, &[tri(on, ins, vec(0.0, -0.5, 1.0))]);
}
#[test]
fn tri_clip_outside_on_on() {
let out = vec(0.0, 0.0, 2.0);
let on1 = vec(1.0, 0.0, 1.0);
let on2 = vec(0.0, -1.0, 1.0);
let tr = tri(out, on1, on2);
let res = &mut vec![];
[tr].clip(&[FAR_PLANE], res);
assert_eq!(res, &[]);
}
#[test]
fn tri_clip_against_frustum_fully_inside() {
let tr = tri(
vec(-1.0, -1.0, -1.0),
vec(1.0, 1.0, 0.0),
vec(0.0, 1.0, 1.0),
);
let res = &mut vec![];
[tr].clip(&PLANES, res);
assert_eq!(res, &[tr]);
}
#[test]
fn tri_clip_against_frustum_fully_outside() {
let tr =
tri(vec(2.0, 2.0, 2.0), vec(2.0, -2.0, 0.0), vec(0.0, -1.0, 2.0));
let res = &mut vec![];
[tr].clip(&PLANES, res);
assert_eq!(res, &[]);
}
#[test]
fn tri_clip_against_frustum_result_is_quad() {
let tr =
tri(vec(0.0, 0.0, 0.0), vec(2.0, 0.0, 0.0), vec(0.0, 0.0, 2.0));
let res = &mut vec![];
[tr].clip(&PLANES, res);
assert_eq!(
res,
&[
tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 0.0), vec(1.0, 0.0, 1.0)),
tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 1.0), vec(0.0, 0.0, 1.0))
]
);
}
#[test]
fn tri_clip_against_frustum_result_is_heptagon() {
let tr =
tri(vec(-1.5, 0.0, 0.0), vec(0.0, 0.0, -1.5), vec(2.0, 0.0, 2.0));
let res = &mut vec![];
[tr].clip(&PLANES, res);
assert_eq!(res.len(), 5);
assert!(res.iter().all(in_bounds));
}
#[test]
#[allow(unused)]
fn tri_clip_against_frustum_all_cases() {
let xs = || (-2.0).vary(1.0, Some(5));
let pts: Vec<_> = xs()
.flat_map(move |x| {
xs().flat_map(move |y| xs().map(move |z| vec(x, y, z)))
})
.collect();
let tris = pts.iter().flat_map(|a| {
pts.iter()
.flat_map(|b| pts.iter().map(|c| tri(*a, *b, *c)))
});
let mut in_tris = 0;
let mut in_degen = 0;
let mut out_tris = [0; 8];
let mut out_degen = 0;
let mut out_total = 0;
for tr in tris {
let res = &mut vec![];
[tr].clip(&PLANES, res);
assert!(
res.iter().all(in_bounds),
"clip returned oob vertex:\n\
input: {:#?}\n\
output: {:#?}",
tr,
&res
);
in_tris += 1;
in_degen += is_degenerate(&tr) as u32;
out_tris[res.len()] += 1;
out_total += res.len();
out_degen += res.iter().filter(|t| is_degenerate(t)).count()
}
#[cfg(feature = "std")]
{
use std::dbg;
dbg!(in_tris);
dbg!(in_degen);
dbg!(out_degen);
dbg!(out_total);
}
assert_eq!(in_tris, 5i32.pow(9));
assert_eq!(
out_tris,
[559754, 536199, 537942, 254406, 58368, 6264, 192, 0]
);
}
fn is_degenerate(Tri([a, b, c]): &Tri<ClipVert<f32>>) -> bool {
a.pos == b.pos || a.pos == c.pos || b.pos == c.pos
}
fn in_bounds(Tri(vs): &Tri<ClipVert<f32>>) -> bool {
vs.iter()
.flat_map(|v| (v.pos / v.pos.w()).0)
.all(|a| a.abs() <= 1.00001)
}
}