use gizmo_math::Vec3;
use rand::{rngs::StdRng, RngExt, SeedableRng};
#[derive(Clone, Debug)]
pub struct ProceduralChunk {
pub vertices: Vec<Vec3>,
pub normals: Vec<Vec3>,
pub indices: Vec<u32>,
pub center_of_mass: Vec3,
pub volume: f32, }
#[derive(Clone, Copy)]
struct MathPlane {
normal: Vec3,
d: f32, }
impl MathPlane {
fn distance(&self, pt: Vec3) -> f32 {
self.normal.dot(pt) - self.d
}
fn from_point_normal(pt: Vec3, normal: Vec3) -> Self {
Self {
normal: normal.normalize(),
d: normal.normalize().dot(pt),
}
}
}
fn compute_convex_volume(vertices: &[Vec3], indices: &[u32]) -> f32 {
if indices.len() < 3 {
return 0.001;
}
let centroid =
vertices.iter().copied().fold(Vec3::ZERO, |a, b| a + b) / vertices.len().max(1) as f32;
let mut vol = 0.0f32;
for tri in indices.chunks_exact(3) {
let a = vertices[tri[0] as usize] - centroid;
let b = vertices[tri[1] as usize] - centroid;
let c = vertices[tri[2] as usize] - centroid;
vol += a.dot(b.cross(c));
}
(vol / 6.0).abs().max(0.001)
}
pub fn voronoi_shatter(extents: Vec3, num_pieces: u32, seed: u64) -> Vec<ProceduralChunk> {
let mut rng = StdRng::seed_from_u64(seed);
let mut seeds = Vec::with_capacity(num_pieces as usize);
for _ in 0..num_pieces {
seeds.push(Vec3::new(
rng.random_range(-extents.x..extents.x),
rng.random_range(-extents.y..extents.y),
rng.random_range(-extents.z..extents.z),
));
}
let mut chunks = Vec::with_capacity(num_pieces as usize);
let box_planes = vec![
MathPlane::from_point_normal(Vec3::new(extents.x, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
MathPlane::from_point_normal(Vec3::new(-extents.x, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)),
MathPlane::from_point_normal(Vec3::new(0.0, extents.y, 0.0), Vec3::new(0.0, 1.0, 0.0)),
MathPlane::from_point_normal(Vec3::new(0.0, -extents.y, 0.0), Vec3::new(0.0, -1.0, 0.0)),
MathPlane::from_point_normal(Vec3::new(0.0, 0.0, extents.z), Vec3::new(0.0, 0.0, 1.0)),
MathPlane::from_point_normal(Vec3::new(0.0, 0.0, -extents.z), Vec3::new(0.0, 0.0, -1.0)),
];
let mut planes = Vec::with_capacity(box_planes.len() + num_pieces as usize);
let mut raw_vertices = Vec::with_capacity(256);
let mut out_vertices = Vec::with_capacity(256);
let mut out_normals = Vec::with_capacity(256);
let mut out_indices = Vec::with_capacity(512);
let mut face_verts = Vec::with_capacity(64);
for i in 0..num_pieces as usize {
let p_i = seeds[i];
planes.clear();
planes.extend_from_slice(&box_planes);
for j in 0..num_pieces as usize {
if i == j {
continue;
}
let p_j = seeds[j];
let dir = p_j - p_i;
let length = dir.length();
if length < 0.001 {
continue;
}
let normal = dir / length;
let mid = (p_i + p_j) * 0.5;
planes.push(MathPlane::from_point_normal(mid, normal));
}
raw_vertices.clear();
let num_planes = planes.len();
for p1 in 0..num_planes {
for p2 in (p1 + 1)..num_planes {
for p3 in (p2 + 1)..num_planes {
if let Some(intersection) =
intersect_planes(&planes[p1], &planes[p2], &planes[p3])
{
let mut is_inside = true;
for (k, plane) in planes.iter().enumerate() {
if k == p1 || k == p2 || k == p3 {
continue;
}
if plane.distance(intersection) > 0.001 {
is_inside = false;
break;
}
}
if is_inside {
let mut dup = false;
for &v in &raw_vertices {
let diff: Vec3 = v - intersection;
if diff.length_squared() < 0.0001 {
dup = true;
break;
}
}
if !dup {
raw_vertices.push(intersection);
}
}
}
}
}
}
if raw_vertices.len() < 4 {
continue;
}
let mut center = Vec3::ZERO;
for &v in &raw_vertices {
center += v;
}
center /= raw_vertices.len() as f32;
out_vertices.clear();
out_normals.clear();
out_indices.clear();
for plane in &planes {
face_verts.clear();
for &v in &raw_vertices {
if plane.distance(v).abs() < 0.005 {
face_verts.push(v);
}
}
if face_verts.len() >= 3 {
let face_center = face_verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b)
/ face_verts.len() as f32;
let n = plane.normal;
let mut ref_v = Vec3::ZERO;
for fv in &face_verts {
let candidate = *fv - face_center;
if candidate.length_squared() > 1e-8 {
ref_v = candidate.normalize();
break;
}
}
if ref_v.length_squared() < 0.5 {
continue;
}
let cross_test = n.cross(ref_v);
if cross_test.length_squared() < 1e-8 {
ref_v = if n.x.abs() > 0.9 {
Vec3::new(0.0, 1.0, 0.0)
} else {
Vec3::new(1.0, 0.0, 0.0)
};
}
let tangent = n.cross(ref_v).normalize();
let bitangent = n.cross(tangent).normalize();
face_verts.sort_by(|a, b| {
let dir_a = *a - face_center;
let dir_b = *b - face_center;
let angle_a = f32::atan2(dir_a.dot(tangent), dir_a.dot(bitangent));
let angle_b = f32::atan2(dir_b.dot(tangent), dir_b.dot(bitangent));
angle_a
.partial_cmp(&angle_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
let base_idx = out_vertices.len() as u32;
let norm = plane.normal;
for v in &face_verts {
out_vertices.push(*v);
out_normals.push(norm);
}
for k in 1..(face_verts.len() - 1) {
out_indices.push(base_idx);
out_indices.push(base_idx + k as u32);
out_indices.push(base_idx + k as u32 + 1);
}
}
}
if out_indices.is_empty() {
continue;
}
let volume = compute_convex_volume(&out_vertices, &out_indices);
chunks.push(ProceduralChunk {
vertices: out_vertices.clone(),
normals: out_normals.clone(),
indices: out_indices.clone(),
center_of_mass: center,
volume,
});
}
chunks
}
fn intersect_planes(p1: &MathPlane, p2: &MathPlane, p3: &MathPlane) -> Option<Vec3> {
let cross = p2.normal.cross(p3.normal);
let det = p1.normal.dot(cross);
if det.abs() < 0.0001 {
return None; }
let inv_det = 1.0 / det;
let res =
(cross * p1.d) + (p3.normal.cross(p1.normal) * p2.d) + (p1.normal.cross(p2.normal) * p3.d);
Some(res * inv_det)
}
pub fn generate_fracture_chunks(
original_transform: &gizmo_physics_core::Transform,
original_body: &crate::components::RigidBody,
original_velocity: &crate::components::Velocity,
extents: Vec3,
num_pieces: u32,
impact_point: Vec3,
impact_force: f32,
) -> Vec<(
crate::components::RigidBody,
gizmo_physics_core::Transform,
gizmo_physics_core::Collider,
crate::components::Velocity,
ProceduralChunk,
)> {
let chunks = voronoi_shatter(extents, num_pieces, rand::random::<u64>());
let mut results = Vec::with_capacity(chunks.len());
let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
let original_mass = original_body.mass;
for chunk in chunks {
let mass = if total_volume > 0.0 {
original_mass * (chunk.volume / total_volume)
} else {
0.1
};
let mut rb = crate::components::RigidBody::new(
mass,
original_body.restitution,
original_body.friction,
original_body.use_gravity,
);
rb.center_of_mass = chunk.center_of_mass;
let mut vel = *original_velocity;
let world_chunk_center =
original_transform.position + original_transform.rotation * chunk.center_of_mass;
let dir = world_chunk_center - impact_point;
if dir.length_squared() > 0.001 {
let explosion_dir = dir.normalize();
let force = impact_force * 0.1 / (dir.length() + 1.0);
vel.linear += explosion_dir * (force / mass);
vel.angular += Vec3::new(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
) * (force / mass)
* 0.5;
}
let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
let collider = gizmo_physics_core::Collider {
shape: gizmo_physics_core::ColliderShape::ConvexHull(
gizmo_physics_core::ConvexHullShape {
vertices: std::sync::Arc::new(hull.vertices),
faces: std::sync::Arc::new(hull.faces),
},
),
is_trigger: false,
material: gizmo_physics_core::PhysicsMaterial::default(),
collision_layer: gizmo_physics_core::CollisionLayer::default(),
};
rb.update_inertia_from_collider(&collider);
let transform = gizmo_physics_core::Transform {
position: original_transform.position, rotation: original_transform.rotation,
scale: original_transform.scale,
..*original_transform
};
results.push((rb, transform, collider, vel, chunk));
}
results
}
#[derive(Default)]
pub struct PreFracturedCache {
pub cache: std::collections::HashMap<gizmo_core::entity::Entity, Vec<ProceduralChunk>>,
}
impl PreFracturedCache {
pub fn new() -> Self {
Self {
cache: std::collections::HashMap::new(),
}
}
pub fn pre_fracture(
&mut self,
entity: gizmo_core::entity::Entity,
extents: Vec3,
num_pieces: u32,
seed: u64,
) {
let chunks = voronoi_shatter(extents, num_pieces, seed);
self.cache.insert(entity, chunks);
}
pub fn get_fracture_chunks(
&self,
entity: gizmo_core::entity::Entity,
original_transform: &gizmo_physics_core::Transform,
original_body: &crate::components::RigidBody,
original_velocity: &crate::components::Velocity,
impact_point: Vec3,
impact_force: f32,
) -> Option<
Vec<(
crate::components::RigidBody,
gizmo_physics_core::Transform,
gizmo_physics_core::Collider,
crate::components::Velocity,
ProceduralChunk,
)>,
> {
let chunks = self.cache.get(&entity)?;
let mut results = Vec::with_capacity(chunks.len());
let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
let original_mass = original_body.mass;
for chunk in chunks {
let mass = if total_volume > 0.0 {
original_mass * (chunk.volume / total_volume)
} else {
0.1
};
let mut rb = crate::components::RigidBody::new(
mass,
original_body.restitution,
original_body.friction,
original_body.use_gravity,
);
rb.center_of_mass = chunk.center_of_mass;
let mut vel = *original_velocity;
let world_chunk_center =
original_transform.position + original_transform.rotation * chunk.center_of_mass;
let dir = world_chunk_center - impact_point;
if dir.length_squared() > 0.001 {
let explosion_dir = dir.normalize();
let force = impact_force * 0.1 / (dir.length() + 1.0);
vel.linear += explosion_dir * (force / mass);
vel.angular += Vec3::new(
(chunk.center_of_mass.x * 12.345).fract() - 0.5,
(chunk.center_of_mass.y * 67.890).fract() - 0.5,
(chunk.center_of_mass.z * 42.123).fract() - 0.5,
) * (force / mass)
* 0.5;
}
let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
let collider = gizmo_physics_core::Collider {
shape: gizmo_physics_core::ColliderShape::ConvexHull(
gizmo_physics_core::ConvexHullShape {
vertices: std::sync::Arc::new(hull.vertices),
faces: std::sync::Arc::new(hull.faces),
},
),
is_trigger: false,
material: gizmo_physics_core::PhysicsMaterial::default(),
collision_layer: gizmo_physics_core::CollisionLayer::default(),
};
rb.update_inertia_from_collider(&collider);
let transform = gizmo_physics_core::Transform {
position: original_transform.position,
rotation: original_transform.rotation,
scale: original_transform.scale,
..*original_transform
};
results.push((rb, transform, collider, vel, chunk.clone()));
}
Some(results)
}
}