1 /*
2  * Copyright (c) 2007-2013 Scott Lembcke and Howling Moon Software
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  */
22 module demo.ChipmunkDemo;
23 
24 import core.memory;
25 
26 import core.stdc.stdio;
27 import core.stdc.stdlib;
28 
29 import std.exception;
30 import std.stdio;
31 import std.string;
32 
33 alias stderr = std.stdio.stderr;
34 
35 import glad.gl.all;
36 import glad.gl.loader;
37 
38 import glwtf.input;
39 import glwtf.window;
40 
41 import deimos.glfw.glfw3;
42 
43 import demo.dchip;
44 
45 import demo.Bench;
46 import demo.ChipmunkDebugDraw;
47 import demo.ChipmunkDemoTextSupport;
48 import demo.glu;
49 import demo.types;
50 
51 import demo.LogoSmash;
52 import demo.PyramidStack;
53 import demo.Plink;
54 import demo.Tumble;
55 import demo.PyramidTopple;
56 import demo.Planet;
57 import demo.Springies;
58 import demo.Pump;
59 import demo.TheoJansen;
60 import demo.Query;
61 import demo.OneWay;
62 import demo.Joints;
63 import demo.Tank;
64 import demo.Chains;
65 import demo.Crane;
66 import demo.ContactGraph;
67 import demo.Buoyancy;
68 import demo.Player;
69 import demo.Slice;
70 import demo.Convex;
71 import demo.Unicycle;
72 import demo.Sticky;
73 import demo.Shatter;
74 import demo.RollBall;
75 
76 ChipmunkDemo[] demo_list;
77 shared static this()
78 {
79     demo_list = [
80         LogoSmash,
81         PyramidStack,
82         Plink,
83         BouncyHexagons,
84         Tumble,
85         PyramidTopple,
86         Planet,
87         Springies,
88         Pump,
89         TheoJansen,
90         Query,
91         OneWay,
92         Joints,
93         Tank,
94         Chains,
95         Crane,
96         ContactGraph,
97         Buoyancy,
98         Player,
99         Slice,
100         Convex,
101         Unicycle,
102         Sticky,
103         Shatter,
104         RollBall,
105     ];
106 }
107 
108 alias ChipmunkDemoInitFunc = cpSpace* function();
109 alias ChipmunkDemoUpdateFunc = void function(cpSpace* space, double dt);
110 alias ChipmunkDemoDrawFunc = void function(cpSpace* space);
111 alias ChipmunkDemoDestroyFunc = void function(cpSpace* space);
112 
113 __gshared GLFWwindow* window;
114 
115 struct ChipmunkDemo
116 {
117     string name;
118     double timestep = 0;
119 
120     ChipmunkDemoInitFunc initFunc;
121     ChipmunkDemoUpdateFunc updateFunc;
122     ChipmunkDemoDrawFunc drawFunc;
123 
124     ChipmunkDemoDestroyFunc destroyFunc;
125 }
126 
127 cpFloat frand()
128 {
129     return cast(cpFloat)rand() / cast(cpFloat)RAND_MAX;
130 }
131 
132 cpVect frand_unit_circle()
133 {
134     cpVect v = cpv(frand() * 2.0f - 1.0f, frand() * 2.0f - 1.0f);
135     return (cpvlengthsq(v) < 1.0f ? v : frand_unit_circle());
136 }
137 
138 enum GRABABLE_MASK_BIT = (1 << 31);
139 enum NOT_GRABABLE_MASK = (~GRABABLE_MASK_BIT);
140 
141 void ChipmunkDemoDefaultDrawImpl(cpSpace* space);
142 void ChipmunkDemoFreeSpaceChildren(cpSpace* space);
143 
144 ChipmunkDemo* demos;
145 size_t demo_count = 0;
146 size_t demo_index = 0;
147 
148 cpBool paused = cpFalse;
149 cpBool step   = cpFalse;
150 
151 cpSpace* space;
152 
153 double Accumulator = 0.0;
154 double LastTime    = 0.0;
155 int ChipmunkDemoTicks = 0;
156 double ChipmunkDemoTime = 0;
157 
158 cpVect ChipmunkDemoMouse;
159 cpBool ChipmunkDemoRightClick = cpFalse;
160 cpBool ChipmunkDemoRightDown  = cpFalse;
161 cpVect ChipmunkDemoKeyboard;
162 
163 cpBody* mouse_body        = null;
164 cpConstraint* mouse_joint = null;
165 
166 char[] ChipmunkDemoMessageString;
167 
168 cpVect  translate = { 0, 0 };
169 cpFloat scale = 1.0;
170 
171 void ShapeFreeWrap(cpSpace* space, cpShape* shape, void* unused)
172 {
173     cpSpaceRemoveShape(space, shape);
174     cpShapeFree(shape);
175 }
176 
177 void PostShapeFree(cpShape* shape, cpSpace* space)
178 {
179     cpSpaceAddPostStepCallback(space, safeCast!cpPostStepFunc(&ShapeFreeWrap), shape, null);
180 }
181 
182 void ConstraintFreeWrap(cpSpace* space, cpConstraint* constraint, void* unused)
183 {
184     cpSpaceRemoveConstraint(space, constraint);
185     cpConstraintFree(constraint);
186 }
187 
188 void PostConstraintFree(cpConstraint* constraint, cpSpace* space)
189 {
190     cpSpaceAddPostStepCallback(space, safeCast!cpPostStepFunc(&ConstraintFreeWrap), constraint, null);
191 }
192 
193 void BodyFreeWrap(cpSpace* space, cpBody* body_, void* unused)
194 {
195     cpSpaceRemoveBody(space, body_);
196     cpBodyFree(body_);
197 }
198 
199 void PostBodyFree(cpBody* body_, cpSpace* space)
200 {
201     cpSpaceAddPostStepCallback(space, safeCast!cpPostStepFunc(&BodyFreeWrap), body_, null);
202 }
203 
204 // Safe and future proof way to remove and free all objects that have been added to the space.
205 void ChipmunkDemoFreeSpaceChildren(cpSpace* space)
206 {
207     // Must remove these BEFORE freeing the body_ or you will access dangling pointers.
208     cpSpaceEachShape(space, safeCast!cpSpaceShapeIteratorFunc(&PostShapeFree), space);
209     cpSpaceEachConstraint(space, safeCast!cpSpaceConstraintIteratorFunc(&PostConstraintFree), space);
210 
211     cpSpaceEachBody(space, safeCast!cpSpaceBodyIteratorFunc(&PostBodyFree), space);
212 }
213 
214 void ChipmunkDemoDefaultDrawImpl(cpSpace* space)
215 {
216     ChipmunkDebugDrawShapes(space);
217     ChipmunkDebugDrawConstraints(space);
218     ChipmunkDebugDrawCollisionPoints(space);
219 }
220 
221 void DrawInstructions()
222 {
223     ChipmunkDemoTextDrawString(cpv(-300, 220),
224                                "Controls:\n"
225                                "A - * Switch demos. (return restarts)\n"
226                                "Use the mouse to grab objects.\n"
227                                );
228 }
229 
230 int max_arbiters    = 0;
231 int max_points      = 0;
232 int max_constraints = 0;
233 
234 void DrawInfo()
235 {
236     int arbiters = space.arbiters.num;
237     int points   = 0;
238 
239     for (int i = 0; i < arbiters; i++)
240         points += (cast(cpArbiter*)(space.arbiters.arr[i])).numContacts;
241 
242     int constraints = (space.constraints.num + points) * space.iterations;
243 
244     max_arbiters    = arbiters > max_arbiters ? arbiters : max_arbiters;
245     max_points      = points > max_points ? points : max_points;
246     max_constraints = constraints > max_constraints ? constraints : max_constraints;
247 
248     char[1024] buffer = 0;
249     string format =
250         "Arbiters: %d (%d) - "
251         "Contact Points: %d (%d)\n"
252         "Other Constraints: %d, Iterations: %d\n"
253         "Constraints x Iterations: %d (%d)\n"
254         "Time:% 5.2fs, KE:% 5.2e\0";
255 
256     cpArray* bodies = space.bodies;
257     cpFloat  ke     = 0.0f;
258 
259     for (int i = 0; i < bodies.num; i++)
260     {
261         cpBody* body_ = cast(cpBody*)bodies.arr[i];
262 
263         if (body_.m == INFINITY || body_.i == INFINITY)
264             continue;
265 
266         ke += body_.m * cpvdot(body_.v, body_.v) + body_.i * body_.w * body_.w;
267     }
268 
269     sprintf(buffer.ptr, format.ptr,
270             arbiters, max_arbiters,
271             points, max_points,
272             space.constraints.num, space.iterations,
273             constraints, max_constraints,
274             ChipmunkDemoTime, (ke < 1e-10f ? 0.0f : ke)
275             );
276 
277     ChipmunkDemoTextDrawString(cpv(0, 220), buffer);
278 }
279 
280 char  PrintStringBuffer[1024 * 8] = 0;
281 size_t PrintStringCursor;
282 
283 void ChipmunkDemoPrintString(Args...)(string fmt, Args args)
284 {
285     ChipmunkDemoMessageString = PrintStringBuffer[];
286     PrintStringCursor += sformat(PrintStringBuffer[PrintStringCursor .. $], fmt, args).length;
287 }
288 
289 void Tick(double dt)
290 {
291     if (!paused || step)
292     {
293         PrintStringBuffer[0] = 0;
294         PrintStringCursor = 0;
295 
296         // Completely reset the renderer only at the beginning of a tick.
297         // That way it can always display at least the last ticks' debug drawing.
298         ChipmunkDebugDrawClearRenderer();
299         ChipmunkDemoTextClearRenderer();
300 
301         cpVect new_point = cpvlerp(mouse_body.p, ChipmunkDemoMouse, 0.25f);
302         mouse_body.v = cpvmult(cpvsub(new_point, mouse_body.p), 60.0f);
303         mouse_body.p = new_point;
304 
305         demos[demo_index].updateFunc(space, dt);
306 
307         ChipmunkDemoTicks++;
308         ChipmunkDemoTime += dt;
309 
310         step = cpFalse;
311         ChipmunkDemoRightDown = cpFalse;
312 
313         ChipmunkDemoTextDrawString(cpv(-300, -200), ChipmunkDemoMessageString);
314     }
315 }
316 
317 void Update()
318 {
319     double time = glfwGetTime();
320     double dt   = time - LastTime;
321 
322     if (dt > 0.2)
323         dt = 0.2;
324 
325     double fixed_dt = demos[demo_index].timestep;
326 
327     for (Accumulator += dt; Accumulator > fixed_dt; Accumulator -= fixed_dt)
328     {
329         Tick(fixed_dt);
330     }
331 
332     LastTime = time;
333 }
334 
335 void Display()
336 {
337     glMatrixMode(GL_MODELVIEW);
338     glLoadIdentity();
339     glTranslatef(cast(GLfloat)translate.x, cast(GLfloat)translate.y, 0.0f);
340     glScalef(cast(GLfloat)scale, cast(GLfloat)scale, 1.0f);
341 
342     Update();
343 
344     ChipmunkDebugDrawPushRenderer();
345     demos[demo_index].drawFunc(space);
346 
347     // Highlight the shape under the mouse because it looks neat.
348     cpShape* nearest = cpSpaceNearestPointQueryNearest(space, ChipmunkDemoMouse, 0.0f, CP_ALL_LAYERS, CP_NO_GROUP, null);
349 
350     if (nearest)
351         ChipmunkDebugDrawShape(nearest, RGBAColor(1.0f, 0.0f, 0.0f, 1.0f), LAColor(0.0f, 0.0f));
352 
353     // Draw the renderer contents and reset it back to the last tick's state.
354     ChipmunkDebugDrawFlushRenderer();
355     ChipmunkDebugDrawPopRenderer();
356 
357     ChipmunkDemoTextPushRenderer();
358 
359     // Now render all the UI text.
360     DrawInstructions();
361     DrawInfo();
362 
363     glMatrixMode(GL_MODELVIEW);
364     glPushMatrix();
365     {
366         // Draw the text at fixed positions,
367         // but save the drawing matrix for the mouse picking
368         glLoadIdentity();
369 
370         ChipmunkDemoTextFlushRenderer();
371         ChipmunkDemoTextPopRenderer();
372     }
373     glPopMatrix();
374 
375     glfwSwapBuffers(window);
376     glClear(GL_COLOR_BUFFER_BIT);
377 }
378 
379 extern(C) void Reshape(GLFWwindow* window, int width, int height)
380 {
381     glViewport(0, 0, width, height);
382 
383     float scale = cast(float)cpfmin(width / 640.0, height / 480.0);
384     float hw    = width * (0.5f / scale);
385     float hh    = height * (0.5f / scale);
386 
387     ChipmunkDebugDrawPointLineScale = scale;
388     glLineWidth(cast(GLfloat)scale);
389 
390     glMatrixMode(GL_PROJECTION);
391     glLoadIdentity();
392     gluOrtho2D(-hw, hw, -hh, hh);
393 }
394 
395 char[] DemoTitle(size_t index)
396 {
397     static char[1024] title;
398     title[] = 0;
399     sformat(title, "Demo(%s): %s", cast(char)('a' + index), demos[demo_index].name);
400     return title;
401 }
402 
403 void RunDemo(size_t index)
404 {
405     srand(45073);
406 
407     demo_index = index;
408 
409     ChipmunkDemoTicks = 0;
410     ChipmunkDemoTime  = 0.0;
411     Accumulator       = 0.0;
412     LastTime = glfwGetTime();
413 
414     mouse_joint = null;
415     ChipmunkDemoMessageString = "\0".dup;
416     max_arbiters    = 0;
417     max_points      = 0;
418     max_constraints = 0;
419     space = demos[demo_index].initFunc();
420 
421     enforce(window !is null);
422     glfwSetWindowTitle(window, DemoTitle(index).toStringz);
423 }
424 
425 extern(C) void Keyboard(GLFWwindow* window, int key, int scancode, int state, int modifier)
426 {
427     if (state != GLFW_REPEAT)  // we ignore repeat
428     switch (key)
429     {
430         case GLFW_KEY_UP:
431             ChipmunkDemoKeyboard.y += (state == GLFW_PRESS ?  1.0 : -1.0);
432             break;
433 
434         case GLFW_KEY_DOWN:
435             ChipmunkDemoKeyboard.y += (state == GLFW_PRESS ? -1.0 :  1.0);
436             break;
437 
438         case GLFW_KEY_RIGHT:
439             ChipmunkDemoKeyboard.x += (state == GLFW_PRESS ?  1.0 : -1.0);
440             break;
441 
442         case GLFW_KEY_LEFT:
443             ChipmunkDemoKeyboard.x += (state == GLFW_PRESS ? -1.0 :  1.0);
444             break;
445 
446         default:
447             break;
448     }
449 
450     if (key == GLFW_KEY_ESCAPE && (state == GLFW_PRESS || state == GLFW_REPEAT))
451         glfwSetWindowShouldClose(window, true);
452 
453     // We ignore release for these next keys.
454     if (state == GLFW_RELEASE)
455         return;
456 
457     int index = key - GLFW_KEY_A;
458 
459     if (0 <= index && index < demo_count)
460     {
461         demos[demo_index].destroyFunc(space);
462         RunDemo(index);
463     }
464     else if (key == ' ')
465     {
466         demos[demo_index].destroyFunc(space);
467         RunDemo(demo_index);
468     }
469     else if (key == '`')
470     {
471         paused = !paused;
472     }
473     else if (key == '1')
474     {
475         step = cpTrue;
476     }
477     else if (key == '\\')
478     {
479         glDisable(GL_LINE_SMOOTH);
480         glDisable(GL_POINT_SMOOTH);
481     }
482 
483     GLfloat translate_increment = 50.0f / cast(GLfloat)scale;
484     GLfloat scale_increment     = 1.2f;
485 
486     if (key == '5')
487     {
488         translate.x = 0.0f;
489         translate.y = 0.0f;
490         scale       = 1.0f;
491     }
492     else if (key == '4')
493     {
494         translate.x += translate_increment;
495     }
496     else if (key == '6')
497     {
498         translate.x -= translate_increment;
499     }
500     else if (key == '2')
501     {
502         translate.y += translate_increment;
503     }
504     else if (key == '8')
505     {
506         translate.y -= translate_increment;
507     }
508     else if (key == '7')
509     {
510         scale /= scale_increment;
511     }
512     else if (key == '9')
513     {
514         scale *= scale_increment;
515     }
516 }
517 
518 cpVect MouseToSpace(double x, double y)
519 {
520     GLdouble model[16] = 0;
521     glGetDoublev(GL_MODELVIEW_MATRIX, model.ptr);
522 
523     GLdouble proj[16] = 0;
524     glGetDoublev(GL_PROJECTION_MATRIX, proj.ptr);
525 
526     GLint view[4] = 0;
527     glGetIntegerv(GL_VIEWPORT, view.ptr);
528 
529     int ww, wh;
530     glfwGetWindowSize(window, &ww, &wh);
531 
532     GLdouble mx = 0, my = 0, mz = 0;
533     gluUnProject(x, wh - y, 0.0f, model, proj, view, &mx, &my, &mz);
534 
535     return cpv(mx, my);
536 }
537 
538 extern(C) void Mouse(GLFWwindow* window, double x, double y)
539 {
540     ChipmunkDemoMouse = MouseToSpace(x, y);
541 }
542 
543 extern(C) void Click(GLFWwindow* window, int button, int state, int mods)
544 {
545     if (button == GLFW_MOUSE_BUTTON_1)
546     {
547         if (state == GLFW_PRESS)
548         {
549             cpShape* shape = cpSpacePointQueryFirst(space, ChipmunkDemoMouse, GRABABLE_MASK_BIT, CP_NO_GROUP);
550 
551             if (shape && !cpBodyIsStatic(shape.body_))
552             {
553                 cpBody* body_ = shape.body_;
554                 mouse_joint = cpPivotJointNew2(mouse_body, body_, cpvzero, cpBodyWorld2Local(body_, ChipmunkDemoMouse));
555                 mouse_joint.maxForce  = 50000.0f;
556                 mouse_joint.errorBias = cpfpow(1.0f - 0.15f, 60.0f);
557                 cpSpaceAddConstraint(space, mouse_joint);
558             }
559         }
560         else if (mouse_joint)
561         {
562             cpSpaceRemoveConstraint(space, mouse_joint);
563             cpConstraintFree(mouse_joint);
564             mouse_joint = null;
565         }
566     }
567     else if (button == GLFW_MOUSE_BUTTON_2)
568     {
569         ChipmunkDemoRightDown = ChipmunkDemoRightClick = (state == GLFW_PRESS);
570     }
571 }
572 
573 extern(C) void WindowClose(GLFWwindow* window)
574 {
575     glfwTerminate();
576     glfwSetWindowShouldClose(window, true);
577 }
578 
579 void SetupGL()
580 {
581     ChipmunkDebugDrawInit();
582     ChipmunkDemoTextInit();
583 
584     glClearColor(52.0f / 255.0f, 62.0f / 255.0f, 72.0f / 255.0f, 1.0f);
585     glClear(GL_COLOR_BUFFER_BIT);
586 
587     glEnable(GL_LINE_SMOOTH);
588     glEnable(GL_POINT_SMOOTH);
589 
590     glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
591     glHint(GL_POINT_SMOOTH_HINT, GL_DONT_CARE);
592 
593     glEnable(GL_BLEND);
594     glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
595 }
596 
597 void TimeTrial(int index, int count)
598 {
599     space = demos[index].initFunc();
600 
601     double start_time = glfwGetTime();
602     double dt         = demos[index].timestep;
603 
604     for (int i = 0; i < count; i++)
605         demos[index].updateFunc(space, dt);
606 
607     double end_time = glfwGetTime();
608 
609     demos[index].destroyFunc(space);
610 
611     printf("Time(%c) = %8.2f ms (%s)\n", index + 'a', (end_time - start_time) * 1e3f, demos[index].name.toStringz);
612 }
613 
614 enum WindowMode
615 {
616     fullscreen,
617     windowed,
618 }
619 
620 /* Wrapper around the glwtf API. */
621 Window createWindow(string windowName, WindowMode windowMode, int width, int height)
622 {
623     auto window = new Window();
624     auto monitor = windowMode == WindowMode.fullscreen ? glfwGetPrimaryMonitor() : null;
625     bool forward_compat = false;
626     auto cv = window.create_highest_available_context(width, height, windowName, monitor, null, GLFW_OPENGL_COMPAT_PROFILE, forward_compat);
627     return window;
628 }
629 
630 void on_glfw_error(int code, string msg)
631 {
632     stderr.writefln("Error (%s): %s", code, msg);
633 }
634 
635 int main(string[] args)
636 {
637     GC.disable();
638     scope (exit)
639         GC.enable();
640 
641     // Segment/segment collisions need to be explicitly enabled currently.
642     // This will becoume enabled by default in future versions of Chipmunk.
643     cpEnableSegmentToSegmentCollisions();
644 
645     demos      = demo_list.ptr;
646     demo_count = demo_list.length;
647     int trial = 0;
648 
649     foreach (arg; args[1 .. $])
650     {
651         if (arg == "-bench")
652         {
653             demos      = cast(ChipmunkDemo*)bench_list.ptr;
654             demo_count = bench_count;
655         }
656         else
657         if (arg == "-trial")
658         {
659             trial = 1;
660         }
661     }
662 
663     if (trial)
664     {
665         cpAssertHard(glfwInit(), "Error initializing GLFW.");
666 
667         //		sleep(1);
668         for (int i = 0; i < demo_count; i++)
669             TimeTrial(i, 1000);
670 
671         //		time_trial('d' - 'a', 10000);
672         return 0;
673     }
674     else
675     {
676         mouse_body = cpBodyNew(INFINITY, INFINITY);
677 
678         // initialize glwf
679         auto res = glfwInit();
680         enforce(res, format("glfwInit call failed with return code: '%s'", res));
681         scope(exit)
682             glfwTerminate();
683 
684         int width = 640;
685         int height = 480;
686 
687         window = enforce(createWindow("Hello World", WindowMode.windowed, width, height).window,
688                          "glfwCreateWindow call failed.");
689 
690         register_glfw_error_callback(&on_glfw_error);
691 
692         // Make the window's context current
693         glfwMakeContextCurrent(window);
694 
695         // Load all OpenGL function pointers via glad.
696         enforce(gladLoadGL());
697 
698         // Support only GL 3x+
699         enforce(GLVersion.major >= 3);
700 
701         SetupGL();
702 
703         // glfw3 doesn't want to automatically do this the first time the window is shown
704         Reshape(window, 640, 480);
705 
706         glfwSetWindowSizeCallback(window, &Reshape);
707         glfwSetKeyCallback(window, &Keyboard);
708 
709         glfwSetCursorPosCallback(window, &Mouse);
710         glfwSetMouseButtonCallback(window, &Click);
711 
712         RunDemo(demo_index);
713 
714         /* Loop until the user closes the window */
715         while (!glfwWindowShouldClose(window))
716         {
717             /* Poll for and process events */
718             glfwPollEvents();
719 
720             /* Render here */
721             Display();
722         }
723     }
724 
725     return 0;
726 }