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