1use crate::{
2 components::{RigidBody, Velocity},
3 integrator::Integrator,
4 solver::ConstraintSolver,
5};
6use gizmo_physics_core::broadphase::SpatialHash;
7use gizmo_physics_core::{CollisionEvent, ContactManifold, TriggerEvent};
8use gizmo_physics_core::raycast::{Ray, Raycast, RaycastHit};
9use gizmo_physics_core::components::{Collider, Transform};
10use gizmo_core::entity::Entity;
11
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
15pub enum ZoneShape {
16 Box {
17 min: gizmo_math::Vec3,
18 max: gizmo_math::Vec3,
19 },
20 Sphere {
21 center: gizmo_math::Vec3,
22 radius: f32,
23 },
24}
25
26impl ZoneShape {
27 pub fn contains(&self, p: gizmo_math::Vec3) -> bool {
28 match self {
29 ZoneShape::Box { min, max } => {
30 p.x >= min.x
31 && p.x <= max.x
32 && p.y >= min.y
33 && p.y <= max.y
34 && p.z >= min.z
35 && p.z <= max.z
36 }
37 ZoneShape::Sphere { center, radius } => {
38 (p - *center).length_squared() <= radius * radius
39 }
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
45pub struct GravityField {
46 pub shape: ZoneShape,
47 pub gravity: gizmo_math::Vec3,
48 pub falloff_radius: f32, pub priority: i32,
50}
51
52#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
53pub struct FluidZone {
54 pub shape: ZoneShape,
55 pub density: f32, pub viscosity: f32, pub linear_drag: f32, pub quadratic_drag: f32, }
60
61const PHYSICS_HZ: f32 = 240.0;
63const FIXED_DT: f32 = 1.0 / PHYSICS_HZ;
64const MAX_SUBSTEPS: u32 = 64; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
68pub enum Weather {
69 Sunny,
70 Rain,
71 Snow,
72}
73
74impl Default for Weather {
75 fn default() -> Self {
76 Self::Sunny
77 }
78}
79
80#[derive(Clone)]
82pub struct PhysicsStateSnapshot {
83 pub transforms: Vec<Transform>,
84 pub velocities: Vec<Velocity>,
85}
86
87#[derive(serde::Serialize, serde::Deserialize)]
89pub struct PhysicsWorld {
90 pub weather: Weather,
91
92 #[serde(skip)]
93 pub integrator: Integrator,
94 #[serde(skip)]
95 pub solver: ConstraintSolver,
96 #[serde(skip)]
97 pub spatial_hash: SpatialHash,
98 #[serde(skip)]
99 pub collision_events: Vec<CollisionEvent>,
100 #[serde(skip)]
101 pub trigger_events: Vec<TriggerEvent>,
102 #[serde(skip)]
103 pub fracture_events: Vec<gizmo_physics_core::FractureEvent>,
104 #[serde(skip)]
105 pub fracture_cache: crate::fracture::PreFracturedCache,
106 #[serde(skip)]
107 pub joints: Vec<crate::joints::Joint>,
108 #[serde(skip)]
109 pub joint_solver: crate::joints::JointSolver,
110
111 pub gravity_fields: Vec<GravityField>,
112 pub fluid_zones: Vec<FluidZone>,
113
114 #[serde(skip)]
115 pub(crate) contact_cache: HashMap<(Entity, Entity), (bool, Option<ContactManifold>)>,
116
117 pub accumulator: f32,
118 pub render_alpha: f32,
119
120 #[serde(skip)]
121 pub metrics: crate::island::PhysicsMetrics,
122
123 pub entities: Vec<Entity>,
125 pub rigid_bodies: Vec<RigidBody>,
126 pub transforms: Vec<Transform>,
127 pub velocities: Vec<Velocity>,
128 pub colliders: Vec<Collider>,
129 pub entity_index_map: HashMap<u32, usize>,
130
131 #[serde(skip)]
133 pub is_paused: bool,
134 #[serde(skip)]
135 pub step_once: bool,
136 #[serde(skip)]
137 pub rewind_requested: bool,
138 #[serde(skip)]
139 pub history: std::collections::VecDeque<PhysicsStateSnapshot>,
140 pub max_history_frames: usize,
141
142 #[serde(skip)]
143 pub watchlist: std::collections::HashSet<Entity>,
144}
145
146impl Default for PhysicsWorld {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152impl PhysicsWorld {
153 pub fn new() -> Self {
154 Self {
155 weather: Weather::Sunny,
156 integrator: Integrator::default(),
157 solver: ConstraintSolver::default(),
158 spatial_hash: SpatialHash::new(10.0),
159 collision_events: Vec::new(),
160 trigger_events: Vec::new(),
161 fracture_events: Vec::new(),
162 fracture_cache: crate::fracture::PreFracturedCache::new(),
163 joints: Vec::new(),
164 joint_solver: crate::joints::JointSolver::default(),
165 gravity_fields: Vec::new(),
166 fluid_zones: Vec::new(),
167
168
169 contact_cache: HashMap::new(),
170 accumulator: 0.0,
171 render_alpha: 1.0,
172 metrics: crate::island::PhysicsMetrics::default(),
173 entities: Vec::new(),
174 rigid_bodies: Vec::new(),
175 transforms: Vec::new(),
176 velocities: Vec::new(),
177 colliders: Vec::new(),
178 entity_index_map: HashMap::new(),
179 is_paused: false,
180 step_once: false,
181 rewind_requested: false,
182 history: std::collections::VecDeque::new(),
183 max_history_frames: 600, watchlist: std::collections::HashSet::new(),
185 }
186 }
187
188 pub fn with_gravity(mut self, gravity: gizmo_math::Vec3) -> Self {
189 self.integrator.gravity = gravity;
190 self
191 }
192
193
194 pub fn enable_gpu_compute(&mut self) {
195 }
196
197 pub fn with_cell_size(mut self, cell_size: f32) -> Self {
198 self.spatial_hash = SpatialHash::new(cell_size);
199 self
200 }
201
202 pub fn add_body(
205 &mut self,
206 entity: Entity,
207 rb: RigidBody,
208 t: Transform,
209 v: Velocity,
210 c: Collider,
211 ) {
212 let idx = self.entities.len();
213
214 let mut aabb = c.compute_aabb(t.position, t.rotation);
215 if rb.ccd_enabled {
216 let movement = v.linear * (1.0 / 60.0); let min_mov = aabb
218 .min
219 .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
220 let max_mov = aabb
221 .max
222 .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
223 aabb = gizmo_math::Aabb::new(min_mov, max_mov);
224 }
225 self.spatial_hash.insert(entity, aabb);
226
227 self.entities.push(entity);
228 self.rigid_bodies.push(rb);
229 self.transforms.push(t);
230 self.velocities.push(v);
231 self.colliders.push(c);
232 self.entity_index_map.insert(entity.id(), idx);
233 }
234
235 pub fn clear_bodies(&mut self) {
236 self.entities.clear();
237 self.rigid_bodies.clear();
238 self.transforms.clear();
239 self.velocities.clear();
240 self.colliders.clear();
241 self.entity_index_map.clear();
242 self.spatial_hash.clear();
243 }
244
245 pub fn sync_bodies<'a>(
246 &mut self,
247 incoming_bodies: impl Iterator<Item = &'a (Entity, RigidBody, Transform, Velocity, Collider)>,
248 ) {
249 let mut active_ids = std::collections::HashSet::new();
250
251 for (entity, rb, trans, vel, col) in incoming_bodies {
252 let e_id = entity.id();
253 active_ids.insert(e_id);
254
255 if let Some(&idx) = self.entity_index_map.get(&e_id) {
256 self.rigid_bodies[idx] = *rb;
258 self.transforms[idx] = *trans;
259 self.velocities[idx] = *vel;
260
261 self.colliders[idx] = col.clone();
263
264 let mut aabb = col.compute_aabb(trans.position, trans.rotation);
266 if rb.ccd_enabled {
267 let movement = vel.linear * (1.0 / 60.0);
268 let min_mov = aabb
269 .min
270 .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
271 let max_mov = aabb
272 .max
273 .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
274 aabb = gizmo_math::Aabb::new(min_mov, max_mov);
275 }
276 self.spatial_hash.update(*entity, aabb);
277 } else {
278 self.add_body(*entity, *rb, *trans, *vel, col.clone());
280 }
281 }
282
283 let mut i = 0;
285 while i < self.entities.len() {
286 if !active_ids.contains(&self.entities[i].id()) {
287 self.remove_body_at(i);
288 } else {
289 i += 1;
290 }
291 }
292 }
293
294 pub fn remove_body_at(&mut self, idx: usize) {
295 let last_idx = self.entities.len() - 1;
296 let entity = self.entities[idx];
297
298 self.spatial_hash.remove(entity);
299 self.entity_index_map.remove(&entity.id());
300
301 if idx != last_idx {
302 let last_entity = self.entities[last_idx];
303
304 self.entities.swap(idx, last_idx);
305 self.rigid_bodies.swap(idx, last_idx);
306 self.transforms.swap(idx, last_idx);
307 self.velocities.swap(idx, last_idx);
308 self.colliders.swap(idx, last_idx);
309
310 self.entity_index_map.insert(last_entity.id(), idx);
311 }
312
313 self.entities.pop();
314 self.rigid_bodies.pop();
315 self.transforms.pop();
316 self.velocities.pop();
317 self.colliders.pop();
318 }
319
320 pub fn step(
325 &mut self,
326
327
328 dt: f32,
329 ) -> Result<(), gizmo_physics_core::GizmoError> {
330 if self.rewind_requested {
331 self.rewind_requested = false;
332 if let Some(snapshot) = self.history.pop_back() {
333 if snapshot.transforms.len() == self.transforms.len() {
334 self.transforms = snapshot.transforms;
335 self.velocities = snapshot.velocities;
336 tracing::info!("Physics rewound by 1 frame!");
337 } else {
338 tracing::warn!("Cannot rewind: Entity count changed.");
339 }
340 }
341 return Ok(());
342 }
343
344 if self.is_paused && !self.step_once {
345 self.collision_events.clear();
347 self.trigger_events.clear();
348 self.fracture_events.clear();
349 return Ok(());
350 }
351
352 let frame_dt = if self.step_once {
354 self.step_once = false;
355 self.accumulator = 0.0; FIXED_DT
357 } else {
358 dt.min(0.25) };
360
361 self.collision_events.clear();
363 self.trigger_events.clear();
364 self.fracture_events.clear();
365
366 self.accumulator += frame_dt;
368
369 let mut steps = 0u32;
370 while self.accumulator >= FIXED_DT && steps < MAX_SUBSTEPS {
371 self.step_internal(FIXED_DT)?;
372 self.accumulator -= FIXED_DT;
373 steps += 1;
374 }
375
376 self.render_alpha = self.accumulator / FIXED_DT;
378
379 self.history.push_back(PhysicsStateSnapshot {
381 transforms: self.transforms.clone(),
382 velocities: self.velocities.clone(),
383 });
384 if self.history.len() > self.max_history_frames {
385 self.history.pop_front();
386 }
387
388 Ok(())
389 }
390
391 fn step_internal(
395 &mut self,
396
397
398 dt: f32,
399 ) -> Result<(), gizmo_physics_core::GizmoError> {
400 let _initial_energy = if cfg!(debug_assertions) {
402 self.calculate_total_energy()
403 } else {
404 0.0
405 };
406
407 self.velocity_integration_step(dt)?;
409
410 self.broadphase_step(dt);
414
415 let manifolds = self.narrowphase_and_collision_step(dt);
417
418 self.constraint_solve_step(manifolds, dt);
420
421 self.position_integration_step(dt)?;
423
424 if cfg!(debug_assertions) {
426 let _final_energy = self.calculate_total_energy();
427 }
428
429 Ok(())
430 }
431
432 pub fn collision_events(&self) -> &[CollisionEvent] {
434 &self.collision_events
435 }
436
437 pub fn trigger_events(&self) -> &[TriggerEvent] {
439 &self.trigger_events
440 }
441
442 pub fn apply_impulse(
444 &self,
445 rb: &RigidBody,
446 transform: &Transform,
447 vel: &mut Velocity,
448 impulse: gizmo_math::Vec3,
449 point: gizmo_math::Vec3,
450 ) {
451 Integrator::apply_impulse_at_point(rb, transform, vel, impulse, point);
452 }
453
454 pub fn apply_force(
456 &self,
457 rb: &RigidBody,
458 vel: &mut Velocity,
459 force: gizmo_math::Vec3,
460 dt: f32,
461 ) {
462 Integrator::apply_force(rb, vel, force, dt);
463 }
464
465 pub fn raycast(&self, ray: &Ray, max_distance: f32) -> Option<RaycastHit> {
467 let mut closest_hit: Option<RaycastHit> = None;
468 let mut closest_distance = max_distance;
469
470 let potential_hits = self
471 .spatial_hash
472 .query_ray(ray.origin, ray.direction, max_distance);
473
474 for (entity, _aabb_t) in potential_hits {
475 if let Some(&i) = self.entity_index_map.get(&entity.id()) {
476 let transform = &self.transforms[i];
477 let collider = &self.colliders[i];
478
479 if let Some((distance, normal)) =
481 Raycast::ray_shape(ray, &collider.shape, transform)
482 {
483 if distance < closest_distance {
484 closest_distance = distance;
485 closest_hit = Some(RaycastHit {
486 entity,
487 point: ray.point_at(distance),
488 normal,
489 distance,
490 });
491 }
492 }
493 }
494 }
495
496 closest_hit
497 }
498
499 pub fn raycast_all(&self, ray: &Ray, max_distance: f32) -> Vec<RaycastHit> {
501 let mut hits = Vec::new();
502
503 let potential_hits = self
504 .spatial_hash
505 .query_ray(ray.origin, ray.direction, max_distance);
506
507 for (entity, _aabb_t) in potential_hits {
508 if let Some(&i) = self.entity_index_map.get(&entity.id()) {
509 let transform = &self.transforms[i];
510 let collider = &self.colliders[i];
511
512 if let Some((distance, normal)) =
514 Raycast::ray_shape(ray, &collider.shape, transform)
515 {
516 hits.push(RaycastHit {
517 entity,
518 point: ray.point_at(distance),
519 normal,
520 distance,
521 });
522 }
523 }
524 }
525
526 hits.sort_by(|a, b| a.distance.total_cmp(&b.distance));
528 hits
529 }
530
531 pub fn trigger_snapshot(&self, reason: &str) {
533 tracing::error!("Creating physics snapshot due to: {}", reason);
534 #[cfg(target_arch = "wasm32")]
535 let timestamp = web_time::SystemTime::now()
536 .duration_since(web_time::SystemTime::UNIX_EPOCH)
537 .unwrap()
538 .as_secs();
539 #[cfg(not(target_arch = "wasm32"))]
540 let timestamp = std::time::SystemTime::now()
541 .duration_since(std::time::SystemTime::UNIX_EPOCH)
542 .unwrap()
543 .as_secs();
544 let filename = format!("physics_snapshot_{}.json", timestamp);
545
546 match std::fs::File::create(&filename) {
547 Ok(file) => {
548 if let Err(e) = serde_json::to_writer_pretty(file, self) {
549 tracing::error!("Failed to serialize physics snapshot: {:?}", e);
550 } else {
551 tracing::info!("Physics snapshot successfully saved to {}", filename);
552 }
553 }
554 Err(e) => {
555 tracing::error!("Failed to create snapshot file {}: {:?}", filename, e);
556 }
557 }
558 }
559
560 pub fn calculate_total_energy(&self) -> f32 {
562 let default_gravity = self.integrator.gravity;
563 let mut total_energy = 0.0;
564
565 for i in 0..self.entities.len() {
566 let rb = &self.rigid_bodies[i];
567 let vel = &self.velocities[i];
568 let trans = &self.transforms[i];
569
570 if rb.is_dynamic() && !rb.is_sleeping {
571 let ke_linear = 0.5 * rb.mass * vel.linear.length_squared();
573
574 let ke_angular = 0.5
577 * (rb.local_inertia.x * vel.angular.x * vel.angular.x
578 + rb.local_inertia.y * vel.angular.y * vel.angular.y
579 + rb.local_inertia.z * vel.angular.z * vel.angular.z);
580
581 let pe = if rb.use_gravity {
583 -rb.mass * default_gravity.dot(trans.position)
584 } else {
585 0.0
586 };
587
588 total_energy += ke_linear + ke_angular + pe;
589 }
590 }
591 total_energy
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use gizmo_core::entity::Entity;
599 use gizmo_math::Vec3;
600
601 #[test]
602 fn test_physics_world_creation() {
603 let world = PhysicsWorld::new();
604 assert_eq!(world.integrator.gravity, Vec3::new(0.0, -9.81, 0.0));
605 }
606
607 #[test]
608 fn test_physics_step() {
609 let mut world = PhysicsWorld::new();
610
611 let entity = Entity::new(1, 0);
612 let rb = RigidBody::default();
613 let transform = Transform::new(Vec3::new(0.0, 10.0, 0.0));
614 let vel = Velocity::default();
615 let collider = Collider::sphere(1.0);
616
617 world.add_body(entity, rb, transform, vel, collider);
618
619 for _ in 0..60 {
621 let _ = world.step(1.0 / 60.0);
622 }
623
624 assert!(world.transforms[0].position.y < 10.0);
626 }
627
628 #[test]
629 fn test_high_stack_stability() {
630 let mut world = PhysicsWorld::new();
631 world.solver.iterations = 30;
633
634 let mut ground_rb = RigidBody::default();
636 ground_rb.body_type = crate::components::rigid_body::BodyType::Static;
637 ground_rb.wake_up();
638 world.add_body(
639 Entity::new(0, 0),
640 ground_rb,
641 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
642 Velocity::default(),
643 Collider::box_collider(Vec3::new(50.0, 0.5, 50.0)),
644 );
645
646 let box_count = 10;
648 let box_size = 1.0;
649 let half_size = box_size / 2.0;
650
651 for i in 1..=box_count {
652 let mut rb = RigidBody::new(1.0, 0.5, 0.5, true);
653 rb.wake_up(); let y_pos = half_size + (i - 1) as f32 * box_size;
656
657 world.add_body(
658 Entity::new(i, 0),
659 rb,
660 Transform::new(Vec3::new(0.0, y_pos, 0.0)),
661 Velocity::default(),
662 Collider::box_collider(Vec3::new(half_size, half_size, half_size)),
663 );
664 }
665
666 for i in 0..600 {
668 let _ = world.step(1.0 / 60.0);
669 if i % 60 == 0 {
670 println!("Frame {}: Y={}, X={}, Z={}", i, world.transforms[10].position.y, world.transforms[10].position.x, world.transforms[10].position.z);
671 }
672 }
673
674 let top_box_idx = box_count as usize; let top_box_pos = world.transforms[top_box_idx].position;
678
679 assert!(
681 top_box_pos.x.abs() < 0.1,
682 "Top box slid too much on X axis: {}",
683 top_box_pos.x
684 );
685 assert!(
686 top_box_pos.z.abs() < 0.1,
687 "Top box slid too much on Z axis: {}",
688 top_box_pos.z
689 );
690
691 let expected_y = half_size + (box_count - 1) as f32 * box_size;
693 let y_error = (top_box_pos.y - expected_y).abs();
694 assert!(
695 y_error < 0.1,
696 "Top box sunk or bounced too much. Expected Y: {}, Actual Y: {}",
697 expected_y,
698 top_box_pos.y
699 );
700 }
701
702 #[test]
703 fn test_ccd_tunneling_prevention() {
704 let mut world = PhysicsWorld::new();
705 world.integrator.gravity = Vec3::ZERO;
707
708 let mut wall_rb = RigidBody::new_static();
710 wall_rb.wake_up();
711 world.add_body(
712 Entity::new(0, 0),
713 wall_rb,
714 Transform::new(Vec3::new(0.0, 0.0, 0.0)),
715 Velocity::default(),
716 Collider::box_collider(Vec3::new(0.1, 5.0, 5.0)),
717 );
718
719 let mut bullet_rb = RigidBody::new(1.0, 0.0, 0.0, false);
721 bullet_rb.ccd_enabled = true;
722 bullet_rb.wake_up();
723
724 world.add_body(
727 Entity::new(1, 0),
728 bullet_rb,
729 Transform::new(Vec3::new(-5.0, 0.0, 0.0)),
730 Velocity::new(Vec3::new(1200.0, 0.0, 0.0)),
731 Collider::sphere(0.2),
732 );
733
734 let _ = world.step(1.0 / 60.0);
736
737 let bullet_pos = world.transforms[1].position;
738 let bullet_vel = world.velocities[1].linear;
739
740 assert!(
744 bullet_pos.x <= 0.0,
745 "TUNNELING FAILED! Bullet phased through the wall. Position: {}",
746 bullet_pos.x
747 );
748 assert!(
750 bullet_vel.x <= 0.01,
751 "Bullet did not lose velocity after hitting the wall. Vel: {}",
752 bullet_vel.x
753 );
754 }
755
756 #[test]
757 fn test_coulomb_friction_and_sleeping() {
758 let mut world = PhysicsWorld::new();
759 world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
761
762 let mut ground_rb = RigidBody::new_static();
764 ground_rb.friction = 0.5;
765 ground_rb.wake_up();
766 world.add_body(
767 Entity::new(0, 0),
768 ground_rb,
769 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
770 Velocity::default(),
771 Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
772 );
773
774 let mut box_a = RigidBody::new(1.0, 0.0, 0.1, true);
776 box_a.wake_up();
777 world.add_body(
778 Entity::new(1, 0),
779 box_a,
780 Transform::new(Vec3::new(0.0, 0.5, -2.0)),
781 Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
782 Collider::box_collider(Vec3::splat(0.5)),
783 );
784
785 let mut box_b = RigidBody::new(1.0, 0.0, 0.8, true);
787 box_b.wake_up();
788 world.add_body(
789 Entity::new(2, 0),
790 box_b,
791 Transform::new(Vec3::new(0.0, 0.5, 2.0)),
792 Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
793 Collider::box_collider(Vec3::splat(0.5)),
794 );
795
796 for _ in 0..300 {
798 let _ = world.step(1.0 / 60.0);
799 }
800
801 let pos_a = world.transforms[1].position;
802 let pos_b = world.transforms[2].position;
803 let sleep_a = world.rigid_bodies[1].is_sleeping;
804 let sleep_b = world.rigid_bodies[2].is_sleeping;
805
806 assert!(
808 pos_b.x < pos_a.x,
809 "High friction box should travel less. Pos A: {}, Pos B: {}",
810 pos_a.x,
811 pos_b.x
812 );
813
814 assert!(
816 sleep_a,
817 "Low friction box did not enter sleeping mode!"
818 );
819 assert!(
820 sleep_b,
821 "High friction box did not enter sleeping mode!"
822 );
823 }
824
825 #[test]
826 fn test_car_simulation() {
827 use crate::joints::data::{Joint, JointData, HingeJointData};
828
829 let mut world = PhysicsWorld::new();
830 world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
832
833 let mut ground_rb = RigidBody::new_static();
835 ground_rb.friction = 0.8;
836 ground_rb.wake_up();
837 world.add_body(
838 Entity::new(0, 0),
839 ground_rb,
840 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
841 Velocity::default(),
842 Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
843 );
844
845 let mut chassis_rb = RigidBody::new(1000.0, 0.1, 0.5, true);
848 chassis_rb.wake_up();
849 let chassis_col = Collider::box_collider(Vec3::new(1.0, 0.5, 2.0)); chassis_rb.update_inertia_from_collider(&chassis_col);
851 let chassis_entity = Entity::new(1, 0);
852 let chassis_pos = Vec3::new(0.0, 1.5, 0.0);
853 world.add_body(
854 chassis_entity,
855 chassis_rb,
856 Transform::new(chassis_pos),
857 Velocity::default(),
858 chassis_col,
859 );
860
861 let wheel_radius = 0.5;
863 let mut wheel_rb = RigidBody::new(50.0, 0.1, 0.9, true); wheel_rb.wake_up();
865 let wheel_col = Collider::sphere(wheel_radius);
866 wheel_rb.update_inertia_from_collider(&wheel_col);
867
868 let wheel_offsets = vec![
869 Vec3::new(-1.2, -0.2, 1.5), Vec3::new(1.2, -0.2, 1.5), Vec3::new(-1.2, -0.2, -1.5), Vec3::new(1.2, -0.2, -1.5), ];
874
875 let mut wheel_entities = Vec::new();
876
877 for (i, offset) in wheel_offsets.iter().enumerate() {
878 let wheel_entity = Entity::new(2 + i as u32, 0);
879 wheel_entities.push(wheel_entity);
880
881 world.add_body(
882 wheel_entity,
883 wheel_rb.clone(),
884 Transform::new(chassis_pos + *offset),
885 Velocity::default(),
886 wheel_col.clone(),
887 );
888
889 let is_rear = i >= 2;
891 let hinge_data = HingeJointData {
892 axis: Vec3::X, use_limits: false,
894 lower_limit: 0.0,
895 upper_limit: 0.0,
896 use_motor: is_rear, motor_target_velocity: if is_rear { 10.0 } else { 0.0 }, motor_max_force: if is_rear { 10000.0 } else { 0.0 }, current_angle: 0.0,
900 };
901
902 let joint = Joint {
903 entity_a: chassis_entity,
904 entity_b: wheel_entity,
905 local_anchor_a: *offset, local_anchor_b: Vec3::ZERO, break_force: f32::MAX, break_torque: f32::MAX,
909 is_broken: false,
910 collision_enabled: false, data: JointData::Hinge(hinge_data),
912 };
913
914 world.joints.push(joint);
915 }
916 for _ in 0..300 {
919 let _ = world.step(1.0 / 60.0);
920 }
921 let final_chassis_pos = world.transforms[1].position;
923
924 assert!(
926 final_chassis_pos.z > 3.0,
927 "Araba yeterince ileri gidemedi! Motor veya sürtünme çalışmıyor. Z pozisyonu: {}",
928 final_chassis_pos.z
929 );
930
931 assert!(
934 final_chassis_pos.y > 0.5 && final_chassis_pos.y < 2.0,
935 "Araba devrildi, uçtu veya yere battı! Y pozisyonu: {}",
936 final_chassis_pos.y
937 );
938
939 assert!(
941 final_chassis_pos.x.abs() < 1.0,
942 "Araba düz gidemedi, sağa sola savruldu! X pozisyonu: {}",
943 final_chassis_pos.x
944 );
945 }
946}