//! Fixed-point convex geometry used by fighter hurtboxes. use crate::{ fighter::baked_animation::FixedMat4, math::{ int::{FGWide, FGi32}, vec3::FGVec3, }, }; /// A capsule after an arbitrary affine transform. Non-uniform scale turns the /// spherical caps and circular cylinder into ellipsoidal geometry. #[derive(Clone, Copy, Debug, PartialEq, Hash)] pub struct FixedAffineCapsule { pub transform: FixedMat4, pub half_length: FGi32, pub radius: FGi32, } /// An ordinary world-space capsule, used for swept attack volumes. #[derive(Clone, Copy, Debug, PartialEq, Hash)] pub struct FixedCapsule { pub start: FGVec3, pub end: FGVec3, pub radius: FGi32, } impl FixedCapsule { pub fn new(start: FGVec3, end: FGVec3, radius: FGi32) -> Self { Self { start, end, radius } } } impl FixedAffineCapsule { pub fn new(transform: FixedMat4, half_length: FGi32, radius: FGi32) -> Self { Self { transform, half_length, radius, } } pub fn transform_point(self, point: FGVec3) -> FGVec3 { self.transform.transform_point(point) } /// Tests the exact affine hurtbox against an ordinary world-space capsule. pub fn intersects(self, rhs: FixedCapsule) -> bool { let mut direction = rhs.start - self.transform.get_translation(); if direction == FGVec3::ZERO { direction = FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO); } let mut simplex = [FGVec3::ZERO; 4]; let mut simplex_len = 0; let mut supports = [FGVec3::ZERO; 20]; let mut support_count = 0; while support_count < supports.len() { let point = self.support(direction) - rhs.support(-direction); if point.dot(direction) < FGWide::ZERO { return false; } if supports[..support_count].contains(&point) { // Fixed-point quantization can make GJK revisit a support // point without making progress. This is not proof of an // intersection, so terminate conservatively as a miss return false; } supports[support_count] = point; support_count += 1; for index in (1..=simplex_len.min(3)).rev() { simplex[index] = simplex[index - 1]; } simplex[0] = point; simplex_len += 1; if update_simplex(&mut simplex, &mut simplex_len, &mut direction) { return true; } if direction == FGVec3::ZERO { return true; } } // Exhaustion means the fixed-point solver could not prove an // intersection. Treating it as a hit creates phantom contacts for // cycling or degenerate simplexes false } } trait SupportMap { fn support(self, direction: FGVec3) -> FGVec3; } impl SupportMap for FixedAffineCapsule { fn support(self, direction: FGVec3) -> FGVec3 { let local_direction = FGVec3::new( dot_column(self.transform, 0, direction), dot_column(self.transform, 1, direction), dot_column(self.transform, 2, direction), ); let round = local_direction.normalized().unwrap_or(FGVec3::new( FGi32::ONE, FGi32::ZERO, FGi32::ZERO, )) * self.radius; let endpoint = if local_direction.y >= FGi32::ZERO { self.half_length } else { -self.half_length }; self.transform_point(round + FGVec3::new(FGi32::ZERO, endpoint, FGi32::ZERO)) } } impl SupportMap for FixedCapsule { fn support(self, direction: FGVec3) -> FGVec3 { let endpoint = if self.start.dot(direction) >= self.end.dot(direction) { self.start } else { self.end }; endpoint + direction .normalized() .unwrap_or(FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO)) * self.radius } } fn dot_column(matrix: FixedMat4, column: usize, vector: FGVec3) -> FGi32 { FGi32::from_num( FGWide::from_num(matrix.cols[column][0]) * FGWide::from_num(vector.x) + FGWide::from_num(matrix.cols[column][1]) * FGWide::from_num(vector.y) + FGWide::from_num(matrix.cols[column][2]) * FGWide::from_num(vector.z), ) } fn cross(lhs: FGVec3, rhs: FGVec3) -> FGVec3 { FGVec3::new( FGi32::from_num( FGWide::from_num(lhs.y) * FGWide::from_num(rhs.z) - FGWide::from_num(lhs.z) * FGWide::from_num(rhs.y), ), FGi32::from_num( FGWide::from_num(lhs.z) * FGWide::from_num(rhs.x) - FGWide::from_num(lhs.x) * FGWide::from_num(rhs.z), ), FGi32::from_num( FGWide::from_num(lhs.x) * FGWide::from_num(rhs.y) - FGWide::from_num(lhs.y) * FGWide::from_num(rhs.x), ), ) } fn triple_cross(lhs: FGVec3, middle: FGVec3, rhs: FGVec3) -> FGVec3 { cross(cross(lhs, middle), rhs) } fn update_simplex( simplex: &mut [FGVec3; 4], simplex_len: &mut usize, direction: &mut FGVec3, ) -> bool { let a = simplex[0]; let ao = -a; match *simplex_len { 2 => { let ab = simplex[1] - a; if ab.dot(ao) > FGWide::ZERO { *direction = triple_cross(ab, ao, ab); } else { *simplex_len = 1; *direction = ao; } false } 3 => { let b = simplex[1]; let c = simplex[2]; let ab = b - a; let ac = c - a; let abc = cross(ab, ac); if cross(abc, ac).dot(ao) > FGWide::ZERO { if ac.dot(ao) > FGWide::ZERO { simplex[1] = c; *simplex_len = 2; *direction = triple_cross(ac, ao, ac); } else { *simplex_len = 2; *direction = triple_cross(ab, ao, ab); } } else if cross(ab, abc).dot(ao) > FGWide::ZERO { *simplex_len = 2; *direction = triple_cross(ab, ao, ab); } else if abc.dot(ao) > FGWide::ZERO { *direction = abc; } else { simplex.swap(1, 2); *direction = -abc; } false } 4 => { let b = simplex[1]; let c = simplex[2]; let d = simplex[3]; for (first, second, opposite) in [(b, c, d), (c, d, b), (d, b, c)] { let mut normal = cross(first - a, second - a); if normal.dot(opposite - a) > FGWide::ZERO { normal = -normal; } if normal.dot(ao) > FGWide::ZERO { simplex[0] = a; simplex[1] = first; simplex[2] = second; *simplex_len = 3; *direction = normal; return false; } } true } _ => { *direction = ao; false } } } #[cfg(test)] mod tests { use super::*; fn affine_capsule(x: &str, scale_y: &str) -> FixedAffineCapsule { let mut transform = FixedMat4::IDENTITY; transform.cols[1][1] = FGi32::lit(scale_y); transform.translate(FGVec3::lit(x, "0", "0")); FixedAffineCapsule::new(transform, FGi32::lit("1"), FGi32::lit("1")) } fn capsule(x: &str) -> FixedCapsule { FixedCapsule::new( FGVec3::lit(x, "-1", "0"), FGVec3::lit(x, "1", "0"), FGi32::lit("1"), ) } #[test] fn detects_touching_and_separated_capsules() { assert!(affine_capsule("0", "1").intersects(capsule("2"))); assert!(!affine_capsule("0", "1").intersects(capsule("2.01"))); } #[test] fn affine_scale_changes_capsule_intersection() { let sphere = FixedAffineCapsule::new(FixedMat4::IDENTITY, FGi32::ZERO, FGi32::lit("1")); let mut squashed_transform = FixedMat4::IDENTITY; squashed_transform.cols[1][1] = FGi32::lit("0.5"); let squashed = FixedAffineCapsule::new(squashed_transform, FGi32::ZERO, FGi32::lit("1")); let target = FixedCapsule::new( FGVec3::lit("0", "1.4", "0"), FGVec3::lit("0", "1.4", "0"), FGi32::lit("0.5"), ); assert!(sphere.intersects(target)); assert!(!squashed.intersects(target)); } }