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