Initial commit: wowee native WoW 3.3.5a client

This commit is contained in:
Kelsi 2026-02-02 12:24:50 -08:00
commit ce6cb8f38e
147 changed files with 32347 additions and 0 deletions

56
src/rendering/camera.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "rendering/camera.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/constants.hpp>
namespace wowee {
namespace rendering {
Camera::Camera() {
updateViewMatrix();
updateProjectionMatrix();
}
void Camera::updateViewMatrix() {
glm::vec3 front = getForward();
// Use Z-up for WoW coordinate system
viewMatrix = glm::lookAt(position, position + front, glm::vec3(0.0f, 0.0f, 1.0f));
}
void Camera::updateProjectionMatrix() {
projectionMatrix = glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
}
glm::vec3 Camera::getForward() const {
// WoW coordinate system: X/Y horizontal, Z vertical
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.z = sin(glm::radians(pitch));
return glm::normalize(front);
}
glm::vec3 Camera::getRight() const {
// Use Z-up for WoW coordinate system
return glm::normalize(glm::cross(getForward(), glm::vec3(0.0f, 0.0f, 1.0f)));
}
glm::vec3 Camera::getUp() const {
return glm::normalize(glm::cross(getRight(), getForward()));
}
Ray Camera::screenToWorldRay(float screenX, float screenY, float screenW, float screenH) const {
float ndcX = (2.0f * screenX / screenW) - 1.0f;
float ndcY = 1.0f - (2.0f * screenY / screenH);
glm::mat4 invVP = glm::inverse(projectionMatrix * viewMatrix);
glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, -1.0f, 1.0f);
glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f);
nearPt /= nearPt.w;
farPt /= farPt.w;
return { glm::vec3(nearPt), glm::normalize(glm::vec3(farPt - nearPt)) };
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,525 @@
#include "rendering/camera_controller.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/water_renderer.hpp"
#include "game/opcodes.hpp"
#include "core/logger.hpp"
#include <glm/glm.hpp>
#include <imgui.h>
namespace wowee {
namespace rendering {
CameraController::CameraController(Camera* cam) : camera(cam) {
yaw = defaultYaw;
pitch = defaultPitch;
reset();
}
void CameraController::update(float deltaTime) {
if (!enabled || !camera) {
return;
}
auto& input = core::Input::getInstance();
// Don't process keyboard input when UI (e.g. chat box) has focus
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
// Determine current key states
bool nowForward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
bool nowBackward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
bool nowStrafeLeft = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
bool nowStrafeRight = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
// Select physics constants based on mode
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY;
// Calculate movement speed based on direction and modifiers
float speed;
if (useWoWSpeed) {
// WoW-correct speeds
if (nowBackward && !nowForward) {
speed = WOW_BACK_SPEED;
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
speed = WOW_WALK_SPEED; // Shift = walk in WoW mode
} else {
speed = WOW_RUN_SPEED;
}
} else {
// Exploration mode (original behavior)
speed = movementSpeed;
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
speed *= sprintMultiplier;
}
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
speed *= slowMultiplier;
}
}
// Get camera axes — project forward onto XY plane for walking
glm::vec3 forward3D = camera->getForward();
glm::vec3 forward = glm::normalize(glm::vec3(forward3D.x, forward3D.y, 0.0f));
glm::vec3 right = camera->getRight();
right.z = 0.0f;
if (glm::length(right) > 0.001f) {
right = glm::normalize(right);
}
// Toggle sit with X key (edge-triggered) — only when UI doesn't want keyboard
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
if (xDown && !xKeyWasDown) {
sitting = !sitting;
}
xKeyWasDown = xDown;
// Calculate horizontal movement vector
glm::vec3 movement(0.0f);
if (nowForward) movement += forward;
if (nowBackward) movement -= forward;
if (nowStrafeLeft) movement -= right;
if (nowStrafeRight) movement += right;
// Stand up if any movement key is pressed while sitting
if (!uiWantsKeyboard && sitting && (input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) ||
input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D) ||
input.isKeyPressed(SDL_SCANCODE_SPACE))) {
sitting = false;
}
// Third-person orbit camera mode
if (thirdPerson && followTarget) {
// Move the follow target (character position) instead of the camera
glm::vec3 targetPos = *followTarget;
// Check for water at current position
std::optional<float> waterH;
if (waterRenderer) {
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
}
bool inWater = waterH && targetPos.z < *waterH;
if (inWater) {
swimming = true;
// Reduce horizontal speed while swimming
float swimSpeed = speed * SWIM_SPEED_FACTOR;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
targetPos += movement * swimSpeed * deltaTime;
}
// Spacebar = swim up (continuous, not a jump)
if (nowJump) {
verticalVelocity = SWIM_BUOYANCY;
} else {
// Gentle sink when not pressing space
verticalVelocity += SWIM_GRAVITY * deltaTime;
if (verticalVelocity < SWIM_SINK_SPEED) {
verticalVelocity = SWIM_SINK_SPEED;
}
}
targetPos.z += verticalVelocity * deltaTime;
// Don't rise above water surface
if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) {
targetPos.z = *waterH - WATER_SURFACE_OFFSET;
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
}
grounded = false;
} else {
swimming = false;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
targetPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
verticalVelocity = jumpVel;
grounded = false;
}
// Apply gravity
verticalVelocity += gravity * deltaTime;
targetPos.z += verticalVelocity * deltaTime;
}
// Wall collision for character
if (wmoRenderer) {
glm::vec3 feetPos = targetPos;
glm::vec3 oldFeetPos = *followTarget;
glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
targetPos.x = adjusted.x;
targetPos.y = adjusted.y;
targetPos.z = adjusted.z;
}
}
// Ground the character to terrain or WMO floor
{
std::optional<float> terrainH;
std::optional<float> wmoH;
if (terrainManager) {
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
}
if (wmoRenderer) {
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
}
std::optional<float> groundH;
if (terrainH && wmoH) {
groundH = std::max(*terrainH, *wmoH);
} else if (terrainH) {
groundH = terrainH;
} else if (wmoH) {
groundH = wmoH;
}
if (groundH) {
lastGroundZ = *groundH;
if (targetPos.z <= *groundH) {
targetPos.z = *groundH;
verticalVelocity = 0.0f;
grounded = true;
swimming = false; // Touching ground = wading, not swimming
} else if (!swimming) {
grounded = false;
}
} else if (!swimming) {
// No terrain found — hold at last known ground
targetPos.z = lastGroundZ;
verticalVelocity = 0.0f;
grounded = true;
}
}
// Update follow target position
*followTarget = targetPos;
// Compute camera position orbiting behind the character
glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight);
glm::vec3 camPos = lookAtPoint - forward3D * orbitDistance;
// Clamp camera above terrain/WMO floor
{
float minCamZ = camPos.z;
if (terrainManager) {
auto h = terrainManager->getHeightAt(camPos.x, camPos.y);
if (h) minCamZ = *h + 1.0f; // 1 unit above ground
}
if (wmoRenderer) {
auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight);
if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f;
}
if (camPos.z < minCamZ) {
camPos.z = minCamZ;
}
}
camera->setPosition(camPos);
} else {
// Free-fly camera mode (original behavior)
glm::vec3 newPos = camera->getPosition();
float feetZ = newPos.z - eyeHeight;
// Check for water at feet position
std::optional<float> waterH;
if (waterRenderer) {
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
}
bool inWater = waterH && feetZ < *waterH;
if (inWater) {
swimming = true;
float swimSpeed = speed * SWIM_SPEED_FACTOR;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
newPos += movement * swimSpeed * deltaTime;
}
if (nowJump) {
verticalVelocity = SWIM_BUOYANCY;
} else {
verticalVelocity += SWIM_GRAVITY * deltaTime;
if (verticalVelocity < SWIM_SINK_SPEED) {
verticalVelocity = SWIM_SINK_SPEED;
}
}
newPos.z += verticalVelocity * deltaTime;
// Don't rise above water surface (feet at water level)
if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) {
newPos.z = *waterH - WATER_SURFACE_OFFSET + eyeHeight;
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
}
grounded = false;
} else {
swimming = false;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
newPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
verticalVelocity = jumpVel;
grounded = false;
}
// Apply gravity
verticalVelocity += gravity * deltaTime;
newPos.z += verticalVelocity * deltaTime;
}
// Wall collision — push out of WMO walls before grounding
if (wmoRenderer) {
glm::vec3 feetPos = newPos - glm::vec3(0, 0, eyeHeight);
glm::vec3 oldFeetPos = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
newPos.x = adjusted.x;
newPos.y = adjusted.y;
newPos.z = adjusted.z + eyeHeight;
}
}
// Ground to terrain or WMO floor
{
std::optional<float> terrainH;
std::optional<float> wmoH;
if (terrainManager) {
terrainH = terrainManager->getHeightAt(newPos.x, newPos.y);
}
if (wmoRenderer) {
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
}
std::optional<float> groundH;
if (terrainH && wmoH) {
groundH = std::max(*terrainH, *wmoH);
} else if (terrainH) {
groundH = terrainH;
} else if (wmoH) {
groundH = wmoH;
}
if (groundH) {
lastGroundZ = *groundH;
float groundZ = *groundH + eyeHeight;
if (newPos.z <= groundZ) {
newPos.z = groundZ;
verticalVelocity = 0.0f;
grounded = true;
swimming = false; // Touching ground = wading
} else if (!swimming) {
grounded = false;
}
} else if (!swimming) {
float groundZ = lastGroundZ + eyeHeight;
newPos.z = groundZ;
verticalVelocity = 0.0f;
grounded = true;
}
}
camera->setPosition(newPos);
}
// --- Edge-detection: send movement opcodes on state transitions ---
if (movementCallback) {
// Forward/backward
if (nowForward && !wasMovingForward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_FORWARD));
}
if (nowBackward && !wasMovingBackward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_BACKWARD));
}
if ((!nowForward && wasMovingForward) || (!nowBackward && wasMovingBackward)) {
if (!nowForward && !nowBackward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP));
}
}
// Strafing
if (nowStrafeLeft && !wasStrafingLeft) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_LEFT));
}
if (nowStrafeRight && !wasStrafingRight) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_RIGHT));
}
if ((!nowStrafeLeft && wasStrafingLeft) || (!nowStrafeRight && wasStrafingRight)) {
if (!nowStrafeLeft && !nowStrafeRight) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_STRAFE));
}
}
// Jump
if (nowJump && !wasJumping && grounded) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
}
// Fall landing
if (wasFalling && grounded) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_FALL_LAND));
}
}
// Swimming state transitions
if (movementCallback) {
if (swimming && !wasSwimming) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_SWIM));
} else if (!swimming && wasSwimming) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_SWIM));
}
}
// Update previous-frame state
wasSwimming = swimming;
wasMovingForward = nowForward;
wasMovingBackward = nowBackward;
wasStrafingLeft = nowStrafeLeft;
wasStrafingRight = nowStrafeRight;
wasJumping = nowJump;
wasFalling = !grounded && verticalVelocity <= 0.0f;
// Reset camera (R key)
if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) {
reset();
}
}
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
if (!enabled || !camera) {
return;
}
if (!mouseButtonDown) {
return;
}
// Directly update stored yaw/pitch (no lossy forward-vector derivation)
yaw -= event.xrel * mouseSensitivity;
pitch += event.yrel * mouseSensitivity;
pitch = glm::clamp(pitch, -89.0f, 89.0f);
camera->setRotation(yaw, pitch);
}
void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
if (!enabled) {
return;
}
if (event.button == SDL_BUTTON_LEFT) {
leftMouseDown = (event.state == SDL_PRESSED);
}
if (event.button == SDL_BUTTON_RIGHT) {
rightMouseDown = (event.state == SDL_PRESSED);
}
bool anyDown = leftMouseDown || rightMouseDown;
if (anyDown && !mouseButtonDown) {
SDL_SetRelativeMouseMode(SDL_TRUE);
} else if (!anyDown && mouseButtonDown) {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
mouseButtonDown = anyDown;
}
void CameraController::reset() {
if (!camera) {
return;
}
yaw = defaultYaw;
pitch = defaultPitch;
verticalVelocity = 0.0f;
grounded = false;
glm::vec3 spawnPos = defaultPosition;
// Snap spawn to terrain or WMO surface
std::optional<float> h;
if (terrainManager) {
h = terrainManager->getHeightAt(spawnPos.x, spawnPos.y);
}
if (wmoRenderer) {
auto wh = wmoRenderer->getFloorHeight(spawnPos.x, spawnPos.y, spawnPos.z);
if (wh && (!h || *wh > *h)) {
h = wh;
}
}
if (h) {
lastGroundZ = *h;
spawnPos.z = *h + eyeHeight;
}
camera->setPosition(spawnPos);
camera->setRotation(yaw, pitch);
LOG_INFO("Camera reset to default position");
}
void CameraController::processMouseWheel(float delta) {
orbitDistance -= delta * zoomSpeed;
orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance);
}
void CameraController::setFollowTarget(glm::vec3* target) {
followTarget = target;
if (target) {
thirdPerson = true;
LOG_INFO("Third-person camera enabled");
} else {
thirdPerson = false;
LOG_INFO("Free-fly camera enabled");
}
}
bool CameraController::isMoving() const {
if (!enabled || !camera) {
return false;
}
if (ImGui::GetIO().WantCaptureKeyboard) {
return false;
}
auto& input = core::Input::getInstance();
return input.isKeyPressed(SDL_SCANCODE_W) ||
input.isKeyPressed(SDL_SCANCODE_S) ||
input.isKeyPressed(SDL_SCANCODE_A) ||
input.isKeyPressed(SDL_SCANCODE_D);
}
bool CameraController::isSprinting() const {
if (!enabled || !camera) {
return false;
}
if (ImGui::GetIO().WantCaptureKeyboard) {
return false;
}
auto& input = core::Input::getInstance();
return isMoving() && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
}
} // namespace rendering
} // namespace wowee

412
src/rendering/celestial.cpp Normal file
View file

@ -0,0 +1,412 @@
#include "rendering/celestial.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
Celestial::Celestial() = default;
Celestial::~Celestial() {
shutdown();
}
bool Celestial::initialize() {
LOG_INFO("Initializing celestial renderer");
// Create celestial shader
celestialShader = std::make_unique<Shader>();
// Vertex shader - billboard facing camera
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 TexCoord;
void main() {
TexCoord = aTexCoord;
// Billboard: remove rotation from view matrix, keep only translation
mat4 viewNoRotation = view;
viewNoRotation[0][0] = 1.0; viewNoRotation[0][1] = 0.0; viewNoRotation[0][2] = 0.0;
viewNoRotation[1][0] = 0.0; viewNoRotation[1][1] = 1.0; viewNoRotation[1][2] = 0.0;
viewNoRotation[2][0] = 0.0; viewNoRotation[2][1] = 0.0; viewNoRotation[2][2] = 1.0;
gl_Position = projection * viewNoRotation * model * vec4(aPos, 1.0);
}
)";
// Fragment shader - disc with glow and moon phase support
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
uniform vec3 celestialColor;
uniform float intensity;
uniform float moonPhase; // 0.0 = new moon, 0.5 = full moon, 1.0 = new moon
out vec4 FragColor;
void main() {
// Create circular disc
vec2 center = vec2(0.5, 0.5);
float dist = distance(TexCoord, center);
// Core disc
float disc = smoothstep(0.5, 0.4, dist);
// Glow around disc
float glow = smoothstep(0.7, 0.0, dist) * 0.3;
float alpha = (disc + glow) * intensity;
// Apply moon phase shadow (only for moon, indicated by low intensity)
if (intensity < 0.5) { // Moon has lower intensity than sun
// Calculate phase position (-1 to 1, where 0 is center)
float phasePos = (moonPhase - 0.5) * 2.0;
// Distance from phase terminator line
float x = (TexCoord.x - 0.5) * 2.0; // -1 to 1
// Create shadow using smoothstep
float shadow = 1.0;
if (moonPhase < 0.5) {
// Waning (right to left shadow)
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, x);
} else {
// Waxing (left to right shadow)
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, -x);
}
// Apply elliptical terminator for 3D effect
float y = (TexCoord.y - 0.5) * 2.0;
float ellipse = sqrt(1.0 - y * y);
float terminatorX = phasePos / ellipse;
if (moonPhase < 0.5) {
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, x);
} else {
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, -x);
}
// Darken shadowed area (not completely black, slight glow remains)
alpha *= mix(0.05, 1.0, shadow);
}
FragColor = vec4(celestialColor, alpha);
}
)";
if (!celestialShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create celestial shader");
return false;
}
// Create billboard quad
createCelestialQuad();
LOG_INFO("Celestial renderer initialized");
return true;
}
void Celestial::shutdown() {
destroyCelestialQuad();
celestialShader.reset();
}
void Celestial::render(const Camera& camera, float timeOfDay) {
if (!renderingEnabled || vao == 0 || !celestialShader) {
return;
}
// Enable blending for celestial glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth writing (but keep depth testing)
glDepthMask(GL_FALSE);
// Render sun and moon
renderSun(camera, timeOfDay);
renderMoon(camera, timeOfDay);
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
void Celestial::renderSun(const Camera& camera, float timeOfDay) {
// Sun visible from 5:00 to 19:00
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
return;
}
celestialShader->use();
// Get sun position
glm::vec3 sunPos = getSunPosition(timeOfDay);
// Create model matrix
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, sunPos);
model = glm::scale(model, glm::vec3(50.0f, 50.0f, 1.0f)); // 50 unit diameter
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
celestialShader->setUniform("model", model);
celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection);
// Sun color and intensity
glm::vec3 color = getSunColor(timeOfDay);
float intensity = getSunIntensity(timeOfDay);
celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", 0.5f); // Sun doesn't use this, but shader expects it
// Render quad
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
void Celestial::renderMoon(const Camera& camera, float timeOfDay) {
// Moon visible from 19:00 to 5:00 (night)
if (timeOfDay >= 5.0f && timeOfDay < 19.0f) {
return;
}
celestialShader->use();
// Get moon position
glm::vec3 moonPos = getMoonPosition(timeOfDay);
// Create model matrix
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, moonPos);
model = glm::scale(model, glm::vec3(40.0f, 40.0f, 1.0f)); // 40 unit diameter (smaller than sun)
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
celestialShader->setUniform("model", model);
celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection);
// Moon color (pale blue-white) and intensity
glm::vec3 color = glm::vec3(0.8f, 0.85f, 1.0f);
// Fade in/out at transitions
float intensity = 1.0f;
if (timeOfDay >= 19.0f && timeOfDay < 21.0f) {
// Fade in (19:00-21:00)
intensity = (timeOfDay - 19.0f) / 2.0f;
}
else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) {
// Fade out (3:00-5:00)
intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f;
}
celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", moonPhase);
// Render quad
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
glm::vec3 Celestial::getSunPosition(float timeOfDay) const {
// Sun rises at 6:00, peaks at 12:00, sets at 18:00
float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f);
const float radius = 800.0f; // Distance from origin
const float height = 600.0f; // Maximum height
// Arc across sky
float x = radius * std::sin(angle);
float z = height * std::cos(angle);
float y = 0.0f; // Y is horizontal in WoW coordinates
return glm::vec3(x, y, z);
}
glm::vec3 Celestial::getMoonPosition(float timeOfDay) const {
// Moon rises at 18:00, peaks at 0:00 (24:00), sets at 6:00
// Adjust time for moon (opposite to sun)
float moonTime = timeOfDay + 12.0f;
if (moonTime >= 24.0f) moonTime -= 24.0f;
float angle = calculateCelestialAngle(moonTime, 6.0f, 18.0f);
const float radius = 800.0f;
const float height = 600.0f;
float x = radius * std::sin(angle);
float z = height * std::cos(angle);
float y = 0.0f;
return glm::vec3(x, y, z);
}
glm::vec3 Celestial::getSunColor(float timeOfDay) const {
// Sunrise/sunset: orange/red
// Midday: bright yellow-white
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
// Sunrise: orange
return glm::vec3(1.0f, 0.6f, 0.2f);
}
else if (timeOfDay >= 7.0f && timeOfDay < 9.0f) {
// Morning: blend to yellow
float t = (timeOfDay - 7.0f) / 2.0f;
glm::vec3 orange = glm::vec3(1.0f, 0.6f, 0.2f);
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
return glm::mix(orange, yellow, t);
}
else if (timeOfDay >= 9.0f && timeOfDay < 16.0f) {
// Day: bright yellow-white
return glm::vec3(1.0f, 1.0f, 0.9f);
}
else if (timeOfDay >= 16.0f && timeOfDay < 18.0f) {
// Evening: blend to orange
float t = (timeOfDay - 16.0f) / 2.0f;
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
glm::vec3 orange = glm::vec3(1.0f, 0.5f, 0.1f);
return glm::mix(yellow, orange, t);
}
else {
// Sunset: deep orange/red
return glm::vec3(1.0f, 0.4f, 0.1f);
}
}
float Celestial::getSunIntensity(float timeOfDay) const {
// Fade in at sunrise (5:00-6:00)
if (timeOfDay >= 5.0f && timeOfDay < 6.0f) {
return (timeOfDay - 5.0f); // 0 to 1
}
// Full intensity during day (6:00-18:00)
else if (timeOfDay >= 6.0f && timeOfDay < 18.0f) {
return 1.0f;
}
// Fade out at sunset (18:00-19:00)
else if (timeOfDay >= 18.0f && timeOfDay < 19.0f) {
return 1.0f - (timeOfDay - 18.0f); // 1 to 0
}
else {
return 0.0f;
}
}
float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const {
// Map time to angle (0 to PI)
// riseTime: 0 radians (horizon east)
// (riseTime + setTime) / 2: PI/2 radians (zenith)
// setTime: PI radians (horizon west)
float duration = setTime - riseTime;
float elapsed = timeOfDay - riseTime;
// Normalize to 0-1
float t = elapsed / duration;
// Map to 0 to PI (arc from east to west)
return t * M_PI;
}
void Celestial::createCelestialQuad() {
// Simple quad centered at origin
float vertices[] = {
// Position // TexCoord
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // Top-left
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top-right
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom-right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f // Bottom-left
};
uint32_t indices[] = {
0, 1, 2, // First triangle
0, 2, 3 // Second triangle
};
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Texture coordinates
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void Celestial::destroyCelestialQuad() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo != 0) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
void Celestial::update(float deltaTime) {
if (!moonPhaseCycling) {
return;
}
// Update moon phase timer
moonPhaseTimer += deltaTime;
// Moon completes full cycle in MOON_CYCLE_DURATION seconds
moonPhase = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f);
}
void Celestial::setMoonPhase(float phase) {
// Clamp phase to 0.0-1.0
moonPhase = glm::clamp(phase, 0.0f, 1.0f);
// Update timer to match phase
moonPhaseTimer = moonPhase * MOON_CYCLE_DURATION;
}
} // namespace rendering
} // namespace wowee

File diff suppressed because it is too large Load diff

312
src/rendering/clouds.cpp Normal file
View file

