Kelsidavis-WoWee/src/rendering/renderer.cpp

3245 lines
129 KiB
C++
Raw Normal View History

#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/lighting_manager.hpp"
#include "rendering/sky_system.hpp"
#include "rendering/swim_effects.hpp"
#include "rendering/mount_dust.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/minimap.hpp"
#include "rendering/quest_marker_renderer.hpp"
#include "rendering/shader.hpp"
#include "game/game_handler.hpp"
#include "pipeline/m2_loader.hpp"
#include <algorithm>
#include "pipeline/asset_manager.hpp"
2026-02-07 20:02:14 -08:00
#include "pipeline/dbc_loader.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
2026-02-07 20:02:14 -08:00
#include "core/application.hpp"
#include "core/window.hpp"
#include "core/logger.hpp"
#include "game/world.hpp"
#include "game/zone_manager.hpp"
#include "audio/audio_engine.hpp"
#include "audio/music_manager.hpp"
#include "audio/footstep_manager.hpp"
#include "audio/activity_sound_manager.hpp"
#include "audio/mount_sound_manager.hpp"
#include "audio/npc_voice_manager.hpp"
#include "audio/ambient_sound_manager.hpp"
#include "audio/ui_sound_manager.hpp"
#include "audio/combat_sound_manager.hpp"
#include "audio/spell_sound_manager.hpp"
#include "audio/movement_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>
#include <set>
namespace wowee {
namespace rendering {
struct EmoteInfo {
2026-02-07 20:02:14 -08:00
uint32_t animId = 0;
bool loop = false;
std::string textNoTarget;
std::string textTarget;
std::string command;
};
2026-02-07 20:02:14 -08:00
static std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE;
static bool emoteTableLoaded = false;
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
std::vector<std::string> out;
std::string cur;
for (char c : raw) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
cur.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
} else if (!cur.empty()) {
out.push_back(cur);
cur.clear();
}
}
if (!cur.empty()) out.push_back(cur);
return out;
}
static bool isLoopingEmote(const std::string& command) {
static const std::unordered_set<std::string> kLooping = {
"dance",
"train",
};
return kLooping.find(command) != kLooping.end();
}
static void loadFallbackEmotes() {
if (!EMOTE_TABLE.empty()) return;
EMOTE_TABLE = {
{"wave", {67, false, "You wave.", "You wave at %s.", "wave"}},
{"bow", {66, false, "You bow down graciously.", "You bow down before %s.", "bow"}},
{"laugh", {70, false, "You laugh.", "You laugh at %s.", "laugh"}},
{"point", {84, false, "You point over yonder.", "You point at %s.", "point"}},
{"cheer", {68, false, "You cheer!", "You cheer at %s.", "cheer"}},
{"dance", {69, true, "You burst into dance.", "You dance with %s.", "dance"}},
{"kneel", {75, false, "You kneel down.", "You kneel before %s.", "kneel"}},
{"applaud", {80, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "applaud"}},
{"shout", {81, false, "You shout.", "You shout at %s.", "shout"}},
{"chicken", {78, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
"With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", "chicken"}},
{"cry", {77, false, "You cry.", "You cry on %s's shoulder.", "cry"}},
{"kiss", {76, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "kiss"}},
{"roar", {74, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "roar"}},
{"salute", {113, false, "You salute.", "You salute %s with respect.", "salute"}},
{"rude", {73, false, "You make a rude gesture.", "You make a rude gesture at %s.", "rude"}},
{"flex", {82, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "flex"}},
{"shy", {83, false, "You smile shyly.", "You smile shyly at %s.", "shy"}},
{"beg", {79, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "beg"}},
{"eat", {61, false, "You begin to eat.", "You begin to eat in front of %s.", "eat"}},
};
}
static std::string replacePlaceholders(const std::string& text, const std::string* targetName) {
if (text.empty()) return text;
std::string out;
out.reserve(text.size() + 16);
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') {
if (targetName && !targetName->empty()) out += *targetName;
i++;
} else {
out.push_back(text[i]);
}
}
return out;
}
static void loadEmotesFromDbc() {
if (emoteTableLoaded) return;
emoteTableLoaded = true;
auto* assetManager = core::Application::getInstance().getAssetManager();
if (!assetManager) {
LOG_WARNING("Emotes: no AssetManager");
loadFallbackEmotes();
return;
}
auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc");
auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc");
if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) {
LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)");
loadFallbackEmotes();
return;
}
std::unordered_map<uint32_t, std::string> textData;
textData.reserve(emotesTextDataDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
uint32_t id = emotesTextDataDbc->getUInt32(r, 0);
std::string text = emotesTextDataDbc->getString(r, 1);
if (!text.empty()) textData.emplace(id, std::move(text));
}
std::unordered_map<uint32_t, uint32_t> emoteIdToAnim;
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
uint32_t emoteId = emotesDbc->getUInt32(r, 0);
uint32_t animId = emotesDbc->getUInt32(r, 2);
if (animId != 0) emoteIdToAnim[emoteId] = animId;
}
}
EMOTE_TABLE.clear();
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
std::string cmdRaw = emotesTextDbc->getString(r, 1);
if (cmdRaw.empty()) continue;
uint32_t emoteRef = emotesTextDbc->getUInt32(r, 2);
uint32_t animId = 0;
auto animIt = emoteIdToAnim.find(emoteRef);
if (animIt != emoteIdToAnim.end()) {
animId = animIt->second;
} else {
animId = emoteRef; // fallback if EmotesText stores animation id directly
}
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, 5); // unisex, target, sender
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, 9); // unisex, no target, sender
std::string textTarget;
std::string textNoTarget;
if (auto it = textData.find(senderTargetTextId); it != textData.end()) {
textTarget = it->second;
}
if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) {
textNoTarget = it->second;
}
for (const std::string& cmd : parseEmoteCommands(cmdRaw)) {
if (cmd.empty()) continue;
EmoteInfo info;
info.animId = animId;
info.loop = isLoopingEmote(cmd);
info.textNoTarget = textNoTarget;
info.textTarget = textTarget;
info.command = cmd;
EMOTE_TABLE.emplace(cmd, std::move(info));
}
}
if (EMOTE_TABLE.empty()) {
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
loadFallbackEmotes();
} else {
LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC");
}
}
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 sky system (coordinator for sky rendering)
skySystem = std::make_unique<SkySystem>();
if (!skySystem->initialize()) {
LOG_WARNING("Failed to initialize sky system");
skySystem.reset();
} else {
// Note: SkySystem manages its own components internally
// Keep existing components for backwards compatibility (PerformanceHUD access)
LOG_INFO("Sky system initialized successfully (coordinator active)");
}
// Create weather system
weather = std::make_unique<Weather>();
if (!weather->initialize()) {
LOG_WARNING("Failed to initialize weather");
weather.reset();
}
// Create lighting system
lightingManager = std::make_unique<LightingManager>();
auto* assetManager = core::Application::getInstance().getAssetManager();
if (assetManager && !lightingManager->initialize(assetManager)) {
LOG_WARNING("Failed to initialize lighting manager");
lightingManager.reset();
}
// Create swim effects
swimEffects = std::make_unique<SwimEffects>();
if (!swimEffects->initialize()) {
LOG_WARNING("Failed to initialize swim effects");
swimEffects.reset();
}
// Create mount dust effects
mountDust = std::make_unique<MountDust>();
if (!mountDust->initialize()) {
LOG_WARNING("Failed to initialize mount dust effects");
mountDust.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 quest marker renderer (initialized later with AssetManager)
questMarkerRenderer = std::make_unique<QuestMarkerRenderer>();
// 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();
// Initialize AudioEngine (singleton)
if (!audio::AudioEngine::instance().initialize()) {
LOG_WARNING("Failed to initialize AudioEngine - audio will be disabled");
}
// 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>();
mountSoundManager = std::make_unique<audio::MountSoundManager>();
npcVoiceManager = std::make_unique<audio::NpcVoiceManager>();
ambientSoundManager = std::make_unique<audio::AmbientSoundManager>();
uiSoundManager = std::make_unique<audio::UiSoundManager>();
combatSoundManager = std::make_unique<audio::CombatSoundManager>();
spellSoundManager = std::make_unique<audio::SpellSoundManager>();
movementSoundManager = std::make_unique<audio::MovementSoundManager>();
// 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();
}
// Shutdown AudioEngine singleton
audio::AudioEngine::instance().shutdown();
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, uint32_t mountDisplayId, float heightOffset) {
mountInstanceId_ = mountInstId;
mountHeightOffset_ = heightOffset;
mountAction_ = MountAction::None; // Clear mount action state
mountActionPhase_ = 0;
charAnimState = CharAnimState::MOUNT;
2026-02-07 20:05:07 -08:00
if (cameraController) {
cameraController->setMounted(true);
cameraController->setMountHeightOffset(heightOffset);
}
// Debug: dump available mount animations
if (characterRenderer && mountInstId > 0) {
characterRenderer->dumpAnimations(mountInstId);
}
// Discover mount animation capabilities (property-based, not hardcoded IDs)
LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
characterRenderer->dumpAnimations(mountInstId);
// Get all sequences for property-based analysis
std::vector<pipeline::M2Sequence> sequences;
if (!characterRenderer->getAnimationSequences(mountInstId, sequences)) {
LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs");
sequences.clear();
}
// Helper: ID-based fallback finder
auto findFirst = [&](std::initializer_list<uint32_t> candidates) -> uint32_t {
for (uint32_t id : candidates) {
if (characterRenderer->hasAnimation(mountInstId, id)) {
return id;
}
}
return 0;
};
// Property-based jump animation discovery with chain-based scoring
auto discoverJumpSet = [&]() {
// Debug: log all sequences for analysis
LOG_INFO("=== Full sequence table for mount ===");
for (const auto& seq : sequences) {
LOG_INFO("SEQ id=", seq.id,
" dur=", seq.duration,
" flags=0x", std::hex, seq.flags, std::dec,
" moveSpd=", seq.movingSpeed,
" blend=", seq.blendTime,
" next=", seq.nextAnimation,
" alias=", seq.aliasNext);
}
LOG_INFO("=== End sequence table ===");
// Known combat/bad animation IDs to avoid
std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack
auto scoreNear = [](int a, int b) -> int {
int d = std::abs(a - b);
return (d <= 8) ? (20 - d) : 0; // within 8 IDs gets points
};
auto isForbidden = [&](uint32_t id) {
return forbiddenIds.count(id) != 0;
};
auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* {
for (const auto& s : sequences) {
if (s.id == id) return &s;
}
return nullptr;
};
uint32_t runId = findFirst({5, 4});
uint32_t standId = findFirst({0});
// Step A: Find loop candidates
std::vector<uint32_t> loops;
for (const auto& seq : sequences) {
if (isForbidden(seq.id)) continue;
// Bit 0x01 NOT set = loops (0x20, 0x60), bit 0x01 set = non-looping (0x21, 0x61)
bool isLoop = (seq.flags & 0x01) == 0;
if (isLoop && seq.duration >= 350 && seq.duration <= 1000 &&
seq.id != runId && seq.id != standId) {
loops.push_back(seq.id);
}
}
// Choose loop: prefer one near known classic IDs (38), else best duration
uint32_t loop = 0;
if (!loops.empty()) {
uint32_t best = loops[0];
int bestScore = -999;
for (uint32_t id : loops) {
int sc = 0;
sc += scoreNear((int)id, 38); // classic hint
const auto* s = findSeqById(id);
if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0;
if (sc > bestScore) {
bestScore = sc;
best = id;
}
}
loop = best;
}
// Step B: Score start/end candidates
uint32_t start = 0, end = 0;
int bestStart = -999, bestEnd = -999;
for (const auto& seq : sequences) {
if (isForbidden(seq.id)) continue;
// Only consider non-looping animations for start/end
bool isLoop = (seq.flags & 0x01) == 0;
if (isLoop) continue;
// Start window
if (seq.duration >= 450 && seq.duration <= 1100) {
int sc = 0;
if (loop) sc += scoreNear((int)seq.id, (int)loop);
// Chain bonus: if this start points at loop or near it
if (loop && (seq.nextAnimation == (int16_t)loop || seq.aliasNext == loop)) sc += 30;
if (loop && scoreNear(seq.nextAnimation, (int)loop) > 0) sc += 10;
// Penalize "stop/brake-ish": very long blendTime can be a stop transition
if (seq.blendTime > 400) sc -= 5;
if (sc > bestStart) {
bestStart = sc;
start = seq.id;
}
}
// End window
if (seq.duration >= 650 && seq.duration <= 1600) {
int sc = 0;
if (loop) sc += scoreNear((int)seq.id, (int)loop);
// Chain bonus: end often points to run/stand or has no next
if (seq.nextAnimation == (int16_t)runId || seq.nextAnimation == (int16_t)standId) sc += 10;
if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal
if (sc > bestEnd) {
bestEnd = sc;
end = seq.id;
}
}
}
LOG_INFO("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
" scores: start=", bestStart, " end=", bestEnd);
return std::make_tuple(start, loop, end);
};
auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet();
// Use discovered animations, fallback to known IDs if discovery fails
mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37});
mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38});
mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39});
mountAnims_.rearUp = findFirst({94, 92, 40}); // RearUp/Special
mountAnims_.run = findFirst({5, 4}); // Run/Walk
mountAnims_.stand = findFirst({0}); // Stand (almost always 0)
// Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers)
mountAnims_.fidgets.clear();
core::Logger::getInstance().info("Scanning for fidget animations in ", sequences.size(), " sequences");
// DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses
core::Logger::getInstance().info("=== ALL potential fidgets (no metadata filter) ===");
for (const auto& seq : sequences) {
bool isLoop = (seq.flags & 0x01) == 0;
bool isStationary = std::abs(seq.movingSpeed) < 0.05f;
bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500;
if (!isLoop && reasonableDuration && isStationary) {
core::Logger::getInstance().info(" ALL: id=", seq.id,
" dur=", seq.duration, "ms",
" freq=", seq.frequency,
" replay=", seq.replayMin, "-", seq.replayMax,
" flags=0x", std::hex, seq.flags, std::dec,
" next=", seq.nextAnimation);
}
}
// Proper fidget discovery: frequency > 0 + replay timers indicate random idle animations
for (const auto& seq : sequences) {
bool isLoop = (seq.flags & 0x01) == 0;
bool hasFrequency = seq.frequency > 0;
bool hasReplay = seq.replayMax > 0;
bool isStationary = std::abs(seq.movingSpeed) < 0.05f;
bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500;
// Log candidates with metadata
if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) {
core::Logger::getInstance().info(" Candidate: id=", seq.id,
" dur=", seq.duration, "ms",
" freq=", seq.frequency,
" replay=", seq.replayMin, "-", seq.replayMax,
" next=", seq.nextAnimation,
" speed=", seq.movingSpeed);
}
// Exclude known problematic animations: death (5,6), combat (16-21), wounds (7-9)
bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9);
bool isCombat = (seq.id >= 16 && seq.id <= 21);
bool isSpecial = (seq.id == 2 || seq.id == 3); // Often aggressive specials
// Select fidgets: non-looping + (frequency OR replay) + stationary + not death/combat
// Relaxed: some mounts may only have one of the markers
if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration &&
!isDeathOrWound && !isCombat && !isSpecial) {
// Bonus: chains back to stand (indicates idle behavior)
bool chainsToStand = (seq.nextAnimation == (int16_t)mountAnims_.stand) ||
(seq.aliasNext == mountAnims_.stand) ||
(seq.nextAnimation == -1);
mountAnims_.fidgets.push_back(seq.id);
core::Logger::getInstance().info(" >> Selected fidget: id=", seq.id,
(chainsToStand ? " (chains to stand)" : ""));
}
}
// Ensure we have fallbacks for movement
if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found
if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run
core::Logger::getInstance().info("Mount animation set: jumpStart=", mountAnims_.jumpStart,
" jumpLoop=", mountAnims_.jumpLoop,
" jumpEnd=", mountAnims_.jumpEnd,
" rearUp=", mountAnims_.rearUp,
" run=", mountAnims_.run,
" stand=", mountAnims_.stand,
" fidgets=", mountAnims_.fidgets.size());
// Notify mount sound manager
if (mountSoundManager) {
bool isFlying = taxiFlight_; // Taxi flights are flying mounts
mountSoundManager->onMount(mountDisplayId, isFlying);
}
}
void Renderer::clearMount() {
mountInstanceId_ = 0;
mountHeightOffset_ = 0.0f;
mountPitch_ = 0.0f;
mountRoll_ = 0.0f;
mountAction_ = MountAction::None;
mountActionPhase_ = 0;
charAnimState = CharAnimState::IDLE;
2026-02-07 20:05:07 -08:00
if (cameraController) {
cameraController->setMounted(false);
cameraController->setMountHeightOffset(0.0f);
}
// Notify mount sound manager
if (mountSoundManager) {
mountSoundManager->onDismount();
}
}
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;
};
// 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<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)
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
float mountBob = 0.0f;
if (mountInstanceId_ > 0) {
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
float yawRad = glm::radians(characterYaw);
// Procedural lean into turns (ground mounts only, optional enhancement)
if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) {
float currentYawDeg = characterYaw;
float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_;
// Normalize to [-180, 180] for wrap-around
while (turnRate > 180.0f) turnRate -= 360.0f;
while (turnRate < -180.0f) turnRate += 360.0f;
float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f);
mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f);
prevMountYaw_ = currentYawDeg;
} else {
// Return to upright when not turning
mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f);
}
// Apply pitch (up/down), roll (banking), and yaw for realistic flight
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad));
// Drive mount model animation: idle when still, run when moving
auto pickMountAnim = [&](std::initializer_list<uint32_t> candidates, uint32_t fallback) -> uint32_t {
for (uint32_t id : candidates) {
if (characterRenderer->hasAnimation(mountInstanceId_, id)) {
return id;
}
}
return fallback;
};
uint32_t mountAnimId = ANIM_STAND;
// Get current mount animation state (used throughout)
uint32_t curMountAnim = 0;
float curMountTime = 0, curMountDur = 0;
bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur);
// Check for jump trigger - use cached per-mount animation IDs
if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) {
if (moving && mountAnims_.jumpLoop > 0) {
// Moving: skip JumpStart (looks like stopping), go straight to airborne loop
LOG_INFO("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop);
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
mountAction_ = MountAction::Jump;
mountActionPhase_ = 1; // Start in airborne phase
mountAnimId = mountAnims_.jumpLoop;
if (mountSoundManager) {
mountSoundManager->playJumpSound();
}
if (cameraController) {
cameraController->triggerMountJump();
}
} else if (!moving && mountAnims_.rearUp > 0) {
// Standing still: rear-up flourish
LOG_INFO("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp);
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false);
mountAction_ = MountAction::RearUp;
mountActionPhase_ = 0;
mountAnimId = mountAnims_.rearUp;
// Trigger semantic rear-up sound
if (mountSoundManager) {
mountSoundManager->playRearUpSound();
}
}
}
// Handle active mount actions (jump chaining or rear-up)
if (mountAction_ != MountAction::None) {
bool animFinished = haveMountState && curMountDur > 0.1f &&
(curMountTime >= curMountDur - 0.05f);
if (mountAction_ == MountAction::Jump) {
// Jump sequence: start → loop → end (physics-driven)
if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) {
// JumpStart finished, go to JumpLoop (airborne)
LOG_INFO("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")");
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
mountActionPhase_ = 1;
mountAnimId = mountAnims_.jumpLoop;
} else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) {
// No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose)
LOG_INFO("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)");
mountActionPhase_ = 1;
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) {
// Landed after airborne phase! Go to JumpEnd (grounded-triggered)
LOG_INFO("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")");
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false);
mountActionPhase_ = 2;
mountAnimId = mountAnims_.jumpEnd;
// Trigger semantic landing sound
if (mountSoundManager) {
mountSoundManager->playLandSound();
}
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) {
// No JumpEnd animation, return directly to movement after landing
LOG_INFO("Mount jump: phase 1→done (landed, no JumpEnd, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
} else if (mountActionPhase_ == 2 && animFinished) {
// JumpEnd finished, return to movement
LOG_INFO("Mount jump: phase 2→done (JumpEnd finished, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
} else {
mountAnimId = curMountAnim; // Keep current jump animation
}
} else if (mountAction_ == MountAction::RearUp) {
// Rear-up: single animation, return to stand when done
if (animFinished) {
LOG_INFO("Mount rear-up: finished, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand));
mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
} else {
mountAnimId = curMountAnim; // Keep current rear-up animation
}
}
} else if (moving) {
// Normal movement animations
if (anyStrafeLeft) {
mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN);
} else if (anyStrafeRight) {
mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN);
} else if (movingBackward) {
mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN);
} else {
mountAnimId = ANIM_RUN;
}
}
// Check if active fidget has completed
if (mountActiveFidget_ != 0) {
uint32_t curAnim = 0;
float curTime = 0.0f, curDur = 0.0f;
if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) {
// If animation changed or completed, clear active fidget
if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) {
mountActiveFidget_ = 0;
LOG_INFO("Mount fidget completed");
}
}
}
// Idle fidgets: random one-shot animations when standing still
if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) {
mountIdleFidgetTimer_ += lastDeltaTime_;
static float nextFidgetTime = 6.0f + (rand() % 7); // 6-12 seconds
if (mountIdleFidgetTimer_ >= nextFidgetTime) {
// Trigger random fidget animation
static std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<size_t> dist(0, mountAnims_.fidgets.size() - 1);
uint32_t fidgetAnim = mountAnims_.fidgets[dist(rng)];
characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false);
mountActiveFidget_ = fidgetAnim; // Track active fidget
mountIdleFidgetTimer_ = 0.0f;
nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time
LOG_INFO("Mount idle fidget: playing anim ", fidgetAnim);
}
} else if (moving) {
mountIdleFidgetTimer_ = 0.0f; // Reset timer when moving
mountActiveFidget_ = 0; // Cancel any active fidget
}
// Idle ambient sounds: snorts and whinnies only, less frequent
if (!moving && mountSoundManager) {
mountIdleSoundTimer_ += lastDeltaTime_;
static float nextIdleSoundTime = 20.0f + (rand() % 21); // 20-40 seconds
if (mountIdleSoundTimer_ >= nextIdleSoundTime) {
mountSoundManager->playIdleSound();
mountIdleSoundTimer_ = 0.0f;
nextIdleSoundTime = 20.0f + (rand() % 21); // Randomize next sound time
}
} else if (moving) {
mountIdleSoundTimer_ = 0.0f; // Reset timer when moving
}
// Only update animation if it changed and we're not in an action sequence or playing a fidget
if (mountAction_ == MountAction::None && mountActiveFidget_ == 0 && (!haveMountState || curMountAnim != mountAnimId)) {
bool loop = true; // Normal movement animations loop
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop);
}
// Rider bob: sinusoidal motion synced to mount's run animation (only used in fallback positioning)
mountBob = 0.0f;
if (moving && haveMountState && curMountDur > 1.0f) {
float norm = std::fmod(curMountTime, curMountDur) / curMountDur;
// One bounce per stride cycle
float bobSpeed = taxiFlight_ ? 2.0f : 1.0f;
mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f;
}
}
// Use mount's attachment point for proper bone-driven rider positioning
glm::mat4 mountSeatTransform;
if (characterRenderer->getAttachmentTransform(mountInstanceId_, 0, mountSeatTransform)) {
// Extract position from mount seat transform (attachment point already includes proper seat height)
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
// Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y)
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f);
// Position rider at mount seat
characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset);
// Rider uses character facing yaw, not mount bone rotation
// (rider faces character direction, seat bone only provides position)
float yawRad = glm::radians(characterYaw);
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
} else {
// Fallback to old manual positioning if attachment not found
float yawRad = glm::radians(characterYaw);
glm::mat4 mountRotation = glm::mat4(1.0f);
mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f));
mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f));
mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob);
glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f));
glm::vec3 playerPos = characterPosition + worldOffset;
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad));
}
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:
// If we got here, the mount state was cleared externally but the
// animation state hasn't been reset yet. Fall back to normal logic.
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;
} 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 = 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) {
2026-02-07 20:02:14 -08:00
loadEmotesFromDbc();
auto it = EMOTE_TABLE.find(emoteName);
if (it == EMOTE_TABLE.end()) return;
const auto& info = it->second;
2026-02-07 20:02:14 -08:00
if (info.animId == 0) return;
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();
}
}
2026-02-07 20:02:14 -08:00
std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) {
loadEmotesFromDbc();
auto it = EMOTE_TABLE.find(emoteName);
if (it != EMOTE_TABLE.end()) {
2026-02-07 20:02:14 -08:00
const auto& info = it->second;
const std::string& base = (targetName ? info.textTarget : info.textNoTarget);
if (!base.empty()) {
return replacePlaceholders(base, targetName);
}
if (targetName && !targetName->empty()) {
return "You " + info.command + " at " + *targetName + ".";
}
return "You " + info.command + ".";
}
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;
// Cache footstep surface to avoid expensive queries every step
// Only update if moved >1.5 units or timer expired (0.5s)
float distSq = glm::dot(p - cachedFootstepPosition, p - cachedFootstepPosition);
if (distSq < 2.25f && cachedFootstepUpdateTimer < 0.5f) {
return cachedFootstepSurface;
}
// Update cache
cachedFootstepPosition = p;
cachedFootstepUpdateTimer = 0.0f;
if (cameraController->isSwimming()) {
cachedFootstepSurface = audio::FootstepSurface::WATER;
return audio::FootstepSurface::WATER;
}
if (waterRenderer) {
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
if (waterH && p.z < (*waterH + 0.25f)) {
cachedFootstepSurface = audio::FootstepSurface::WATER;
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)) {
cachedFootstepSurface = audio::FootstepSurface::STONE;
return audio::FootstepSurface::STONE;
}
}
// Determine surface type (expensive - only done when cache needs update)
audio::FootstepSurface surface = 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) surface = audio::FootstepSurface::SNOW;
else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS;
else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT;
else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD;
else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL;
else 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) surface = audio::FootstepSurface::STONE;
}
}
cachedFootstepSurface = surface;
return surface;
}
void Renderer::update(float deltaTime) {
auto updateStart = std::chrono::steady_clock::now();
lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation()
// Renderer update profiling
static int rendProfileCounter = 0;
static float camTime = 0.0f, lightTime = 0.0f, charAnimTime = 0.0f;
static float terrainTime = 0.0f, skyTime = 0.0f, charRendTime = 0.0f;
static float audioTime = 0.0f, footstepTime = 0.0f, ambientTime = 0.0f;
if (wmoRenderer) wmoRenderer->resetQueryStats();
if (m2Renderer) m2Renderer->resetQueryStats();
auto cam1 = std::chrono::high_resolution_clock::now();
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();
// Update 3D audio listener position/orientation to match camera
if (camera) {
audio::AudioEngine::instance().setListenerPosition(camera->getPosition());
audio::AudioEngine::instance().setListenerOrientation(camera->getForward(), camera->getUp());
}
} else {
lastCameraUpdateMs = 0.0;
}
auto cam2 = std::chrono::high_resolution_clock::now();
camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count();
// Update lighting system
auto light1 = std::chrono::high_resolution_clock::now();
if (lightingManager) {
// TODO: Get actual map ID from game state (0 = Eastern Kingdoms for now)
// TODO: Get actual game time from server (use -1 for local time fallback)
// TODO: Get weather/underwater state from game state
uint32_t mapId = 0; // Eastern Kingdoms
float gameTime = -1.0f; // Use local time for now
bool isRaining = false; // TODO: Get from weather system
bool isUnderwater = false; // TODO: Get from player state
lightingManager->update(characterPosition, mapId, gameTime, isRaining, isUnderwater);
}
auto light2 = std::chrono::high_resolution_clock::now();
lightTime += std::chrono::duration<float, std::milli>(light2 - light1).count();
// Sync character model position/rotation and animation with follow target
auto charAnim1 = std::chrono::high_resolution_clock::now();
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.
// During taxi flights, orientation is controlled by the flight path (not player input)
if (taxiFlight_) {
// Taxi flight: use orientation from flight path
characterYaw = cameraController->getFacingYaw();
} else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) {
characterYaw = cameraController->getFacingYaw();
} else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) {
// 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();
}
auto charAnim2 = std::chrono::high_resolution_clock::now();
charAnimTime += std::chrono::duration<float, std::milli>(charAnim2 - charAnim1).count();
// Update terrain streaming
auto terrain1 = std::chrono::high_resolution_clock::now();
if (terrainManager && camera) {
terrainManager->update(*camera, deltaTime);
}
auto terrain2 = std::chrono::high_resolution_clock::now();
terrainTime += std::chrono::duration<float, std::milli>(terrain2 - terrain1).count();
// Update skybox time progression
auto sky1 = std::chrono::high_resolution_clock::now();
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 mount dust effects
if (mountDust) {
mountDust->update(deltaTime);
// Spawn dust when mounted and moving on ground
if (isMounted() && camera && cameraController && !taxiFlight_) {
bool isMoving = cameraController->isMoving();
bool onGround = cameraController->isGrounded();
if (isMoving && onGround) {
// Calculate velocity from camera direction and speed
glm::vec3 forward = camera->getForward();
float speed = cameraController->getMovementSpeed();
glm::vec3 velocity = forward * speed;
velocity.z = 0.0f; // Ignore vertical component
// Spawn dust at mount's feet (slightly below character position)
glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mountHeightOffset_ * 0.8f);
mountDust->spawnDust(dustPos, velocity, isMoving);
}
}
}
auto sky2 = std::chrono::high_resolution_clock::now();
skyTime += std::chrono::duration<float, std::milli>(sky2 - sky1).count();
// Update character animations
auto charRend1 = std::chrono::high_resolution_clock::now();
if (characterRenderer && camera) {
characterRenderer->update(deltaTime, camera->getPosition());
}
auto charRend2 = std::chrono::high_resolution_clock::now();
charRendTime += std::chrono::duration<float, std::milli>(charRend2 - charRend1).count();
// Update AudioEngine (cleanup finished sounds, etc.)
auto audio1 = std::chrono::high_resolution_clock::now();
audio::AudioEngine::instance().update(deltaTime);
auto audio2 = std::chrono::high_resolution_clock::now();
audioTime += std::chrono::duration<float, std::milli>(audio2 - audio1).count();
// Footsteps: animation-event driven + surface query at event time.
auto footstep1 = std::chrono::high_resolution_clock::now();
if (footstepManager) {
footstepManager->update(deltaTime);
cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
cameraController && cameraController->isThirdPerson() &&
cameraController->isGrounded() && !cameraController->isSwimming();
if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) {
// Mount footsteps: use mount's animation for timing
uint32_t animId = 0;
float animTimeMs = 0.0f, animDurationMs = 0.0f;
if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) &&
animDurationMs > 1.0f && cameraController->isMoving()) {
float norm = std::fmod(animTimeMs, animDurationMs) / animDurationMs;
if (norm < 0.0f) norm += 1.0f;
if (animId != mountFootstepLastAnimId) {
mountFootstepLastAnimId = animId;
mountFootstepLastNormTime = norm;
mountFootstepNormInitialized = true;
} else if (!mountFootstepNormInitialized) {
mountFootstepNormInitialized = true;
mountFootstepLastNormTime = norm;
} else {
// Mount gait: 2 hoofbeats per cycle (synced with animation)
auto crossed = [&](float eventNorm) {
if (mountFootstepLastNormTime <= norm) {
return mountFootstepLastNormTime < eventNorm && eventNorm <= norm;
}
return mountFootstepLastNormTime < eventNorm || eventNorm <= norm;
};
if (crossed(0.25f) || crossed(0.75f)) {
footstepManager->playFootstep(resolveFootstepSurface(), true);
}
mountFootstepLastNormTime = norm;
}
} else {
mountFootstepNormInitialized = false;
}
footstepNormInitialized = false; // Reset player footstep tracking
} else if (canPlayFootsteps && isFootstepAnimationState()) {
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());
}
mountFootstepNormInitialized = false;
} else {
footstepNormInitialized = false;
mountFootstepNormInitialized = 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);
// Fade music underwater
if (musicManager) {
musicManager->setUnderwaterMode(swimming);
}
sfxPrevGrounded = grounded;
sfxPrevJumping = jumping;
sfxPrevFalling = falling;
sfxPrevSwimming = swimming;
} else {
activitySoundManager->setSwimmingState(false, false);
// Restore music volume when activity sounds disabled
if (musicManager) {
musicManager->setUnderwaterMode(false);
}
sfxStateInitialized = false;
}
}
// Mount ambient sounds: wing flaps, breathing, etc.
if (mountSoundManager) {
mountSoundManager->update(deltaTime);
if (cameraController && isMounted()) {
bool moving = cameraController->isMoving();
bool flying = taxiFlight_ || !cameraController->isGrounded(); // Flying if taxi or airborne
mountSoundManager->setMoving(moving);
mountSoundManager->setFlying(flying);
}
}
auto footstep2 = std::chrono::high_resolution_clock::now();
footstepTime += std::chrono::duration<float, std::milli>(footstep2 - footstep1).count();
// Ambient environmental sounds: fireplaces, water, birds, etc.
auto ambient1 = std::chrono::high_resolution_clock::now();
if (ambientSoundManager && camera && wmoRenderer && cameraController) {
glm::vec3 camPos = camera->getPosition();
uint32_t wmoId = 0;
bool isIndoor = wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoId);
bool isSwimming = cameraController->isSwimming();
// Check if inside blacksmith (96048 = Goldshire blacksmith)
bool isBlacksmith = (wmoId == 96048);
// Sync weather audio with visual weather system
if (weather) {
auto weatherType = weather->getWeatherType();
float intensity = weather->getIntensity();
audio::AmbientSoundManager::WeatherType audioWeatherType = audio::AmbientSoundManager::WeatherType::NONE;
if (weatherType == Weather::Type::RAIN) {
if (intensity < 0.33f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_LIGHT;
} else if (intensity < 0.66f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_MEDIUM;
} else {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_HEAVY;
}
} else if (weatherType == Weather::Type::SNOW) {
if (intensity < 0.33f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_LIGHT;
} else if (intensity < 0.66f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_MEDIUM;
} else {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_HEAVY;
}
}
ambientSoundManager->setWeather(audioWeatherType);
}
ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
}
auto ambient2 = std::chrono::high_resolution_clock::now();
ambientTime += std::chrono::duration<float, std::milli>(ambient2 - ambient1).count();
// Update M2 doodad animations (pass camera for frustum-culling bone computation)
static int m2ProfileCounter = 0;
static float m2Time = 0.0f;
auto m21 = std::chrono::high_resolution_clock::now();
if (m2Renderer && camera) {
m2Renderer->update(deltaTime, camera->getPosition(),
camera->getProjectionMatrix() * camera->getViewMatrix());
}
auto m22 = std::chrono::high_resolution_clock::now();
m2Time += std::chrono::duration<float, std::milli>(m22 - m21).count();
// 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);
bool insideTavern = false;
bool insideBlacksmith = false;
std::string tavernMusic;
// Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths)
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
}
// Detect taverns/inns/blacksmiths by WMO model ID
// Log WMO ID for debugging
static uint32_t lastLoggedWmoId = 0;
if (wmoModelId != lastLoggedWmoId) {
LOG_INFO("Inside WMO model ID: ", wmoModelId);
lastLoggedWmoId = wmoModelId;
}
// Blacksmith detection
if (wmoModelId == 96048) { // Goldshire blacksmith
insideBlacksmith = true;
LOG_INFO("Detected blacksmith WMO ", wmoModelId);
}
// These IDs represent typical Alliance and Horde inn buildings
if (wmoModelId == 191 || // Goldshire inn (old ID)
wmoModelId == 71414 || // Goldshire inn (actual)
wmoModelId == 190 || // Small inn (common)
wmoModelId == 220 || // Tavern building
wmoModelId == 221 || // Large tavern
wmoModelId == 5392 || // Horde inn
wmoModelId == 5393) { // Another inn variant
insideTavern = true;
// WoW tavern music (cozy ambient tracks) - FIXED PATHS
static const std::vector<std::string> tavernTracks = {
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
};
static int tavernTrackIndex = 0;
tavernMusic = tavernTracks[tavernTrackIndex % tavernTracks.size()];
LOG_INFO("Detected tavern WMO ", wmoModelId, ", playing: ", tavernMusic);
}
}
}
// Handle tavern music transitions
if (insideTavern) {
if (!inTavern_ && !tavernMusic.empty()) {
inTavern_ = true;
LOG_INFO("Entered tavern");
musicManager->playMusic(tavernMusic, true); // Immediate playback, looping
}
} else if (inTavern_) {
// Exited tavern - restore zone music with crossfade
inTavern_ = false;
LOG_INFO("Exited tavern");
auto* info = zoneManager->getZoneInfo(currentZoneId);
if (info) {
std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) {
musicManager->crossfadeTo(music);
}
}
}
// Handle blacksmith music (stop music when entering blacksmith, let ambience play)
if (insideBlacksmith) {
if (!inBlacksmith_) {
inBlacksmith_ = true;
LOG_INFO("Entered blacksmith - stopping music");
musicManager->stopMusic();
}
} else if (inBlacksmith_) {
// Exited blacksmith - restore zone music with crossfade
inBlacksmith_ = false;
LOG_INFO("Exited blacksmith - restoring music");
auto* info = zoneManager->getZoneInfo(currentZoneId);
if (info) {
std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) {
musicManager->crossfadeTo(music);
}
}
}
// Handle normal zone transitions (only if not in tavern or blacksmith)
if (!insideTavern && !insideBlacksmith && 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();
// Log renderer profiling every 60 frames
if (++rendProfileCounter >= 60) {
LOG_INFO("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f,
"ms light=", lightTime / 60.0f, "ms charAnim=", charAnimTime / 60.0f,
"ms terrain=", terrainTime / 60.0f, "ms sky=", skyTime / 60.0f,
"ms charRend=", charRendTime / 60.0f, "ms audio=", audioTime / 60.0f,
"ms footstep=", footstepTime / 60.0f, "ms ambient=", ambientTime / 60.0f,
"ms m2Anim=", m2Time / 60.0f, "ms");
rendProfileCounter = 0;
camTime = lightTime = charAnimTime = 0.0f;
terrainTime = skyTime = charRendTime = 0.0f;
audioTime = footstepTime = ambientTime = 0.0f;
m2Time = 0.0f;
}
if (++m2ProfileCounter >= 60) {
m2ProfileCounter = 0;
}
}
// ============================================================
// 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<float> verts;
for (int i = 0; i <= SEGMENTS; i++) {
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(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<int>((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, game::GameHandler* gameHandler) {
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 sky system (unified coordinator for skybox, stars, celestial, clouds, lens flare)
if (skySystem && camera) {
// Populate SkyParams from lighting manager
rendering::SkyParams skyParams;
skyParams.timeOfDay = timeOfDay;
skyParams.gameTime = gameHandler ? gameHandler->getGameTime() : -1.0f;
if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
skyParams.directionalDir = lighting.directionalDir;
skyParams.sunColor = lighting.diffuseColor;
skyParams.skyTopColor = lighting.skyTopColor;
skyParams.skyMiddleColor = lighting.skyMiddleColor;
skyParams.skyBand1Color = lighting.skyBand1Color;
skyParams.skyBand2Color = lighting.skyBand2Color;
skyParams.cloudDensity = lighting.cloudDensity;
skyParams.fogDensity = lighting.fogDensity;
skyParams.horizonGlow = lighting.horizonGlow;
}
// TODO: Set skyboxModelId from LightSkybox.dbc (future)
skyParams.skyboxModelId = 0;
skyParams.skyboxHasStars = false; // Gradient skybox has no baked stars
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies Add SkySystem coordinator that follows WoW's actual architecture where skyboxes are authoritative and procedural elements serve as fallbacks. Integrate lighting system across all renderers (terrain, WMO, M2, character) with unified parameters. Sky System: - SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare - Skybox is authoritative (baked stars from M2 models, procedural fallback only) - skyboxHasStars flag gates procedural star rendering (prevents double-star bug) Celestial Bodies (Lore-Accurate): - Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue) - Deterministic moon phases from server gameTime (not deltaTime toys) - Sun positioning driven by LightingManager directionalDir (DBC-sourced) - Camera-locked sky dome (translation ignored, rotation applied) Lighting Integration: - Apply LightingManager params to WMO, M2, character renderers - Unified lighting: directional light, diffuse color, ambient color, fog - Star occlusion by cloud density (70% weight) and fog density (30% weight) Documentation: - Add comprehensive SKY_SYSTEM.md technical guide - Update MEMORY.md with sky system architecture and anti-patterns - Update README.md with WoW-accurate descriptions Critical design decisions: - NO latitude-based star rotation (Azeroth not modeled as spherical planet) - NO always-on procedural stars (skybox authority prevents zone identity loss) - NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
skySystem->render(*camera, skyParams);
} else {
// Fallback: render individual components (backwards compatibility)
if (skybox && camera) {
skybox->render(*camera, timeOfDay);
}
// Get lighting parameters for celestial rendering
const glm::vec3* sunDir = nullptr;
const glm::vec3* sunColor = nullptr;
float cloudDensity = 0.0f;
float fogDensity = 0.0f;
if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
sunDir = &lighting.directionalDir;
sunColor = &lighting.diffuseColor;
cloudDensity = lighting.cloudDensity;
fogDensity = lighting.fogDensity;
}
if (starField && camera) {
starField->render(*camera, timeOfDay, cloudDensity, fogDensity);
}
if (celestial && camera) {
celestial->render(*camera, timeOfDay, sunDir, sunColor);
}
if (clouds && camera) {
clouds->render(*camera, timeOfDay);
}
if (lensFlare && camera && celestial) {
glm::vec3 sunPosition;
if (sunDir) {
const float sunDistance = 800.0f;
sunPosition = -*sunDir * sunDistance;
} else {
sunPosition = celestial->getSunPosition(timeOfDay);
}
lensFlare->render(*camera, sunPosition, timeOfDay);
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies Add SkySystem coordinator that follows WoW's actual architecture where skyboxes are authoritative and procedural elements serve as fallbacks. Integrate lighting system across all renderers (terrain, WMO, M2, character) with unified parameters. Sky System: - SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare - Skybox is authoritative (baked stars from M2 models, procedural fallback only) - skyboxHasStars flag gates procedural star rendering (prevents double-star bug) Celestial Bodies (Lore-Accurate): - Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue) - Deterministic moon phases from server gameTime (not deltaTime toys) - Sun positioning driven by LightingManager directionalDir (DBC-sourced) - Camera-locked sky dome (translation ignored, rotation applied) Lighting Integration: - Apply LightingManager params to WMO, M2, character renderers - Unified lighting: directional light, diffuse color, ambient color, fog - Star occlusion by cloud density (70% weight) and fog density (30% weight) Documentation: - Add comprehensive SKY_SYSTEM.md technical guide - Update MEMORY.md with sky system architecture and anti-patterns - Update README.md with WoW-accurate descriptions Critical design decisions: - NO latitude-based star rotation (Azeroth not modeled as spherical planet) - NO always-on procedural stars (skybox authority prevents zone identity loss) - NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
}
}
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies Add SkySystem coordinator that follows WoW's actual architecture where skyboxes are authoritative and procedural elements serve as fallbacks. Integrate lighting system across all renderers (terrain, WMO, M2, character) with unified parameters. Sky System: - SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare - Skybox is authoritative (baked stars from M2 models, procedural fallback only) - skyboxHasStars flag gates procedural star rendering (prevents double-star bug) Celestial Bodies (Lore-Accurate): - Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue) - Deterministic moon phases from server gameTime (not deltaTime toys) - Sun positioning driven by LightingManager directionalDir (DBC-sourced) - Camera-locked sky dome (translation ignored, rotation applied) Lighting Integration: - Apply LightingManager params to WMO, M2, character renderers - Unified lighting: directional light, diffuse color, ambient color, fog - Star occlusion by cloud density (70% weight) and fog density (30% weight) Documentation: - Add comprehensive SKY_SYSTEM.md technical guide - Update MEMORY.md with sky system architecture and anti-patterns - Update README.md with WoW-accurate descriptions Critical design decisions: - NO latitude-based star rotation (Azeroth not modeled as spherical planet) - NO always-on procedural stars (skybox authority prevents zone identity loss) - NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
// Apply lighting and fog to all renderers
if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
float lightDir[3] = {lighting.directionalDir.x, lighting.directionalDir.y, lighting.directionalDir.z};
float lightColor[3] = {lighting.diffuseColor.r, lighting.diffuseColor.g, lighting.diffuseColor.b};
float ambientColor[3] = {lighting.ambientColor.r, lighting.ambientColor.g, lighting.ambientColor.b};
float fogColorArray[3] = {lighting.fogColor.r, lighting.fogColor.g, lighting.fogColor.b};
if (wmoRenderer) {
wmoRenderer->setLighting(lightDir, lightColor, ambientColor);
wmoRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
if (m2Renderer) {
m2Renderer->setLighting(lightDir, lightColor, ambientColor);
m2Renderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
if (characterRenderer) {
characterRenderer->setLighting(lightDir, lightColor, ambientColor);
characterRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
} else if (skybox) {
// Fallback to skybox-based fog if no lighting manager
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);
}
// Apply lighting from lighting manager
if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
// Set lighting (direction, color, ambient)
float lightDir[3] = {lighting.directionalDir.x, lighting.directionalDir.y, lighting.directionalDir.z};
float lightColor[3] = {lighting.diffuseColor.r, lighting.diffuseColor.g, lighting.diffuseColor.b};
float ambientColor[3] = {lighting.ambientColor.r, lighting.ambientColor.g, lighting.ambientColor.b};
terrainRenderer->setLighting(lightDir, lightColor, ambientColor);
// Set fog
float fogColor[3] = {lighting.fogColor.r, lighting.fogColor.g, lighting.fogColor.b};
terrainRenderer->setFog(fogColor, lighting.fogStart, lighting.fogEnd);
} else if (skybox) {
// Fallback to skybox-based fog if no lighting manager
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)
// Skip during taxi flights for performance and visual clarity
bool onTaxi = cameraController && cameraController->isOnTaxi();
if (weather && camera && !onTaxi) {
weather->render(*camera);
}
// Render swim effects (ripples and bubbles)
if (swimEffects && camera) {
swimEffects->render(*camera);
}
// Render mount dust effects
if (mountDust && camera) {
mountDust->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<double, std::milli>(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());
m2Renderer->setOnTaxi(cameraController->isOnTaxi());
}
auto m2Start = std::chrono::steady_clock::now();
m2Renderer->render(*camera, view, projection);
// Skip particle fog during taxi (expensive and visually distracting)
if (!onTaxi) {
m2Renderer->renderSmokeParticles(*camera, view, projection);
m2Renderer->renderM2Particles(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);
}
// Render quest markers (billboards above NPCs)
if (questMarkerRenderer && camera) {
questMarkerRenderer->render(*camera);
}
// 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());
}
// Set ambient sound manager for environmental audio emitters
if (ambientSoundManager) {
terrainManager->setAmbientSoundManager(ambientSoundManager.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);
}
if (mountSoundManager) {
mountSoundManager->initialize(assetManager);
}
if (npcVoiceManager) {
npcVoiceManager->initialize(assetManager);
}
if (ambientSoundManager) {
ambientSoundManager->initialize(assetManager);
}
if (uiSoundManager) {
uiSoundManager->initialize(assetManager);
}
if (combatSoundManager) {
combatSoundManager->initialize(assetManager);
}
if (spellSoundManager) {
spellSoundManager->initialize(assetManager);
}
if (movementSoundManager) {
movementSoundManager->initialize(assetManager);
}
if (questMarkerRenderer) {
questMarkerRenderer->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;
// Get asset manager from Application if not cached yet
if (!cachedAssetManager) {
cachedAssetManager = core::Application::getInstance().getAssetManager();
}
// Initialize music manager with asset manager
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);
}
}
if (mountSoundManager && cachedAssetManager) {
mountSoundManager->initialize(cachedAssetManager);
}
if (npcVoiceManager && cachedAssetManager) {
npcVoiceManager->initialize(cachedAssetManager);
}
if (ambientSoundManager && cachedAssetManager) {
ambientSoundManager->initialize(cachedAssetManager);
}
if (uiSoundManager && cachedAssetManager) {
uiSoundManager->initialize(cachedAssetManager);
}
if (combatSoundManager && cachedAssetManager) {
combatSoundManager->initialize(cachedAssetManager);
}
if (spellSoundManager && cachedAssetManager) {
spellSoundManager->initialize(cachedAssetManager);
}
if (movementSoundManager && cachedAssetManager) {
movementSoundManager->initialize(cachedAssetManager);
}
if (questMarkerRenderer && cachedAssetManager) {
questMarkerRenderer->initialize(cachedAssetManager);
}
// Wire ambient sound manager to terrain manager for emitter registration
if (terrainManager && ambientSoundManager) {
terrainManager->setAmbientSoundManager(ambientSoundManager.get());
}
// 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