1 // MIT License
2
3 // Copyright (c) 2019 Erin Catto
4
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11
12 // The above copyright notice and this permission notice shall be included in all
13 // copies or substantial portions of the Software.
14
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22
23 #define _CRT_SECURE_NO_WARNINGS
24 #define IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1
25
26 #include "imgui/imgui.h"
27 #include "imgui_impl_glfw.h"
28 #include "imgui_impl_opengl3.h"
29 #include "draw.h"
30 #include "settings.h"
31 #include "test.h"
32
33 #include <algorithm>
34 #include <stdio.h>
35 #include <thread>
36 #include <chrono>
37
38 #if defined(_WIN32)
39 #include <crtdbg.h>
40 #endif
41
42 GLFWwindow* g_mainWindow = nullptr;
43 static int32 s_testSelection = 0;
44 static Test* s_test = nullptr;
45 static Settings s_settings;
46 static bool s_rightMouseDown = false;
47 static b2Vec2 s_clickPointWS = b2Vec2_zero;
48
glfwErrorCallback(int error,const char * description)49 void glfwErrorCallback(int error, const char* description)
50 {
51 fprintf(stderr, "GLFW error occured. Code: %d. Description: %s\n", error, description);
52 }
53
CompareTests(const TestEntry & a,const TestEntry & b)54 static inline bool CompareTests(const TestEntry& a, const TestEntry& b)
55 {
56 int result = strcmp(a.category, b.category);
57 if (result == 0)
58 {
59 result = strcmp(a.name, b.name);
60 }
61
62 return result < 0;
63 }
64
SortTests()65 static void SortTests()
66 {
67 std::sort(g_testEntries, g_testEntries + g_testCount, CompareTests);
68 }
69
CreateUI(GLFWwindow * window,const char * glslVersion=NULL)70 static void CreateUI(GLFWwindow* window, const char* glslVersion = NULL)
71 {
72 IMGUI_CHECKVERSION();
73 ImGui::CreateContext();
74
75 bool success;
76 success = ImGui_ImplGlfw_InitForOpenGL(window, false);
77 if (success == false)
78 {
79 printf("ImGui_ImplGlfw_InitForOpenGL failed\n");
80 assert(false);
81 }
82
83 success = ImGui_ImplOpenGL3_Init(glslVersion);
84 if (success == false)
85 {
86 printf("ImGui_ImplOpenGL3_Init failed\n");
87 assert(false);
88 }
89
90 // Search for font file
91 const char* fontPath1 = "data/droid_sans.ttf";
92 const char* fontPath2 = "../data/droid_sans.ttf";
93 const char* fontPath = nullptr;
94 FILE* file1 = fopen(fontPath1, "rb");
95 FILE* file2 = fopen(fontPath2, "rb");
96 if (file1)
97 {
98 fontPath = fontPath1;
99 fclose(file1);
100 }
101
102 if (file2)
103 {
104 fontPath = fontPath2;
105 fclose(file2);
106 }
107
108 if (fontPath)
109 {
110 ImGui::GetIO().Fonts->AddFontFromFileTTF(fontPath, 13.0f);
111 }
112 }
113
ResizeWindowCallback(GLFWwindow *,int width,int height)114 static void ResizeWindowCallback(GLFWwindow*, int width, int height)
115 {
116 g_camera.m_width = width;
117 g_camera.m_height = height;
118 s_settings.m_windowWidth = width;
119 s_settings.m_windowHeight = height;
120 }
121
KeyCallback(GLFWwindow * window,int key,int scancode,int action,int mods)122 static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
123 {
124 ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
125 if (ImGui::GetIO().WantCaptureKeyboard)
126 {
127 return;
128 }
129
130 if (action == GLFW_PRESS)
131 {
132 switch (key)
133 {
134 case GLFW_KEY_ESCAPE:
135 // Quit
136 glfwSetWindowShouldClose(g_mainWindow, GL_TRUE);
137 break;
138
139 case GLFW_KEY_LEFT:
140 // Pan left
141 if (mods == GLFW_MOD_CONTROL)
142 {
143 b2Vec2 newOrigin(2.0f, 0.0f);
144 s_test->ShiftOrigin(newOrigin);
145 }
146 else
147 {
148 g_camera.m_center.x -= 0.5f;
149 }
150 break;
151
152 case GLFW_KEY_RIGHT:
153 // Pan right
154 if (mods == GLFW_MOD_CONTROL)
155 {
156 b2Vec2 newOrigin(-2.0f, 0.0f);
157 s_test->ShiftOrigin(newOrigin);
158 }
159 else
160 {
161 g_camera.m_center.x += 0.5f;
162 }
163 break;
164
165 case GLFW_KEY_DOWN:
166 // Pan down
167 if (mods == GLFW_MOD_CONTROL)
168 {
169 b2Vec2 newOrigin(0.0f, 2.0f);
170 s_test->ShiftOrigin(newOrigin);
171 }
172 else
173 {
174 g_camera.m_center.y -= 0.5f;
175 }
176 break;
177
178 case GLFW_KEY_UP:
179 // Pan up
180 if (mods == GLFW_MOD_CONTROL)
181 {
182 b2Vec2 newOrigin(0.0f, -2.0f);
183 s_test->ShiftOrigin(newOrigin);
184 }
185 else
186 {
187 g_camera.m_center.y += 0.5f;
188 }
189 break;
190
191 case GLFW_KEY_HOME:
192 // Reset view
193 g_camera.m_zoom = 1.0f;
194 g_camera.m_center.Set(0.0f, 20.0f);
195 break;
196
197 case GLFW_KEY_Z:
198 // Zoom out
199 g_camera.m_zoom = b2Min(1.1f * g_camera.m_zoom, 20.0f);
200 break;
201
202 case GLFW_KEY_X:
203 // Zoom in
204 g_camera.m_zoom = b2Max(0.9f * g_camera.m_zoom, 0.02f);
205 break;
206
207 case GLFW_KEY_R:
208 // Reset test
209 delete s_test;
210 s_test = g_testEntries[s_settings.m_testIndex].createFcn();
211 break;
212
213 case GLFW_KEY_SPACE:
214 // Launch a bomb.
215 if (s_test)
216 {
217 s_test->LaunchBomb();
218 }
219 break;
220
221 case GLFW_KEY_O:
222 s_settings.m_singleStep = true;
223 break;
224
225 case GLFW_KEY_P:
226 s_settings.m_pause = !s_settings.m_pause;
227 break;
228
229 case GLFW_KEY_LEFT_BRACKET:
230 // Switch to previous test
231 --s_testSelection;
232 if (s_testSelection < 0)
233 {
234 s_testSelection = g_testCount - 1;
235 }
236 break;
237
238 case GLFW_KEY_RIGHT_BRACKET:
239 // Switch to next test
240 ++s_testSelection;
241 if (s_testSelection == g_testCount)
242 {
243 s_testSelection = 0;
244 }
245 break;
246
247 case GLFW_KEY_TAB:
248 g_debugDraw.m_showUI = !g_debugDraw.m_showUI;
249
250 default:
251 if (s_test)
252 {
253 s_test->Keyboard(key);
254 }
255 }
256 }
257 else if (action == GLFW_RELEASE)
258 {
259 s_test->KeyboardUp(key);
260 }
261 // else GLFW_REPEAT
262 }
263
CharCallback(GLFWwindow * window,unsigned int c)264 static void CharCallback(GLFWwindow* window, unsigned int c)
265 {
266 ImGui_ImplGlfw_CharCallback(window, c);
267 }
268
MouseButtonCallback(GLFWwindow * window,int32 button,int32 action,int32 mods)269 static void MouseButtonCallback(GLFWwindow* window, int32 button, int32 action, int32 mods)
270 {
271 ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods);
272
273 double xd, yd;
274 glfwGetCursorPos(g_mainWindow, &xd, &yd);
275 b2Vec2 ps((float)xd, (float)yd);
276
277 // Use the mouse to move things around.
278 if (button == GLFW_MOUSE_BUTTON_1)
279 {
280 //<##>
281 //ps.Set(0, 0);
282 b2Vec2 pw = g_camera.ConvertScreenToWorld(ps);
283 if (action == GLFW_PRESS)
284 {
285 if (mods == GLFW_MOD_SHIFT)
286 {
287 s_test->ShiftMouseDown(pw);
288 }
289 else
290 {
291 s_test->MouseDown(pw);
292 }
293 }
294
295 if (action == GLFW_RELEASE)
296 {
297 s_test->MouseUp(pw);
298 }
299 }
300 else if (button == GLFW_MOUSE_BUTTON_2)
301 {
302 if (action == GLFW_PRESS)
303 {
304 s_clickPointWS = g_camera.ConvertScreenToWorld(ps);
305 s_rightMouseDown = true;
306 }
307
308 if (action == GLFW_RELEASE)
309 {
310 s_rightMouseDown = false;
311 }
312 }
313 }
314
MouseMotionCallback(GLFWwindow *,double xd,double yd)315 static void MouseMotionCallback(GLFWwindow*, double xd, double yd)
316 {
317 b2Vec2 ps((float)xd, (float)yd);
318
319 b2Vec2 pw = g_camera.ConvertScreenToWorld(ps);
320 s_test->MouseMove(pw);
321
322 if (s_rightMouseDown)
323 {
324 b2Vec2 diff = pw - s_clickPointWS;
325 g_camera.m_center.x -= diff.x;
326 g_camera.m_center.y -= diff.y;
327 s_clickPointWS = g_camera.ConvertScreenToWorld(ps);
328 }
329 }
330
ScrollCallback(GLFWwindow * window,double dx,double dy)331 static void ScrollCallback(GLFWwindow* window, double dx, double dy)
332 {
333 ImGui_ImplGlfw_ScrollCallback(window, dx, dy);
334 if (ImGui::GetIO().WantCaptureMouse)
335 {
336 return;
337 }
338
339 if (dy > 0)
340 {
341 g_camera.m_zoom /= 1.1f;
342 }
343 else
344 {
345 g_camera.m_zoom *= 1.1f;
346 }
347 }
348
RestartTest()349 static void RestartTest()
350 {
351 delete s_test;
352 s_test = g_testEntries[s_settings.m_testIndex].createFcn();
353 }
354
UpdateUI()355 static void UpdateUI()
356 {
357 int menuWidth = 180;
358 if (g_debugDraw.m_showUI)
359 {
360 ImGui::SetNextWindowPos(ImVec2((float)g_camera.m_width - menuWidth - 10, 10));
361 ImGui::SetNextWindowSize(ImVec2((float)menuWidth, (float)g_camera.m_height - 20));
362
363 ImGui::Begin("Tools", &g_debugDraw.m_showUI, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
364
365 if (ImGui::BeginTabBar("ControlTabs", ImGuiTabBarFlags_None))
366 {
367 if (ImGui::BeginTabItem("Controls"))
368 {
369 ImGui::SliderInt("Vel Iters", &s_settings.m_velocityIterations, 0, 50);
370 ImGui::SliderInt("Pos Iters", &s_settings.m_positionIterations, 0, 50);
371 ImGui::SliderFloat("Hertz", &s_settings.m_hertz, 5.0f, 120.0f, "%.0f hz");
372
373 ImGui::Separator();
374
375 ImGui::Checkbox("Sleep", &s_settings.m_enableSleep);
376 ImGui::Checkbox("Warm Starting", &s_settings.m_enableWarmStarting);
377 ImGui::Checkbox("Time of Impact", &s_settings.m_enableContinuous);
378 ImGui::Checkbox("Sub-Stepping", &s_settings.m_enableSubStepping);
379
380 ImGui::Separator();
381
382 ImGui::Checkbox("Shapes", &s_settings.m_drawShapes);
383 ImGui::Checkbox("Joints", &s_settings.m_drawJoints);
384 ImGui::Checkbox("AABBs", &s_settings.m_drawAABBs);
385 ImGui::Checkbox("Contact Points", &s_settings.m_drawContactPoints);
386 ImGui::Checkbox("Contact Normals", &s_settings.m_drawContactNormals);
387 ImGui::Checkbox("Contact Impulses", &s_settings.m_drawContactImpulse);
388 ImGui::Checkbox("Friction Impulses", &s_settings.m_drawFrictionImpulse);
389 ImGui::Checkbox("Center of Masses", &s_settings.m_drawCOMs);
390 ImGui::Checkbox("Statistics", &s_settings.m_drawStats);
391 ImGui::Checkbox("Profile", &s_settings.m_drawProfile);
392
393 ImVec2 button_sz = ImVec2(-1, 0);
394 if (ImGui::Button("Pause (P)", button_sz))
395 {
396 s_settings.m_pause = !s_settings.m_pause;
397 }
398
399 if (ImGui::Button("Single Step (O)", button_sz))
400 {
401 s_settings.m_singleStep = !s_settings.m_singleStep;
402 }
403
404 if (ImGui::Button("Restart (R)", button_sz))
405 {
406 RestartTest();
407 }
408
409 if (ImGui::Button("Quit", button_sz))
410 {
411 glfwSetWindowShouldClose(g_mainWindow, GL_TRUE);
412 }
413
414 ImGui::EndTabItem();
415 }
416
417 ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
418 leafNodeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
419
420 ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
421
422 if (ImGui::BeginTabItem("Tests"))
423 {
424 int categoryIndex = 0;
425 const char* category = g_testEntries[categoryIndex].category;
426 int i = 0;
427 while (i < g_testCount)
428 {
429 bool categorySelected = strcmp(category, g_testEntries[s_settings.m_testIndex].category) == 0;
430 ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags_Selected : 0;
431 bool nodeOpen = ImGui::TreeNodeEx(category, nodeFlags | nodeSelectionFlags);
432
433 if (nodeOpen)
434 {
435 while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0)
436 {
437 ImGuiTreeNodeFlags selectionFlags = 0;
438 if (s_settings.m_testIndex == i)
439 {
440 selectionFlags = ImGuiTreeNodeFlags_Selected;
441 }
442 ImGui::TreeNodeEx((void*)(intptr_t)i, leafNodeFlags | selectionFlags, "%s", g_testEntries[i].name);
443 if (ImGui::IsItemClicked())
444 {
445 delete s_test;
446 s_settings.m_testIndex = i;
447 s_test = g_testEntries[i].createFcn();
448 s_testSelection = i;
449 }
450 ++i;
451 }
452 ImGui::TreePop();
453 }
454 else
455 {
456 while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0)
457 {
458 ++i;
459 }
460 }
461
462 if (i < g_testCount)
463 {
464 category = g_testEntries[i].category;
465 categoryIndex = i;
466 }
467 }
468 ImGui::EndTabItem();
469 }
470 ImGui::EndTabBar();
471 }
472
473 ImGui::End();
474
475 s_test->UpdateUI();
476 }
477 }
478
479 //
main(int,char **)480 int main(int, char**)
481 {
482 #if defined(_WIN32)
483 // Enable memory-leak reports
484 _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG));
485 #endif
486
487 char buffer[128];
488
489 s_settings.Load();
490 SortTests();
491
492 glfwSetErrorCallback(glfwErrorCallback);
493
494 g_camera.m_width = s_settings.m_windowWidth;
495 g_camera.m_height = s_settings.m_windowHeight;
496
497 if (glfwInit() == 0)
498 {
499 fprintf(stderr, "Failed to initialize GLFW\n");
500 return -1;
501 }
502
503 #if __APPLE__
504 const char* glslVersion = "#version 150";
505 #else
506 const char* glslVersion = NULL;
507 #endif
508
509 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
510 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
511 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
512 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
513
514 sprintf(buffer, "Box2D Testbed Version %d.%d.%d", b2_version.major, b2_version.minor, b2_version.revision);
515
516 bool fullscreen = false;
517 if (fullscreen)
518 {
519 g_mainWindow = glfwCreateWindow(1920, 1080, buffer, glfwGetPrimaryMonitor(), NULL);
520 }
521 else
522 {
523 g_mainWindow = glfwCreateWindow(g_camera.m_width, g_camera.m_height, buffer, NULL, NULL);
524 }
525
526 if (g_mainWindow == NULL)
527 {
528 fprintf(stderr, "Failed to open GLFW g_mainWindow.\n");
529 glfwTerminate();
530 return -1;
531 }
532
533 glfwMakeContextCurrent(g_mainWindow);
534
535 // Load OpenGL functions using glad
536 int version = gladLoadGL(glfwGetProcAddress);
537 printf("GL %d.%d\n", GLAD_VERSION_MAJOR(version), GLAD_VERSION_MINOR(version));
538 printf("OpenGL %s, GLSL %s\n", glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION));
539
540 glfwSetScrollCallback(g_mainWindow, ScrollCallback);
541 glfwSetWindowSizeCallback(g_mainWindow, ResizeWindowCallback);
542 glfwSetKeyCallback(g_mainWindow, KeyCallback);
543 glfwSetCharCallback(g_mainWindow, CharCallback);
544 glfwSetMouseButtonCallback(g_mainWindow, MouseButtonCallback);
545 glfwSetCursorPosCallback(g_mainWindow, MouseMotionCallback);
546 glfwSetScrollCallback(g_mainWindow, ScrollCallback);
547
548 g_debugDraw.Create();
549
550 CreateUI(g_mainWindow, glslVersion);
551
552 s_settings.m_testIndex = b2Clamp(s_settings.m_testIndex, 0, g_testCount - 1);
553 s_testSelection = s_settings.m_testIndex;
554 s_test = g_testEntries[s_settings.m_testIndex].createFcn();
555
556 // Control the frame rate. One draw per monitor refresh.
557 //glfwSwapInterval(1);
558
559 glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
560
561 std::chrono::duration<double> frameTime(0.0);
562 std::chrono::duration<double> sleepAdjust(0.0);
563
564 while (!glfwWindowShouldClose(g_mainWindow))
565 {
566 std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
567
568 glfwGetWindowSize(g_mainWindow, &g_camera.m_width, &g_camera.m_height);
569
570 int bufferWidth, bufferHeight;
571 glfwGetFramebufferSize(g_mainWindow, &bufferWidth, &bufferHeight);
572 glViewport(0, 0, bufferWidth, bufferHeight);
573
574 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
575
576 ImGui_ImplOpenGL3_NewFrame();
577 ImGui_ImplGlfw_NewFrame();
578
579 ImGui::NewFrame();
580
581 if (g_debugDraw.m_showUI)
582 {
583 ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
584 ImGui::SetNextWindowSize(ImVec2(float(g_camera.m_width), float(g_camera.m_height)));
585 ImGui::SetNextWindowBgAlpha(0.0f);
586 ImGui::Begin("Overlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar);
587 ImGui::End();
588
589 const TestEntry& entry = g_testEntries[s_settings.m_testIndex];
590 sprintf(buffer, "%s : %s", entry.category, entry.name);
591 s_test->DrawTitle(buffer);
592 }
593
594 s_test->Step(s_settings);
595
596 UpdateUI();
597
598 // ImGui::ShowDemoWindow();
599
600 if (g_debugDraw.m_showUI)
601 {
602 sprintf(buffer, "%.1f ms", 1000.0 * frameTime.count());
603 g_debugDraw.DrawString(5, g_camera.m_height - 20, buffer);
604 }
605
606 ImGui::Render();
607 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
608
609 glfwSwapBuffers(g_mainWindow);
610
611 if (s_testSelection != s_settings.m_testIndex)
612 {
613 s_settings.m_testIndex = s_testSelection;
614 delete s_test;
615 s_test = g_testEntries[s_settings.m_testIndex].createFcn();
616 g_camera.m_zoom = 1.0f;
617 g_camera.m_center.Set(0.0f, 20.0f);
618 }
619
620 glfwPollEvents();
621
622 // Throttle to cap at 60Hz. This adaptive using a sleep adjustment. This could be improved by
623 // using mm_pause or equivalent for the last millisecond.
624 std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
625 std::chrono::duration<double> target(1.0 / 60.0);
626 std::chrono::duration<double> timeUsed = t2 - t1;
627 std::chrono::duration<double> sleepTime = target - timeUsed + sleepAdjust;
628 if (sleepTime > std::chrono::duration<double>(0))
629 {
630 std::this_thread::sleep_for(sleepTime);
631 }
632
633 std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now();
634 frameTime = t3 - t1;
635
636 // Compute the sleep adjustment using a low pass filter
637 sleepAdjust = 0.9 * sleepAdjust + 0.1 * (target - frameTime);
638 }
639
640 delete s_test;
641 s_test = nullptr;
642
643 g_debugDraw.Destroy();
644 ImGui_ImplOpenGL3_Shutdown();
645 ImGui_ImplGlfw_Shutdown();
646 glfwTerminate();
647
648 s_settings.Save();
649
650 return 0;
651 }
652