@ -0,0 +1,312 @@
#include "rendering/clouds.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
Clouds::Clouds() {
}
Clouds::~Clouds() {
cleanup();
}
bool Clouds::initialize() {
LOG_INFO("Initializing cloud system");
// Generate cloud dome mesh
generateMesh();
// Create VAO
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
vertices.size() * sizeof(glm::vec3),
vertices.data(),
GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
indices.size() * sizeof(unsigned int),
indices.data(),
GL_STATIC_DRAW);
// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// Create shader
shader = std::make_unique<Shader>();
// Cloud vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 WorldPos;
out vec3 LocalPos;
void main() {
LocalPos = aPos;
WorldPos = aPos;
// Remove translation from view matrix (billboard effect)
mat4 viewNoTranslation = uView;
viewNoTranslation[3][0] = 0.0;
viewNoTranslation[3][1] = 0.0;
viewNoTranslation[3][2] = 0.0;
vec4 pos = uProjection * viewNoTranslation * vec4(aPos, 1.0);
gl_Position = pos.xyww; // Put at far plane
}
)";
// Cloud fragment shader with procedural noise
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 WorldPos;
in vec3 LocalPos;
uniform vec3 uCloudColor;
uniform float uDensity;
uniform float uWindOffset;
out vec4 FragColor;
// Simple 3D noise function
float hash(vec3 p) {
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
p += dot(p, p.yxz + 19.19);
return fract((p.x + p.y) * p.z);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
f.z);
}
// Fractal Brownian Motion for cloud-like patterns
float fbm(vec3 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
void main() {
// Normalize position for noise sampling
vec3 pos = normalize(LocalPos);
// Only render on upper hemisphere
if (pos.y < 0.1) {
discard;
}
// Apply wind offset to x coordinate
vec3 samplePos = vec3(pos.x + uWindOffset, pos.y, pos.z) * 3.0;
// Generate two cloud layers
float clouds1 = fbm(samplePos * 1.0);
float clouds2 = fbm(samplePos * 2.0 + vec3(100.0));
// Combine layers
float cloudPattern = clouds1 * 0.6 + clouds2 * 0.4;
// Apply density threshold to create cloud shapes
float cloudMask = smoothstep(0.4 + (1.0 - uDensity) * 0.3, 0.7, cloudPattern);
// Add some variation to cloud edges
float edgeNoise = noise(samplePos * 5.0);
cloudMask *= smoothstep(0.3, 0.7, edgeNoise);
// Fade clouds near horizon
float horizonFade = smoothstep(0.0, 0.3, pos.y);
cloudMask *= horizonFade;
// Final alpha
float alpha = cloudMask * 0.85;
if (alpha < 0.05) {
discard;
}
FragColor = vec4(uCloudColor, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create cloud shader");
return false;
}
LOG_INFO("Cloud system initialized: ", triangleCount, " triangles");
return true;
}
void Clouds::generateMesh() {
vertices.clear();
indices.clear();
// Generate hemisphere mesh for clouds
for (int ring = 0; ring <= RINGS; ++ring) {
float phi = (ring / static_cast<float>(RINGS)) * (M_PI * 0.5f); // 0 to π/2
float y = RADIUS * cos(phi);
float ringRadius = RADIUS * sin(phi);
for (int segment = 0; segment <= SEGMENTS; ++segment) {
float theta = (segment / static_cast<float>(SEGMENTS)) * (2.0f * M_PI);
float x = ringRadius * cos(theta);
float z = ringRadius * sin(theta);
vertices.push_back(glm::vec3(x, y, z));
}
}
// Generate indices
for (int ring = 0; ring < RINGS; ++ring) {
for (int segment = 0; segment < SEGMENTS; ++segment) {
int current = ring * (SEGMENTS + 1) + segment;
int next = current + SEGMENTS + 1;
// Two triangles per quad
indices.push_back(current);
indices.push_back(next);
indices.push_back(current + 1);
indices.push_back(current + 1);
indices.push_back(next);
indices.push_back(next + 1);
}
}
triangleCount = static_cast<int>(indices.size()) / 3;
}
void Clouds::update(float deltaTime) {
if (!enabled) {
return;
}
// Accumulate wind movement
windOffset += deltaTime * windSpeed * 0.05f; // Slow drift
}
glm::vec3 Clouds::getCloudColor(float timeOfDay) const {
// Base cloud color (white/light gray)
glm::vec3 dayColor(0.95f, 0.95f, 1.0f);
// Dawn clouds (orange tint)
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
float t = (timeOfDay - 5.0f) / 2.0f;
glm::vec3 dawnColor(1.0f, 0.7f, 0.5f);
return glm::mix(dawnColor, dayColor, t);
}
// Dusk clouds (orange/pink tint)
else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) {
float t = (timeOfDay - 17.0f) / 2.0f;
glm::vec3 duskColor(1.0f, 0.6f, 0.4f);
return glm::mix(dayColor, duskColor, t);
}
// Night clouds (dark blue-gray)
else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) {
return glm::vec3(0.15f, 0.15f, 0.25f);
}
return dayColor;
}
void Clouds::render(const Camera& camera, float timeOfDay) {
if (!enabled || !shader) {
return;
}
// Enable blending for transparent clouds
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth write (clouds are in sky)
glDepthMask(GL_FALSE);
// Enable depth test so clouds are behind skybox
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
shader->use();
// Set matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set cloud parameters
glm::vec3 cloudColor = getCloudColor(timeOfDay);
shader->setUniform("uCloudColor", cloudColor);
shader->setUniform("uDensity", density);
shader->setUniform("uWindOffset", windOffset);
// Render
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(indices.size()), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// Restore state
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);
}
void Clouds::setDensity(float density) {
this->density = glm::clamp(density, 0.0f, 1.0f);
}
void Clouds::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
} // namespace rendering
} // namespace wowee

106
src/rendering/frustum.cpp Normal file
View file

@ -0,0 +1,106 @@
#include "rendering/frustum.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
void Frustum::extractFromMatrix(const glm::mat4& vp) {
// Extract planes from view-projection matrix
// Based on Gribb & Hartmann method (Fast Extraction of Viewing Frustum Planes)
// Left plane: row4 + row1
planes[LEFT].normal.x = vp[0][3] + vp[0][0];
planes[LEFT].normal.y = vp[1][3] + vp[1][0];
planes[LEFT].normal.z = vp[2][3] + vp[2][0];
planes[LEFT].distance = vp[3][3] + vp[3][0];
normalizePlane(planes[LEFT]);
// Right plane: row4 - row1
planes[RIGHT].normal.x = vp[0][3] - vp[0][0];
planes[RIGHT].normal.y = vp[1][3] - vp[1][0];
planes[RIGHT].normal.z = vp[2][3] - vp[2][0];
planes[RIGHT].distance = vp[3][3] - vp[3][0];
normalizePlane(planes[RIGHT]);
// Bottom plane: row4 + row2
planes[BOTTOM].normal.x = vp[0][3] + vp[0][1];
planes[BOTTOM].normal.y = vp[1][3] + vp[1][1];
planes[BOTTOM].normal.z = vp[2][3] + vp[2][1];
planes[BOTTOM].distance = vp[3][3] + vp[3][1];
normalizePlane(planes[BOTTOM]);
// Top plane: row4 - row2
planes[TOP].normal.x = vp[0][3] - vp[0][1];
planes[TOP].normal.y = vp[1][3] - vp[1][1];
planes[TOP].normal.z = vp[2][3] - vp[2][1];
planes[TOP].distance = vp[3][3] - vp[3][1];
normalizePlane(planes[TOP]);
// Near plane: row4 + row3
planes[NEAR].normal.x = vp[0][3] + vp[0][2];
planes[NEAR].normal.y = vp[1][3] + vp[1][2];
planes[NEAR].normal.z = vp[2][3] + vp[2][2];
planes[NEAR].distance = vp[3][3] + vp[3][2];
normalizePlane(planes[NEAR]);
// Far plane: row4 - row3
planes[FAR].normal.x = vp[0][3] - vp[0][2];
planes[FAR].normal.y = vp[1][3] - vp[1][2];
planes[FAR].normal.z = vp[2][3] - vp[2][2];
planes[FAR].distance = vp[3][3] - vp[3][2];
normalizePlane(planes[FAR]);
}
void Frustum::normalizePlane(Plane& plane) {
float length = glm::length(plane.normal);
if (length > 0.0001f) {
plane.normal /= length;
plane.distance /= length;
}
}
bool Frustum::containsPoint(const glm::vec3& point) const {
// Point must be in front of all planes
for (const auto& plane : planes) {
if (plane.distanceToPoint(point) < 0.0f) {
return false;
}
}
return true;
}
bool Frustum::intersectsSphere(const glm::vec3& center, float radius) const {
// Sphere is visible if distance from center to any plane is >= -radius
for (const auto& plane : planes) {
float distance = plane.distanceToPoint(center);
if (distance < -radius) {
// Sphere is completely behind this plane
return false;
}
}
return true;
}
bool Frustum::intersectsAABB(const glm::vec3& min, const glm::vec3& max) const {
// Test all 8 corners of the AABB
// If all corners are behind any plane, AABB is outside
// Otherwise, AABB is at least partially visible
for (const auto& plane : planes) {
// Find the positive vertex (corner furthest in plane normal direction)
glm::vec3 positiveVertex;
positiveVertex.x = (plane.normal.x >= 0.0f) ? max.x : min.x;
positiveVertex.y = (plane.normal.y >= 0.0f) ? max.y : min.y;
positiveVertex.z = (plane.normal.z >= 0.0f) ? max.z : min.z;
// If positive vertex is behind plane, entire box is behind
if (plane.distanceToPoint(positiveVertex) < 0.0f) {
return false;
}
}
return true;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,288 @@
#include "rendering/lens_flare.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
LensFlare::LensFlare() {
}
LensFlare::~LensFlare() {
cleanup();
}
bool LensFlare::initialize() {
LOG_INFO("Initializing lens flare system");
// Generate flare elements
generateFlareElements();
// Create VAO and VBO for quad rendering
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Position (x, y) and UV (u, v) for a quad
float quadVertices[] = {
// Pos UV
-0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 1.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
// Position attribute
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// UV attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
// Create shader
shader = std::make_unique<Shader>();
// Lens flare vertex shader (2D screen-space rendering)
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec2 uPosition; // Screen-space position (-1 to 1)
uniform float uSize; // Size in screen space
uniform float uAspectRatio;
out vec2 TexCoord;
void main() {
// Scale by size and aspect ratio
vec2 scaledPos = aPos * uSize;
scaledPos.x /= uAspectRatio;
// Translate to position
vec2 finalPos = scaledPos + uPosition;
gl_Position = vec4(finalPos, 0.0, 1.0);
TexCoord = aUV;
}
)";
// Lens flare fragment shader (circular gradient)
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
uniform vec3 uColor;
uniform float uBrightness;
out vec4 FragColor;
void main() {
// Distance from center
vec2 center = vec2(0.5);
float dist = distance(TexCoord, center);
// Circular gradient with soft edges
float alpha = smoothstep(0.5, 0.0, dist);
// Add some variation - brighter in center
float centerGlow = smoothstep(0.5, 0.0, dist * 2.0);
alpha = max(alpha * 0.3, centerGlow);
// Apply brightness
alpha *= uBrightness;
if (alpha < 0.01) {
discard;
}
FragColor = vec4(uColor, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create lens flare shader");
return false;
}
LOG_INFO("Lens flare system initialized: ", flareElements.size(), " elements");
return true;
}
void LensFlare::generateFlareElements() {
flareElements.clear();
// Main sun glow (at sun position)
flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f});
// Flare ghosts along sun-to-center axis
// These appear at various positions between sun and opposite side
// Bright white ghost near sun
flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f});
// Blue-tinted ghost
flareElements.push_back({0.4f, 0.15f, glm::vec3(0.3f, 0.5f, 1.0f), 0.4f});
// Small bright spot
flareElements.push_back({0.6f, 0.05f, glm::vec3(1.0f, 0.8f, 0.6f), 0.6f});
// Green-tinted ghost (chromatic aberration)
flareElements.push_back({0.8f, 0.12f, glm::vec3(0.4f, 1.0f, 0.5f), 0.3f});
// Large halo on opposite side
flareElements.push_back({-0.5f, 0.25f, glm::vec3(1.0f, 0.7f, 0.4f), 0.2f});
// Purple ghost far from sun
flareElements.push_back({-0.8f, 0.1f, glm::vec3(0.8f, 0.4f, 1.0f), 0.25f});
// Small red ghost
flareElements.push_back({-1.2f, 0.06f, glm::vec3(1.0f, 0.3f, 0.3f), 0.3f});
}
glm::vec2 LensFlare::worldToScreen(const Camera& camera, const glm::vec3& worldPos) const {
// Transform to clip space
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
glm::mat4 viewProj = projection * view;
glm::vec4 clipPos = viewProj * glm::vec4(worldPos, 1.0f);
// Perspective divide
if (clipPos.w > 0.0f) {
glm::vec2 ndc = glm::vec2(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
return ndc;
}
// Behind camera
return glm::vec2(10.0f, 10.0f); // Off-screen
}
float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& sunPosition) const {
// Get sun position in screen space
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
// Check if sun is behind camera
glm::vec3 camPos = camera.getPosition();
glm::vec3 camForward = camera.getForward();
glm::vec3 toSun = glm::normalize(sunPosition - camPos);
float dotProduct = glm::dot(camForward, toSun);
if (dotProduct < 0.0f) {
return 0.0f; // Sun is behind camera
}
// Check if sun is outside screen bounds (with some margin)
if (std::abs(sunScreen.x) > 1.5f || std::abs(sunScreen.y) > 1.5f) {
return 0.0f;
}
// Fade based on angle (stronger when looking directly at sun)
float angleFactor = glm::smoothstep(0.3f, 1.0f, dotProduct);
// Fade at screen edges
float edgeFade = 1.0f;
if (std::abs(sunScreen.x) > 0.8f) {
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.x));
}
if (std::abs(sunScreen.y) > 0.8f) {
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.y));
}
return angleFactor * edgeFade;
}
void LensFlare::render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) {
if (!enabled || !shader) {
return;
}
// Only render lens flare during daytime (when sun is visible)
if (timeOfDay < 5.0f || timeOfDay > 19.0f) {
return;
}
// Calculate sun visibility
float visibility = calculateSunVisibility(camera, sunPosition);
if (visibility < 0.01f) {
return;
}
// Get sun screen position
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
glm::vec2 screenCenter(0.0f, 0.0f);
// Vector from sun to screen center
glm::vec2 sunToCenter = screenCenter - sunScreen;
// Enable additive blending for flare effect
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
// Disable depth test (render on top)
glDisable(GL_DEPTH_TEST);
shader->use();
// Set aspect ratio
float aspectRatio = camera.getAspectRatio();
shader->setUniform("uAspectRatio", aspectRatio);
glBindVertexArray(vao);
// Render each flare element
for (const auto& element : flareElements) {
// Calculate position along sun-to-center axis
glm::vec2 position = sunScreen + sunToCenter * element.position;
// Set uniforms
shader->setUniform("uPosition", position);
shader->setUniform("uSize", element.size);
shader->setUniform("uColor", element.color);
// Apply visibility and intensity
float brightness = element.brightness * visibility * intensityMultiplier;
shader->setUniform("uBrightness", brightness);
// Render quad
glDrawArrays(GL_TRIANGLES, 0, VERTICES_PER_QUAD);
}
glBindVertexArray(0);
// Restore state
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Restore standard blending
}
void LensFlare::setIntensity(float intensity) {
this->intensityMultiplier = glm::clamp(intensity, 0.0f, 2.0f);
}
void LensFlare::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
} // namespace rendering
} // namespace wowee

414
src/rendering/lightning.cpp Normal file
View file

