#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 #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 #include #include #include #include #include #include #include #include #include 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 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->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(camera.get()); cameraController->setUseWoWSpeed(true); // Use realistic WoW movement speed cameraController->setMouseSensitivity(0.15f); // Create scene scene = std::make_unique(); // Create performance HUD performanceHUD = std::make_unique(); performanceHUD->setPosition(PerformanceHUD::Position::TOP_LEFT); // Create water renderer waterRenderer = std::make_unique(); if (!waterRenderer->initialize()) { LOG_WARNING("Failed to initialize water renderer"); waterRenderer.reset(); } // Create skybox skybox = std::make_unique(); 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(); if (!celestial->initialize()) { LOG_WARNING("Failed to initialize celestial renderer"); celestial.reset(); } // Create star field starField = std::make_unique(); if (!starField->initialize()) { LOG_WARNING("Failed to initialize star field"); starField.reset(); } // Create clouds clouds = std::make_unique(); 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(); if (!lensFlare->initialize()) { LOG_WARNING("Failed to initialize lens flare"); lensFlare.reset(); } // Create weather system weather = std::make_unique(); if (!weather->initialize()) { LOG_WARNING("Failed to initialize weather"); weather.reset(); } // Create swim effects swimEffects = std::make_unique(); if (!swimEffects->initialize()) { LOG_WARNING("Failed to initialize swim effects"); swimEffects.reset(); } // Create character renderer characterRenderer = std::make_unique(); if (!characterRenderer->initialize()) { LOG_WARNING("Failed to initialize character renderer"); characterRenderer.reset(); } // Create WMO renderer wmoRenderer = std::make_unique(); if (!wmoRenderer->initialize()) { LOG_WARNING("Failed to initialize WMO renderer"); wmoRenderer.reset(); } // Create minimap minimap = std::make_unique(); if (!minimap->initialize(200)) { LOG_WARNING("Failed to initialize minimap"); minimap.reset(); } // Create M2 renderer (for doodads) m2Renderer = std::make_unique(); // Note: M2 renderer needs asset manager, will be initialized when terrain loads // Create zone manager zoneManager = std::make_unique(); zoneManager->initialize(); // Create music manager (initialized later with asset manager) musicManager = std::make_unique(); footstepManager = std::make_unique(); activitySoundManager = std::make_unique(); // Underwater full-screen tint overlay (applies to all world geometry). underwaterOverlayShader = std::make_unique(); 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); } } void Renderer::setMounted(uint32_t mountInstId, float heightOffset) { mountInstanceId_ = mountInstId; mountHeightOffset_ = heightOffset; charAnimState = CharAnimState::MOUNT; if (cameraController) cameraController->setMounted(true); } void Renderer::clearMount() { mountInstanceId_ = 0; mountHeightOffset_ = 0.0f; charAnimState = CharAnimState::IDLE; if (cameraController) cameraController->setMounted(false); } 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 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(seq.duration); } } return 0.0f; }; // Select animation priority based on equipped weapon type // WoW inventory types: 17 = 2H weapon, 13/21 = 1H, 0 = unarmed // WoW anim IDs: 16 = unarmed, 17 = 1H attack, 18 = 2H attack const uint32_t* attackCandidates; size_t candidateCount; static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; if (equippedWeaponInvType_ == 17) { // INVTYPE_2HWEAPON attackCandidates = candidates2H; candidateCount = 6; } else if (equippedWeaponInvType_ == 0) { attackCandidates = candidatesUnarmed; candidateCount = 6; } else { attackCandidates = candidates1H; candidateCount = 6; } for (size_t ci = 0; ci < candidateCount; ci++) { uint32_t id = attackCandidates[ci]; 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(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) constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount 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; // When mounted, force MOUNT state and skip normal transitions if (isMounted()) { newState = CharAnimState::MOUNT; charAnimState = newState; // Play seated animation on player uint32_t currentAnimId = 0; float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); if (!haveState || currentAnimId != ANIM_MOUNT) { characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); } // Sync mount instance position and rotation if (mountInstanceId_ > 0) { characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); float yawRad = glm::radians(characterYaw); characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(0.0f, 0.0f, yawRad)); // Drive mount model animation: idle when still, run when moving uint32_t mountAnimId = moving ? ANIM_RUN : ANIM_STAND; uint32_t curMountAnim = 0; float curMountTime = 0, curMountDur = 0; bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); if (!haveMountState || curMountAnim != mountAnimId) { characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); } } // Offset player Z above mount glm::vec3 playerPos = characterPosition; playerPos.z += mountHeightOffset_; characterRenderer->setInstancePosition(characterInstanceId, playerPos); return; } 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; case CharAnimState::MOUNT: break; // Handled by early return above } if (forceMelee) { newState = CharAnimState::MELEE_SWING; } if (newState != charAnimState) { charAnimState = newState; } auto pickFirstAvailable = [&](std::initializer_list 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 = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); } 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 = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); } 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; case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; 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(std::tolower(static_cast(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(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 (inCombat_ && targetPosition && !emoteActive) { // Face target when in combat and 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)); 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 (pass camera for frustum-culling bone computation) if (m2Renderer && camera) { m2Renderer->update(deltaTime, camera->getPosition(), camera->getProjectionMatrix() * camera->getViewMatrix()); } // 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(updateEnd - updateStart).count(); } // ============================================================ // Selection Circle // ============================================================ void Renderer::initSelectionCircle() { if (selCircleVAO) return; // Minimal shader: position + uniform MVP + color const char* vsSrc = R"( #version 330 core layout(location = 0) in vec3 aPos; uniform mat4 uMVP; void main() { gl_Position = uMVP * vec4(aPos, 1.0); } )"; const char* fsSrc = R"( #version 330 core uniform vec3 uColor; out vec4 FragColor; void main() { FragColor = vec4(uColor, 0.6); } )"; auto compile = [](GLenum type, const char* src) -> GLuint { GLuint s = glCreateShader(type); glShaderSource(s, 1, &src, nullptr); glCompileShader(s); return s; }; GLuint vs = compile(GL_VERTEX_SHADER, vsSrc); GLuint fs = compile(GL_FRAGMENT_SHADER, fsSrc); selCircleShader = glCreateProgram(); glAttachShader(selCircleShader, vs); glAttachShader(selCircleShader, fs); glLinkProgram(selCircleShader); glDeleteShader(vs); glDeleteShader(fs); // Build ring vertices (two concentric circles forming a strip) constexpr int SEGMENTS = 48; constexpr float INNER = 0.85f; constexpr float OUTER = 1.0f; std::vector verts; for (int i = 0; i <= SEGMENTS; i++) { float angle = 2.0f * 3.14159265f * static_cast(i) / static_cast(SEGMENTS); float c = std::cos(angle), s = std::sin(angle); // Outer vertex verts.push_back(c * OUTER); verts.push_back(s * OUTER); verts.push_back(0.0f); // Inner vertex verts.push_back(c * INNER); verts.push_back(s * INNER); verts.push_back(0.0f); } selCircleVertCount = static_cast((SEGMENTS + 1) * 2); glGenVertexArrays(1, &selCircleVAO); glGenBuffers(1, &selCircleVBO); glBindVertexArray(selCircleVAO); glBindBuffer(GL_ARRAY_BUFFER, selCircleVBO); glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr); glEnableVertexAttribArray(0); glBindVertexArray(0); } void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) { selCirclePos = pos; selCircleRadius = radius; selCircleColor = color; selCircleVisible = true; } void Renderer::clearSelectionCircle() { selCircleVisible = false; } void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection) { if (!selCircleVisible) return; initSelectionCircle(); // Small Z offset to prevent clipping under terrain glm::vec3 raisedPos = selCirclePos; raisedPos.z += 0.15f; glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos); model = glm::scale(model, glm::vec3(selCircleRadius)); glm::mat4 mvp = projection * view * model; glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_CULL_FACE); glDepthMask(GL_FALSE); glUseProgram(selCircleShader); glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]); glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]); glBindVertexArray(selCircleVAO); glDrawArrays(GL_TRIANGLE_STRIP, 0, selCircleVertCount); glBindVertexArray(0); glDepthMask(GL_TRUE); glEnable(GL_CULL_FACE); } 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 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(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 selection circle under targeted creature renderSelectionCircle(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(wmoEnd - wmoStart).count(); } // Render M2 doodads (trees, rocks, etc.) if (m2Renderer && camera) { // Dim M2 lighting when player is inside a WMO if (cameraController) { m2Renderer->setInsideInterior(cameraController->isInsideWMO()); } auto m2Start = std::chrono::steady_clock::now(); m2Renderer->render(*camera, view, projection); m2Renderer->renderSmokeParticles(*camera, view, projection); m2Renderer->renderM2Particles(view, projection); auto m2End = std::chrono::steady_clock::now(); lastM2RenderMs = std::chrono::duration(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(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(); 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(); 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(); 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("Enqueuing initial tile [", tileX, ",", tileY, "] via terrain manager"); // Enqueue the initial tile for async loading (avoids long sync stalls) if (!terrainManager->enqueueTile(tileX, tileY)) { LOG_ERROR("Failed to enqueue 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(); // 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(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