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 pub fn new() -> Self {
31 let mut scene = gp3d::PhysicsScene::new();
32 scene.set_gravity(DEFAULT_GRAVITY);
33 Self {
34 scene,
35 bodies: Vec::new(),
36 }
37 }
38
39 pub fn set_gravity(&mut self, gravity: Vec3) {
41 self.scene.set_gravity(gravity);
42 }
43
44 pub fn gravity(&self) -> Vec3 {
46 self.scene.gravity()
47 }
48
49 pub fn add_dynamic_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
52 self.add_box(object, half_extents, transform, Motion::Dynamic)
53 }
54
55 pub fn add_static_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
58 self.add_box(object, half_extents, transform, Motion::Static)
59 }
60
61 pub fn add_kinematic_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
64 self.add_box(object, half_extents, transform, Motion::Kinematic)
65 }
66
67 fn add_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform, motion: Motion) -> bool {
71 if self.handle(object).is_some() {
72 return false;
73 }
74 let desc = BodyDesc {
75 motion,
76 collider: gp3d::Collider::Box { half_extents },
77 position: transform.position,
78 orientation: transform.orientation,
79 ..BodyDesc::default()
80 };
81 match self.scene.add_body(desc) {
82 Ok(handle) => {
83 self.bodies.push((object, handle));
84 true
85 }
86 Err(_) => false,
87 }
88 }
89
90 pub fn add_static_mesh(&mut self, object: ObjectId, mesh: &Mesh, transform: Transform) -> bool {
96 if self.handle(object).is_some() {
97 return false;
98 }
99 let vertices = mesh.vertices().to_vec();
100 if vertices.is_empty() || vertices.len() % 3 != 0 {
101 return false;
102 }
103 match self
104 .scene
105 .add_static_mesh(vertices, transform.position, transform.orientation)
106 {
107 Ok(handle) => {
108 self.bodies.push((object, handle));
109 true
110 }
111 Err(_) => false,
112 }
113 }
114
115 pub fn set_transform(&mut self, object: ObjectId, transform: Transform) -> bool {
118 match self.handle(object) {
119 Some(handle) => {
120 self.scene
121 .set_transform(handle, transform.position, transform.orientation)
122 }
123 None => false,
124 }
125 }
126
127 pub fn transform(&self, object: ObjectId) -> Option<Transform> {
130 let handle = self.handle(object)?;
131 let position = self.scene.position(handle)?;
132 let orientation = self.scene.orientation(handle)?;
133 Some(Transform::from_position_orientation(position, orientation))
134 }
135
136 pub fn set_material(&mut self, object: ObjectId, friction: f32, restitution: f32) -> bool {
139 match self.handle(object) {
140 Some(handle) => self.scene.set_material(handle, friction, restitution),
141 None => false,
142 }
143 }
144
145 pub fn set_damping(&mut self, object: ObjectId, linear: f32, angular: f32) -> bool {
147 let Some(handle) = self.handle(object) else {
148 return false;
149 };
150 self.scene.set_linear_damping(handle, linear);
151 self.scene.set_angular_damping(handle, angular);
152 true
153 }
154
155 pub fn has_contacts(&self, object: ObjectId) -> bool {
158 match self.handle(object) {
159 Some(handle) => self.scene.has_contacts(handle),
160 None => false,
161 }
162 }
163
164 pub fn remove_body(&mut self, object: ObjectId) -> bool {
165 let Some(position) = self.bodies.iter().position(|(id, _)| *id == object) else {
166 return false;
167 };
168 let (_, handle) = self.bodies.swap_remove(position);
169 self.scene.remove_body(handle)
170 }
171
172 pub fn has_body(&self, object: ObjectId) -> bool {
173 self.handle(object).is_some()
174 }
175
176 pub fn set_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
178 match self.handle(object) {
179 Some(handle) => self.scene.set_velocity(handle, velocity),
180 None => false,
181 }
182 }
183
184 pub fn velocity(&self, object: ObjectId) -> Vec3 {
185 match self.handle(object) {
186 Some(handle) => self.scene.velocity(handle),
187 None => Vec3::ZERO,
188 }
189 }
190
191 pub fn set_angular_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
193 match self.handle(object) {
194 Some(handle) => self.scene.set_angular_velocity(handle, velocity),
195 None => false,
196 }
197 }
198
199 pub fn angular_velocity(&self, object: ObjectId) -> Vec3 {
200 match self.handle(object) {
201 Some(handle) => self.scene.angular_velocity(handle),
202 None => Vec3::ZERO,
203 }
204 }
205
206 pub fn add_force(&mut self, object: ObjectId, force: Vec3) -> bool {
207 match self.handle(object) {
208 Some(handle) => self.scene.add_force(handle, force),
209 None => false,
210 }
211 }
212
213 pub fn add_torque(&mut self, object: ObjectId, torque: Vec3) -> bool {
214 match self.handle(object) {
215 Some(handle) => self.scene.add_torque(handle, torque),
216 None => false,
217 }
218 }
219
220 pub fn add_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
221 match self.handle(object) {
222 Some(handle) => self.scene.add_impulse(handle, impulse),
223 None => false,
224 }
225 }
226
227 pub fn add_angular_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
228 match self.handle(object) {
229 Some(handle) => self.scene.add_angular_impulse(handle, impulse),
230 None => false,
231 }
232 }
233
234 pub fn step(&mut self, delta_time: f32) -> StepResult {
240 self.scene.step(delta_time, 4)
241 }
242
243 pub fn synchronize(&mut self, scene: &mut Scene) {
246 for &(object, handle) in &self.bodies {
247 let Some(render_object) = scene.objects.get_mut(object) else {
248 continue;
249 };
250 let Some(position) = self.scene.position(handle) else {
251 continue;
252 };
253 let Some(orientation) = self.scene.orientation(handle) else {
254 continue;
255 };
256 render_object.transform.position = position;
257 render_object.transform.orientation = orientation;
258 }
259 }
260
261 fn handle(&self, object: ObjectId) -> Option<BodyHandle> {
262 self.bodies
263 .iter()
264 .find(|(id, _)| *id == object)
265 .map(|(_, handle)| *handle)
266 }
267}
268
269impl Default for PhysicsWorld {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use alloc::vec;
278
279 use super::*;
280 use he3d_core::{ColorRgba8, Material, Mesh, RenderObject, Rgb, Texture};
281 use he3d_math::Quat;
282
283 const DT: f32 = 1.0 / 60.0;
284
285 fn spawn_box(scene: &mut Scene, position: Vec3) -> ObjectId {
286 let mesh = scene.meshes.insert(Mesh::cube(1.0, 1.0, 1.0).expect("cube"));
287 let solid = scene.textures.insert(Texture::solid(ColorRgba8::WHITE));
288 scene.spawn(RenderObject {
289 mesh,
290 texture: Some(solid),
291 transform: Transform::from_position(position),
292 material: Material::new(Rgb::WHITE),
293 visible: true,
294 })
295 }
296
297 fn spawn_quad_floor(scene: &mut Scene, half: f32) -> (ObjectId, Mesh) {
300 let h = half;
301 let quad = Mesh::from_vertices(vec![
302 Vec3::new(-h, 0.0, -h),
303 Vec3::new(h, 0.0, h),
304 Vec3::new(h, 0.0, -h),
305 Vec3::new(-h, 0.0, -h),
306 Vec3::new(-h, 0.0, h),
307 Vec3::new(h, 0.0, h),
308 ])
309 .expect("quad");
310 let mesh_id = scene.meshes.insert(quad.clone());
311 let solid = scene.textures.insert(Texture::solid(ColorRgba8::from_rgb(77, 179, 77)));
312 let object = scene.spawn(RenderObject {
313 mesh: mesh_id,
314 texture: Some(solid),
315 transform: Transform::default(),
316 material: Material::new(Rgb::WHITE),
317 visible: true,
318 });
319 (object, quad)
320 }
321
322 fn run(world: &mut PhysicsWorld, steps: usize) {
323 for _ in 0..steps {
324 assert_eq!(world.step(DT), StepResult::Completed);
325 }
326 }
327
328 #[test]
329 fn registration_and_removal() {
330 let mut scene = Scene::new();
331 let cube = spawn_box(&mut scene, Vec3::ZERO);
332 let mut world = PhysicsWorld::new();
333
334 assert!(world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
335 assert!(world.has_body(cube));
336 assert!(!world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
338 assert!(!world.add_static_box(cube, Vec3::splat(0.5), Transform::default()));
339
340 assert!(world.remove_body(cube));
341 assert!(!world.has_body(cube));
342 assert!(!world.remove_body(cube));
343 assert!(!world.add_force(cube, Vec3::X));
344 }
345
346 #[test]
347 fn transform_and_velocity_passthrough() {
348 let mut scene = Scene::new();
349 let cube = spawn_box(&mut scene, Vec3::new(0.0, 1.0, 0.0));
350 let mut world = PhysicsWorld::new();
351 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::from_position(Vec3::new(0.0, 1.0, 0.0)));
352
353 assert!(world.set_velocity(cube, Vec3::new(2.0, 0.0, 0.0)));
354 assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
355
356 let teleport = Transform::from_position_orientation(
358 Vec3::new(3.0, 4.0, 5.0),
359 Quat::from_rotation_y(1.0),
360 );
361 assert!(world.set_transform(cube, teleport));
362 assert_eq!(world.transform(cube), Some(teleport));
363 assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
364
365 assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 1.0)));
366 assert_eq!(world.angular_velocity(cube), Vec3::new(0.0, 0.0, 1.0));
367
368 let stale = spawn_box(&mut scene, Vec3::ZERO);
370 scene.objects.remove(stale);
371 assert!(!world.set_transform(stale, Transform::default()));
372 assert_eq!(world.transform(stale), None);
373 }
374
375 #[test]
376 fn gravity_pulls_dynamic_body_down() {
377 let mut scene = Scene::new();
378 let cube = spawn_box(&mut scene, Vec3::new(0.0, 10.0, 0.0));
379 let mut world = PhysicsWorld::new();
380 world.add_dynamic_box(
381 cube,
382 Vec3::splat(0.5),
383 Transform::from_position(Vec3::new(0.0, 10.0, 0.0)),
384 );
385 run(&mut world, 60);
386 world.synchronize(&mut scene);
387 let y = scene.objects.get(cube).unwrap().transform.position.y;
388 assert!(y < 9.0, "body should have fallen, got y={y}");
389 assert!(y > 0.0, "body should not have fallen through, got y={y}");
390 }
391
392 #[test]
393 fn cube_rests_on_static_mesh_ground() {
394 let mut scene = Scene::new();
395 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
396 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
397 let mut world = PhysicsWorld::new();
398 world.add_dynamic_box(
399 cube,
400 Vec3::splat(0.5),
401 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
402 );
403 assert!(world.add_static_mesh(
404 floor,
405 &floor_mesh,
406 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
407 ));
408
409 run(&mut world, 300);
410 world.synchronize(&mut scene);
411 let y = scene.objects.get(cube).unwrap().transform.position.y;
412 assert!((y - 0.1).abs() < 0.05, "cube should rest near y=0.1, got y={y}");
413 }
414
415 #[test]
416 fn restitution_bounces_on_fast_impact() {
417 let mut scene = Scene::new();
418 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
419 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 6.0);
420 let mut world = PhysicsWorld::new();
421 world.add_dynamic_box(
422 cube,
423 Vec3::splat(0.5),
424 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
425 );
426 world.set_material(cube, 0.5, 0.5);
427 world.add_static_mesh(floor, &floor_mesh, Transform::from_position(Vec3::new(0.0, -0.4, 0.0)));
428
429 let mut max_vy = 0.0f32;
430 for _ in 0..120 {
431 world.step(DT);
432 max_vy = max_vy.max(world.velocity(cube).y);
433 }
434 assert!(max_vy > 0.5, "fast impact must bounce, got max_vy={max_vy}");
435 }
436
437 #[test]
438 fn force_acts_for_one_step_then_is_cleared() {
439 let mut scene = Scene::new();
440 let cube = spawn_box(&mut scene, Vec3::ZERO);
441 let mut world = PhysicsWorld::new();
442 world.set_gravity(Vec3::ZERO);
443 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
444
445 assert!(world.add_force(cube, Vec3::new(60.0, 0.0, 0.0)));
446 assert_eq!(world.step(DT), StepResult::Completed);
447 let after_force = world.velocity(cube).x;
448 assert!(after_force > 0.9, "got {after_force}");
449
450 assert_eq!(world.step(DT), StepResult::Completed);
451 let after_idle = world.velocity(cube).x;
452 assert!(after_idle < after_force);
453 assert!(after_idle > after_force * 0.99);
454 }
455
456 #[test]
457 fn damping_decays_velocity() {
458 let mut scene = Scene::new();
459 let cube = spawn_box(&mut scene, Vec3::ZERO);
460 let mut world = PhysicsWorld::new();
461 world.set_gravity(Vec3::ZERO);
462 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
463 assert!(world.set_damping(cube, 0.5, 0.5));
464 assert!(world.set_velocity(cube, Vec3::new(10.0, 0.0, 0.0)));
465 assert_eq!(world.step(DT), StepResult::Completed);
466 let vx = world.velocity(cube).x;
467 assert!(vx > 9.0 && vx < 10.0, "got {vx}");
468 }
469
470 #[test]
471 fn contacts_detect_overlap_and_separation() {
472 let mut scene = Scene::new();
473 let cube = spawn_box(&mut scene, Vec3::ZERO);
474 let other = spawn_box(&mut scene, Vec3::ZERO);
475 let mut world = PhysicsWorld::new();
476 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
477 world.add_dynamic_box(other, Vec3::splat(0.5), Transform::default());
478
479 assert!(world.has_contacts(cube));
480 assert!(world.has_contacts(other));
481
482 assert!(world.set_transform(
483 other,
484 Transform::from_position(Vec3::new(10.0, 0.0, 0.0)),
485 ));
486 assert!(!world.has_contacts(cube));
487 }
488
489 #[test]
490 fn kinematic_body_moves_and_ignores_gravity() {
491 let mut scene = Scene::new();
492 let mover = spawn_box(&mut scene, Vec3::ZERO);
493 let mut world = PhysicsWorld::new();
494 world.add_kinematic_box(mover, Vec3::splat(0.5), Transform::default());
495 assert!(world.set_velocity(mover, Vec3::new(1.0, 0.0, 0.0)));
496 run(&mut world, 10);
497 let position = world.transform(mover).unwrap().position;
498 assert!((position.x - 10.0 * DT).abs() < 1e-4, "got x={}", position.x);
499 assert_eq!(position.y, 0.0);
500 }
501
502 #[test]
503 fn step_rejects_invalid_input() {
504 let mut world = PhysicsWorld::new();
505 assert_eq!(world.step(0.0), StepResult::InvalidInput);
506 assert_eq!(world.step(-1.0), StepResult::InvalidInput);
507 assert_eq!(world.step(f32::NAN), StepResult::InvalidInput);
508 }
509
510 #[test]
511 fn synchronize_writes_orientation_and_position() {
512 let mut scene = Scene::new();
513 let cube = spawn_box(&mut scene, Vec3::new(0.0, 2.0, 0.0));
514 let mut world = PhysicsWorld::new();
515 world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::from_position(Vec3::new(0.0, 2.0, 0.0)));
516 assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 2.0)));
517 run(&mut world, 30);
518 world.synchronize(&mut scene);
519 let orientation = scene.objects.get(cube).unwrap().transform.orientation;
520 assert_ne!(orientation, Quat::IDENTITY, "cube should have rotated");
521 assert!(orientation.is_finite());
522 }
523
524 #[test]
525 fn add_static_mesh_registers_and_rejects_duplicates() {
526 let mut scene = Scene::new();
527 let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
528 let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
529 let mut world = PhysicsWorld::new();
530 assert!(world.add_static_mesh(
531 floor,
532 &floor_mesh,
533 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
534 ));
535 assert!(world.has_body(floor));
536 assert!(!world.add_static_mesh(
539 floor,
540 &floor_mesh,
541 Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
542 ));
543 assert!(world.add_dynamic_box(
544 cube,
545 Vec3::splat(0.5),
546 Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
547 ));
548 run(&mut world, 300);
549 world.synchronize(&mut scene);
550 let y = scene.objects.get(cube).unwrap().transform.position.y;
551 assert!((y - 0.1).abs() < 0.05, "cube should rest near y=0.1, got y={y}");
552 }
553}
554