1 /** 3d rigid body */ 2 3 module re.phys.rigid3d; 4 5 version (physics) { 6 import std.math; 7 import std.format; 8 import std.typecons; 9 import std.container.array; 10 11 import re.ecs.component; 12 import re.ecs.updatable; 13 import re.math; 14 import re.time; 15 import re.core; 16 import re.ng.manager; 17 import re.ng.scene; 18 import re.phys.collider; 19 import re.util.dual_map; 20 import re.util.newtonphys; 21 import re.phys.newton3d; 22 import newton = bindbc.newton; 23 24 /// represents a manager for physics bodies 25 class PhysicsManager3D : Manager { 26 /// the maximum number of collisions to support in this world 27 public int max_collisions = 1024; 28 /// position correction iterations used by physics engine 29 public int pos_correction_iterations = 20; 30 /// velocity correction iterations used by physics engine 31 public int vel_correction_iterations = 40; 32 /// gravity in the physics world 33 public Vector3 gravity = Vector3(0.0f, -9.80665f, 0.0f); // earth's gravity 34 35 /// the internal newton physics world 36 private newton.NewtonWorld* world; 37 38 /// some cursed dlib stuff 39 private NewtonWorldManager manager; 40 41 /// physics target timestep 42 private float _timestep; 43 /// used to track time to keep physics timestep fixed 44 private float _phys_time = 0; 45 46 private PhysicsBody3D[NewtonRigidBody] _bodies; 47 48 /// the total number of bodies in this physics world 49 @property public size_t body_count() { 50 return newton.NewtonWorldGetBodyCount(world); 51 } 52 53 override void setup() { 54 world = newton.NewtonCreate(); 55 manager = new NewtonWorldManager(world); 56 57 // set to iterative (1) mode https://newtondynamics.com/wiki/index.php/NewtonSetSolverIterations 58 newton.NewtonSetSolverIterations(world, 1); 59 60 // world.gravity = convert_vec3(gravity); 61 // _timestep = 1f / Core.target_fps; // set target _timestep 62 } 63 64 override void destroy() { 65 newton.NewtonDestroyAllBodies(world); 66 newton.NewtonDestroy(world); 67 } 68 69 override void update() { 70 // step the simulation 71 _phys_time += Time.delta_time; 72 // TODO: should this be while? 73 if (_phys_time >= _timestep) { 74 _phys_time -= _timestep; 75 76 // // sync FROM bodies: physical properties (mass, inertia) 77 // // sync TO bodies: transforms, momentum 78 // foreach (comp; _bodies.byValue()) { 79 // NewtonRigidBody bod = comp._phys_body; 80 81 // // sync properties -> physics engine 82 // if (abs(bod.mass - comp.mass) > float.epsilon) { 83 // bod.mass = comp.mass; 84 // bod.invMass = 1f / comp.mass; 85 // // TODO: update mass contribtion of shapes? 86 // // foreach (shape; body_comp._phys_shapes) { 87 88 // // } 89 // } 90 // // TODO: sync inertia? right now it's automatically set from mass 91 92 // // sync velocity 93 // bod.linearVelocity = convert_vec3(comp.velocity); 94 // bod.angularVelocity = convert_vec3(comp.angular_velocity); 95 // bod.maxSpeed = comp.max_speed; 96 97 // // apply forces and impulses 98 // // these are queued up, then we apply them all to the object 99 // foreach (force; comp._forces) { 100 // bod.applyForceAtPos(convert_vec3(force.value), 101 // convert_vec3(force.pos) + bod.position); 102 // } 103 // comp._forces.removeBack(cast(uint) comp._forces.length); 104 // foreach (impulse; comp._impulses) { 105 // bod.applyImpulse(convert_vec3(impulse.value), 106 // convert_vec3(impulse.pos) + bod.position); 107 // } 108 // comp._impulses.removeBack(cast(uint) comp._impulses.length); 109 // foreach (torque; comp._torques) { 110 // bod.applyTorque(convert_vec3(torque)); 111 // } 112 // comp._torques.removeBack(cast(uint) comp._torques.length); 113 // } 114 115 // // sync options to world 116 // world.gravity = convert_vec3(gravity); 117 118 // world.update(_timestep); 119 newton.NewtonUpdate(world, _timestep); 120 121 // foreach (comp; _bodies.byValue()) { 122 // NewtonRigidBody bod = comp._phys_body; 123 124 // // sync physics engine -> components 125 126 // // sync position 127 // auto bod_pos = bod.position; 128 // comp.transform.position = convert_vec3(bod_pos); 129 130 // // sync rotation/orientation 131 // auto bod_rot = bod.orientation; 132 // comp.transform.orientation = convert_quat(bod_rot); 133 134 // // sync velocity 135 // comp.velocity = convert_vec3(bod.linearVelocity); 136 // comp.angular_velocity = convert_vec3(bod.angularVelocity); 137 // } 138 } 139 } 140 141 /// register all colliders in this body 142 private void register_colliders(PhysicsBody3D body_comp) { 143 // add colliders to the physics world 144 auto bod = body_comp._phys_body; 145 146 auto box_colliders = body_comp.entity.get_components!BoxCollider(); 147 148 foreach (box; box_colliders) { 149 auto box_size = box.size; 150 auto box_origin = box.offset + box.size * 0.5; 151 152 // auto offset_mat = newton_matrix(); 153 // newton.GetId 154 // offset_mat.posit = box_origin; 155 // newton.NewtonCreateBox 156 157 // auto box_geom = New!(geom.GeomBox)(world, convert_vec3(box.size)); 158 // auto shape = world.addShapeComponent(bod, box_geom, 159 // convert_vec3(box.offset), bod.mass); 160 // body_comp._shapes[shape] = box; 161 } 162 } 163 164 /// unregister all colliders in this body 165 private void unregister_colliders(PhysicsBody3D body_comp) { 166 // import std.range : front; 167 // import std.algorithm : countUntil, remove; 168 169 // // we need to remove from the world each collider that we internally have registered to that body 170 // foreach (shape; body_comp._shapes.byKey()) { 171 // world.shapeComponents.removeFirst(shape); 172 // } 173 174 // // then clear the internal collider list 175 // body_comp._shapes.clear(); 176 } 177 178 // private NewtonRigidBody createDynamicBody(NewtonCollisionShape shape, float mass) { 179 // NewtonRigidBody b = New!NewtonRigidBody(shape, mass, this, this); 180 // b.dynamic = true; 181 // // TODO: store a list of bodies 182 // return b; 183 // } 184 185 // private NewtonRigidBody createStaticBody(NewtonCollisionShape shape) { 186 // auto b = createDynamicBody(shape, 0.0f); 187 // b.dynamic = false; 188 // return b; 189 // } 190 191 /// registers a body 192 private void register(PhysicsBody3D body_comp) { 193 // NewtonRigidBody bod; 194 // switch (body_comp._body_type) { 195 // case PhysicsBody3D.BodyType.Static: 196 // bod = world.addStaticBody(convert_vec3(body_comp.transform.position)); 197 // break; 198 // case PhysicsBody3D.BodyType.Dynamic: 199 // bod = world.addDynamicBody(convert_vec3(body_comp.transform.position), 200 // body_comp.mass); 201 // break; 202 // default: 203 // assert(0); 204 // } 205 206 // bod.rotation = convert_quat(body_comp.transform.orientation); 207 208 // // update registration 209 // body_comp._phys_body = bod; 210 // _bodies[bod] = body_comp; 211 212 // // add colliders 213 // register_colliders(body_comp); 214 215 // // sync properties 216 // sync_properties(body_comp); 217 218 // // mark as synced 219 // body_comp.physics_synced = true; 220 } 221 222 /// unregisters a body 223 private void unregister(PhysicsBody3D body_comp) { 224 // // remove colliders 225 // unregister_colliders(body_comp); 226 227 // auto bod = body_comp._phys_body; 228 229 // // remove body 230 // switch (body_comp._body_type) { 231 // case PhysicsBody3D.BodyType.Static: 232 // world.staticBodies.removeFirst(bod); 233 // break; 234 // case PhysicsBody3D.BodyType.Dynamic: 235 // world.dynamicBodies.removeFirst(bod); 236 // break; 237 // default: 238 // assert(0); 239 // } 240 241 // // mark as unsynced 242 // body_comp.physics_synced = false; 243 244 // // clear registration 245 // _bodies.remove(bod); 246 // body_comp._phys_body = null; 247 } 248 249 /// sync a body's colliders in the physics engine, necessary when shapes change 250 private void sync_colliders(PhysicsBody3D body_comp) { 251 unregister_colliders(body_comp); 252 register_colliders(body_comp); 253 } 254 255 /// synchronize the transform from body to physics engine 256 private void sync_transform(PhysicsBody3D body_comp) { 257 // sync position 258 body_comp._phys_body.position = convert_vec3(body_comp.transform.position); 259 260 // sync rotation 261 body_comp._phys_body.rotation = convert_quat(body_comp.transform.orientation); 262 } 263 264 /// sync physical properties from body to physics engine 265 private void sync_properties(PhysicsBody3D body_comp) { 266 auto bod = body_comp._phys_body; 267 // if (body_comp.custom_gravity) { 268 // bod.useOwnGravity = true; 269 // bod.gravity = convert_vec3(body_comp.gravity); 270 // } 271 // bod.damping = body_comp.damping; 272 // bod.bounce = body_comp.bounce; 273 // bod.friction = body_comp.friction; 274 } 275 276 /// cast a ray of a given length in a given direction and return the result. null if no hits. 277 public Nullable!RaycastResult raycast(Vector3 start, Vector3 direction, float dist) { 278 // import std.algorithm.searching : countUntil; 279 280 // dm_ray.CastResult cr; 281 // if (world.raycast(convert_vec3(start), convert_vec3(direction), dist, cr, true, true)) { 282 // // get matching physics body 283 // auto body_comp = _bodies[cr.rbody]; 284 // auto res = RaycastResult(convert_vec3(cr.point), convert_vec3(cr.normal), body_comp); 285 // // get matching collider 286 // if (cr.shape !is null) { 287 // auto col = body_comp._shapes[cr.shape]; 288 // res.collider = Nullable!Collider(col); 289 // } 290 // return Nullable!RaycastResult(res); 291 // } 292 // no result 293 return Nullable!RaycastResult.init; 294 } 295 296 /// raycast from a physics body in a certain direction, excluding the source body 297 public Nullable!RaycastResult raycast_from(PhysicsBody3D source, 298 Vector3 offset, Vector3 direction, float dist) { 299 // we want to exclude this body, so we're temporarily going to disable it 300 source._phys_body.raycastable = false; 301 auto res = raycast(source.transform.position + offset, direction, dist); 302 source._phys_body.raycastable = true; 303 return res; 304 } 305 } 306 307 public struct RaycastResult { 308 Vector3 point; 309 Vector3 normal; 310 PhysicsBody3D pbody; 311 Nullable!Collider collider; 312 } 313 314 /// represents a physics body 315 abstract class PhysicsBody3D : Component { 316 mixin Reflect; 317 // - references to things in the physics engine 318 private NewtonRigidBody _phys_body; 319 // private newton.NewtonCollision*[] _phys_shapes; 320 private Collider[newton.NewtonCollision* ] _shapes; 321 322 // - physical properties 323 /// object mass 324 public float mass = 0; 325 /// moment of inertia 326 // public float inertia = 1; 327 /// max speed of object 328 public float max_speed = float.max; 329 /// current linear velocity of object 330 public Vector3 velocity = Vector3(0, 0, 0); 331 /// current angular velocity of object 332 public Vector3 angular_velocity = Vector3(0, 0, 0); 333 /// damping amount 334 public float damping = 0.5; 335 /// bounce amount 336 public float bounce = 0; 337 /// coefficient of friction 338 public float friction = 0.5; 339 /// whether to use a custom gravity value 340 public bool custom_gravity = false; 341 /// if custom gravity is enabled, the gravity to use 342 public Vector3 gravity = Vector3(0, 0, 0); 343 344 /// whether this body is currently in sync with the physics system 345 public bool physics_synced = false; 346 347 private PhysicsManager3D _mgr; 348 private BodyType _body_type; 349 350 /// physics body mode: dynamic or static 351 public enum BodyType { 352 Dynamic, 353 Static 354 } 355 356 // - used to queue forces and impulses to be applied by the physics engine 357 private Array!VecAtPoint _forces; 358 private Array!Vector3 _torques; 359 private Array!VecAtPoint _impulses; 360 361 private struct VecAtPoint { 362 Vector3 value; 363 Vector3 pos; 364 } 365 366 /// creates a physics body with a given mass and type 367 this(float mass, BodyType type) { 368 this.mass = mass; 369 _body_type = type; 370 } 371 372 override void setup() { 373 // register the body in the physics manager 374 auto mgr = entity.scene.get_manager!PhysicsManager3D(); 375 assert(!mgr.isNull, "scene did not have PhysicsManager3D registered." 376 ~ "please add that to the scene before creating this component."); 377 _mgr = mgr.get; 378 _mgr.register(this); 379 } 380 381 /// apply an impulse to the physics body 382 public void apply_impulse(Vector3 value, Vector3 pos) { 383 _impulses.insertBack(VecAtPoint(value, pos)); 384 } 385 386 /// apply a force to the physics body 387 public void apply_force(Vector3 value, Vector3 pos) { 388 _forces.insertBack(VecAtPoint(value, pos)); 389 } 390 391 /// apply a torque to the physics body 392 public void apply_torque(Vector3 value) { 393 _torques.insertBack(value); 394 } 395 396 /// notify physics engine about new physical properties, such as gravity 397 public void sync_properties() { 398 _mgr.sync_properties(this); 399 } 400 401 /// used to notify the physics engine to update colliders if they have changed 402 public void sync_colliders() { 403 _mgr.sync_colliders(this); 404 } 405 406 /// used to notify the physics engine when transform is directly modified 407 public void sync_transform() { 408 _mgr.sync_transform(this); 409 } 410 411 override void destroy() { 412 _mgr.unregister(this); 413 } 414 } 415 416 /// a dynamic physics body that is affected by forces 417 public class DynamicBody : PhysicsBody3D { 418 mixin Reflect; 419 this(float mass = 1f) { 420 super(mass, PhysicsBody3D.BodyType.Dynamic); 421 } 422 } 423 424 /// a static physics body that is not affected by forces 425 public class StaticBody : PhysicsBody3D { 426 mixin Reflect; 427 this(float mass = 1f) { 428 super(mass, PhysicsBody3D.BodyType.Static); 429 } 430 } 431 432 @("phys-rigid3d-basic") unittest { 433 import re.ng.scene : Scene2D; 434 import re.util.test : test_scene; 435 436 class TestScene : Scene2D { 437 override void on_start() { 438 add_manager(new PhysicsManager3D()); 439 440 auto nt = create_entity("block"); 441 nt.add_component(new DynamicBody()); 442 } 443 } 444 445 auto test = test_scene(new TestScene()); 446 test.game.run(); 447 448 // check conditions 449 450 test.game.destroy(); 451 } 452 453 @("phys-rigid3d-lifecycle") unittest { 454 import re.ng.scene : Scene2D; 455 import re.util.test : test_scene; 456 import re.ecs.entity : Entity; 457 458 class TestScene : Scene2D { 459 private Entity nt1; 460 461 override void on_start() { 462 add_manager(new PhysicsManager3D()); 463 464 nt1 = create_entity("one"); 465 nt1.add_component(new DynamicBody()); 466 auto nt2 = create_entity("two"); 467 nt2.add_component(new DynamicBody()); 468 auto nt3 = create_entity("three"); 469 nt3.add_component(new DynamicBody()); 470 } 471 472 public void kill_one() { 473 nt1.destroy(); 474 } 475 } 476 477 auto test = test_scene(new TestScene()); 478 test.game.run(); 479 480 // check conditions 481 auto mgr = test.scene.get_manager!PhysicsManager3D; 482 assert(!mgr.isNull); 483 assert(mgr.get.body_count == 3, "physics body count does not match"); 484 485 (cast(TestScene) test.scene).kill_one(); 486 assert(mgr.get.body_count == 2, "physics body was not unregistered on component destroy"); 487 488 test.game.destroy(); 489 } 490 491 @("phys-rigid3d-colliders") unittest { 492 import re.ng.scene : Scene2D; 493 import re.util.test : test_scene; 494 import re.ecs.entity : Entity; 495 import std.algorithm : canFind; 496 497 class TestScene : Scene2D { 498 private Entity nt1; 499 500 override void on_start() { 501 add_manager(new PhysicsManager3D()); 502 503 nt1 = create_entity("block"); 504 nt1.add_component(new BoxCollider(Vector3(1, 1, 1), Vector3Zero)); 505 nt1.add_component(new DynamicBody()); 506 } 507 508 /// inform the physics body that the colliders need to be synced 509 public void reload_colliders() { 510 nt1.get_component!DynamicBody().sync_colliders(); 511 } 512 513 /// remove the existing box collider, and replace with a new one, then reload 514 public void replace_colliders() { 515 nt1.remove_component!BoxCollider(); 516 nt1.add_component(new BoxCollider(Vector3(2, 2, 2), Vector3Zero)); 517 reload_colliders(); 518 } 519 } 520 521 auto test = test_scene(new TestScene()); 522 test.game.run(); 523 524 // check conditions 525 auto scn = cast(TestScene) test.scene; 526 auto mgr = test.scene.get_manager!PhysicsManager3D.get; 527 auto bod = test.scene.get_entity("block").get_component!DynamicBody(); 528 529 // check that colliders are registered 530 assert(mgr.world.dynamicBodies.data.canFind(bod._phys_body)); 531 auto shape1 = bod._shapes.keys[0]; 532 immutable auto collider1_size_x = (cast(geom.GeomBox)(shape1.geometry)).halfSize.x; 533 assert(collider1_size_x == 1, 534 "collider #1 size from physics engine does not match provided collider size"); 535 536 // sync the colliders, then ensure that the registration is different 537 scn.reload_colliders(); 538 auto shape2 = bod._shapes.keys[0]; 539 assert(shape1 != shape2, 540 "colliders were synced, which was supposed to reset collider registration, but entry was not changed"); 541 542 // replace the colliders 543 scn.replace_colliders(); 544 assert(bod._shapes.length > 0, "registration entry for new collider missing"); 545 auto shape3 = bod._shapes.keys[0]; 546 immutable auto collider3_size_x = (cast(geom.GeomBox)(shape3.geometry)).halfSize.x; 547 assert(collider3_size_x == 2, 548 "collider #3 size from physics engine does not match replaced collider size"); 549 550 test.game.destroy(); 551 } 552 }