Files
shinespark/src/fighter/hurtbox.rs
T

270 lines
8.5 KiB
Rust

//! 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));
}
}