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