// // Copyright (c) 2009-2010 Mikko Mononen memon@inside.org // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. // #include #define _USE_MATH_DEFINES #include #include "SDL.h" #include "SDL_opengl.h" #ifdef __APPLE__ # include #else # include #endif #include #include #include "imgui.h" #include "imguiRenderGL.h" #include "Recast.h" #include "RecastDebugDraw.h" #include "InputGeom.h" #include "TestCase.h" #include "Filelist.h" #include "Sample_SoloMesh.h" #include "Sample_TileMesh.h" #include "Sample_TempObstacles.h" #include "Sample_Debug.h" #ifdef WIN32 # define snprintf _snprintf # define putenv _putenv #endif using std::string; using std::vector; struct SampleItem { Sample* (*create)(); const string name; }; Sample* createSolo() { return new Sample_SoloMesh(); } Sample* createTile() { return new Sample_TileMesh(); } Sample* createTempObstacle() { return new Sample_TempObstacles(); } Sample* createDebug() { return new Sample_Debug(); } static SampleItem g_samples[] = { { createSolo, "Solo Mesh" }, { createTile, "Tile Mesh" }, { createTempObstacle, "Temp Obstacles" }, }; static const int g_nsamples = sizeof(g_samples) / sizeof(SampleItem); int main(int /*argc*/, char** /*argv*/) { // Init SDL if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { printf("Could not initialise SDL.\nError: %s\n", SDL_GetError()); return -1; } // Enable depth buffer. SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); // Set color channel depth. SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); // 4x MSAA. SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); SDL_DisplayMode displayMode; SDL_GetCurrentDisplayMode(0, &displayMode); bool presentationMode = false; Uint32 flags = SDL_WINDOW_OPENGL; int width; int height; if (presentationMode) { // Create a fullscreen window at the native resolution. width = displayMode.w; height = displayMode.h; flags |= SDL_WINDOW_FULLSCREEN; } else { float aspect = 16.0f / 9.0f; width = rcMin(displayMode.w, (int)(displayMode.h * aspect)) - 80; height = displayMode.h - 80; } SDL_Window* window; SDL_Renderer* renderer; int errorCode = SDL_CreateWindowAndRenderer(width, height, flags, &window, &renderer); if (errorCode != 0 || !window || !renderer) { printf("Could not initialise SDL opengl\nError: %s\n", SDL_GetError()); return -1; } SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); SDL_GL_CreateContext(window); if (!imguiRenderGLInit("DroidSans.ttf")) { printf("Could not init GUI renderer.\n"); SDL_Quit(); return -1; } float t = 0.0f; float timeAcc = 0.0f; Uint32 prevFrameTime = SDL_GetTicks(); int mousePos[2] = {0, 0}; int origMousePos[2] = {0, 0}; // Used to compute mouse movement totals across frames. float cameraEulers[] = {45, -45}; float cameraPos[] = {0, 0, 0}; float camr = 1000; float origCameraEulers[] = {0, 0}; // Used to compute rotational changes across frames. float moveFront = 0.0f, moveBack = 0.0f, moveLeft = 0.0f, moveRight = 0.0f, moveUp = 0.0f, moveDown = 0.0f; float scrollZoom = 0; bool rotate = false; bool movedDuringRotate = false; float rayStart[3]; float rayEnd[3]; bool mouseOverMenu = false; bool showMenu = !presentationMode; bool showLog = false; bool showTools = true; bool showLevels = false; bool showSample = false; bool showTestCases = false; // Window scroll positions. int propScroll = 0; int logScroll = 0; int toolsScroll = 0; string sampleName = "Choose Sample..."; vector files; const string meshesFolder = "Meshes"; string meshName = "Choose Mesh..."; float markerPosition[3] = {0, 0, 0}; bool markerPositionSet = false; InputGeom* geom = 0; Sample* sample = 0; const string testCasesFolder = "TestCases"; TestCase* test = 0; BuildContext ctx; // Fog. float fogColor[4] = { 0.32f, 0.31f, 0.30f, 1.0f }; glEnable(GL_FOG); glFogi(GL_FOG_MODE, GL_LINEAR); glFogf(GL_FOG_START, camr * 0.1f); glFogf(GL_FOG_END, camr * 1.25f); glFogfv(GL_FOG_COLOR, fogColor); glEnable(GL_CULL_FACE); glDepthFunc(GL_LEQUAL); bool done = false; while(!done) { // Handle input events. int mouseScroll = 0; bool processHitTest = false; bool processHitTestShift = false; SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_KEYDOWN: // Handle any key presses here. if (event.key.keysym.sym == SDLK_ESCAPE) { done = true; } else if (event.key.keysym.sym == SDLK_t) { showLevels = false; showSample = false; showTestCases = true; scanDirectory(testCasesFolder, ".txt", files); } else if (event.key.keysym.sym == SDLK_TAB) { showMenu = !showMenu; } else if (event.key.keysym.sym == SDLK_SPACE) { if (sample) sample->handleToggle(); } else if (event.key.keysym.sym == SDLK_1) { if (sample) sample->handleStep(); } else if (event.key.keysym.sym == SDLK_9) { if (sample && geom) { string savePath = meshesFolder + "/"; BuildSettings settings; memset(&settings, 0, sizeof(settings)); rcVcopy(settings.navMeshBMin, geom->getNavMeshBoundsMin()); rcVcopy(settings.navMeshBMax, geom->getNavMeshBoundsMax()); sample->collectSettings(settings); geom->saveGeomSet(&settings); } } break; case SDL_MOUSEWHEEL: if (event.wheel.y < 0) { // wheel down if (mouseOverMenu) { mouseScroll++; } else { scrollZoom += 1.0f; } } else { if (mouseOverMenu) { mouseScroll--; } else { scrollZoom -= 1.0f; } } break; case SDL_MOUSEBUTTONDOWN: if (event.button.button == SDL_BUTTON_RIGHT) { if (!mouseOverMenu) { // Rotate view rotate = true; movedDuringRotate = false; origMousePos[0] = mousePos[0]; origMousePos[1] = mousePos[1]; origCameraEulers[0] = cameraEulers[0]; origCameraEulers[1] = cameraEulers[1]; } } break; case SDL_MOUSEBUTTONUP: // Handle mouse clicks here. if (event.button.button == SDL_BUTTON_RIGHT) { rotate = false; if (!mouseOverMenu) { if (!movedDuringRotate) { processHitTest = true; processHitTestShift = true; } } } else if (event.button.button == SDL_BUTTON_LEFT) { if (!mouseOverMenu) { processHitTest = true; processHitTestShift = (SDL_GetModState() & KMOD_SHIFT) ? true : false; } } break; case SDL_MOUSEMOTION: mousePos[0] = event.motion.x; mousePos[1] = height-1 - event.motion.y; if (rotate) { int dx = mousePos[0] - origMousePos[0]; int dy = mousePos[1] - origMousePos[1]; cameraEulers[0] = origCameraEulers[0] - dy * 0.25f; cameraEulers[1] = origCameraEulers[1] + dx * 0.25f; if (dx * dx + dy * dy > 3 * 3) { movedDuringRotate = true; } } break; case SDL_QUIT: done = true; break; default: break; } } unsigned char mouseButtonMask = 0; if (SDL_GetMouseState(0, 0) & SDL_BUTTON_LMASK) mouseButtonMask |= IMGUI_MBUT_LEFT; if (SDL_GetMouseState(0, 0) & SDL_BUTTON_RMASK) mouseButtonMask |= IMGUI_MBUT_RIGHT; Uint32 time = SDL_GetTicks(); float dt = (time - prevFrameTime) / 1000.0f; prevFrameTime = time; t += dt; // Hit test mesh. if (processHitTest && geom && sample) { float hitTime; bool hit = geom->raycastMesh(rayStart, rayEnd, hitTime); if (hit) { if (SDL_GetModState() & KMOD_CTRL) { // Marker markerPositionSet = true; markerPosition[0] = rayStart[0] + (rayEnd[0] - rayStart[0]) * hitTime; markerPosition[1] = rayStart[1] + (rayEnd[1] - rayStart[1]) * hitTime; markerPosition[2] = rayStart[2] + (rayEnd[2] - rayStart[2]) * hitTime; } else { float pos[3]; pos[0] = rayStart[0] + (rayEnd[0] - rayStart[0]) * hitTime; pos[1] = rayStart[1] + (rayEnd[1] - rayStart[1]) * hitTime; pos[2] = rayStart[2] + (rayEnd[2] - rayStart[2]) * hitTime; sample->handleClick(rayStart, pos, processHitTestShift); } } else { if (SDL_GetModState() & KMOD_CTRL) { // Marker markerPositionSet = false; } } } // Update sample simulation. const float SIM_RATE = 20; const float DELTA_TIME = 1.0f / SIM_RATE; timeAcc = rcClamp(timeAcc + dt, -1.0f, 1.0f); int simIter = 0; while (timeAcc > DELTA_TIME) { timeAcc -= DELTA_TIME; if (simIter < 5 && sample) { sample->handleUpdate(DELTA_TIME); } simIter++; } // Clamp the framerate so that we do not hog all the CPU. const float MIN_FRAME_TIME = 1.0f / 40.0f; if (dt < MIN_FRAME_TIME) { int ms = (int)((MIN_FRAME_TIME - dt) * 1000.0f); if (ms > 10) ms = 10; if (ms >= 0) SDL_Delay(ms); } // Set the viewport. glViewport(0, 0, width, height); GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); // Clear the screen glClearColor(0.3f, 0.3f, 0.32f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_TEXTURE_2D); glEnable(GL_DEPTH_TEST); // Compute the projection matrix. glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(50.0f, (float)width/(float)height, 1.0f, camr); GLdouble projectionMatrix[16]; glGetDoublev(GL_PROJECTION_MATRIX, projectionMatrix); // Compute the modelview matrix. glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glRotatef(cameraEulers[0], 1, 0, 0); glRotatef(cameraEulers[1], 0, 1, 0); glTranslatef(-cameraPos[0], -cameraPos[1], -cameraPos[2]); GLdouble modelviewMatrix[16]; glGetDoublev(GL_MODELVIEW_MATRIX, modelviewMatrix); // Get hit ray position and direction. GLdouble x, y, z; gluUnProject(mousePos[0], mousePos[1], 0.0f, modelviewMatrix, projectionMatrix, viewport, &x, &y, &z); rayStart[0] = (float)x; rayStart[1] = (float)y; rayStart[2] = (float)z; gluUnProject(mousePos[0], mousePos[1], 1.0f, modelviewMatrix, projectionMatrix, viewport, &x, &y, &z); rayEnd[0] = (float)x; rayEnd[1] = (float)y; rayEnd[2] = (float)z; // Handle keyboard movement. const Uint8* keystate = SDL_GetKeyboardState(NULL); moveFront = rcClamp(moveFront + dt * 4 * ((keystate[SDL_SCANCODE_W] || keystate[SDL_SCANCODE_UP ]) ? 1 : -1), 0.0f, 1.0f); moveLeft = rcClamp(moveLeft + dt * 4 * ((keystate[SDL_SCANCODE_A] || keystate[SDL_SCANCODE_LEFT ]) ? 1 : -1), 0.0f, 1.0f); moveBack = rcClamp(moveBack + dt * 4 * ((keystate[SDL_SCANCODE_S] || keystate[SDL_SCANCODE_DOWN ]) ? 1 : -1), 0.0f, 1.0f); moveRight = rcClamp(moveRight + dt * 4 * ((keystate[SDL_SCANCODE_D] || keystate[SDL_SCANCODE_RIGHT ]) ? 1 : -1), 0.0f, 1.0f); moveUp = rcClamp(moveUp + dt * 4 * ((keystate[SDL_SCANCODE_Q] || keystate[SDL_SCANCODE_PAGEUP ]) ? 1 : -1), 0.0f, 1.0f); moveDown = rcClamp(moveDown + dt * 4 * ((keystate[SDL_SCANCODE_E] || keystate[SDL_SCANCODE_PAGEDOWN ]) ? 1 : -1), 0.0f, 1.0f); float keybSpeed = 22.0f; if (SDL_GetModState() & KMOD_SHIFT) { keybSpeed *= 4.0f; } float movex = (moveRight - moveLeft) * keybSpeed * dt; float movey = (moveBack - moveFront) * keybSpeed * dt + scrollZoom * 2.0f; scrollZoom = 0; cameraPos[0] += movex * (float)modelviewMatrix[0]; cameraPos[1] += movex * (float)modelviewMatrix[4]; cameraPos[2] += movex * (float)modelviewMatrix[8]; cameraPos[0] += movey * (float)modelviewMatrix[2]; cameraPos[1] += movey * (float)modelviewMatrix[6]; cameraPos[2] += movey * (float)modelviewMatrix[10]; cameraPos[1] += (moveUp - moveDown) * keybSpeed * dt; glEnable(GL_FOG); if (sample) sample->handleRender(); if (test) test->handleRender(); glDisable(GL_FOG); // Render GUI glDisable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0, width, 0, height); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); mouseOverMenu = false; imguiBeginFrame(mousePos[0], mousePos[1], mouseButtonMask, mouseScroll); if (sample) { sample->handleRenderOverlay((double*)projectionMatrix, (double*)modelviewMatrix, (int*)viewport); } if (test) { if (test->handleRenderOverlay((double*)projectionMatrix, (double*)modelviewMatrix, (int*)viewport)) mouseOverMenu = true; } // Help text. if (showMenu) { const char msg[] = "W/S/A/D: Move RMB: Rotate"; imguiDrawText(280, height-20, IMGUI_ALIGN_LEFT, msg, imguiRGBA(255,255,255,128)); } if (showMenu) { if (imguiBeginScrollArea("Properties", width-250-10, 10, 250, height-20, &propScroll)) mouseOverMenu = true; if (imguiCheck("Show Log", showLog)) showLog = !showLog; if (imguiCheck("Show Tools", showTools)) showTools = !showTools; imguiSeparator(); imguiLabel("Sample"); if (imguiButton(sampleName.c_str())) { if (showSample) { showSample = false; } else { showSample = true; showLevels = false; showTestCases = false; } } imguiSeparator(); imguiLabel("Input Mesh"); if (imguiButton(meshName.c_str())) { if (showLevels) { showLevels = false; } else { showSample = false; showTestCases = false; showLevels = true; scanDirectory(meshesFolder, ".obj", files); scanDirectoryAppend(meshesFolder, ".gset", files); } } if (geom) { char text[64]; snprintf(text, 64, "Verts: %.1fk Tris: %.1fk", geom->getMesh()->getVertCount()/1000.0f, geom->getMesh()->getTriCount()/1000.0f); imguiValue(text); } imguiSeparator(); if (geom && sample) { imguiSeparatorLine(); sample->handleSettings(); if (imguiButton("Build")) { ctx.resetLog(); if (!sample->handleBuild()) { showLog = true; logScroll = 0; } ctx.dumpLog("Build log %s:", meshName.c_str()); // Clear test. delete test; test = 0; } imguiSeparator(); } if (sample) { imguiSeparatorLine(); sample->handleDebugMode(); } imguiEndScrollArea(); } // Sample selection dialog. if (showSample) { static int levelScroll = 0; if (imguiBeginScrollArea("Choose Sample", width-10-250-10-200, height-10-250, 200, 250, &levelScroll)) mouseOverMenu = true; Sample* newSample = 0; for (int i = 0; i < g_nsamples; ++i) { if (imguiItem(g_samples[i].name.c_str())) { newSample = g_samples[i].create(); if (newSample) sampleName = g_samples[i].name; } } if (newSample) { delete sample; sample = newSample; sample->setContext(&ctx); if (geom) { sample->handleMeshChanged(geom); } showSample = false; } if (geom || sample) { const float* bmin = 0; const float* bmax = 0; if (geom) { bmin = geom->getNavMeshBoundsMin(); bmax = geom->getNavMeshBoundsMax(); } // Reset camera and fog to match the mesh bounds. if (bmin && bmax) { camr = sqrtf(rcSqr(bmax[0]-bmin[0]) + rcSqr(bmax[1]-bmin[1]) + rcSqr(bmax[2]-bmin[2])) / 2; cameraPos[0] = (bmax[0] + bmin[0]) / 2 + camr; cameraPos[1] = (bmax[1] + bmin[1]) / 2 + camr; cameraPos[2] = (bmax[2] + bmin[2]) / 2 + camr; camr *= 3; } cameraEulers[0] = 45; cameraEulers[1] = -45; glFogf(GL_FOG_START, camr*0.1f); glFogf(GL_FOG_END, camr*1.25f); } imguiEndScrollArea(); } // Level selection dialog. if (showLevels) { static int levelScroll = 0; if (imguiBeginScrollArea("Choose Level", width - 10 - 250 - 10 - 200, height - 10 - 450, 200, 450, &levelScroll)) mouseOverMenu = true; vector::const_iterator fileIter = files.begin(); vector::const_iterator filesEnd = files.end(); vector::const_iterator levelToLoad = filesEnd; for (; fileIter != filesEnd; ++fileIter) { if (imguiItem(fileIter->c_str())) { levelToLoad = fileIter; } } if (levelToLoad != filesEnd) { meshName = *levelToLoad; showLevels = false; delete geom; geom = 0; string path = meshesFolder + "/" + meshName; geom = new InputGeom; if (!geom->load(&ctx, path)) { delete geom; geom = 0; // Destroy the sample if it already had geometry loaded, as we've just deleted it! if (sample && sample->getInputGeom()) { delete sample; sample = 0; } showLog = true; logScroll = 0; ctx.dumpLog("Geom load log %s:", meshName.c_str()); } if (sample && geom) { sample->handleMeshChanged(geom); } if (geom || sample) { const float* bmin = 0; const float* bmax = 0; if (geom) { bmin = geom->getNavMeshBoundsMin(); bmax = geom->getNavMeshBoundsMax(); } // Reset camera and fog to match the mesh bounds. if (bmin && bmax) { camr = sqrtf(rcSqr(bmax[0]-bmin[0]) + rcSqr(bmax[1]-bmin[1]) + rcSqr(bmax[2]-bmin[2])) / 2; cameraPos[0] = (bmax[0] + bmin[0]) / 2 + camr; cameraPos[1] = (bmax[1] + bmin[1]) / 2 + camr; cameraPos[2] = (bmax[2] + bmin[2]) / 2 + camr; camr *= 3; } cameraEulers[0] = 45; cameraEulers[1] = -45; glFogf(GL_FOG_START, camr * 0.1f); glFogf(GL_FOG_END, camr * 1.25f); } } imguiEndScrollArea(); } // Test cases if (showTestCases) { static int testScroll = 0; if (imguiBeginScrollArea("Choose Test To Run", width-10-250-10-200, height-10-450, 200, 450, &testScroll)) mouseOverMenu = true; vector::const_iterator fileIter = files.begin(); vector::const_iterator filesEnd = files.end(); vector::const_iterator testToLoad = filesEnd; for (; fileIter != filesEnd; ++fileIter) { if (imguiItem(fileIter->c_str())) { testToLoad = fileIter; } } if (testToLoad != filesEnd) { string path = testCasesFolder + "/" + *testToLoad; test = new TestCase; if (test) { // Load the test. if (!test->load(path)) { delete test; test = 0; } // Create sample Sample* newSample = 0; for (int i = 0; i < g_nsamples; ++i) { if (g_samples[i].name == test->getSampleName()) { newSample = g_samples[i].create(); if (newSample) sampleName = g_samples[i].name; } } delete sample; sample = newSample; if (sample) { sample->setContext(&ctx); showSample = false; } // Load geom. meshName = test->getGeomFileName(); path = meshesFolder + "/" + meshName; delete geom; geom = new InputGeom; if (!geom || !geom->load(&ctx, path)) { delete geom; geom = 0; delete sample; sample = 0; showLog = true; logScroll = 0; ctx.dumpLog("Geom load log %s:", meshName.c_str()); } if (sample && geom) { sample->handleMeshChanged(geom); } // This will ensure that tile & poly bits are updated in tiled sample. if (sample) sample->handleSettings(); ctx.resetLog(); if (sample && !sample->handleBuild()) { ctx.dumpLog("Build log %s:", meshName.c_str()); } if (geom || sample) { const float* bmin = 0; const float* bmax = 0; if (geom) { bmin = geom->getNavMeshBoundsMin(); bmax = geom->getNavMeshBoundsMax(); } // Reset camera and fog to match the mesh bounds. if (bmin && bmax) { camr = sqrtf(rcSqr(bmax[0] - bmin[0]) + rcSqr(bmax[1] - bmin[1]) + rcSqr(bmax[2] - bmin[2])) / 2; cameraPos[0] = (bmax[0] + bmin[0]) / 2 + camr; cameraPos[1] = (bmax[1] + bmin[1]) / 2 + camr; cameraPos[2] = (bmax[2] + bmin[2]) / 2 + camr; camr *= 3; } cameraEulers[0] = 45; cameraEulers[1] = -45; glFogf(GL_FOG_START, camr * 0.2f); glFogf(GL_FOG_END, camr * 1.25f); } // Do the tests. if (sample) test->doTests(sample->getNavMesh(), sample->getNavMeshQuery()); } } imguiEndScrollArea(); } // Log if (showLog && showMenu) { if (imguiBeginScrollArea("Log", 250 + 20, 10, width - 300 - 250, 200, &logScroll)) mouseOverMenu = true; for (int i = 0; i < ctx.getLogCount(); ++i) imguiLabel(ctx.getLogText(i)); imguiEndScrollArea(); } // Left column tools menu if (!showTestCases && showTools && showMenu) // && geom && sample) { if (imguiBeginScrollArea("Tools", 10, 10, 250, height - 20, &toolsScroll)) mouseOverMenu = true; if (sample) sample->handleTools(); imguiEndScrollArea(); } // Marker if (markerPositionSet && gluProject((GLdouble)markerPosition[0], (GLdouble)markerPosition[1], (GLdouble)markerPosition[2], modelviewMatrix, projectionMatrix, viewport, &x, &y, &z)) { // Draw marker circle glLineWidth(5.0f); glColor4ub(240,220,0,196); glBegin(GL_LINE_LOOP); const float r = 25.0f; for (int i = 0; i < 20; ++i) { const float a = (float)i / 20.0f * RC_PI*2; const float fx = (float)x + cosf(a)*r; const float fy = (float)y + sinf(a)*r; glVertex2f(fx,fy); } glEnd(); glLineWidth(1.0f); } imguiEndFrame(); imguiRenderGLDraw(); glEnable(GL_DEPTH_TEST); SDL_GL_SwapWindow(window); } imguiRenderGLDestroy(); SDL_Quit(); delete sample; delete geom; return 0; }