Skip to main content

gizmo_physics_rigid/
fracture.rs

1use gizmo_math::Vec3;
2use rand::{rngs::StdRng, RngExt, SeedableRng};
3
4#[derive(Clone, Debug)]
5pub struct ProceduralChunk {
6    pub vertices: Vec<Vec3>,
7    pub normals: Vec<Vec3>,
8    pub indices: Vec<u32>,
9    pub center_of_mass: Vec3,
10    pub volume: f32, // approximated
11}
12
13#[derive(Clone, Copy)]
14struct MathPlane {
15    normal: Vec3,
16    d: f32, // dot(N, P) - d = 0 => dot(N, P) = d
17}
18
19impl MathPlane {
20    // Normal points OUTSIDE
21    fn distance(&self, pt: Vec3) -> f32 {
22        self.normal.dot(pt) - self.d
23    }
24
25    fn from_point_normal(pt: Vec3, normal: Vec3) -> Self {
26        Self {
27            normal: normal.normalize(),
28            d: normal.normalize().dot(pt),
29        }
30    }
31}
32
33/// Compute the approximate volume of a convex polyhedron defined by its vertices
34/// using signed tetrahedron decomposition relative to the centroid.
35fn compute_convex_volume(vertices: &[Vec3], indices: &[u32]) -> f32 {
36    if indices.len() < 3 {
37        return 0.001;
38    }
39    // Use the centroid as the reference point
40    let centroid =
41        vertices.iter().copied().fold(Vec3::ZERO, |a, b| a + b) / vertices.len().max(1) as f32;
42    let mut vol = 0.0f32;
43    // Sum signed tetrahedron volumes for each triangle face
44    for tri in indices.chunks_exact(3) {
45        let a = vertices[tri[0] as usize] - centroid;
46        let b = vertices[tri[1] as usize] - centroid;
47        let c = vertices[tri[2] as usize] - centroid;
48        vol += a.dot(b.cross(c));
49    }
50    (vol / 6.0).abs().max(0.001)
51}
52
53pub fn voronoi_shatter(extents: Vec3, num_pieces: u32, seed: u64) -> Vec<ProceduralChunk> {
54    let mut rng = StdRng::seed_from_u64(seed);
55
56    // 1. Generate seeds
57    let mut seeds = Vec::with_capacity(num_pieces as usize);
58    for _ in 0..num_pieces {
59        seeds.push(Vec3::new(
60            rng.random_range(-extents.x..extents.x),
61            rng.random_range(-extents.y..extents.y),
62            rng.random_range(-extents.z..extents.z),
63        ));
64    }
65
66    let mut chunks = Vec::with_capacity(num_pieces as usize);
67
68    let box_planes = vec![
69        MathPlane::from_point_normal(Vec3::new(extents.x, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
70        MathPlane::from_point_normal(Vec3::new(-extents.x, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)),
71        MathPlane::from_point_normal(Vec3::new(0.0, extents.y, 0.0), Vec3::new(0.0, 1.0, 0.0)),
72        MathPlane::from_point_normal(Vec3::new(0.0, -extents.y, 0.0), Vec3::new(0.0, -1.0, 0.0)),
73        MathPlane::from_point_normal(Vec3::new(0.0, 0.0, extents.z), Vec3::new(0.0, 0.0, 1.0)),
74        MathPlane::from_point_normal(Vec3::new(0.0, 0.0, -extents.z), Vec3::new(0.0, 0.0, -1.0)),
75    ];
76
77    for i in 0..num_pieces as usize {
78        let p_i = seeds[i];
79
80        let mut planes = box_planes.clone();
81
82        for j in 0..num_pieces as usize {
83            if i == j {
84                continue;
85            }
86            let p_j = seeds[j];
87            let dir = p_j - p_i;
88            let length = dir.length();
89            if length < 0.001 {
90                continue;
91            }
92            let normal = dir / length;
93            let mid = (p_i + p_j) * 0.5;
94            planes.push(MathPlane::from_point_normal(mid, normal));
95        }
96
97        // Find vertices via plane intersections
98        let mut raw_vertices = Vec::new();
99        let num_planes = planes.len();
100
101        for p1 in 0..num_planes {
102            for p2 in (p1 + 1)..num_planes {
103                for p3 in (p2 + 1)..num_planes {
104                    if let Some(intersection) =
105                        intersect_planes(&planes[p1], &planes[p2], &planes[p3])
106                    {
107                        // Check if it's inside all other planes
108                        let mut is_inside = true;
109                        for (k, plane) in planes.iter().enumerate() {
110                            if k == p1 || k == p2 || k == p3 {
111                                continue;
112                            }
113                            if plane.distance(intersection) > 0.001 {
114                                // Slight epsilon
115                                is_inside = false;
116                                break;
117                            }
118                        }
119                        if is_inside {
120                            // Don't add duplicates
121                            let mut dup = false;
122                            for &v in &raw_vertices {
123                                let diff: Vec3 = v - intersection;
124                                if diff.length_squared() < 0.0001 {
125                                    dup = true;
126                                    break;
127                                }
128                            }
129                            if !dup {
130                                raw_vertices.push(intersection);
131                            }
132                        }
133                    }
134                }
135            }
136        }
137
138        // If something went wrong and we couldn't form a 3D boundary, skip
139        if raw_vertices.len() < 4 {
140            continue;
141        }
142
143        let mut center = Vec3::ZERO;
144        for &v in &raw_vertices {
145            center += v;
146        }
147        center /= raw_vertices.len() as f32;
148
149        let mut out_vertices = Vec::new();
150        let mut out_normals = Vec::new();
151        let mut out_indices = Vec::new();
152
153        // Accumulate face triangles
154        // A face is formed by a subset of raw_vertices that lie on one of the `planes`.
155        for plane in &planes {
156            let mut face_verts = Vec::new();
157            for &v in &raw_vertices {
158                if plane.distance(v).abs() < 0.005 {
159                    face_verts.push(v);
160                }
161            }
162            if face_verts.len() >= 3 {
163                // Sort vertices around the plane normal, projecting onto a 2D coordinate system
164                let face_center = face_verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b)
165                    / face_verts.len() as f32;
166
167                // create local basis — guard against degenerate ref_v
168                let n = plane.normal;
169                let mut ref_v = Vec3::ZERO;
170                for fv in &face_verts {
171                    let candidate = *fv - face_center;
172                    if candidate.length_squared() > 1e-8 {
173                        ref_v = candidate.normalize();
174                        break;
175                    }
176                }
177                // If all vertices coincide with face_center (degenerate), skip face
178                if ref_v.length_squared() < 0.5 {
179                    continue;
180                }
181                // Ensure ref_v is not parallel to normal
182                let cross_test = n.cross(ref_v);
183                if cross_test.length_squared() < 1e-8 {
184                    // Pick an arbitrary perpendicular
185                    ref_v = if n.x.abs() > 0.9 {
186                        Vec3::new(0.0, 1.0, 0.0)
187                    } else {
188                        Vec3::new(1.0, 0.0, 0.0)
189                    };
190                }
191                let tangent = n.cross(ref_v).normalize();
192                let bitangent = n.cross(tangent).normalize();
193
194                face_verts.sort_by(|a, b| {
195                    let dir_a = *a - face_center;
196                    let dir_b = *b - face_center;
197                    let angle_a = f32::atan2(dir_a.dot(tangent), dir_a.dot(bitangent));
198                    let angle_b = f32::atan2(dir_b.dot(tangent), dir_b.dot(bitangent));
199                    angle_a
200                        .partial_cmp(&angle_b)
201                        .unwrap_or(std::cmp::Ordering::Equal)
202                });
203
204                // Fan triangulation
205                let base_idx = out_vertices.len() as u32;
206
207                // To keep hard edges, duplicate the vertices for this face and calculate proper normals
208                let norm = plane.normal;
209                for v in &face_verts {
210                    out_vertices.push(*v);
211                    out_normals.push(norm);
212                }
213
214                for k in 1..(face_verts.len() - 1) {
215                    out_indices.push(base_idx);
216                    out_indices.push(base_idx + k as u32);
217                    out_indices.push(base_idx + k as u32 + 1);
218                }
219            }
220        }
221
222        if out_indices.is_empty() {
223            continue;
224        }
225
226        let volume = compute_convex_volume(&out_vertices, &out_indices);
227        chunks.push(ProceduralChunk {
228            vertices: out_vertices,
229            normals: out_normals,
230            indices: out_indices,
231            center_of_mass: center,
232            volume,
233        });
234    }
235
236    chunks
237}
238
239// Intersects three planes and finds the intersection point
240fn intersect_planes(p1: &MathPlane, p2: &MathPlane, p3: &MathPlane) -> Option<Vec3> {
241    let cross = p2.normal.cross(p3.normal);
242    let det = p1.normal.dot(cross);
243    if det.abs() < 0.0001 {
244        return None; // Planes do not intersect at a single point (parallel)
245    }
246
247    let inv_det = 1.0 / det;
248    let res =
249        (cross * p1.d) + (p3.normal.cross(p1.normal) * p2.d) + (p1.normal.cross(p2.normal) * p3.d);
250
251    Some(res * inv_det)
252}
253
254/// Helper function to create physics chunks from a fracturing event.
255/// Returns a list of (RigidBody, Transform, Collider, ProceduralChunk) for the ECS to spawn.
256pub fn generate_fracture_chunks(
257    original_transform: &gizmo_physics_core::Transform,
258    original_body: &crate::components::RigidBody,
259    original_velocity: &crate::components::Velocity,
260    extents: Vec3,
261    num_pieces: u32,
262    impact_point: Vec3,
263    impact_force: f32,
264) -> Vec<(
265    crate::components::RigidBody,
266    gizmo_physics_core::Transform,
267    gizmo_physics_core::Collider,
268    crate::components::Velocity,
269    ProceduralChunk,
270)> {
271    let chunks = voronoi_shatter(extents, num_pieces, rand::random::<u64>());
272
273    let mut results = Vec::with_capacity(chunks.len());
274    let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
275    let original_mass = original_body.mass;
276
277    for chunk in chunks {
278        // Calculate fraction of mass
279        let mass = if total_volume > 0.0 {
280            original_mass * (chunk.volume / total_volume)
281        } else {
282            0.1
283        };
284
285        // Create new rigid body
286        let mut rb = crate::components::RigidBody::new(
287            mass,
288            original_body.restitution,
289            original_body.friction,
290            original_body.use_gravity,
291        );
292        rb.center_of_mass = chunk.center_of_mass;
293
294        // Inherit exact same velocity + explosion force away from impact point
295        let mut vel = *original_velocity;
296
297        // Calculate explosion force direction
298        let world_chunk_center =
299            original_transform.position + original_transform.rotation * chunk.center_of_mass;
300        let dir = world_chunk_center - impact_point;
301        if dir.length_squared() > 0.001 {
302            let explosion_dir = dir.normalize();
303            // Force drops off with distance (simplified)
304            let force = impact_force * 0.1 / (dir.length() + 1.0);
305            vel.linear += explosion_dir * (force / mass);
306
307            // Add some random spin
308            vel.angular += Vec3::new(
309                rand::random::<f32>() - 0.5,
310                rand::random::<f32>() - 0.5,
311                rand::random::<f32>() - 0.5,
312            ) * (force / mass)
313                * 0.5;
314        }
315
316        // Create convex hull collider
317        let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
318        let collider = gizmo_physics_core::Collider {
319            shape: gizmo_physics_core::ColliderShape::ConvexHull(
320                gizmo_physics_core::ConvexHullShape {
321                    vertices: std::sync::Arc::new(hull.vertices),
322                    faces: std::sync::Arc::new(hull.faces),
323                },
324            ),
325            is_trigger: false,
326            material: gizmo_physics_core::PhysicsMaterial::default(),
327            collision_layer: gizmo_physics_core::CollisionLayer::default(),
328        };
329
330        rb.update_inertia_from_collider(&collider);
331
332        let transform = gizmo_physics_core::Transform {
333            position: original_transform.position, // The vertices in the chunk are local to the original center
334            rotation: original_transform.rotation,
335            scale: original_transform.scale,
336            ..*original_transform
337        };
338
339        results.push((rb, transform, collider, vel, chunk));
340    }
341
342    results
343}
344
345/// Stores pre-fractured chunks to avoid expensive runtime calculations (Pre-fracture Caching).
346/// Ideal for AAA games where destruction must not drop frames.
347#[derive(Default)]
348pub struct PreFracturedCache {
349    /// Maps an Entity ID to its pre-calculated fracture data
350    pub cache: std::collections::HashMap<gizmo_core::entity::Entity, Vec<ProceduralChunk>>,
351}
352
353impl PreFracturedCache {
354    pub fn new() -> Self {
355        Self {
356            cache: std::collections::HashMap::new(),
357        }
358    }
359
360    /// Pre-calculates fracture chunks for an entity and stores them in the cache.
361    /// This should be called during a loading screen.
362    pub fn pre_fracture(
363        &mut self,
364        entity: gizmo_core::entity::Entity,
365        extents: Vec3,
366        num_pieces: u32,
367        seed: u64,
368    ) {
369        let chunks = voronoi_shatter(extents, num_pieces, seed);
370        self.cache.insert(entity, chunks);
371    }
372
373    /// Spawns the chunks from the cache if available, taking only O(N) time to clone instead of O(N^3).
374    /// If not in cache, optionally falls back to runtime calculation.
375    pub fn get_fracture_chunks(
376        &self,
377        entity: gizmo_core::entity::Entity,
378        original_transform: &gizmo_physics_core::Transform,
379        original_body: &crate::components::RigidBody,
380        original_velocity: &crate::components::Velocity,
381        impact_point: Vec3,
382        impact_force: f32,
383    ) -> Option<
384        Vec<(
385            crate::components::RigidBody,
386            gizmo_physics_core::Transform,
387            gizmo_physics_core::Collider,
388            crate::components::Velocity,
389            ProceduralChunk,
390        )>,
391    > {
392        let chunks = self.cache.get(&entity)?;
393
394        let mut results = Vec::with_capacity(chunks.len());
395        let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
396        let original_mass = original_body.mass;
397
398        for chunk in chunks {
399            let mass = if total_volume > 0.0 {
400                original_mass * (chunk.volume / total_volume)
401            } else {
402                0.1
403            };
404
405            let mut rb = crate::components::RigidBody::new(
406                mass,
407                original_body.restitution,
408                original_body.friction,
409                original_body.use_gravity,
410            );
411            rb.center_of_mass = chunk.center_of_mass;
412
413            let mut vel = *original_velocity;
414            let world_chunk_center =
415                original_transform.position + original_transform.rotation * chunk.center_of_mass;
416            let dir = world_chunk_center - impact_point;
417            if dir.length_squared() > 0.001 {
418                let explosion_dir = dir.normalize();
419                let force = impact_force * 0.1 / (dir.length() + 1.0);
420                vel.linear += explosion_dir * (force / mass);
421
422                // Deterministic spin based on chunk properties (since cache is pre-calculated)
423                vel.angular += Vec3::new(
424                    (chunk.center_of_mass.x * 12.345).fract() - 0.5,
425                    (chunk.center_of_mass.y * 67.890).fract() - 0.5,
426                    (chunk.center_of_mass.z * 42.123).fract() - 0.5,
427                ) * (force / mass)
428                    * 0.5;
429            }
430
431            let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
432            let collider = gizmo_physics_core::Collider {
433                shape: gizmo_physics_core::ColliderShape::ConvexHull(
434                    gizmo_physics_core::ConvexHullShape {
435                        vertices: std::sync::Arc::new(hull.vertices),
436                        faces: std::sync::Arc::new(hull.faces),
437                    },
438                ),
439                is_trigger: false,
440                material: gizmo_physics_core::PhysicsMaterial::default(),
441                collision_layer: gizmo_physics_core::CollisionLayer::default(),
442            };
443
444            rb.update_inertia_from_collider(&collider);
445
446            let transform = gizmo_physics_core::Transform {
447                position: original_transform.position,
448                rotation: original_transform.rotation,
449                scale: original_transform.scale,
450                ..*original_transform
451            };
452
453            results.push((rb, transform, collider, vel, chunk.clone()));
454        }
455
456        Some(results)
457    }
458}