1use gizmo_math::Vec3;
2use gizmo_physics_core::BodyHandle;
3use rand::{rngs::StdRng, RngExt, SeedableRng};
4
5const MAX_EXPLOSION_DV: f32 = 50.0;
9
10#[derive(Clone, Debug)]
11pub struct ProceduralChunk {
12 pub vertices: Vec<Vec3>,
13 pub normals: Vec<Vec3>,
14 pub indices: Vec<u32>,
15 pub center_of_mass: Vec3,
16 pub volume: f32, }
18
19#[derive(Clone, Copy)]
20struct MathPlane {
21 normal: Vec3,
22 d: f32, }
24
25impl MathPlane {
26 fn distance(&self, pt: Vec3) -> f32 {
28 self.normal.dot(pt) - self.d
29 }
30
31 fn from_point_normal(pt: Vec3, normal: Vec3) -> Self {
32 Self {
33 normal: normal.normalize(),
34 d: normal.normalize().dot(pt),
35 }
36 }
37}
38
39fn compute_convex_mass_props(vertices: &[Vec3], indices: &[u32]) -> (Vec3, f32) {
53 let vertex_centroid =
54 vertices.iter().copied().fold(Vec3::ZERO, |a, b| a + b) / vertices.len().max(1) as f32;
55 if indices.len() < 3 {
56 return (vertex_centroid, 0.001);
57 }
58 let mut vol6 = 0.0f32; let mut com_acc = Vec3::ZERO; for tri in indices.chunks_exact(3) {
61 let a = vertices[tri[0] as usize] - vertex_centroid;
62 let b = vertices[tri[1] as usize] - vertex_centroid;
63 let c = vertices[tri[2] as usize] - vertex_centroid;
64 let v6 = a.dot(b.cross(c));
65 vol6 += v6;
66 com_acc += (a + b + c) * v6;
67 }
68 let volume = (vol6 / 6.0).abs().max(0.001);
69 let com = if vol6.abs() > 1e-8 {
72 vertex_centroid + com_acc / (4.0 * vol6)
73 } else {
74 vertex_centroid
75 };
76 (com, volume)
77}
78
79pub fn voronoi_shatter(extents: Vec3, num_pieces: u32, seed: u64) -> Vec<ProceduralChunk> {
80 let extents = extents.abs().max(Vec3::splat(1e-3));
86 let mut rng = StdRng::seed_from_u64(seed);
87
88 let mut seeds = Vec::with_capacity(num_pieces as usize);
90 for _ in 0..num_pieces {
91 seeds.push(Vec3::new(
92 rng.random_range(-extents.x..extents.x),
93 rng.random_range(-extents.y..extents.y),
94 rng.random_range(-extents.z..extents.z),
95 ));
96 }
97
98 let mut chunks = Vec::with_capacity(num_pieces as usize);
99
100 let box_planes = vec![
101 MathPlane::from_point_normal(Vec3::new(extents.x, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
102 MathPlane::from_point_normal(Vec3::new(-extents.x, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)),
103 MathPlane::from_point_normal(Vec3::new(0.0, extents.y, 0.0), Vec3::new(0.0, 1.0, 0.0)),
104 MathPlane::from_point_normal(Vec3::new(0.0, -extents.y, 0.0), Vec3::new(0.0, -1.0, 0.0)),
105 MathPlane::from_point_normal(Vec3::new(0.0, 0.0, extents.z), Vec3::new(0.0, 0.0, 1.0)),
106 MathPlane::from_point_normal(Vec3::new(0.0, 0.0, -extents.z), Vec3::new(0.0, 0.0, -1.0)),
107 ];
108
109 let mut planes = Vec::with_capacity(box_planes.len() + num_pieces as usize);
111 let mut raw_vertices = Vec::with_capacity(256);
112 let mut out_vertices = Vec::with_capacity(256);
113 let mut out_normals = Vec::with_capacity(256);
114 let mut out_indices = Vec::with_capacity(512);
115 let mut face_verts = Vec::with_capacity(64);
116
117 for i in 0..num_pieces as usize {
118 let p_i = seeds[i];
119
120 planes.clear();
121 planes.extend_from_slice(&box_planes);
122
123 for (j, &p_j) in seeds[..num_pieces as usize].iter().enumerate() {
124 if i == j {
125 continue;
126 }
127 let dir = p_j - p_i;
128 let length = dir.length();
129 if length < 0.001 {
130 continue;
131 }
132 let normal = dir / length;
133 let mid = (p_i + p_j) * 0.5;
134 planes.push(MathPlane::from_point_normal(mid, normal));
135 }
136
137 raw_vertices.clear();
139 let num_planes = planes.len();
140
141 for p1 in 0..num_planes {
142 for p2 in (p1 + 1)..num_planes {
143 for p3 in (p2 + 1)..num_planes {
144 if let Some(intersection) =
145 intersect_planes(&planes[p1], &planes[p2], &planes[p3])
146 {
147 let mut is_inside = true;
149 for (k, plane) in planes.iter().enumerate() {
150 if k == p1 || k == p2 || k == p3 {
151 continue;
152 }
153 if plane.distance(intersection) > 0.001 {
154 is_inside = false;
156 break;
157 }
158 }
159 if is_inside {
160 let mut dup = false;
162 for &v in &raw_vertices {
163 let diff: Vec3 = v - intersection;
164 if diff.length_squared() < 0.0001 {
165 dup = true;
166 break;
167 }
168 }
169 if !dup {
170 raw_vertices.push(intersection);
171 }
172 }
173 }
174 }
175 }
176 }
177
178 if raw_vertices.len() < 4 {
180 continue;
181 }
182
183 out_vertices.clear();
184 out_normals.clear();
185 out_indices.clear();
186
187 for plane in &planes {
190 face_verts.clear();
191 for &v in &raw_vertices {
192 if plane.distance(v).abs() < 0.005 {
193 face_verts.push(v);
194 }
195 }
196 if face_verts.len() >= 3 {
197 let face_center = face_verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b)
199 / face_verts.len() as f32;
200
201 let n = plane.normal;
203 let mut ref_v = Vec3::ZERO;
204 for fv in &face_verts {
205 let candidate = *fv - face_center;
206 if candidate.length_squared() > 1e-8 {
207 ref_v = candidate.normalize();
208 break;
209 }
210 }
211 if ref_v.length_squared() < 0.5 {
213 continue;
214 }
215 let cross_test = n.cross(ref_v);
217 if cross_test.length_squared() < 1e-8 {
218 ref_v = if n.x.abs() > 0.9 {
220 Vec3::new(0.0, 1.0, 0.0)
221 } else {
222 Vec3::new(1.0, 0.0, 0.0)
223 };
224 }
225 let tangent = n.cross(ref_v).normalize();
226 let bitangent = n.cross(tangent).normalize();
227
228 face_verts.sort_by(|a, b| {
229 let dir_a = *a - face_center;
230 let dir_b = *b - face_center;
231 let angle_a = f32::atan2(dir_a.dot(tangent), dir_a.dot(bitangent));
232 let angle_b = f32::atan2(dir_b.dot(tangent), dir_b.dot(bitangent));
233 angle_a
234 .partial_cmp(&angle_b)
235 .unwrap_or(std::cmp::Ordering::Equal)
236 });
237
238 let base_idx = out_vertices.len() as u32;
240
241 let norm = plane.normal;
243 for v in &face_verts {
244 out_vertices.push(*v);
245 out_normals.push(norm);
246 }
247
248 for k in 1..(face_verts.len() - 1) {
249 out_indices.push(base_idx);
250 out_indices.push(base_idx + k as u32);
251 out_indices.push(base_idx + k as u32 + 1);
252 }
253 }
254 }
255
256 if out_indices.is_empty() {
257 continue;
258 }
259
260 let (center_of_mass, volume) = compute_convex_mass_props(&out_vertices, &out_indices);
263 chunks.push(ProceduralChunk {
264 vertices: out_vertices.clone(),
265 normals: out_normals.clone(),
266 indices: out_indices.clone(),
267 center_of_mass,
268 volume,
269 });
270 }
271
272 chunks
273}
274
275fn intersect_planes(p1: &MathPlane, p2: &MathPlane, p3: &MathPlane) -> Option<Vec3> {
277 let cross = p2.normal.cross(p3.normal);
278 let det = p1.normal.dot(cross);
279 if det.abs() < 0.0001 {
280 return None; }
282
283 let inv_det = 1.0 / det;
284 let res =
285 (cross * p1.d) + (p3.normal.cross(p1.normal) * p2.d) + (p1.normal.cross(p2.normal) * p3.d);
286
287 Some(res * inv_det)
288}
289
290pub fn generate_fracture_chunks(
293 original_transform: &gizmo_physics_core::Transform,
294 original_body: &crate::components::RigidBody,
295 original_velocity: &crate::components::Velocity,
296 extents: Vec3,
297 num_pieces: u32,
298 impact_point: Vec3,
299 impact_force: f32,
300) -> Vec<(
301 crate::components::RigidBody,
302 gizmo_physics_core::Transform,
303 gizmo_physics_core::Collider,
304 crate::components::Velocity,
305 ProceduralChunk,
306)> {
307 let chunks = voronoi_shatter(extents, num_pieces, rand::random::<u64>());
308
309 let mut results = Vec::with_capacity(chunks.len());
310 let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
311 let original_mass = original_body.mass;
312
313 for chunk in chunks {
314 let mass = if total_volume > 0.0 {
316 original_mass * (chunk.volume / total_volume)
317 } else {
318 0.1
319 };
320
321 let mut rb = crate::components::RigidBody::new(mass, original_body.use_gravity);
324 rb.center_of_mass = chunk.center_of_mass;
325
326 let mut vel = *original_velocity;
328
329 let world_chunk_center =
331 original_transform.position + original_transform.rotation * chunk.center_of_mass;
332 let dir = world_chunk_center - impact_point;
333 if dir.length_squared() > 0.001 {
334 let explosion_dir = dir.normalize();
335 let force = impact_force * 0.1 / (dir.length() + 1.0);
337 let dv = (force / mass).min(MAX_EXPLOSION_DV);
338 vel.linear += explosion_dir * dv;
339
340 vel.angular += Vec3::new(
342 rand::random::<f32>() - 0.5,
343 rand::random::<f32>() - 0.5,
344 rand::random::<f32>() - 0.5,
345 ) * dv
346 * 0.5;
347 }
348
349 let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
351 let collider = gizmo_physics_core::Collider::from_shape(
352 gizmo_physics_core::ColliderShape::ConvexHull(gizmo_physics_core::ConvexHullShape {
353 vertices: std::sync::Arc::new(hull.vertices),
354 faces: std::sync::Arc::new(hull.faces),
355 }),
356 );
357
358 rb.update_inertia_from_collider(&collider);
359
360 let transform = gizmo_physics_core::Transform {
361 position: original_transform.position, rotation: original_transform.rotation,
363 scale: original_transform.scale,
364 ..*original_transform
365 };
366
367 results.push((rb, transform, collider, vel, chunk));
368 }
369
370 results
371}
372
373#[derive(Default)]
376pub struct PreFracturedCache {
377 pub cache: std::collections::HashMap<BodyHandle, Vec<ProceduralChunk>>,
379}
380
381impl PreFracturedCache {
382 pub fn new() -> Self {
383 Self {
384 cache: std::collections::HashMap::new(),
385 }
386 }
387
388 pub fn pre_fracture(
391 &mut self,
392 entity: BodyHandle,
393 extents: Vec3,
394 num_pieces: u32,
395 seed: u64,
396 ) {
397 let chunks = voronoi_shatter(extents, num_pieces, seed);
398 self.cache.insert(entity, chunks);
399 }
400
401 pub fn get_fracture_chunks(
404 &self,
405 entity: BodyHandle,
406 original_transform: &gizmo_physics_core::Transform,
407 original_body: &crate::components::RigidBody,
408 original_velocity: &crate::components::Velocity,
409 impact_point: Vec3,
410 impact_force: f32,
411 ) -> Option<
412 Vec<(
413 crate::components::RigidBody,
414 gizmo_physics_core::Transform,
415 gizmo_physics_core::Collider,
416 crate::components::Velocity,
417 ProceduralChunk,
418 )>,
419 > {
420 let chunks = self.cache.get(&entity)?;
421
422 let mut results = Vec::with_capacity(chunks.len());
423 let total_volume: f32 = chunks.iter().map(|c| c.volume).sum();
424 let original_mass = original_body.mass;
425
426 for chunk in chunks {
427 let mass = if total_volume > 0.0 {
428 original_mass * (chunk.volume / total_volume)
429 } else {
430 0.1
431 };
432
433 let mut rb = crate::components::RigidBody::new(mass, original_body.use_gravity);
434 rb.center_of_mass = chunk.center_of_mass;
435
436 let mut vel = *original_velocity;
437 let world_chunk_center =
438 original_transform.position + original_transform.rotation * chunk.center_of_mass;
439 let dir = world_chunk_center - impact_point;
440 if dir.length_squared() > 0.001 {
441 let explosion_dir = dir.normalize();
442 let force = impact_force * 0.1 / (dir.length() + 1.0);
443 let dv = (force / mass).min(MAX_EXPLOSION_DV);
444 vel.linear += explosion_dir * dv;
445
446 vel.angular += Vec3::new(
448 (chunk.center_of_mass.x * 12.345).fract() - 0.5,
449 (chunk.center_of_mass.y * 67.890).fract() - 0.5,
450 (chunk.center_of_mass.z * 42.123).fract() - 0.5,
451 ) * dv
452 * 0.5;
453 }
454
455 let hull = gizmo_physics_core::quickhull::compute_convex_hull(&chunk.vertices);
456 let collider = gizmo_physics_core::Collider::from_shape(
457 gizmo_physics_core::ColliderShape::ConvexHull(gizmo_physics_core::ConvexHullShape {
458 vertices: std::sync::Arc::new(hull.vertices),
459 faces: std::sync::Arc::new(hull.faces),
460 }),
461 );
462
463 rb.update_inertia_from_collider(&collider);
464
465 let transform = gizmo_physics_core::Transform {
466 position: original_transform.position,
467 rotation: original_transform.rotation,
468 scale: original_transform.scale,
469 ..*original_transform
470 };
471
472 results.push((rb, transform, collider, vel, chunk.clone()));
473 }
474
475 Some(results)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::components::{RigidBody, Velocity};
483 use gizmo_physics_core::BodyHandle;
484 use gizmo_physics_core::Transform;
485
486 #[test]
487 fn voronoi_shatter_does_not_panic_on_degenerate_extents() {
488 let flat = voronoi_shatter(Vec3::new(1.0, 0.0, 1.0), 6, 42);
492 assert!(!flat.is_empty(), "flat box should still produce chunks");
493 let neg = voronoi_shatter(Vec3::new(-1.0, 0.5, 0.0), 4, 7);
494 assert!(!neg.is_empty(), "negative/zero extents must be handled, not panic");
495 assert!(!voronoi_shatter(Vec3::splat(1.0), 8, 1).is_empty());
497 }
498
499 #[test]
503 fn convex_mass_props_returns_volume_centroid_not_vertex_average() {
504 let verts = vec![
506 Vec3::new(-1.0, -1.0, 0.0),
507 Vec3::new(1.0, -1.0, 0.0),
508 Vec3::new(1.0, 1.0, 0.0),
509 Vec3::new(-1.0, 1.0, 0.0),
510 Vec3::new(0.0, 0.0, 3.0),
511 ];
512 let mut tris: Vec<[u32; 3]> = vec![
514 [0, 1, 2],
515 [0, 2, 3], [0, 1, 4],
517 [1, 2, 4],
518 [2, 3, 4],
519 [3, 0, 4], ];
521 let centroid = verts.iter().copied().fold(Vec3::ZERO, |a, b| a + b) / verts.len() as f32;
523 let mut indices = Vec::new();
524 for t in &mut tris {
525 let (a, b, c) = (
526 verts[t[0] as usize],
527 verts[t[1] as usize],
528 verts[t[2] as usize],
529 );
530 let n = (b - a).cross(c - a);
531 let face_center = (a + b + c) / 3.0;
532 if n.dot(face_center - centroid) < 0.0 {
533 t.swap(1, 2);
534 }
535 indices.extend_from_slice(t);
536 }
537
538 let (com, vol) = compute_convex_mass_props(&verts, &indices);
539 assert!((vol - 4.0).abs() < 1e-3, "hacim ~4 olmalı, bulundu {vol}");
540 assert!(
541 com.x.abs() < 1e-4 && com.y.abs() < 1e-4,
542 "simetri: com.xy ≈ 0, bulundu {com:?}"
543 );
544 assert!(
545 (com.z - 0.75).abs() < 1e-3,
546 "hacim merkezi z ≈ 0.75 olmalı (vertex ortalaması 0.6 DEĞİL), bulundu {}",
547 com.z
548 );
549 assert!(
550 (com.z - 0.6).abs() > 0.1,
551 "COM vertex ortalamasından (0.6) belirgin farklı olmalı"
552 );
553 }
554
555 #[test]
558 fn explosion_velocity_clamped_for_tiny_chunks() {
559 let tetra = || {
560 vec![
561 Vec3::ZERO,
562 Vec3::X * 0.1,
563 Vec3::Y * 0.1,
564 Vec3::Z * 0.1,
565 ]
566 };
567 let big = ProceduralChunk {
568 vertices: tetra(),
569 normals: vec![],
570 indices: vec![],
571 center_of_mass: Vec3::new(1.0, 0.0, 0.0),
572 volume: 10.0,
573 };
574 let tiny = ProceduralChunk {
575 vertices: tetra(),
576 normals: vec![],
577 indices: vec![],
578 center_of_mass: Vec3::new(-1.0, 0.0, 0.0),
579 volume: 0.001, };
581
582 let mut cache = PreFracturedCache::new();
583 let e = BodyHandle::from_id(1);
584 cache.cache.insert(e, vec![big, tiny]);
585
586 let tr = Transform::new(Vec3::ZERO);
587 let body = RigidBody::new(10.0, true);
588 let out = cache
589 .get_fracture_chunks(e, &tr, &body, &Velocity::default(), Vec3::new(0.0, 5.0, 0.0), 1.0e6)
590 .unwrap();
591
592 assert_eq!(out.len(), 2);
593 for (_rb, _t, _c, v, _chunk) in &out {
594 assert!(v.linear.is_finite() && v.angular.is_finite(), "hız sonlu olmalı");
595 assert!(
596 v.linear.length() < 100.0,
597 "patlama hızı makul sınırda olmalı (clamp), oldu: {}",
598 v.linear.length()
599 );
600 }
601 }
602}