Add loading screen with random WOWEE splash images

- Add loading screen system with stb_image for JPEG loading
- Two loading screen images (orc and dwarf) randomly selected
- Display loading screen while terrain data loads
- Cache WMO inverse matrices to reduce per-frame computation
- Stub WMO liquid rendering (needs coordinate system fix)
- Update spawn point to Stormwind Trade District
This commit is contained in:
Kelsi 2026-02-03 13:33:31 -08:00
parent 665a73e75f
commit 01bf3b4c08
14 changed files with 8395 additions and 165 deletions

View file

@ -0,0 +1,225 @@
#include "rendering/loading_screen.hpp"
#include "core/logger.hpp"
#include <random>
#include <chrono>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
namespace wowee {
namespace rendering {
LoadingScreen::LoadingScreen() {
// Add loading screen image paths
imagePaths.push_back("assets/loading1.jpg");
imagePaths.push_back("assets/loading2.jpg");
}
LoadingScreen::~LoadingScreen() {
shutdown();
}
bool LoadingScreen::initialize() {
LOG_INFO("Initializing loading screen");
// Create simple shader for textured quad
const char* vertexSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
TexCoord = aTexCoord;
}
)";
const char* fragmentSrc = R"(
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D screenTexture;
void main() {
FragColor = texture(screenTexture, TexCoord);
}
)";
// Compile vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSrc, nullptr);
glCompileShader(vertexShader);
GLint success;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
LOG_ERROR("Loading screen vertex shader compilation failed: ", infoLog);
return false;
}
// Compile fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSrc, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
LOG_ERROR("Loading screen fragment shader compilation failed: ", infoLog);
return false;
}
// Link shader program
shaderId = glCreateProgram();
glAttachShader(shaderId, vertexShader);
glAttachShader(shaderId, fragmentShader);
glLinkProgram(shaderId);
glGetProgramiv(shaderId, GL_LINK_STATUS, &success);
if (!success) {
char infoLog[512];
glGetProgramInfoLog(shaderId, 512, nullptr, infoLog);
LOG_ERROR("Loading screen shader linking failed: ", infoLog);
return false;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
createQuad();
selectRandomImage();
LOG_INFO("Loading screen initialized");
return true;
}
void LoadingScreen::shutdown() {
if (textureId) {
glDeleteTextures(1, &textureId);
textureId = 0;
}
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (shaderId) {
glDeleteProgram(shaderId);
shaderId = 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);
std::uniform_int_distribution<int> distribution(0, imagePaths.size() - 1);
currentImageIndex = distribution(generator);
LOG_INFO("Selected loading screen: ", imagePaths[currentImageIndex]);
loadImage(imagePaths[currentImageIndex]);
}
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);
if (!data) {
LOG_ERROR("Failed to load loading screen image: ", path);
return false;
}
LOG_INFO("Loaded loading screen image: ", imageWidth, "x", imageHeight);
// Create OpenGL texture
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
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, // Top-left
1.0f, -1.0f, 1.0f, 0.0f, // Bottom-right
1.0f, 1.0f, 1.0f, 1.0f // Top-right
};
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
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;
// 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 quad
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
// Re-enable depth test
glEnable(GL_DEPTH_TEST);
}
} // namespace rendering
} // namespace wowee

View file

@ -469,15 +469,37 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
}
int loadedWMOs = 0;
int loadedLiquids = 0;
for (auto& wmoReady : pending->wmoModels) {
if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) {
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
if (wmoInstId) {
wmoInstanceIds.push_back(wmoInstId);
loadedWMOs++;
// Load WMO liquids (canals, pools, etc.)
if (waterRenderer) {
// Compute the same model matrix as WMORenderer uses
glm::mat4 modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, wmoReady.position);
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
// Load liquids from each WMO group
for (const auto& group : wmoReady.model.groups) {
if (group.liquid.hasLiquid()) {
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
loadedLiquids++;
}
}
}
}
}
}
if (loadedLiquids > 0) {
LOG_INFO(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
}
// Upload WMO doodad M2 models
if (m2Renderer) {
@ -608,9 +630,13 @@ void TerrainManager::unloadTile(int x, int y) {
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
}
// Remove WMO instances
// Remove WMO instances and their liquids
if (wmoRenderer) {
for (uint32_t id : tile->wmoInstanceIds) {
// Remove WMO liquids associated with this instance
if (waterRenderer) {
waterRenderer->removeWMO(id);
}
wmoRenderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");

View file

@ -2,6 +2,7 @@
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
@ -209,6 +210,16 @@ void WaterRenderer::removeTile(int tileX, int tileY) {
}
}
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
[[maybe_unused]] const glm::mat4& modelMatrix,
[[maybe_unused]] uint32_t wmoId) {
// WMO liquid rendering not yet implemented
}
void WaterRenderer::removeWMO([[maybe_unused]] uint32_t wmoId) {
// WMO liquid rendering not yet implemented
}
void WaterRenderer::clear() {
for (auto& surface : surfaces) {
destroyWaterMesh(surface);

View file

@ -620,6 +620,9 @@ void WMORenderer::WMOInstance::updateModelMatrix() {
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
// Cache inverse for collision detection
invModelMatrix = glm::inverse(modelMatrix);
}
GLuint WMORenderer::loadTexture(const std::string& path) {
@ -728,10 +731,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
const ModelData& model = it->second;
// Transform ray into model-local space
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(worldOrigin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(worldDir, 0.0f)));
// Use cached inverse matrix
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
for (const auto& group : model.groups) {
// Quick bounding box check: does the ray intersect this group's AABB?
@ -785,10 +787,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
if (it == loadedModels.end()) continue;
const ModelData& model = it->second;
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
// Transform positions into local space
glm::vec3 localTo = glm::vec3(invModel * glm::vec4(to, 1.0f));
// Transform positions into local space using cached inverse
glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f));
for (const auto& group : model.groups) {
// Quick bounding box check
@ -865,8 +866,7 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode
if (it == loadedModels.end()) continue;
const ModelData& model = it->second;
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
glm::vec3 localPos = glm::vec3(invModel * glm::vec4(glX, glY, glZ, 1.0f));
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
// Check if inside any group's bounding box
for (const auto& group : model.groups) {
@ -890,10 +890,9 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
const ModelData& model = it->second;
// Transform ray into local space
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(origin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(direction, 0.0f)));
// Use cached inverse matrix
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(origin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(direction, 0.0f)));
for (const auto& group : model.groups) {
// Ray-AABB intersection (slab method)