@ -0,0 +1,414 @@
#include "rendering/lightning.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
namespace {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
float randomRange(float min, float max) {
return min + dist(gen) * (max - min);
}
}
Lightning::Lightning() {
flash.active = false;
flash.intensity = 0.0f;
flash.lifetime = 0.0f;
flash.maxLifetime = FLASH_LIFETIME;
bolts.resize(MAX_BOLTS);
for (auto& bolt : bolts) {
bolt.active = false;
bolt.lifetime = 0.0f;
bolt.maxLifetime = BOLT_LIFETIME;
bolt.brightness = 1.0f;
}
// Random initial strike time
nextStrikeTime = randomRange(MIN_STRIKE_INTERVAL, MAX_STRIKE_INTERVAL);
}
Lightning::~Lightning() {
shutdown();
}
bool Lightning::initialize() {
core::Logger::getInstance().info("Initializing lightning system...");
// Create bolt shader
const char* boltVertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uViewProjection;
uniform float uBrightness;
out float vBrightness;
void main() {
gl_Position = uViewProjection * vec4(aPos, 1.0);
vBrightness = uBrightness;
}
)";
const char* boltFragmentSrc = R"(
#version 330 core
in float vBrightness;
out vec4 FragColor;
void main() {
// Electric blue-white color
vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5);
FragColor = vec4(color, vBrightness);
}
)";
boltShader = std::make_unique<Shader>();
if (!boltShader->loadFromSource(boltVertexSrc, boltFragmentSrc)) {
core::Logger::getInstance().error("Failed to create bolt shader");
return false;
}
// Create flash shader (fullscreen quad)
const char* flashVertexSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
const char* flashFragmentSrc = R"(
#version 330 core
uniform float uIntensity;
out vec4 FragColor;
void main() {
// Bright white flash with fade
vec3 color = vec3(1.0);
FragColor = vec4(color, uIntensity * 0.6);
}
)";
flashShader = std::make_unique<Shader>();
if (!flashShader->loadFromSource(flashVertexSrc, flashFragmentSrc)) {
core::Logger::getInstance().error("Failed to create flash shader");
return false;
}
// Create bolt VAO/VBO
glGenVertexArrays(1, &boltVAO);
glGenBuffers(1, &boltVBO);
glBindVertexArray(boltVAO);
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
// Reserve space for segments
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * MAX_SEGMENTS * 2, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
// Create flash quad VAO/VBO
glGenVertexArrays(1, &flashVAO);
glGenBuffers(1, &flashVBO);
float flashQuad[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
};
glBindVertexArray(flashVAO);
glBindBuffer(GL_ARRAY_BUFFER, flashVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(flashQuad), flashQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindVertexArray(0);
core::Logger::getInstance().info("Lightning system initialized");
return true;
}
void Lightning::shutdown() {
if (boltVAO) {
glDeleteVertexArrays(1, &boltVAO);
glDeleteBuffers(1, &boltVBO);
boltVAO = 0;
boltVBO = 0;
}
if (flashVAO) {
glDeleteVertexArrays(1, &flashVAO);
glDeleteBuffers(1, &flashVBO);
flashVAO = 0;
flashVBO = 0;
}
boltShader.reset();
flashShader.reset();
}
void Lightning::update(float deltaTime, const Camera& camera) {
if (!enabled) {
return;
}
// Update strike timer
strikeTimer += deltaTime;
// Spawn random strikes based on intensity
if (strikeTimer >= nextStrikeTime) {
spawnRandomStrike(camera.getPosition());
strikeTimer = 0.0f;
// Calculate next strike time (higher intensity = more frequent)
float intervalRange = MAX_STRIKE_INTERVAL - MIN_STRIKE_INTERVAL;
float adjustedInterval = MIN_STRIKE_INTERVAL + intervalRange * (1.0f - intensity);
nextStrikeTime = randomRange(adjustedInterval * 0.8f, adjustedInterval * 1.2f);
}
updateBolts(deltaTime);
updateFlash(deltaTime);
}
void Lightning::updateBolts(float deltaTime) {
for (auto& bolt : bolts) {
if (!bolt.active) {
continue;
}
bolt.lifetime += deltaTime;
if (bolt.lifetime >= bolt.maxLifetime) {
bolt.active = false;
continue;
}
// Fade out
float t = bolt.lifetime / bolt.maxLifetime;
bolt.brightness = 1.0f - t;
}
}
void Lightning::updateFlash(float deltaTime) {
if (!flash.active) {
return;
}
flash.lifetime += deltaTime;
if (flash.lifetime >= flash.maxLifetime) {
flash.active = false;
flash.intensity = 0.0f;
return;
}
// Quick fade
float t = flash.lifetime / flash.maxLifetime;
flash.intensity = 1.0f - (t * t); // Quadratic fade
}
void Lightning::spawnRandomStrike(const glm::vec3& cameraPos) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return; // All bolts active
}
// Random position around camera
float angle = randomRange(0.0f, 2.0f * 3.14159f);
float distance = randomRange(50.0f, STRIKE_DISTANCE);
glm::vec3 strikePos;
strikePos.x = cameraPos.x + std::cos(angle) * distance;
strikePos.z = cameraPos.z + std::sin(angle) * distance;
strikePos.y = cameraPos.y + randomRange(80.0f, 150.0f); // High in sky
triggerStrike(strikePos);
}
void Lightning::triggerStrike(const glm::vec3& position) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return;
}
// Setup bolt
bolt->active = true;
bolt->lifetime = 0.0f;
bolt->brightness = 1.0f;
bolt->startPos = position;
bolt->endPos = position;
bolt->endPos.y = position.y - randomRange(100.0f, 200.0f); // Strike downward
// Generate segments
bolt->segments.clear();
bolt->branches.clear();
generateLightningBolt(*bolt);
// Trigger screen flash
flash.active = true;
flash.lifetime = 0.0f;
flash.intensity = 1.0f;
}
void Lightning::generateLightningBolt(LightningBolt& bolt) {
generateBoltSegments(bolt.startPos, bolt.endPos, bolt.segments, 0);
}
void Lightning::generateBoltSegments(const glm::vec3& start, const glm::vec3& end,
std::vector<glm::vec3>& segments, int depth) {
if (depth > 4) { // Max recursion depth
return;
}
int numSegments = 8 + static_cast<int>(randomRange(0.0f, 8.0f));
glm::vec3 direction = end - start;
float length = glm::length(direction);
direction = glm::normalize(direction);
glm::vec3 current = start;
segments.push_back(current);
for (int i = 1; i < numSegments; i++) {
float t = static_cast<float>(i) / static_cast<float>(numSegments);
glm::vec3 target = start + direction * (length * t);
// Add random offset perpendicular to direction
float offsetAmount = (1.0f - t) * 8.0f; // More offset at start
glm::vec3 perpendicular1 = glm::normalize(glm::cross(direction, glm::vec3(0.0f, 1.0f, 0.0f)));
glm::vec3 perpendicular2 = glm::normalize(glm::cross(direction, perpendicular1));
glm::vec3 offset = perpendicular1 * randomRange(-offsetAmount, offsetAmount) +
perpendicular2 * randomRange(-offsetAmount, offsetAmount);
current = target + offset;
segments.push_back(current);
// Random branches
if (dist(gen) < BRANCH_PROBABILITY && depth < 3) {
glm::vec3 branchEnd = current;
branchEnd += glm::vec3(randomRange(-20.0f, 20.0f),
randomRange(-30.0f, -10.0f),
randomRange(-20.0f, 20.0f));
generateBoltSegments(current, branchEnd, segments, depth + 1);
}
}
segments.push_back(end);
}
void Lightning::render([[maybe_unused]] const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
if (!enabled) {
return;
}
glm::mat4 viewProj = projection * view;
renderBolts(viewProj);
renderFlash();
}
void Lightning::renderBolts(const glm::mat4& viewProj) {
// Enable additive blending for electric glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glDisable(GL_DEPTH_TEST); // Always visible
boltShader->use();
boltShader->setUniform("uViewProjection", viewProj);
glBindVertexArray(boltVAO);
glLineWidth(3.0f);
for (const auto& bolt : bolts) {
if (!bolt.active || bolt.segments.empty()) {
continue;
}
boltShader->setUniform("uBrightness", bolt.brightness);
// Upload segments
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0,
bolt.segments.size() * sizeof(glm::vec3),
bolt.segments.data());
// Draw as line strip
glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(bolt.segments.size()));
}
glLineWidth(1.0f);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
void Lightning::renderFlash() {
if (!flash.active || flash.intensity <= 0.01f) {
return;
}
// Fullscreen flash overlay
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
flashShader->use();
flashShader->setUniform("uIntensity", flash.intensity);
glBindVertexArray(flashVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
}
void Lightning::setEnabled(bool enabled) {
this->enabled = enabled;
if (!enabled) {
// Clear active effects
for (auto& bolt : bolts) {
bolt.active = false;
}
flash.active = false;
}
}
void Lightning::setIntensity(float intensity) {
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,466 @@
#include "rendering/m2_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
namespace wowee {
namespace rendering {
void M2Instance::updateModelMatrix() {
modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, position);
// Rotation in radians
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
}
M2Renderer::M2Renderer() {
}
M2Renderer::~M2Renderer() {
shutdown();
}
bool M2Renderer::initialize(pipeline::AssetManager* assets) {
assetManager = assets;
LOG_INFO("Initializing M2 renderer...");
// Create M2 shader
const char* vertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal;
TexCoord = aTexCoord;
gl_Position = uProjection * uView * worldPos;
}
)";
const char* fragmentSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
uniform vec3 uLightDir;
uniform vec3 uAmbientColor;
uniform sampler2D uTexture;
uniform bool uHasTexture;
uniform bool uAlphaTest;
out vec4 FragColor;
void main() {
vec4 texColor;
if (uHasTexture) {
texColor = texture(uTexture, TexCoord);
} else {
texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish
}
// Alpha test for leaves, fences, etc.
if (uAlphaTest && texColor.a < 0.5) {
discard;
}
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(uLightDir);
// Two-sided lighting for foliage
float diff = max(abs(dot(normal, lightDir)), 0.3);
vec3 ambient = uAmbientColor * texColor.rgb;
vec3 diffuse = diff * texColor.rgb;
vec3 result = ambient + diffuse;
FragColor = vec4(result, texColor.a);
}
)";
shader = std::make_unique<Shader>();
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
LOG_ERROR("Failed to create M2 shader");
return false;
}
// Create white fallback texture
uint8_t white[] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
LOG_INFO("M2 renderer initialized");
return true;
}
void M2Renderer::shutdown() {
LOG_INFO("Shutting down M2 renderer...");
// Delete GPU resources
for (auto& [id, model] : models) {
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
}
models.clear();
instances.clear();
// Delete cached textures
for (auto& [path, texId] : textureCache) {
if (texId != 0 && texId != whiteTexture) {
glDeleteTextures(1, &texId);
}
}
textureCache.clear();
if (whiteTexture != 0) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
shader.reset();
}
bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
if (models.find(modelId) != models.end()) {
// Already loaded
return true;
}
if (model.vertices.empty() || model.indices.empty()) {
LOG_WARNING("M2 model has no geometry: ", model.name);
return false;
}
M2ModelGPU gpuModel;
gpuModel.name = model.name;
gpuModel.boundMin = model.boundMin;
gpuModel.boundMax = model.boundMax;
gpuModel.boundRadius = model.boundRadius;
gpuModel.indexCount = static_cast<uint32_t>(model.indices.size());
gpuModel.vertexCount = static_cast<uint32_t>(model.vertices.size());
// Create VAO
glGenVertexArrays(1, &gpuModel.vao);
glBindVertexArray(gpuModel.vao);
// Create VBO with interleaved vertex data
// Format: position (3), normal (3), texcoord (2)
std::vector<float> vertexData;
vertexData.reserve(model.vertices.size() * 8);
for (const auto& v : model.vertices) {
vertexData.push_back(v.position.x);
vertexData.push_back(v.position.y);
vertexData.push_back(v.position.z);
vertexData.push_back(v.normal.x);
vertexData.push_back(v.normal.y);
vertexData.push_back(v.normal.z);
vertexData.push_back(v.texCoords[0].x);
vertexData.push_back(v.texCoords[0].y);
}
glGenBuffers(1, &gpuModel.vbo);
glBindBuffer(GL_ARRAY_BUFFER, gpuModel.vbo);
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float),
vertexData.data(), GL_STATIC_DRAW);
// Create EBO
glGenBuffers(1, &gpuModel.ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuModel.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, model.indices.size() * sizeof(uint16_t),
model.indices.data(), GL_STATIC_DRAW);
// Set up vertex attributes
const size_t stride = 8 * sizeof(float);
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
glBindVertexArray(0);
// Load ALL textures from the model into a local vector
std::vector<GLuint> allTextures;
if (assetManager) {
for (const auto& tex : model.textures) {
if (!tex.filename.empty()) {
allTextures.push_back(loadTexture(tex.filename));
} else {
allTextures.push_back(whiteTexture);
}
}
}
// Build per-batch GPU entries
if (!model.batches.empty()) {
for (const auto& batch : model.batches) {
M2ModelGPU::BatchGPU bgpu;
bgpu.indexStart = batch.indexStart;
bgpu.indexCount = batch.indexCount;
// Resolve texture: batch.textureIndex → textureLookup → allTextures
GLuint tex = whiteTexture;
if (batch.textureIndex < model.textureLookup.size()) {
uint16_t texIdx = model.textureLookup[batch.textureIndex];
if (texIdx < allTextures.size()) {
tex = allTextures[texIdx];
}
} else if (!allTextures.empty()) {
tex = allTextures[0];
}
bgpu.texture = tex;
bgpu.hasAlpha = (tex != 0 && tex != whiteTexture);
gpuModel.batches.push_back(bgpu);
}
} else {
// Fallback: single batch covering all indices with first texture
M2ModelGPU::BatchGPU bgpu;
bgpu.indexStart = 0;
bgpu.indexCount = gpuModel.indexCount;
bgpu.texture = allTextures.empty() ? whiteTexture : allTextures[0];
bgpu.hasAlpha = (bgpu.texture != 0 && bgpu.texture != whiteTexture);
gpuModel.batches.push_back(bgpu);
}
models[modelId] = std::move(gpuModel);
LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ",
models[modelId].indexCount / 3, " triangles, ", models[modelId].batches.size(), " batches)");
return true;
}
uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation, float scale) {
if (models.find(modelId) == models.end()) {
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
return 0;
}
M2Instance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position;
instance.rotation = rotation;
instance.scale = scale;
instance.updateModelMatrix();
instances.push_back(instance);
return instance.id;
}
uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& modelMatrix,
const glm::vec3& position) {
if (models.find(modelId) == models.end()) {
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
return 0;
}
M2Instance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position; // Used for frustum culling
instance.rotation = glm::vec3(0.0f);
instance.scale = 1.0f;
instance.modelMatrix = modelMatrix;
instances.push_back(instance);
return instance.id;
}
void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
(void)camera; // unused for now
if (instances.empty() || !shader) {
return;
}
// Debug: log once when we start rendering
static bool loggedOnce = false;
if (!loggedOnce) {
loggedOnce = true;
LOG_INFO("M2 render: ", instances.size(), " instances, ", models.size(), " models");
}
// Set up GL state for M2 rendering
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDisable(GL_BLEND); // No blend leaking from prior renderers
glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided
// Make models render with a bright color for debugging
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Wireframe mode
// Build frustum for culling
Frustum frustum;
frustum.extractFromMatrix(projection * view);
shader->use();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
shader->setUniform("uLightDir", lightDir);
shader->setUniform("uAmbientColor", ambientColor);
lastDrawCallCount = 0;
for (const auto& instance : instances) {
auto it = models.find(instance.modelId);
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
if (!model.isValid()) continue;
// Frustum cull: test bounding sphere in world space
float worldRadius = model.boundRadius * instance.scale;
if (worldRadius > 0.0f && !frustum.intersectsSphere(instance.position, worldRadius)) {
continue;
}
shader->setUniform("uModel", instance.modelMatrix);
glBindVertexArray(model.vao);
for (const auto& batch : model.batches) {
if (batch.indexCount == 0) continue;
bool hasTexture = (batch.texture != 0);
shader->setUniform("uHasTexture", hasTexture);
shader->setUniform("uAlphaTest", batch.hasAlpha);
if (hasTexture) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, batch.texture);
shader->setUniform("uTexture", 0);
}
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
(void*)(batch.indexStart * sizeof(uint16_t)));
lastDrawCallCount++;
}
// Check for GL errors (only first draw)
static bool checkedOnce = false;
if (!checkedOnce) {
checkedOnce = true;
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
LOG_ERROR("GL error after M2 draw: ", err);
} else {
LOG_INFO("M2 draw successful: ", model.indexCount, " indices");
}
}
glBindVertexArray(0);
}
// Restore cull face state
glEnable(GL_CULL_FACE);
}
void M2Renderer::removeInstance(uint32_t instanceId) {
for (auto it = instances.begin(); it != instances.end(); ++it) {
if (it->id == instanceId) {
instances.erase(it);
return;
}
}
}
void M2Renderer::clear() {
for (auto& [id, model] : models) {
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
}
models.clear();
instances.clear();
}
GLuint M2Renderer::loadTexture(const std::string& path) {
// Check cache
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
LOG_WARNING("M2: Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
textureCache[path] = textureID;
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
uint32_t M2Renderer::getTotalTriangleCount() const {
uint32_t total = 0;
for (const auto& instance : instances) {
auto it = models.find(instance.modelId);
if (it != models.end()) {
total += it->second.indexCount / 3;
}
}
return total;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,8 @@
#include "rendering/material.hpp"
// All implementations are inline in header
namespace wowee {
namespace rendering {
// Empty file - all methods are inline
} // namespace rendering
} // namespace wowee

56
src/rendering/mesh.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "rendering/mesh.hpp"
namespace wowee {
namespace rendering {
Mesh::~Mesh() {
destroy();
}
void Mesh::create(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices) {
indexCount = indices.size();
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glBindVertexArray(0);
}
void Mesh::destroy() {
if (VAO) glDeleteVertexArrays(1, &VAO);
if (VBO) glDeleteBuffers(1, &VBO);
if (EBO) glDeleteBuffers(1, &EBO);
VAO = VBO = EBO = 0;
}
void Mesh::draw() const {
if (VAO && indexCount > 0) {
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
}
} // namespace rendering
} // namespace wowee

213
src/rendering/minimap.cpp Normal file
View file

@ -0,0 +1,213 @@
#include "rendering/minimap.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/terrain_renderer.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
namespace wowee {
namespace rendering {
Minimap::Minimap() = default;
Minimap::~Minimap() {
shutdown();
}
bool Minimap::initialize(int size) {
mapSize = size;
// Create FBO
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// Color texture
glGenTextures(1, &fboTexture);
glBindTexture(GL_TEXTURE_2D, fboTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mapSize, mapSize, 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, fboTexture, 0);
// Depth renderbuffer
glGenRenderbuffers(1, &fboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, fboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mapSize, mapSize);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fboDepth);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Minimap FBO incomplete");
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Screen quad (NDC fullscreen, we'll position via uniforms)
float quadVerts[] = {
// pos (x,y), uv (u,v)
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
};
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);
// Quad shader with circular mask and border
const char* vertSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec4 uRect; // x, y, w, h in NDC
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* fragSrc = R"(
#version 330 core
in vec2 TexCoord;
uniform sampler2D uMapTexture;
out vec4 FragColor;
void main() {
vec2 center = TexCoord - vec2(0.5);
float dist = length(center);
// Circular mask
if (dist > 0.5) discard;
// Gold border ring
float borderWidth = 0.02;
if (dist > 0.5 - borderWidth) {
FragColor = vec4(0.8, 0.65, 0.2, 1.0);
return;
}
vec4 texColor = texture(uMapTexture, TexCoord);
// Player dot at center
if (dist < 0.02) {
FragColor = vec4(1.0, 0.3, 0.3, 1.0);
return;
}
FragColor = texColor;
}
)";
quadShader = std::make_unique<Shader>();
if (!quadShader->loadFromSource(vertSrc, fragSrc)) {
LOG_ERROR("Failed to create minimap shader");
return false;
}
LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, ")");
return true;
}
void Minimap::shutdown() {
if (fbo) { glDeleteFramebuffers(1, &fbo); fbo = 0; }
if (fboTexture) { glDeleteTextures(1, &fboTexture); fboTexture = 0; }
if (fboDepth) { glDeleteRenderbuffers(1, &fboDepth); fboDepth = 0; }
if (quadVAO) { glDeleteVertexArrays(1, &quadVAO); quadVAO = 0; }
if (quadVBO) { glDeleteBuffers(1, &quadVBO); quadVBO = 0; }
quadShader.reset();
}
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
if (!enabled || !terrainRenderer || !fbo) return;
// 1. Render terrain from top-down into FBO
renderTerrainToFBO(playerCamera);
// 2. Draw the minimap quad on screen
renderQuad(screenWidth, screenHeight);
}
void Minimap::renderTerrainToFBO(const Camera& playerCamera) {
// Save current viewport
GLint prevViewport[4];
glGetIntegerv(GL_VIEWPORT, prevViewport);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, mapSize, mapSize);
glClearColor(0.05f, 0.1f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Create a top-down camera at the player's XY position
Camera topDownCamera;
glm::vec3 playerPos = playerCamera.getPosition();
topDownCamera.setPosition(glm::vec3(playerPos.x, playerPos.y, playerPos.z + 5000.0f));
topDownCamera.setRotation(0.0f, -89.9f); // Look straight down
topDownCamera.setAspectRatio(1.0f);
topDownCamera.setFov(1.0f); // Will be overridden by ortho below
// We need orthographic projection, but Camera only supports perspective.
// Use the terrain renderer's render with a custom view/projection.
// For now, render with the top-down camera (perspective, narrow FOV approximates ortho)
// The narrow FOV + high altitude gives a near-orthographic result.
// Calculate FOV that covers viewRadius at the altitude
float altitude = 5000.0f;
float fovDeg = glm::degrees(2.0f * std::atan(viewRadius / altitude));
topDownCamera.setFov(fovDeg);
terrainRenderer->render(topDownCamera);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Restore viewport
glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
}
void Minimap::renderQuad(int screenWidth, int screenHeight) {
glDisable(GL_DEPTH_TEST);
quadShader->use();
// Position minimap in top-right corner with margin
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;
// uRect: x, y, w, h in 0..1 screen space
quadShader->setUniform("uRect", glm::vec4(x, y, pixelW, pixelH));
quadShader->setUniform("uMapTexture", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, fboTexture);
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,416 @@
#include "rendering/performance_hud.hpp"
#include "rendering/renderer.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/skybox.hpp"
#include "rendering/celestial.hpp"
#include "rendering/starfield.hpp"
#include "rendering/clouds.hpp"
#include "rendering/lens_flare.hpp"
#include "rendering/weather.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp"
#include <imgui.h>
#include <algorithm>
#include <sstream>
#include <iomanip>
namespace wowee {
namespace rendering {
PerformanceHUD::PerformanceHUD() {
}
PerformanceHUD::~PerformanceHUD() {
}
void PerformanceHUD::update(float deltaTime) {
if (!enabled) {
return;
}
// Store frame time
frameTime = deltaTime;
frameTimeHistory.push_back(deltaTime);
// Keep history size limited
while (frameTimeHistory.size() > MAX_FRAME_HISTORY) {
frameTimeHistory.pop_front();
}
// Update stats periodically
updateTimer += deltaTime;
if (updateTimer >= UPDATE_INTERVAL) {
updateTimer = 0.0f;
calculateFPS();
}
}
void PerformanceHUD::calculateFPS() {
if (frameTimeHistory.empty()) {
return;
}
// Current FPS (from last frame time)
currentFPS = frameTime > 0.0001f ? 1.0f / frameTime : 0.0f;
// Average FPS
float sum = 0.0f;
for (float ft : frameTimeHistory) {
sum += ft;
}
float avgFrameTime = sum / frameTimeHistory.size();
averageFPS = avgFrameTime > 0.0001f ? 1.0f / avgFrameTime : 0.0f;
// Min/Max FPS (from last 2 seconds)
minFPS = 10000.0f;
maxFPS = 0.0f;
for (float ft : frameTimeHistory) {
if (ft > 0.0001f) {
float fps = 1.0f / ft;
minFPS = std::min(minFPS, fps);
maxFPS = std::max(maxFPS, fps);
}
}
}
void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
if (!enabled || !renderer) {
return;
}
// Set window position based on setting
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav;
const float PADDING = 10.0f;
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 work_pos = viewport->WorkPos;
ImVec2 work_size = viewport->WorkSize;
ImVec2 window_pos, window_pos_pivot;
switch (position) {
case Position::TOP_LEFT:
window_pos.x = work_pos.x + PADDING;
window_pos.y = work_pos.y + PADDING;
window_pos_pivot.x = 0.0f;
window_pos_pivot.y = 0.0f;
break;
case Position::TOP_RIGHT:
window_pos.x = work_pos.x + work_size.x - PADDING;
window_pos.y = work_pos.y + PADDING;
window_pos_pivot.x = 1.0f;
window_pos_pivot.y = 0.0f;
break;
case Position::BOTTOM_LEFT:
window_pos.x = work_pos.x + PADDING;
window_pos.y = work_pos.y + work_size.y - PADDING;
window_pos_pivot.x = 0.0f;
window_pos_pivot.y = 1.0f;
break;
case Position::BOTTOM_RIGHT:
window_pos.x = work_pos.x + work_size.x - PADDING;
window_pos.y = work_pos.y + work_size.y - PADDING;
window_pos_pivot.x = 1.0f;
window_pos_pivot.y = 1.0f;
break;
}
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot);
ImGui::SetNextWindowBgAlpha(0.7f); // Transparent background
if (!ImGui::Begin("Performance", nullptr, flags)) {
ImGui::End();
return;
}
// FPS section
if (showFPS) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "PERFORMANCE");
ImGui::Separator();
// Color-code FPS
ImVec4 fpsColor;
if (currentFPS >= 60.0f) {
fpsColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green
} else if (currentFPS >= 30.0f) {
fpsColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow
} else {
fpsColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
}
ImGui::Text("FPS: ");
ImGui::SameLine();
ImGui::TextColored(fpsColor, "%.1f", currentFPS);
ImGui::Text("Avg: %.1f", averageFPS);
ImGui::Text("Min: %.1f", minFPS);
ImGui::Text("Max: %.1f", maxFPS);
ImGui::Text("Frame: %.2f ms", frameTime * 1000.0f);
// Frame time graph
if (!frameTimeHistory.empty()) {
std::vector<float> frameTimesMs;
frameTimesMs.reserve(frameTimeHistory.size());
for (float ft : frameTimeHistory) {
frameTimesMs.push_back(ft * 1000.0f); // Convert to ms
}
ImGui::PlotLines("##frametime", frameTimesMs.data(), static_cast<int>(frameTimesMs.size()),
0, nullptr, 0.0f, 33.33f, ImVec2(200, 40));
}
ImGui::Spacing();
}
// Renderer stats
if (showRenderer) {
auto* terrainRenderer = renderer->getTerrainRenderer();
if (terrainRenderer) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "RENDERING");
ImGui::Separator();
int totalChunks = terrainRenderer->getChunkCount();
int rendered = terrainRenderer->getRenderedChunkCount();
int culled = terrainRenderer->getCulledChunkCount();
int triangles = terrainRenderer->getTriangleCount();
ImGui::Text("Chunks: %d", totalChunks);
ImGui::Text("Rendered: %d", rendered);
ImGui::Text("Culled: %d", culled);
if (totalChunks > 0) {
float visiblePercent = (rendered * 100.0f) / totalChunks;
ImGui::Text("Visible: %.1f%%", visiblePercent);
}
ImGui::Text("Triangles: %s",
triangles >= 1000000 ?
(std::to_string(triangles / 1000) + "K").c_str() :
std::to_string(triangles).c_str());
ImGui::Spacing();
}
}
// Terrain streaming info
if (showTerrain) {
auto* terrainManager = renderer->getTerrainManager();
if (terrainManager) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "TERRAIN");
ImGui::Separator();
ImGui::Text("Loaded tiles: %d", terrainManager->getLoadedTileCount());
auto currentTile = terrainManager->getCurrentTile();
ImGui::Text("Current tile: [%d,%d]", currentTile.x, currentTile.y);
ImGui::Spacing();
}
// Water info
auto* waterRenderer = renderer->getWaterRenderer();
if (waterRenderer) {
ImGui::TextColored(ImVec4(0.2f, 0.5f, 1.0f, 1.0f), "WATER");
ImGui::Separator();
ImGui::Text("Surfaces: %d", waterRenderer->getSurfaceCount());
ImGui::Text("Enabled: %s", waterRenderer->isEnabled() ? "YES" : "NO");
ImGui::Spacing();
}
}
// Skybox info
if (showTerrain) {
auto* skybox = renderer->getSkybox();
if (skybox) {
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "SKY");
ImGui::Separator();
float time = skybox->getTimeOfDay();
int hours = static_cast<int>(time);
int minutes = static_cast<int>((time - hours) * 60);
ImGui::Text("Time: %02d:%02d", hours, minutes);
ImGui::Text("Auto: %s", skybox->isTimeProgressionEnabled() ? "YES" : "NO");
// Celestial info
auto* celestial = renderer->getCelestial();
if (celestial) {
ImGui::Text("Sun/Moon: %s", celestial->isEnabled() ? "YES" : "NO");
// Moon phase info
float phase = celestial->getMoonPhase();
const char* phaseName = "Unknown";
if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New";
else if (phase < 0.1875f) phaseName = "Wax Cresc";
else if (phase < 0.3125f) phaseName = "1st Qtr";
else if (phase < 0.4375f) phaseName = "Wax Gibb";
else if (phase < 0.5625f) phaseName = "Full";
else if (phase < 0.6875f) phaseName = "Wan Gibb";
else if (phase < 0.8125f) phaseName = "Last Qtr";
else phaseName = "Wan Cresc";
ImGui::Text("Moon: %s (%.0f%%)", phaseName, phase * 100.0f);
ImGui::Text("Cycling: %s", celestial->isMoonPhaseCycling() ? "YES" : "NO");
}
// Star field info
auto* starField = renderer->getStarField();
if (starField) {
ImGui::Text("Stars: %d (%s)", starField->getStarCount(),
starField->isEnabled() ? "ON" : "OFF");
}
// Cloud info
auto* clouds = renderer->getClouds();
if (clouds) {
ImGui::Text("Clouds: %s (%.0f%%)",
clouds->isEnabled() ? "ON" : "OFF",
clouds->getDensity() * 100.0f);
}
// Lens flare info
auto* lensFlare = renderer->getLensFlare();
if (lensFlare) {
ImGui::Text("Lens Flare: %s (%.0f%%)",
lensFlare->isEnabled() ? "ON" : "OFF",
lensFlare->getIntensity() * 100.0f);
}
ImGui::Spacing();
}
}
// Weather info
if (showRenderer) {
auto* weather = renderer->getWeather();
if (weather) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "WEATHER");
ImGui::Separator();
const char* typeName = "None";
using WeatherType = rendering::Weather::Type;
auto type = weather->getWeatherType();
if (type == WeatherType::RAIN) typeName = "Rain";
else if (type == WeatherType::SNOW) typeName = "Snow";
ImGui::Text("Type: %s", typeName);
if (weather->isEnabled()) {
ImGui::Text("Particles: %d", weather->getParticleCount());
ImGui::Text("Intensity: %.0f%%", weather->getIntensity() * 100.0f);
}
ImGui::Spacing();
}
}
// Fog info
if (showRenderer) {
auto* terrainRenderer = renderer->getTerrainRenderer();
if (terrainRenderer) {
ImGui::TextColored(ImVec4(0.7f, 0.8f, 0.9f, 1.0f), "FOG");
ImGui::Separator();
ImGui::Text("Distance fog: %s", terrainRenderer->isFogEnabled() ? "ON" : "OFF");
ImGui::Spacing();
}
}
// Character info
if (showRenderer) {
auto* charRenderer = renderer->getCharacterRenderer();
if (charRenderer) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.4f, 1.0f), "CHARACTERS");
ImGui::Separator();
ImGui::Text("Instances: %zu", charRenderer->getInstanceCount());
ImGui::Spacing();
}
}
// WMO building info
if (showRenderer) {
auto* wmoRenderer = renderer->getWMORenderer();
if (wmoRenderer) {
ImGui::TextColored(ImVec4(0.8f, 0.7f, 0.6f, 1.0f), "WMO BUILDINGS");
ImGui::Separator();
ImGui::Text("Models: %u", wmoRenderer->getModelCount());
ImGui::Text("Instances: %u", wmoRenderer->getInstanceCount());
ImGui::Text("Triangles: %u", wmoRenderer->getTotalTriangleCount());
ImGui::Text("Draw Calls: %u", wmoRenderer->getDrawCallCount());
ImGui::Spacing();
}
}
// Zone info
{
const std::string& zoneName = renderer->getCurrentZoneName();
if (!zoneName.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "ZONE");
ImGui::Separator();
ImGui::Text("%s", zoneName.c_str());
ImGui::Spacing();
}
}
// Camera info
if (showCamera && camera) {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "CAMERA");
ImGui::Separator();
glm::vec3 pos = camera->getPosition();
ImGui::Text("Pos: %.1f, %.1f, %.1f", pos.x, pos.y, pos.z);
glm::vec3 forward = camera->getForward();
ImGui::Text("Dir: %.2f, %.2f, %.2f", forward.x, forward.y, forward.z);
ImGui::Spacing();
}
// Controls help
if (showControls) {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS");
ImGui::Separator();
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle HUD");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F2: Wireframe");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F3: Single tile");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Culling");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F5: Stats");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F6: Multi-tile");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Streaming");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F8: Water");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F9: Time");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F10: Sun/Moon");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F11: Stars");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F12: Fog");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "+/-: Change time");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Clouds");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[/]: Density");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Lens Flare");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ",/.: Intensity");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: Moon Cycle");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ";/': Moon Phase");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "W: Weather");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "</>: Wx Intensity");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "K: Spawn Character");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "J: Remove Chars");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "O: Spawn Test WMO");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Shift+O: Real WMO");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Clear WMOs");
}
ImGui::End();
}
} // namespace rendering
} // namespace wowee

