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 }