Add progress bar to loading screen and handle resize during loading

Loading screen now shows a gold progress bar with percentage and status
text. All loading steps poll SDL events for window resize and quit.
This commit is contained in:
Kelsi 2026-02-06 14:43:18 -08:00
parent a0c24e70f9
commit 6f33392155
3 changed files with 276 additions and 52 deletions

View file

@ -1,7 +1,11 @@
#include "rendering/loading_screen.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl2.h>
#include <random>
#include <chrono>
#include <cstdio>
#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<unsigned>(
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<int>(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);
}