diff --git a/include/rendering/loading_screen.hpp b/include/rendering/loading_screen.hpp index d4f8f27b..3d5272bb 100644 --- a/include/rendering/loading_screen.hpp +++ b/include/rendering/loading_screen.hpp @@ -18,7 +18,7 @@ public: // Select a random loading screen image void selectRandomImage(); - // Render the loading screen (call in a loop while loading) + // Render the loading screen with progress bar and status text void render(); // Update loading progress (0.0 to 1.0) @@ -30,12 +30,18 @@ public: private: bool loadImage(const std::string& path); void createQuad(); + void createBarQuad(); GLuint textureId = 0; GLuint vao = 0; GLuint vbo = 0; GLuint shaderId = 0; + // Progress bar GL objects + GLuint barVao = 0; + GLuint barVbo = 0; + GLuint barShaderId = 0; + std::vector imagePaths; int currentImageIndex = 0; diff --git a/src/core/application.cpp b/src/core/application.cpp index d14abaa3..5b485523 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1276,9 +1276,6 @@ void Application::startSinglePlayer() { } } - // Load weapon models for equipped items (after inventory is populated) - loadEquippedWeapons(); - if (gameHandler && renderer && window) { game::GameHandler::SinglePlayerSettings settings; bool hasSettings = gameHandler->getSinglePlayerSettings(settings); @@ -1326,7 +1323,40 @@ void Application::startSinglePlayer() { } } - // --- Loading screen: load terrain and wait for streaming before spawning --- + // --- Loading screen --- + rendering::LoadingScreen loadingScreen; + bool loadingScreenOk = loadingScreen.initialize(); + + // Helper: poll events (resize/quit), update progress bar, swap buffers + auto showProgress = [&](const char* msg, float progress) { + // Poll SDL events so resizing and quit work during loading + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window->setShouldClose(true); + loadingScreen.shutdown(); + return; + } + if (event.type == SDL_WINDOWEVENT && + event.window.event == SDL_WINDOWEVENT_RESIZED) { + int w = event.window.data1; + int h = event.window.data2; + window->setSize(w, h); + glViewport(0, 0, w, h); + if (renderer && renderer->getCamera()) { + renderer->getCamera()->setAspectRatio(static_cast(w) / h); + } + } + } + if (!loadingScreenOk) return; + loadingScreen.setStatus(msg); + loadingScreen.setProgress(progress); + loadingScreen.render(); + window->swapBuffers(); + }; + + showProgress("Preparing world...", 0.0f); + const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN")); // Canonical WoW coords: +X=North, +Y=West, +Z=Up glm::vec3 spawnCanonical = spawnPreset ? spawnPreset->spawnCanonical : spSpawnCanonical_; @@ -1365,17 +1395,12 @@ void Application::startSinglePlayer() { LOG_INFO("Optional spawn overrides (canonical WoW X,Y,Z): WOW_SPAWN_POS=x,y,z WOW_SPAWN_ROT=yaw,pitch"); } - rendering::LoadingScreen loadingScreen; - bool loadingScreenOk = loadingScreen.initialize(); + showProgress("Loading character model...", 0.05f); - auto showStatus = [&](const char* msg) { - if (!loadingScreenOk) return; - loadingScreen.setStatus(msg); - loadingScreen.render(); - window->swapBuffers(); - }; + // Spawn player character (loads M2 model, skin, textures, animations, weapons) + spawnPlayerCharacter(); - showStatus("Loading terrain..."); + showProgress("Loading terrain...", 0.25f); // Set map name for zone-specific floor cache if (renderer->getWMORenderer()) { @@ -1396,6 +1421,8 @@ void Application::startSinglePlayer() { } } + showProgress("Streaming terrain tiles...", 0.40f); + // Wait for surrounding terrain tiles to stream in if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) { auto* terrainMgr = renderer->getTerrainManager(); @@ -1407,6 +1434,8 @@ void Application::startSinglePlayer() { auto startTime = std::chrono::high_resolution_clock::now(); const float maxWaitSeconds = 15.0f; + int initialPending = terrainMgr->getPendingTileCount(); + while (terrainMgr->getPendingTileCount() > 0) { // Poll events to keep window responsive SDL_Event event; @@ -1416,19 +1445,34 @@ void Application::startSinglePlayer() { loadingScreen.shutdown(); return; } + if (event.type == SDL_WINDOWEVENT && + event.window.event == SDL_WINDOWEVENT_RESIZED) { + int w = event.window.data1; + int h = event.window.data2; + window->setSize(w, h); + glViewport(0, 0, w, h); + if (renderer && renderer->getCamera()) { + renderer->getCamera()->setAspectRatio(static_cast(w) / h); + } + } } // Process ready tiles from worker threads terrainMgr->update(*camera, 0.016f); - // Update loading screen with progress + // Update loading screen with tile progress (40% - 85% range) if (loadingScreenOk) { int loaded = terrainMgr->getLoadedTileCount(); int pending = terrainMgr->getPendingTileCount(); + float tileProgress = (initialPending > 0) + ? static_cast(initialPending - pending) / initialPending + : 1.0f; + float progress = 0.40f + tileProgress * 0.45f; char buf[128]; snprintf(buf, sizeof(buf), "Loading terrain... %d tiles loaded, %d remaining", loaded, pending); loadingScreen.setStatus(buf); + loadingScreen.setProgress(progress); loadingScreen.render(); window->swapBuffers(); } @@ -1445,11 +1489,12 @@ void Application::startSinglePlayer() { LOG_INFO("Terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); + showProgress("Building collision cache...", 0.88f); + // Load zone-specific floor cache, or precompute if none exists if (renderer->getWMORenderer()) { renderer->getWMORenderer()->loadFloorCache(); if (renderer->getWMORenderer()->getFloorCacheSize() == 0) { - showStatus("Pre-computing collision cache..."); renderer->getWMORenderer()->precomputeFloorCache(); } } @@ -1461,10 +1506,7 @@ void Application::startSinglePlayer() { } } - showStatus("Spawning character..."); - - // Spawn player character on loaded terrain - spawnPlayerCharacter(); + showProgress("Entering world...", 0.95f); // Final camera reset: now that follow target exists and terrain is loaded, // snap the third-person camera into the correct orbit position. @@ -1473,6 +1515,8 @@ void Application::startSinglePlayer() { renderer->getCameraController()->startIntroPan(2.8f, 140.0f); } + showProgress("Entering world...", 1.0f); + if (loadingScreenOk) { loadingScreen.shutdown(); } diff --git a/src/rendering/loading_screen.cpp b/src/rendering/loading_screen.cpp index 01e886ed..25a78bc1 100644 --- a/src/rendering/loading_screen.cpp +++ b/src/rendering/loading_screen.cpp @@ -1,7 +1,11 @@ #include "rendering/loading_screen.hpp" #include "core/logger.hpp" +#include +#include +#include #include #include +#include #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" @@ -10,7 +14,6 @@ namespace wowee { namespace rendering { LoadingScreen::LoadingScreen() { - // Add loading screen image paths imagePaths.push_back("assets/loading1.jpeg"); imagePaths.push_back("assets/loading2.jpeg"); } @@ -22,7 +25,7 @@ LoadingScreen::~LoadingScreen() { bool LoadingScreen::initialize() { LOG_INFO("Initializing loading screen"); - // Create simple shader for textured quad + // Background image shader (textured quad) const char* vertexSrc = R"( #version 330 core layout (location = 0) in vec2 aPos; @@ -44,7 +47,6 @@ bool LoadingScreen::initialize() { } )"; - // Compile vertex shader GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexSrc, nullptr); glCompileShader(vertexShader); @@ -58,7 +60,6 @@ bool LoadingScreen::initialize() { return false; } - // Compile fragment shader GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentSrc, nullptr); glCompileShader(fragmentShader); @@ -71,7 +72,6 @@ bool LoadingScreen::initialize() { return false; } - // Link shader program shaderId = glCreateProgram(); glAttachShader(shaderId, vertexShader); glAttachShader(shaderId, fragmentShader); @@ -88,7 +88,41 @@ bool LoadingScreen::initialize() { glDeleteShader(vertexShader); glDeleteShader(fragmentShader); + // Simple solid-color shader for progress bar + const char* barVertSrc = R"( + #version 330 core + layout (location = 0) in vec2 aPos; + void main() { + gl_Position = vec4(aPos, 0.0, 1.0); + } + )"; + + const char* barFragSrc = R"( + #version 330 core + out vec4 FragColor; + uniform vec4 uColor; + void main() { + FragColor = uColor; + } + )"; + + GLuint bv = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(bv, 1, &barVertSrc, nullptr); + glCompileShader(bv); + GLuint bf = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(bf, 1, &barFragSrc, nullptr); + glCompileShader(bf); + + barShaderId = glCreateProgram(); + glAttachShader(barShaderId, bv); + glAttachShader(barShaderId, bf); + glLinkProgram(barShaderId); + + glDeleteShader(bv); + glDeleteShader(bf); + createQuad(); + createBarQuad(); selectRandomImage(); LOG_INFO("Loading screen initialized"); @@ -112,12 +146,23 @@ void LoadingScreen::shutdown() { glDeleteProgram(shaderId); shaderId = 0; } + if (barVao) { + glDeleteVertexArrays(1, &barVao); + barVao = 0; + } + if (barVbo) { + glDeleteBuffers(1, &barVbo); + barVbo = 0; + } + if (barShaderId) { + glDeleteProgram(barShaderId); + barShaderId = 0; + } } void LoadingScreen::selectRandomImage() { if (imagePaths.empty()) return; - // Seed with current time unsigned seed = static_cast( std::chrono::system_clock::now().time_since_epoch().count()); std::default_random_engine generator(seed); @@ -130,13 +175,11 @@ void LoadingScreen::selectRandomImage() { } bool LoadingScreen::loadImage(const std::string& path) { - // Delete old texture if exists if (textureId) { glDeleteTextures(1, &textureId); textureId = 0; } - // Load image with stb_image int channels; stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load(path.c_str(), &imageWidth, &imageHeight, &channels, 4); @@ -148,7 +191,6 @@ bool LoadingScreen::loadImage(const std::string& path) { LOG_INFO("Loaded loading screen image: ", imageWidth, "x", imageHeight); - // Create OpenGL texture glGenTextures(1, &textureId); glBindTexture(GL_TEXTURE_2D, textureId); @@ -167,16 +209,15 @@ bool LoadingScreen::loadImage(const std::string& path) { } void LoadingScreen::createQuad() { - // Full-screen quad vertices (position + texcoord) float vertices[] = { // Position // TexCoord - -1.0f, 1.0f, 0.0f, 1.0f, // Top-left - -1.0f, -1.0f, 0.0f, 0.0f, // Bottom-left - 1.0f, -1.0f, 1.0f, 0.0f, // Bottom-right + -1.0f, 1.0f, 0.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 1.0f, // Top-left - 1.0f, -1.0f, 1.0f, 0.0f, // Bottom-right - 1.0f, 1.0f, 1.0f, 1.0f // Top-right + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f }; glGenVertexArrays(1, &vao); @@ -186,38 +227,171 @@ void LoadingScreen::createQuad() { glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - // Position attribute glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); - - // Texture coordinate attribute glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); } -void LoadingScreen::render() { - if (!textureId || !vao || !shaderId) return; +void LoadingScreen::createBarQuad() { + // Dynamic quad — vertices updated each frame via glBufferSubData + glGenVertexArrays(1, &barVao); + glGenBuffers(1, &barVbo); + + glBindVertexArray(barVao); + glBindBuffer(GL_ARRAY_BUFFER, barVbo); + glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(float), nullptr, GL_DYNAMIC_DRAW); + + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glBindVertexArray(0); +} + +void LoadingScreen::render() { + if (!vao || !shaderId) return; - // Clear screen glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - // Disable depth test for 2D rendering glDisable(GL_DEPTH_TEST); - // Use shader and bind texture - glUseProgram(shaderId); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textureId); + // Draw background image + if (textureId) { + glUseProgram(shaderId); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textureId); + glBindVertexArray(vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + } - // Draw quad - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLES, 0, 6); - glBindVertexArray(0); + // Draw progress bar at bottom center + if (barVao && barShaderId) { + // Bar dimensions in NDC: centered, near bottom + const float barWidth = 0.6f; // half-width in NDC (total 1.2 of 2.0 range = 60% of screen) + const float barHeight = 0.015f; + const float barY = -0.82f; // near bottom + + float left = -barWidth; + float right = -barWidth + 2.0f * barWidth * loadProgress; + float top = barY + barHeight; + float bottom = barY - barHeight; + + // Background (dark) + { + float bgVerts[] = { + -barWidth, top, + -barWidth, bottom, + barWidth, bottom, + -barWidth, top, + barWidth, bottom, + barWidth, top, + }; + glUseProgram(barShaderId); + GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); + glUniform4f(colorLoc, 0.1f, 0.1f, 0.1f, 0.8f); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glBindVertexArray(barVao); + glBindBuffer(GL_ARRAY_BUFFER, barVbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(bgVerts), bgVerts); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + // Filled portion (gold/amber like WoW) + if (loadProgress > 0.001f) { + float fillVerts[] = { + left, top, + left, bottom, + right, bottom, + left, top, + right, bottom, + right, top, + }; + GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); + glUniform4f(colorLoc, 0.78f, 0.61f, 0.13f, 1.0f); + + glBindBuffer(GL_ARRAY_BUFFER, barVbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(fillVerts), fillVerts); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + // Border (thin bright outline) + { + const float borderInset = 0.002f; + float borderLeft = -barWidth - borderInset; + float borderRight = barWidth + borderInset; + float borderTop = top + borderInset; + float borderBottom = bottom - borderInset; + + // Draw 4 thin border edges as line strip + glUseProgram(barShaderId); + GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); + glUniform4f(colorLoc, 0.55f, 0.43f, 0.1f, 1.0f); + + float borderVerts[] = { + borderLeft, borderTop, + borderRight, borderTop, + borderRight, borderBottom, + borderLeft, borderBottom, + borderLeft, borderTop, + }; + glBindBuffer(GL_ARRAY_BUFFER, barVbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(borderVerts), borderVerts); + glDrawArrays(GL_LINE_STRIP, 0, 5); + } + + glBindVertexArray(0); + glDisable(GL_BLEND); + } + + // Draw status text and percentage with ImGui overlay + { + ImGuiIO& io = ImGui::GetIO(); + float screenW = io.DisplaySize.x; + float screenH = io.DisplaySize.y; + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); + + // Invisible fullscreen window for text overlay + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); + ImGui::Begin("##LoadingOverlay", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoBringToFrontOnFocus); + + // Percentage text centered above bar + char pctBuf[32]; + snprintf(pctBuf, sizeof(pctBuf), "%d%%", static_cast(loadProgress * 100.0f)); + + float barCenterY = screenH * (1.0f - ((-0.82f + 1.0f) / 2.0f)); // NDC -0.82 to screen Y + float textY = barCenterY - 30.0f; + + ImVec2 pctSize = ImGui::CalcTextSize(pctBuf); + ImGui::SetCursorPos(ImVec2((screenW - pctSize.x) * 0.5f, textY)); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%s", pctBuf); + + // Status text centered below bar + float statusY = barCenterY + 16.0f; + ImVec2 statusSize = ImGui::CalcTextSize(statusText.c_str()); + ImGui::SetCursorPos(ImVec2((screenW - statusSize.x) * 0.5f, statusY)); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", statusText.c_str()); + + ImGui::End(); + ImGui::Render(); + + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } - // Re-enable depth test glEnable(GL_DEPTH_TEST); }