911
src/rendering/renderer.cpp Normal file
View file

@ -0,0 +1,911 @@
#include "rendering/renderer.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/scene.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/performance_hud.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/skybox.hpp"
#include "rendering/celestial.hpp"
#include "rendering/starfield.hpp"
#include "rendering/clouds.hpp"
#include "rendering/lens_flare.hpp"
#include "rendering/weather.hpp"
#include "rendering/swim_effects.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/minimap.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "core/window.hpp"
#include "core/logger.hpp"
#include "game/world.hpp"
#include "game/zone_manager.hpp"
#include "audio/music_manager.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cctype>
#include <unordered_map>
#include <unordered_set>
namespace wowee {
namespace rendering {
struct EmoteInfo {
uint32_t animId;
bool loop;
std::string text;
};
// AnimationData.dbc IDs for WotLK HumanMale emotes
// Reference: https://wowdev.wiki/M2/AnimationList
static const std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE = {
{"wave", {67, false, "waves."}},
{"bow", {66, false, "bows down graciously."}},
{"laugh", {70, false, "laughs."}},
{"point", {84, false, "points over there."}},
{"cheer", {68, false, "cheers!"}},
{"dance", {69, true, "begins to dance."}},
{"kneel", {75, false, "kneels down."}},
{"applaud", {80, false, "applauds."}},
{"shout", {81, false, "shouts."}},
{"chicken", {78, false, "clucks like a chicken."}},
{"cry", {77, false, "cries."}},
{"kiss", {76, false, "blows a kiss."}},
{"roar", {74, false, "roars with bestial vigor."}},
{"salute", {113, false, "salutes."}},
{"rude", {73, false, "makes a rude gesture."}},
{"flex", {82, false, "flexes muscles."}},
{"shy", {83, false, "acts shy."}},
{"beg", {79, false, "begs everyone around."}},
{"eat", {61, false, "begins to eat."}},
};
Renderer::Renderer() = default;
Renderer::~Renderer() = default;
bool Renderer::initialize(core::Window* win) {
window = win;
LOG_INFO("Initializing renderer");
// Create camera (in front of Stormwind gate, looking north)
camera = std::make_unique<Camera>();
camera->setPosition(glm::vec3(-8900.0f, -170.0f, 150.0f));
camera->setRotation(0.0f, -5.0f);
camera->setAspectRatio(window->getAspectRatio());
camera->setFov(60.0f);
// Create camera controller
cameraController = std::make_unique<CameraController>(camera.get());
cameraController->setMovementSpeed(100.0f); // Fast movement for terrain exploration
cameraController->setMouseSensitivity(0.15f);
// Create scene
scene = std::make_unique<Scene>();
// Create performance HUD
performanceHUD = std::make_unique<PerformanceHUD>();
performanceHUD->setPosition(PerformanceHUD::Position::TOP_LEFT);
// Create water renderer
waterRenderer = std::make_unique<WaterRenderer>();
if (!waterRenderer->initialize()) {
LOG_WARNING("Failed to initialize water renderer");
waterRenderer.reset();
}
// Create skybox
skybox = std::make_unique<Skybox>();
if (!skybox->initialize()) {
LOG_WARNING("Failed to initialize skybox");
skybox.reset();
} else {
skybox->setTimeOfDay(12.0f); // Start at noon
}
// Create celestial renderer (sun and moon)
celestial = std::make_unique<Celestial>();
if (!celestial->initialize()) {
LOG_WARNING("Failed to initialize celestial renderer");
celestial.reset();
}
// Create star field
starField = std::make_unique<StarField>();
if (!starField->initialize()) {
LOG_WARNING("Failed to initialize star field");
starField.reset();
}
// Create clouds
clouds = std::make_unique<Clouds>();
if (!clouds->initialize()) {
LOG_WARNING("Failed to initialize clouds");
clouds.reset();
} else {
clouds->setDensity(0.5f); // Medium cloud coverage
}
// Create lens flare
lensFlare = std::make_unique<LensFlare>();
if (!lensFlare->initialize()) {
LOG_WARNING("Failed to initialize lens flare");
lensFlare.reset();
}
// Create weather system
weather = std::make_unique<Weather>();
if (!weather->initialize()) {
LOG_WARNING("Failed to initialize weather");
weather.reset();
}
// Create swim effects
swimEffects = std::make_unique<SwimEffects>();
if (!swimEffects->initialize()) {
LOG_WARNING("Failed to initialize swim effects");
swimEffects.reset();
}
// Create character renderer
characterRenderer = std::make_unique<CharacterRenderer>();
if (!characterRenderer->initialize()) {
LOG_WARNING("Failed to initialize character renderer");
characterRenderer.reset();
}
// Create WMO renderer
wmoRenderer = std::make_unique<WMORenderer>();
if (!wmoRenderer->initialize()) {
LOG_WARNING("Failed to initialize WMO renderer");
wmoRenderer.reset();
}
// Create minimap
minimap = std::make_unique<Minimap>();
if (!minimap->initialize(200)) {
LOG_WARNING("Failed to initialize minimap");
minimap.reset();
}
// Create M2 renderer (for doodads)
m2Renderer = std::make_unique<M2Renderer>();
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
// Create zone manager
zoneManager = std::make_unique<game::ZoneManager>();
zoneManager->initialize();
// Create music manager (initialized later with asset manager)
musicManager = std::make_unique<audio::MusicManager>();
LOG_INFO("Renderer initialized");
return true;
}
void Renderer::shutdown() {
if (terrainManager) {
terrainManager->unloadAll();
terrainManager.reset();
}
if (terrainRenderer) {
terrainRenderer->shutdown();
terrainRenderer.reset();
}
if (waterRenderer) {
waterRenderer->shutdown();
waterRenderer.reset();
}
if (skybox) {
skybox->shutdown();
skybox.reset();
}
if (celestial) {
celestial->shutdown();
celestial.reset();
}
if (starField) {
starField->shutdown();
starField.reset();
}
if (clouds) {
clouds.reset();
}
if (lensFlare) {
lensFlare.reset();
}
if (weather) {
weather.reset();
}
if (swimEffects) {
swimEffects->shutdown();
swimEffects.reset();
}
if (characterRenderer) {
characterRenderer->shutdown();
characterRenderer.reset();
}
if (wmoRenderer) {
wmoRenderer->shutdown();
wmoRenderer.reset();
}
if (m2Renderer) {
m2Renderer->shutdown();
m2Renderer.reset();
}
if (musicManager) {
musicManager->shutdown();
musicManager.reset();
}
zoneManager.reset();
performanceHUD.reset();
scene.reset();
cameraController.reset();
camera.reset();
LOG_INFO("Renderer shutdown");
}
void Renderer::beginFrame() {
// Black background (skybox will render over it)
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::endFrame() {
// Nothing needed here for now
}
void Renderer::setCharacterFollow(uint32_t instanceId) {
characterInstanceId = instanceId;
if (cameraController && instanceId > 0) {
cameraController->setFollowTarget(&characterPosition);
}
}
void Renderer::updateCharacterAnimation() {
// WoW WotLK AnimationData.dbc IDs
constexpr uint32_t ANIM_STAND = 0;
constexpr uint32_t ANIM_WALK = 4;
constexpr uint32_t ANIM_RUN = 5;
constexpr uint32_t ANIM_JUMP_START = 37;
constexpr uint32_t ANIM_JUMP_MID = 38;
constexpr uint32_t ANIM_JUMP_END = 39;
constexpr uint32_t ANIM_SIT_DOWN = 97; // SitGround — transition to sitting
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
CharAnimState newState = charAnimState;
bool moving = cameraController->isMoving();
bool grounded = cameraController->isGrounded();
bool jumping = cameraController->isJumping();
bool sprinting = cameraController->isSprinting();
bool sitting = cameraController->isSitting();
bool swim = cameraController->isSwimming();
switch (charAnimState) {
case CharAnimState::IDLE:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (sitting && grounded) {
newState = CharAnimState::SIT_DOWN;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (moving && sprinting) {
newState = CharAnimState::RUN;
} else if (moving) {
newState = CharAnimState::WALK;
}
break;
case CharAnimState::WALK:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (!moving) {
newState = CharAnimState::IDLE;
} else if (sprinting) {
newState = CharAnimState::RUN;
}
break;
case CharAnimState::RUN:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (!moving) {
newState = CharAnimState::IDLE;
} else if (!sprinting) {
newState = CharAnimState::WALK;
}
break;
case CharAnimState::JUMP_START:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (grounded) {
newState = CharAnimState::JUMP_END;
} else {
newState = CharAnimState::JUMP_MID;
}
break;
case CharAnimState::JUMP_MID:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (grounded) {
newState = CharAnimState::JUMP_END;
}
break;
case CharAnimState::JUMP_END:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (moving && sprinting) {
newState = CharAnimState::RUN;
} else if (moving) {
newState = CharAnimState::WALK;
} else {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::SIT_DOWN:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (!sitting) {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::SITTING:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (!sitting) {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::EMOTE:
if (swim) {
cancelEmote();
newState = CharAnimState::SWIM_IDLE;
} else if (jumping || !grounded) {
cancelEmote();
newState = CharAnimState::JUMP_START;
} else if (moving) {
cancelEmote();
newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK;
} else if (sitting) {
cancelEmote();
newState = CharAnimState::SIT_DOWN;
}
break;
case CharAnimState::SWIM_IDLE:
if (!swim) {
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
} else if (moving) {
newState = CharAnimState::SWIM;
}
break;
case CharAnimState::SWIM:
if (!swim) {
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
} else if (!moving) {
newState = CharAnimState::SWIM_IDLE;
}
break;
}
if (newState != charAnimState) {
charAnimState = newState;
uint32_t animId = ANIM_STAND;
bool loop = true;
switch (charAnimState) {
case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break;
case CharAnimState::WALK: animId = ANIM_WALK; loop = true; break;
case CharAnimState::RUN: animId = ANIM_RUN; loop = true; break;
case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break;
case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break;
case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break;
case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break;
case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break;
case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break;
case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break;
case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break;
}
characterRenderer->playAnimation(characterInstanceId, animId, loop);
}
}
void Renderer::playEmote(const std::string& emoteName) {
auto it = EMOTE_TABLE.find(emoteName);
if (it == EMOTE_TABLE.end()) return;
const auto& info = it->second;
emoteActive = true;
emoteAnimId = info.animId;
emoteLoop = info.loop;
charAnimState = CharAnimState::EMOTE;
if (characterRenderer && characterInstanceId > 0) {
characterRenderer->playAnimation(characterInstanceId, emoteAnimId, emoteLoop);
}
}
void Renderer::cancelEmote() {
emoteActive = false;
emoteAnimId = 0;
emoteLoop = false;
}
std::string Renderer::getEmoteText(const std::string& emoteName) {
auto it = EMOTE_TABLE.find(emoteName);
if (it != EMOTE_TABLE.end()) {
return it->second.text;
}
return "";
}
void Renderer::setTargetPosition(const glm::vec3* pos) {
targetPosition = pos;
}
bool Renderer::isMoving() const {
return cameraController && cameraController->isMoving();
}
void Renderer::update(float deltaTime) {
if (cameraController) {
cameraController->update(deltaTime);
}
// Sync character model position/rotation and animation with follow target
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
// Only rotate character to face camera direction when right-click is held
// Left-click orbits camera without turning the character
if (cameraController->isRightMouseHeld() || cameraController->isMoving()) {
characterYaw = cameraController->getYaw();
} else if (targetPosition && !emoteActive && !cameraController->isMoving()) {
// Face target when idle
glm::vec3 toTarget = *targetPosition - characterPosition;
if (glm::length(glm::vec2(toTarget.x, toTarget.y)) > 0.1f) {
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
// Smooth rotation toward target
float diff = targetYaw - characterYaw;
while (diff > 180.0f) diff -= 360.0f;
while (diff < -180.0f) diff += 360.0f;
float rotSpeed = 360.0f * deltaTime;
if (std::abs(diff) < rotSpeed) {
characterYaw = targetYaw;
} else {
characterYaw += (diff > 0 ? rotSpeed : -rotSpeed);
}
}
}
float yawRad = glm::radians(characterYaw);
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
// Update animation based on movement state
updateCharacterAnimation();
}
// Update terrain streaming
if (terrainManager && camera) {
terrainManager->update(*camera, deltaTime);
}
// Update skybox time progression
if (skybox) {
skybox->update(deltaTime);
}
// Update star field twinkle
if (starField) {
starField->update(deltaTime);
}
// Update clouds animation
if (clouds) {
clouds->update(deltaTime);
}
// Update celestial (moon phase cycling)
if (celestial) {
celestial->update(deltaTime);
}
// Update weather particles
if (weather && camera) {
weather->update(*camera, deltaTime);
}
// Update swim effects
if (swimEffects && camera && cameraController && waterRenderer) {
swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime);
}
// Update character animations
if (characterRenderer) {
characterRenderer->update(deltaTime);
}
// Update zone detection and music
if (zoneManager && musicManager && terrainManager && camera) {
// First check tile-based zone
auto tile = terrainManager->getCurrentTile();
uint32_t zoneId = zoneManager->getZoneId(tile.x, tile.y);
// Override with WMO-based detection (e.g., inside Stormwind)
if (wmoRenderer) {
glm::vec3 camPos = camera->getPosition();
uint32_t wmoModelId = 0;
if (wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoModelId)) {
// Check if inside Stormwind WMO (model ID 10047)
if (wmoModelId == 10047) {
zoneId = 1519; // Stormwind City
}
}
}
if (zoneId != currentZoneId && zoneId != 0) {
currentZoneId = zoneId;
auto* info = zoneManager->getZoneInfo(zoneId);
if (info) {
currentZoneName = info->name;
LOG_INFO("Entered zone: ", info->name);
std::string music = zoneManager->getRandomMusic(zoneId);
if (!music.empty()) {
musicManager->crossfadeTo(music);
}
}
}
musicManager->update(deltaTime);
}
// Update performance HUD
if (performanceHUD) {
performanceHUD->update(deltaTime);
}
}
void Renderer::renderWorld(game::World* world) {
(void)world; // Unused for now
// Get time of day for sky-related rendering
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
// Render skybox first (furthest back)
if (skybox && camera) {
skybox->render(*camera, timeOfDay);
}
// Render stars after skybox
if (starField && camera) {
starField->render(*camera, timeOfDay);
}
// Render celestial bodies (sun/moon) after stars
if (celestial && camera) {
celestial->render(*camera, timeOfDay);
}
// Render clouds after celestial bodies
if (clouds && camera) {
clouds->render(*camera, timeOfDay);
}
// Render lens flare (screen-space effect, render after celestial bodies)
if (lensFlare && camera && celestial) {
glm::vec3 sunPosition = celestial->getSunPosition(timeOfDay);
lensFlare->render(*camera, sunPosition, timeOfDay);
}
// Render terrain if loaded and enabled
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
// Check if camera is underwater for fog override
bool underwater = false;
if (waterRenderer && camera) {
glm::vec3 camPos = camera->getPosition();
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
if (waterH && camPos.z < *waterH) {
underwater = true;
}
}
if (underwater) {
float fogColor[3] = {0.05f, 0.15f, 0.25f};
terrainRenderer->setFog(fogColor, 10.0f, 200.0f);
glClearColor(0.05f, 0.15f, 0.25f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); // Re-clear with underwater color
} else if (skybox) {
// Update terrain fog based on time of day (match sky color)
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
}
terrainRenderer->render(*camera);
// Render water after terrain (transparency requires back-to-front rendering)
if (waterRenderer) {
// Use accumulated time for water animation
static float time = 0.0f;
time += 0.016f; // Approximate frame time
waterRenderer->render(*camera, time);
}
}
// Render weather particles (after terrain/water, before characters)
if (weather && camera) {
weather->render(*camera);
}
// Render swim effects (ripples and bubbles)
if (swimEffects && camera) {
swimEffects->render(*camera);
}
// Render characters (after weather)
if (characterRenderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
characterRenderer->render(*camera, view, projection);
}
// Render WMO buildings (after characters, before UI)
if (wmoRenderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
wmoRenderer->render(*camera, view, projection);
}
// Render M2 doodads (trees, rocks, etc.)
if (m2Renderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
m2Renderer->render(*camera, view, projection);
}
// Render minimap overlay
if (minimap && camera && window) {
minimap->render(*camera, window->getWidth(), window->getHeight());
}
}
bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::string& adtPath) {
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
LOG_INFO("Loading test terrain: ", adtPath);
// Create terrain renderer if not already created
if (!terrainRenderer) {
terrainRenderer = std::make_unique<TerrainRenderer>();
if (!terrainRenderer->initialize(assetManager)) {
LOG_ERROR("Failed to initialize terrain renderer");
terrainRenderer.reset();
return false;
}
}
// Create and initialize terrain manager
if (!terrainManager) {
terrainManager = std::make_unique<TerrainManager>();
if (!terrainManager->initialize(assetManager, terrainRenderer.get())) {
LOG_ERROR("Failed to initialize terrain manager");
terrainManager.reset();
return false;
}
// Set water renderer for terrain streaming
if (waterRenderer) {
terrainManager->setWaterRenderer(waterRenderer.get());
}
// Set M2 renderer for doodad loading during streaming
if (m2Renderer) {
terrainManager->setM2Renderer(m2Renderer.get());
}
// Set WMO renderer for building loading during streaming
if (wmoRenderer) {
terrainManager->setWMORenderer(wmoRenderer.get());
}
// Pass asset manager to character renderer for texture loading
if (characterRenderer) {
characterRenderer->setAssetManager(assetManager);
}
// Wire terrain renderer to minimap
if (minimap) {
minimap->setTerrainRenderer(terrainRenderer.get());
}
// Wire terrain manager, WMO renderer, and water renderer to camera controller
if (cameraController) {
cameraController->setTerrainManager(terrainManager.get());
if (wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get());
}
}
}
// Parse tile coordinates from ADT path
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
int tileX = 32, tileY = 49; // defaults
{
// Find last path separator
size_t lastSep = adtPath.find_last_of("\\/");
if (lastSep != std::string::npos) {
std::string filename = adtPath.substr(lastSep + 1);
// Find first underscore after map name
size_t firstUnderscore = filename.find('_');
if (firstUnderscore != std::string::npos) {
size_t secondUnderscore = filename.find('_', firstUnderscore + 1);
if (secondUnderscore != std::string::npos) {
size_t dot = filename.find('.', secondUnderscore);
if (dot != std::string::npos) {
tileX = std::stoi(filename.substr(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1));
tileY = std::stoi(filename.substr(secondUnderscore + 1, dot - secondUnderscore - 1));
}
}
}
// Extract map name
std::string mapName = filename.substr(0, firstUnderscore != std::string::npos ? firstUnderscore : filename.size());
terrainManager->setMapName(mapName);
}
}
LOG_INFO("Loading initial tile [", tileX, ",", tileY, "] via terrain manager");
// Load the initial tile through TerrainManager (properly tracked for streaming)
if (!terrainManager->loadTile(tileX, tileY)) {
LOG_ERROR("Failed to load initial tile [", tileX, ",", tileY, "]");
return false;
}
terrainLoaded = true;
// Initialize music manager with asset manager
if (musicManager && assetManager && !cachedAssetManager) {
musicManager->initialize(assetManager);
cachedAssetManager = assetManager;
}
// Snap camera to ground now that terrain is loaded
if (cameraController) {
cameraController->reset();
}
LOG_INFO("Test terrain loaded successfully!");
LOG_INFO(" Chunks: ", terrainRenderer->getChunkCount());
LOG_INFO(" Triangles: ", terrainRenderer->getTriangleCount());
return true;
}
void Renderer::setWireframeMode(bool enabled) {
if (terrainRenderer) {
terrainRenderer->setWireframe(enabled);
}
}
bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int centerY, int radius) {
// Create terrain renderer if not already created
if (!terrainRenderer) {
LOG_ERROR("Terrain renderer not initialized");
return false;
}
// Create terrain manager if not already created
if (!terrainManager) {
terrainManager = std::make_unique<TerrainManager>();
// Wire terrain manager to camera controller for grounding
if (cameraController) {
cameraController->setTerrainManager(terrainManager.get());
}
}
LOG_INFO("Loading terrain area: ", mapName, " [", centerX, ",", centerY, "] radius=", radius);
terrainManager->setMapName(mapName);
terrainManager->setLoadRadius(radius);
terrainManager->setUnloadRadius(radius + 1);
// Load tiles in radius
for (int dy = -radius; dy <= radius; dy++) {
for (int dx = -radius; dx <= radius; dx++) {
int tileX = centerX + dx;
int tileY = centerY + dy;
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
terrainManager->loadTile(tileX, tileY);
}
}
}
terrainLoaded = true;
// Initialize music manager with asset manager (if available from loadTestTerrain)
if (musicManager && cachedAssetManager) {
if (!musicManager->isInitialized()) {
musicManager->initialize(cachedAssetManager);
}
}
// Wire WMO and water renderer to camera controller
if (cameraController && wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (cameraController && waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get());
}
// Snap camera to ground now that terrain is loaded
if (cameraController) {
cameraController->reset();
}
LOG_INFO("Terrain area loaded: ", terrainManager->getLoadedTileCount(), " tiles");
return true;
}
void Renderer::setTerrainStreaming(bool enabled) {
if (terrainManager) {
terrainManager->setStreamingEnabled(enabled);
LOG_INFO("Terrain streaming: ", enabled ? "ON" : "OFF");
}
}
void Renderer::renderHUD() {
if (performanceHUD && camera) {
performanceHUD->render(this, camera.get());
}
}
} // namespace rendering
} // namespace wowee

