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                 auto res = RaycastResult(convert_vec3(cr.point), convert_vec3(cr.normal), body_comp);
278                 // get matching collider
279                 if (cr.shape !is null) {
280                     auto col = body_comp._shapes[cr.shape];
281                     res.collider = Nullable!Collider(col);
282                 }
283                 return Nullable!RaycastResult(res);
284             }
285             // no result
286             return Nullable!RaycastResult.init;
287         }
288 
289         /// raycast from a physics body in a certain direction, excluding the source body
290         public Nullable!RaycastResult raycast_from(PhysicsBody source,
291                 Vector3 offset, Vector3 direction, float dist) {
292             // we want to exclude this body, so we're temporarily going to disable it
293             source._phys_body.raycastable = false;
294             auto res = raycast(source.transform.position + offset, direction, dist);
295             source._phys_body.raycastable = true;
296             return res;
297         }
298     }
299 
300     public struct RaycastResult {
301         Vector3 point;
302         Vector3 normal;
303         PhysicsBody pbody;
304         Nullable!Collider collider;
305     }
306 
307     /// represents a physics body
308     abstract class PhysicsBody : Component {
309         mixin Reflect;
310         // - references to things in the physics engine
311         private rb.RigidBody _phys_body;
312         // private shape.ShapeComponent[] _phys_shapes;
313         private Collider[shape.ShapeComponent] _shapes;
314 
315         // - physical properties
316         /// object mass
317         public float mass = 0;
318         /// moment of inertia
319         // public float inertia = 1;
320         /// max speed of object
321         public float max_speed = float.max;
322         /// current linear velocity of object
323         public Vector3 velocity = Vector3(0, 0, 0);
324         /// current angular velocity of object
325         public Vector3 angular_velocity = Vector3(0, 0, 0);
326         /// damping amount
327         public float damping = 0.5;
328         /// bounce amount
329         public float bounce = 0;
330         /// coefficient of friction
331         public float friction = 0.5;
332         /// whether to use a custom gravity value
333         public bool custom_gravity = false;
334         /// if custom gravity is enabled, the gravity to use
335         public Vector3 gravity = Vector3(0, 0, 0);
336 
337         /// whether this body is currently in sync with the physics system
338         public bool physics_synced = false;
339 
340         private PhysicsManager _mgr;
341         private BodyType _body_type;
342 
343         /// physics body mode: dynamic or static
344         public enum BodyType {
345             Dynamic,
346             Static
347         }
348 
349         // - used to queue forces and impulses to be applied by the physics engine
350         private DynamicArray!VecAtPoint _forces;
351         private DynamicArray!Vector3 _torques;
352         private DynamicArray!VecAtPoint _impulses;
353 
354         private struct VecAtPoint {
355             Vector3 value;
356             Vector3 pos;
357         }
358 
359         /// creates a physics body with a given mass and type
360         this(float mass, BodyType type) {
361             this.mass = mass;
362             _body_type = type;
363         }
364 
365         override void setup() {
366             // register the body in the physics manager
367             auto mgr = entity.scene.get_manager!PhysicsManager();
368             assert(!mgr.isNull, "scene did not have PhysicsManager registered."
369                     ~ "please add that to the scene before creating this component.");
370             _mgr = mgr.get;
371             _mgr.register(this);
372         }
373 
374         /// apply an impulse to the physics body
375         public void apply_impulse(Vector3 value, Vector3 pos) {
376             _impulses.append(VecAtPoint(value, pos));
377         }
378 
379         /// apply a force to the physics body
380         public void apply_force(Vector3 value, Vector3 pos) {
381             _forces.append(VecAtPoint(value, pos));
382         }
383 
384         /// apply a torque to the physics body
385         public void apply_torque(Vector3 value) {
386             _torques.append(value);
387         }
388 
389         /// notify physics engine about new physical properties, such as gravity
390         public void sync_properties() {
391             _mgr.sync_properties(this);
392         }
393 
394         /// used to notify the physics engine to update colliders if they have changed
395         public void sync_colliders() {
396             _mgr.sync_colliders(this);
397         }
398 
399         /// used to notify the physics engine when transform is directly modified
400         public void sync_transform() {
401             _mgr.sync_transform(this);
402         }
403 
404         override void destroy() {
405             _mgr.unregister(this);
406         }
407     }
408 
409     /// a dynamic physics body that is affected by forces
410     public class DynamicBody : PhysicsBody {
411         mixin Reflect;
412         this(float mass = 1f) {
413             super(mass, PhysicsBody.BodyType.Dynamic);
414         }
415     }
416 
417     /// a static physics body that is not affected by forces
418     public class StaticBody : PhysicsBody {
419         mixin Reflect;
420         this(float mass = 1f) {
421             super(mass, PhysicsBody.BodyType.Static);
422         }
423     }
424 
425     @("phys-rigid3d-basic") unittest {
426         import re.ng.scene : Scene2D;
427         import re.util.test : test_scene;
428 
429         class TestScene : Scene2D {
430             override void on_start() {
431                 add_manager(new PhysicsManager());
432 
433                 auto nt = create_entity("block");
434                 nt.add_component(new DynamicBody());
435             }
436         }
437 
438         auto test = test_scene(new TestScene());
439         test.game.run();
440 
441         // check conditions
442 
443         test.game.destroy();
444     }
445 
446     @("phys-rigid3d-lifecycle") unittest {
447         import re.ng.scene : Scene2D;
448         import re.util.test : test_scene;
449         import re.ecs.entity : Entity;
450 
451         class TestScene : Scene2D {
452             private Entity nt1;
453 
454             override void on_start() {
455                 add_manager(new PhysicsManager());
456 
457                 nt1 = create_entity("one");
458                 nt1.add_component(new DynamicBody());
459                 auto nt2 = create_entity("two");
460                 nt2.add_component(new DynamicBody());
461                 auto nt3 = create_entity("three");
462                 nt3.add_component(new DynamicBody());
463             }
464 
465             public void kill_one() {
466                 nt1.destroy();
467             }
468         }
469 
470         auto test = test_scene(new TestScene());
471         test.game.run();
472 
473         // check conditions
474         auto mgr = test.scene.get_manager!PhysicsManager;
475         assert(!mgr.isNull);
476         assert(mgr.get.body_count == 3, "physics body count does not match");
477 
478         (cast(TestScene) test.scene).kill_one();
479         assert(mgr.get.body_count == 2, "physics body was not unregistered on component destroy");
480 
481         test.game.destroy();
482     }
483 
484     @("phys-rigid3d-colliders") unittest {
485         import re.ng.scene : Scene2D;
486         import re.util.test : test_scene;
487         import re.ecs.entity : Entity;
488         import std.algorithm : canFind;
489 
490         class TestScene : Scene2D {
491             private Entity nt1;
492 
493             override void on_start() {
494                 add_manager(new PhysicsManager());
495 
496                 nt1 = create_entity("block");
497                 nt1.add_component(new BoxCollider(Vector3(1, 1, 1), Vector3Zero));
498                 nt1.add_component(new DynamicBody());
499             }
500 
501             /// inform the physics body that the colliders need to be synced
502             public void reload_colliders() {
503                 nt1.get_component!DynamicBody().sync_colliders();
504             }
505 
506             /// remove the existing box collider, and replace with a new one, then reload
507             public void replace_colliders() {
508                 nt1.remove_component!BoxCollider();
509                 nt1.add_component(new BoxCollider(Vector3(2, 2, 2), Vector3Zero));
510                 reload_colliders();
511             }
512         }
513 
514         auto test = test_scene(new TestScene());
515         test.game.run();
516 
517         // check conditions
518         auto scn = cast(TestScene) test.scene;
519         auto mgr = test.scene.get_manager!PhysicsManager.get;
520         auto bod = test.scene.get_entity("block").get_component!DynamicBody();
521 
522         // check that colliders are registered
523         assert(mgr.world.dynamicBodies.data.canFind(bod._phys_body));
524         auto shape1 = bod._shapes.keys[0];
525         immutable auto collider1_size_x = (cast(geom.GeomBox)(shape1.geometry)).halfSize.x;
526         assert(collider1_size_x == 1,
527                 "collider #1 size from physics engine does not match provided collider size");
528 
529         // sync the colliders, then ensure that the registration is different
530         scn.reload_colliders();
531         auto shape2 = bod._shapes.keys[0];
532         assert(shape1 != shape2,
533                 "colliders were synced, which was supposed to reset collider registration, but entry was not changed");
534 
535         // replace the colliders
536         scn.replace_colliders();
537         assert(bod._shapes.length > 0, "registration entry for new collider missing");
538         auto shape3 = bod._shapes.keys[0];
539         immutable auto collider3_size_x = (cast(geom.GeomBox)(shape3.geometry)).halfSize.x;
540         assert(collider3_size_x == 2,
541                 "collider #3 size from physics engine does not match replaced collider size");
542 
543         test.game.destroy();
544     }
545 }