mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 15:50:20 +00:00
- Render selection circle before character/WMO/M2 passes\n- Disable depth test during selection pass so terrain does not occlude it\n- Keep model passes after selection so monsters/M2s render over the ring
3672 lines
149 KiB
C++
3672 lines
149 KiB
C++
#include "rendering/renderer.hpp"
|
|
#include "rendering/camera.hpp"
|
|
#include "rendering/camera_controller.hpp"
|
|
#include "rendering/scene.hpp"
|
|
#include "rendering/terrain_renderer.hpp"
|
|
#include "rendering/terrain_manager.hpp"
|
|
#include "rendering/performance_hud.hpp"
|
|
#include "rendering/water_renderer.hpp"
|
|
#include "rendering/skybox.hpp"
|
|
#include "rendering/celestial.hpp"
|
|
#include "rendering/starfield.hpp"
|
|
#include "rendering/clouds.hpp"
|
|
#include "rendering/lens_flare.hpp"
|
|
#include "rendering/weather.hpp"
|
|
#include "rendering/lighting_manager.hpp"
|
|
#include "rendering/sky_system.hpp"
|
|
#include "rendering/swim_effects.hpp"
|
|
#include "rendering/mount_dust.hpp"
|
|
#include "rendering/charge_effect.hpp"
|
|
#include "rendering/levelup_effect.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"
|
|
#include "pipeline/dbc_loader.hpp"
|
|
#include "pipeline/dbc_layout.hpp"
|
|
#include "pipeline/m2_loader.hpp"
|
|
#include "pipeline/wmo_loader.hpp"
|
|
#include "pipeline/adt_loader.hpp"
|
|
#include "pipeline/terrain_mesh.hpp"
|
|
#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 {
|
|
uint32_t animId = 0;
|
|
uint32_t dbcId = 0; // EmotesText.dbc record ID (for CMSG_TEXT_EMOTE)
|
|
bool loop = false;
|
|
std::string textNoTarget; // sender sees, no target: "You dance."
|
|
std::string textTarget; // sender sees, with target: "You dance with %s."
|
|
std::string othersNoTarget; // others see, no target: "%s dances."
|
|
std::string othersTarget; // others see, with target: "%s dances with %s."
|
|
std::string command;
|
|
};
|
|
|
|
static std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE;
|
|
static std::unordered_map<uint32_t, const EmoteInfo*> EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo*
|
|
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, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}},
|
|
{"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}},
|
|
{"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}},
|
|
{"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}},
|
|
{"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}},
|
|
{"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}},
|
|
{"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}},
|
|
{"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}},
|
|
{"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}},
|
|
{"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
|
|
"With arms flapping, you strut around %s. Cluck, Cluck, Chicken!",
|
|
"%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}},
|
|
{"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}},
|
|
{"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}},
|
|
{"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}},
|
|
{"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}},
|
|
{"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}},
|
|
{"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}},
|
|
{"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}},
|
|
{"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}},
|
|
{"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins 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;
|
|
}
|
|
|
|
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
|
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
|
|
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
|
|
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
|
|
|
|
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, etdL ? (*etdL)["ID"] : 0);
|
|
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 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, emL ? (*emL)["ID"] : 0);
|
|
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
|
|
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
|
}
|
|
}
|
|
|
|
EMOTE_TABLE.clear();
|
|
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
|
|
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
|
|
uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0);
|
|
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
|
|
if (cmdRaw.empty()) continue;
|
|
|
|
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 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, etL ? (*etL)["SenderTargetTextID"] : 5);
|
|
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9);
|
|
uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3);
|
|
uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7);
|
|
|
|
std::string textTarget, textNoTarget, oTarget, oNoTarget;
|
|
if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second;
|
|
if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second;
|
|
if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second;
|
|
if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second;
|
|
|
|
for (const std::string& cmd : parseEmoteCommands(cmdRaw)) {
|
|
if (cmd.empty()) continue;
|
|
EmoteInfo info;
|
|
info.animId = animId;
|
|
info.dbcId = recordId;
|
|
info.loop = isLoopingEmote(cmd);
|
|
info.textNoTarget = textNoTarget;
|
|
info.textTarget = textTarget;
|
|
info.othersNoTarget = oNoTarget;
|
|
info.othersTarget = oTarget;
|
|
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");
|
|
}
|
|
|
|
// Build reverse lookup by dbcId (only first command per emote needed)
|
|
EMOTE_BY_DBCID.clear();
|
|
for (auto& [cmd, info] : EMOTE_TABLE) {
|
|
if (info.dbcId != 0) {
|
|
EMOTE_BY_DBCID.emplace(info.dbcId, &info);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 level-up effect (model loaded later via loadLevelUpEffect)
|
|
levelUpEffect = std::make_unique<LevelUpEffect>();
|
|
|
|
// Create charge effect (point-sprite particles + optional M2 models)
|
|
chargeEffect = std::make_unique<ChargeEffect>();
|
|
if (!chargeEffect->initialize()) {
|
|
LOG_WARNING("Failed to initialize charge effect");
|
|
chargeEffect.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(assetManager)) {
|
|
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, const std::string& modelPath) {
|
|
mountInstanceId_ = mountInstId;
|
|
mountHeightOffset_ = heightOffset;
|
|
mountSeatAttachmentId_ = -1;
|
|
smoothedMountSeatPos_ = characterPosition;
|
|
mountSeatSmoothingInit_ = false;
|
|
mountAction_ = MountAction::None; // Clear mount action state
|
|
mountActionPhase_ = 0;
|
|
charAnimState = CharAnimState::MOUNT;
|
|
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_DEBUG("=== 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_DEBUG("=== Full sequence table for mount ===");
|
|
for (const auto& seq : sequences) {
|
|
LOG_DEBUG("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_DEBUG("=== 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_DEBUG("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().debug("Scanning for fidget animations in ", sequences.size(), " sequences");
|
|
|
|
// DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses
|
|
core::Logger::getInstance().debug("=== 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().debug(" 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().debug(" 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), wounds (7-9), combat (16-21), attacks (11-15)
|
|
bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9);
|
|
bool isAttackOrCombat = (seq.id >= 11 && seq.id <= 21);
|
|
bool isSpecial = (seq.id == 2 || seq.id == 3); // Often aggressive specials
|
|
|
|
// Select fidgets: (frequency OR replay) + exclude problematic ID ranges
|
|
// Relaxed back to OR since some mounts may only have one metadata marker
|
|
if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration &&
|
|
!isDeathOrWound && !isAttackOrCombat && !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().debug(" >> 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().debug("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, modelPath);
|
|
}
|
|
}
|
|
|
|
void Renderer::clearMount() {
|
|
mountInstanceId_ = 0;
|
|
mountHeightOffset_ = 0.0f;
|
|
mountPitch_ = 0.0f;
|
|
mountRoll_ = 0.0f;
|
|
mountSeatAttachmentId_ = -1;
|
|
smoothedMountSeatPos_ = glm::vec3(0.0f);
|
|
mountSeatSmoothingInit_ = false;
|
|
mountAction_ = MountAction::None;
|
|
mountActionPhase_ = 0;
|
|
charAnimState = CharAnimState::IDLE;
|
|
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
|
|
// Canonical player ready stances (AnimationData.dbc)
|
|
constexpr uint32_t ANIM_READY_UNARMED = 22; // ReadyUnarmed
|
|
constexpr uint32_t ANIM_READY_1H = 23; // Ready1H
|
|
constexpr uint32_t ANIM_READY_2H = 24; // Ready2H
|
|
constexpr uint32_t ANIM_READY_2H_L = 25; // Ready2HL (some 2H left-handed rigs)
|
|
constexpr uint32_t ANIM_FLY_IDLE = 158; // Flying mount idle/hover
|
|
constexpr uint32_t ANIM_FLY_FORWARD = 159; // Flying mount forward
|
|
|
|
CharAnimState newState = charAnimState;
|
|
|
|
bool moving = cameraController->isMoving();
|
|
bool movingForward = cameraController->isMovingForward();
|
|
bool movingBackward = cameraController->isMovingBackward();
|
|
bool autoRunning = cameraController->isAutoRunning();
|
|
bool strafeLeft = cameraController->isStrafingLeft();
|
|
bool strafeRight = cameraController->isStrafingRight();
|
|
// Strafe animation only plays during *pure* strafing (no forward/backward/autorun).
|
|
// When forward+strafe are both held, the walk/run animation plays — same as the real client.
|
|
bool pureStrafe = !movingForward && !movingBackward && !autoRunning;
|
|
bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe;
|
|
bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe;
|
|
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;
|
|
float mountYawRad = glm::radians(characterYaw);
|
|
if (mountInstanceId_ > 0) {
|
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
|
|
|
// 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_, mountYawRad));
|
|
|
|
// 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);
|
|
|
|
// Taxi flight: use flying animations instead of ground movement
|
|
if (taxiFlight_) {
|
|
// Log available animations once when taxi starts
|
|
if (!taxiAnimsLogged_) {
|
|
taxiAnimsLogged_ = true;
|
|
LOG_INFO("Taxi flight active: mountInstanceId_=", mountInstanceId_,
|
|
" curMountAnim=", curMountAnim, " haveMountState=", haveMountState);
|
|
std::vector<pipeline::M2Sequence> seqs;
|
|
if (characterRenderer->getAnimationSequences(mountInstanceId_, seqs)) {
|
|
std::string animList;
|
|
for (const auto& s : seqs) {
|
|
if (!animList.empty()) animList += ", ";
|
|
animList += std::to_string(s.id);
|
|
}
|
|
LOG_INFO("Taxi mount available animations: [", animList, "]");
|
|
}
|
|
}
|
|
|
|
// Try multiple flying animation IDs in priority order:
|
|
// 159=FlyForward, 158=FlyIdle (WotLK flying mounts)
|
|
// 234=FlyRun, 229=FlyStand (Vanilla creature fly anims)
|
|
// 233=FlyWalk, 141=FlyMounted, 369=FlyRun (alternate IDs)
|
|
// 6=Fly (classic creature fly)
|
|
// Fallback: Run, then Stand (hover)
|
|
uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN};
|
|
mountAnimId = ANIM_STAND; // ultimate fallback: hover/idle
|
|
for (uint32_t fa : flyAnims) {
|
|
if (characterRenderer->hasAnimation(mountInstanceId_, fa)) {
|
|
mountAnimId = fa;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!haveMountState || curMountAnim != mountAnimId) {
|
|
LOG_INFO("Taxi mount: playing animation ", mountAnimId);
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
|
}
|
|
|
|
// Skip all ground mount logic (jumps, fidgets, etc.)
|
|
goto taxi_mount_done;
|
|
} else {
|
|
taxiAnimsLogged_ = false;
|
|
}
|
|
|
|
// 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_DEBUG("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_DEBUG("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_DEBUG("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_DEBUG("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_DEBUG("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_DEBUG("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_DEBUG("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_DEBUG("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;
|
|
}
|
|
}
|
|
|
|
// Cancel active fidget immediately if movement starts
|
|
if (moving && mountActiveFidget_ != 0) {
|
|
mountActiveFidget_ = 0;
|
|
// Force play run animation to stop fidget immediately
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
|
}
|
|
|
|
// Check if active fidget has completed (only when not moving)
|
|
if (!moving && 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_DEBUG("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_DEBUG("Mount idle fidget: playing anim ", fidgetAnim);
|
|
}
|
|
}
|
|
if (moving) {
|
|
mountIdleFidgetTimer_ = 0.0f; // Reset timer when moving
|
|
}
|
|
|
|
// Idle ambient sounds: snorts and whinnies only, infrequent
|
|
if (!moving && mountSoundManager) {
|
|
mountIdleSoundTimer_ += lastDeltaTime_;
|
|
static float nextIdleSoundTime = 45.0f + (rand() % 46); // 45-90 seconds
|
|
|
|
if (mountIdleSoundTimer_ >= nextIdleSoundTime) {
|
|
mountSoundManager->playIdleSound();
|
|
mountIdleSoundTimer_ = 0.0f;
|
|
nextIdleSoundTime = 45.0f + (rand() % 46); // 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);
|
|
}
|
|
|
|
taxi_mount_done:
|
|
// 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.
|
|
if (taxiFlight_) {
|
|
glm::mat4 mountSeatTransform(1.0f);
|
|
bool haveSeat = false;
|
|
static constexpr uint32_t kTaxiSeatAttachmentId = 0; // deterministic rider seat
|
|
if (mountSeatAttachmentId_ == -1) {
|
|
mountSeatAttachmentId_ = static_cast<int>(kTaxiSeatAttachmentId);
|
|
}
|
|
if (mountSeatAttachmentId_ >= 0) {
|
|
haveSeat = characterRenderer->getAttachmentTransform(
|
|
mountInstanceId_, static_cast<uint32_t>(mountSeatAttachmentId_), mountSeatTransform);
|
|
}
|
|
if (!haveSeat) {
|
|
mountSeatAttachmentId_ = -2;
|
|
}
|
|
|
|
if (haveSeat) {
|
|
glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f);
|
|
// Taxi passengers should be rigidly parented to mount attachment transforms.
|
|
// Smoothing here introduces visible seat lag/drift on turns.
|
|
mountSeatSmoothingInit_ = false;
|
|
smoothedMountSeatPos_ = targetRiderPos;
|
|
characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos);
|
|
} else {
|
|
mountSeatSmoothingInit_ = false;
|
|
glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f);
|
|
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
|
}
|
|
|
|
float riderPitch = mountPitch_ * 0.35f;
|
|
float riderRoll = mountRoll_ * 0.35f;
|
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRad));
|
|
return;
|
|
}
|
|
|
|
// Ground mounts: try a seat attachment first.
|
|
glm::mat4 mountSeatTransform;
|
|
bool haveSeat = false;
|
|
if (mountSeatAttachmentId_ >= 0) {
|
|
haveSeat = characterRenderer->getAttachmentTransform(
|
|
mountInstanceId_, static_cast<uint32_t>(mountSeatAttachmentId_), mountSeatTransform);
|
|
} else if (mountSeatAttachmentId_ == -1) {
|
|
// Probe common rider seat attachment IDs once per mount.
|
|
static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8};
|
|
for (uint32_t attId : kSeatAttachments) {
|
|
if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) {
|
|
mountSeatAttachmentId_ = static_cast<int>(attId);
|
|
haveSeat = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!haveSeat) {
|
|
mountSeatAttachmentId_ = -2;
|
|
}
|
|
}
|
|
|
|
if (haveSeat) {
|
|
// Extract position from mount seat transform (attachment point already includes proper seat height)
|
|
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
|
|
|
|
// Keep seat offset minimal; large offsets amplify visible bobble.
|
|
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f);
|
|
glm::vec3 targetRiderPos = mountSeatPos + seatOffset;
|
|
// When moving, smoothing the seat position produces visible lag that looks like
|
|
// the rider sliding toward the rump. Anchor rigidly while moving.
|
|
if (moving) {
|
|
mountSeatSmoothingInit_ = false;
|
|
smoothedMountSeatPos_ = targetRiderPos;
|
|
} else if (!mountSeatSmoothingInit_) {
|
|
smoothedMountSeatPos_ = targetRiderPos;
|
|
mountSeatSmoothingInit_ = true;
|
|
} else {
|
|
float smoothHz = taxiFlight_ ? 10.0f : 14.0f;
|
|
float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f));
|
|
smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha);
|
|
}
|
|
|
|
// Position rider at mount seat
|
|
characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_);
|
|
|
|
// Rider uses character facing yaw, not mount bone rotation
|
|
// (rider faces character direction, seat bone only provides position)
|
|
float yawRad = glm::radians(characterYaw);
|
|
float riderPitch = taxiFlight_ ? mountPitch_ * 0.35f : 0.0f;
|
|
float riderRoll = taxiFlight_ ? mountRoll_ * 0.35f : 0.0f;
|
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad));
|
|
} else {
|
|
// Fallback to old manual positioning if attachment not found
|
|
mountSeatSmoothingInit_ = false;
|
|
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;
|
|
} else if (inCombat_ && grounded) {
|
|
newState = CharAnimState::COMBAT_IDLE;
|
|
}
|
|
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 if (inCombat_) {
|
|
newState = CharAnimState::COMBAT_IDLE;
|
|
} 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;
|
|
|
|
case CharAnimState::COMBAT_IDLE:
|
|
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 && sprinting) {
|
|
newState = CharAnimState::RUN;
|
|
} else if (moving) {
|
|
newState = CharAnimState::WALK;
|
|
} else if (!inCombat_) {
|
|
newState = CharAnimState::IDLE;
|
|
}
|
|
break;
|
|
|
|
case CharAnimState::CHARGE:
|
|
// Stay in CHARGE until charging_ is cleared
|
|
break;
|
|
}
|
|
|
|
if (forceMelee) {
|
|
newState = CharAnimState::MELEE_SWING;
|
|
}
|
|
|
|
if (charging_) {
|
|
newState = CharAnimState::CHARGE;
|
|
}
|
|
|
|
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;
|
|
case CharAnimState::COMBAT_IDLE:
|
|
animId = pickFirstAvailable(
|
|
{ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED},
|
|
ANIM_STAND);
|
|
loop = true;
|
|
break;
|
|
case CharAnimState::CHARGE:
|
|
animId = ANIM_RUN;
|
|
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) {
|
|
loadEmotesFromDbc();
|
|
auto it = EMOTE_TABLE.find(emoteName);
|
|
if (it == EMOTE_TABLE.end()) return;
|
|
|
|
const auto& info = it->second;
|
|
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::triggerLevelUpEffect(const glm::vec3& position) {
|
|
if (!levelUpEffect) return;
|
|
|
|
// Lazy-load the M2 model on first trigger
|
|
if (!levelUpEffect->isModelLoaded() && m2Renderer) {
|
|
if (!cachedAssetManager) {
|
|
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
|
}
|
|
if (!cachedAssetManager) {
|
|
LOG_WARNING("LevelUpEffect: no asset manager available");
|
|
} else {
|
|
auto m2Data = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp.m2");
|
|
auto skinData = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp00.skin");
|
|
LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size());
|
|
if (!m2Data.empty()) {
|
|
levelUpEffect->loadModel(m2Renderer.get(), m2Data, skinData);
|
|
} else {
|
|
LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2");
|
|
}
|
|
}
|
|
}
|
|
|
|
levelUpEffect->trigger(position);
|
|
}
|
|
|
|
void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
|
|
if (!chargeEffect) return;
|
|
|
|
// Lazy-load M2 models on first use
|
|
if (!chargeEffect->isActive() && m2Renderer) {
|
|
if (!cachedAssetManager) {
|
|
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
|
}
|
|
if (cachedAssetManager) {
|
|
chargeEffect->tryLoadM2Models(m2Renderer.get(), cachedAssetManager);
|
|
}
|
|
}
|
|
|
|
chargeEffect->start(position, direction);
|
|
}
|
|
|
|
void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
|
|
if (chargeEffect) {
|
|
chargeEffect->emit(position, direction);
|
|
}
|
|
}
|
|
|
|
void Renderer::stopChargeEffect() {
|
|
if (chargeEffect) {
|
|
chargeEffect->stop();
|
|
}
|
|
}
|
|
|
|
void Renderer::triggerMeleeSwing() {
|
|
if (!characterRenderer || characterInstanceId == 0) return;
|
|
if (meleeSwingCooldown > 0.0f) return;
|
|
if (emoteActive) {
|
|
cancelEmote();
|
|
}
|
|
resolveMeleeAnimId();
|
|
meleeSwingCooldown = 0.1f;
|
|
float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f;
|
|
if (durationSec < 0.25f) durationSec = 0.25f;
|
|
if (durationSec > 1.0f) durationSec = 1.0f;
|
|
meleeSwingTimer = durationSec;
|
|
if (activitySoundManager) {
|
|
activitySoundManager->playMeleeSwing();
|
|
}
|
|
}
|
|
|
|
std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) {
|
|
loadEmotesFromDbc();
|
|
auto it = EMOTE_TABLE.find(emoteName);
|
|
if (it != EMOTE_TABLE.end()) {
|
|
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 "";
|
|
}
|
|
|
|
uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) {
|
|
loadEmotesFromDbc();
|
|
auto it = EMOTE_TABLE.find(emoteName);
|
|
if (it != EMOTE_TABLE.end()) {
|
|
return it->second.dbcId;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName,
|
|
const std::string* targetName) {
|
|
loadEmotesFromDbc();
|
|
auto it = EMOTE_BY_DBCID.find(dbcId);
|
|
if (it == EMOTE_BY_DBCID.end()) return "";
|
|
|
|
const EmoteInfo& info = *it->second;
|
|
|
|
// Use "others see" text templates: "%s dances." / "%s dances with %s."
|
|
if (targetName && !targetName->empty()) {
|
|
if (!info.othersTarget.empty()) {
|
|
// Replace first %s with sender, second %s with target
|
|
std::string out;
|
|
out.reserve(info.othersTarget.size() + senderName.size() + targetName->size());
|
|
bool firstReplaced = false;
|
|
for (size_t i = 0; i < info.othersTarget.size(); ++i) {
|
|
if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') {
|
|
out += firstReplaced ? *targetName : senderName;
|
|
firstReplaced = true;
|
|
++i;
|
|
} else {
|
|
out.push_back(info.othersTarget[i]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
return senderName + " " + info.command + "s at " + *targetName + ".";
|
|
} else {
|
|
if (!info.othersNoTarget.empty()) {
|
|
return replacePlaceholders(info.othersNoTarget, &senderName);
|
|
}
|
|
return senderName + " " + info.command + "s.";
|
|
}
|
|
}
|
|
|
|
uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) {
|
|
loadEmotesFromDbc();
|
|
auto it = EMOTE_BY_DBCID.find(dbcId);
|
|
if (it != EMOTE_BY_DBCID.end()) {
|
|
return it->second->animId;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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) {
|
|
if (musicSwitchCooldown_ > 0.0f) {
|
|
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - 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();
|
|
|
|
// Visibility hardening: ensure player instance cannot stay hidden after
|
|
// taxi/camera transitions, but preserve first-person self-hide.
|
|
if (characterRenderer && characterInstanceId > 0 && cameraController) {
|
|
if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || taxiFlight_) {
|
|
characterRenderer->setInstanceVisible(characterInstanceId, true);
|
|
}
|
|
}
|
|
|
|
// Update lighting system
|
|
auto light1 = std::chrono::high_resolution_clock::now();
|
|
if (lightingManager) {
|
|
const auto* gh = core::Application::getInstance().getGameHandler();
|
|
uint32_t mapId = gh ? gh->getCurrentMapId() : 0;
|
|
float gameTime = gh ? gh->getGameTime() : -1.0f;
|
|
bool isRaining = gh ? gh->isRaining() : false;
|
|
bool isUnderwater = cameraController ? cameraController->isSwimming() : false;
|
|
|
|
lightingManager->update(characterPosition, mapId, gameTime, isRaining, isUnderwater);
|
|
|
|
// Sync weather visual renderer with game state
|
|
if (weather && gh) {
|
|
uint32_t wType = gh->getWeatherType();
|
|
float wInt = gh->getWeatherIntensity();
|
|
if (wType == 1) weather->setWeatherType(Weather::Type::RAIN);
|
|
else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW);
|
|
else weather->setWeatherType(Weather::Type::NONE);
|
|
weather->setIntensity(wInt);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// Update level-up effect
|
|
if (levelUpEffect) {
|
|
levelUpEffect->update(deltaTime);
|
|
}
|
|
// Update charge effect
|
|
if (chargeEffect) {
|
|
chargeEffect->update(deltaTime);
|
|
}
|
|
|
|
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();
|
|
|
|
// Helper: play zone music, dispatching local files (file: prefix) vs MPQ paths
|
|
auto playZoneMusic = [&](const std::string& music) {
|
|
if (music.empty()) return;
|
|
if (music.rfind("file:", 0) == 0) {
|
|
musicManager->crossfadeToFile(music.substr(5));
|
|
} else {
|
|
musicManager->crossfadeTo(music);
|
|
}
|
|
};
|
|
|
|
// 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
|
|
musicSwitchCooldown_ = 6.0f;
|
|
}
|
|
} 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()) {
|
|
playZoneMusic(music);
|
|
musicSwitchCooldown_ = 6.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()) {
|
|
playZoneMusic(music);
|
|
musicSwitchCooldown_ = 6.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
if (musicSwitchCooldown_ <= 0.0f) {
|
|
std::string music = zoneManager->getRandomMusic(zoneId);
|
|
if (!music.empty()) {
|
|
playZoneMusic(music);
|
|
musicSwitchCooldown_ = 6.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
musicManager->update(deltaTime);
|
|
|
|
// When a track finishes, pick a new random track from the current zone
|
|
if (!musicManager->isPlaying() && !inTavern_ && !inBlacksmith_ &&
|
|
currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) {
|
|
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
|
if (!music.empty()) {
|
|
playZoneMusic(music);
|
|
musicSwitchCooldown_ = 2.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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_DEBUG("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;
|
|
|
|
// Selection effect shader: thin outer ring + inward fade toward center.
|
|
const char* vsSrc = R"(
|
|
#version 330 core
|
|
layout(location = 0) in vec3 aPos;
|
|
uniform mat4 uMVP;
|
|
out vec2 vLocalPos;
|
|
void main() {
|
|
vLocalPos = aPos.xy;
|
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
|
}
|
|
)";
|
|
const char* fsSrc = R"(
|
|
#version 330 core
|
|
uniform vec3 uColor;
|
|
in vec2 vLocalPos;
|
|
out vec4 FragColor;
|
|
void main() {
|
|
float r = clamp(length(vLocalPos), 0.0, 1.0);
|
|
|
|
float ringInner = 0.93;
|
|
float ringOuter = 1.00;
|
|
float ring = smoothstep(ringInner - 0.01, ringInner + 0.01, r) *
|
|
(1.0 - smoothstep(ringOuter - 0.008, ringOuter + 0.004, r));
|
|
|
|
float inward = smoothstep(0.0, ringInner, r);
|
|
inward = pow(inward, 1.9) * (1.0 - smoothstep(ringInner - 0.015, ringInner + 0.01, r));
|
|
|
|
float alpha = max(ring * 0.9, inward * 0.45);
|
|
FragColor = vec4(uColor, alpha);
|
|
}
|
|
)";
|
|
|
|
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 a unit disc; fragment shader shapes ring+gradient by radius.
|
|
constexpr int SEGMENTS = 48;
|
|
std::vector<float> verts;
|
|
verts.reserve((SEGMENTS + 2) * 3);
|
|
|
|
verts.push_back(0.0f);
|
|
verts.push_back(0.0f);
|
|
verts.push_back(0.0f);
|
|
|
|
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);
|
|
verts.push_back(c);
|
|
verts.push_back(s);
|
|
verts.push_back(0.0f);
|
|
}
|
|
selCircleVertCount = static_cast<int>(SEGMENTS + 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();
|
|
|
|
// Clamp the circle to the best floor estimate at target XY to avoid clipping into
|
|
// terrain/WMO/M2 surfaces, then keep a small visual lift above that plane.
|
|
float floorZ = selCirclePos.z;
|
|
if (terrainManager) {
|
|
auto terrainH = terrainManager->getHeightAt(selCirclePos.x, selCirclePos.y);
|
|
if (terrainH) floorZ = std::max(floorZ, *terrainH);
|
|
}
|
|
if (wmoRenderer) {
|
|
auto wmoH = wmoRenderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 3.0f);
|
|
if (wmoH) floorZ = std::max(floorZ, *wmoH);
|
|
}
|
|
if (m2Renderer) {
|
|
auto m2H = m2Renderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 2.0f);
|
|
if (m2H) floorZ = std::max(floorZ, *m2H);
|
|
}
|
|
|
|
glm::vec3 raisedPos = selCirclePos;
|
|
raisedPos.z = floorZ + 0.17f;
|
|
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);
|
|
GLboolean depthTestWasEnabled = glIsEnabled(GL_DEPTH_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
|
|
glUseProgram(selCircleShader);
|
|
glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]);
|
|
glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]);
|
|
|
|
glBindVertexArray(selCircleVAO);
|
|
glDrawArrays(GL_TRIANGLE_FAN, 0, selCircleVertCount);
|
|
glBindVertexArray(0);
|
|
|
|
if (depthTestWasEnabled) glEnable(GL_DEPTH_TEST);
|
|
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) — throttled to every 2 frames (depth buffer persists)
|
|
if (shadowsEnabled && shadowFBO && shadowShaderProgram && terrainLoaded) {
|
|
if (shadowFrameCounter_++ % 2 == 0) {
|
|
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
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
if (weather && camera) {
|
|
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);
|
|
}
|
|
|
|
// Render charge effect (red haze + dust)
|
|
if (chargeEffect && camera) {
|
|
chargeEffect->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 selection circle before model passes: this keeps it visible through terrain
|
|
// (depth test off in its pass), while characters/WMO/M2 still draw over it.
|
|
renderSelectionCircle(view, projection);
|
|
|
|
// Render characters (after weather)
|
|
if (characterRenderer && camera) {
|
|
characterRenderer->render(*camera, view, projection);
|
|
}
|
|
|
|
// Render WMO buildings (after characters, before UI)
|
|
if (wmoRenderer && camera) {
|
|
auto wmoStart = std::chrono::steady_clock::now();
|
|
wmoRenderer->render(*camera, view, projection);
|
|
auto wmoEnd = std::chrono::steady_clock::now();
|
|
lastWMORenderMs = std::chrono::duration<double, std::milli>(wmoEnd - wmoStart).count();
|
|
}
|
|
|
|
// Render M2 doodads (trees, rocks, etc.)
|
|
if (m2Renderer && camera) {
|
|
// 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);
|
|
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) {
|
|
audio::AudioEngine::instance().setAssetManager(assetManager);
|
|
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);
|
|
}
|
|
|
|
// Prewarm frequently used zone/tavern music so zone transitions don't stall on MPQ I/O.
|
|
if (zoneManager) {
|
|
for (const auto& musicPath : zoneManager->getAllMusicPaths()) {
|
|
musicManager->preloadMusic(musicPath);
|
|
}
|
|
}
|
|
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",
|
|
};
|
|
for (const auto& musicPath : tavernTracks) {
|
|
musicManager->preloadMusic(musicPath);
|
|
}
|
|
|
|
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 (only chunks within shadow frustum)
|
|
if (terrainRenderer) {
|
|
glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition;
|
|
terrainRenderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent);
|
|
}
|
|
|
|
// 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 (only instances within shadow frustum)
|
|
if (m2Renderer) {
|
|
glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition;
|
|
m2Renderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent);
|
|
}
|
|
|
|
// 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
|