24
src/rendering/scene.cpp Normal file
View file

@ -0,0 +1,24 @@
#include "rendering/scene.hpp"
#include "rendering/mesh.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
void Scene::addMesh(std::shared_ptr<Mesh> mesh) {
meshes.push_back(mesh);
}
void Scene::removeMesh(std::shared_ptr<Mesh> mesh) {
auto it = std::find(meshes.begin(), meshes.end(), mesh);
if (it != meshes.end()) {
meshes.erase(it);
}
}
void Scene::clear() {
meshes.clear();
}
} // namespace rendering
} // namespace wowee

127
src/rendering/shader.cpp Normal file
View file

@ -0,0 +1,127 @@
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
namespace wowee {
namespace rendering {
Shader::~Shader() {
if (program) glDeleteProgram(program);
if (vertexShader) glDeleteShader(vertexShader);
if (fragmentShader) glDeleteShader(fragmentShader);
}
bool Shader::loadFromFile(const std::string& vertexPath, const std::string& fragmentPath) {
// Load vertex shader
std::ifstream vFile(vertexPath);
if (!vFile.is_open()) {
LOG_ERROR("Failed to open vertex shader: ", vertexPath);
return false;
}
std::stringstream vStream;
vStream << vFile.rdbuf();
std::string vertexSource = vStream.str();
// Load fragment shader
std::ifstream fFile(fragmentPath);
if (!fFile.is_open()) {
LOG_ERROR("Failed to open fragment shader: ", fragmentPath);
return false;
}
std::stringstream fStream;
fStream << fFile.rdbuf();
std::string fragmentSource = fStream.str();
return compile(vertexSource, fragmentSource);
}
bool Shader::loadFromSource(const std::string& vertexSource, const std::string& fragmentSource) {
return compile(vertexSource, fragmentSource);
}
bool Shader::compile(const std::string& vertexSource, const std::string& fragmentSource) {
GLint success;
GLchar infoLog[512];
// Compile vertex shader
const char* vCode = vertexSource.c_str();
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vCode, nullptr);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
LOG_ERROR("Vertex shader compilation failed: ", infoLog);
return false;
}
// Compile fragment shader
const char* fCode = fragmentSource.c_str();
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fCode, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
LOG_ERROR("Fragment shader compilation failed: ", infoLog);
return false;
}
// Link program
program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(program, 512, nullptr, infoLog);
LOG_ERROR("Shader program linking failed: ", infoLog);
return false;
}
return true;
}
void Shader::use() const {
glUseProgram(program);
}
void Shader::unuse() const {
glUseProgram(0);
}
GLint Shader::getUniformLocation(const std::string& name) const {
return glGetUniformLocation(program, name.c_str());
}
void Shader::setUniform(const std::string& name, int value) {
glUniform1i(getUniformLocation(name), value);
}
void Shader::setUniform(const std::string& name, float value) {
glUniform1f(getUniformLocation(name), value);
}
void Shader::setUniform(const std::string& name, const glm::vec2& value) {
glUniform2fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::vec3& value) {
glUniform3fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::vec4& value) {
glUniform4fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::mat3& value) {
glUniformMatrix3fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}
void Shader::setUniform(const std::string& name, const glm::mat4& value) {
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}
} // namespace rendering
} // namespace wowee

334
src/rendering/skybox.cpp Normal file
View file

