Kelsidavis-WoWee/src/rendering/renderer.cpp
Kelsi ebf349ec7c Add shadow toggle (F4) and distance-based WMO group culling
Shadow toggle allows disabling shadow pass for performance testing.
Distance culling skips WMO groups beyond 200 units. Occlusion queries
disabled by default as overhead outweighs benefits in dense scenes.
2026-02-05 16:11:24 -08:00

1919 lines
69 KiB
C++

#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 "rendering/shader.hpp"
#include "pipeline/m2_loader.hpp"
#include <algorithm>
#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 "audio/footstep_manager.hpp"
#include "audio/activity_sound_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 <cmath>
#include <chrono>
#include <optional>
#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->setUseWoWSpeed(true); // Use realistic WoW movement speed
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>();
footstepManager = std::make_unique<audio::FootstepManager>();
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
// Underwater full-screen tint overlay (applies to all world geometry).
underwaterOverlayShader = std::make_unique<Shader>();
const char* overlayVS = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
const char* overlayFS = R"(
#version 330 core
uniform vec4 uTint;
out vec4 FragColor;
void main() { FragColor = uTint; }
)";
if (!underwaterOverlayShader->loadFromSource(overlayVS, overlayFS)) {
LOG_WARNING("Failed to initialize underwater overlay shader");
underwaterOverlayShader.reset();
} else {
const float quadVerts[] = {
-1.0f, -1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, 1.0f
};
glGenVertexArrays(1, &underwaterOverlayVAO);
glGenBuffers(1, &underwaterOverlayVBO);
glBindVertexArray(underwaterOverlayVAO);
glBindBuffer(GL_ARRAY_BUFFER, underwaterOverlayVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
}
// Initialize post-process FBO pipeline
initPostProcess(window->getWidth(), window->getHeight());
// Initialize shadow map
initShadowMap();
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();
}
if (footstepManager) {
footstepManager->shutdown();
footstepManager.reset();
}
if (activitySoundManager) {
activitySoundManager->shutdown();
activitySoundManager.reset();
}
if (underwaterOverlayVAO) {
glDeleteVertexArrays(1, &underwaterOverlayVAO);
underwaterOverlayVAO = 0;
}
if (underwaterOverlayVBO) {
glDeleteBuffers(1, &underwaterOverlayVBO);
underwaterOverlayVBO = 0;
}
underwaterOverlayShader.reset();
// Cleanup shadow map resources
if (shadowFBO) { glDeleteFramebuffers(1, &shadowFBO); shadowFBO = 0; }
if (shadowDepthTex) { glDeleteTextures(1, &shadowDepthTex); shadowDepthTex = 0; }
if (shadowShaderProgram) { glDeleteProgram(shadowShaderProgram); shadowShaderProgram = 0; }
shutdownPostProcess();
zoneManager.reset();
performanceHUD.reset();
scene.reset();
cameraController.reset();
camera.reset();
LOG_INFO("Renderer shutdown");
}
void Renderer::beginFrame() {
// Resize post-process FBO if window size changed
int w = window->getWidth();
int h = window->getHeight();
if (w != fbWidth || h != fbHeight) {
resizePostProcess(w, h);
}
// Clear default framebuffer (login screen renders here directly)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
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);
}
}
uint32_t Renderer::resolveMeleeAnimId() {
if (!characterRenderer || characterInstanceId == 0) {
meleeAnimId = 0;
meleeAnimDurationMs = 0.0f;
return 0;
}
if (meleeAnimId != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId)) {
return meleeAnimId;
}
std::vector<pipeline::M2Sequence> sequences;
if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) {
meleeAnimId = 0;
meleeAnimDurationMs = 0.0f;
return 0;
}
auto findDuration = [&](uint32_t id) -> float {
for (const auto& seq : sequences) {
if (seq.id == id && seq.duration > 0) {
return static_cast<float>(seq.duration);
}
}
return 0.0f;
};
const uint32_t attackCandidates[] = {16, 17, 18, 19, 20, 21};
for (uint32_t id : attackCandidates) {
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
meleeAnimId = id;
meleeAnimDurationMs = findDuration(id);
return meleeAnimId;
}
}
const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97};
auto isAvoid = [&](uint32_t id) -> bool {
for (uint32_t avoid : avoidIds) {
if (id == avoid) return true;
}
return false;
};
uint32_t bestId = 0;
uint32_t bestDuration = 0;
for (const auto& seq : sequences) {
if (seq.duration == 0) continue;
if (isAvoid(seq.id)) continue;
if (seq.movingSpeed > 0.1f) continue;
if (seq.duration < 150 || seq.duration > 2000) continue;
if (bestId == 0 || seq.duration < bestDuration) {
bestId = seq.id;
bestDuration = seq.duration;
}
}
if (bestId == 0) {
for (const auto& seq : sequences) {
if (seq.duration == 0) continue;
if (isAvoid(seq.id)) continue;
if (bestId == 0 || seq.duration < bestDuration) {
bestId = seq.id;
bestDuration = seq.duration;
}
}
}
meleeAnimId = bestId;
meleeAnimDurationMs = static_cast<float>(bestDuration);
return meleeAnimId;
}
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;
// Candidate locomotion clips by common WotLK IDs.
constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92;
constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93;
constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11;
constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12;
constexpr uint32_t ANIM_BACKPEDAL = 13;
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 movingBackward = cameraController->isMovingBackward();
bool strafeLeft = cameraController->isStrafingLeft();
bool strafeRight = cameraController->isStrafingRight();
bool anyStrafeLeft = strafeLeft && !strafeRight;
bool anyStrafeRight = strafeRight && !strafeLeft;
bool grounded = cameraController->isGrounded();
bool jumping = cameraController->isJumping();
bool sprinting = cameraController->isSprinting();
bool sitting = cameraController->isSitting();
bool swim = cameraController->isSwimming();
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
if (!forceMelee) 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;
case CharAnimState::MELEE_SWING:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} 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;
} else if (sitting) {
newState = CharAnimState::SIT_DOWN;
} else {
newState = CharAnimState::IDLE;
}
break;
}
if (forceMelee) {
newState = CharAnimState::MELEE_SWING;
}
if (newState != charAnimState) {
charAnimState = newState;
}
auto pickFirstAvailable = [&](std::initializer_list<uint32_t> candidates, uint32_t fallback) -> uint32_t {
for (uint32_t id : candidates) {
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
return id;
}
}
return fallback;
};
uint32_t animId = ANIM_STAND;
bool loop = true;
switch (charAnimState) {
case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break;
case CharAnimState::WALK:
if (movingBackward) {
animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK);
} else if (anyStrafeLeft) {
animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK);
} else if (anyStrafeRight) {
animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK);
} else {
animId = ANIM_WALK;
}
loop = true;
break;
case CharAnimState::RUN:
if (movingBackward) {
animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK);
} else if (anyStrafeLeft) {
animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN);
} else if (anyStrafeRight) {
animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN);
} else {
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;
case CharAnimState::MELEE_SWING:
animId = resolveMeleeAnimId();
if (animId == 0) {
animId = ANIM_STAND;
}
loop = false;
break;
}
uint32_t currentAnimId = 0;
float currentAnimTimeMs = 0.0f;
float currentAnimDurationMs = 0.0f;
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
if (!haveState || currentAnimId != animId) {
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;
}
void Renderer::triggerMeleeSwing() {
if (!characterRenderer || characterInstanceId == 0) return;
if (meleeSwingCooldown > 0.0f) return;
if (emoteActive) {
cancelEmote();
}
resolveMeleeAnimId();
meleeSwingCooldown = 0.1f;
float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f;
if (durationSec < 0.25f) durationSec = 0.25f;
if (durationSec > 1.0f) durationSec = 1.0f;
meleeSwingTimer = durationSec;
if (activitySoundManager) {
activitySoundManager->playMeleeSwing();
}
}
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();
}
bool Renderer::isFootstepAnimationState() const {
return charAnimState == CharAnimState::WALK || charAnimState == CharAnimState::RUN;
}
bool Renderer::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) {
if (animationDurationMs <= 1.0f) {
footstepNormInitialized = false;
return false;
}
float norm = std::fmod(animationTimeMs, animationDurationMs) / animationDurationMs;
if (norm < 0.0f) norm += 1.0f;
if (animationId != footstepLastAnimationId) {
footstepLastAnimationId = animationId;
footstepLastNormTime = norm;
footstepNormInitialized = true;
return false;
}
if (!footstepNormInitialized) {
footstepNormInitialized = true;
footstepLastNormTime = norm;
return false;
}
auto crossed = [&](float eventNorm) {
if (footstepLastNormTime <= norm) {
return footstepLastNormTime < eventNorm && eventNorm <= norm;
}
return footstepLastNormTime < eventNorm || eventNorm <= norm;
};
bool trigger = crossed(0.22f) || crossed(0.72f);
footstepLastNormTime = norm;
return trigger;
}
audio::FootstepSurface Renderer::resolveFootstepSurface() const {
if (!cameraController || !cameraController->isThirdPerson()) {
return audio::FootstepSurface::STONE;
}
const glm::vec3& p = characterPosition;
if (cameraController->isSwimming()) {
return audio::FootstepSurface::WATER;
}
if (waterRenderer) {
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
if (waterH && p.z < (*waterH + 0.25f)) {
return audio::FootstepSurface::WATER;
}
}
if (wmoRenderer) {
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
return audio::FootstepSurface::STONE;
}
}
if (terrainManager) {
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
if (texture) {
std::string t = *texture;
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) return audio::FootstepSurface::SNOW;
if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) return audio::FootstepSurface::GRASS;
if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) return audio::FootstepSurface::DIRT;
if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) return audio::FootstepSurface::WOOD;
if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) return audio::FootstepSurface::METAL;
if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) return audio::FootstepSurface::STONE;
}
}
return audio::FootstepSurface::STONE;
}
void Renderer::update(float deltaTime) {
auto updateStart = std::chrono::steady_clock::now();
if (wmoRenderer) wmoRenderer->resetQueryStats();
if (m2Renderer) m2Renderer->resetQueryStats();
if (cameraController) {
auto cameraStart = std::chrono::steady_clock::now();
cameraController->update(deltaTime);
auto cameraEnd = std::chrono::steady_clock::now();
lastCameraUpdateMs = std::chrono::duration<double, std::milli>(cameraEnd - cameraStart).count();
} else {
lastCameraUpdateMs = 0.0;
}
// Sync character model position/rotation and animation with follow target
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
if (meleeSwingCooldown > 0.0f) {
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
}
if (meleeSwingTimer > 0.0f) {
meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime);
}
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
if (activitySoundManager) {
std::string modelName;
if (characterRenderer->getInstanceModelName(characterInstanceId, modelName)) {
activitySoundManager->setCharacterVoiceProfile(modelName);
}
}
// Movement-facing comes from camera controller and is decoupled from LMB orbit.
if (cameraController->isMoving() || cameraController->isRightMouseHeld()) {
characterYaw = cameraController->getFacingYaw();
} 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);
}
// Footsteps: animation-event driven + surface query at event time.
if (footstepManager) {
footstepManager->update(deltaTime);
if (characterRenderer && characterInstanceId > 0 &&
cameraController && cameraController->isThirdPerson() &&
isFootstepAnimationState() && cameraController->isGrounded() &&
!cameraController->isSwimming()) {
uint32_t animId = 0;
float animTimeMs = 0.0f;
float animDurationMs = 0.0f;
if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) &&
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting());
}
} else {
footstepNormInitialized = false;
}
}
// Activity SFX: animation/state-driven jump, landing, and swim loops/splashes.
if (activitySoundManager) {
activitySoundManager->update(deltaTime);
if (cameraController && cameraController->isThirdPerson()) {
bool grounded = cameraController->isGrounded();
bool jumping = cameraController->isJumping();
bool falling = cameraController->isFalling();
bool swimming = cameraController->isSwimming();
bool moving = cameraController->isMoving();
if (!sfxStateInitialized) {
sfxPrevGrounded = grounded;
sfxPrevJumping = jumping;
sfxPrevFalling = falling;
sfxPrevSwimming = swimming;
sfxStateInitialized = true;
}
if (jumping && !sfxPrevJumping && !swimming) {
activitySoundManager->playJump();
}
if (grounded && !sfxPrevGrounded) {
bool hardLanding = sfxPrevFalling;
activitySoundManager->playLanding(resolveFootstepSurface(), hardLanding);
}
if (swimming && !sfxPrevSwimming) {
activitySoundManager->playWaterEnter();
} else if (!swimming && sfxPrevSwimming) {
activitySoundManager->playWaterExit();
}
activitySoundManager->setSwimmingState(swimming, moving);
sfxPrevGrounded = grounded;
sfxPrevJumping = jumping;
sfxPrevFalling = falling;
sfxPrevSwimming = swimming;
} else {
activitySoundManager->setSwimmingState(false, false);
sfxStateInitialized = false;
}
}
// Update M2 doodad animations
if (m2Renderer) {
m2Renderer->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);
}
auto updateEnd = std::chrono::steady_clock::now();
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
}
void Renderer::renderWorld(game::World* world) {
auto renderStart = std::chrono::steady_clock::now();
lastTerrainRenderMs = 0.0;
lastWMORenderMs = 0.0;
lastM2RenderMs = 0.0;
// Shadow pass (before main scene)
if (shadowsEnabled && shadowFBO && shadowShaderProgram && terrainLoaded) {
renderShadowPass();
} else {
// Clear shadow maps when disabled
if (terrainRenderer) terrainRenderer->clearShadowMap();
if (wmoRenderer) wmoRenderer->clearShadowMap();
if (m2Renderer) m2Renderer->clearShadowMap();
if (characterRenderer) characterRenderer->clearShadowMap();
}
// Bind HDR scene framebuffer for world rendering
glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO);
glViewport(0, 0, fbWidth, fbHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
(void)world; // Unused for now
// Get time of day for sky-related rendering
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
bool underwater = false;
bool canalUnderwater = false;
// 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);
}
// Update fog across all renderers based on time of day (match sky color)
if (skybox) {
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
if (wmoRenderer) wmoRenderer->setFog(horizonColor, 100.0f, 600.0f);
if (m2Renderer) m2Renderer->setFog(horizonColor, 100.0f, 600.0f);
if (characterRenderer) characterRenderer->setFog(horizonColor, 100.0f, 600.0f);
}
// Render terrain if loaded and enabled
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
// Check if camera/character is underwater for fog override
if (cameraController && cameraController->isSwimming() && waterRenderer && camera) {
glm::vec3 camPos = camera->getPosition();
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
constexpr float MAX_UNDERWATER_DEPTH = 12.0f;
// Require camera to be meaningfully below the surface before
// underwater fog/tint kicks in (avoids "wrong plane" near surface).
constexpr float UNDERWATER_ENTER_EPS = 1.10f;
if (waterH &&
camPos.z < (*waterH - UNDERWATER_ENTER_EPS) &&
(*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) {
underwater = true;
}
}
if (underwater) {
glm::vec3 camPos = camera->getPosition();
std::optional<uint16_t> liquidType = waterRenderer ? waterRenderer->getWaterTypeAt(camPos.x, camPos.y) : std::nullopt;
if (!liquidType && cameraController) {
const glm::vec3* followTarget = cameraController->getFollowTarget();
if (followTarget && waterRenderer) {
liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y);
}
}
canalUnderwater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17);
}
if (skybox) {
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
}
auto terrainStart = std::chrono::steady_clock::now();
terrainRenderer->render(*camera);
auto terrainEnd = std::chrono::steady_clock::now();
lastTerrainRenderMs = std::chrono::duration<double, std::milli>(terrainEnd - terrainStart).count();
}
// 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);
}
// Compute view/projection once for all sub-renderers
const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f);
const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f);
// Render characters (after weather)
if (characterRenderer && camera) {
characterRenderer->render(*camera, view, projection);
}
// Render WMO buildings (after characters, before UI)
if (wmoRenderer && camera) {
auto wmoStart = std::chrono::steady_clock::now();
wmoRenderer->render(*camera, view, projection);
auto wmoEnd = std::chrono::steady_clock::now();
lastWMORenderMs = std::chrono::duration<double, std::milli>(wmoEnd - wmoStart).count();
}
// Render M2 doodads (trees, rocks, etc.)
if (m2Renderer && camera) {
auto m2Start = std::chrono::steady_clock::now();
m2Renderer->render(*camera, view, projection);
m2Renderer->renderSmokeParticles(*camera, view, projection);
auto m2End = std::chrono::steady_clock::now();
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
}
// Render water after opaque terrain/WMO/M2 so transparent surfaces remain visible.
if (waterRenderer && camera) {
static float time = 0.0f;
time += 0.016f; // Approximate frame time
waterRenderer->render(*camera, time);
}
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
underwaterOverlayShader->use();
if (canalUnderwater) {
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.01f, 0.05f, 0.11f, 0.50f));
} else {
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.02f, 0.08f, 0.15f, 0.30f));
}
glBindVertexArray(underwaterOverlayVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
// --- Resolve MSAA → non-MSAA texture ---
glBindFramebuffer(GL_READ_FRAMEBUFFER, sceneFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolveFBO);
glBlitFramebuffer(0, 0, fbWidth, fbHeight, 0, 0, fbWidth, fbHeight,
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);
// --- Post-process: tonemap via fullscreen quad ---
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, window->getWidth(), window->getHeight());
glDisable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT);
if (postProcessShader && screenQuadVAO) {
postProcessShader->use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
postProcessShader->setUniform("uScene", 0);
glBindVertexArray(screenQuadVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
postProcessShader->unuse();
}
// Render minimap overlay (after post-process so it's not overwritten)
if (minimap && camera && window) {
glm::vec3 minimapCenter = camera->getPosition();
if (cameraController && cameraController->isThirdPerson()) {
minimapCenter = characterPosition;
}
minimap->render(*camera, minimapCenter, window->getWidth(), window->getHeight());
}
glEnable(GL_DEPTH_TEST);
auto renderEnd = std::chrono::steady_clock::now();
lastRenderMs = std::chrono::duration<double, std::milli>(renderEnd - renderStart).count();
}
// ──────────────────────────────────────────────────────
// Post-process FBO helpers
// ──────────────────────────────────────────────────────
void Renderer::initPostProcess(int w, int h) {
fbWidth = w;
fbHeight = h;
constexpr int SAMPLES = 4;
// --- MSAA FBO (render target) ---
glGenRenderbuffers(1, &sceneColorRBO);
glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h);
glGenRenderbuffers(1, &sceneDepthRBO);
glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h);
glGenFramebuffers(1, &sceneFBO);
glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sceneColorRBO);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sceneDepthRBO);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("MSAA scene FBO incomplete!");
}
// --- Resolve FBO (non-MSAA, for post-process sampling) ---
glGenTextures(1, &resolveColorTex);
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, 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);
glGenTextures(1, &resolveDepthTex);
glBindTexture(GL_TEXTURE_2D, resolveDepthTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGenFramebuffers(1, &resolveFBO);
glBindFramebuffer(GL_FRAMEBUFFER, resolveFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolveColorTex, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, resolveDepthTex, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Resolve FBO incomplete!");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// --- Fullscreen quad (triangle strip, pos + UV) ---
const 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, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
};
glGenVertexArrays(1, &screenQuadVAO);
glGenBuffers(1, &screenQuadVBO);
glBindVertexArray(screenQuadVAO);
glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO);
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);
// --- Post-process shader (Reinhard tonemap + gamma 2.2) ---
const char* ppVS = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
out vec2 vUV;
void main() {
vUV = aUV;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
const char* ppFS = R"(
#version 330 core
in vec2 vUV;
uniform sampler2D uScene;
out vec4 FragColor;
void main() {
vec3 color = texture(uScene, vUV).rgb;
// Shoulder tonemap: identity below 0.9, soft rolloff above
vec3 excess = max(color - 0.9, 0.0);
vec3 mapped = min(color, vec3(0.9)) + 0.1 * excess / (excess + 0.1);
FragColor = vec4(mapped, 1.0);
}
)";
postProcessShader = std::make_unique<Shader>();
if (!postProcessShader->loadFromSource(ppVS, ppFS)) {
LOG_ERROR("Failed to compile post-process shader");
postProcessShader.reset();
}
LOG_INFO("Post-process FBO initialized (", w, "x", h, ")");
}
void Renderer::resizePostProcess(int w, int h) {
if (w <= 0 || h <= 0) return;
fbWidth = w;
fbHeight = h;
constexpr int SAMPLES = 4;
// Resize MSAA renderbuffers
glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h);
glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h);
// Resize resolve textures
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr);
glBindTexture(GL_TEXTURE_2D, resolveDepthTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
LOG_INFO("Post-process FBO resized (", w, "x", h, ")");
}
void Renderer::shutdownPostProcess() {
if (sceneFBO) {
glDeleteFramebuffers(1, &sceneFBO);
sceneFBO = 0;
}
if (sceneColorRBO) {
glDeleteRenderbuffers(1, &sceneColorRBO);
sceneColorRBO = 0;
}
if (sceneDepthRBO) {
glDeleteRenderbuffers(1, &sceneDepthRBO);
sceneDepthRBO = 0;
}
if (resolveFBO) {
glDeleteFramebuffers(1, &resolveFBO);
resolveFBO = 0;
}
if (resolveColorTex) {
glDeleteTextures(1, &resolveColorTex);
resolveColorTex = 0;
}
if (resolveDepthTex) {
glDeleteTextures(1, &resolveDepthTex);
resolveDepthTex = 0;
}
if (screenQuadVAO) {
glDeleteVertexArrays(1, &screenQuadVAO);
screenQuadVAO = 0;
}
if (screenQuadVBO) {
glDeleteBuffers(1, &screenQuadVBO);
screenQuadVBO = 0;
}
postProcessShader.reset();
}
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 asset manager to minimap for tile texture loading
if (minimap) {
minimap->setAssetManager(assetManager);
}
// Wire terrain manager, WMO renderer, and water renderer to camera controller
if (cameraController) {
cameraController->setTerrainManager(terrainManager.get());
if (wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (m2Renderer) {
cameraController->setM2Renderer(m2Renderer.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);
if (minimap) {
minimap->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);
if (footstepManager) {
footstepManager->initialize(assetManager);
}
if (activitySoundManager) {
activitySoundManager->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);
}
}
if (footstepManager && cachedAssetManager) {
if (!footstepManager->isInitialized()) {
footstepManager->initialize(cachedAssetManager);
}
}
if (activitySoundManager && cachedAssetManager) {
if (!activitySoundManager->isInitialized()) {
activitySoundManager->initialize(cachedAssetManager);
}
}
// Wire WMO, M2, and water renderer to camera controller
if (cameraController && wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (cameraController && m2Renderer) {
cameraController->setM2Renderer(m2Renderer.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());
}
}
// ──────────────────────────────────────────────────────
// Shadow mapping helpers
// ──────────────────────────────────────────────────────
void Renderer::initShadowMap() {
// Compile shadow shader
shadowShaderProgram = compileShadowShader();
if (!shadowShaderProgram) {
LOG_ERROR("Failed to compile shadow shader");
return;
}
// Create depth texture
glGenTextures(1, &shadowDepthTex);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24,
SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, 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_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glBindTexture(GL_TEXTURE_2D, 0);
// Create depth-only FBO
glGenFramebuffers(1, &shadowFBO);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTex, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Shadow FBO incomplete!");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
LOG_INFO("Shadow map initialized (", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
}
uint32_t Renderer::compileShadowShader() {
const char* vertSrc = R"(
#version 330 core
uniform mat4 uLightSpaceMatrix;
uniform mat4 uModel;
layout(location = 0) in vec3 aPos;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec4 aBoneWeights;
layout(location = 4) in vec4 aBoneIndicesF;
uniform bool uUseBones;
uniform mat4 uBones[200];
out vec2 vTexCoord;
void main() {
vec3 pos = aPos;
if (uUseBones) {
ivec4 bi = ivec4(aBoneIndicesF);
mat4 boneTransform = uBones[bi.x] * aBoneWeights.x
+ uBones[bi.y] * aBoneWeights.y
+ uBones[bi.z] * aBoneWeights.z
+ uBones[bi.w] * aBoneWeights.w;
pos = vec3(boneTransform * vec4(aPos, 1.0));
}
vTexCoord = aTexCoord;
gl_Position = uLightSpaceMatrix * uModel * vec4(pos, 1.0);
}
)";
const char* fragSrc = R"(
#version 330 core
in vec2 vTexCoord;
uniform bool uUseTexture;
uniform sampler2D uTexture;
uniform bool uAlphaTest;
uniform float uShadowOpacity;
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
float opacity = clamp(uShadowOpacity, 0.0, 1.0);
if (uUseTexture) {
vec4 tex = texture(uTexture, vTexCoord);
if (uAlphaTest && tex.a < 0.5) discard;
opacity *= tex.a;
}
// Stochastic alpha for soft/translucent shadow casters (foliage).
// Use UV-space hash so pattern stays stable with camera movement.
if (opacity < 0.999) {
float d = hash12(floor(vTexCoord * 4096.0));
if (d > opacity) discard;
}
}
)";
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vertSrc, nullptr);
glCompileShader(vs);
GLint success;
glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
if (!success) {
char log[512];
glGetShaderInfoLog(vs, 512, nullptr, log);
LOG_ERROR("Shadow vertex shader error: ", log);
glDeleteShader(vs);
return 0;
}
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fragSrc, nullptr);
glCompileShader(fs);
glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
if (!success) {
char log[512];
glGetShaderInfoLog(fs, 512, nullptr, log);
LOG_ERROR("Shadow fragment shader error: ", log);
glDeleteShader(vs);
glDeleteShader(fs);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
char log[512];
glGetProgramInfoLog(program, 512, nullptr, log);
LOG_ERROR("Shadow shader link error: ", log);
glDeleteProgram(program);
program = 0;
}
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
glm::mat4 Renderer::computeLightSpaceMatrix() {
constexpr float kShadowHalfExtent = 180.0f;
constexpr float kShadowLightDistance = 280.0f;
constexpr float kShadowNearPlane = 1.0f;
constexpr float kShadowFarPlane = 600.0f;
// Sun direction matching WMO light dir
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
// Keep a stable shadow focus center and only recentre occasionally.
glm::vec3 desiredCenter = characterPosition;
if (!shadowCenterInitialized) {
shadowCenter = desiredCenter;
shadowCenterInitialized = true;
} else {
constexpr float recenterThreshold = 30.0f; // world units
if (std::abs(desiredCenter.x - shadowCenter.x) > recenterThreshold ||
std::abs(desiredCenter.y - shadowCenter.y) > recenterThreshold) {
shadowCenter.x = desiredCenter.x;
shadowCenter.y = desiredCenter.y;
}
// Avoid vertical jitter from tiny terrain/camera height changes.
if (std::abs(desiredCenter.z - shadowCenter.z) > 4.0f) {
shadowCenter.z = desiredCenter.z;
}
}
glm::vec3 center = shadowCenter;
// Texel snapping: round center to shadow texel boundaries to prevent shimmer
float halfExtent = kShadowHalfExtent;
float texelWorld = (2.0f * halfExtent) / static_cast<float>(SHADOW_MAP_SIZE);
// Build light view to get stable axes
glm::vec3 up(0.0f, 0.0f, 1.0f);
// If sunDir is nearly parallel to up, pick a different up vector
if (std::abs(glm::dot(sunDir, up)) > 0.99f) {
up = glm::vec3(0.0f, 1.0f, 0.0f);
}
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
// Snap center in light space to texel grid
glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f);
centerLS.x = std::round(centerLS.x / texelWorld) * texelWorld;
centerLS.y = std::round(centerLS.y / texelWorld) * texelWorld;
glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS;
center = glm::vec3(snappedCenter);
shadowCenter = center;
// Rebuild with snapped center
lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
kShadowNearPlane, kShadowFarPlane);
return lightProj * lightView;
}
void Renderer::renderShadowPass() {
constexpr float kShadowHalfExtent = 180.0f;
constexpr float kShadowLightDistance = 280.0f;
constexpr float kShadowNearPlane = 1.0f;
constexpr float kShadowFarPlane = 600.0f;
// Compute light space matrix
lightSpaceMatrix = computeLightSpaceMatrix();
// Bind shadow FBO
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glViewport(0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
glClear(GL_DEPTH_BUFFER_BIT);
// Caster-side bias: front-face culling + polygon offset
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.0f, 4.0f);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
// Use shadow shader
glUseProgram(shadowShaderProgram);
GLint lsmLoc = glGetUniformLocation(shadowShaderProgram, "uLightSpaceMatrix");
glUniformMatrix4fv(lsmLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]);
GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture");
GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture");
GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest");
GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity");
GLint useBonesLoc = glGetUniformLocation(shadowShaderProgram, "uUseBones");
if (useTexLoc >= 0) glUniform1i(useTexLoc, 0);
if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0);
if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f);
if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0);
if (texLoc >= 0) glUniform1i(texLoc, 0);
// Render terrain into shadow map
if (terrainRenderer) {
terrainRenderer->renderShadow(shadowShaderProgram);
}
// Render WMO into shadow map
if (wmoRenderer) {
// WMO renderShadow takes separate view/proj matrices and a Shader ref.
// We need to decompose our lightSpaceMatrix or use the raw shader program.
// Since WMO::renderShadow sets uModel per instance, we use the shadow shader
// directly by calling renderShadow with the light view/proj split.
// For simplicity, compute the split:
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition;
float halfExtent = kShadowHalfExtent;
glm::vec3 up(0.0f, 0.0f, 1.0f);
if (std::abs(glm::dot(sunDir, up)) > 0.99f) up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
kShadowNearPlane, kShadowFarPlane);
// WMO renderShadow needs a Shader reference — but it only uses setUniform("uModel", ...)
// We'll create a thin wrapper. Actually, WMO's renderShadow takes a Shader& and calls
// shadowShader.setUniform("uModel", ...). We need a Shader object wrapping our program.
// Instead, let's use the lower-level approach: WMO renderShadow uses the shader passed in.
// We need to temporarily wrap our GL program in a Shader object.
Shader shadowShaderWrapper;
shadowShaderWrapper.setProgram(shadowShaderProgram);
wmoRenderer->renderShadow(lightView, lightProj, shadowShaderWrapper);
shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program
}
// Render M2 doodads into shadow map
if (m2Renderer) {
m2Renderer->renderShadow(shadowShaderProgram);
}
// Render characters into shadow map
if (characterRenderer) {
// Character shadows need less caster bias to avoid "floating" away from feet.
glDisable(GL_POLYGON_OFFSET_FILL);
glCullFace(GL_BACK);
characterRenderer->renderShadow(lightSpaceMatrix);
glCullFace(GL_FRONT);
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.0f, 4.0f);
}
// Restore state
glDisable(GL_POLYGON_OFFSET_FILL);
glCullFace(GL_BACK);
// Restore main viewport
glViewport(0, 0, fbWidth, fbHeight);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Distribute shadow map to all receivers
if (terrainRenderer) terrainRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (wmoRenderer) wmoRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (m2Renderer) m2Renderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
if (characterRenderer) characterRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
}
} // namespace rendering
} // namespace wowee