2026-02-02 12:24:50 -08:00
|
|
|
#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"
|
2026-02-10 13:48:50 -08:00
|
|
|
#include "rendering/lighting_manager.hpp"
|
2026-02-10 19:30:45 -08:00
|
|
|
#include "rendering/sky_system.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/swim_effects.hpp"
|
2026-02-09 01:24:17 -08:00
|
|
|
#include "rendering/mount_dust.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/character_renderer.hpp"
|
|
|
|
|
#include "rendering/wmo_renderer.hpp"
|
|
|
|
|
#include "rendering/m2_renderer.hpp"
|
|
|
|
|
#include "rendering/minimap.hpp"
|
2026-02-09 23:41:38 -08:00
|
|
|
#include "rendering/quest_marker_renderer.hpp"
|
2026-02-03 20:40:59 -08:00
|
|
|
#include "rendering/shader.hpp"
|
2026-02-10 19:30:45 -08:00
|
|
|
#include "game/game_handler.hpp"
|
2026-02-05 14:01:26 -08:00
|
|
|
#include "pipeline/m2_loader.hpp"
|
|
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
2026-02-07 20:02:14 -08:00
|
|
|
#include "pipeline/dbc_loader.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "pipeline/m2_loader.hpp"
|
|
|
|
|
#include "pipeline/wmo_loader.hpp"
|
|
|
|
|
#include "pipeline/adt_loader.hpp"
|
|
|
|
|
#include "pipeline/terrain_mesh.hpp"
|
2026-02-07 20:02:14 -08:00
|
|
|
#include "core/application.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/window.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include "game/world.hpp"
|
|
|
|
|
#include "game/zone_manager.hpp"
|
2026-02-09 00:40:50 -08:00
|
|
|
#include "audio/audio_engine.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "audio/music_manager.hpp"
|
2026-02-03 14:55:32 -08:00
|
|
|
#include "audio/footstep_manager.hpp"
|
2026-02-03 19:49:56 -08:00
|
|
|
#include "audio/activity_sound_manager.hpp"
|
2026-02-09 01:04:53 -08:00
|
|
|
#include "audio/mount_sound_manager.hpp"
|
2026-02-09 01:29:44 -08:00
|
|
|
#include "audio/npc_voice_manager.hpp"
|
2026-02-09 14:50:14 -08:00
|
|
|
#include "audio/ambient_sound_manager.hpp"
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
#include "audio/ui_sound_manager.hpp"
|
|
|
|
|
#include "audio/combat_sound_manager.hpp"
|
|
|
|
|
#include "audio/spell_sound_manager.hpp"
|
|
|
|
|
#include "audio/movement_sound_manager.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <GL/glew.h>
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
#include <glm/gtx/euler_angles.hpp>
|
|
|
|
|
#include <glm/gtc/quaternion.hpp>
|
|
|
|
|
#include <cctype>
|
2026-02-03 14:55:32 -08:00
|
|
|
#include <cmath>
|
2026-02-03 16:21:48 -08:00
|
|
|
#include <chrono>
|
2026-02-03 14:55:32 -08:00
|
|
|
#include <optional>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <unordered_set>
|
2026-02-10 19:30:45 -08:00
|
|
|
#include <set>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
|
|
|
|
struct EmoteInfo {
|
2026-02-07 20:02:14 -08:00
|
|
|
uint32_t animId = 0;
|
|
|
|
|
bool loop = false;
|
|
|
|
|
std::string textNoTarget;
|
|
|
|
|
std::string textTarget;
|
|
|
|
|
std::string command;
|
2026-02-02 12:24:50 -08:00
|
|
|
};
|
|
|
|
|
|
2026-02-07 20:02:14 -08:00
|
|
|
static std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE;
|
|
|
|
|
static bool emoteTableLoaded = false;
|
|
|
|
|
|
|
|
|
|
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
std::string cur;
|
|
|
|
|
for (char c : raw) {
|
|
|
|
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
|
|
|
|
|
cur.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
|
|
|
|
} else if (!cur.empty()) {
|
|
|
|
|
out.push_back(cur);
|
|
|
|
|
cur.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!cur.empty()) out.push_back(cur);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool isLoopingEmote(const std::string& command) {
|
|
|
|
|
static const std::unordered_set<std::string> kLooping = {
|
|
|
|
|
"dance",
|
|
|
|
|
"train",
|
|
|
|
|
};
|
|
|
|
|
return kLooping.find(command) != kLooping.end();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void loadFallbackEmotes() {
|
|
|
|
|
if (!EMOTE_TABLE.empty()) return;
|
|
|
|
|
EMOTE_TABLE = {
|
|
|
|
|
{"wave", {67, false, "You wave.", "You wave at %s.", "wave"}},
|
|
|
|
|
{"bow", {66, false, "You bow down graciously.", "You bow down before %s.", "bow"}},
|
|
|
|
|
{"laugh", {70, false, "You laugh.", "You laugh at %s.", "laugh"}},
|
|
|
|
|
{"point", {84, false, "You point over yonder.", "You point at %s.", "point"}},
|
|
|
|
|
{"cheer", {68, false, "You cheer!", "You cheer at %s.", "cheer"}},
|
|
|
|
|
{"dance", {69, true, "You burst into dance.", "You dance with %s.", "dance"}},
|
|
|
|
|
{"kneel", {75, false, "You kneel down.", "You kneel before %s.", "kneel"}},
|
|
|
|
|
{"applaud", {80, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "applaud"}},
|
|
|
|
|
{"shout", {81, false, "You shout.", "You shout at %s.", "shout"}},
|
|
|
|
|
{"chicken", {78, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
|
|
|
|
|
"With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", "chicken"}},
|
|
|
|
|
{"cry", {77, false, "You cry.", "You cry on %s's shoulder.", "cry"}},
|
|
|
|
|
{"kiss", {76, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "kiss"}},
|
|
|
|
|
{"roar", {74, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "roar"}},
|
|
|
|
|
{"salute", {113, false, "You salute.", "You salute %s with respect.", "salute"}},
|
|
|
|
|
{"rude", {73, false, "You make a rude gesture.", "You make a rude gesture at %s.", "rude"}},
|
|
|
|
|
{"flex", {82, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "flex"}},
|
|
|
|
|
{"shy", {83, false, "You smile shyly.", "You smile shyly at %s.", "shy"}},
|
|
|
|
|
{"beg", {79, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "beg"}},
|
|
|
|
|
{"eat", {61, false, "You begin to eat.", "You begin to eat in front of %s.", "eat"}},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string replacePlaceholders(const std::string& text, const std::string* targetName) {
|
|
|
|
|
if (text.empty()) return text;
|
|
|
|
|
std::string out;
|
|
|
|
|
out.reserve(text.size() + 16);
|
|
|
|
|
for (size_t i = 0; i < text.size(); ++i) {
|
|
|
|
|
if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') {
|
|
|
|
|
if (targetName && !targetName->empty()) out += *targetName;
|
|
|
|
|
i++;
|
|
|
|
|
} else {
|
|
|
|
|
out.push_back(text[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void loadEmotesFromDbc() {
|
|
|
|
|
if (emoteTableLoaded) return;
|
|
|
|
|
emoteTableLoaded = true;
|
|
|
|
|
|
|
|
|
|
auto* assetManager = core::Application::getInstance().getAssetManager();
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
LOG_WARNING("Emotes: no AssetManager");
|
|
|
|
|
loadFallbackEmotes();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc");
|
|
|
|
|
auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc");
|
|
|
|
|
if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) {
|
|
|
|
|
LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)");
|
|
|
|
|
loadFallbackEmotes();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_map<uint32_t, std::string> textData;
|
|
|
|
|
textData.reserve(emotesTextDataDbc->getRecordCount());
|
|
|
|
|
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
|
|
|
|
|
uint32_t id = emotesTextDataDbc->getUInt32(r, 0);
|
|
|
|
|
std::string text = emotesTextDataDbc->getString(r, 1);
|
|
|
|
|
if (!text.empty()) textData.emplace(id, std::move(text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_map<uint32_t, uint32_t> emoteIdToAnim;
|
|
|
|
|
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
|
|
|
|
|
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
|
|
|
|
|
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
|
|
|
|
|
uint32_t emoteId = emotesDbc->getUInt32(r, 0);
|
|
|
|
|
uint32_t animId = emotesDbc->getUInt32(r, 2);
|
|
|
|
|
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMOTE_TABLE.clear();
|
|
|
|
|
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
|
|
|
|
|
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
|
|
|
|
|
std::string cmdRaw = emotesTextDbc->getString(r, 1);
|
|
|
|
|
if (cmdRaw.empty()) continue;
|
|
|
|
|
|
|
|
|
|
uint32_t emoteRef = emotesTextDbc->getUInt32(r, 2);
|
|
|
|
|
uint32_t animId = 0;
|
|
|
|
|
auto animIt = emoteIdToAnim.find(emoteRef);
|
|
|
|
|
if (animIt != emoteIdToAnim.end()) {
|
|
|
|
|
animId = animIt->second;
|
|
|
|
|
} else {
|
|
|
|
|
animId = emoteRef; // fallback if EmotesText stores animation id directly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, 5); // unisex, target, sender
|
|
|
|
|
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, 9); // unisex, no target, sender
|
|
|
|
|
|
|
|
|
|
std::string textTarget;
|
|
|
|
|
std::string textNoTarget;
|
|
|
|
|
if (auto it = textData.find(senderTargetTextId); it != textData.end()) {
|
|
|
|
|
textTarget = it->second;
|
|
|
|
|
}
|
|
|
|
|
if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) {
|
|
|
|
|
textNoTarget = it->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const std::string& cmd : parseEmoteCommands(cmdRaw)) {
|
|
|
|
|
if (cmd.empty()) continue;
|
|
|
|
|
EmoteInfo info;
|
|
|
|
|
info.animId = animId;
|
|
|
|
|
info.loop = isLoopingEmote(cmd);
|
|
|
|
|
info.textNoTarget = textNoTarget;
|
|
|
|
|
info.textTarget = textTarget;
|
|
|
|
|
info.command = cmd;
|
|
|
|
|
EMOTE_TABLE.emplace(cmd, std::move(info));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (EMOTE_TABLE.empty()) {
|
|
|
|
|
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
|
|
|
|
|
loadFallbackEmotes();
|
|
|
|
|
} else {
|
|
|
|
|
LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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());
|
2026-02-02 23:03:45 -08:00
|
|
|
cameraController->setUseWoWSpeed(true); // Use realistic WoW movement speed
|
2026-02-02 12:24:50 -08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// 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)");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Create weather system
|
|
|
|
|
weather = std::make_unique<Weather>();
|
|
|
|
|
if (!weather->initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize weather");
|
|
|
|
|
weather.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:48:50 -08:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Create swim effects
|
|
|
|
|
swimEffects = std::make_unique<SwimEffects>();
|
|
|
|
|
if (!swimEffects->initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize swim effects");
|
|
|
|
|
swimEffects.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:24:17 -08:00
|
|
|
// Create mount dust effects
|
|
|
|
|
mountDust = std::make_unique<MountDust>();
|
|
|
|
|
if (!mountDust->initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize mount dust effects");
|
|
|
|
|
mountDust.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Create character renderer
|
|
|
|
|
characterRenderer = std::make_unique<CharacterRenderer>();
|
|
|
|
|
if (!characterRenderer->initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize character renderer");
|
|
|
|
|
characterRenderer.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create WMO renderer
|
|
|
|
|
wmoRenderer = std::make_unique<WMORenderer>();
|
|
|
|
|
if (!wmoRenderer->initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize WMO renderer");
|
|
|
|
|
wmoRenderer.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create minimap
|
|
|
|
|
minimap = std::make_unique<Minimap>();
|
|
|
|
|
if (!minimap->initialize(200)) {
|
|
|
|
|
LOG_WARNING("Failed to initialize minimap");
|
|
|
|
|
minimap.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 23:41:38 -08:00
|
|
|
// Create quest marker renderer (initialized later with AssetManager)
|
|
|
|
|
questMarkerRenderer = std::make_unique<QuestMarkerRenderer>();
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// 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();
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Initialize AudioEngine (singleton)
|
|
|
|
|
if (!audio::AudioEngine::instance().initialize()) {
|
|
|
|
|
LOG_WARNING("Failed to initialize AudioEngine - audio will be disabled");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Create music manager (initialized later with asset manager)
|
|
|
|
|
musicManager = std::make_unique<audio::MusicManager>();
|
2026-02-03 14:55:32 -08:00
|
|
|
footstepManager = std::make_unique<audio::FootstepManager>();
|
2026-02-03 19:49:56 -08:00
|
|
|
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
|
2026-02-09 01:04:53 -08:00
|
|
|
mountSoundManager = std::make_unique<audio::MountSoundManager>();
|
2026-02-09 01:29:44 -08:00
|
|
|
npcVoiceManager = std::make_unique<audio::NpcVoiceManager>();
|
2026-02-09 14:50:14 -08:00
|
|
|
ambientSoundManager = std::make_unique<audio::AmbientSoundManager>();
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
uiSoundManager = std::make_unique<audio::UiSoundManager>();
|
|
|
|
|
combatSoundManager = std::make_unique<audio::CombatSoundManager>();
|
|
|
|
|
spellSoundManager = std::make_unique<audio::SpellSoundManager>();
|
|
|
|
|
movementSoundManager = std::make_unique<audio::MovementSoundManager>();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
// Initialize post-process FBO pipeline
|
|
|
|
|
initPostProcess(window->getWidth(), window->getHeight());
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// Initialize shadow map
|
|
|
|
|
initShadowMap();
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-03 14:55:32 -08:00
|
|
|
if (footstepManager) {
|
|
|
|
|
footstepManager->shutdown();
|
|
|
|
|
footstepManager.reset();
|
|
|
|
|
}
|
2026-02-03 19:49:56 -08:00
|
|
|
if (activitySoundManager) {
|
|
|
|
|
activitySoundManager->shutdown();
|
|
|
|
|
activitySoundManager.reset();
|
|
|
|
|
}
|
2026-02-09 00:40:50 -08:00
|
|
|
|
|
|
|
|
// Shutdown AudioEngine singleton
|
|
|
|
|
audio::AudioEngine::instance().shutdown();
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
if (underwaterOverlayVAO) {
|
|
|
|
|
glDeleteVertexArrays(1, &underwaterOverlayVAO);
|
|
|
|
|
underwaterOverlayVAO = 0;
|
|
|
|
|
}
|
|
|
|
|
if (underwaterOverlayVBO) {
|
|
|
|
|
glDeleteBuffers(1, &underwaterOverlayVBO);
|
|
|
|
|
underwaterOverlayVBO = 0;
|
|
|
|
|
}
|
|
|
|
|
underwaterOverlayShader.reset();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// Cleanup shadow map resources
|
|
|
|
|
if (shadowFBO) { glDeleteFramebuffers(1, &shadowFBO); shadowFBO = 0; }
|
|
|
|
|
if (shadowDepthTex) { glDeleteTextures(1, &shadowDepthTex); shadowDepthTex = 0; }
|
|
|
|
|
if (shadowShaderProgram) { glDeleteProgram(shadowShaderProgram); shadowShaderProgram = 0; }
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
shutdownPostProcess();
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
zoneManager.reset();
|
|
|
|
|
|
|
|
|
|
performanceHUD.reset();
|
|
|
|
|
scene.reset();
|
|
|
|
|
cameraController.reset();
|
|
|
|
|
camera.reset();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Renderer shutdown");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::beginFrame() {
|
2026-02-04 15:21:04 -08:00
|
|
|
// 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);
|
2026-02-02 12:24:50 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:19:35 -08:00
|
|
|
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
2026-02-07 17:59:40 -08:00
|
|
|
mountInstanceId_ = mountInstId;
|
|
|
|
|
mountHeightOffset_ = heightOffset;
|
2026-02-10 19:30:45 -08:00
|
|
|
mountAction_ = MountAction::None; // Clear mount action state
|
|
|
|
|
mountActionPhase_ = 0;
|
2026-02-07 17:59:40 -08:00
|
|
|
charAnimState = CharAnimState::MOUNT;
|
2026-02-07 20:05:07 -08:00
|
|
|
if (cameraController) {
|
|
|
|
|
cameraController->setMounted(true);
|
|
|
|
|
cameraController->setMountHeightOffset(heightOffset);
|
|
|
|
|
}
|
2026-02-09 01:19:35 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Debug: dump available mount animations
|
|
|
|
|
if (characterRenderer && mountInstId > 0) {
|
|
|
|
|
characterRenderer->dumpAnimations(mountInstId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Discover mount animation capabilities (property-based, not hardcoded IDs)
|
|
|
|
|
LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
|
|
|
|
|
characterRenderer->dumpAnimations(mountInstId);
|
|
|
|
|
|
|
|
|
|
// Get all sequences for property-based analysis
|
|
|
|
|
std::vector<pipeline::M2Sequence> sequences;
|
|
|
|
|
if (!characterRenderer->getAnimationSequences(mountInstId, sequences)) {
|
|
|
|
|
LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs");
|
|
|
|
|
sequences.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: ID-based fallback finder
|
|
|
|
|
auto findFirst = [&](std::initializer_list<uint32_t> candidates) -> uint32_t {
|
|
|
|
|
for (uint32_t id : candidates) {
|
|
|
|
|
if (characterRenderer->hasAnimation(mountInstId, id)) {
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Property-based jump animation discovery with chain-based scoring
|
|
|
|
|
auto discoverJumpSet = [&]() {
|
|
|
|
|
// Debug: log all sequences for analysis
|
|
|
|
|
LOG_INFO("=== Full sequence table for mount ===");
|
|
|
|
|
for (const auto& seq : sequences) {
|
|
|
|
|
LOG_INFO("SEQ id=", seq.id,
|
|
|
|
|
" dur=", seq.duration,
|
|
|
|
|
" flags=0x", std::hex, seq.flags, std::dec,
|
|
|
|
|
" moveSpd=", seq.movingSpeed,
|
|
|
|
|
" blend=", seq.blendTime,
|
|
|
|
|
" next=", seq.nextAnimation,
|
|
|
|
|
" alias=", seq.aliasNext);
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("=== End sequence table ===");
|
|
|
|
|
|
|
|
|
|
// Known combat/bad animation IDs to avoid
|
|
|
|
|
std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack
|
|
|
|
|
|
|
|
|
|
auto scoreNear = [](int a, int b) -> int {
|
|
|
|
|
int d = std::abs(a - b);
|
|
|
|
|
return (d <= 8) ? (20 - d) : 0; // within 8 IDs gets points
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto isForbidden = [&](uint32_t id) {
|
|
|
|
|
return forbiddenIds.count(id) != 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* {
|
|
|
|
|
for (const auto& s : sequences) {
|
|
|
|
|
if (s.id == id) return &s;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
uint32_t runId = findFirst({5, 4});
|
|
|
|
|
uint32_t standId = findFirst({0});
|
|
|
|
|
|
|
|
|
|
// Step A: Find loop candidates
|
|
|
|
|
std::vector<uint32_t> loops;
|
|
|
|
|
for (const auto& seq : sequences) {
|
|
|
|
|
if (isForbidden(seq.id)) continue;
|
|
|
|
|
// Bit 0x01 NOT set = loops (0x20, 0x60), bit 0x01 set = non-looping (0x21, 0x61)
|
|
|
|
|
bool isLoop = (seq.flags & 0x01) == 0;
|
|
|
|
|
if (isLoop && seq.duration >= 350 && seq.duration <= 1000 &&
|
|
|
|
|
seq.id != runId && seq.id != standId) {
|
|
|
|
|
loops.push_back(seq.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Choose loop: prefer one near known classic IDs (38), else best duration
|
|
|
|
|
uint32_t loop = 0;
|
|
|
|
|
if (!loops.empty()) {
|
|
|
|
|
uint32_t best = loops[0];
|
|
|
|
|
int bestScore = -999;
|
|
|
|
|
for (uint32_t id : loops) {
|
|
|
|
|
int sc = 0;
|
|
|
|
|
sc += scoreNear((int)id, 38); // classic hint
|
|
|
|
|
const auto* s = findSeqById(id);
|
|
|
|
|
if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0;
|
|
|
|
|
if (sc > bestScore) {
|
|
|
|
|
bestScore = sc;
|
|
|
|
|
best = id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
loop = best;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step B: Score start/end candidates
|
|
|
|
|
uint32_t start = 0, end = 0;
|
|
|
|
|
int bestStart = -999, bestEnd = -999;
|
|
|
|
|
|
|
|
|
|
for (const auto& seq : sequences) {
|
|
|
|
|
if (isForbidden(seq.id)) continue;
|
|
|
|
|
// Only consider non-looping animations for start/end
|
|
|
|
|
bool isLoop = (seq.flags & 0x01) == 0;
|
|
|
|
|
if (isLoop) continue;
|
|
|
|
|
|
|
|
|
|
// Start window
|
|
|
|
|
if (seq.duration >= 450 && seq.duration <= 1100) {
|
|
|
|
|
int sc = 0;
|
|
|
|
|
if (loop) sc += scoreNear((int)seq.id, (int)loop);
|
|
|
|
|
// Chain bonus: if this start points at loop or near it
|
|
|
|
|
if (loop && (seq.nextAnimation == (int16_t)loop || seq.aliasNext == loop)) sc += 30;
|
|
|
|
|
if (loop && scoreNear(seq.nextAnimation, (int)loop) > 0) sc += 10;
|
|
|
|
|
// Penalize "stop/brake-ish": very long blendTime can be a stop transition
|
|
|
|
|
if (seq.blendTime > 400) sc -= 5;
|
|
|
|
|
|
|
|
|
|
if (sc > bestStart) {
|
|
|
|
|
bestStart = sc;
|
|
|
|
|
start = seq.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End window
|
|
|
|
|
if (seq.duration >= 650 && seq.duration <= 1600) {
|
|
|
|
|
int sc = 0;
|
|
|
|
|
if (loop) sc += scoreNear((int)seq.id, (int)loop);
|
|
|
|
|
// Chain bonus: end often points to run/stand or has no next
|
|
|
|
|
if (seq.nextAnimation == (int16_t)runId || seq.nextAnimation == (int16_t)standId) sc += 10;
|
|
|
|
|
if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal
|
|
|
|
|
if (sc > bestEnd) {
|
|
|
|
|
bestEnd = sc;
|
|
|
|
|
end = seq.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
|
|
|
|
|
" scores: start=", bestStart, " end=", bestEnd);
|
|
|
|
|
return std::make_tuple(start, loop, end);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet();
|
|
|
|
|
|
|
|
|
|
// Use discovered animations, fallback to known IDs if discovery fails
|
|
|
|
|
mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37});
|
|
|
|
|
mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38});
|
|
|
|
|
mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39});
|
|
|
|
|
mountAnims_.rearUp = findFirst({94, 92, 40}); // RearUp/Special
|
|
|
|
|
mountAnims_.run = findFirst({5, 4}); // Run/Walk
|
|
|
|
|
mountAnims_.stand = findFirst({0}); // Stand (almost always 0)
|
|
|
|
|
|
2026-02-10 20:13:42 -08:00
|
|
|
// Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers)
|
2026-02-10 19:53:23 -08:00
|
|
|
mountAnims_.fidgets.clear();
|
2026-02-10 19:59:01 -08:00
|
|
|
core::Logger::getInstance().info("Scanning for fidget animations in ", sequences.size(), " sequences");
|
2026-02-10 20:01:32 -08:00
|
|
|
|
2026-02-10 20:13:42 -08:00
|
|
|
// Proper fidget discovery: frequency > 0 + replay timers indicate random idle animations
|
2026-02-10 20:01:32 -08:00
|
|
|
for (const auto& seq : sequences) {
|
|
|
|
|
bool isLoop = (seq.flags & 0x01) == 0;
|
2026-02-10 20:13:42 -08:00
|
|
|
bool hasFrequency = seq.frequency > 0;
|
|
|
|
|
bool hasReplay = seq.replayMax > 0;
|
|
|
|
|
bool isStationary = std::abs(seq.movingSpeed) < 0.05f;
|
|
|
|
|
bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500;
|
|
|
|
|
|
|
|
|
|
// Log candidates with metadata
|
|
|
|
|
if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) {
|
|
|
|
|
core::Logger::getInstance().info(" Candidate: id=", seq.id,
|
|
|
|
|
" dur=", seq.duration, "ms",
|
|
|
|
|
" freq=", seq.frequency,
|
|
|
|
|
" replay=", seq.replayMin, "-", seq.replayMax,
|
|
|
|
|
" next=", seq.nextAnimation,
|
|
|
|
|
" speed=", seq.movingSpeed);
|
2026-02-10 20:01:32 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:13:42 -08:00
|
|
|
// Select fidgets: non-looping + (frequency OR replay) + stationary
|
|
|
|
|
// Relaxed: some mounts may only have one of the markers
|
|
|
|
|
if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration) {
|
|
|
|
|
// Bonus: chains back to stand (indicates idle behavior)
|
|
|
|
|
bool chainsToStand = (seq.nextAnimation == (int16_t)mountAnims_.stand) ||
|
|
|
|
|
(seq.aliasNext == mountAnims_.stand) ||
|
|
|
|
|
(seq.nextAnimation == -1);
|
|
|
|
|
|
2026-02-10 19:53:23 -08:00
|
|
|
mountAnims_.fidgets.push_back(seq.id);
|
2026-02-10 20:13:42 -08:00
|
|
|
core::Logger::getInstance().info(" >> Selected fidget: id=", seq.id,
|
|
|
|
|
(chainsToStand ? " (chains to stand)" : ""));
|
2026-02-10 19:53:23 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Ensure we have fallbacks for movement
|
|
|
|
|
if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found
|
|
|
|
|
if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run
|
|
|
|
|
|
|
|
|
|
core::Logger::getInstance().info("Mount animation set: jumpStart=", mountAnims_.jumpStart,
|
|
|
|
|
" jumpLoop=", mountAnims_.jumpLoop,
|
|
|
|
|
" jumpEnd=", mountAnims_.jumpEnd,
|
|
|
|
|
" rearUp=", mountAnims_.rearUp,
|
|
|
|
|
" run=", mountAnims_.run,
|
2026-02-10 19:53:23 -08:00
|
|
|
" stand=", mountAnims_.stand,
|
|
|
|
|
" fidgets=", mountAnims_.fidgets.size());
|
2026-02-10 19:30:45 -08:00
|
|
|
|
2026-02-09 01:19:35 -08:00
|
|
|
// Notify mount sound manager
|
|
|
|
|
if (mountSoundManager) {
|
|
|
|
|
bool isFlying = taxiFlight_; // Taxi flights are flying mounts
|
|
|
|
|
mountSoundManager->onMount(mountDisplayId, isFlying);
|
|
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::clearMount() {
|
|
|
|
|
mountInstanceId_ = 0;
|
|
|
|
|
mountHeightOffset_ = 0.0f;
|
2026-02-08 22:05:38 -08:00
|
|
|
mountPitch_ = 0.0f;
|
|
|
|
|
mountRoll_ = 0.0f;
|
2026-02-10 19:30:45 -08:00
|
|
|
mountAction_ = MountAction::None;
|
|
|
|
|
mountActionPhase_ = 0;
|
2026-02-07 17:59:40 -08:00
|
|
|
charAnimState = CharAnimState::IDLE;
|
2026-02-07 20:05:07 -08:00
|
|
|
if (cameraController) {
|
|
|
|
|
cameraController->setMounted(false);
|
|
|
|
|
cameraController->setMountHeightOffset(0.0f);
|
|
|
|
|
}
|
2026-02-09 01:19:35 -08:00
|
|
|
|
|
|
|
|
// Notify mount sound manager
|
|
|
|
|
if (mountSoundManager) {
|
|
|
|
|
mountSoundManager->onDismount();
|
|
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:01:26 -08:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
// 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];
|
2026-02-05 14:01:26 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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;
|
2026-02-03 19:29:11 -08:00
|
|
|
// 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;
|
2026-02-02 12:24:50 -08:00
|
|
|
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)
|
2026-02-07 17:59:40 -08:00
|
|
|
constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
CharAnimState newState = charAnimState;
|
|
|
|
|
|
|
|
|
|
bool moving = cameraController->isMoving();
|
2026-02-03 19:29:11 -08:00
|
|
|
bool movingBackward = cameraController->isMovingBackward();
|
|
|
|
|
bool strafeLeft = cameraController->isStrafingLeft();
|
|
|
|
|
bool strafeRight = cameraController->isStrafingRight();
|
|
|
|
|
bool anyStrafeLeft = strafeLeft && !strafeRight;
|
|
|
|
|
bool anyStrafeRight = strafeRight && !strafeLeft;
|
2026-02-02 12:24:50 -08:00
|
|
|
bool grounded = cameraController->isGrounded();
|
|
|
|
|
bool jumping = cameraController->isJumping();
|
|
|
|
|
bool sprinting = cameraController->isSprinting();
|
|
|
|
|
bool sitting = cameraController->isSitting();
|
|
|
|
|
bool swim = cameraController->isSwimming();
|
2026-02-05 14:01:26 -08:00
|
|
|
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 17:59:40 -08:00
|
|
|
// 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
|
2026-02-07 18:38:36 -08:00
|
|
|
float mountBob = 0.0f;
|
2026-02-07 17:59:40 -08:00
|
|
|
if (mountInstanceId_ > 0) {
|
|
|
|
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
|
|
|
|
float yawRad = glm::radians(characterYaw);
|
2026-02-10 19:30:45 -08:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:05:38 -08:00
|
|
|
// Apply pitch (up/down), roll (banking), and yaw for realistic flight
|
|
|
|
|
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad));
|
2026-02-07 17:59:40 -08:00
|
|
|
|
|
|
|
|
// Drive mount model animation: idle when still, run when moving
|
2026-02-07 20:24:25 -08:00
|
|
|
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;
|
2026-02-10 19:30:45 -08:00
|
|
|
|
|
|
|
|
// Get current mount animation state (used throughout)
|
|
|
|
|
uint32_t curMountAnim = 0;
|
|
|
|
|
float curMountTime = 0, curMountDur = 0;
|
|
|
|
|
bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur);
|
|
|
|
|
|
|
|
|
|
// Check for jump trigger - use cached per-mount animation IDs
|
|
|
|
|
if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) {
|
|
|
|
|
if (moving && mountAnims_.jumpLoop > 0) {
|
|
|
|
|
// Moving: skip JumpStart (looks like stopping), go straight to airborne loop
|
|
|
|
|
LOG_INFO("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop);
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
|
|
|
|
|
mountAction_ = MountAction::Jump;
|
|
|
|
|
mountActionPhase_ = 1; // Start in airborne phase
|
|
|
|
|
mountAnimId = mountAnims_.jumpLoop;
|
|
|
|
|
if (mountSoundManager) {
|
|
|
|
|
mountSoundManager->playJumpSound();
|
|
|
|
|
}
|
|
|
|
|
if (cameraController) {
|
|
|
|
|
cameraController->triggerMountJump();
|
|
|
|
|
}
|
|
|
|
|
} else if (!moving && mountAnims_.rearUp > 0) {
|
|
|
|
|
// Standing still: rear-up flourish
|
|
|
|
|
LOG_INFO("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp);
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false);
|
|
|
|
|
mountAction_ = MountAction::RearUp;
|
|
|
|
|
mountActionPhase_ = 0;
|
|
|
|
|
mountAnimId = mountAnims_.rearUp;
|
|
|
|
|
// Trigger semantic rear-up sound
|
|
|
|
|
if (mountSoundManager) {
|
|
|
|
|
mountSoundManager->playRearUpSound();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle active mount actions (jump chaining or rear-up)
|
|
|
|
|
if (mountAction_ != MountAction::None) {
|
|
|
|
|
bool animFinished = haveMountState && curMountDur > 0.1f &&
|
|
|
|
|
(curMountTime >= curMountDur - 0.05f);
|
|
|
|
|
|
|
|
|
|
if (mountAction_ == MountAction::Jump) {
|
|
|
|
|
// Jump sequence: start → loop → end (physics-driven)
|
|
|
|
|
if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) {
|
|
|
|
|
// JumpStart finished, go to JumpLoop (airborne)
|
|
|
|
|
LOG_INFO("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")");
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
|
|
|
|
|
mountActionPhase_ = 1;
|
|
|
|
|
mountAnimId = mountAnims_.jumpLoop;
|
|
|
|
|
} else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) {
|
|
|
|
|
// No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose)
|
|
|
|
|
LOG_INFO("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)");
|
|
|
|
|
mountActionPhase_ = 1;
|
|
|
|
|
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) {
|
|
|
|
|
// Landed after airborne phase! Go to JumpEnd (grounded-triggered)
|
|
|
|
|
LOG_INFO("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")");
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false);
|
|
|
|
|
mountActionPhase_ = 2;
|
|
|
|
|
mountAnimId = mountAnims_.jumpEnd;
|
|
|
|
|
// Trigger semantic landing sound
|
|
|
|
|
if (mountSoundManager) {
|
|
|
|
|
mountSoundManager->playLandSound();
|
|
|
|
|
}
|
|
|
|
|
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) {
|
|
|
|
|
// No JumpEnd animation, return directly to movement after landing
|
|
|
|
|
LOG_INFO("Mount jump: phase 1→done (landed, no JumpEnd, returning to ",
|
|
|
|
|
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
|
|
|
|
|
mountAction_ = MountAction::None;
|
|
|
|
|
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
|
|
|
|
} else if (mountActionPhase_ == 2 && animFinished) {
|
|
|
|
|
// JumpEnd finished, return to movement
|
|
|
|
|
LOG_INFO("Mount jump: phase 2→done (JumpEnd finished, returning to ",
|
|
|
|
|
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
|
|
|
|
|
mountAction_ = MountAction::None;
|
|
|
|
|
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
|
|
|
|
} else {
|
|
|
|
|
mountAnimId = curMountAnim; // Keep current jump animation
|
|
|
|
|
}
|
|
|
|
|
} else if (mountAction_ == MountAction::RearUp) {
|
|
|
|
|
// Rear-up: single animation, return to stand when done
|
|
|
|
|
if (animFinished) {
|
|
|
|
|
LOG_INFO("Mount rear-up: finished, returning to ",
|
|
|
|
|
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand));
|
|
|
|
|
mountAction_ = MountAction::None;
|
|
|
|
|
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
|
|
|
|
} else {
|
|
|
|
|
mountAnimId = curMountAnim; // Keep current rear-up animation
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
// Normal movement animations
|
2026-02-07 20:24:25 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
|
2026-02-10 19:59:01 -08:00
|
|
|
// Check if active fidget has completed
|
|
|
|
|
if (mountActiveFidget_ != 0) {
|
|
|
|
|
uint32_t curAnim = 0;
|
|
|
|
|
float curTime = 0.0f, curDur = 0.0f;
|
|
|
|
|
if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) {
|
|
|
|
|
// If animation changed or completed, clear active fidget
|
|
|
|
|
if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) {
|
|
|
|
|
mountActiveFidget_ = 0;
|
|
|
|
|
LOG_INFO("Mount fidget completed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:53:23 -08:00
|
|
|
// Idle fidgets: random one-shot animations when standing still
|
2026-02-10 19:59:01 -08:00
|
|
|
if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) {
|
2026-02-10 19:53:23 -08:00
|
|
|
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);
|
2026-02-10 19:59:01 -08:00
|
|
|
mountActiveFidget_ = fidgetAnim; // Track active fidget
|
2026-02-10 19:53:23 -08:00
|
|
|
mountIdleFidgetTimer_ = 0.0f;
|
|
|
|
|
nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Mount idle fidget: playing anim ", fidgetAnim);
|
|
|
|
|
}
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
mountIdleFidgetTimer_ = 0.0f; // Reset timer when moving
|
2026-02-10 19:59:01 -08:00
|
|
|
mountActiveFidget_ = 0; // Cancel any active fidget
|
2026-02-10 19:53:23 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:20:26 -08:00
|
|
|
// Idle ambient sounds: snorts and whinnies only, less frequent
|
|
|
|
|
if (!moving && mountSoundManager) {
|
|
|
|
|
mountIdleSoundTimer_ += lastDeltaTime_;
|
|
|
|
|
static float nextIdleSoundTime = 20.0f + (rand() % 21); // 20-40 seconds
|
|
|
|
|
|
|
|
|
|
if (mountIdleSoundTimer_ >= nextIdleSoundTime) {
|
|
|
|
|
mountSoundManager->playIdleSound();
|
|
|
|
|
mountIdleSoundTimer_ = 0.0f;
|
|
|
|
|
nextIdleSoundTime = 20.0f + (rand() % 21); // Randomize next sound time
|
|
|
|
|
}
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
mountIdleSoundTimer_ = 0.0f; // Reset timer when moving
|
|
|
|
|
}
|
2026-02-10 19:53:23 -08:00
|
|
|
|
2026-02-10 19:59:01 -08:00
|
|
|
// 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)) {
|
2026-02-10 19:30:45 -08:00
|
|
|
bool loop = true; // Normal movement animations loop
|
|
|
|
|
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop);
|
2026-02-07 17:59:40 -08:00
|
|
|
}
|
2026-02-07 18:38:36 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Rider bob: sinusoidal motion synced to mount's run animation (only used in fallback positioning)
|
|
|
|
|
mountBob = 0.0f;
|
2026-02-07 18:38:36 -08:00
|
|
|
if (moving && haveMountState && curMountDur > 1.0f) {
|
|
|
|
|
float norm = std::fmod(curMountTime, curMountDur) / curMountDur;
|
2026-02-07 18:40:54 -08:00
|
|
|
// One bounce per stride cycle
|
2026-02-08 03:05:38 -08:00
|
|
|
float bobSpeed = taxiFlight_ ? 2.0f : 1.0f;
|
|
|
|
|
mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f;
|
2026-02-07 18:38:36 -08:00
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Use mount's attachment point for proper bone-driven rider positioning
|
|
|
|
|
glm::mat4 mountSeatTransform;
|
|
|
|
|
if (characterRenderer->getAttachmentTransform(mountInstanceId_, 0, mountSeatTransform)) {
|
|
|
|
|
// Extract position from mount seat transform (attachment point already includes proper seat height)
|
|
|
|
|
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
|
2026-02-08 22:11:40 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y)
|
|
|
|
|
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f);
|
2026-02-08 22:11:40 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Position rider at mount seat
|
|
|
|
|
characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset);
|
2026-02-08 22:11:40 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// Rider uses character facing yaw, not mount bone rotation
|
|
|
|
|
// (rider faces character direction, seat bone only provides position)
|
|
|
|
|
float yawRad = glm::radians(characterYaw);
|
|
|
|
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to old manual positioning if attachment not found
|
|
|
|
|
float yawRad = glm::radians(characterYaw);
|
|
|
|
|
glm::mat4 mountRotation = glm::mat4(1.0f);
|
|
|
|
|
mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
|
|
|
mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f));
|
|
|
|
|
mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f));
|
|
|
|
|
glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob);
|
|
|
|
|
glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f));
|
|
|
|
|
glm::vec3 playerPos = characterPosition + worldOffset;
|
|
|
|
|
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
|
|
|
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad));
|
|
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:01:26 -08:00
|
|
|
if (!forceMelee) switch (charAnimState) {
|
2026-02-02 12:24:50 -08:00
|
|
|
case CharAnimState::IDLE:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (sitting && grounded) {
|
|
|
|
|
newState = CharAnimState::SIT_DOWN;
|
|
|
|
|
} else if (!grounded && jumping) {
|
|
|
|
|
newState = CharAnimState::JUMP_START;
|
|
|
|
|
} else if (!grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_MID;
|
|
|
|
|
} else if (moving && sprinting) {
|
|
|
|
|
newState = CharAnimState::RUN;
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
newState = CharAnimState::WALK;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::WALK:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (!grounded && jumping) {
|
|
|
|
|
newState = CharAnimState::JUMP_START;
|
|
|
|
|
} else if (!grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_MID;
|
|
|
|
|
} else if (!moving) {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
} else if (sprinting) {
|
|
|
|
|
newState = CharAnimState::RUN;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::RUN:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (!grounded && jumping) {
|
|
|
|
|
newState = CharAnimState::JUMP_START;
|
|
|
|
|
} else if (!grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_MID;
|
|
|
|
|
} else if (!moving) {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
} else if (!sprinting) {
|
|
|
|
|
newState = CharAnimState::WALK;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::JUMP_START:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_END;
|
|
|
|
|
} else {
|
|
|
|
|
newState = CharAnimState::JUMP_MID;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::JUMP_MID:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_END;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::JUMP_END:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (moving && sprinting) {
|
|
|
|
|
newState = CharAnimState::RUN;
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
newState = CharAnimState::WALK;
|
|
|
|
|
} else {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::SIT_DOWN:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (!sitting) {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::SITTING:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (!sitting) {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::EMOTE:
|
|
|
|
|
if (swim) {
|
|
|
|
|
cancelEmote();
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (jumping || !grounded) {
|
|
|
|
|
cancelEmote();
|
|
|
|
|
newState = CharAnimState::JUMP_START;
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
cancelEmote();
|
|
|
|
|
newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK;
|
|
|
|
|
} else if (sitting) {
|
|
|
|
|
cancelEmote();
|
|
|
|
|
newState = CharAnimState::SIT_DOWN;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::SWIM_IDLE:
|
|
|
|
|
if (!swim) {
|
|
|
|
|
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
newState = CharAnimState::SWIM;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CharAnimState::SWIM:
|
|
|
|
|
if (!swim) {
|
|
|
|
|
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
|
|
|
|
|
} else if (!moving) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-02-05 14:01:26 -08:00
|
|
|
|
|
|
|
|
case CharAnimState::MELEE_SWING:
|
|
|
|
|
if (swim) {
|
|
|
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
|
|
|
} else if (!grounded && jumping) {
|
|
|
|
|
newState = CharAnimState::JUMP_START;
|
|
|
|
|
} else if (!grounded) {
|
|
|
|
|
newState = CharAnimState::JUMP_MID;
|
|
|
|
|
} else if (moving && sprinting) {
|
|
|
|
|
newState = CharAnimState::RUN;
|
|
|
|
|
} else if (moving) {
|
|
|
|
|
newState = CharAnimState::WALK;
|
|
|
|
|
} else if (sitting) {
|
|
|
|
|
newState = CharAnimState::SIT_DOWN;
|
|
|
|
|
} else {
|
|
|
|
|
newState = CharAnimState::IDLE;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-02-07 17:59:40 -08:00
|
|
|
|
|
|
|
|
case CharAnimState::MOUNT:
|
2026-02-07 20:24:25 -08:00
|
|
|
// 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;
|
2026-02-05 14:01:26 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (forceMelee) {
|
|
|
|
|
newState = CharAnimState::MELEE_SWING;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newState != charAnimState) {
|
|
|
|
|
charAnimState = newState;
|
2026-02-03 19:29:11 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 19:29:11 -08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-03 19:29:11 -08:00
|
|
|
return fallback;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
uint32_t animId = ANIM_STAND;
|
|
|
|
|
bool loop = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 19:29:11 -08:00
|
|
|
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 {
|
2026-02-06 18:52:28 -08:00
|
|
|
animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND);
|
2026-02-03 19:29:11 -08:00
|
|
|
}
|
|
|
|
|
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 {
|
2026-02-06 18:52:28 -08:00
|
|
|
animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND);
|
2026-02-03 19:29:11 -08:00
|
|
|
}
|
|
|
|
|
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;
|
2026-02-05 14:01:26 -08:00
|
|
|
case CharAnimState::MELEE_SWING:
|
|
|
|
|
animId = resolveMeleeAnimId();
|
|
|
|
|
if (animId == 0) {
|
|
|
|
|
animId = ANIM_STAND;
|
|
|
|
|
}
|
|
|
|
|
loop = false;
|
|
|
|
|
break;
|
2026-02-07 17:59:40 -08:00
|
|
|
case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break;
|
2026-02-03 19:29:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t currentAnimId = 0;
|
|
|
|
|
float currentAnimTimeMs = 0.0f;
|
|
|
|
|
float currentAnimDurationMs = 0.0f;
|
|
|
|
|
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
|
|
|
|
if (!haveState || currentAnimId != animId) {
|
2026-02-02 12:24:50 -08:00
|
|
|
characterRenderer->playAnimation(characterInstanceId, animId, loop);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::playEmote(const std::string& emoteName) {
|
2026-02-07 20:02:14 -08:00
|
|
|
loadEmotesFromDbc();
|
2026-02-02 12:24:50 -08:00
|
|
|
auto it = EMOTE_TABLE.find(emoteName);
|
|
|
|
|
if (it == EMOTE_TABLE.end()) return;
|
|
|
|
|
|
|
|
|
|
const auto& info = it->second;
|
2026-02-07 20:02:14 -08:00
|
|
|
if (info.animId == 0) return;
|
2026-02-02 12:24:50 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:01:26 -08:00
|
|
|
void Renderer::triggerMeleeSwing() {
|
|
|
|
|
if (!characterRenderer || characterInstanceId == 0) return;
|
|
|
|
|
if (meleeSwingCooldown > 0.0f) return;
|
|
|
|
|
if (emoteActive) {
|
|
|
|
|
cancelEmote();
|
|
|
|
|
}
|
|
|
|
|
resolveMeleeAnimId();
|
|
|
|
|
meleeSwingCooldown = 0.1f;
|
|
|
|
|
float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f;
|
|
|
|
|
if (durationSec < 0.25f) durationSec = 0.25f;
|
|
|
|
|
if (durationSec > 1.0f) durationSec = 1.0f;
|
|
|
|
|
meleeSwingTimer = durationSec;
|
|
|
|
|
if (activitySoundManager) {
|
|
|
|
|
activitySoundManager->playMeleeSwing();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 20:02:14 -08:00
|
|
|
std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) {
|
|
|
|
|
loadEmotesFromDbc();
|
2026-02-02 12:24:50 -08:00
|
|
|
auto it = EMOTE_TABLE.find(emoteName);
|
|
|
|
|
if (it != EMOTE_TABLE.end()) {
|
2026-02-07 20:02:14 -08:00
|
|
|
const auto& info = it->second;
|
|
|
|
|
const std::string& base = (targetName ? info.textTarget : info.textNoTarget);
|
|
|
|
|
if (!base.empty()) {
|
|
|
|
|
return replacePlaceholders(base, targetName);
|
|
|
|
|
}
|
|
|
|
|
if (targetName && !targetName->empty()) {
|
|
|
|
|
return "You " + info.command + " at " + *targetName + ".";
|
|
|
|
|
}
|
|
|
|
|
return "You " + info.command + ".";
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::setTargetPosition(const glm::vec3* pos) {
|
|
|
|
|
targetPosition = pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Renderer::isMoving() const {
|
|
|
|
|
return cameraController && cameraController->isMoving();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
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;
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// 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;
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
if (cameraController->isSwimming()) {
|
2026-02-09 00:40:50 -08:00
|
|
|
cachedFootstepSurface = audio::FootstepSurface::WATER;
|
2026-02-03 14:55:32 -08:00
|
|
|
return audio::FootstepSurface::WATER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
|
|
|
|
if (waterH && p.z < (*waterH + 0.25f)) {
|
2026-02-09 00:40:50 -08:00
|
|
|
cachedFootstepSurface = audio::FootstepSurface::WATER;
|
2026-02-03 14:55:32 -08:00
|
|
|
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)) {
|
2026-02-09 00:40:50 -08:00
|
|
|
cachedFootstepSurface = audio::FootstepSurface::STONE;
|
2026-02-03 14:55:32 -08:00
|
|
|
return audio::FootstepSurface::STONE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Determine surface type (expensive - only done when cache needs update)
|
|
|
|
|
audio::FootstepSurface surface = audio::FootstepSurface::STONE;
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
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)));
|
2026-02-09 00:40:50 -08:00
|
|
|
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;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
cachedFootstepSurface = surface;
|
|
|
|
|
return surface;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void Renderer::update(float deltaTime) {
|
2026-02-03 16:21:48 -08:00
|
|
|
auto updateStart = std::chrono::steady_clock::now();
|
2026-02-10 19:30:45 -08:00
|
|
|
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;
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
if (wmoRenderer) wmoRenderer->resetQueryStats();
|
|
|
|
|
if (m2Renderer) m2Renderer->resetQueryStats();
|
|
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
auto cam1 = std::chrono::high_resolution_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
if (cameraController) {
|
2026-02-03 16:21:48 -08:00
|
|
|
auto cameraStart = std::chrono::steady_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
cameraController->update(deltaTime);
|
2026-02-03 16:21:48 -08:00
|
|
|
auto cameraEnd = std::chrono::steady_clock::now();
|
|
|
|
|
lastCameraUpdateMs = std::chrono::duration<double, std::milli>(cameraEnd - cameraStart).count();
|
2026-02-09 00:40:50 -08:00
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
} else {
|
|
|
|
|
lastCameraUpdateMs = 0.0;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto cam2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 13:48:50 -08:00
|
|
|
// Update lighting system
|
2026-02-10 19:30:45 -08:00
|
|
|
auto light1 = std::chrono::high_resolution_clock::now();
|
2026-02-10 13:48:50 -08:00
|
|
|
if (lightingManager) {
|
|
|
|
|
// TODO: Get actual map ID from game state (0 = Eastern Kingdoms for now)
|
|
|
|
|
// TODO: Get actual game time from server (use -1 for local time fallback)
|
|
|
|
|
// TODO: Get weather/underwater state from game state
|
|
|
|
|
uint32_t mapId = 0; // Eastern Kingdoms
|
|
|
|
|
float gameTime = -1.0f; // Use local time for now
|
|
|
|
|
bool isRaining = false; // TODO: Get from weather system
|
|
|
|
|
bool isUnderwater = false; // TODO: Get from player state
|
|
|
|
|
|
|
|
|
|
lightingManager->update(characterPosition, mapId, gameTime, isRaining, isUnderwater);
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto light2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
lightTime += std::chrono::duration<float, std::milli>(light2 - light1).count();
|
2026-02-10 13:48:50 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Sync character model position/rotation and animation with follow target
|
2026-02-10 19:30:45 -08:00
|
|
|
auto charAnim1 = std::chrono::high_resolution_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
2026-02-05 14:01:26 -08:00
|
|
|
if (meleeSwingCooldown > 0.0f) {
|
|
|
|
|
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
|
|
|
|
|
}
|
|
|
|
|
if (meleeSwingTimer > 0.0f) {
|
|
|
|
|
meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
|
2026-02-03 19:49:56 -08:00
|
|
|
if (activitySoundManager) {
|
|
|
|
|
std::string modelName;
|
|
|
|
|
if (characterRenderer->getInstanceModelName(characterInstanceId, modelName)) {
|
|
|
|
|
activitySoundManager->setCharacterVoiceProfile(modelName);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 19:49:56 -08:00
|
|
|
// Movement-facing comes from camera controller and is decoupled from LMB orbit.
|
2026-02-08 22:00:33 -08:00
|
|
|
// 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()) {
|
2026-02-03 19:49:56 -08:00
|
|
|
characterYaw = cameraController->getFacingYaw();
|
2026-02-07 18:57:27 -08:00
|
|
|
} else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) {
|
2026-02-07 13:56:58 -08:00
|
|
|
// Face target when in combat and idle
|
2026-02-02 12:24:50 -08:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto charAnim2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
charAnimTime += std::chrono::duration<float, std::milli>(charAnim2 - charAnim1).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Update terrain streaming
|
2026-02-10 19:30:45 -08:00
|
|
|
auto terrain1 = std::chrono::high_resolution_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
if (terrainManager && camera) {
|
|
|
|
|
terrainManager->update(*camera, deltaTime);
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto terrain2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
terrainTime += std::chrono::duration<float, std::milli>(terrain2 - terrain1).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Update skybox time progression
|
2026-02-10 19:30:45 -08:00
|
|
|
auto sky1 = std::chrono::high_resolution_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:24:17 -08:00
|
|
|
// Update mount dust effects
|
|
|
|
|
if (mountDust) {
|
|
|
|
|
mountDust->update(deltaTime);
|
|
|
|
|
|
|
|
|
|
// Spawn dust when mounted and moving on ground
|
2026-02-09 01:26:28 -08:00
|
|
|
if (isMounted() && camera && cameraController && !taxiFlight_) {
|
2026-02-09 01:24:17 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto sky2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
skyTime += std::chrono::duration<float, std::milli>(sky2 - sky1).count();
|
2026-02-09 01:24:17 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Update character animations
|
2026-02-10 19:30:45 -08:00
|
|
|
auto charRend1 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
if (characterRenderer && camera) {
|
|
|
|
|
characterRenderer->update(deltaTime, camera->getPosition());
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto charRend2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
charRendTime += std::chrono::duration<float, std::milli>(charRend2 - charRend1).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Update AudioEngine (cleanup finished sounds, etc.)
|
2026-02-10 19:30:45 -08:00
|
|
|
auto audio1 = std::chrono::high_resolution_clock::now();
|
2026-02-09 00:40:50 -08:00
|
|
|
audio::AudioEngine::instance().update(deltaTime);
|
2026-02-10 19:30:45 -08:00
|
|
|
auto audio2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
audioTime += std::chrono::duration<float, std::milli>(audio2 - audio1).count();
|
2026-02-09 00:40:50 -08:00
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
// Footsteps: animation-event driven + surface query at event time.
|
2026-02-10 19:30:45 -08:00
|
|
|
auto footstep1 = std::chrono::high_resolution_clock::now();
|
2026-02-03 14:55:32 -08:00
|
|
|
if (footstepManager) {
|
|
|
|
|
footstepManager->update(deltaTime);
|
2026-02-09 00:40:50 -08:00
|
|
|
cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer
|
2026-02-07 18:38:36 -08:00
|
|
|
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
|
2026-02-03 14:55:32 -08:00
|
|
|
cameraController && cameraController->isThirdPerson() &&
|
2026-02-07 18:38:36 -08:00
|
|
|
cameraController->isGrounded() && !cameraController->isSwimming();
|
|
|
|
|
|
2026-02-08 03:05:38 -08:00
|
|
|
if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) {
|
2026-02-07 18:38:36 -08:00
|
|
|
// 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 {
|
2026-02-09 01:15:30 -08:00
|
|
|
// Mount gait: 2 hoofbeats per cycle (synced with animation)
|
2026-02-07 18:38:36 -08:00
|
|
|
auto crossed = [&](float eventNorm) {
|
|
|
|
|
if (mountFootstepLastNormTime <= norm) {
|
|
|
|
|
return mountFootstepLastNormTime < eventNorm && eventNorm <= norm;
|
|
|
|
|
}
|
|
|
|
|
return mountFootstepLastNormTime < eventNorm || eventNorm <= norm;
|
|
|
|
|
};
|
2026-02-09 01:15:30 -08:00
|
|
|
if (crossed(0.25f) || crossed(0.75f)) {
|
2026-02-07 18:38:36 -08:00
|
|
|
footstepManager->playFootstep(resolveFootstepSurface(), true);
|
|
|
|
|
}
|
|
|
|
|
mountFootstepLastNormTime = norm;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
mountFootstepNormInitialized = false;
|
|
|
|
|
}
|
|
|
|
|
footstepNormInitialized = false; // Reset player footstep tracking
|
|
|
|
|
} else if (canPlayFootsteps && isFootstepAnimationState()) {
|
2026-02-03 14:55:32 -08:00
|
|
|
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());
|
|
|
|
|
}
|
2026-02-07 18:38:36 -08:00
|
|
|
mountFootstepNormInitialized = false;
|
2026-02-03 14:55:32 -08:00
|
|
|
} else {
|
|
|
|
|
footstepNormInitialized = false;
|
2026-02-07 18:38:36 -08:00
|
|
|
mountFootstepNormInitialized = false;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 19:49:56 -08:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-09 14:50:14 -08:00
|
|
|
// Fade music underwater
|
|
|
|
|
if (musicManager) {
|
|
|
|
|
musicManager->setUnderwaterMode(swimming);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 19:49:56 -08:00
|
|
|
sfxPrevGrounded = grounded;
|
|
|
|
|
sfxPrevJumping = jumping;
|
|
|
|
|
sfxPrevFalling = falling;
|
|
|
|
|
sfxPrevSwimming = swimming;
|
|
|
|
|
} else {
|
|
|
|
|
activitySoundManager->setSwimmingState(false, false);
|
2026-02-09 14:50:14 -08:00
|
|
|
// Restore music volume when activity sounds disabled
|
|
|
|
|
if (musicManager) {
|
|
|
|
|
musicManager->setUnderwaterMode(false);
|
|
|
|
|
}
|
2026-02-03 19:49:56 -08:00
|
|
|
sfxStateInitialized = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:19:35 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto footstep2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
footstepTime += std::chrono::duration<float, std::milli>(footstep2 - footstep1).count();
|
2026-02-09 01:19:35 -08:00
|
|
|
|
2026-02-09 14:50:14 -08:00
|
|
|
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
2026-02-10 19:30:45 -08:00
|
|
|
auto ambient1 = std::chrono::high_resolution_clock::now();
|
2026-02-09 14:50:14 -08:00
|
|
|
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();
|
|
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
// Check if inside blacksmith (96048 = Goldshire blacksmith)
|
|
|
|
|
bool isBlacksmith = (wmoId == 96048);
|
|
|
|
|
|
Add comprehensive weather, water, and zone ambient audio systems
Implemented three new ambient audio systems with automatic day/night transitions:
Weather ambience:
- Rain sounds (light/medium/heavy intensity based on weather system)
- Snow sounds (light/medium/heavy intensity)
- Automatically syncs with visual weather system in renderer
- Different loop intervals based on intensity (18-30s)
- Disabled indoors
Water ambience:
- Underwater swimming sounds (18s loop)
- Ocean surface sounds
- State tracking for entering/exiting water
Zone ambience:
- Forest (normal and snow variants)
- Beach sounds
- Grasslands
- Jungle
- Marsh/swamp
- Desert (canyon and plains variants)
- All zones have separate day/night sound files
- 30s loop interval for subtle background atmosphere
- Disabled indoors
Technical details:
- Added WeatherType enum (NONE, RAIN/SNOW LIGHT/MEDIUM/HEAVY)
- Added ZoneType enum (NONE, FOREST_NORMAL, FOREST_SNOW, BEACH, GRASSLANDS, JUNGLE, MARSH, DESERT_CANYON, DESERT_PLAINS)
- Loads 26 new sound files from Sound\Ambience\Weather and Sound\Ambience\ZoneAmbience
- Weather intensity thresholds: <0.33 = light, 0.33-0.66 = medium, >0.66 = heavy
- Renderer automatically converts Weather::Type + intensity to AmbientSoundManager::WeatherType
- All ambience respects volumeScale_ and indoor state
- State change logging for debugging transitions
2026-02-09 16:12:06 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
|
2026-02-09 14:50:14 -08:00
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto ambient2 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
ambientTime += std::chrono::duration<float, std::milli>(ambient2 - ambient1).count();
|
2026-02-09 14:50:14 -08:00
|
|
|
|
2026-02-07 14:37:14 -08:00
|
|
|
// Update M2 doodad animations (pass camera for frustum-culling bone computation)
|
2026-02-10 19:30:45 -08:00
|
|
|
static int m2ProfileCounter = 0;
|
|
|
|
|
static float m2Time = 0.0f;
|
|
|
|
|
auto m21 = std::chrono::high_resolution_clock::now();
|
2026-02-07 14:37:14 -08:00
|
|
|
if (m2Renderer && camera) {
|
|
|
|
|
m2Renderer->update(deltaTime, camera->getPosition(),
|
|
|
|
|
camera->getProjectionMatrix() * camera->getViewMatrix());
|
2026-02-02 23:10:19 -08:00
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
auto m22 = std::chrono::high_resolution_clock::now();
|
|
|
|
|
m2Time += std::chrono::duration<float, std::milli>(m22 - m21).count();
|
2026-02-02 23:10:19 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-09 01:39:12 -08:00
|
|
|
bool insideTavern = false;
|
2026-02-09 15:21:07 -08:00
|
|
|
bool insideBlacksmith = false;
|
2026-02-09 01:39:12 -08:00
|
|
|
std::string tavernMusic;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
// Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths)
|
2026-02-02 12:24:50 -08:00
|
|
|
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
|
|
|
|
|
}
|
2026-02-09 01:39:12 -08:00
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
// Detect taverns/inns/blacksmiths by WMO model ID
|
2026-02-09 01:43:20 -08:00
|
|
|
// Log WMO ID for debugging
|
|
|
|
|
static uint32_t lastLoggedWmoId = 0;
|
|
|
|
|
if (wmoModelId != lastLoggedWmoId) {
|
|
|
|
|
LOG_INFO("Inside WMO model ID: ", wmoModelId);
|
|
|
|
|
lastLoggedWmoId = wmoModelId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
// Blacksmith detection
|
|
|
|
|
if (wmoModelId == 96048) { // Goldshire blacksmith
|
|
|
|
|
insideBlacksmith = true;
|
|
|
|
|
LOG_INFO("Detected blacksmith WMO ", wmoModelId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:39:12 -08:00
|
|
|
// These IDs represent typical Alliance and Horde inn buildings
|
2026-02-09 15:21:07 -08:00
|
|
|
if (wmoModelId == 191 || // Goldshire inn (old ID)
|
|
|
|
|
wmoModelId == 71414 || // Goldshire inn (actual)
|
2026-02-09 01:39:12 -08:00
|
|
|
wmoModelId == 190 || // Small inn (common)
|
|
|
|
|
wmoModelId == 220 || // Tavern building
|
|
|
|
|
wmoModelId == 221 || // Large tavern
|
|
|
|
|
wmoModelId == 5392 || // Horde inn
|
|
|
|
|
wmoModelId == 5393) { // Another inn variant
|
|
|
|
|
insideTavern = true;
|
2026-02-09 15:21:07 -08:00
|
|
|
// WoW tavern music (cozy ambient tracks) - FIXED PATHS
|
2026-02-09 01:39:12 -08:00
|
|
|
static const std::vector<std::string> tavernTracks = {
|
2026-02-09 15:21:07 -08:00
|
|
|
"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",
|
2026-02-09 01:39:12 -08:00
|
|
|
};
|
|
|
|
|
static int tavernTrackIndex = 0;
|
|
|
|
|
tavernMusic = tavernTracks[tavernTrackIndex % tavernTracks.size()];
|
2026-02-09 15:21:07 -08:00
|
|
|
LOG_INFO("Detected tavern WMO ", wmoModelId, ", playing: ", tavernMusic);
|
2026-02-09 01:39:12 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle tavern music transitions
|
|
|
|
|
if (insideTavern) {
|
|
|
|
|
if (!inTavern_ && !tavernMusic.empty()) {
|
|
|
|
|
inTavern_ = true;
|
|
|
|
|
LOG_INFO("Entered tavern");
|
2026-02-09 15:21:07 -08:00
|
|
|
musicManager->playMusic(tavernMusic, true); // Immediate playback, looping
|
2026-02-09 01:39:12 -08:00
|
|
|
}
|
|
|
|
|
} else if (inTavern_) {
|
2026-02-09 15:21:07 -08:00
|
|
|
// Exited tavern - restore zone music with crossfade
|
2026-02-09 01:39:12 -08:00
|
|
|
inTavern_ = false;
|
|
|
|
|
LOG_INFO("Exited tavern");
|
|
|
|
|
auto* info = zoneManager->getZoneInfo(currentZoneId);
|
|
|
|
|
if (info) {
|
|
|
|
|
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
|
|
|
|
if (!music.empty()) {
|
|
|
|
|
musicManager->crossfadeTo(music);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 15:21:07 -08:00
|
|
|
// Handle blacksmith music (stop music when entering blacksmith, let ambience play)
|
|
|
|
|
if (insideBlacksmith) {
|
|
|
|
|
if (!inBlacksmith_) {
|
|
|
|
|
inBlacksmith_ = true;
|
|
|
|
|
LOG_INFO("Entered blacksmith - stopping music");
|
|
|
|
|
musicManager->stopMusic();
|
|
|
|
|
}
|
|
|
|
|
} else if (inBlacksmith_) {
|
|
|
|
|
// Exited blacksmith - restore zone music with crossfade
|
|
|
|
|
inBlacksmith_ = false;
|
|
|
|
|
LOG_INFO("Exited blacksmith - restoring music");
|
|
|
|
|
auto* info = zoneManager->getZoneInfo(currentZoneId);
|
|
|
|
|
if (info) {
|
|
|
|
|
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
|
|
|
|
if (!music.empty()) {
|
|
|
|
|
musicManager->crossfadeTo(music);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle normal zone transitions (only if not in tavern or blacksmith)
|
|
|
|
|
if (!insideTavern && !insideBlacksmith && zoneId != currentZoneId && zoneId != 0) {
|
2026-02-02 12:24:50 -08:00
|
|
|
currentZoneId = zoneId;
|
|
|
|
|
auto* info = zoneManager->getZoneInfo(zoneId);
|
|
|
|
|
if (info) {
|
|
|
|
|
currentZoneName = info->name;
|
|
|
|
|
LOG_INFO("Entered zone: ", info->name);
|
|
|
|
|
std::string music = zoneManager->getRandomMusic(zoneId);
|
|
|
|
|
if (!music.empty()) {
|
|
|
|
|
musicManager->crossfadeTo(music);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
musicManager->update(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update performance HUD
|
|
|
|
|
if (performanceHUD) {
|
|
|
|
|
performanceHUD->update(deltaTime);
|
|
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
|
|
|
|
|
auto updateEnd = std::chrono::steady_clock::now();
|
|
|
|
|
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
2026-02-10 19:30:45 -08:00
|
|
|
|
|
|
|
|
// Log renderer profiling every 60 frames
|
|
|
|
|
if (++rendProfileCounter >= 60) {
|
|
|
|
|
LOG_INFO("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f,
|
|
|
|
|
"ms light=", lightTime / 60.0f, "ms charAnim=", charAnimTime / 60.0f,
|
|
|
|
|
"ms terrain=", terrainTime / 60.0f, "ms sky=", skyTime / 60.0f,
|
|
|
|
|
"ms charRend=", charRendTime / 60.0f, "ms audio=", audioTime / 60.0f,
|
|
|
|
|
"ms footstep=", footstepTime / 60.0f, "ms ambient=", ambientTime / 60.0f,
|
|
|
|
|
"ms m2Anim=", m2Time / 60.0f, "ms");
|
|
|
|
|
rendProfileCounter = 0;
|
|
|
|
|
camTime = lightTime = charAnimTime = 0.0f;
|
|
|
|
|
terrainTime = skyTime = charRendTime = 0.0f;
|
|
|
|
|
audioTime = footstepTime = ambientTime = 0.0f;
|
|
|
|
|
m2Time = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
if (++m2ProfileCounter >= 60) {
|
|
|
|
|
m2ProfileCounter = 0;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Selection Circle
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void Renderer::initSelectionCircle() {
|
|
|
|
|
if (selCircleVAO) return;
|
|
|
|
|
|
|
|
|
|
// Minimal shader: position + uniform MVP + color
|
|
|
|
|
const char* vsSrc = R"(
|
|
|
|
|
#version 330 core
|
|
|
|
|
layout(location = 0) in vec3 aPos;
|
|
|
|
|
uniform mat4 uMVP;
|
|
|
|
|
void main() {
|
|
|
|
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
|
|
|
|
}
|
|
|
|
|
)";
|
|
|
|
|
const char* fsSrc = R"(
|
|
|
|
|
#version 330 core
|
|
|
|
|
uniform vec3 uColor;
|
|
|
|
|
out vec4 FragColor;
|
|
|
|
|
void main() {
|
|
|
|
|
FragColor = vec4(uColor, 0.6);
|
|
|
|
|
}
|
|
|
|
|
)";
|
|
|
|
|
|
|
|
|
|
auto compile = [](GLenum type, const char* src) -> GLuint {
|
|
|
|
|
GLuint s = glCreateShader(type);
|
|
|
|
|
glShaderSource(s, 1, &src, nullptr);
|
|
|
|
|
glCompileShader(s);
|
|
|
|
|
return s;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
GLuint vs = compile(GL_VERTEX_SHADER, vsSrc);
|
|
|
|
|
GLuint fs = compile(GL_FRAGMENT_SHADER, fsSrc);
|
|
|
|
|
selCircleShader = glCreateProgram();
|
|
|
|
|
glAttachShader(selCircleShader, vs);
|
|
|
|
|
glAttachShader(selCircleShader, fs);
|
|
|
|
|
glLinkProgram(selCircleShader);
|
|
|
|
|
glDeleteShader(vs);
|
|
|
|
|
glDeleteShader(fs);
|
|
|
|
|
|
|
|
|
|
// Build ring vertices (two concentric circles forming a strip)
|
|
|
|
|
constexpr int SEGMENTS = 48;
|
|
|
|
|
constexpr float INNER = 0.85f;
|
|
|
|
|
constexpr float OUTER = 1.0f;
|
|
|
|
|
std::vector<float> verts;
|
|
|
|
|
for (int i = 0; i <= SEGMENTS; i++) {
|
|
|
|
|
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
|
|
|
|
|
float c = std::cos(angle), s = std::sin(angle);
|
|
|
|
|
// Outer vertex
|
|
|
|
|
verts.push_back(c * OUTER);
|
|
|
|
|
verts.push_back(s * OUTER);
|
|
|
|
|
verts.push_back(0.0f);
|
|
|
|
|
// Inner vertex
|
|
|
|
|
verts.push_back(c * INNER);
|
|
|
|
|
verts.push_back(s * INNER);
|
|
|
|
|
verts.push_back(0.0f);
|
|
|
|
|
}
|
|
|
|
|
selCircleVertCount = static_cast<int>((SEGMENTS + 1) * 2);
|
|
|
|
|
|
|
|
|
|
glGenVertexArrays(1, &selCircleVAO);
|
|
|
|
|
glGenBuffers(1, &selCircleVBO);
|
|
|
|
|
glBindVertexArray(selCircleVAO);
|
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, selCircleVBO);
|
|
|
|
|
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW);
|
|
|
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
|
|
|
|
|
glEnableVertexAttribArray(0);
|
|
|
|
|
glBindVertexArray(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
|
|
|
|
|
selCirclePos = pos;
|
|
|
|
|
selCircleRadius = radius;
|
|
|
|
|
selCircleColor = color;
|
|
|
|
|
selCircleVisible = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::clearSelectionCircle() {
|
|
|
|
|
selCircleVisible = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection) {
|
|
|
|
|
if (!selCircleVisible) return;
|
|
|
|
|
initSelectionCircle();
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Small Z offset to prevent clipping under terrain
|
|
|
|
|
glm::vec3 raisedPos = selCirclePos;
|
|
|
|
|
raisedPos.z += 0.15f;
|
|
|
|
|
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
|
2026-02-06 13:47:03 -08:00
|
|
|
model = glm::scale(model, glm::vec3(selCircleRadius));
|
|
|
|
|
|
|
|
|
|
glm::mat4 mvp = projection * view * model;
|
|
|
|
|
|
|
|
|
|
glEnable(GL_BLEND);
|
|
|
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
|
glDisable(GL_CULL_FACE);
|
|
|
|
|
glDepthMask(GL_FALSE);
|
|
|
|
|
|
|
|
|
|
glUseProgram(selCircleShader);
|
|
|
|
|
glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]);
|
|
|
|
|
glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]);
|
|
|
|
|
|
|
|
|
|
glBindVertexArray(selCircleVAO);
|
|
|
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, selCircleVertCount);
|
|
|
|
|
glBindVertexArray(0);
|
|
|
|
|
|
|
|
|
|
glDepthMask(GL_TRUE);
|
|
|
|
|
glEnable(GL_CULL_FACE);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
2026-02-03 16:21:48 -08:00
|
|
|
auto renderStart = std::chrono::steady_clock::now();
|
|
|
|
|
lastTerrainRenderMs = 0.0;
|
|
|
|
|
lastWMORenderMs = 0.0;
|
|
|
|
|
lastM2RenderMs = 0.0;
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// Shadow pass (before main scene)
|
2026-02-05 16:11:24 -08:00
|
|
|
if (shadowsEnabled && shadowFBO && shadowShaderProgram && terrainLoaded) {
|
2026-02-04 16:08:35 -08:00
|
|
|
renderShadowPass();
|
2026-02-05 16:11:24 -08:00
|
|
|
} else {
|
|
|
|
|
// Clear shadow maps when disabled
|
|
|
|
|
if (terrainRenderer) terrainRenderer->clearShadowMap();
|
|
|
|
|
if (wmoRenderer) wmoRenderer->clearShadowMap();
|
|
|
|
|
if (m2Renderer) m2Renderer->clearShadowMap();
|
|
|
|
|
if (characterRenderer) characterRenderer->clearShadowMap();
|
2026-02-04 16:08:35 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
(void)world; // Unused for now
|
|
|
|
|
|
|
|
|
|
// Get time of day for sky-related rendering
|
|
|
|
|
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
|
2026-02-03 20:40:59 -08:00
|
|
|
bool underwater = false;
|
|
|
|
|
bool canalUnderwater = false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// TODO: Set skyboxModelId from LightSkybox.dbc (future)
|
|
|
|
|
skyParams.skyboxModelId = 0;
|
|
|
|
|
skyParams.skyboxHasStars = false; // Gradient skybox has no baked stars
|
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies
Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.
Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)
Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)
Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)
Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions
Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
skySystem->render(*camera, skyParams);
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: render individual components (backwards compatibility)
|
|
|
|
|
if (skybox && camera) {
|
|
|
|
|
skybox->render(*camera, timeOfDay);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
if (starField && camera) {
|
|
|
|
|
starField->render(*camera, timeOfDay, cloudDensity, fogDensity);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-10 19:30:45 -08:00
|
|
|
if (celestial && camera) {
|
|
|
|
|
celestial->render(*camera, timeOfDay, sunDir, sunColor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clouds && camera) {
|
|
|
|
|
clouds->render(*camera, timeOfDay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lensFlare && camera && celestial) {
|
|
|
|
|
glm::vec3 sunPosition;
|
|
|
|
|
if (sunDir) {
|
|
|
|
|
const float sunDistance = 800.0f;
|
|
|
|
|
sunPosition = -*sunDir * sunDistance;
|
|
|
|
|
} else {
|
|
|
|
|
sunPosition = celestial->getSunPosition(timeOfDay);
|
|
|
|
|
}
|
|
|
|
|
lensFlare->render(*camera, sunPosition, timeOfDay);
|
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies
Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.
Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)
Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)
Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)
Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions
Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies
Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.
Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)
Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)
Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)
Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions
Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
|
|
|
// Apply lighting and fog to all renderers
|
|
|
|
|
if (lightingManager) {
|
|
|
|
|
const auto& lighting = lightingManager->getLightingParams();
|
|
|
|
|
|
|
|
|
|
float lightDir[3] = {lighting.directionalDir.x, lighting.directionalDir.y, lighting.directionalDir.z};
|
|
|
|
|
float lightColor[3] = {lighting.diffuseColor.r, lighting.diffuseColor.g, lighting.diffuseColor.b};
|
|
|
|
|
float ambientColor[3] = {lighting.ambientColor.r, lighting.ambientColor.g, lighting.ambientColor.b};
|
|
|
|
|
float fogColorArray[3] = {lighting.fogColor.r, lighting.fogColor.g, lighting.fogColor.b};
|
|
|
|
|
|
|
|
|
|
if (wmoRenderer) {
|
|
|
|
|
wmoRenderer->setLighting(lightDir, lightColor, ambientColor);
|
|
|
|
|
wmoRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
|
|
|
|
|
lighting.fogStart, lighting.fogEnd);
|
|
|
|
|
}
|
|
|
|
|
if (m2Renderer) {
|
|
|
|
|
m2Renderer->setLighting(lightDir, lightColor, ambientColor);
|
|
|
|
|
m2Renderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
|
|
|
|
|
lighting.fogStart, lighting.fogEnd);
|
|
|
|
|
}
|
|
|
|
|
if (characterRenderer) {
|
|
|
|
|
characterRenderer->setLighting(lightDir, lightColor, ambientColor);
|
|
|
|
|
characterRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
|
|
|
|
|
lighting.fogStart, lighting.fogEnd);
|
|
|
|
|
}
|
|
|
|
|
} else if (skybox) {
|
|
|
|
|
// Fallback to skybox-based fog if no lighting manager
|
2026-02-04 15:05:46 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Render terrain if loaded and enabled
|
|
|
|
|
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
|
2026-02-03 20:40:59 -08:00
|
|
|
// Check if camera/character is underwater for fog override
|
|
|
|
|
if (cameraController && cameraController->isSwimming() && waterRenderer && camera) {
|
2026-02-02 12:24:50 -08:00
|
|
|
glm::vec3 camPos = camera->getPosition();
|
|
|
|
|
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
|
2026-02-03 20:40:59 -08:00
|
|
|
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).
|
2026-02-03 21:11:10 -08:00
|
|
|
constexpr float UNDERWATER_ENTER_EPS = 1.10f;
|
2026-02-03 20:40:59 -08:00
|
|
|
if (waterH &&
|
|
|
|
|
camPos.z < (*waterH - UNDERWATER_ENTER_EPS) &&
|
|
|
|
|
(*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) {
|
2026-02-02 12:24:50 -08:00
|
|
|
underwater = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (underwater) {
|
2026-02-03 20:40:59 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 21:11:10 -08:00
|
|
|
canalUnderwater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:48:50 -08:00
|
|
|
// 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
|
2026-02-02 12:24:50 -08:00
|
|
|
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
|
|
|
|
|
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
|
|
|
|
|
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
auto terrainStart = std::chrono::steady_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
terrainRenderer->render(*camera);
|
2026-02-03 16:21:48 -08:00
|
|
|
auto terrainEnd = std::chrono::steady_clock::now();
|
|
|
|
|
lastTerrainRenderMs = std::chrono::duration<double, std::milli>(terrainEnd - terrainStart).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render weather particles (after terrain/water, before characters)
|
2026-02-08 21:32:38 -08:00
|
|
|
// Skip during taxi flights for performance and visual clarity
|
|
|
|
|
bool onTaxi = cameraController && cameraController->isOnTaxi();
|
|
|
|
|
if (weather && camera && !onTaxi) {
|
2026-02-02 12:24:50 -08:00
|
|
|
weather->render(*camera);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render swim effects (ripples and bubbles)
|
|
|
|
|
if (swimEffects && camera) {
|
|
|
|
|
swimEffects->render(*camera);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:24:17 -08:00
|
|
|
// Render mount dust effects
|
|
|
|
|
if (mountDust && camera) {
|
|
|
|
|
mountDust->render(*camera);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Render characters (after weather)
|
|
|
|
|
if (characterRenderer && camera) {
|
|
|
|
|
characterRenderer->render(*camera, view, projection);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Render selection circle under targeted creature
|
|
|
|
|
renderSelectionCircle(view, projection);
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Render WMO buildings (after characters, before UI)
|
|
|
|
|
if (wmoRenderer && camera) {
|
2026-02-03 16:21:48 -08:00
|
|
|
auto wmoStart = std::chrono::steady_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
wmoRenderer->render(*camera, view, projection);
|
2026-02-03 16:21:48 -08:00
|
|
|
auto wmoEnd = std::chrono::steady_clock::now();
|
|
|
|
|
lastWMORenderMs = std::chrono::duration<double, std::milli>(wmoEnd - wmoStart).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render M2 doodads (trees, rocks, etc.)
|
2026-02-08 20:45:59 -08:00
|
|
|
if (m2Renderer && camera) {
|
2026-02-07 17:05:30 -08:00
|
|
|
// Dim M2 lighting when player is inside a WMO
|
|
|
|
|
if (cameraController) {
|
|
|
|
|
m2Renderer->setInsideInterior(cameraController->isInsideWMO());
|
2026-02-08 21:32:38 -08:00
|
|
|
m2Renderer->setOnTaxi(cameraController->isOnTaxi());
|
2026-02-07 17:05:30 -08:00
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
auto m2Start = std::chrono::steady_clock::now();
|
2026-02-02 12:24:50 -08:00
|
|
|
m2Renderer->render(*camera, view, projection);
|
2026-02-08 21:32:38 -08:00
|
|
|
// Skip particle fog during taxi (expensive and visually distracting)
|
|
|
|
|
if (!onTaxi) {
|
|
|
|
|
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
|
|
|
|
m2Renderer->renderM2Particles(view, projection);
|
|
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
auto m2End = std::chrono::steady_clock::now();
|
|
|
|
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 23:41:38 -08:00
|
|
|
// Render quest markers (billboards above NPCs)
|
|
|
|
|
if (questMarkerRenderer && camera) {
|
|
|
|
|
questMarkerRenderer->render(*camera);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
2026-02-03 21:11:10 -08:00
|
|
|
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
2026-02-03 20:40:59 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
// --- 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
// 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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
auto renderEnd = std::chrono::steady_clock::now();
|
|
|
|
|
lastRenderMs = std::chrono::duration<double, std::milli>(renderEnd - renderStart).count();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:21:04 -08:00
|
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
|
// 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;
|
2026-02-04 15:33:00 -08:00
|
|
|
// 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);
|
2026-02-04 15:28:47 -08:00
|
|
|
FragColor = vec4(mapped, 1.0);
|
2026-02-04 15:21:04 -08:00
|
|
|
}
|
|
|
|
|
)";
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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());
|
|
|
|
|
}
|
2026-02-09 14:50:14 -08:00
|
|
|
// Set ambient sound manager for environmental audio emitters
|
|
|
|
|
if (ambientSoundManager) {
|
|
|
|
|
terrainManager->setAmbientSoundManager(ambientSoundManager.get());
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
// Pass asset manager to character renderer for texture loading
|
|
|
|
|
if (characterRenderer) {
|
|
|
|
|
characterRenderer->setAssetManager(assetManager);
|
|
|
|
|
}
|
2026-02-04 20:06:27 -08:00
|
|
|
// Wire asset manager to minimap for tile texture loading
|
2026-02-02 12:24:50 -08:00
|
|
|
if (minimap) {
|
2026-02-04 20:06:27 -08:00
|
|
|
minimap->setAssetManager(assetManager);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
// Wire terrain manager, WMO renderer, and water renderer to camera controller
|
|
|
|
|
if (cameraController) {
|
|
|
|
|
cameraController->setTerrainManager(terrainManager.get());
|
|
|
|
|
if (wmoRenderer) {
|
|
|
|
|
cameraController->setWMORenderer(wmoRenderer.get());
|
|
|
|
|
}
|
2026-02-02 23:03:45 -08:00
|
|
|
if (m2Renderer) {
|
|
|
|
|
cameraController->setM2Renderer(m2Renderer.get());
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
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);
|
2026-02-04 20:06:27 -08:00
|
|
|
if (minimap) {
|
|
|
|
|
minimap->setMapName(mapName);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 18:52:28 -08:00
|
|
|
LOG_INFO("Enqueuing initial tile [", tileX, ",", tileY, "] via terrain manager");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 18:52:28 -08:00
|
|
|
// 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, "]");
|
2026-02-02 12:24:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
terrainLoaded = true;
|
|
|
|
|
|
|
|
|
|
// Initialize music manager with asset manager
|
|
|
|
|
if (musicManager && assetManager && !cachedAssetManager) {
|
|
|
|
|
musicManager->initialize(assetManager);
|
2026-02-03 14:55:32 -08:00
|
|
|
if (footstepManager) {
|
|
|
|
|
footstepManager->initialize(assetManager);
|
|
|
|
|
}
|
2026-02-03 19:49:56 -08:00
|
|
|
if (activitySoundManager) {
|
|
|
|
|
activitySoundManager->initialize(assetManager);
|
|
|
|
|
}
|
2026-02-09 01:50:42 -08:00
|
|
|
if (mountSoundManager) {
|
|
|
|
|
mountSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
|
|
|
|
if (npcVoiceManager) {
|
|
|
|
|
npcVoiceManager->initialize(assetManager);
|
|
|
|
|
}
|
2026-02-09 14:50:14 -08:00
|
|
|
if (ambientSoundManager) {
|
|
|
|
|
ambientSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
if (uiSoundManager) {
|
|
|
|
|
uiSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
|
|
|
|
if (combatSoundManager) {
|
|
|
|
|
combatSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
|
|
|
|
if (spellSoundManager) {
|
|
|
|
|
spellSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
|
|
|
|
if (movementSoundManager) {
|
|
|
|
|
movementSoundManager->initialize(assetManager);
|
|
|
|
|
}
|
2026-02-09 23:41:38 -08:00
|
|
|
if (questMarkerRenderer) {
|
|
|
|
|
questMarkerRenderer->initialize(assetManager);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
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;
|
|
|
|
|
|
2026-02-09 01:48:19 -08:00
|
|
|
// Get asset manager from Application if not cached yet
|
|
|
|
|
if (!cachedAssetManager) {
|
|
|
|
|
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize music manager with asset manager
|
2026-02-02 12:24:50 -08:00
|
|
|
if (musicManager && cachedAssetManager) {
|
|
|
|
|
if (!musicManager->isInitialized()) {
|
|
|
|
|
musicManager->initialize(cachedAssetManager);
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (footstepManager && cachedAssetManager) {
|
|
|
|
|
if (!footstepManager->isInitialized()) {
|
|
|
|
|
footstepManager->initialize(cachedAssetManager);
|
2026-02-03 19:49:56 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (activitySoundManager && cachedAssetManager) {
|
|
|
|
|
if (!activitySoundManager->isInitialized()) {
|
|
|
|
|
activitySoundManager->initialize(cachedAssetManager);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 01:29:44 -08:00
|
|
|
if (mountSoundManager && cachedAssetManager) {
|
|
|
|
|
mountSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
|
|
|
|
if (npcVoiceManager && cachedAssetManager) {
|
|
|
|
|
npcVoiceManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
2026-02-09 14:50:14 -08:00
|
|
|
if (ambientSoundManager && cachedAssetManager) {
|
|
|
|
|
ambientSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
if (uiSoundManager && cachedAssetManager) {
|
|
|
|
|
uiSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
|
|
|
|
if (combatSoundManager && cachedAssetManager) {
|
|
|
|
|
combatSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
|
|
|
|
if (spellSoundManager && cachedAssetManager) {
|
|
|
|
|
spellSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
|
|
|
|
if (movementSoundManager && cachedAssetManager) {
|
|
|
|
|
movementSoundManager->initialize(cachedAssetManager);
|
|
|
|
|
}
|
2026-02-09 23:41:38 -08:00
|
|
|
if (questMarkerRenderer && cachedAssetManager) {
|
|
|
|
|
questMarkerRenderer->initialize(cachedAssetManager);
|
|
|
|
|
}
|
2026-02-09 14:50:14 -08:00
|
|
|
|
|
|
|
|
// Wire ambient sound manager to terrain manager for emitter registration
|
|
|
|
|
if (terrainManager && ambientSoundManager) {
|
|
|
|
|
terrainManager->setAmbientSoundManager(ambientSoundManager.get());
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-02 23:03:45 -08:00
|
|
|
// Wire WMO, M2, and water renderer to camera controller
|
2026-02-02 12:24:50 -08:00
|
|
|
if (cameraController && wmoRenderer) {
|
|
|
|
|
cameraController->setWMORenderer(wmoRenderer.get());
|
|
|
|
|
}
|
2026-02-02 23:03:45 -08:00
|
|
|
if (cameraController && m2Renderer) {
|
|
|
|
|
cameraController->setM2Renderer(m2Renderer.get());
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
|
// 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;
|
2026-02-04 16:36:03 -08:00
|
|
|
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];
|
2026-02-04 16:22:18 -08:00
|
|
|
out vec2 vTexCoord;
|
2026-02-04 16:08:35 -08:00
|
|
|
void main() {
|
2026-02-04 16:36:03 -08:00
|
|
|
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));
|
|
|
|
|
}
|
2026-02-04 16:22:18 -08:00
|
|
|
vTexCoord = aTexCoord;
|
2026-02-04 16:36:03 -08:00
|
|
|
gl_Position = uLightSpaceMatrix * uModel * vec4(pos, 1.0);
|
2026-02-04 16:08:35 -08:00
|
|
|
}
|
|
|
|
|
)";
|
|
|
|
|
const char* fragSrc = R"(
|
|
|
|
|
#version 330 core
|
2026-02-04 16:22:18 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 16:08:35 -08:00
|
|
|
)";
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-02-04 16:30:24 -08:00
|
|
|
constexpr float kShadowHalfExtent = 180.0f;
|
|
|
|
|
constexpr float kShadowLightDistance = 280.0f;
|
|
|
|
|
constexpr float kShadowNearPlane = 1.0f;
|
|
|
|
|
constexpr float kShadowFarPlane = 600.0f;
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// Sun direction matching WMO light dir
|
|
|
|
|
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
|
|
|
|
|
|
2026-02-04 16:22:18 -08:00
|
|
|
// 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;
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
// Texel snapping: round center to shadow texel boundaries to prevent shimmer
|
2026-02-04 16:30:24 -08:00
|
|
|
float halfExtent = kShadowHalfExtent;
|
2026-02-04 16:08:35 -08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-04 16:30:24 -08:00
|
|
|
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
// Snap center in light space to texel grid
|
|
|
|
|
glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f);
|
2026-02-04 16:22:18 -08:00
|
|
|
centerLS.x = std::round(centerLS.x / texelWorld) * texelWorld;
|
|
|
|
|
centerLS.y = std::round(centerLS.y / texelWorld) * texelWorld;
|
2026-02-04 16:08:35 -08:00
|
|
|
glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS;
|
|
|
|
|
center = glm::vec3(snappedCenter);
|
2026-02-04 16:22:18 -08:00
|
|
|
shadowCenter = center;
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
// Rebuild with snapped center
|
2026-02-04 16:30:24 -08:00
|
|
|
lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
|
|
|
|
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
|
|
|
|
|
kShadowNearPlane, kShadowFarPlane);
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
return lightProj * lightView;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::renderShadowPass() {
|
2026-02-04 16:30:24 -08:00
|
|
|
constexpr float kShadowHalfExtent = 180.0f;
|
|
|
|
|
constexpr float kShadowLightDistance = 280.0f;
|
|
|
|
|
constexpr float kShadowNearPlane = 1.0f;
|
|
|
|
|
constexpr float kShadowFarPlane = 600.0f;
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// 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]);
|
2026-02-04 16:22:18 -08:00
|
|
|
GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture");
|
|
|
|
|
GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture");
|
|
|
|
|
GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest");
|
|
|
|
|
GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity");
|
2026-02-04 16:36:03 -08:00
|
|
|
GLint useBonesLoc = glGetUniformLocation(shadowShaderProgram, "uUseBones");
|
2026-02-04 16:22:18 -08:00
|
|
|
if (useTexLoc >= 0) glUniform1i(useTexLoc, 0);
|
|
|
|
|
if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0);
|
|
|
|
|
if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f);
|
2026-02-04 16:36:03 -08:00
|
|
|
if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0);
|
2026-02-04 16:22:18 -08:00
|
|
|
if (texLoc >= 0) glUniform1i(texLoc, 0);
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
// Render terrain into shadow map
|
|
|
|
|
if (terrainRenderer) {
|
|
|
|
|
terrainRenderer->renderShadow(shadowShaderProgram);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render WMO into shadow map
|
|
|
|
|
if (wmoRenderer) {
|
|
|
|
|
// WMO renderShadow takes separate view/proj matrices and a Shader ref.
|
|
|
|
|
// We need to decompose our lightSpaceMatrix or use the raw shader program.
|
|
|
|
|
// Since WMO::renderShadow sets uModel per instance, we use the shadow shader
|
|
|
|
|
// directly by calling renderShadow with the light view/proj split.
|
|
|
|
|
// For simplicity, compute the split:
|
|
|
|
|
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
|
2026-02-04 16:22:18 -08:00
|
|
|
glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition;
|
2026-02-04 16:30:24 -08:00
|
|
|
float halfExtent = kShadowHalfExtent;
|
2026-02-04 16:08:35 -08:00
|
|
|
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);
|
2026-02-04 16:30:24 -08:00
|
|
|
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
|
|
|
|
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
|
|
|
|
|
kShadowNearPlane, kShadowFarPlane);
|
2026-02-04 16:08:35 -08:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 20:45:59 -08:00
|
|
|
// Render M2 doodads into shadow map
|
|
|
|
|
if (m2Renderer) {
|
2026-02-04 16:22:18 -08:00
|
|
|
m2Renderer->renderShadow(shadowShaderProgram);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 16:36:03 -08:00
|
|
|
// Render characters into shadow map
|
|
|
|
|
if (characterRenderer) {
|
2026-02-04 16:41:40 -08:00
|
|
|
// 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);
|
2026-02-04 16:36:03 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 16:08:35 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|