use glam::{Quat, Vec3};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ColliderShape {
Box {
half_extents: [f32; 3],
},
Sphere {
radius: f32,
},
Capsule {
half_height: f32,
radius: f32,
},
Cylinder {
half_height: f32,
radius: f32,
},
Cone {
half_height: f32,
radius: f32,
},
Ellipsoid {
half_extents: [f32; 3],
},
}
impl ColliderShape {
pub fn default_box() -> Self {
Self::Box {
half_extents: [0.5, 0.5, 0.5],
}
}
pub fn default_sphere() -> Self {
Self::Sphere { radius: 1.0 }
}
pub fn default_capsule() -> Self {
Self::Capsule {
half_height: 0.5,
radius: 0.3,
}
}
pub fn default_cylinder() -> Self {
Self::Cylinder {
half_height: 0.5,
radius: 0.3,
}
}
pub fn default_cone() -> Self {
Self::Cone {
half_height: 0.5,
radius: 0.3,
}
}
pub fn default_ellipsoid() -> Self {
Self::Ellipsoid {
half_extents: [0.6, 0.4, 0.4],
}
}
}
pub struct EllipsoidHullMesh {
pub vertices: Vec<[f32; 3]>,
pub edges: Vec<(u16, u16)>,
}
pub fn ellipsoid_hull_mesh() -> &'static EllipsoidHullMesh {
use std::sync::OnceLock;
static CACHE: OnceLock<EllipsoidHullMesh> = OnceLock::new();
CACHE.get_or_init(build_ellipsoid_hull_mesh)
}
fn build_ellipsoid_hull_mesh() -> EllipsoidHullMesh {
let phi = (1.0_f32 + 5.0_f32.sqrt()) * 0.5;
let raw: [[f32; 3]; 12] = [
[-1.0, phi, 0.0],
[1.0, phi, 0.0],
[-1.0, -phi, 0.0],
[1.0, -phi, 0.0],
[0.0, -1.0, phi],
[0.0, 1.0, phi],
[0.0, -1.0, -phi],
[0.0, 1.0, -phi],
[phi, 0.0, -1.0],
[phi, 0.0, 1.0],
[-phi, 0.0, -1.0],
[-phi, 0.0, 1.0],
];
let base_faces: [[usize; 3]; 20] = [
[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1],
];
let normalize = |v: [f32; 3]| {
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
[v[0] / n, v[1] / n, v[2] / n]
};
let mut vertices: Vec<[f32; 3]> = raw.iter().copied().map(normalize).collect();
let mut midpoint_cache: std::collections::HashMap<(u16, u16), u16> =
std::collections::HashMap::new();
let mut sub_faces: Vec<[u16; 3]> = Vec::with_capacity(base_faces.len() * 4);
for face in &base_faces {
let a = face[0] as u16;
let b = face[1] as u16;
let c = face[2] as u16;
let ab = midpoint_index(&mut vertices, &mut midpoint_cache, a, b);
let bc = midpoint_index(&mut vertices, &mut midpoint_cache, b, c);
let ca = midpoint_index(&mut vertices, &mut midpoint_cache, c, a);
sub_faces.push([a, ab, ca]);
sub_faces.push([b, bc, ab]);
sub_faces.push([c, ca, bc]);
sub_faces.push([ab, bc, ca]);
}
let mut edge_set: std::collections::BTreeSet<(u16, u16)> = std::collections::BTreeSet::new();
for face in &sub_faces {
for (a, b) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
let key = if a < b { (a, b) } else { (b, a) };
edge_set.insert(key);
}
}
let edges: Vec<(u16, u16)> = edge_set.into_iter().collect();
debug_assert_eq!(vertices.len(), 42, "icosphere subdivision-1 vertex count");
debug_assert_eq!(edges.len(), 120, "icosphere subdivision-1 edge count");
EllipsoidHullMesh { vertices, edges }
}
fn midpoint_index(
vertices: &mut Vec<[f32; 3]>,
cache: &mut std::collections::HashMap<(u16, u16), u16>,
a: u16,
b: u16,
) -> u16 {
let key = if a < b { (a, b) } else { (b, a) };
if let Some(&idx) = cache.get(&key) {
return idx;
}
let va = vertices[a as usize];
let vb = vertices[b as usize];
let mid = [
(va[0] + vb[0]) * 0.5,
(va[1] + vb[1]) * 0.5,
(va[2] + vb[2]) * 0.5,
];
let n = (mid[0] * mid[0] + mid[1] * mid[1] + mid[2] * mid[2]).sqrt();
let normalized = [mid[0] / n, mid[1] / n, mid[2] / n];
let idx = vertices.len() as u16;
vertices.push(normalized);
cache.insert(key, idx);
idx
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ColliderSpec {
pub translation: Vec3,
pub rotation: Quat,
pub shape: ColliderShape,
}
impl ColliderSpec {
pub fn new(translation: Vec3, rotation: Quat, shape: ColliderShape) -> Self {
Self {
translation,
rotation,
shape,
}
}
pub fn from_node(node: &super::tree::EditorNode) -> Option<Self> {
let shape = match &node.kind {
super::tree::NodeKind::Collider(s) => s.clone(),
_ => return None,
};
Some(Self::new(
Vec3::from_array(node.transform.translation),
Quat::from_xyzw(
node.transform.rotation[0],
node.transform.rotation[1],
node.transform.rotation[2],
node.transform.rotation[3],
),
shape,
))
}
}