Kelsidavis-WoWee/src/rendering/minimap.cpp

526 lines
18 KiB
C++

#include "rendering/minimap.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/coordinates.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <sstream>
#include <cmath>
namespace wowee {
namespace rendering {
Minimap::Minimap() = default;
Minimap::~Minimap() {
shutdown();
}
bool Minimap::initialize(int size) {
mapSize = size;
// --- Composite FBO (3x3 tiles = 768x768) ---
glGenFramebuffers(1, &compositeFBO);
glBindFramebuffer(GL_FRAMEBUFFER, compositeFBO);
glGenTextures(1, &compositeTexture);
glBindTexture(GL_TEXTURE_2D, compositeTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, COMPOSITE_PX, COMPOSITE_PX, 0,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, compositeTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Minimap composite FBO incomplete");
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// --- Unit quad for tile compositing ---
float quadVerts[] = {
// pos (x,y), uv (u,v)
0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
};
glGenVertexArrays(1, &tileQuadVAO);
glGenBuffers(1, &tileQuadVBO);
glBindVertexArray(tileQuadVAO);
glBindBuffer(GL_ARRAY_BUFFER, tileQuadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// --- Tile compositing shader ---
const char* tileVertSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec2 uGridOffset; // (col, row) in 0-2
out vec2 TexCoord;
void main() {
vec2 gridPos = (uGridOffset + aPos) / 3.0;
gl_Position = vec4(gridPos * 2.0 - 1.0, 0.0, 1.0);
TexCoord = aUV;
}
)";
const char* tileFragSrc = R"(
#version 330 core
in vec2 TexCoord;
uniform sampler2D uTileTexture;
out vec4 FragColor;
void main() {
// BLP minimap tiles have same axis transposition as ADT terrain:
// tile U (cols) = north-south, tile V (rows) = west-east
// Composite grid: TexCoord.x = west-east, TexCoord.y = north-south
// So swap to match
FragColor = texture(uTileTexture, vec2(TexCoord.y, TexCoord.x));
}
)";
tileShader = std::make_unique<Shader>();
if (!tileShader->loadFromSource(tileVertSrc, tileFragSrc)) {
LOG_ERROR("Failed to create minimap tile compositing shader");
return false;
}
// --- Screen quad ---
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// --- Screen quad shader with rotation + circular mask ---
const char* quadVertSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec4 uRect; // x, y, w, h in 0..1 screen space
out vec2 TexCoord;
void main() {
vec2 pos = uRect.xy + aUV * uRect.zw;
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
TexCoord = aUV;
}
)";
const char* quadFragSrc = R"(
#version 330 core
in vec2 TexCoord;
uniform sampler2D uComposite;
uniform vec2 uPlayerUV;
uniform float uRotation;
uniform float uArrowRotation;
uniform float uZoomRadius;
out vec4 FragColor;
bool pointInTriangle(vec2 p, vec2 a, vec2 b, vec2 c) {
vec2 v0 = c - a, v1 = b - a, v2 = p - a;
float d00 = dot(v0, v0);
float d01 = dot(v0, v1);
float d02 = dot(v0, v2);
float d11 = dot(v1, v1);
float d12 = dot(v1, v2);
float inv = 1.0 / (d00 * d11 - d01 * d01);
float u = (d11 * d02 - d01 * d12) * inv;
float v = (d00 * d12 - d01 * d02) * inv;
return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0);
}
vec2 rot2(vec2 v, float ang) {
float c = cos(ang);
float s = sin(ang);
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
}
void main() {
vec2 centered = TexCoord - 0.5;
float dist = length(centered);
if (dist > 0.5) discard;
// Rotate screen coords → composite UV offset
// Composite: U increases east, V increases south
// Screen: +X=right, +Y=up
// The -cos(a) term in dV inherently flips V (screen up → composite north)
float c = cos(uRotation);
float s = sin(uRotation);
float scale = uZoomRadius * 2.0;
vec2 offset = vec2(
centered.x * c + centered.y * s,
centered.x * s - centered.y * c
) * scale;
vec2 uv = uPlayerUV + offset;
vec3 color = texture(uComposite, uv).rgb;
// Thin dark border at circle edge
if (dist > 0.49) {
color = mix(color, vec3(0.08), smoothstep(0.49, 0.5, dist));
}
// Player arrow at center (always points up = forward)
vec2 ap = rot2(centered, -uArrowRotation);
vec2 tip = vec2(0.0, 0.035);
vec2 lt = vec2(-0.018, -0.016);
vec2 rt = vec2(0.018, -0.016);
vec2 nL = vec2(-0.006, -0.006);
vec2 nR = vec2(0.006, -0.006);
vec2 nB = vec2(0.0, 0.006);
bool inArrow = pointInTriangle(ap, tip, lt, rt)
&& !pointInTriangle(ap, nL, nR, nB);
if (inArrow) {
color = vec3(0.0, 0.0, 0.0);
}
FragColor = vec4(color, 0.8);
}
)";
quadShader = std::make_unique<Shader>();
if (!quadShader->loadFromSource(quadVertSrc, quadFragSrc)) {
LOG_ERROR("Failed to create minimap screen quad shader");
return false;
}
// --- No-data fallback texture (dark blue-gray) ---
glGenTextures(1, &noDataTexture);
glBindTexture(GL_TEXTURE_2D, noDataTexture);
uint8_t darkPixel[4] = { 12, 20, 30, 255 };
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, darkPixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, " screen, ",
COMPOSITE_PX, "x", COMPOSITE_PX, " composite)");
return true;
}
void Minimap::shutdown() {
if (compositeFBO) { glDeleteFramebuffers(1, &compositeFBO); compositeFBO = 0; }
if (compositeTexture) { glDeleteTextures(1, &compositeTexture); compositeTexture = 0; }
if (tileQuadVAO) { glDeleteVertexArrays(1, &tileQuadVAO); tileQuadVAO = 0; }
if (tileQuadVBO) { glDeleteBuffers(1, &tileQuadVBO); tileQuadVBO = 0; }
if (quadVAO) { glDeleteVertexArrays(1, &quadVAO); quadVAO = 0; }
if (quadVBO) { glDeleteBuffers(1, &quadVBO); quadVBO = 0; }
if (noDataTexture) { glDeleteTextures(1, &noDataTexture); noDataTexture = 0; }
// Delete cached tile textures
for (auto& [hash, tex] : tileTextureCache) {
if (tex) glDeleteTextures(1, &tex);
}
tileTextureCache.clear();
tileShader.reset();
quadShader.reset();
}
void Minimap::setMapName(const std::string& name) {
if (mapName != name) {
mapName = name;
hasCachedFrame = false;
lastCenterTileX = -1;
lastCenterTileY = -1;
}
}
// --------------------------------------------------------
// TRS parsing
// --------------------------------------------------------
void Minimap::parseTRS() {
if (trsParsed || !assetManager) return;
trsParsed = true;
auto data = assetManager->getMPQManager().readFile("Textures\\Minimap\\md5translate.trs");
if (data.empty()) {
LOG_WARNING("Failed to load md5translate.trs");
return;
}
std::string content(reinterpret_cast<const char*>(data.data()), data.size());
std::istringstream stream(content);
std::string line;
int count = 0;
while (std::getline(stream, line)) {
// Remove \r
if (!line.empty() && line.back() == '\r') line.pop_back();
// Skip "dir:" lines and empty lines
if (line.empty() || line.substr(0, 4) == "dir:") continue;
// Format: "Azeroth\map32_49.blp\t<hash>.blp"
auto tabPos = line.find('\t');
if (tabPos == std::string::npos) continue;
std::string key = line.substr(0, tabPos);
std::string hashFile = line.substr(tabPos + 1);
// Strip .blp from key: "Azeroth\map32_49"
if (key.size() > 4 && key.substr(key.size() - 4) == ".blp") {
key = key.substr(0, key.size() - 4);
}
// Strip .blp from hash to get just the md5: "e7f0dea73ee6baca78231aaf4b7e772a"
if (hashFile.size() > 4 && hashFile.substr(hashFile.size() - 4) == ".blp") {
hashFile = hashFile.substr(0, hashFile.size() - 4);
}
trsLookup[key] = hashFile;
count++;
}
LOG_INFO("Parsed md5translate.trs: ", count, " entries");
}
// --------------------------------------------------------
// Tile texture loading
// --------------------------------------------------------
GLuint Minimap::getOrLoadTileTexture(int tileX, int tileY) {
// Build TRS key: "Azeroth\map32_49"
std::string key = mapName + "\\map" + std::to_string(tileX) + "_" + std::to_string(tileY);
auto trsIt = trsLookup.find(key);
if (trsIt == trsLookup.end()) {
return noDataTexture;
}
const std::string& hash = trsIt->second;
// Check texture cache
auto cacheIt = tileTextureCache.find(hash);
if (cacheIt != tileTextureCache.end()) {
return cacheIt->second;
}
// Load from MPQ
std::string blpPath = "Textures\\Minimap\\" + hash + ".blp";
auto blpImage = assetManager->loadTexture(blpPath);
if (!blpImage.isValid()) {
tileTextureCache[hash] = noDataTexture;
return noDataTexture;
}
// Create GL texture
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, blpImage.width, blpImage.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blpImage.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
tileTextureCache[hash] = tex;
return tex;
}
// --------------------------------------------------------
// Composite 3x3 tiles into FBO
// --------------------------------------------------------
void Minimap::compositeTilesToFBO(const glm::vec3& centerWorldPos) {
// centerWorldPos is in render coords (renderX=wowY, renderY=wowX)
auto [tileX, tileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y);
// Save GL state
GLint prevFBO = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO);
GLint prevViewport[4];
glGetIntegerv(GL_VIEWPORT, prevViewport);
glBindFramebuffer(GL_FRAMEBUFFER, compositeFBO);
glViewport(0, 0, COMPOSITE_PX, COMPOSITE_PX);
glClearColor(0.05f, 0.08f, 0.12f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
tileShader->use();
tileShader->setUniform("uTileTexture", 0);
glBindVertexArray(tileQuadVAO);
// Draw 3x3 tile grid into composite FBO.
// BLP first row → GL V=0 (bottom) = north edge of tile.
// So north tile (dr=-1) goes to row 0 (bottom), south (dr=+1) to row 2 (top).
// West tile (dc=-1) goes to col 0 (left), east (dc=+1) to col 2 (right).
// Result: composite U=0→west, U=1→east, V=0→north, V=1→south.
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
int tx = tileX + dr;
int ty = tileY + dc;
GLuint tileTex = getOrLoadTileTexture(tx, ty);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tileTex);
// Grid position: dr=-1 (north) → row 0, dr=0 → row 1, dr=+1 (south) → row 2
float col = static_cast<float>(dc + 1); // 0, 1, 2
float row = static_cast<float>(dr + 1); // 0, 1, 2
tileShader->setUniform("uGridOffset", glm::vec2(col, row));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
}
glBindVertexArray(0);
// Restore GL state
glBindFramebuffer(GL_FRAMEBUFFER, prevFBO);
glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
lastCenterTileX = tileX;
lastCenterTileY = tileY;
}
// --------------------------------------------------------
// Main render
// --------------------------------------------------------
void Minimap::render(const Camera& playerCamera, const glm::vec3& centerWorldPos,
int screenWidth, int screenHeight) {
if (!enabled || !assetManager || !compositeFBO) return;
// Lazy-parse TRS on first use
if (!trsParsed) parseTRS();
// Check if composite needs refresh
const auto now = std::chrono::steady_clock::now();
bool needsRefresh = !hasCachedFrame;
if (!needsRefresh) {
float moved = glm::length(glm::vec2(centerWorldPos.x - lastUpdatePos.x,
centerWorldPos.y - lastUpdatePos.y));
float elapsed = std::chrono::duration<float>(now - lastUpdateTime).count();
needsRefresh = (moved >= updateDistance) || (elapsed >= updateIntervalSec);
}
// Also refresh if player crossed a tile boundary
auto [curTileX, curTileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y);
if (curTileX != lastCenterTileX || curTileY != lastCenterTileY) {
needsRefresh = true;
}
if (needsRefresh) {
compositeTilesToFBO(centerWorldPos);
lastUpdateTime = now;
lastUpdatePos = centerWorldPos;
hasCachedFrame = true;
}
// Draw screen quad
renderQuad(playerCamera, centerWorldPos, screenWidth, screenHeight);
}
void Minimap::renderQuad(const Camera& playerCamera, const glm::vec3& centerWorldPos,
int screenWidth, int screenHeight) {
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
quadShader->use();
// Position minimap in top-right corner
float margin = 10.0f;
float pixelW = static_cast<float>(mapSize) / screenWidth;
float pixelH = static_cast<float>(mapSize) / screenHeight;
float x = 1.0f - pixelW - margin / screenWidth;
float y = 1.0f - pixelH - margin / screenHeight;
quadShader->setUniform("uRect", glm::vec4(x, y, pixelW, pixelH));
// Compute player's UV in the composite texture
// Render coords: renderX = wowY (west axis), renderY = wowX (north axis)
constexpr float TILE_SIZE = core::coords::TILE_SIZE;
auto [tileX, tileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y);
// Fractional position within center tile
// tileX = floor(32 - wowX/TILE_SIZE), wowX = renderY
// fracNS: 0 = north edge of tile, 1 = south edge
float fracNS = 32.0f - static_cast<float>(tileX) - centerWorldPos.y / TILE_SIZE;
// fracEW: 0 = west edge of tile, 1 = east edge
float fracEW = 32.0f - static_cast<float>(tileY) - centerWorldPos.x / TILE_SIZE;
// Composite UV: center tile is grid slot (1,1) → UV range [1/3, 2/3]
// Composite orientation: U=0→west, U=1→east, V=0→north, V=1→south
float playerU = (1.0f + fracEW) / 3.0f;
float playerV = (1.0f + fracNS) / 3.0f;
quadShader->setUniform("uPlayerUV", glm::vec2(playerU, playerV));
// Zoom: convert view radius from world units to composite UV fraction
float zoomRadius = viewRadius / (TILE_SIZE * 3.0f);
quadShader->setUniform("uZoomRadius", zoomRadius);
// Rotation: compass bearing from north, clockwise
// renderX = wowY (west), renderY = wowX (north)
// Facing north: fwd=(0,1,0) → bearing=0
// Facing east: fwd=(-1,0,0) → bearing=π/2
float rotation = 0.0f;
if (rotateWithCamera) {
glm::vec3 fwd = playerCamera.getForward();
rotation = std::atan2(-fwd.x, fwd.y);
}
quadShader->setUniform("uRotation", rotation);
float arrowRotation = 0.0f;
if (!rotateWithCamera) {
glm::vec3 fwd = playerCamera.getForward();
arrowRotation = std::atan2(-fwd.x, fwd.y);
}
quadShader->setUniform("uArrowRotation", arrowRotation);
quadShader->setUniform("uComposite", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, compositeTexture);
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
} // namespace rendering
} // namespace wowee