1 module unecht.core.components.physics.system;
2 
3 import derelict.ode.ode;
4 import derelict.util.system;
5 
6 import unecht.core.hideFlags;
7 import unecht.core.component;
8 import unecht.core.defaultInspector;
9 import unecht.core.components.physics.material;
10 
11 import gl3n.linalg;
12 
13 version(UEProfiling)import unecht.core.profiler;
14 
15 ///
16 @UEDefaultInspector!UEPhysicsSystem
17 final class UEPhysicsSystem : UEComponent {
18     
19     mixin(UERegisterObject!());
20     
21     static dWorldID world;
22     static dSpaceID space;
23     
24     private static bool initialised;
25     private static dJointGroupID contactgroup;
26     
27     ///
28     static void setGravity(vec3 _v)
29     {
30         dWorldSetGravity(world, _v.x,_v.y,_v.z);
31     }
32 
33     ///
34     override void onCreate() {
35 
36         sceneNode.hideFlags.set(HideFlags.hideInHirarchie);
37 
38         if(!initialised)
39         {
40             DerelictODE.load();
41             dInitODE ();
42 
43             // Create a new, empty world and assign its ID number to World. Most applications will only need one world.
44             world = dWorldCreate();
45             
46             // Create a new collision space and assign its ID number to Space, passing 0 instead of an existing dSpaceID.
47             // There are three different types of collision spaces we could create here depending on the number of objects 
48             // in the world but dSimpleSpaceCreate is fine for a small number of objects. If there were more objects we
49             // would be using dHashSpaceCreate or dQuadTreeSpaceCreate (look these up in the ODE docs)  
50             space = dSimpleSpaceCreate(null);
51             
52             // Create a joint group object and assign its ID number to contactgroup. dJointGroupCreate used to have a 
53             // max_size parameter but it is no longer used so we just pass 0 as its argument.
54             contactgroup = dJointGroupCreate(0);
55             
56             // Now we set the gravity vector for our world by passing World as the first argument to dWorldSetGravity.
57             // Earth's gravity vector would be (0, -9.81, 0) assuming that +Y is up. I found that a lighter gravity looked
58             // more realistic in this case. 
59             dWorldSetGravity(world, 0, -1, 0);
60             
61             // These next two functions control how much error correcting and constraint force mixing occurs in the world.
62             // Don't worry about these for now as they are set to the default values and we could happily delete them from
63             // this example. Different values, however, can drastically change the behaviour of the objects colliding, so
64             // I suggest you look up the full info on them in the ODE docs.
65             dWorldSetERP(world, 0.2);
66             
67             dWorldSetCFM(world, 1e-5);
68             
69             // This function sets the velocity that interpenetrating objects will separate at. The default value is infinity.
70             dWorldSetContactMaxCorrectingVel(world, 0.9);
71             
72             // This function sets the depth of the surface layer around the world objects. Contacts are allowed to sink into
73             // each other up to this depth. Setting it to a small value reduces the amount of jittering between contacting
74             // objects, the default value is 0.     
75             dWorldSetContactSurfaceLayer(world, 0.001);
76             
77             // To save some CPU time we set the auto disable flag to 1. This means that objects that have come to rest (based
78             // on their current linear and angular velocity) will no longer participate in the simulation, unless acted upon
79             // by a moving object. If you do not want to use this feature then set the flag to 0. You can also manually enable
80             // or disable objects using dBodyEnable and dBodyDisable, see the docs for more info on this.
81             dWorldSetAutoDisableFlag(world, 1);
82             
83             initialised = true;
84         }
85     }
86 
87     ///
88     override void onDestroy() {
89         if(initialised)
90         {
91             if(world)
92                 dWorldDestroy(world);
93 
94             if(contactgroup)
95                 dJointGroupDestroy(contactgroup);
96 
97             world = null;
98             contactgroup = null;
99 
100             initialised = false;
101 
102             dCloseODE();
103         }
104     }
105     
106     ///
107     override void onUpdate() {
108         version(UEProfiling)
109         auto profZone = Zone(profiler, "physics update");
110         
111         _collisionsThisFrame = 0;
112 
113         // Remove all temporary collision joints
114         dJointGroupEmpty(contactgroup);
115         
116         {
117             version(UEProfiling)
118                 auto profZone2 = Zone(profiler, "physics collide");
119             /++
120              + dSpaceCollide determines which pairs of geoms in the space we pass to it may potentially intersect. 
121              + We must also pass the address of a callback function that we will provide. 
122              + The callback function is responsible for determining which of the potential intersections 
123              + are actual collisions before adding the collision joints to our joint group called contactgroup, 
124              + this gives us the chance to set the behaviour of these joints before adding them to the group. 
125              + The second parameter is a pointer to any data that we may want to pass to our callback routine. 
126              + We will cover the details of the nearCallback routine in the next section.
127              +/
128             dSpaceCollide(space, null, &nearCallback);
129         }
130         
131         {
132             version(UEProfiling)
133                 auto profZone2 = Zone(profiler, "physics step");
134             /+ 
135              + Now we advance the simulation by calling dWorldQuickStep. 
136              + This is a faster version of dWorldStep but it is also 
137              + slightly less accurate. As well as the World object ID we also pass a step size value. 
138              + In each step the simulation is updated by a certain number of smaller steps or iterations. 
139              + The default number of iterations is 20 but you can change this by calling 
140              + dWorldSetQuickStepNumIterations.
141             +/
142             dWorldQuickStep(world, 0.05);
143         }
144         
145         propagateCollisions();
146     }
147     
148     ///
149     void propagateCollisions()
150     {
151         version(UEProfiling)
152             auto profZone = Zone(profiler, "physics propagate");
153         
154         foreach(col; _collisions[0.._collisionsThisFrame])
155         {
156             if(col.c1)
157                 col.c1.entity.broadcast!"onCollision"(col.c2);
158             if(col.c2)
159                 col.c2.entity.broadcast!"onCollision"(col.c1);
160         }
161     }
162     
163     ///
164     private struct Collision
165     {
166         UEComponent c1,c2;
167     }
168     
169     private static Collision[1024] _collisions;
170     private static uint _collisionsThisFrame;
171     
172     ///
173     private extern(C) @nogc nothrow static void nearCallback(void *data, dGeomID o1, dGeomID o2)
174     {
175         // Get the dynamics body for each geom
176         dBodyID b1 = dGeomGetBody(o1);
177         dBodyID b2 = dGeomGetBody(o2);
178         
179         void* gData1 = dGeomGetData(o1);
180         void* gData2 = dGeomGetData(o2);
181         
182         UEComponent comp1 = cast(UEComponent)gData1;
183         UEComponent comp2 = cast(UEComponent)gData2;
184         
185         UEPhysicsMaterialInfo m1,m2;
186         
187         if(comp1)
188             if(auto mat = comp1.entity.getComponent!UEPhysicsMaterial)
189                 m1 = mat.materialInfo;
190         
191         if(comp2)
192             if(auto mat = comp2.entity.getComponent!UEPhysicsMaterial)
193                 m2 = mat.materialInfo;
194         
195         dSurfaceParameters surfaceParams;
196         with(surfaceParams){
197             mode = 0;
198             
199             if(m1.isBouncy || m2.isBouncy)
200                 mode = dContactBounce;
201             
202             mu = (m1.friction + m2.friction) / 2.0f;
203 
204             if(m1.friction >= dInfinity || m2.friction >= dInfinity)
205                 mu = dInfinity;
206 
207             //mu2 = 0;
208             //rho = 0
209             //rho2 = 0;
210             //rhoN = 0;
211             bounce = (m1.bouncyness + m2.bouncyness) / 2.0f;
212             bounce_vel = 0.001;
213             //soft_erp = 0;
214             //soft_cfm = 0;
215             //motion1 = 0;
216             //motion2 = 0;
217             //motionN = 0;
218             //dReal slip1, slip2;
219         }
220         
221         // Create an array of dContact objects to hold the contact joints
222         static immutable CONTACT_COUNT = 32;
223         dContact[CONTACT_COUNT] contacts;
224         foreach(ref c; contacts)
225             c.surface = surfaceParams;
226         
227         // Here we do the actual collision test by calling dCollide. It returns the number of actual contact points or zero
228         // if there were none. As well as the geom IDs, max number of contacts we also pass the address of a dContactGeom
229         // as the fourth parameter. dContactGeom is a substructure of a dContact object so we simply pass the address of
230         // the first dContactGeom from our array of dContact objects and then pass the offset to the next dContactGeom
231         // as the fifth paramater, which is the size of a dContact structure. That made sense didn't it?  
232         if (int numc = dCollide(o1, o2, CONTACT_COUNT, &contacts[0].geom, dContact.sizeof))
233         {   
234             // To add each contact point found to our joint group we call dJointCreateContact which is just one of the many
235             // different joint types available.  
236             foreach (i; 0..numc)
237             {
238                 // dJointCreateContact needs to know which world and joint group to work with as well as the dContact
239                 // object itself. It returns a new dJointID which we then use with dJointAttach to finally create the
240                 // temporary contact joint between the two geom bodies.
241                 dJointID c = dJointCreateContact(UEPhysicsSystem.world, contactgroup, &contacts[i]);
242                 
243                 dJointAttach(c, b1, b2);    
244             }
245 
246             auto component1 = cast(UEComponent)gData1;
247             auto component2 = cast(UEComponent)gData2;
248 
249             if(_collisionsThisFrame<_collisions.length)
250                 _collisions[_collisionsThisFrame++] = Collision(component1,component2);
251             else
252                 assert(false);
253         }
254     }
255 }