Skip to main content

gizmo_engine/systems/
physics.rs

1use crate::math::Vec3;
2use gizmo_physics_core::{Collider, ColliderShape, Transform, components::GpuPhysicsLink};
3use gizmo_physics_rigid::components::RigidBody;
4#[cfg(feature = "render")]
5use crate::renderer::Renderer;
6
7#[cfg(feature = "render")]
8pub fn physics_debug_system(world: &crate::core::World) {
9    if let Some(mut gizmos) = world.get_resource_mut::<crate::renderer::Gizmos>() {
10        fn draw_collider(
11            trans: &Transform,
12            col: &Collider,
13            color: [f32; 4],
14            gizmos: &mut crate::renderer::Gizmos,
15        ) {
16            match &col.shape {
17                gizmo_physics_core::ColliderShape::Box(b) => {
18                    let h = b.half_extents;
19                    let r = trans.rotation;
20                    let p = trans.position;
21                    let p0 = p + r.mul_vec3(Vec3::new(-h.x, -h.y, -h.z));
22                    let p1 = p + r.mul_vec3(Vec3::new(h.x, -h.y, -h.z));
23                    let p2 = p + r.mul_vec3(Vec3::new(h.x, h.y, -h.z));
24                    let p3 = p + r.mul_vec3(Vec3::new(-h.x, h.y, -h.z));
25                    let p4 = p + r.mul_vec3(Vec3::new(-h.x, -h.y, h.z));
26                    let p5 = p + r.mul_vec3(Vec3::new(h.x, -h.y, h.z));
27                    let p6 = p + r.mul_vec3(Vec3::new(h.x, h.y, h.z));
28                    let p7 = p + r.mul_vec3(Vec3::new(-h.x, h.y, h.z));
29
30                    gizmos.draw_line(p0, p1, color);
31                    gizmos.draw_line(p1, p2, color);
32                    gizmos.draw_line(p2, p3, color);
33                    gizmos.draw_line(p3, p0, color);
34                    gizmos.draw_line(p4, p5, color);
35                    gizmos.draw_line(p5, p6, color);
36                    gizmos.draw_line(p6, p7, color);
37                    gizmos.draw_line(p7, p4, color);
38                    gizmos.draw_line(p0, p4, color);
39                    gizmos.draw_line(p1, p5, color);
40                    gizmos.draw_line(p2, p6, color);
41                    gizmos.draw_line(p3, p7, color);
42                }
43                gizmo_physics_core::ColliderShape::Sphere(s) => {
44                    let r = s.radius;
45                    let min = trans.position - Vec3::new(r, r, r);
46                    let max = trans.position + Vec3::new(r, r, r);
47                    gizmos.draw_box(min, max, color);
48                }
49                gizmo_physics_core::ColliderShape::ConvexHull(ch) => {
50                    let r = trans.rotation;
51                    let p = trans.position;
52                    for face in ch.faces.iter() {
53                        let p0 = p + r.mul_vec3(ch.vertices[face[0] as usize]);
54                        let p1 = p + r.mul_vec3(ch.vertices[face[1] as usize]);
55                        let p2 = p + r.mul_vec3(ch.vertices[face[2] as usize]);
56                        gizmos.draw_line(p0, p1, color);
57                        gizmos.draw_line(p1, p2, color);
58                        gizmos.draw_line(p2, p0, color);
59                    }
60                }
61                gizmo_physics_core::ColliderShape::Compound(shapes) => {
62                    for (local_t, sub_shape) in shapes {
63                        let world_pos = trans.position + trans.rotation.mul_vec3(local_t.position);
64                        let world_rot = trans.rotation * local_t.rotation;
65                        let sub_trans = gizmo_physics_core::Transform {
66                            position: world_pos,
67                            rotation: world_rot,
68                            scale: trans.scale,
69                            ..*trans
70                        };
71                        let temp_col = gizmo_physics_core::Collider::from_shape(*sub_shape.clone());
72                        draw_collider(&sub_trans, &temp_col, color, gizmos);
73                    }
74                }
75                gizmo_physics_core::ColliderShape::Plane(p) => {
76                    // Sonsuz zemin düzlemini büyük düz tel-ızgara olarak çiz ki hitbox görünür olsun.
77                    let n = p.normal.normalize();
78                    let up = if n.y.abs() < 0.99 {
79                        Vec3::new(0.0, 1.0, 0.0)
80                    } else {
81                        Vec3::new(1.0, 0.0, 0.0)
82                    };
83                    let t1 = n.cross(up).normalize();
84                    let t2 = n.cross(t1).normalize();
85                    // Izgara merkezini trans.position'ı düzleme izdüşürerek bul (düzlem: dot(n,x)=distance).
86                    let signed = n.dot(trans.position) - p.distance;
87                    let center = trans.position - n * signed;
88                    let half = 40.0_f32;
89                    let step = 4.0_f32;
90                    let n_lines = (half * 2.0 / step) as i32;
91                    for i in 0..=n_lines {
92                        let o = -half + i as f32 * step;
93                        gizmos.draw_line(center + t1 * o - t2 * half, center + t1 * o + t2 * half, color);
94                        gizmos.draw_line(center + t2 * o - t1 * half, center + t2 * o + t1 * half, color);
95                    }
96                }
97                _ => {
98                    let min = trans.position - Vec3::new(1.0, 1.0, 1.0);
99                    let max = trans.position + Vec3::new(1.0, 1.0, 1.0);
100                    gizmos.draw_box(min, max, color);
101                }
102            }
103        }
104
105        if let Some(q) = world.query::<(
106            &gizmo_physics_core::Transform,
107            &gizmo_physics_core::Collider,
108            &gizmo_physics_rigid::components::RigidBody,
109        )>() {
110            for (_, (trans, col, rb)) in q.iter() {
111                let color = if rb.is_static() {
112                    [0.5, 0.5, 0.5, 1.0]
113                } else if rb.is_sleeping {
114                    [0.9, 0.1, 0.1, 1.0]
115                } else {
116                    [0.1, 0.9, 0.1, 1.0]
117                };
118                draw_collider(trans, col, color, &mut gizmos);
119            }
120        }
121
122        if let Some(q) = world.query::<(
123            &gizmo_physics_core::Transform,
124            &gizmo_physics_core::Collider,
125            crate::core::query::Without<gizmo_physics_rigid::components::RigidBody>,
126        )>() {
127            for (_, (trans, col, _)) in q.iter() {
128                draw_collider(trans, col, [0.5, 0.5, 0.5, 1.0], &mut gizmos);
129            }
130        }
131
132        // --- Phase 5.5: Hitbox / Hurtbox Visuals ---
133        let draw_oriented_box = |trans: &gizmo_physics_core::Transform,
134                                 offset: gizmo_math::Vec3,
135                                 h: gizmo_math::Vec3,
136                                 color: [f32; 4],
137                                 gizmos: &mut crate::renderer::Gizmos| {
138            let p = trans.position + trans.rotation.mul_vec3(offset);
139            let r = trans.rotation;
140            let p0 = p + r.mul_vec3(Vec3::new(-h.x, -h.y, -h.z));
141            let p1 = p + r.mul_vec3(Vec3::new(h.x, -h.y, -h.z));
142            let p2 = p + r.mul_vec3(Vec3::new(h.x, h.y, -h.z));
143            let p3 = p + r.mul_vec3(Vec3::new(-h.x, h.y, -h.z));
144            let p4 = p + r.mul_vec3(Vec3::new(-h.x, -h.y, h.z));
145            let p5 = p + r.mul_vec3(Vec3::new(h.x, -h.y, h.z));
146            let p6 = p + r.mul_vec3(Vec3::new(h.x, h.y, h.z));
147            let p7 = p + r.mul_vec3(Vec3::new(-h.x, h.y, h.z));
148
149            gizmos.draw_line(p0, p1, color);
150            gizmos.draw_line(p1, p2, color);
151            gizmos.draw_line(p2, p3, color);
152            gizmos.draw_line(p3, p0, color);
153            gizmos.draw_line(p4, p5, color);
154            gizmos.draw_line(p5, p6, color);
155            gizmos.draw_line(p6, p7, color);
156            gizmos.draw_line(p7, p4, color);
157            gizmos.draw_line(p0, p4, color);
158            gizmos.draw_line(p1, p5, color);
159            gizmos.draw_line(p2, p6, color);
160            gizmos.draw_line(p3, p7, color);
161        };
162
163        if let Some(q) = world.query::<(&gizmo_physics_core::Transform, &gizmo_physics_core::components::Hitbox)>() {
164            for (_, (trans, hitbox)) in q.iter() {
165                if hitbox.active {
166                    draw_oriented_box(trans, hitbox.offset, hitbox.half_extents, [1.0, 0.0, 0.0, 1.0], &mut gizmos); // RED
167                }
168            }
169        }
170
171        if let Some(q) = world.query::<(&gizmo_physics_core::Transform, &gizmo_physics_core::components::Hurtbox)>() {
172            for (_, (trans, hurtbox)) in q.iter() {
173                draw_oriented_box(trans, hurtbox.offset, hurtbox.half_extents, [0.0, 1.0, 0.0, 1.0], &mut gizmos); // GREEN
174            }
175        }
176
177        let soft_color = [1.0, 0.4, 0.8, 1.0]; // Pinkish for soft body
178        #[cfg(feature = "physics-soft")]
179        if let Some(q) = world.query::<&gizmo_physics_soft::SoftBodyMesh>() {
180            for (_, sm) in q.iter() {
181                for elem in &sm.elements {
182                    let p0 = sm.nodes[elem.node_indices[0] as usize].position;
183                    let p1 = sm.nodes[elem.node_indices[1] as usize].position;
184                    let p2 = sm.nodes[elem.node_indices[2] as usize].position;
185                    let p3 = sm.nodes[elem.node_indices[3] as usize].position;
186
187                    // 6 edges of a tetrahedron
188                    gizmos.draw_line(p0, p1, soft_color);
189                    gizmos.draw_line(p0, p2, soft_color);
190                    gizmos.draw_line(p0, p3, soft_color);
191                    gizmos.draw_line(p1, p2, soft_color);
192                    gizmos.draw_line(p1, p3, soft_color);
193                    gizmos.draw_line(p2, p3, soft_color);
194                }
195            }
196        }
197
198        // --- Phase 6.1: Süspansiyon Raycast Çizgisi + Kuvvet Okları ---
199        #[cfg(feature = "physics-dynamics")]
200        if let Some(q) = world.query::<(
201            &gizmo_physics_core::Transform,
202            &gizmo_physics_dynamics::VehicleController,
203        )>() {
204            for (_, (trans, vehicle)) in q.iter() {
205                for wheel in &vehicle.wheels {
206                    let attach_world =
207                        trans.position + trans.rotation.mul_vec3(wheel.attachment_local_pos);
208                    let ray_dir = trans.rotation.mul_vec3(wheel.direction_local).normalize();
209                    let ray_end = attach_world
210                        + ray_dir
211                            * (wheel.suspension_rest_length
212                                + wheel.suspension_max_travel
213                                + wheel.radius);
214
215                    // Draw raycast maximum extent (Yellow line)
216                    gizmos.draw_line(attach_world, ray_end, [1.0, 1.0, 0.0, 1.0]);
217
218                    if wheel.is_grounded {
219                        if let Some(hit) = &wheel.ground_hit {
220                            // Kuvvet oku (Mavi) - sadece uzunluğu normalize edip görselleştirmek için / 10000 kullanıyoruz
221                            let force_dir = -ray_dir;
222                            let force_len = (wheel.suspension_force / 10000.0).clamp(0.1, 2.0);
223                            let arrow_end = hit.point + force_dir * force_len;
224                            gizmos.draw_line(hit.point, arrow_end, [0.0, 0.0, 1.0, 1.0]);
225
226                            // Mevcut süspansiyon uzunluğu + tekerlek merkezi çizgisi (Turuncu)
227                            let wheel_center = attach_world + ray_dir * wheel.suspension_length;
228                            gizmos.draw_line(wheel_center, hit.point, [1.0, 0.5, 0.0, 1.0]);
229                        }
230                    }
231                }
232            }
233        }
234
235        // (Ölü rigid `Vehicle` süspansiyon debug-draw bloğu kaldırıldı — o araç sistemi silindi;
236        // kanonik dynamics `VehicleController` debug-draw'u yukarıda.)
237
238        // --- Phase 6.2: Temas Normalleri ve Penetrasyon Derinliği ---
239        if let Some(phys_world) = world.get_resource::<gizmo_physics_rigid::world::PhysicsWorld>() {
240            for event in phys_world.collision_events() {
241                for contact in &event.contact_points {
242                    let p1 = contact.point;
243                    let p2 = contact.point + contact.normal * 0.5; // Normal arrow
244                    gizmos.draw_line(p1, p2, [1.0, 0.0, 0.0, 1.0]); // Red normal
245
246                    let p_pen = contact.point - contact.normal * contact.penetration;
247                    gizmos.draw_line(p1, p_pen, [1.0, 0.0, 1.0, 1.0]); // Magenta penetration depth
248                }
249            }
250        }
251        tracing::info!(
252            "physics_debug_system: gizmos lines count = {}",
253            gizmos.lines.len()
254        );
255    }
256}
257
258/// ECS'deki yeni yaratılmış Fiziksel Objeleri (RigidBody + Transform + Collider)
259/// GPU Physics çekirdeğinin otoyoluna (GpuPhysicsSystem::spheres_buffer) kaydeder.
260/// Statik collider'lar için ayrı sayaç. İlk 3 slot başlangıç collider'larına ayrılmıştır.
261static NEXT_STATIC_COLLIDER_SLOT: std::sync::atomic::AtomicU32 =
262    std::sync::atomic::AtomicU32::new(3);
263
264#[cfg(feature = "render")]
265pub fn gpu_physics_submit_system(world: &mut crate::core::World, renderer: &Renderer) {
266    use gizmo_physics_rigid::components::Velocity;
267
268    if let Some(physics) = &renderer.gpu_physics {
269        let mut unlinked_entities = Vec::new();
270        if let Some(q) = world.query::<(&RigidBody, &Transform, &Collider)>() {
271            let links = world.borrow::<GpuPhysicsLink>();
272            let velocities = world.borrow::<Velocity>();
273            for (e, (rb, trans, col)) in q.iter() {
274                if links.get(e).is_none() {
275                    let vel = velocities.get(e).copied().unwrap_or_default();
276                    unlinked_entities.push((e, *rb, *trans, col.clone(), vel));
277                }
278            }
279        }
280
281        let mut next_dynamic_id = world
282            .query::<&GpuPhysicsLink>()
283            .map(|q| q.iter().count() as u32)
284            .unwrap_or(0);
285
286        for (e, rb, trans, col, vel) in unlinked_entities {
287            if matches!(col.shape, ColliderShape::Plane(_)) {
288                // Statik engel — ayrı slot sayacı kullan
289                let slot =
290                    NEXT_STATIC_COLLIDER_SLOT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
291                if slot >= 100 {
292                    tracing::error!("[GpuPhysics] Statik collider slot limiti (100) aşıldı, collider atlanıyor.");
293                    NEXT_STATIC_COLLIDER_SLOT.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
294                    continue;
295                }
296
297                let gpu_col = gizmo_renderer::gpu_physics::GpuCollider {
298                    shape_type: match col.shape {
299                        ColliderShape::Plane(_) => 1,
300                        _ => 0, // Varsayılan Box (AABB)
301                    },
302                    _pad1: [0; 3],
303                    data1: match &col.shape {
304                        ColliderShape::Plane(p) => [p.normal.x, p.normal.y, p.normal.z, 0.0],
305                        ColliderShape::Box(b) => {
306                            let min = trans.position - b.half_extents;
307                            [min.x, min.y, min.z, 0.0]
308                        }
309                        _ => [0.0; 4],
310                    },
311                    data2: match &col.shape {
312                        ColliderShape::Plane(p) => [p.distance, 0.0, 0.0, 0.0],
313                        ColliderShape::Box(b) => {
314                            let max = trans.position + b.half_extents;
315                            [max.x, max.y, max.z, 0.0]
316                        }
317                        _ => [0.0; 4],
318                    },
319                };
320                physics.update_collider(&renderer.queue, slot, &gpu_col);
321            } else {
322                // Dinamik Kutu (AABB)
323                let id = next_dynamic_id;
324                next_dynamic_id += 1;
325
326                let (extents, offset) = match &col.shape {
327                    ColliderShape::Box(b) => ([b.half_extents.x, b.half_extents.y, b.half_extents.z], Vec3::ZERO),
328                    ColliderShape::Compound(shapes) => {
329                        if let Some((local_t, box_shape)) = shapes.first() {
330                            if let ColliderShape::Box(b) = &**box_shape {
331                                ([b.half_extents.x, b.half_extents.y, b.half_extents.z], local_t.position)
332                            } else {
333                                ([0.5, 0.5, 0.5], Vec3::ZERO)
334                            }
335                        } else {
336                            ([0.5, 0.5, 0.5], Vec3::ZERO)
337                        }
338                    }
339                    _ => ([0.5, 0.5, 0.5], Vec3::ZERO),
340                };
341
342                let world_pos = trans.position + trans.rotation.mul_vec3(offset);
343
344                let gpu_box = gizmo_renderer::gpu_physics::GpuBox {
345                    position: [world_pos.x, world_pos.y, world_pos.z],
346                    mass: rb.mass,
347                    velocity: [vel.linear.x, vel.linear.y, vel.linear.z],
348                    state: 0,
349                    rotation: [
350                        trans.rotation.x,
351                        trans.rotation.y,
352                        trans.rotation.z,
353                        trans.rotation.w,
354                    ],
355                    angular_velocity: [vel.angular.x, vel.angular.y, vel.angular.z],
356                    sleep_counter: if rb.is_sleeping { 60 } else { 0 },
357                    color: [0.3, 0.8, 1.0, 1.0],
358                    half_extents: extents,
359                    _pad: 0,
360                };
361                physics.update_box(&renderer.queue, id, &gpu_box);
362
363                world.add_component(world.get_entity(e).unwrap(), GpuPhysicsLink { id });
364            }
365        }
366    }
367}
368
369/// GPU'dan Asenkron (0ms) çekilen devasa Fizik lokasyon durumlarını,
370/// Ekrandaki objelerin render edilmesi için ECS'deki Transform'larına kopyalar.
371pub fn gpu_physics_readback_system(world: &mut crate::core::World, renderer: &Renderer) {
372    if let Some(physics) = &renderer.gpu_physics {
373        if let Some(gpu_data) = physics.poll_readback_data(&renderer.device) {
374            if let Some(mut q) =
375                world.query_mut::<(gizmo_core::prelude::Mut<Transform>, &GpuPhysicsLink, &gizmo_physics_core::Collider)>()
376            {
377                for (_, (mut trans, link, col)) in q.iter_mut() {
378                    let idx = link.id as usize;
379                    if idx < gpu_data.len() {
380                        let box_data = &gpu_data[idx];
381                        
382                        let offset = match &col.shape {
383                            gizmo_physics_core::ColliderShape::Compound(shapes) => {
384                                if let Some((local_t, _)) = shapes.first() {
385                                    local_t.position
386                                } else {
387                                    gizmo_math::Vec3::ZERO
388                                }
389                            }
390                            _ => gizmo_math::Vec3::ZERO,
391                        };
392
393                        let new_rot = gizmo_math::Quat::from_xyzw(
394                            box_data.rotation[0],
395                            box_data.rotation[1],
396                            box_data.rotation[2],
397                            box_data.rotation[3],
398                        );
399
400                        trans.position = gizmo_math::Vec3::new(
401                            box_data.position[0],
402                            box_data.position[1],
403                            box_data.position[2],
404                        ) - new_rot.mul_vec3(offset);
405                        
406                        trans.rotation = new_rot;
407                        trans.update_local_matrix();
408                    }
409                }
410            }
411        }
412    }
413}
414
415// Phase 7.1: Fluid-Rigid Coupling
416// Senkronize eder: GpuPhysicsLink sahibi objeleri FluidCollider buffer'ına yazar.
417pub fn cpu_physics_step_system(world: &crate::core::World, dt: f32) {
418    gizmo_physics_rigid::system::physics_step_system(world, dt);
419
420    #[cfg(feature = "physics-soft")]
421    {
422        // Obtain gravity from the PhysicsWorld if it exists
423        let gravity = world.get_resource::<gizmo_physics_rigid::world::PhysicsWorld>()
424            .map(|w| w.integrator.gravity)
425            .unwrap_or(gizmo_math::Vec3::new(0.0, -9.81, 0.0));
426            
427        gizmo_physics_soft::system::soft_body_step_system(world, dt, gravity);
428        gizmo_physics_soft::system::cloth_step_system(world, dt, gravity);
429        gizmo_physics_soft::system::rope_step_system(world, dt, gravity);
430    }
431}
432
433/// Schedulable wrapper around [`cpu_physics_step_system`].
434///
435/// [`crate::plugins::PhysicsPlugin`] adds this to the app schedule so the physics
436/// step runs automatically at the app's **fixed timestep** (the accumulator loop
437/// driven by `PhysicsTime`) — callers no longer hand-call `cpu_physics_step_system`
438/// every frame, and physics gets a stable fixed step for free.
439pub struct PhysicsStepSystem;
440
441impl gizmo_core::system::System for PhysicsStepSystem {
442    fn access_info(&self) -> gizmo_core::system::AccessInfo {
443        let mut info = gizmo_core::system::AccessInfo::new();
444        // Exclusive barrier — mirrors `physics_step_system`'s whole-World access.
445        info.is_exclusive = true;
446        info
447    }
448
449    fn run(&mut self, world: &crate::core::World, dt: f32) {
450        cpu_physics_step_system(world, dt);
451    }
452}
453
454#[cfg(test)]
455mod physics_step_tests {
456    use super::*;
457    use crate::core::World;
458    use gizmo_core::system::System;
459    use gizmo_physics_rigid::components::Velocity;
460    use gizmo_physics_rigid::world::PhysicsWorld;
461
462    #[test]
463    fn physics_step_system_advances_the_world_under_gravity() {
464        // Proves the schedulable wrapper actually steps physics (what PhysicsPlugin
465        // now runs automatically at the fixed timestep).
466        let mut world = World::new();
467        world.insert_resource(PhysicsWorld::new().with_gravity(Vec3::new(0.0, -9.81, 0.0)));
468
469        let body = world.spawn();
470        world.add_component(body, Transform::new(Vec3::new(0.0, 10.0, 0.0)));
471        world.add_component(body, RigidBody::new(1.0, true));
472        world.add_component(body, Velocity::default());
473        world.add_component(body, Collider::sphere(0.5));
474
475        let y0 = world.borrow::<Transform>().get(body.id()).unwrap().position.y;
476        let mut sys = PhysicsStepSystem;
477        for _ in 0..30 {
478            sys.run(&world, 1.0 / 60.0);
479        }
480        let y1 = world.borrow::<Transform>().get(body.id()).unwrap().position.y;
481        assert!(y1 < y0 - 0.5, "body must fall under gravity via the scheduled step: {y0} -> {y1}");
482    }
483}