@ -0,0 +1,334 @@
#include "rendering/skybox.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <vector>
namespace wowee {
namespace rendering {
Skybox::Skybox() = default;
Skybox::~Skybox() {
shutdown();
}
bool Skybox::initialize() {
LOG_INFO("Initializing skybox");
// Create sky shader
skyShader = std::make_unique<Shader>();
// Vertex shader - position-only skybox
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 view;
uniform mat4 projection;
out vec3 WorldPos;
out float Altitude;
void main() {
WorldPos = aPos;
// Calculate altitude (0 at horizon, 1 at zenith)
Altitude = normalize(aPos).z;
// Remove translation from view matrix (keep rotation only)
mat4 viewNoTranslation = mat4(mat3(view));
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
// Ensure skybox is always at far plane
gl_Position = gl_Position.xyww;
}
)";
// Fragment shader - gradient sky with time of day
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 WorldPos;
in float Altitude;
uniform vec3 horizonColor;
uniform vec3 zenithColor;
uniform float timeOfDay;
out vec4 FragColor;
void main() {
// Smooth gradient from horizon to zenith
float t = pow(max(Altitude, 0.0), 0.5); // Curve for more interesting gradient
vec3 skyColor = mix(horizonColor, zenithColor, t);
// Add atmospheric scattering effect (more saturated near horizon)
float scattering = 1.0 - t * 0.3;
skyColor *= scattering;
FragColor = vec4(skyColor, 1.0);
}
)";
if (!skyShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create sky shader");
return false;
}
// Create sky dome mesh
createSkyDome();
LOG_INFO("Skybox initialized");
return true;
}
void Skybox::shutdown() {
destroySkyDome();
skyShader.reset();
}
void Skybox::render(const Camera& camera, float time) {
if (!renderingEnabled || vao == 0 || !skyShader) {
return;
}
// Render skybox first (before terrain), with depth test set to LEQUAL
glDepthFunc(GL_LEQUAL);
skyShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
skyShader->setUniform("view", view);
skyShader->setUniform("projection", projection);
skyShader->setUniform("timeOfDay", time);
// Get colors based on time of day
glm::vec3 horizon = getHorizonColor(time);
glm::vec3 zenith = getZenithColor(time);
skyShader->setUniform("horizonColor", horizon);
skyShader->setUniform("zenithColor", zenith);
// Render dome
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
// Restore depth function
glDepthFunc(GL_LESS);
}
void Skybox::update(float deltaTime) {
if (timeProgressionEnabled) {
timeOfDay += deltaTime * timeSpeed;
// Wrap around 24 hours
if (timeOfDay >= 24.0f) {
timeOfDay -= 24.0f;
}
}
}
void Skybox::setTimeOfDay(float time) {
// Clamp to 0-24 range
while (time < 0.0f) time += 24.0f;
while (time >= 24.0f) time -= 24.0f;
timeOfDay = time;
}
void Skybox::createSkyDome() {
// Create an extended dome that goes below horizon for better coverage
const int rings = 16; // Vertical resolution
const int sectors = 32; // Horizontal resolution
const float radius = 2000.0f; // Large enough to cover view without looking curved
std::vector<float> vertices;
std::vector<uint32_t> indices;
// Generate vertices - extend slightly below horizon
const float minPhi = -M_PI / 12.0f; // Start 15° below horizon
const float maxPhi = M_PI / 2.0f; // End at zenith
for (int ring = 0; ring <= rings; ring++) {
float phi = minPhi + (maxPhi - minPhi) * (static_cast<float>(ring) / rings);
float y = radius * std::sin(phi);
float ringRadius = radius * std::cos(phi);
for (int sector = 0; sector <= sectors; sector++) {
float theta = (2.0f * M_PI) * (static_cast<float>(sector) / sectors);
float x = ringRadius * std::cos(theta);
float z = ringRadius * std::sin(theta);
// Position
vertices.push_back(x);
vertices.push_back(z); // Z up in WoW coordinates
vertices.push_back(y);
}
}
// Generate indices
for (int ring = 0; ring < rings; ring++) {
for (int sector = 0; sector < sectors; sector++) {
int current = ring * (sectors + 1) + sector;
int next = current + sectors + 1;
// Two triangles per quad
indices.push_back(current);
indices.push_back(next);
indices.push_back(current + 1);
indices.push_back(current + 1);
indices.push_back(next);
indices.push_back(next + 1);
}
}
indexCount = static_cast<int>(indices.size());
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Set vertex attributes (position only)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
LOG_DEBUG("Sky dome created: ", (rings + 1) * (sectors + 1), " vertices, ", indexCount / 3, " triangles");
}
void Skybox::destroySkyDome() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo != 0) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
glm::vec3 Skybox::getHorizonColor(float time) const {
// Time-based horizon colors
// 0-6: Night (dark blue)
// 6-8: Dawn (orange/pink)
// 8-16: Day (light blue)
// 16-18: Dusk (orange/red)
// 18-24: Night (dark blue)
if (time < 5.0f || time >= 21.0f) {
// Night - dark blue/purple horizon
return glm::vec3(0.05f, 0.05f, 0.15f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to orange
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend from orange to blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - light blue horizon
return glm::vec3(0.6f, 0.7f, 0.9f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend from blue to orange/red
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend from orange to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getZenithColor(float time) const {
// Zenith (top of sky) colors
if (time < 5.0f || time >= 21.0f) {
// Night - very dark blue, almost black
return glm::vec3(0.01f, 0.01f, 0.05f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to light blue
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend to bright blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - bright blue zenith
return glm::vec3(0.2f, 0.5f, 1.0f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend to darker blue
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getSkyColor(float altitude, float time) const {
// Blend between horizon and zenith based on altitude
glm::vec3 horizon = getHorizonColor(time);
glm::vec3 zenith = getZenithColor(time);
// Use power curve for more natural gradient
float t = std::pow(std::max(altitude, 0.0f), 0.5f);
return glm::mix(horizon, zenith, t);
}
} // namespace rendering
} // namespace wowee

259
src/rendering/starfield.cpp Normal file
View file

@ -0,0 +1,259 @@
#include "rendering/starfield.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <random>
namespace wowee {
namespace rendering {
StarField::StarField() = default;
StarField::~StarField() {
shutdown();
}
bool StarField::initialize() {
LOG_INFO("Initializing star field");
// Create star shader
starShader = std::make_unique<Shader>();
// Vertex shader - simple point rendering
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aBrightness;
layout (location = 2) in float aTwinklePhase;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
uniform float intensity;
out float Brightness;
void main() {
// Remove translation from view matrix (stars are infinitely far)
mat4 viewNoTranslation = mat4(mat3(view));
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
// Twinkle effect (subtle brightness variation)
float twinkle = sin(time * 2.0 + aTwinklePhase) * 0.2 + 0.8; // 0.6 to 1.0
Brightness = aBrightness * twinkle * intensity;
// Point size based on brightness
gl_PointSize = 2.0 + aBrightness * 2.0; // 2-4 pixels
}
)";
// Fragment shader - star color
const char* fragmentShaderSource = R"(
#version 330 core
in float Brightness;
out vec4 FragColor;
void main() {
// Circular point (not square)
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) {
discard;
}
// Soften edges
float alpha = smoothstep(0.5, 0.3, dist);
// Star color (slightly blue-white)
vec3 starColor = vec3(0.9, 0.95, 1.0);
FragColor = vec4(starColor * Brightness, alpha * Brightness);
}
)";
if (!starShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create star shader");
return false;
}
// Generate random stars
generateStars();
// Create OpenGL buffers
createStarBuffers();
LOG_INFO("Star field initialized: ", starCount, " stars");
return true;
}
void StarField::shutdown() {
destroyStarBuffers();
starShader.reset();
stars.clear();
}
void StarField::render(const Camera& camera, float timeOfDay) {
if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) {
return;
}
// Get star intensity based on time of day
float intensity = getStarIntensity(timeOfDay);
// Don't render if stars would be invisible
if (intensity <= 0.01f) {
return;
}
// Enable blending for star glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Enable point sprites
glEnable(GL_PROGRAM_POINT_SIZE);
// Disable depth writing (stars are background)
glDepthMask(GL_FALSE);
starShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
starShader->setUniform("view", view);
starShader->setUniform("projection", projection);
starShader->setUniform("time", twinkleTime);
starShader->setUniform("intensity", intensity);
// Render stars as points
glBindVertexArray(vao);
glDrawArrays(GL_POINTS, 0, starCount);
glBindVertexArray(0);
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
glDisable(GL_BLEND);
}
void StarField::update(float deltaTime) {
// Update twinkle animation
twinkleTime += deltaTime;
}
void StarField::generateStars() {
stars.clear();
stars.reserve(starCount);
// Random number generator
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> phiDist(0.0f, M_PI / 2.0f); // 0 to 90 degrees (hemisphere)
std::uniform_real_distribution<float> thetaDist(0.0f, 2.0f * M_PI); // 0 to 360 degrees
std::uniform_real_distribution<float> brightnessDist(0.3f, 1.0f); // Varying brightness
std::uniform_real_distribution<float> twinkleDist(0.0f, 2.0f * M_PI); // Random twinkle phase
const float radius = 900.0f; // Slightly larger than skybox
for (int i = 0; i < starCount; i++) {
Star star;
// Spherical coordinates (hemisphere)
float phi = phiDist(gen); // Elevation angle
float theta = thetaDist(gen); // Azimuth angle
// Convert to Cartesian coordinates
float x = radius * std::sin(phi) * std::cos(theta);
float y = radius * std::sin(phi) * std::sin(theta);
float z = radius * std::cos(phi);
star.position = glm::vec3(x, y, z);
star.brightness = brightnessDist(gen);
star.twinklePhase = twinkleDist(gen);
stars.push_back(star);
}
LOG_DEBUG("Generated ", stars.size(), " stars");
}
void StarField::createStarBuffers() {
// Prepare vertex data (position, brightness, twinkle phase)
std::vector<float> vertexData;
vertexData.reserve(stars.size() * 5); // 3 pos + 1 brightness + 1 phase
for (const auto& star : stars) {
vertexData.push_back(star.position.x);
vertexData.push_back(star.position.y);
vertexData.push_back(star.position.z);
vertexData.push_back(star.brightness);
vertexData.push_back(star.twinklePhase);
}
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Brightness
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Twinkle phase
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
}
void StarField::destroyStarBuffers() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
float StarField::getStarIntensity(float timeOfDay) const {
// Stars visible at night (fade in/out at dusk/dawn)
// Full night: 20:00-4:00
if (timeOfDay >= 20.0f || timeOfDay < 4.0f) {
return 1.0f;
}
// Fade in at dusk: 18:00-20:00
else if (timeOfDay >= 18.0f && timeOfDay < 20.0f) {
return (timeOfDay - 18.0f) / 2.0f; // 0 to 1 over 2 hours
}
// Fade out at dawn: 4:00-6:00
else if (timeOfDay >= 4.0f && timeOfDay < 6.0f) {
return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 to 0 over 2 hours
}
// Daytime: no stars
else {
return 0.0f;
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,380 @@
#include "rendering/swim_effects.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
static std::mt19937& rng() {
static std::random_device rd;
static std::mt19937 gen(rd());
return gen;
}
static float randFloat(float lo, float hi) {
std::uniform_real_distribution<float> dist(lo, hi);
return dist(rng());
}
SwimEffects::SwimEffects() = default;
SwimEffects::~SwimEffects() { shutdown(); }
bool SwimEffects::initialize() {
LOG_INFO("Initializing swim effects");
// --- Ripple/splash shader (small white spray droplets) ---
rippleShader = std::make_unique<Shader>();
const char* rippleVS = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aSize;
layout (location = 2) in float aAlpha;
uniform mat4 uView;
uniform mat4 uProjection;
out float vAlpha;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = aSize;
vAlpha = aAlpha;
}
)";
const char* rippleFS = R"(
#version 330 core
in float vAlpha;
out vec4 FragColor;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
// Soft circular splash droplet
float alpha = smoothstep(0.5, 0.2, dist) * vAlpha;
FragColor = vec4(0.85, 0.92, 1.0, alpha);
}
)";
if (!rippleShader->loadFromSource(rippleVS, rippleFS)) {
LOG_ERROR("Failed to create ripple shader");
return false;
}
// --- Bubble shader ---
bubbleShader = std::make_unique<Shader>();
const char* bubbleVS = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aSize;
layout (location = 2) in float aAlpha;
uniform mat4 uView;
uniform mat4 uProjection;
out float vAlpha;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = aSize;
vAlpha = aAlpha;
}
)";
const char* bubbleFS = R"(
#version 330 core
in float vAlpha;
out vec4 FragColor;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
// Bubble with highlight
float edge = smoothstep(0.5, 0.35, dist);
float hollow = smoothstep(0.25, 0.35, dist);
float bubble = edge * hollow;
// Specular highlight near top-left
float highlight = smoothstep(0.3, 0.0, length(coord - vec2(-0.12, -0.12)));
float alpha = (bubble * 0.6 + highlight * 0.4) * vAlpha;
vec3 color = vec3(0.7, 0.85, 1.0);
FragColor = vec4(color, alpha);
}
)";
if (!bubbleShader->loadFromSource(bubbleVS, bubbleFS)) {
LOG_ERROR("Failed to create bubble shader");
return false;
}
// --- Ripple VAO/VBO ---
glGenVertexArrays(1, &rippleVAO);
glGenBuffers(1, &rippleVBO);
glBindVertexArray(rippleVAO);
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
// layout: vec3 pos, float size, float alpha (stride = 5 floats)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
// --- Bubble VAO/VBO ---
glGenVertexArrays(1, &bubbleVAO);
glGenBuffers(1, &bubbleVBO);
glBindVertexArray(bubbleVAO);
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
ripples.reserve(MAX_RIPPLE_PARTICLES);
bubbles.reserve(MAX_BUBBLE_PARTICLES);
rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5);
bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5);
LOG_INFO("Swim effects initialized");
return true;
}
void SwimEffects::shutdown() {
if (rippleVAO) { glDeleteVertexArrays(1, &rippleVAO); rippleVAO = 0; }
if (rippleVBO) { glDeleteBuffers(1, &rippleVBO); rippleVBO = 0; }
if (bubbleVAO) { glDeleteVertexArrays(1, &bubbleVAO); bubbleVAO = 0; }
if (bubbleVBO) { glDeleteBuffers(1, &bubbleVBO); bubbleVBO = 0; }
rippleShader.reset();
bubbleShader.reset();
ripples.clear();
bubbles.clear();
}
void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) {
if (static_cast<int>(ripples.size()) >= MAX_RIPPLE_PARTICLES) return;
Particle p;
// Scatter splash droplets around the character at the water surface
float ox = randFloat(-1.5f, 1.5f);
float oy = randFloat(-1.5f, 1.5f);
p.position = glm::vec3(pos.x + ox, pos.y + oy, waterH + 0.3f);
// Spray outward + upward from movement direction
float spread = randFloat(-1.0f, 1.0f);
glm::vec3 perp(-moveDir.y, moveDir.x, 0.0f);
glm::vec3 outDir = -moveDir + perp * spread;
float speed = randFloat(1.5f, 4.0f);
p.velocity = glm::vec3(outDir.x * speed, outDir.y * speed, randFloat(1.0f, 3.0f));
p.lifetime = 0.0f;
p.maxLifetime = randFloat(0.5f, 1.0f);
p.size = randFloat(3.0f, 7.0f);
p.alpha = randFloat(0.5f, 0.8f);
ripples.push_back(p);
}
void SwimEffects::spawnBubble(const glm::vec3& pos, float /*waterH*/) {
if (static_cast<int>(bubbles.size()) >= MAX_BUBBLE_PARTICLES) return;
Particle p;
float ox = randFloat(-3.0f, 3.0f);
float oy = randFloat(-3.0f, 3.0f);
float oz = randFloat(-2.0f, 0.0f);
p.position = glm::vec3(pos.x + ox, pos.y + oy, pos.z + oz);
p.velocity = glm::vec3(randFloat(-0.3f, 0.3f), randFloat(-0.3f, 0.3f), randFloat(4.0f, 8.0f));
p.lifetime = 0.0f;
p.maxLifetime = randFloat(2.0f, 3.5f);
p.size = randFloat(6.0f, 12.0f);
p.alpha = 0.6f;
bubbles.push_back(p);
}
void SwimEffects::update(const Camera& camera, const CameraController& cc,
const WaterRenderer& water, float deltaTime) {
glm::vec3 camPos = camera.getPosition();
// Use character position for ripples in third-person mode
glm::vec3 charPos = camPos;
const glm::vec3* followTarget = cc.getFollowTarget();
if (cc.isThirdPerson() && followTarget) {
charPos = *followTarget;
}
// Check water at character position (for ripples) and camera position (for bubbles)
auto charWaterH = water.getWaterHeightAt(charPos.x, charPos.y);
auto camWaterH = water.getWaterHeightAt(camPos.x, camPos.y);
bool swimming = cc.isSwimming();
bool moving = cc.isMoving();
// --- Ripple/splash spawning ---
if (swimming && charWaterH) {
float wh = *charWaterH;
float spawnRate = moving ? 40.0f : 8.0f;
rippleSpawnAccum += spawnRate * deltaTime;
// Compute movement direction from camera yaw
float yawRad = glm::radians(cc.getYaw());
glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f);
if (glm::length(glm::vec2(moveDir)) > 0.001f) {
moveDir = glm::normalize(moveDir);
}
while (rippleSpawnAccum >= 1.0f) {
spawnRipple(charPos, moveDir, wh);
rippleSpawnAccum -= 1.0f;
}
} else {
rippleSpawnAccum = 0.0f;
ripples.clear();
}
// --- Bubble spawning ---
bool underwater = camWaterH && camPos.z < *camWaterH;
if (underwater) {
float bubbleRate = 20.0f;
bubbleSpawnAccum += bubbleRate * deltaTime;
while (bubbleSpawnAccum >= 1.0f) {
spawnBubble(camPos, *camWaterH);
bubbleSpawnAccum -= 1.0f;
}
} else {
bubbleSpawnAccum = 0.0f;
bubbles.clear();
}
// --- Update ripples (splash droplets with gravity) ---
for (int i = static_cast<int>(ripples.size()) - 1; i >= 0; --i) {
auto& p = ripples[i];
p.lifetime += deltaTime;
if (p.lifetime >= p.maxLifetime) {
ripples[i] = ripples.back();
ripples.pop_back();
continue;
}
// Apply gravity to splash droplets
p.velocity.z -= 9.8f * deltaTime;
p.position += p.velocity * deltaTime;
// Kill if fallen back below water
float surfaceZ = charWaterH ? *charWaterH : 0.0f;
if (p.position.z < surfaceZ && p.lifetime > 0.1f) {
ripples[i] = ripples.back();
ripples.pop_back();
continue;
}
float t = p.lifetime / p.maxLifetime;
p.alpha = glm::mix(0.7f, 0.0f, t);
p.size = glm::mix(5.0f, 2.0f, t);
}
// --- Update bubbles ---
float bubbleCeilH = camWaterH ? *camWaterH : 0.0f;
for (int i = static_cast<int>(bubbles.size()) - 1; i >= 0; --i) {
auto& p = bubbles[i];
p.lifetime += deltaTime;
if (p.lifetime >= p.maxLifetime || p.position.z >= bubbleCeilH) {
bubbles[i] = bubbles.back();
bubbles.pop_back();
continue;
}
// Wobble
float wobbleX = std::sin(p.lifetime * 3.0f) * 0.5f;
float wobbleY = std::cos(p.lifetime * 2.5f) * 0.5f;
p.position += (p.velocity + glm::vec3(wobbleX, wobbleY, 0.0f)) * deltaTime;
float t = p.lifetime / p.maxLifetime;
if (t > 0.8f) {
p.alpha = 0.6f * (1.0f - (t - 0.8f) / 0.2f);
} else {
p.alpha = 0.6f;
}
}
// --- Build vertex data ---
rippleVertexData.clear();
for (const auto& p : ripples) {
rippleVertexData.push_back(p.position.x);
rippleVertexData.push_back(p.position.y);
rippleVertexData.push_back(p.position.z);
rippleVertexData.push_back(p.size);
rippleVertexData.push_back(p.alpha);
}
bubbleVertexData.clear();
for (const auto& p : bubbles) {
bubbleVertexData.push_back(p.position.x);
bubbleVertexData.push_back(p.position.y);
bubbleVertexData.push_back(p.position.z);
bubbleVertexData.push_back(p.size);
bubbleVertexData.push_back(p.alpha);
}
}
void SwimEffects::render(const Camera& camera) {
if (rippleVertexData.empty() && bubbleVertexData.empty()) return;
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(GL_FALSE);
glEnable(GL_PROGRAM_POINT_SIZE);
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
// --- Render ripples (splash droplets above water surface) ---
if (!rippleVertexData.empty() && rippleShader) {
rippleShader->use();
rippleShader->setUniform("uView", view);
rippleShader->setUniform("uProjection", projection);
glBindVertexArray(rippleVAO);
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
glBufferData(GL_ARRAY_BUFFER,
rippleVertexData.size() * sizeof(float),
rippleVertexData.data(),
GL_DYNAMIC_DRAW);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(rippleVertexData.size() / 5));
glBindVertexArray(0);
}
// --- Render bubbles ---
if (!bubbleVertexData.empty() && bubbleShader) {
bubbleShader->use();
bubbleShader->setUniform("uView", view);
bubbleShader->setUniform("uProjection", projection);
glBindVertexArray(bubbleVAO);
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
glBufferData(GL_ARRAY_BUFFER,
bubbleVertexData.size() * sizeof(float),
bubbleVertexData.data(),
GL_DYNAMIC_DRAW);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(bubbleVertexData.size() / 5));
glBindVertexArray(0);
}
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,826 @@
#include "rendering/terrain_manager.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <cmath>
#include <cctype>
#include <functional>
#include <unordered_set>
namespace wowee {
namespace rendering {
TerrainManager::TerrainManager() {
}
TerrainManager::~TerrainManager() {
// Stop worker thread before cleanup (containers clean up via destructors)
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
if (workerThread.joinable()) {
workerThread.join();
}
}
}
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
assetManager = assets;
terrainRenderer = renderer;
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
if (!terrainRenderer) {
LOG_ERROR("Terrain renderer is null");
return false;
}
// Start background worker thread
workerRunning.store(true);
workerThread = std::thread(&TerrainManager::workerLoop, this);
LOG_INFO("Terrain manager initialized (async loading enabled)");
LOG_INFO(" Map: ", mapName);
LOG_INFO(" Load radius: ", loadRadius, " tiles");
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
return true;
}
void TerrainManager::update(const Camera& camera, float deltaTime) {
if (!streamingEnabled || !assetManager || !terrainRenderer) {
return;
}
// Always process ready tiles each frame (GPU uploads from background thread)
processReadyTiles();
timeSinceLastUpdate += deltaTime;
// Only update streaming periodically (not every frame)
if (timeSinceLastUpdate < updateInterval) {
return;
}
timeSinceLastUpdate = 0.0f;
// Get current tile from camera position
// GL coordinate mapping: GL Y = -(wowX - ZEROPOINT), GL X = -(wowZ - ZEROPOINT), GL Z = height
// worldToTile expects: worldX = -glY (maps to tileX), worldY = glX (maps to tileY)
glm::vec3 camPos = camera.getPosition();
TileCoord newTile = worldToTile(-camPos.y, camPos.x);
// Check if we've moved to a different tile
if (newTile.x != currentTile.x || newTile.y != currentTile.y) {
LOG_DEBUG("Camera moved to tile [", newTile.x, ",", newTile.y, "]");
currentTile = newTile;
}
// Stream tiles if we've moved significantly or initial load
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
") tile=[", newTile.x, ",", newTile.y,
"] loaded=", loadedTiles.size());
streamTiles();
lastStreamTile = newTile;
}
}
// Synchronous fallback for initial tile loading (before worker thread is useful)
bool TerrainManager::loadTile(int x, int y) {
TileCoord coord = {x, y};
// Check if already loaded
if (loadedTiles.find(coord) != loadedTiles.end()) {
return true;
}
// Don't retry tiles that already failed
if (failedTiles.find(coord) != failedTiles.end()) {
return false;
}
LOG_INFO("Loading terrain tile [", x, ",", y, "] (synchronous)");
auto pending = prepareTile(x, y);
if (!pending) {
failedTiles[coord] = true;
return false;
}
finalizeTile(std::move(pending));
return true;
}
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
TileCoord coord = {x, y};
LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)");
// Load ADT file
std::string adtPath = getADTPath(coord);
auto adtData = assetManager->readFile(adtPath);
if (adtData.empty()) {
LOG_WARNING("Failed to load ADT file: ", adtPath);
return nullptr;
}
// Parse ADT
pipeline::ADTTerrain terrain = pipeline::ADTLoader::load(adtData);
if (!terrain.isLoaded()) {
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
return nullptr;
}
// Set tile coordinates so mesh knows where to position this tile in world
terrain.coord.x = x;
terrain.coord.y = y;
// Generate mesh
pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(terrain);
if (mesh.validChunkCount == 0) {
LOG_ERROR("Failed to generate terrain mesh: ", adtPath);
return nullptr;
}
auto pending = std::make_unique<PendingTile>();
pending->coord = coord;
pending->terrain = std::move(terrain);
pending->mesh = std::move(mesh);
// Pre-load M2 doodads (CPU: read files, parse models)
if (!pending->terrain.doodadPlacements.empty()) {
std::unordered_set<uint32_t> preparedModelIds;
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
for (const auto& placement : pending->terrain.doodadPlacements) {
if (placement.nameId >= pending->terrain.doodadNames.size()) {
skippedNameId++;
continue;
}
std::string m2Path = pending->terrain.doodadNames[placement.nameId];
// Convert .mdx to .m2 if needed
if (m2Path.size() > 4) {
std::string ext = m2Path.substr(m2Path.size() - 4);
for (char& c : ext) c = std::tolower(c);
if (ext == ".mdx") {
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
}
}
// Use path hash as globally unique model ID (nameId is per-tile local)
uint32_t modelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
// Parse model if not already done for this tile
if (preparedModelIds.find(modelId) == preparedModelIds.end()) {
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
if (!m2Data.empty()) {
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
// Try to load skin file
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
if (!skinData.empty()) {
pipeline::M2Loader::loadSkin(skinData, m2Model);
} else {
skippedSkinNotFound++;
LOG_WARNING("M2 skin not found: ", skinPath);
}
if (m2Model.isValid()) {
PendingTile::M2Ready ready;
ready.modelId = modelId;
ready.model = std::move(m2Model);
ready.path = m2Path;
pending->m2Models.push_back(std::move(ready));
preparedModelIds.insert(modelId);
} else {
skippedInvalid++;
LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path);
}
} else {
skippedFileNotFound++;
LOG_WARNING("M2 file not found: ", m2Path);
}
}
// Store placement data for instance creation on main thread
if (preparedModelIds.count(modelId)) {
const float ZEROPOINT = 32.0f * 533.33333f;
float wowX = placement.position[0];
float wowY = placement.position[1];
float wowZ = placement.position[2];
PendingTile::M2Placement p;
p.modelId = modelId;
p.uniqueId = placement.uniqueId;
p.position = glm::vec3(
-(wowZ - ZEROPOINT),
-(wowX - ZEROPOINT),
wowY
);
p.rotation = glm::vec3(
-placement.rotation[2] * 3.14159f / 180.0f,
-placement.rotation[0] * 3.14159f / 180.0f,
placement.rotation[1] * 3.14159f / 180.0f
);
p.scale = placement.scale / 1024.0f;
pending->m2Placements.push_back(p);
}
}
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) {
LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ",
skippedNameId, " bad nameId, ",
skippedFileNotFound, " file not found, ",
skippedInvalid, " invalid model, ",
skippedSkinNotFound, " skin not found");
}
}
// Pre-load WMOs (CPU: read files, parse models and groups)
if (!pending->terrain.wmoPlacements.empty()) {
for (const auto& placement : pending->terrain.wmoPlacements) {
if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
if (wmoData.empty()) continue;
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) {
std::string basePath = wmoPath;
std::string extension;
if (basePath.size() > 4) {
extension = basePath.substr(basePath.size() - 4);
std::string extLower = extension;
for (char& c : extLower) c = std::tolower(c);
if (extLower == ".wmo") {
basePath = basePath.substr(0, basePath.size() - 4);
}
}
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
}
}
}
if (!wmoModel.groups.empty()) {
const float ZEROPOINT = 32.0f * 533.33333f;
glm::vec3 pos(
-(placement.position[2] - ZEROPOINT),
-(placement.position[0] - ZEROPOINT),
placement.position[1]
);
glm::vec3 rot(
-placement.rotation[2] * 3.14159f / 180.0f,
-placement.rotation[0] * 3.14159f / 180.0f,
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
);
// Pre-load WMO doodads (M2 models inside WMO)
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
glm::mat4 wmoMatrix(1.0f);
wmoMatrix = glm::translate(wmoMatrix, pos);
wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1));
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
const auto& doodadSet = wmoModel.doodadSets[0];
for (uint32_t di = 0; di < doodadSet.count; di++) {
uint32_t doodadIdx = doodadSet.startIndex + di;
if (doodadIdx >= wmoModel.doodads.size()) break;
const auto& doodad = wmoModel.doodads[doodadIdx];
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
if (nameIt == wmoModel.doodadNames.end()) continue;
std::string m2Path = nameIt->second;
if (m2Path.empty()) continue;
if (m2Path.size() > 4) {
std::string ext = m2Path.substr(m2Path.size() - 4);
for (char& c : ext) c = std::tolower(c);
if (ext == ".mdx" || ext == ".mdl") {
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
}
}
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
if (m2Data.empty()) continue;
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
if (!skinData.empty()) {
pipeline::M2Loader::loadSkin(skinData, m2Model);
}
if (!m2Model.isValid()) continue;
// Build doodad's local transform (WoW coordinates)
// WMO doodads use quaternion rotation
glm::mat4 doodadLocal(1.0f);
doodadLocal = glm::translate(doodadLocal, doodad.position);
doodadLocal *= glm::mat4_cast(doodad.rotation);
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
// Full world transform = WMO world transform * doodad local transform
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
// Extract world position for frustum culling
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
PendingTile::WMODoodadReady doodadReady;
doodadReady.modelId = doodadModelId;
doodadReady.model = std::move(m2Model);
doodadReady.worldPosition = worldPos;
doodadReady.modelMatrix = worldMatrix;
pending->wmoDoodads.push_back(std::move(doodadReady));
}
}
PendingTile::WMOReady ready;
ready.modelId = placement.uniqueId;
ready.model = std::move(wmoModel);
ready.position = pos;
ready.rotation = rot;
pending->wmoModels.push_back(std::move(ready));
}
}
}
LOG_INFO("Prepared tile [", x, ",", y, "]: ",
pending->m2Models.size(), " M2 models, ",
pending->m2Placements.size(), " M2 placements, ",
pending->wmoModels.size(), " WMOs, ",
pending->wmoDoodads.size(), " WMO doodads");
return pending;
}
void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
int x = pending->coord.x;
int y = pending->coord.y;
TileCoord coord = pending->coord;
LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)");
// Check if tile was already loaded (race condition guard) or failed
if (loadedTiles.find(coord) != loadedTiles.end()) {
return;
}
if (failedTiles.find(coord) != failedTiles.end()) {
return;
}
// Upload terrain to GPU
if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) {
LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]");
failedTiles[coord] = true;
return;
}
// Load water
if (waterRenderer) {
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
}
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> tileUniqueIds;
// Upload M2 models to GPU and create instances
if (m2Renderer && assetManager) {
if (!m2Renderer->getModelCount()) {
m2Renderer->initialize(assetManager);
}
// Upload unique models
std::unordered_set<uint32_t> uploadedModelIds;
for (auto& m2Ready : pending->m2Models) {
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
uploadedModelIds.insert(m2Ready.modelId);
}
}
// Create instances (deduplicate by uniqueId across tile boundaries)
int loadedDoodads = 0;
int skippedDedup = 0;
for (const auto& p : pending->m2Placements) {
// Skip if this doodad was already placed by a neighboring tile
if (placedDoodadIds.count(p.uniqueId)) {
skippedDedup++;
continue;
}
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
if (instId) {
m2InstanceIds.push_back(instId);
placedDoodadIds.insert(p.uniqueId);
tileUniqueIds.push_back(p.uniqueId);
loadedDoodads++;
}
}
LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ",
loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
skippedDedup, " dedup skipped)");
}
// Upload WMO models to GPU and create instances
if (wmoRenderer && assetManager) {
if (!wmoRenderer->getModelCount()) {
wmoRenderer->initialize(assetManager);
}
int loadedWMOs = 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++;
}
}
}
// Upload WMO doodad M2 models
if (m2Renderer) {
for (auto& doodad : pending->wmoDoodads) {
m2Renderer->loadModel(doodad.model, doodad.modelId);
uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix(
doodad.modelId, doodad.modelMatrix, doodad.worldPosition);
if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId);
}
}
if (loadedWMOs > 0) {
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
}
}
// Create tile entry
auto tile = std::make_unique<TerrainTile>();
tile->coord = coord;
tile->terrain = std::move(pending->terrain);
tile->mesh = std::move(pending->mesh);
tile->loaded = true;
tile->m2InstanceIds = std::move(m2InstanceIds);
tile->wmoInstanceIds = std::move(wmoInstanceIds);
tile->doodadUniqueIds = std::move(tileUniqueIds);
// Calculate world bounds
getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY);
loadedTiles[coord] = std::move(tile);
LOG_INFO(" Finalized tile [", x, ",", y, "]");
}
void TerrainManager::workerLoop() {
LOG_INFO("Terrain worker thread started");
while (workerRunning.load()) {
TileCoord coord;
bool hasWork = false;
{
std::unique_lock<std::mutex> lock(queueMutex);
queueCV.wait(lock, [this]() {
return !loadQueue.empty() || !workerRunning.load();
});
if (!workerRunning.load()) {
break;
}
if (!loadQueue.empty()) {
coord = loadQueue.front();
loadQueue.pop();
hasWork = true;
}
}
if (hasWork) {
auto pending = prepareTile(coord.x, coord.y);
std::lock_guard<std::mutex> lock(queueMutex);
if (pending) {
readyQueue.push(std::move(pending));
} else {
// Mark as failed so we don't re-enqueue
// We'll set failedTiles on the main thread in processReadyTiles
// For now, just remove from pending tracking
pendingTiles.erase(coord);
}
}
}
LOG_INFO("Terrain worker thread stopped");
}
void TerrainManager::processReadyTiles() {
// Process up to 2 ready tiles per frame to spread GPU work
int processed = 0;
const int maxPerFrame = 2;
while (processed < maxPerFrame) {
std::unique_ptr<PendingTile> pending;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (readyQueue.empty()) {
break;
}
pending = std::move(readyQueue.front());
readyQueue.pop();
}
if (pending) {
TileCoord coord = pending->coord;
finalizeTile(std::move(pending));
pendingTiles.erase(coord);
processed++;
}
}
}
void TerrainManager::unloadTile(int x, int y) {
TileCoord coord = {x, y};
// Also remove from pending if it was queued but not yet loaded
pendingTiles.erase(coord);
auto it = loadedTiles.find(coord);
if (it == loadedTiles.end()) {
return;
}
LOG_INFO("Unloading terrain tile [", x, ",", y, "]");
const auto& tile = it->second;
// Remove doodad unique IDs from dedup set
for (uint32_t uid : tile->doodadUniqueIds) {
placedDoodadIds.erase(uid);
}
// Remove M2 doodad instances
if (m2Renderer) {
for (uint32_t id : tile->m2InstanceIds) {
m2Renderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
}
// Remove WMO instances
if (wmoRenderer) {
for (uint32_t id : tile->wmoInstanceIds) {
wmoRenderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
}
// Remove terrain chunks for this tile
if (terrainRenderer) {
terrainRenderer->removeTile(x, y);
}
// Remove water surfaces for this tile
if (waterRenderer) {
waterRenderer->removeTile(x, y);
}
loadedTiles.erase(it);
}
void TerrainManager::unloadAll() {
// Stop worker thread
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
if (workerThread.joinable()) {
workerThread.join();
}
}
// Clear queues
{
std::lock_guard<std::mutex> lock(queueMutex);
while (!loadQueue.empty()) loadQueue.pop();
while (!readyQueue.empty()) readyQueue.pop();
}
pendingTiles.clear();
placedDoodadIds.clear();
LOG_INFO("Unloading all terrain tiles");
loadedTiles.clear();
failedTiles.clear();
// Clear terrain renderer
if (terrainRenderer) {
terrainRenderer->clear();
}
// Clear water
if (waterRenderer) {
waterRenderer->clear();
}
}
TileCoord TerrainManager::worldToTile(float worldX, float worldY) const {
// WoW world coordinate system:
// - Tiles are 8533.33 units wide (TILE_SIZE)
// - Tile (32, 32) is roughly at world origin for continents
// - Coordinates increase going east (X) and south (Y)
int tileX = 32 + static_cast<int>(std::floor(worldX / TILE_SIZE));
int tileY = 32 - static_cast<int>(std::floor(worldY / TILE_SIZE));
// Clamp to valid range (0-63)
tileX = std::max(0, std::min(63, tileX));
tileY = std::max(0, std::min(63, tileY));
return {tileX, tileY};
}
void TerrainManager::getTileBounds(const TileCoord& coord, float& minX, float& minY,
float& maxX, float& maxY) const {
// Calculate world bounds for this tile
// Tile (32, 32) is at origin
float offsetX = (32 - coord.x) * TILE_SIZE;
float offsetY = (32 - coord.y) * TILE_SIZE;
minX = offsetX - TILE_SIZE;
minY = offsetY - TILE_SIZE;
maxX = offsetX;
maxY = offsetY;
}
std::string TerrainManager::getADTPath(const TileCoord& coord) const {
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
// Example: Azeroth_32_49.adt for tile at coord.x=32, coord.y=49
return "World\\Maps\\" + mapName + "\\" + mapName + "_" +
std::to_string(coord.x) + "_" + std::to_string(coord.y) + ".adt";
}
std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
// Terrain mesh vertices are positioned as:
// vertex.position[0] = chunk.position[0] - (offsetY * unitSize) -> GL X
// vertex.position[1] = chunk.position[1] - (offsetX * unitSize) -> GL Y
// vertex.position[2] = chunk.position[2] + height -> GL Z (height)
//
// The 9x9 outer vertex grid has offsetX, offsetY in [0, 8].
// So the chunk spans:
// X: [chunk.position[0] - 8*unitSize, chunk.position[0]]
// Y: [chunk.position[1] - 8*unitSize, chunk.position[1]]
const float unitSize = CHUNK_SIZE / 8.0f;
for (const auto& [coord, tile] : loadedTiles) {
if (!tile || !tile->loaded) continue;
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
const auto& chunk = tile->terrain.getChunk(cx, cy);
if (!chunk.hasHeightMap()) continue;
float chunkMaxX = chunk.position[0];
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
float chunkMaxY = chunk.position[1];
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
if (glX < chunkMinX || glX > chunkMaxX ||
glY < chunkMinY || glY > chunkMaxY) {
continue;
}
// Fractional position within chunk (0-8 range)
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
fracX = glm::clamp(fracX, 0.0f, 8.0f);
fracY = glm::clamp(fracY, 0.0f, 8.0f);
// Bilinear interpolation on 9x9 outer grid
int gx0 = static_cast<int>(std::floor(fracX));
int gy0 = static_cast<int>(std::floor(fracY));
int gx1 = std::min(gx0 + 1, 8);
int gy1 = std::min(gy0 + 1, 8);
float tx = fracX - gx0;
float ty = fracY - gy0;
// Outer vertex heights from the 9x17 layout
// Outer vertex (gx, gy) is at index: gy * 17 + gx
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
float h = h00 * (1 - tx) * (1 - ty) +
h10 * tx * (1 - ty) +
h01 * (1 - tx) * ty +
h11 * tx * ty;
return chunk.position[2] + h;
}
}
}
return std::nullopt;
}
void TerrainManager::streamTiles() {
// Enqueue tiles in radius around current tile for async loading
{
std::lock_guard<std::mutex> lock(queueMutex);
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
int tileX = currentTile.x + dx;
int tileY = currentTile.y + dy;
// Check valid range
if (tileX < 0 || tileX > 63 || tileY < 0 || tileY > 63) {
continue;
}
TileCoord coord = {tileX, tileY};
// Skip if already loaded, pending, or failed
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
if (failedTiles.find(coord) != failedTiles.end()) continue;
loadQueue.push(coord);
pendingTiles[coord] = true;
}
}
}
// Notify worker thread that there's work
queueCV.notify_one();
// Unload tiles beyond unload radius (well past the camera far clip)
std::vector<TileCoord> tilesToUnload;
for (const auto& pair : loadedTiles) {
const TileCoord& coord = pair.first;
int dx = std::abs(coord.x - currentTile.x);
int dy = std::abs(coord.y - currentTile.y);
// Chebyshev distance
if (dx > unloadRadius || dy > unloadRadius) {
tilesToUnload.push_back(coord);
}
}
for (const auto& coord : tilesToUnload) {
unloadTile(coord.x, coord.y);
}
if (!tilesToUnload.empty()) {
LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ",
loadedTiles.size(), " remain");
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,520 @@
#include "rendering/terrain_renderer.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <limits>
namespace wowee {
namespace rendering {
TerrainRenderer::TerrainRenderer() {
}
TerrainRenderer::~TerrainRenderer() {
shutdown();
}
bool TerrainRenderer::initialize(pipeline::AssetManager* assets) {
assetManager = assets;
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
LOG_INFO("Initializing terrain renderer");
// Load terrain shader
shader = std::make_unique<Shader>();
if (!shader->loadFromFile("assets/shaders/terrain.vert", "assets/shaders/terrain.frag")) {
LOG_ERROR("Failed to load terrain shader");
return false;
}
// Create default white texture for fallback
uint8_t whitePixel[4] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
LOG_INFO("Terrain renderer initialized");
return true;
}
void TerrainRenderer::shutdown() {
LOG_INFO("Shutting down terrain renderer");
clear();
// Delete white texture
if (whiteTexture) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
// Delete cached textures
for (auto& pair : textureCache) {
glDeleteTextures(1, &pair.second);
}
textureCache.clear();
shader.reset();
}
bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
const std::vector<std::string>& texturePaths,
int tileX, int tileY) {
LOG_INFO("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
// Upload each chunk to GPU
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
const auto& chunk = mesh.getChunk(x, y);
if (!chunk.isValid()) {
continue;
}
TerrainChunkGPU gpuChunk = uploadChunk(chunk);
if (!gpuChunk.isValid()) {
LOG_WARNING("Failed to upload chunk [", x, ",", y, "]");
continue;
}
// Calculate bounding sphere for frustum culling
calculateBoundingSphere(gpuChunk, chunk);
// Load textures for this chunk
if (!chunk.layers.empty()) {
// Base layer (always present)
uint32_t baseTexId = chunk.layers[0].textureId;
if (baseTexId < texturePaths.size()) {
gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]);
} else {
gpuChunk.baseTexture = whiteTexture;
}
// Additional layers (with alpha blending)
for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) {
const auto& layer = chunk.layers[i];
// Load layer texture
GLuint layerTex = whiteTexture;
if (layer.textureId < texturePaths.size()) {
layerTex = loadTexture(texturePaths[layer.textureId]);
}
gpuChunk.layerTextures.push_back(layerTex);
// Create alpha texture
GLuint alphaTex = 0;
if (!layer.alphaData.empty()) {
alphaTex = createAlphaTexture(layer.alphaData);
}
gpuChunk.alphaTextures.push_back(alphaTex);
}
} else {
// No layers, use default white texture
gpuChunk.baseTexture = whiteTexture;
}
gpuChunk.tileX = tileX;
gpuChunk.tileY = tileY;
chunks.push_back(gpuChunk);
}
}
LOG_INFO("Loaded ", chunks.size(), " terrain chunks to GPU");
return !chunks.empty();
}
TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
TerrainChunkGPU gpuChunk;
gpuChunk.worldX = chunk.worldX;
gpuChunk.worldY = chunk.worldY;
gpuChunk.worldZ = chunk.worldZ;
gpuChunk.indexCount = static_cast<uint32_t>(chunk.indices.size());
// Debug: verify Z values in uploaded vertices
static int uploadLogCount = 0;
if (uploadLogCount < 3 && !chunk.vertices.empty()) {
float minZ = 999999.0f, maxZ = -999999.0f;
for (const auto& v : chunk.vertices) {
if (v.position[2] < minZ) minZ = v.position[2];
if (v.position[2] > maxZ) maxZ = v.position[2];
}
LOG_DEBUG("GPU upload Z range: [", minZ, ", ", maxZ, "] delta=", maxZ - minZ);
uploadLogCount++;
}
// Create VAO
glGenVertexArrays(1, &gpuChunk.vao);
glBindVertexArray(gpuChunk.vao);
// Create VBO
glGenBuffers(1, &gpuChunk.vbo);
glBindBuffer(GL_ARRAY_BUFFER, gpuChunk.vbo);
glBufferData(GL_ARRAY_BUFFER,
chunk.vertices.size() * sizeof(pipeline::TerrainVertex),
chunk.vertices.data(),
GL_STATIC_DRAW);
// Create IBO
glGenBuffers(1, &gpuChunk.ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuChunk.ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
chunk.indices.size() * sizeof(pipeline::TerrainIndex),
chunk.indices.data(),
GL_STATIC_DRAW);
// Set up vertex attributes
// Location 0: Position (vec3)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, position));
// Location 1: Normal (vec3)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, normal));
// Location 2: TexCoord (vec2)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, texCoord));
// Location 3: LayerUV (vec2)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, layerUV));
glBindVertexArray(0);
return gpuChunk;
}
GLuint TerrainRenderer::loadTexture(const std::string& path) {
// Check cache first
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
LOG_WARNING("Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Upload texture data (BLP loader outputs RGBA8)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
// Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// Generate mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
// Cache texture
textureCache[path] = textureID;
LOG_DEBUG("Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) {
if (alphaData.empty()) {
return 0;
}
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Alpha data is always expanded to 4096 bytes (64x64 at 8-bit) by terrain_mesh
int width = 64;
int height = static_cast<int>(alphaData.size()) / 64;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,
width, height, 0,
GL_RED, GL_UNSIGNED_BYTE, alphaData.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);
glBindTexture(GL_TEXTURE_2D, 0);
return textureID;
}
void TerrainRenderer::render(const Camera& camera) {
if (chunks.empty() || !shader) {
return;
}
// Enable depth testing
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// Disable backface culling temporarily to debug flashing
glDisable(GL_CULL_FACE);
// glEnable(GL_CULL_FACE);
// glCullFace(GL_BACK);
// Wireframe mode
if (wireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
} else {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
// Use shader
shader->use();
// Set view/projection matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
glm::mat4 model = glm::mat4(1.0f);
shader->setUniform("uModel", model);
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set lighting
shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2]));
shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2]));
shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2]));
// Set camera position
glm::vec3 camPos = camera.getPosition();
shader->setUniform("uViewPos", camPos);
// Set fog (disable by setting very far distances)
shader->setUniform("uFogColor", glm::vec3(fogColor[0], fogColor[1], fogColor[2]));
if (fogEnabled) {
shader->setUniform("uFogStart", fogStart);
shader->setUniform("uFogEnd", fogEnd);
} else {
shader->setUniform("uFogStart", 100000.0f); // Very far
shader->setUniform("uFogEnd", 100001.0f); // Effectively disabled
}
// Extract frustum for culling
Frustum frustum;
if (frustumCullingEnabled) {
glm::mat4 viewProj = projection * view;
frustum.extractFromMatrix(viewProj);
}
// Render each chunk
renderedChunks = 0;
culledChunks = 0;
for (const auto& chunk : chunks) {
if (!chunk.isValid()) {
continue;
}
// Frustum culling
if (frustumCullingEnabled && !isChunkVisible(chunk, frustum)) {
culledChunks++;
continue;
}
// Bind textures
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, chunk.baseTexture);
shader->setUniform("uBaseTexture", 0);
// Bind layer textures and alphas
bool hasLayer1 = chunk.layerTextures.size() > 0;
bool hasLayer2 = chunk.layerTextures.size() > 1;
bool hasLayer3 = chunk.layerTextures.size() > 2;
shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0);
shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0);
shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0);
if (hasLayer1) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]);
shader->setUniform("uLayer1Texture", 1);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]);
shader->setUniform("uLayer1Alpha", 4);
}
if (hasLayer2) {
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]);
shader->setUniform("uLayer2Texture", 2);
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]);
shader->setUniform("uLayer2Alpha", 5);
}
if (hasLayer3) {
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]);
shader->setUniform("uLayer3Texture", 3);
glActiveTexture(GL_TEXTURE6);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]);
shader->setUniform("uLayer3Alpha", 6);
}
// Draw chunk
glBindVertexArray(chunk.vao);
glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
renderedChunks++;
}
// Reset wireframe
if (wireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
}
void TerrainRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = chunks.begin();
while (it != chunks.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
if (it->vao) glDeleteVertexArrays(1, &it->vao);
if (it->vbo) glDeleteBuffers(1, &it->vbo);
if (it->ibo) glDeleteBuffers(1, &it->ibo);
for (GLuint alpha : it->alphaTextures) {
if (alpha) glDeleteTextures(1, &alpha);
}
it = chunks.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Removed ", removed, " terrain chunks for tile [", tileX, ",", tileY, "]");
}
}
void TerrainRenderer::clear() {
// Delete all GPU resources
for (auto& chunk : chunks) {
if (chunk.vao) glDeleteVertexArrays(1, &chunk.vao);
if (chunk.vbo) glDeleteBuffers(1, &chunk.vbo);
if (chunk.ibo) glDeleteBuffers(1, &chunk.ibo);
// Delete alpha textures (not cached)
for (GLuint alpha : chunk.alphaTextures) {
if (alpha) glDeleteTextures(1, &alpha);
}
}
chunks.clear();
renderedChunks = 0;
}
void TerrainRenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3],
const float ambientColorIn[3]) {
lightDir[0] = lightDirIn[0];
lightDir[1] = lightDirIn[1];
lightDir[2] = lightDirIn[2];
lightColor[0] = lightColorIn[0];
lightColor[1] = lightColorIn[1];
lightColor[2] = lightColorIn[2];
ambientColor[0] = ambientColorIn[0];
ambientColor[1] = ambientColorIn[1];
ambientColor[2] = ambientColorIn[2];
}
void TerrainRenderer::setFog(const float fogColorIn[3], float fogStartIn, float fogEndIn) {
fogColor[0] = fogColorIn[0];
fogColor[1] = fogColorIn[1];
fogColor[2] = fogColorIn[2];
fogStart = fogStartIn;
fogEnd = fogEndIn;
}
int TerrainRenderer::getTriangleCount() const {
int total = 0;
for (const auto& chunk : chunks) {
total += chunk.indexCount / 3;
}
return total;
}
bool TerrainRenderer::isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum) {
// Test bounding sphere against frustum
return frustum.intersectsSphere(chunk.boundingSphereCenter, chunk.boundingSphereRadius);
}
void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk,
const pipeline::ChunkMesh& meshChunk) {
if (meshChunk.vertices.empty()) {
gpuChunk.boundingSphereRadius = 0.0f;
gpuChunk.boundingSphereCenter = glm::vec3(0.0f);
return;
}
// Calculate AABB first
glm::vec3 min(std::numeric_limits<float>::max());
glm::vec3 max(std::numeric_limits<float>::lowest());
for (const auto& vertex : meshChunk.vertices) {
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
min = glm::min(min, pos);
max = glm::max(max, pos);
}
// Center is midpoint of AABB
gpuChunk.boundingSphereCenter = (min + max) * 0.5f;
// Radius is distance from center to furthest vertex
float maxDistSq = 0.0f;
for (const auto& vertex : meshChunk.vertices) {
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
glm::vec3 diff = pos - gpuChunk.boundingSphereCenter;
float distSq = glm::dot(diff, diff);
maxDistSq = std::max(maxDistSq, distSq);
}
gpuChunk.boundingSphereRadius = std::sqrt(maxDistSq);
}
} // namespace rendering
} // namespace wowee

