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, }
12
13#[derive(Clone, Copy)]
14struct MathPlane {
15 normal: Vec3,
16 d: f32, }
18
19impl MathPlane {
20 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
33fn compute_convex_volume(vertices: &[Vec3], indices: &[u32]) -> f32 {
36 if indices.len() < 3 {
37 return 0.001;
38 }
39 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 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 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 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 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 is_inside = false;
116 break;
117 }
118 }
119 if is_inside {
120 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 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 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 let face_center = face_verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b)
165 / face_verts.len() as f32;
166
167 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 ref_v.length_squared() < 0.5 {
179 continue;
180 }
181 let cross_test = n.cross(ref_v);
183 if cross_test.length_squared() < 1e-8 {
184 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 let base_idx = out_vertices.len() as u32;
206
207 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
239fn 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; }
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
254pub 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 let mass = if total_volume > 0.0 {
280 original_mass * (chunk.volume / total_volume)
281 } else {
282 0.1
283 };
284
285 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 let mut vel = *original_velocity;
296
297 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 let force = impact_force * 0.1 / (dir.length() + 1.0);
305 vel.linear += explosion_dir * (force / mass);
306
307 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 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, 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#[derive(Default)]
348pub struct PreFracturedCache {
349 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 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 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 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}