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 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 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 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 is_inside = false;
125 break;
126 }
127 }
128 if is_inside {
129 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 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 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 let face_center = face_verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b)
174 / face_verts.len() as f32;
175
176 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 ref_v.length_squared() < 0.5 {
188 continue;
189 }
190 let cross_test = n.cross(ref_v);
192 if cross_test.length_squared() < 1e-8 {
193 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 let base_idx = out_vertices.len() as u32;
215
216 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
248fn 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; }
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
263pub 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 let mass = if total_volume > 0.0 {
289 original_mass * (chunk.volume / total_volume)
290 } else {
291 0.1
292 };
293
294 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 let mut vel = *original_velocity;
305
306 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 let force = impact_force * 0.1 / (dir.length() + 1.0);
314 vel.linear += explosion_dir * (force / mass);
315
316 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 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, 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#[derive(Default)]
357pub struct PreFracturedCache {
358 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 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 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 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}