51
src/rendering/texture.cpp Normal file
View file

@ -0,0 +1,51 @@
#include "rendering/texture.hpp"
#include "core/logger.hpp"
// Stub implementation - would use stb_image or similar
namespace wowee {
namespace rendering {
Texture::~Texture() {
if (textureID) {
glDeleteTextures(1, &textureID);
}
}
bool Texture::loadFromFile(const std::string& path) {
// TODO: Implement with stb_image or BLP loader
LOG_WARNING("Texture loading not yet implemented: ", path);
return false;
}
bool Texture::loadFromMemory(const unsigned char* data, int w, int h, int channels) {
width = w;
height = h;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
GLenum format = (channels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void Texture::bind(GLuint unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, textureID);
}
void Texture::unbind() const {
glBindTexture(GL_TEXTURE_2D, 0);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,497 @@
#include "rendering/water_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/adt_loader.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
WaterRenderer::WaterRenderer() = default;
WaterRenderer::~WaterRenderer() {
shutdown();
}
bool WaterRenderer::initialize() {
LOG_INFO("Initializing water renderer");
// Create water shader
waterShader = std::make_unique<Shader>();
// Vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out float WaveOffset;
void main() {
// Simple pass-through for debugging (no wave animation)
vec3 pos = aPos;
FragPos = vec3(model * vec4(pos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoord = aTexCoord;
WaveOffset = 0.0;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
)";
// Fragment shader
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in float WaveOffset;
uniform vec3 viewPos;
uniform vec4 waterColor;
uniform float waterAlpha;
uniform float time;
out vec4 FragColor;
void main() {
// Normalize interpolated normal
vec3 norm = normalize(Normal);
// Simple directional light (sun)
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
float diff = max(dot(norm, lightDir), 0.0);
// Specular highlights (shininess for water)
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0);
// Animated texture coordinates for flowing effect
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
vec2 uv2 = TexCoord + vec2(-time * 0.01, time * 0.015);
// Combine lighting
vec3 ambient = vec3(0.3) * waterColor.rgb;
vec3 diffuse = vec3(0.6) * diff * waterColor.rgb;
vec3 specular = vec3(1.0) * spec;
// Add wave offset to brightness
float brightness = 1.0 + WaveOffset * 0.1;
vec3 result = (ambient + diffuse + specular) * brightness;
// Apply transparency
FragColor = vec4(result, waterAlpha);
}
)";
if (!waterShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create water shader");
return false;
}
LOG_INFO("Water renderer initialized");
return true;
}
void WaterRenderer::shutdown() {
clear();
waterShader.reset();
}
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
int tileX, int tileY) {
if (!append) {
LOG_INFO("Loading water from terrain (replacing)");
clear();
} else {
LOG_INFO("Loading water from terrain (appending)");
}
// Load water surfaces from MH2O data
int totalLayers = 0;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
const auto& chunkWater = terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) {
continue;
}
// Get the terrain chunk for position reference
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
const auto& terrainChunk = terrain.getChunk(chunkX, chunkY);
// Process each water layer in this chunk
for (const auto& layer : chunkWater.layers) {
WaterSurface surface;
// Use the chunk base position - layer offsets will be applied in mesh generation
// to match terrain's coordinate transformation
surface.position = glm::vec3(
terrainChunk.position[0],
terrainChunk.position[1],
layer.minHeight
);
// Debug log first few water surfaces
if (totalLayers < 5) {
LOG_DEBUG("Water layer ", totalLayers, ": chunk=", chunkIdx,
" liquidType=", layer.liquidType,
" offset=(", (int)layer.x, ",", (int)layer.y, ")",
" size=", (int)layer.width, "x", (int)layer.height,
" height range=[", layer.minHeight, ",", layer.maxHeight, "]");
}
surface.minHeight = layer.minHeight;
surface.maxHeight = layer.maxHeight;
surface.liquidType = layer.liquidType;
// Store dimensions
surface.xOffset = layer.x;
surface.yOffset = layer.y;
surface.width = layer.width;
surface.height = layer.height;
// Copy height data
if (!layer.heights.empty()) {
surface.heights = layer.heights;
} else {
// Flat water at minHeight if no height data
size_t numVertices = (layer.width + 1) * (layer.height + 1);
surface.heights.resize(numVertices, layer.minHeight);
}
// Copy render mask
surface.mask = layer.mask;
surface.tileX = tileX;
surface.tileY = tileY;
createWaterMesh(surface);
surfaces.push_back(surface);
totalLayers++;
}
}
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data");
}
void WaterRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Removed ", removed, " water surfaces for tile [", tileX, ",", tileY, "]");
}
}
void WaterRenderer::clear() {
for (auto& surface : surfaces) {
destroyWaterMesh(surface);
}
surfaces.clear();
}
void WaterRenderer::render(const Camera& camera, float time) {
if (!renderingEnabled || surfaces.empty() || !waterShader) {
return;
}
// Enable alpha blending for transparent water
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth writing so terrain shows through water
glDepthMask(GL_FALSE);
waterShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
waterShader->setUniform("view", view);
waterShader->setUniform("projection", projection);
waterShader->setUniform("viewPos", camera.getPosition());
waterShader->setUniform("time", time);
// Render each water surface
for (const auto& surface : surfaces) {
if (surface.vao == 0) {
continue;
}
// Model matrix (identity, position already in vertices)
glm::mat4 model = glm::mat4(1.0f);
waterShader->setUniform("model", model);
// Set liquid-specific color and alpha
glm::vec4 color = getLiquidColor(surface.liquidType);
float alpha = getLiquidAlpha(surface.liquidType);
waterShader->setUniform("waterColor", color);
waterShader->setUniform("waterAlpha", alpha);
// Render
glBindVertexArray(surface.vao);
glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
// Variable-size grid based on water layer dimensions
const int gridWidth = surface.width + 1; // Vertices = tiles + 1
const int gridHeight = surface.height + 1;
const float TILE_SIZE = 33.33333f / 8.0f; // Size of one tile (same as terrain unitSize)
std::vector<float> vertices;
std::vector<uint32_t> indices;
// Generate vertices
// Match terrain coordinate transformation: pos[0] = baseX - (y * unitSize), pos[1] = baseY - (x * unitSize)
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
int index = y * gridWidth + x;
// Use per-vertex height data if available, otherwise flat at minHeight
float height;
if (index < static_cast<int>(surface.heights.size())) {
height = surface.heights[index];
} else {
height = surface.minHeight;
}
// Position - match terrain coordinate transformation (swap and negate)
// Terrain uses: X = baseX - (offsetY * unitSize), Y = baseY - (offsetX * unitSize)
// Also apply layer offset within chunk (xOffset, yOffset)
float posX = surface.position.x - ((surface.yOffset + y) * TILE_SIZE);
float posY = surface.position.y - ((surface.xOffset + x) * TILE_SIZE);
float posZ = height;
// Debug first surface's corner vertices
static int debugCount = 0;
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
LOG_DEBUG("Water vertex: (", posX, ", ", posY, ", ", posZ, ")");
debugCount++;
}
vertices.push_back(posX);
vertices.push_back(posY);
vertices.push_back(posZ);
// Normal (pointing up for water surface)
vertices.push_back(0.0f);
vertices.push_back(0.0f);
vertices.push_back(1.0f);
// Texture coordinates
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
}
}
// Generate indices (triangles), respecting the render mask
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
// Check render mask - each bit represents a tile
bool renderTile = true;
if (!surface.mask.empty()) {
int tileIndex = y * surface.width + x;
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
}
}
if (!renderTile) {
continue; // Skip this tile
}
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
// First triangle
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
// Second triangle
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
if (indices.empty()) {
// No visible tiles
return;
}
surface.indexCount = static_cast<int>(indices.size());
// Create OpenGL buffers
glGenVertexArrays(1, &surface.vao);
glGenBuffers(1, &surface.vbo);
glGenBuffers(1, &surface.ebo);
glBindVertexArray(surface.vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, surface.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Normal
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Texture coordinates
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
}
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
if (surface.vao != 0) {
glDeleteVertexArrays(1, &surface.vao);
surface.vao = 0;
}
if (surface.vbo != 0) {
glDeleteBuffers(1, &surface.vbo);
surface.vbo = 0;
}
if (surface.ebo != 0) {
glDeleteBuffers(1, &surface.ebo);
surface.ebo = 0;
}
}
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
const float TILE_SIZE = 33.33333f / 8.0f;
std::optional<float> best;
for (size_t si = 0; si < surfaces.size(); si++) {
const auto& surface = surfaces[si];
float gy = (surface.position.x - glX) / TILE_SIZE - static_cast<float>(surface.yOffset);
float gx = (surface.position.y - glY) / TILE_SIZE - static_cast<float>(surface.xOffset);
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
continue;
}
int gridWidth = surface.width + 1;
// Bilinear interpolation
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
float fx = gx - ix;
float fy = gy - iy;
// Clamp to valid vertex range
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
int idx00 = iy * gridWidth + ix;
int idx10 = idx00 + 1;
int idx01 = idx00 + gridWidth;
int idx11 = idx01 + 1;
int total = static_cast<int>(surface.heights.size());
if (idx11 >= total) continue;
float h00 = surface.heights[idx00];
float h10 = surface.heights[idx10];
float h01 = surface.heights[idx01];
float h11 = surface.heights[idx11];
float h = h00 * (1-fx) * (1-fy) + h10 * fx * (1-fy) +
h01 * (1-fx) * fy + h11 * fx * fy;
if (!best || h > *best) {
best = h;
}
}
return best;
}
glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const {
// WoW 3.3.5a LiquidType.dbc IDs:
// 1,5,9,13,17 = Water variants (still, slow, fast)
// 2,6,10,14 = Ocean
// 3,7,11,15 = Magma
// 4,8,12 = Slime
// Map to basic type using (id - 1) % 4 for standard IDs, or handle ranges
uint8_t basicType;
if (liquidType == 0) {
basicType = 0; // Water (fallback)
} else {
basicType = ((liquidType - 1) % 4);
}
switch (basicType) {
case 0: // Water
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
case 1: // Ocean
return glm::vec4(0.1f, 0.3f, 0.5f, 1.0f);
case 2: // Magma
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
case 3: // Slime
return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
default:
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); // Water fallback
}
}
float WaterRenderer::getLiquidAlpha(uint8_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) {
case 2: return 0.85f; // Magma - mostly opaque
case 3: return 0.75f; // Slime - semi-opaque
default: return 0.55f; // Water/Ocean - semi-transparent
}
}
} // namespace rendering
} // namespace wowee

275
src/rendering/weather.cpp Normal file
View file

