// MIT License // Copyright (c) 2019 Erin Catto // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #define _CRT_SECURE_NO_WARNINGS #define IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1 #include "imgui/imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "draw.h" #include "settings.h" #include "test.h" #include #include #include #include #if defined(_WIN32) #include #endif GLFWwindow* g_mainWindow = nullptr; static int32 s_testSelection = 0; static Test* s_test = nullptr; static Settings s_settings; static bool s_rightMouseDown = false; static b2Vec2 s_clickPointWS = b2Vec2_zero; void glfwErrorCallback(int error, const char* description) { fprintf(stderr, "GLFW error occured. Code: %d. Description: %s\n", error, description); } static inline bool CompareTests(const TestEntry& a, const TestEntry& b) { int result = strcmp(a.category, b.category); if (result == 0) { result = strcmp(a.name, b.name); } return result < 0; } static void SortTests() { std::sort(g_testEntries, g_testEntries + g_testCount, CompareTests); } static void CreateUI(GLFWwindow* window, const char* glslVersion = NULL) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); bool success; success = ImGui_ImplGlfw_InitForOpenGL(window, false); if (success == false) { printf("ImGui_ImplGlfw_InitForOpenGL failed\n"); assert(false); } success = ImGui_ImplOpenGL3_Init(glslVersion); if (success == false) { printf("ImGui_ImplOpenGL3_Init failed\n"); assert(false); } // Search for font file const char* fontPath1 = "data/droid_sans.ttf"; const char* fontPath2 = "../data/droid_sans.ttf"; const char* fontPath = nullptr; FILE* file1 = fopen(fontPath1, "rb"); FILE* file2 = fopen(fontPath2, "rb"); if (file1) { fontPath = fontPath1; fclose(file1); } if (file2) { fontPath = fontPath2; fclose(file2); } if (fontPath) { ImGui::GetIO().Fonts->AddFontFromFileTTF(fontPath, 13.0f); } } static void ResizeWindowCallback(GLFWwindow*, int width, int height) { g_camera.m_width = width; g_camera.m_height = height; s_settings.m_windowWidth = width; s_settings.m_windowHeight = height; } static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods); if (ImGui::GetIO().WantCaptureKeyboard) { return; } if (action == GLFW_PRESS) { switch (key) { case GLFW_KEY_ESCAPE: // Quit glfwSetWindowShouldClose(g_mainWindow, GL_TRUE); break; case GLFW_KEY_LEFT: // Pan left if (mods == GLFW_MOD_CONTROL) { b2Vec2 newOrigin(2.0f, 0.0f); s_test->ShiftOrigin(newOrigin); } else { g_camera.m_center.x -= 0.5f; } break; case GLFW_KEY_RIGHT: // Pan right if (mods == GLFW_MOD_CONTROL) { b2Vec2 newOrigin(-2.0f, 0.0f); s_test->ShiftOrigin(newOrigin); } else { g_camera.m_center.x += 0.5f; } break; case GLFW_KEY_DOWN: // Pan down if (mods == GLFW_MOD_CONTROL) { b2Vec2 newOrigin(0.0f, 2.0f); s_test->ShiftOrigin(newOrigin); } else { g_camera.m_center.y -= 0.5f; } break; case GLFW_KEY_UP: // Pan up if (mods == GLFW_MOD_CONTROL) { b2Vec2 newOrigin(0.0f, -2.0f); s_test->ShiftOrigin(newOrigin); } else { g_camera.m_center.y += 0.5f; } break; case GLFW_KEY_HOME: // Reset view g_camera.m_zoom = 1.0f; g_camera.m_center.Set(0.0f, 20.0f); break; case GLFW_KEY_Z: // Zoom out g_camera.m_zoom = b2Min(1.1f * g_camera.m_zoom, 20.0f); break; case GLFW_KEY_X: // Zoom in g_camera.m_zoom = b2Max(0.9f * g_camera.m_zoom, 0.02f); break; case GLFW_KEY_R: // Reset test delete s_test; s_test = g_testEntries[s_settings.m_testIndex].createFcn(); break; case GLFW_KEY_SPACE: // Launch a bomb. if (s_test) { s_test->LaunchBomb(); } break; case GLFW_KEY_O: s_settings.m_singleStep = true; break; case GLFW_KEY_P: s_settings.m_pause = !s_settings.m_pause; break; case GLFW_KEY_LEFT_BRACKET: // Switch to previous test --s_testSelection; if (s_testSelection < 0) { s_testSelection = g_testCount - 1; } break; case GLFW_KEY_RIGHT_BRACKET: // Switch to next test ++s_testSelection; if (s_testSelection == g_testCount) { s_testSelection = 0; } break; case GLFW_KEY_TAB: g_debugDraw.m_showUI = !g_debugDraw.m_showUI; default: if (s_test) { s_test->Keyboard(key); } } } else if (action == GLFW_RELEASE) { s_test->KeyboardUp(key); } // else GLFW_REPEAT } static void CharCallback(GLFWwindow* window, unsigned int c) { ImGui_ImplGlfw_CharCallback(window, c); } static void MouseButtonCallback(GLFWwindow* window, int32 button, int32 action, int32 mods) { ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods); double xd, yd; glfwGetCursorPos(g_mainWindow, &xd, &yd); b2Vec2 ps((float)xd, (float)yd); // Use the mouse to move things around. if (button == GLFW_MOUSE_BUTTON_1) { //<##> //ps.Set(0, 0); b2Vec2 pw = g_camera.ConvertScreenToWorld(ps); if (action == GLFW_PRESS) { if (mods == GLFW_MOD_SHIFT) { s_test->ShiftMouseDown(pw); } else { s_test->MouseDown(pw); } } if (action == GLFW_RELEASE) { s_test->MouseUp(pw); } } else if (button == GLFW_MOUSE_BUTTON_2) { if (action == GLFW_PRESS) { s_clickPointWS = g_camera.ConvertScreenToWorld(ps); s_rightMouseDown = true; } if (action == GLFW_RELEASE) { s_rightMouseDown = false; } } } static void MouseMotionCallback(GLFWwindow*, double xd, double yd) { b2Vec2 ps((float)xd, (float)yd); b2Vec2 pw = g_camera.ConvertScreenToWorld(ps); s_test->MouseMove(pw); if (s_rightMouseDown) { b2Vec2 diff = pw - s_clickPointWS; g_camera.m_center.x -= diff.x; g_camera.m_center.y -= diff.y; s_clickPointWS = g_camera.ConvertScreenToWorld(ps); } } static void ScrollCallback(GLFWwindow* window, double dx, double dy) { ImGui_ImplGlfw_ScrollCallback(window, dx, dy); if (ImGui::GetIO().WantCaptureMouse) { return; } if (dy > 0) { g_camera.m_zoom /= 1.1f; } else { g_camera.m_zoom *= 1.1f; } } static void RestartTest() { delete s_test; s_test = g_testEntries[s_settings.m_testIndex].createFcn(); } static void UpdateUI() { int menuWidth = 180; if (g_debugDraw.m_showUI) { ImGui::SetNextWindowPos(ImVec2((float)g_camera.m_width - menuWidth - 10, 10)); ImGui::SetNextWindowSize(ImVec2((float)menuWidth, (float)g_camera.m_height - 20)); ImGui::Begin("Tools", &g_debugDraw.m_showUI, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); if (ImGui::BeginTabBar("ControlTabs", ImGuiTabBarFlags_None)) { if (ImGui::BeginTabItem("Controls")) { ImGui::SliderInt("Vel Iters", &s_settings.m_velocityIterations, 0, 50); ImGui::SliderInt("Pos Iters", &s_settings.m_positionIterations, 0, 50); ImGui::SliderFloat("Hertz", &s_settings.m_hertz, 5.0f, 120.0f, "%.0f hz"); ImGui::Separator(); ImGui::Checkbox("Sleep", &s_settings.m_enableSleep); ImGui::Checkbox("Warm Starting", &s_settings.m_enableWarmStarting); ImGui::Checkbox("Time of Impact", &s_settings.m_enableContinuous); ImGui::Checkbox("Sub-Stepping", &s_settings.m_enableSubStepping); ImGui::Separator(); ImGui::Checkbox("Shapes", &s_settings.m_drawShapes); ImGui::Checkbox("Joints", &s_settings.m_drawJoints); ImGui::Checkbox("AABBs", &s_settings.m_drawAABBs); ImGui::Checkbox("Contact Points", &s_settings.m_drawContactPoints); ImGui::Checkbox("Contact Normals", &s_settings.m_drawContactNormals); ImGui::Checkbox("Contact Impulses", &s_settings.m_drawContactImpulse); ImGui::Checkbox("Friction Impulses", &s_settings.m_drawFrictionImpulse); ImGui::Checkbox("Center of Masses", &s_settings.m_drawCOMs); ImGui::Checkbox("Statistics", &s_settings.m_drawStats); ImGui::Checkbox("Profile", &s_settings.m_drawProfile); ImVec2 button_sz = ImVec2(-1, 0); if (ImGui::Button("Pause (P)", button_sz)) { s_settings.m_pause = !s_settings.m_pause; } if (ImGui::Button("Single Step (O)", button_sz)) { s_settings.m_singleStep = !s_settings.m_singleStep; } if (ImGui::Button("Restart (R)", button_sz)) { RestartTest(); } if (ImGui::Button("Quit", button_sz)) { glfwSetWindowShouldClose(g_mainWindow, GL_TRUE); } ImGui::EndTabItem(); } ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; leafNodeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (ImGui::BeginTabItem("Tests")) { int categoryIndex = 0; const char* category = g_testEntries[categoryIndex].category; int i = 0; while (i < g_testCount) { bool categorySelected = strcmp(category, g_testEntries[s_settings.m_testIndex].category) == 0; ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags_Selected : 0; bool nodeOpen = ImGui::TreeNodeEx(category, nodeFlags | nodeSelectionFlags); if (nodeOpen) { while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0) { ImGuiTreeNodeFlags selectionFlags = 0; if (s_settings.m_testIndex == i) { selectionFlags = ImGuiTreeNodeFlags_Selected; } ImGui::TreeNodeEx((void*)(intptr_t)i, leafNodeFlags | selectionFlags, "%s", g_testEntries[i].name); if (ImGui::IsItemClicked()) { delete s_test; s_settings.m_testIndex = i; s_test = g_testEntries[i].createFcn(); s_testSelection = i; } ++i; } ImGui::TreePop(); } else { while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0) { ++i; } } if (i < g_testCount) { category = g_testEntries[i].category; categoryIndex = i; } } ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImGui::End(); s_test->UpdateUI(); } } // int main(int, char**) { #if defined(_WIN32) // Enable memory-leak reports _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)); #endif char buffer[128]; s_settings.Load(); SortTests(); glfwSetErrorCallback(glfwErrorCallback); g_camera.m_width = s_settings.m_windowWidth; g_camera.m_height = s_settings.m_windowHeight; if (glfwInit() == 0) { fprintf(stderr, "Failed to initialize GLFW\n"); return -1; } #if __APPLE__ const char* glslVersion = "#version 150"; #else const char* glslVersion = NULL; #endif glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); sprintf(buffer, "Box2D Testbed Version %d.%d.%d", b2_version.major, b2_version.minor, b2_version.revision); bool fullscreen = false; if (fullscreen) { g_mainWindow = glfwCreateWindow(1920, 1080, buffer, glfwGetPrimaryMonitor(), NULL); } else { g_mainWindow = glfwCreateWindow(g_camera.m_width, g_camera.m_height, buffer, NULL, NULL); } if (g_mainWindow == NULL) { fprintf(stderr, "Failed to open GLFW g_mainWindow.\n"); glfwTerminate(); return -1; } glfwMakeContextCurrent(g_mainWindow); // Load OpenGL functions using glad int version = gladLoadGL(glfwGetProcAddress); printf("GL %d.%d\n", GLAD_VERSION_MAJOR(version), GLAD_VERSION_MINOR(version)); printf("OpenGL %s, GLSL %s\n", glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION)); glfwSetScrollCallback(g_mainWindow, ScrollCallback); glfwSetWindowSizeCallback(g_mainWindow, ResizeWindowCallback); glfwSetKeyCallback(g_mainWindow, KeyCallback); glfwSetCharCallback(g_mainWindow, CharCallback); glfwSetMouseButtonCallback(g_mainWindow, MouseButtonCallback); glfwSetCursorPosCallback(g_mainWindow, MouseMotionCallback); glfwSetScrollCallback(g_mainWindow, ScrollCallback); g_debugDraw.Create(); CreateUI(g_mainWindow, glslVersion); s_settings.m_testIndex = b2Clamp(s_settings.m_testIndex, 0, g_testCount - 1); s_testSelection = s_settings.m_testIndex; s_test = g_testEntries[s_settings.m_testIndex].createFcn(); // Control the frame rate. One draw per monitor refresh. //glfwSwapInterval(1); glClearColor(0.2f, 0.2f, 0.2f, 1.0f); std::chrono::duration frameTime(0.0); std::chrono::duration sleepAdjust(0.0); while (!glfwWindowShouldClose(g_mainWindow)) { std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now(); glfwGetWindowSize(g_mainWindow, &g_camera.m_width, &g_camera.m_height); int bufferWidth, bufferHeight; glfwGetFramebufferSize(g_mainWindow, &bufferWidth, &bufferHeight); glViewport(0, 0, bufferWidth, bufferHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); if (g_debugDraw.m_showUI) { ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); ImGui::SetNextWindowSize(ImVec2(float(g_camera.m_width), float(g_camera.m_height))); ImGui::SetNextWindowBgAlpha(0.0f); ImGui::Begin("Overlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar); ImGui::End(); const TestEntry& entry = g_testEntries[s_settings.m_testIndex]; sprintf(buffer, "%s : %s", entry.category, entry.name); s_test->DrawTitle(buffer); } s_test->Step(s_settings); UpdateUI(); // ImGui::ShowDemoWindow(); if (g_debugDraw.m_showUI) { sprintf(buffer, "%.1f ms", 1000.0 * frameTime.count()); g_debugDraw.DrawString(5, g_camera.m_height - 20, buffer); } ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(g_mainWindow); if (s_testSelection != s_settings.m_testIndex) { s_settings.m_testIndex = s_testSelection; delete s_test; s_test = g_testEntries[s_settings.m_testIndex].createFcn(); g_camera.m_zoom = 1.0f; g_camera.m_center.Set(0.0f, 20.0f); } glfwPollEvents(); // Throttle to cap at 60Hz. This adaptive using a sleep adjustment. This could be improved by // using mm_pause or equivalent for the last millisecond. std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now(); std::chrono::duration target(1.0 / 60.0); std::chrono::duration timeUsed = t2 - t1; std::chrono::duration sleepTime = target - timeUsed + sleepAdjust; if (sleepTime > std::chrono::duration(0)) { std::this_thread::sleep_for(sleepTime); } std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now(); frameTime = t3 - t1; // Compute the sleep adjustment using a low pass filter sleepAdjust = 0.9 * sleepAdjust + 0.1 * (target - frameTime); } delete s_test; s_test = nullptr; g_debugDraw.Destroy(); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); glfwTerminate(); s_settings.Save(); return 0; }