1use alloc::vec::Vec;
2
3use gp3d::{BodyDesc, BodyHandle, Motion};
4use he3d_core::{Mesh, ObjectId, Scene, Transform};
5use he3d_math::Vec3;
6
7use crate::StepResult;
8
9const DEFAULT_GRAVITY: Vec3 = Vec3::new(0.0, -9.8, 0.0);
12
13pub struct PhysicsWorld {
25 scene: gp3d::PhysicsScene,
26 bodies: Vec<(ObjectId, BodyHandle)>,
27}
28
29impl PhysicsWorld {
30 #[must_use]
60 pub fn new() -> Self {
61 let mut scene = gp3d::PhysicsScene::new();
62 scene.set_gravity(DEFAULT_GRAVITY);
63 Self {
64 scene,
65 bodies: Vec::new(),
66 }
67 }
68
69 pub fn set_gravity(&mut self, gravity: Vec3) {
71 self.scene.set_gravity(gravity);
72 }
73
74 #[must_use]
76 pub fn gravity(&self) -> Vec3 {
77 self.scene.gravity()
78 }
79
80 pub fn add_dynamic_box(
83 &mut self,
84 object: ObjectId,
85 half_extents: Vec3,
86 transform: Transform,
87 ) -> bool {
88 self.add_box(object, half_extents, transform, Motion::Dynamic)
89 }
90
91 pub fn add_static_box(
94 &mut self,
95 object: ObjectId,
96 half_extents: Vec3,
97 transform: Transform,
98 ) -> bool {
99 self.add_box(object, half_extents, transform, Motion::Static)
100 }
101
102 pub fn add_kinematic_box(
105 &mut self,
106 object: ObjectId,
107 half_extents: Vec3,
108 transform: Transform,
109 ) -> bool {
110 self.add_box(object, half_extents, transform, Motion::Kinematic)
111 }
112
113 fn add_box(
117 &mut self,
118 object: ObjectId,
119 half_extents: Vec3,
120 transform: Transform,
121 motion: Motion,
122 ) -> bool {
123 if self.handle(object).is_some() {
124 return false;
125 }
126 let mut desc = BodyDesc::default();
127 desc.motion = motion;
128 desc.collider = gp3d::Collider::box_collider(half_extents);
129 desc.position = transform.position;
130 desc.orientation = transform.orientation;
131 match self.scene.add_body(desc) {
132 Ok(handle) => {
133 self.bodies.push((object, handle));
134 true
135 }
136 Err(_) => false,
137 }
138 }
139
140 pub fn add_static_mesh(&mut self, object: ObjectId, mesh: &Mesh, transform: Transform) -> bool {
146 if self.handle(object).is_some() {
147 return false;
148 }
149 let vertices = mesh.vertices().to_vec();
150 if vertices.is_empty() || vertices.len() % 3 != 0 {
151 return false;
152 }
153 match self
154 .scene
155 .add_static_mesh(vertices, transform.position, transform.orientation)
156 {
157 Ok(handle) => {
158 self.bodies.push((object, handle));
159 true
160 }
161 Err(_) => false,
162 }
163 }
164
165 pub fn set_transform(&mut self, object: ObjectId, transform: Transform) -> bool {
168 match self.handle(object) {
169 Some(handle) => {
170 self
171 .scene
172 .set_transform(handle, transform.position, transform.orientation)
173 }
174 None => false,
175 }
176 }
177
178 #[must_use]
181 pub fn transform(&self, object: ObjectId) -> Option<Transform> {
182 let handle = self.handle(object)?;
183 let position = self.scene.position(handle)?;
184 let orientation = self.scene.orientation(handle)?;
185 Some(Transform::from_position_orientation(position, orientation))
186 }
187
188 pub fn set_material(&mut self, object: ObjectId, friction: f32, restitution: f32) -> bool {
191 match self.handle(object) {
192 Some(handle) => self.scene.set_material(handle, friction, restitution),
193 None => false,
194 }
195 }
196
197 pub fn set_damping(&mut self, object: ObjectId, linear: f32, angular: f32) -> bool {
199 let Some(handle) = self.handle(object) else {
200 return false;
201 };
202 self.scene.set_linear_damping(handle, linear);
203 self.scene.set_angular_damping(handle, angular);
204 true
205 }
206
207 #[must_use]
210 pub fn has_contacts(&self, object: ObjectId) -> bool {
211 match self.handle(object) {
212 Some(handle) => self.scene.has_contacts(handle),
213 None => false,
214 }
215 }
216
217 pub fn remove_body(&mut self, object: ObjectId) -> bool {
220 let Some(position) = self.bodies.iter().position(|(id, _)| *id == object) else {
221 return false;
222 };
223 let (_, handle) = self.bodies.swap_remove(position);
224 self.scene.remove_body(handle)
225 }
226
227 #[must_use]
230 pub fn has_body(&self, object: ObjectId) -> bool {
231 self.handle(object).is_some()
232 }
233
234 pub fn set_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
236 match self.handle(object) {
237 Some(handle) => self.scene.set_velocity(handle, velocity),
238 None => false,
239 }
240 }
241
242 #[must_use]
245 pub fn velocity(&self, object: ObjectId) -> Vec3 {
246 match self.handle(object) {
247 Some(handle) => self.scene.velocity(handle),
248 None => Vec3::ZERO,
249 }
250 }
251
252 pub fn set_angular_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
254 match self.handle(object) {
255 Some(handle) => self.scene.set_angular_velocity(handle, velocity),
256 None => false,
257 }
258 }
259
260 #[must_use]
263 pub fn angular_velocity(&self, object: ObjectId) -> Vec3 {
264 match self.handle(object) {
265 Some(handle) => self.scene.angular_velocity(handle),
266 None => Vec3::ZERO,
267 }
268 }
269
270 pub fn add_force(&mut self, object: ObjectId, force: Vec3) -> bool {
272 match self.handle(object) {
273 Some(handle) => self.scene.add_force(handle, force),
274 None => false,
275 }
276 }
277
278 pub fn add_torque(&mut self, object: ObjectId, torque: Vec3) -> bool {
280 match self.handle(object) {
281 Some(handle) => self.scene.add_torque(handle, torque),
282 None => false,
283 }
284 }
285
286 pub fn add_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
288 match self.handle(object) {
289 Some(handle) => self.scene.add_impulse(handle, impulse),
290 None => false,
291 }
292 }
293
294 pub fn add_angular_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
296 match self.handle(object) {
297 Some(handle) => self.scene.add_angular_impulse(handle, impulse),
298 None => false,
299 }
300 }
301
302 pub fn step(&mut self, delta_time: f32) -> StepResult {
308 self.scene.step(delta_time, 4)
309 }
310
311 pub fn synchronize(&mut self, scene: &mut Scene) {
314 for &(object, handle) in &self.bodies {
315 let Some(render_object) = scene.objects.get_mut(object) else {
316 continue;
317 };
318 let Some(position) = self.scene.position(handle) else {
319 continue;
320 };
321 let Some(orientation) = self.scene.orientation(handle) else {
322 continue;
323 };
324 render_object.transform.position = position;
325 render_object.transform.orientation = orientation;
326 }
327 }
328
329 fn handle(&self, object: ObjectId) -> Option<BodyHandle> {
330 self
331 .bodies
332 .iter()
333 .find(|(id, _)| *id == object)
334 .map(|(_, handle)| *handle)
335 }
336}
337
338impl Default for PhysicsWorld {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use alloc::vec;
347
348 use super::*;
349 use he3d_core::{ColorRgba8, Material, Mesh, RenderObject, Rgb, Texture};
350 use he3d_math::Quat;
351
352 const DT: f32 = 1.0 / 60.0;
353
354 fn spawn_box(scene: &mut Scene, position: Vec3) -> ObjectId {
355 let mesh = scene
356 .meshes
357 .insert(Mesh::cube(1.0, 1.0, 1.0).expect("cube"));
358 let solid = scene.textures.insert(Texture::solid(ColorRgba8::WHITE));
359 scene.spawn(RenderObject {
360 mesh,
361 texture: Some(solid),
362 transform: Transform::from_position(position),
363 material: Material::new(Rgb::WHITE),
364 visible: true,
365 })
366 }
367
368 fn spawn_quad_floor(scene: &mut Scene, half: f32) -> (ObjectId, Mesh) {
371 let h = half;
372 let quad = Mesh::from_vertices(vec![
373 Vec3::new(-h, 0.0, -h),
374 Vec3::new(h, 0.0, h),
375 Vec3::new(h, 0.0, -h),
376 Vec3::new(-h, 0.0, -h),
377 Vec3::new(-h, 0.0, h),
378 Vec3::new(h, 0.0, h),
379 ])
380 .expect("quad");
381 let mesh_id = scene.meshes.insert(quad.clone());
382 let solid = scene
383 .textures
384 .insert(Texture::solid(ColorRgba8::from_rgb(77, 179, 77)));
385 let object = scene.spawn(RenderObject {
386 mesh: mesh_id,
387 texture: Some(solid),
388 transform: Transform::default(),
389 material: Material::new(Rgb::WHITE),
390 visible: true,
391 });
392 (object, quad)
393 }
394
395 fn run(world: &mut PhysicsWorld, steps: usize) {
396 for _ in 0..steps {
397 assert_eq!(world.step(DT), StepResult::Completed);
398 }
399 }
400
401 #[test]
402 fn registration_and_removal() {
403 let mut scene = Scene::new();
404 let cube = spawn_box(&mut scene, Vec3::ZERO);
405 let mut world = PhysicsWorld::new();
406
407 assert!(world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
408 assert!(world.has_body(cube));
409 assert!(!world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
411 assert!(!world.add_static_box(cube, Vec3::splat(0.5), Transform::default()));
412
413 assert!(world.remove_body(cube));
414 assert!(!world.has_body(cube));
415 assert!(!world.remove_body(cube));
416 assert!(!world.add_force(cube, Vec3::X));
417 }
418
419 #[test]
420 fn transform_and_velocity_passthrough() {
421 let mut scene = Scene::new();
422 let cube = spawn_box(&mut scene, Vec3::new(0.0, 1.0, 0.0));
423 let mut world = PhysicsWorld::new();
424 world.add_dynamic_box(
425 cube,
426 Vec3::splat(0.5),
427 Transform::from_position(Vec3::new(0.0, 1.0, 0.0)),
428 );
429
430 assert!(world.set_velocity(cube, Vec3::new(2.0, 0.0, 0.0)));
431 assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
432
433 let teleport =
435 Transform::from_position_orientation(Vec3::new(3.0, 4.0, 5.0), Quat::from_rotation_y(1.0));
436 assert!(world.set_transform(cube, teleport));
437 assert_eq!(world.transform(cube), Some(teleport));
438 assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
439
440 assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 1.0)));
441 assert_eq!(world.angular_velocity(cube), Vec3::new(0.0, 0.0, 1.0));
442
443 let stale = spawn_box(&mut scene, Vec3::ZERO);
445 let _ = scene.objects.remove(stale);
446 assert!(!world.set_transform(stale, Transform::default()));
447 assert_eq!(world.transform(stale), None);
448 }
449
450 #[test]
451 fn gravity_pulls_dynamic_body_down() {
452 let mut scene = Scene::new();
453 let cube = spawn_box(&mut scene, Vec3::new(0.0, 10.0, 0.0));
454 let mut world = PhysicsWorld::new();
455 world.add_dynamic_box(
456 cube,
457 Vec3::splat(0.5),
458 Transform::from_position(Vec3::new(0.0, 10.0, 0.0)),
459 );
460 run(&mut world, 60);
461 world.synchronize(&mut scene);
462 let y = scene.objects.get(cube).unwrap().transform.position.y;
463 assert!(y < 9.0, "body should have fallen, got y={y}");
464 assert!(y > 0.0, "body should not have fallen through, got y={y}");
465 }
466
467 #[test]
468 fn cube_rests_on_static_mesh_ground() {
469 let mut scene = Scene::new();
470 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
471 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
472 let mut world = PhysicsWorld::new();
473 world.add_dynamic_box(
474 cube,
475 Vec3::splat(0.5),
476 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
477 );
478 assert!(world.add_static_mesh(
479 floor,
480 &floor_mesh,
481 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
482 ));
483
484 run(&mut world, 300);
485 world.synchronize(&mut scene);
486 let y = scene.objects.get(cube).unwrap().transform.position.y;
487 assert!(
488 (y - 0.1).abs() < 0.05,
489 "cube should rest near y=0.1, got y={y}"
490 );
491 }
492
493 #[test]
494 fn restitution_bounces_on_fast_impact() {
495 let mut scene = Scene::new();
496 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
497 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 6.0);
498 let mut world = PhysicsWorld::new();
499 world.add_dynamic_box(
500 cube,
501 Vec3::splat(0.5),
502 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
503 );
504 world.set_material(cube, 0.5, 0.5);
505 world.add_static_mesh(
506 floor,
507 &floor_mesh,
508 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
509 );
510
511 let mut max_vy = 0.0f32;
512 for _ in 0..120 {
513 world.step(DT);
514 max_vy = max_vy.max(world.velocity(cube).y);
515 }
516 assert!(max_vy > 0.5, "fast impact must bounce, got max_vy={max_vy}");
517 }
518
519 #[test]
520 fn force_acts_for_one_step_then_is_cleared() {
521 let mut scene = Scene::new();
522 let cube = spawn_box(&mut scene, Vec3::ZERO);
523 let mut world = PhysicsWorld::new();
524 world.set_gravity(Vec3::ZERO);
525 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
526
527 assert!(world.add_force(cube, Vec3::new(60.0, 0.0, 0.0)));
528 assert_eq!(world.step(DT), StepResult::Completed);
529 let after_force = world.velocity(cube).x;
530 assert!(after_force > 0.9, "got {after_force}");
531
532 assert_eq!(world.step(DT), StepResult::Completed);
533 let after_idle = world.velocity(cube).x;
534 assert!(after_idle < after_force);
535 assert!(after_idle > after_force * 0.99);
536 }
537
538 #[test]
539 fn damping_decays_velocity() {
540 let mut scene = Scene::new();
541 let cube = spawn_box(&mut scene, Vec3::ZERO);
542 let mut world = PhysicsWorld::new();
543 world.set_gravity(Vec3::ZERO);
544 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
545 assert!(world.set_damping(cube, 0.5, 0.5));
546 assert!(world.set_velocity(cube, Vec3::new(10.0, 0.0, 0.0)));
547 assert_eq!(world.step(DT), StepResult::Completed);
548 let vx = world.velocity(cube).x;
549 assert!(vx > 9.0 && vx < 10.0, "got {vx}");
550 }
551
552 #[test]
553 fn contacts_detect_overlap_and_separation() {
554 let mut scene = Scene::new();
555 let cube = spawn_box(&mut scene, Vec3::ZERO);
556 let other = spawn_box(&mut scene, Vec3::ZERO);
557 let mut world = PhysicsWorld::new();
558 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
559 world.add_dynamic_box(other, Vec3::splat(0.5), Transform::default());
560
561 assert!(world.has_contacts(cube));
562 assert!(world.has_contacts(other));
563
564 assert!(world.set_transform(other, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)),));
565 assert!(!world.has_contacts(cube));
566 }
567
568 #[test]
569 #[expect(clippy::float_cmp)]
573 fn kinematic_body_moves_and_ignores_gravity() {
574 let mut scene = Scene::new();
575 let mover = spawn_box(&mut scene, Vec3::ZERO);
576 let mut world = PhysicsWorld::new();
577 world.add_kinematic_box(mover, Vec3::splat(0.5), Transform::default());
578 assert!(world.set_velocity(mover, Vec3::new(1.0, 0.0, 0.0)));
579 run(&mut world, 10);
580 let position = world.transform(mover).unwrap().position;
581 assert!(
582 (position.x - 10.0 * DT).abs() < 1e-4,
583 "got x={}",
584 position.x
585 );
586 assert_eq!(position.y, 0.0);
587 }
588
589 #[test]
590 fn step_rejects_invalid_input() {
591 let mut world = PhysicsWorld::new();
592 assert_eq!(world.step(0.0), StepResult::InvalidInput);
593 assert_eq!(world.step(-1.0), StepResult::InvalidInput);
594 assert_eq!(world.step(f32::NAN), StepResult::InvalidInput);
595 }
596
597 #[test]
598 fn synchronize_writes_orientation_and_position() {
599 let mut scene = Scene::new();
600 let cube = spawn_box(&mut scene, Vec3::new(0.0, 2.0, 0.0));
601 let mut world = PhysicsWorld::new();
602 world.add_dynamic_box(
603 cube,
604 Vec3::splat(0.5),
605 Transform::from_position(Vec3::new(0.0, 2.0, 0.0)),
606 );
607 assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 2.0)));
608 run(&mut world, 30);
609 world.synchronize(&mut scene);
610 let orientation = scene.objects.get(cube).unwrap().transform.orientation;
611 assert_ne!(orientation, Quat::IDENTITY, "cube should have rotated");
612 assert!(orientation.is_finite());
613 }
614
615 #[test]
616 fn add_static_mesh_registers_and_rejects_duplicates() {
617 let mut scene = Scene::new();
618 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
619 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
620 let mut world = PhysicsWorld::new();
621 assert!(world.add_static_mesh(
622 floor,
623 &floor_mesh,
624 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
625 ));
626 assert!(world.has_body(floor));
627 assert!(!world.add_static_mesh(
630 floor,
631 &floor_mesh,
632 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
633 ));
634 assert!(world.add_dynamic_box(
635 cube,
636 Vec3::splat(0.5),
637 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
638 ));
639 run(&mut world, 300);
640 world.synchronize(&mut scene);
641 let y = scene.objects.get(cube).unwrap().transform.position.y;
642 assert!(
643 (y - 0.1).abs() < 0.05,
644 "cube should rest near y=0.1, got y={y}"
645 );
646 }
647}