@ -0,0 +1,275 @@
#include "rendering/weather.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
Weather::Weather() {
}
Weather::~Weather() {
cleanup();
}
bool Weather::initialize() {
LOG_INFO("Initializing weather system");
// Create shader
shader = std::make_unique<Shader>();
// Vertex shader - point sprites with instancing
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uParticleSize;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = uParticleSize;
}
)";
// Fragment shader - simple particle with alpha
const char* fragmentShaderSource = R"(
#version 330 core
uniform vec4 uParticleColor;
out vec4 FragColor;
void main() {
// Circular particle shape
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) {
discard;
}
// Soft edges
float alpha = smoothstep(0.5, 0.3, dist) * uParticleColor.a;
FragColor = vec4(uParticleColor.rgb, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create weather shader");
return false;
}
// Create VAO and VBO for particle positions
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// Reserve space for particles
particles.reserve(MAX_PARTICLES);
particlePositions.reserve(MAX_PARTICLES);
LOG_INFO("Weather system initialized");
return true;
}
void Weather::update(const Camera& camera, float deltaTime) {
if (!enabled || weatherType == Type::NONE) {
return;
}
// Initialize particles if needed
if (particles.empty()) {
resetParticles(camera);
}
// Calculate active particle count based on intensity
int targetParticleCount = static_cast<int>(MAX_PARTICLES * intensity);
// Adjust particle count
while (static_cast<int>(particles.size()) < targetParticleCount) {
Particle p;
p.position = getRandomPosition(camera.getPosition());
p.position.y = camera.getPosition().y + SPAWN_HEIGHT;
p.lifetime = 0.0f;
if (weatherType == Type::RAIN) {
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f); // Fast downward
p.maxLifetime = 5.0f;
} else { // SNOW
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f); // Slow downward
p.maxLifetime = 10.0f;
}
particles.push_back(p);
}
while (static_cast<int>(particles.size()) > targetParticleCount) {
particles.pop_back();
}
// Update each particle
for (auto& particle : particles) {
updateParticle(particle, camera, deltaTime);
}
// Update position buffer
particlePositions.clear();
for (const auto& particle : particles) {
particlePositions.push_back(particle.position);
}
}
void Weather::updateParticle(Particle& particle, const Camera& camera, float deltaTime) {
// Update lifetime
particle.lifetime += deltaTime;
// Reset if lifetime exceeded or too far from camera
glm::vec3 cameraPos = camera.getPosition();
float distance = glm::length(particle.position - cameraPos);
if (particle.lifetime >= particle.maxLifetime || distance > SPAWN_VOLUME_SIZE ||
particle.position.y < cameraPos.y - 20.0f) {
// Respawn at top
particle.position = getRandomPosition(cameraPos);
particle.position.y = cameraPos.y + SPAWN_HEIGHT;
particle.lifetime = 0.0f;
}
// Add wind effect for snow
if (weatherType == Type::SNOW) {
float windX = std::sin(particle.lifetime * 0.5f) * 2.0f;
float windZ = std::cos(particle.lifetime * 0.3f) * 2.0f;
particle.velocity.x = windX;
particle.velocity.z = windZ;
}
// Update position
particle.position += particle.velocity * deltaTime;
}
void Weather::render(const Camera& camera) {
if (!enabled || weatherType == Type::NONE || particlePositions.empty() || !shader) {
return;
}
// Enable blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth write (particles are transparent)
glDepthMask(GL_FALSE);
// Enable point sprites
glEnable(GL_PROGRAM_POINT_SIZE);
shader->use();
// Set matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set particle appearance based on weather type
if (weatherType == Type::RAIN) {
// Rain: white/blue streaks, small size
shader->setUniform("uParticleColor", glm::vec4(0.7f, 0.8f, 0.9f, 0.6f));
shader->setUniform("uParticleSize", 3.0f);
} else { // SNOW
// Snow: white fluffy, larger size
shader->setUniform("uParticleColor", glm::vec4(1.0f, 1.0f, 1.0f, 0.9f));
shader->setUniform("uParticleSize", 8.0f);
}
// Upload particle positions
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
particlePositions.size() * sizeof(glm::vec3),
particlePositions.data(),
GL_DYNAMIC_DRAW);
// Render particles as points
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(particlePositions.size()));
glBindVertexArray(0);
// Restore state
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
}
void Weather::resetParticles(const Camera& camera) {
particles.clear();
int particleCount = static_cast<int>(MAX_PARTICLES * intensity);
glm::vec3 cameraPos = camera.getPosition();
for (int i = 0; i < particleCount; ++i) {
Particle p;
p.position = getRandomPosition(cameraPos);
p.position.y = cameraPos.y + SPAWN_HEIGHT * (static_cast<float>(rand()) / RAND_MAX);
p.lifetime = 0.0f;
if (weatherType == Type::RAIN) {
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f);
p.maxLifetime = 5.0f;
} else { // SNOW
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f);
p.maxLifetime = 10.0f;
}
particles.push_back(p);
}
}
glm::vec3 Weather::getRandomPosition(const glm::vec3& center) const {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
float x = center.x + dist(gen) * SPAWN_VOLUME_SIZE;
float z = center.z + dist(gen) * SPAWN_VOLUME_SIZE;
float y = center.y;
return glm::vec3(x, y, z);
}
void Weather::setIntensity(float intensity) {
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
}
int Weather::getParticleCount() const {
return static_cast<int>(particles.size());
}
void Weather::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,835 @@
#include "rendering/wmo_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
namespace wowee {
namespace rendering {
WMORenderer::WMORenderer() {
}
WMORenderer::~WMORenderer() {
shutdown();
}
bool WMORenderer::initialize(pipeline::AssetManager* assets) {
core::Logger::getInstance().info("Initializing WMO renderer...");
assetManager = assets;
// Create WMO shader with texture support
const char* vertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in vec4 aColor;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out vec4 VertexColor;
void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal;
TexCoord = aTexCoord;
VertexColor = aColor;
gl_Position = uProjection * uView * worldPos;
}
)";
const char* fragmentSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in vec4 VertexColor;
uniform vec3 uLightDir;
uniform vec3 uViewPos;
uniform vec3 uAmbientColor;
uniform sampler2D uTexture;
uniform bool uHasTexture;
uniform bool uAlphaTest;
out vec4 FragColor;
void main() {
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(uLightDir);
// Diffuse lighting
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * vec3(1.0);
// Ambient
vec3 ambient = uAmbientColor;
// Sample texture or use vertex color
vec4 texColor;
if (uHasTexture) {
texColor = texture(uTexture, TexCoord);
// Alpha test only for cutout materials (lattice, grating, etc.)
if (uAlphaTest && texColor.a < 0.5) discard;
} else {
// MOCV vertex color alpha is a lighting blend factor, not transparency
texColor = vec4(VertexColor.rgb, 1.0);
}
// Combine lighting with texture
vec3 result = (ambient + diffuse) * texColor.rgb;
FragColor = vec4(result, 1.0);
}
)";
shader = std::make_unique<Shader>();
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
core::Logger::getInstance().error("Failed to create WMO shader");
return false;
}
// Create default white texture for fallback
uint8_t whitePixel[4] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
core::Logger::getInstance().info("WMO renderer initialized");
return true;
}
void WMORenderer::shutdown() {
core::Logger::getInstance().info("Shutting down WMO renderer...");
// Free all GPU resources
for (auto& [id, model] : loadedModels) {
for (auto& group : model.groups) {
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
}
}
// Free cached textures
for (auto& [path, texId] : textureCache) {
if (texId != 0 && texId != whiteTexture) {
glDeleteTextures(1, &texId);
}
}
textureCache.clear();
// Free white texture
if (whiteTexture != 0) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
loadedModels.clear();
instances.clear();
shader.reset();
}
bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
if (!model.isValid()) {
core::Logger::getInstance().error("Cannot load invalid WMO model");
return false;
}
// Check if already loaded
if (loadedModels.find(id) != loadedModels.end()) {
core::Logger::getInstance().warning("WMO model ", id, " already loaded");
return true;
}
core::Logger::getInstance().info("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
model.textures.size(), " textures...");
ModelData modelData;
modelData.id = id;
modelData.boundingBoxMin = model.boundingBoxMin;
modelData.boundingBoxMax = model.boundingBoxMax;
core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
// Load textures for this model
core::Logger::getInstance().info(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
if (assetManager && !model.textures.empty()) {
for (size_t i = 0; i < model.textures.size(); i++) {
const auto& texPath = model.textures[i];
core::Logger::getInstance().debug(" Loading texture ", i, ": ", texPath);
GLuint texId = loadTexture(texPath);
modelData.textures.push_back(texId);
}
core::Logger::getInstance().info(" Loaded ", modelData.textures.size(), " textures for WMO");
}
// Store material -> texture index mapping
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
// We need to convert it using the textureOffsetToIndex map
core::Logger::getInstance().info(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
static int matLogCount = 0;
for (size_t i = 0; i < model.materials.size(); i++) {
const auto& mat = model.materials[i];
uint32_t texIndex = 0; // Default to first texture
auto it = model.textureOffsetToIndex.find(mat.texture1);
if (it != model.textureOffsetToIndex.end()) {
texIndex = it->second;
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
matLogCount++;
}
} else if (mat.texture1 < model.textures.size()) {
// Fallback: maybe it IS an index in some files?
texIndex = mat.texture1;
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": using texture1 as direct index: ", texIndex);
matLogCount++;
}
} else {
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
matLogCount++;
}
}
modelData.materialTextureIndices.push_back(texIndex);
modelData.materialBlendModes.push_back(mat.blendMode);
}
// Create GPU resources for each group
uint32_t loadedGroups = 0;
for (const auto& wmoGroup : model.groups) {
// Skip empty groups
if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) {
continue;
}
GroupResources resources;
if (createGroupResources(wmoGroup, resources)) {
modelData.groups.push_back(resources);
loadedGroups++;
}
}
if (loadedGroups == 0) {
core::Logger::getInstance().warning("No valid groups loaded for WMO ", id);
return false;
}
loadedModels[id] = std::move(modelData);
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
return true;
}
void WMORenderer::unloadModel(uint32_t id) {
auto it = loadedModels.find(id);
if (it == loadedModels.end()) {
return;
}
// Free GPU resources
for (auto& group : it->second.groups) {
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
}
loadedModels.erase(it);
core::Logger::getInstance().info("WMO model ", id, " unloaded");
}
uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation, float scale) {
// Check if model is loaded
if (loadedModels.find(modelId) == loadedModels.end()) {
core::Logger::getInstance().error("Cannot create instance of unloaded WMO model ", modelId);
return 0;
}
WMOInstance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position;
instance.rotation = rotation;
instance.scale = scale;
instance.updateModelMatrix();
instances.push_back(instance);
core::Logger::getInstance().info("Created WMO instance ", instance.id, " (model ", modelId, ")");
return instance.id;
}
void WMORenderer::removeInstance(uint32_t instanceId) {
auto it = std::find_if(instances.begin(), instances.end(),
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
if (it != instances.end()) {
instances.erase(it);
core::Logger::getInstance().info("Removed WMO instance ", instanceId);
}
}
void WMORenderer::clearInstances() {
instances.clear();
core::Logger::getInstance().info("Cleared all WMO instances");
}
void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
if (!shader || instances.empty()) {
lastDrawCalls = 0;
return;
}
lastDrawCalls = 0;
// Set shader uniforms
shader->use();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
shader->setUniform("uViewPos", camera.getPosition());
shader->setUniform("uLightDir", glm::vec3(-0.3f, -0.7f, -0.6f)); // Default sun direction
shader->setUniform("uAmbientColor", glm::vec3(0.4f, 0.4f, 0.5f));
// Enable wireframe if requested
if (wireframeMode) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
// WMOs are opaque — ensure blending is off (alpha test via discard in shader)
glDisable(GL_BLEND);
// Disable backface culling for WMOs (some faces may have wrong winding)
glDisable(GL_CULL_FACE);
// Render all instances
for (const auto& instance : instances) {
auto modelIt = loadedModels.find(instance.modelId);
if (modelIt == loadedModels.end()) {
continue;
}
const ModelData& model = modelIt->second;
shader->setUniform("uModel", instance.modelMatrix);
// Render all groups
for (const auto& group : model.groups) {
// Frustum culling
if (frustumCulling && !isGroupVisible(group, instance.modelMatrix, camera)) {
continue;
}
renderGroup(group, model, instance.modelMatrix, view, projection);
}
}
// Restore polygon mode
if (wireframeMode) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
// Re-enable backface culling
glEnable(GL_CULL_FACE);
}
uint32_t WMORenderer::getTotalTriangleCount() const {
uint32_t total = 0;
for (const auto& instance : instances) {
auto modelIt = loadedModels.find(instance.modelId);
if (modelIt != loadedModels.end()) {
total += modelIt->second.getTotalTriangles();
}
}
return total;
}
bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources) {
if (group.vertices.empty() || group.indices.empty()) {
return false;
}
resources.vertexCount = group.vertices.size();
resources.indexCount = group.indices.size();
resources.boundingBoxMin = group.boundingBoxMin;
resources.boundingBoxMax = group.boundingBoxMax;
// Create vertex data (position, normal, texcoord, color)
struct VertexData {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
glm::vec4 color;
};
std::vector<VertexData> vertices;
vertices.reserve(group.vertices.size());
for (const auto& v : group.vertices) {
VertexData vd;
vd.position = v.position;
vd.normal = v.normal;
vd.texCoord = v.texCoord;
vd.color = v.color;
vertices.push_back(vd);
}
// Create VAO/VBO/EBO
glGenVertexArrays(1, &resources.vao);
glGenBuffers(1, &resources.vbo);
glGenBuffers(1, &resources.ebo);
glBindVertexArray(resources.vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, resources.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexData),
vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, resources.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, group.indices.size() * sizeof(uint16_t),
group.indices.data(), GL_STATIC_DRAW);
// Vertex attributes
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, position));
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, normal));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, texCoord));
// Color
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, color));
glBindVertexArray(0);
// Store collision geometry for floor raycasting
resources.collisionVertices.reserve(group.vertices.size());
for (const auto& v : group.vertices) {
resources.collisionVertices.push_back(v.position);
}
resources.collisionIndices = group.indices;
// Compute actual bounding box from vertices (WMO header bboxes can be unreliable)
if (!resources.collisionVertices.empty()) {
resources.boundingBoxMin = resources.collisionVertices[0];
resources.boundingBoxMax = resources.collisionVertices[0];
for (const auto& v : resources.collisionVertices) {
resources.boundingBoxMin = glm::min(resources.boundingBoxMin, v);
resources.boundingBoxMax = glm::max(resources.boundingBoxMax, v);
}
}
// Create batches
if (!group.batches.empty()) {
for (const auto& batch : group.batches) {
GroupResources::Batch resBatch;
resBatch.startIndex = batch.startIndex;
resBatch.indexCount = batch.indexCount;
resBatch.materialId = batch.materialId;
resources.batches.push_back(resBatch);
}
} else {
// No batches defined - render entire group as one batch
GroupResources::Batch batch;
batch.startIndex = 0;
batch.indexCount = resources.indexCount;
batch.materialId = 0;
resources.batches.push_back(batch);
}
return true;
}
void WMORenderer::renderGroup(const GroupResources& group, const ModelData& model,
[[maybe_unused]] const glm::mat4& modelMatrix,
[[maybe_unused]] const glm::mat4& view,
[[maybe_unused]] const glm::mat4& projection) {
glBindVertexArray(group.vao);
static int debugLogCount = 0;
// Render each batch
for (const auto& batch : group.batches) {
// Bind texture for this batch's material
// materialId -> materialTextureIndices[materialId] -> textures[texIndex]
GLuint texId = whiteTexture;
bool hasTexture = false;
if (batch.materialId < model.materialTextureIndices.size()) {
uint32_t texIndex = model.materialTextureIndices[batch.materialId];
if (texIndex < model.textures.size()) {
texId = model.textures[texIndex];
hasTexture = (texId != 0 && texId != whiteTexture);
if (debugLogCount < 10) {
core::Logger::getInstance().debug(" Batch: materialId=", (int)batch.materialId,
" -> texIndex=", texIndex, " -> texId=", texId, " hasTexture=", hasTexture);
debugLogCount++;
}
}
}
// Determine if this material uses alpha-test cutout (blendMode 1)
bool alphaTest = false;
if (batch.materialId < model.materialBlendModes.size()) {
alphaTest = (model.materialBlendModes[batch.materialId] == 1);
}
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
shader->setUniform("uTexture", 0);
shader->setUniform("uHasTexture", hasTexture);
shader->setUniform("uAlphaTest", alphaTest);
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
(void*)(batch.startIndex * sizeof(uint16_t)));
lastDrawCalls++;
}
glBindVertexArray(0);
}
bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix,
const Camera& camera) const {
// Simple frustum culling using bounding box
// Transform bounding box corners to world space
glm::vec3 corners[8] = {
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMax.z)
};
// Transform corners to world space
for (int i = 0; i < 8; i++) {
glm::vec4 worldPos = modelMatrix * glm::vec4(corners[i], 1.0f);
corners[i] = glm::vec3(worldPos);
}
// Simple check: if all corners are behind camera, cull
// (This is a very basic culling implementation - a full frustum test would be better)
glm::vec3 forward = camera.getForward();
glm::vec3 camPos = camera.getPosition();
int behindCount = 0;
for (int i = 0; i < 8; i++) {
glm::vec3 toCorner = corners[i] - camPos;
if (glm::dot(toCorner, forward) < 0.0f) {
behindCount++;
}
}
// If all corners are behind camera, cull
return behindCount < 8;
}
void WMORenderer::WMOInstance::updateModelMatrix() {
modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, position);
// Apply MODF placement rotation (WoW-to-GL coordinate transform)
// WoW Ry(B)*Rx(A)*Rz(C) becomes GL Rz(B)*Ry(-A)*Rx(-C)
// rotation stored as (-C, -A, B) in radians by caller
// Apply in Z, Y, X order to get Rz(B) * Ry(-A) * Rx(-C)
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
}
GLuint WMORenderer::loadTexture(const std::string& path) {
if (!assetManager) {
return whiteTexture;
}
// Check cache first
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Upload texture data (BLP loader outputs RGBA8)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
// Set texture parameters with mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glBindTexture(GL_TEXTURE_2D, 0);
// Cache it
textureCache[path] = textureID;
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
// Ray-AABB intersection (slab method)
// Returns true if the ray intersects the axis-aligned bounding box
static bool rayIntersectsAABB(const glm::vec3& origin, const glm::vec3& dir,
const glm::vec3& bmin, const glm::vec3& bmax) {
float tmin = -1e30f, tmax = 1e30f;
for (int i = 0; i < 3; i++) {
if (std::abs(dir[i]) < 1e-8f) {
// Ray is parallel to this slab — check if origin is inside
if (origin[i] < bmin[i] || origin[i] > bmax[i]) return false;
} else {
float invD = 1.0f / dir[i];
float t0 = (bmin[i] - origin[i]) * invD;
float t1 = (bmax[i] - origin[i]) * invD;
if (t0 > t1) std::swap(t0, t1);
tmin = std::max(tmin, t0);
tmax = std::min(tmax, t1);
if (tmin > tmax) return false;
}
}
return tmax >= 0.0f; // At least part of the ray is forward
}
// MöllerTrumbore ray-triangle intersection
// Returns distance along ray if hit, or negative if miss
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
const float EPSILON = 1e-6f;
glm::vec3 e1 = v1 - v0;
glm::vec3 e2 = v2 - v0;
glm::vec3 h = glm::cross(dir, e2);
float a = glm::dot(e1, h);
if (a > -EPSILON && a < EPSILON) return -1.0f;
float f = 1.0f / a;
glm::vec3 s = origin - v0;
float u = f * glm::dot(s, h);
if (u < 0.0f || u > 1.0f) return -1.0f;
glm::vec3 q = glm::cross(s, e1);
float v = f * glm::dot(dir, q);
if (v < 0.0f || u + v > 1.0f) return -1.0f;
float t = f * glm::dot(e2, q);
return t > EPSILON ? t : -1.0f;
}
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
std::optional<float> bestFloor;
// World-space ray: from high above, pointing straight down
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
if (it == loadedModels.end()) continue;
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)));
for (const auto& group : model.groups) {
// Quick bounding box check: does the ray intersect this group's AABB?
// Use proper ray-AABB intersection (slab method) which handles rotated rays
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
continue;
}
// Raycast against triangles
const auto& verts = group.collisionVertices;
const auto& indices = group.collisionIndices;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const glm::vec3& v0 = verts[indices[i]];
const glm::vec3& v1 = verts[indices[i + 1]];
const glm::vec3& v2 = verts[indices[i + 2]];
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
if (t > 0.0f) {
// Hit point in local space -> world space
glm::vec3 hitLocal = localOrigin + localDir * t;
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
// Only use floors below or near the query point
if (hitWorld.z <= glZ + 2.0f) {
if (!bestFloor || hitWorld.z > *bestFloor) {
bestFloor = hitWorld.z;
}
}
}
}
}
// (debug logging removed)
}
return bestFloor;
}
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos) const {
adjustedPos = to;
bool blocked = false;
glm::vec3 moveDir = to - from;
float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y));
if (moveDistXY < 0.001f) return false;
// Player collision radius
const float PLAYER_RADIUS = 2.5f;
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
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));
for (const auto& group : model.groups) {
// Quick bounding box check
float margin = PLAYER_RADIUS + 5.0f;
if (localTo.x < group.boundingBoxMin.x - margin || localTo.x > group.boundingBoxMax.x + margin ||
localTo.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin ||
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
continue;
}
const auto& verts = group.collisionVertices;
const auto& indices = group.collisionIndices;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const glm::vec3& v0 = verts[indices[i]];
const glm::vec3& v1 = verts[indices[i + 1]];
const glm::vec3& v2 = verts[indices[i + 2]];
// Get triangle normal
glm::vec3 edge1 = v1 - v0;
glm::vec3 edge2 = v2 - v0;
glm::vec3 normal = glm::cross(edge1, edge2);
float normalLen = glm::length(normal);
if (normalLen < 0.001f) continue;
normal /= normalLen;
// Skip mostly-horizontal triangles (floors/ceilings)
if (std::abs(normal.z) > 0.7f) continue;
// Signed distance from player to triangle plane
float planeDist = glm::dot(localTo - v0, normal);
float absPlaneDist = std::abs(planeDist);
if (absPlaneDist > PLAYER_RADIUS) continue;
// Project point onto plane
glm::vec3 projected = localTo - normal * planeDist;
// Check if projected point is inside triangle using same-side test
// Use edge cross products and check they all point same direction as normal
float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal);
float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal);
float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal);
// Also check nearby: if projected point is close to a triangle edge
bool insideTriangle = (d0 >= 0.0f && d1 >= 0.0f && d2 >= 0.0f);
if (insideTriangle) {
// Push player away from wall
float pushDist = PLAYER_RADIUS - absPlaneDist;
if (pushDist > 0.0f) {
// Push in the direction the player is on (sign of planeDist)
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
glm::vec3 pushLocal = normal * sign * pushDist;
// Transform push vector back to world space (direction, not point)
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
// Only apply horizontal push (don't push vertically)
adjustedPos.x += pushWorld.x;
adjustedPos.y += pushWorld.y;
blocked = true;
}
}
}
}
}
return blocked;
}
bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId) const {
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
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));
// Check if inside any group's bounding box
for (const auto& group : model.groups) {
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
if (outModelId) *outModelId = instance.modelId;
return true;
}
}
}
return false;
}
} // namespace rendering
} // namespace wowee