diff --git a/include/audio/mount_sound_manager.hpp b/include/audio/mount_sound_manager.hpp index 3eb96560..271a041a 100644 --- a/include/audio/mount_sound_manager.hpp +++ b/include/audio/mount_sound_manager.hpp @@ -40,6 +40,11 @@ public: void setFlying(bool flying); void setGrounded(bool grounded); + // Play semantic mount action sounds (triggered on animation state changes) + void playRearUpSound(); // Rear-up flourish (whinny/roar) + void playJumpSound(); // Jump start (grunt/snort) + void playLandSound(); // Landing (thud/hoof) + bool isMounted() const { return mounted_; } void setVolumeScale(float scale) { volumeScale_ = scale; } float getVolumeScale() const { return volumeScale_; } @@ -69,6 +74,7 @@ private: bool playingMovementSound_ = false; bool playingIdleSound_ = false; std::chrono::steady_clock::time_point lastSoundUpdate_; + std::chrono::steady_clock::time_point lastActionSoundTime_; // Cooldown for action sounds float soundLoopTimer_ = 0.0f; }; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7a2ddad0..2c3d2688 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -425,6 +425,10 @@ public: uint32_t getPlayerLevel() const { return serverPlayerLevel_; } static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel); + // Server time (for deterministic moon phases, etc.) + float getGameTime() const { return gameTime_; } + float getTimeSpeed() const { return timeSpeed_; } + // Player skills const std::map& getPlayerSkills() const { return playerSkills_; } const std::string& getSkillName(uint32_t skillId) const; @@ -1096,6 +1100,11 @@ private: uint32_t serverPlayerLevel_ = 1; static uint32_t xpForLevel(uint32_t level); + // ---- Server time tracking (for deterministic celestial/sky systems) ---- + float gameTime_ = 0.0f; // Server game time in seconds + float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour) + void handleLoginSetTimeSpeed(network::Packet& packet); + // ---- Player skills ---- std::map playerSkills_; std::unordered_map skillLineNames_; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 0b1439ed..ba0e75cb 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -42,6 +42,7 @@ enum class Opcode : uint16_t { SMSG_CHAR_DELETE = 0x03C, SMSG_PONG = 0x1DD, SMSG_LOGIN_VERIFY_WORLD = 0x236, + SMSG_LOGIN_SETTIMESPEED = 0x042, SMSG_ACCOUNT_DATA_TIMES = 0x209, SMSG_FEATURE_SYSTEM_STATUS = 0x3ED, SMSG_MOTD = 0x33D, diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index d81c20cb..6d8d716c 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -67,6 +67,7 @@ public: bool isGrounded() const { return grounded; } bool isJumping() const { return !grounded && verticalVelocity > 0.0f; } bool isFalling() const { return !grounded && verticalVelocity <= 0.0f; } + bool isJumpKeyPressed() const { return jumpBufferTimer > 0.0f; } bool isSprinting() const; bool isMovingForward() const { return moveForwardActive; } bool isMovingBackward() const { return moveBackwardActive; } @@ -92,6 +93,9 @@ public: void setFacingYaw(float yaw) { facingYaw = yaw; } // For taxi/scripted movement void clearMovementInputs(); + // Trigger mount jump (applies vertical velocity for physics hop) + void triggerMountJump(); + // For first-person player hiding void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { characterRenderer = cr; @@ -168,6 +172,16 @@ private: std::optional cachedCamWmoFloor; bool hasCachedCamFloor = false; + // Cached floor height queries (update every 5 frames or 2 unit movement) + glm::vec3 lastFloorQueryPos = glm::vec3(0.0f); + std::optional cachedFloorHeight; + int floorQueryFrameCounter = 0; + static constexpr float FLOOR_QUERY_DISTANCE_THRESHOLD = 2.0f; // Increased from 1.0 + static constexpr int FLOOR_QUERY_FRAME_INTERVAL = 5; // Increased from 3 + + // Helper to get cached floor height (reduces expensive queries) + std::optional getCachedFloorHeight(float x, float y, float z); + // Swimming bool swimming = false; bool wasSwimming = false; @@ -212,6 +226,13 @@ private: static constexpr float WOW_TURN_SPEED = 180.0f; // Keyboard turn deg/sec static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_JUMP_VELOCITY = 7.96f; + static constexpr float MOUNT_GRAVITY = -18.0f; // Snappy WoW-feel jump + static constexpr float MOUNT_JUMP_HEIGHT = 1.0f; // Desired jump height in meters + + // Computed jump velocity using vz = sqrt(2 * g * h) + static inline float getMountJumpVelocity() { + return std::sqrt(2.0f * std::abs(MOUNT_GRAVITY) * MOUNT_JUMP_HEIGHT); + } // Server-driven run speed override (0 = use default WOW_RUN_SPEED) float runSpeedOverride_ = 0.0f; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 9a5832de..6726b08e 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -54,7 +54,7 @@ public: void playAnimation(uint32_t instanceId, uint32_t animationId, bool loop = true); - void update(float deltaTime); + void update(float deltaTime, const glm::vec3& cameraPos = glm::vec3(0.0f)); void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection); void renderShadow(const glm::mat4& lightSpaceMatrix); @@ -74,6 +74,9 @@ public: bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const; bool getInstanceBounds(uint32_t instanceId, glm::vec3& outCenter, float& outRadius) const; + /** Debug: Log all available animations for an instance */ + void dumpAnimations(uint32_t instanceId) const; + /** Attach a weapon model to a character instance at the given attachment point. */ bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId, const pipeline::M2Model& weaponModel, uint32_t weaponModelId, @@ -82,6 +85,15 @@ public: /** Detach a weapon from the given attachment point. */ void detachWeapon(uint32_t charInstanceId, uint32_t attachmentId); + /** Get the world-space transform of an attachment point on an instance. + * Used for mount seats, weapon positions, etc. + * @param instanceId The character/mount instance + * @param attachmentId The attachment point ID (0=Mount, 1=RightHand, 2=LeftHand, etc.) + * @param outTransform The resulting world-space transform matrix + * @return true if attachment found and matrix computed + */ + bool getAttachmentTransform(uint32_t instanceId, uint32_t attachmentId, glm::mat4& outTransform); + size_t getInstanceCount() const { return instances.size(); } void setFog(const glm::vec3& color, float start, float end) { diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index a7b135f3..56104032 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -7,7 +7,7 @@ namespace wowee { namespace core { class Window; } -namespace game { class World; class ZoneManager; } +namespace game { class World; class ZoneManager; class GameHandler; } namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } namespace pipeline { class AssetManager; } @@ -27,6 +27,7 @@ class Clouds; class LensFlare; class Weather; class LightingManager; +class SkySystem; class SwimEffects; class MountDust; class CharacterRenderer; @@ -47,7 +48,7 @@ public: void beginFrame(); void endFrame(); - void renderWorld(game::World* world); + void renderWorld(game::World* world, game::GameHandler* gameHandler = nullptr); /** * Update renderer (camera, etc.) @@ -108,6 +109,7 @@ public: M2Renderer* getM2Renderer() const { return m2Renderer.get(); } Minimap* getMinimap() const { return minimap.get(); } QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); } + SkySystem* getSkySystem() const { return skySystem.get(); } const std::string& getCurrentZoneName() const { return currentZoneName; } // Third-person character follow @@ -176,6 +178,7 @@ private: std::unique_ptr lensFlare; std::unique_ptr weather; std::unique_ptr lightingManager; + std::unique_ptr skySystem; // Coordinator for sky rendering std::unique_ptr swimEffects; std::unique_ptr mountDust; std::unique_ptr characterRenderer; @@ -302,10 +305,27 @@ private: uint32_t equippedWeaponInvType_ = 0; // Mount state + // Mount animation capabilities (discovered at mount time, varies per model) + struct MountAnimSet { + uint32_t jumpStart = 0; // Jump start animation + uint32_t jumpLoop = 0; // Jump airborne loop + uint32_t jumpEnd = 0; // Jump landing + uint32_t rearUp = 0; // Rear-up / special flourish + uint32_t run = 0; // Run animation (discovered, don't assume) + uint32_t stand = 0; // Stand animation (discovered) + }; + + enum class MountAction { None, Jump, RearUp }; + uint32_t mountInstanceId_ = 0; float mountHeightOffset_ = 0.0f; float mountPitch_ = 0.0f; // Up/down tilt (radians) float mountRoll_ = 0.0f; // Left/right banking (radians) + float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) + float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() + MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) + uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining) + MountAnimSet mountAnims_; // Cached animation IDs for current mount bool taxiFlight_ = false; bool terrainEnabled = true; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 06215fe7..0be7f4fe 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -282,8 +282,8 @@ private: // Streaming parameters bool streamingEnabled = true; - int loadRadius = 8; // Load tiles within this radius (17x17 grid) - int unloadRadius = 12; // Unload tiles beyond this radius + int loadRadius = 4; // Load tiles within this radius (9x9 grid = 81 tiles) + int unloadRadius = 7; // Unload tiles beyond this radius float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps) float timeSinceLastUpdate = 0.0f; @@ -326,6 +326,17 @@ private: // Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x) std::unordered_set placedWmoIds; + + // Progressive M2 upload queue (spread heavy uploads across frames) + struct PendingM2Upload { + uint32_t modelId; + pipeline::M2Model model; + std::string path; + }; + std::queue m2UploadQueue_; + static constexpr int MAX_M2_UPLOADS_PER_FRAME = 5; // Upload up to 5 models per frame + + void processM2UploadQueue(); }; } // namespace rendering diff --git a/src/audio/mount_sound_manager.cpp b/src/audio/mount_sound_manager.cpp index 27671755..a87ed2f0 100644 --- a/src/audio/mount_sound_manager.cpp +++ b/src/audio/mount_sound_manager.cpp @@ -116,16 +116,19 @@ void MountSoundManager::loadMountSounds() { bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) { if (!assetManager_ || !assetManager_->fileExists(path)) { + LOG_WARNING("Mount sound file not found: ", path); return false; } auto data = assetManager_->readFile(path); if (data.empty()) { + LOG_WARNING("Mount sound file empty: ", path); return false; } sample.path = path; sample.data = std::move(data); + LOG_INFO("Loaded mount sound: ", path); return true; } @@ -184,6 +187,85 @@ void MountSoundManager::setGrounded(bool grounded) { setFlying(!grounded); } +void MountSoundManager::playRearUpSound() { + if (!mounted_) return; + + // Cooldown to prevent spam (200ms) + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); + if (elapsed < 200) return; + lastActionSoundTime_ = now; + + // Use semantic sound based on mount family + if (currentMountType_ == MountType::GROUND && !horseMoveSounds_.empty()) { + // Ground mounts: whinny/roar + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, horseMoveSounds_.size() - 1); + const auto& sample = horseMoveSounds_[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.7f * volumeScale_, 1.0f); + } + } else if (currentMountType_ == MountType::FLYING && !wingIdleSounds_.empty()) { + // Flying mounts: screech/roar + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, wingIdleSounds_.size() - 1); + const auto& sample = wingIdleSounds_[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 1.1f); + } + } +} + +void MountSoundManager::playJumpSound() { + if (!mounted_) return; + + // Cooldown to prevent spam + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); + if (elapsed < 200) return; + lastActionSoundTime_ = now; + + // Shorter, quieter sound for jump start + if (currentMountType_ == MountType::GROUND && !horseBreathSounds_.empty()) { + // Ground mounts: grunt/snort + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, horseBreathSounds_.size() - 1); + const auto& sample = horseBreathSounds_[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.5f * volumeScale_, 1.2f); + } + } else if (currentMountType_ == MountType::FLYING && !wingFlapSounds_.empty()) { + // Flying mounts: wing whoosh + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, wingFlapSounds_.size() - 1); + const auto& sample = wingFlapSounds_[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.4f * volumeScale_, 1.0f); + } + } +} + +void MountSoundManager::playLandSound() { + if (!mounted_) return; + + // Cooldown to prevent spam + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); + if (elapsed < 200) return; + lastActionSoundTime_ = now; + + // Landing thud/hoof sound + if (currentMountType_ == MountType::GROUND && !horseBreathSounds_.empty()) { + // Ground mounts: hoof thud (use breath as placeholder for now) + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, horseBreathSounds_.size() - 1); + const auto& sample = horseBreathSounds_[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 0.8f); // Lower pitch for thud + } + } +} + MountType MountSoundManager::detectMountType(uint32_t creatureDisplayId) const { // TODO: Load from CreatureDisplayInfo.dbc or CreatureModelData.dbc // For now, use simple heuristics based on common display IDs diff --git a/src/core/application.cpp b/src/core/application.cpp index 2ca9262f..e61bd7c8 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -389,25 +389,62 @@ void Application::update(float deltaTime) { break; case AppState::IN_GAME: { + // Application update profiling + static int appProfileCounter = 0; + static float ghTime = 0.0f, worldTime = 0.0f, spawnTime = 0.0f; + static float creatureQTime = 0.0f, goQTime = 0.0f, mountTime = 0.0f; + static float npcMgrTime = 0.0f, questMarkTime = 0.0f, syncTime = 0.0f; + + auto gh1 = std::chrono::high_resolution_clock::now(); if (gameHandler) { gameHandler->update(deltaTime); } + auto gh2 = std::chrono::high_resolution_clock::now(); + ghTime += std::chrono::duration(gh2 - gh1).count(); + + auto w1 = std::chrono::high_resolution_clock::now(); if (world) { world->update(deltaTime); } + auto w2 = std::chrono::high_resolution_clock::now(); + worldTime += std::chrono::duration(w2 - w1).count(); + + auto s1 = std::chrono::high_resolution_clock::now(); // Spawn NPCs once when entering world spawnNpcs(); + auto s2 = std::chrono::high_resolution_clock::now(); + spawnTime += std::chrono::duration(s2 - s1).count(); + auto cq1 = std::chrono::high_resolution_clock::now(); // Process deferred online creature spawns (throttled) processCreatureSpawnQueue(); + auto cq2 = std::chrono::high_resolution_clock::now(); + creatureQTime += std::chrono::duration(cq2 - cq1).count(); + + auto goq1 = std::chrono::high_resolution_clock::now(); processGameObjectSpawnQueue(); + auto goq2 = std::chrono::high_resolution_clock::now(); + goQTime += std::chrono::duration(goq2 - goq1).count(); + + auto m1 = std::chrono::high_resolution_clock::now(); processPendingMount(); + auto m2 = std::chrono::high_resolution_clock::now(); + mountTime += std::chrono::duration(m2 - m1).count(); + + auto nm1 = std::chrono::high_resolution_clock::now(); if (npcManager && renderer && renderer->getCharacterRenderer()) { npcManager->update(deltaTime, renderer->getCharacterRenderer()); } + auto nm2 = std::chrono::high_resolution_clock::now(); + npcMgrTime += std::chrono::duration(nm2 - nm1).count(); + auto qm1 = std::chrono::high_resolution_clock::now(); // Update 3D quest markers above NPCs updateQuestMarkers(); + auto qm2 = std::chrono::high_resolution_clock::now(); + questMarkTime += std::chrono::duration(qm2 - qm1).count(); + + auto sync1 = std::chrono::high_resolution_clock::now(); // Sync server run speed to camera controller if (renderer && gameHandler && renderer->getCameraController()) { @@ -489,6 +526,22 @@ void Application::update(float deltaTime) { } else { movementHeartbeatTimer = 0.0f; } + + auto sync2 = std::chrono::high_resolution_clock::now(); + syncTime += std::chrono::duration(sync2 - sync1).count(); + + // Log profiling every 60 frames + if (++appProfileCounter >= 60) { + LOG_INFO("APP UPDATE PROFILE (60 frames): gameHandler=", ghTime / 60.0f, + "ms world=", worldTime / 60.0f, "ms spawn=", spawnTime / 60.0f, + "ms creatureQ=", creatureQTime / 60.0f, "ms goQ=", goQTime / 60.0f, + "ms mount=", mountTime / 60.0f, "ms npcMgr=", npcMgrTime / 60.0f, + "ms questMark=", questMarkTime / 60.0f, "ms sync=", syncTime / 60.0f, "ms"); + appProfileCounter = 0; + ghTime = worldTime = spawnTime = 0.0f; + creatureQTime = goQTime = mountTime = 0.0f; + npcMgrTime = questMarkTime = syncTime = 0.0f; + } break; } @@ -498,14 +551,30 @@ void Application::update(float deltaTime) { } // Update renderer (camera, etc.) only when in-game + static int rendererProfileCounter = 0; + static float rendererTime = 0.0f, uiTime = 0.0f; + + auto r1 = std::chrono::high_resolution_clock::now(); if (renderer && state == AppState::IN_GAME) { renderer->update(deltaTime); } + auto r2 = std::chrono::high_resolution_clock::now(); + rendererTime += std::chrono::duration(r2 - r1).count(); // Update UI + auto u1 = std::chrono::high_resolution_clock::now(); if (uiManager) { uiManager->update(deltaTime); } + auto u2 = std::chrono::high_resolution_clock::now(); + uiTime += std::chrono::duration(u2 - u1).count(); + + if (state == AppState::IN_GAME && ++rendererProfileCounter >= 60) { + LOG_INFO("RENDERER/UI PROFILE (60 frames): renderer=", rendererTime / 60.0f, + "ms ui=", uiTime / 60.0f, "ms"); + rendererProfileCounter = 0; + rendererTime = uiTime = 0.0f; + } } void Application::render() { @@ -518,9 +587,9 @@ void Application::render() { // Only render 3D world when in-game (after server connect or single-player) if (state == AppState::IN_GAME) { if (world) { - renderer->renderWorld(world.get()); + renderer->renderWorld(world.get(), gameHandler.get()); } else { - renderer->renderWorld(nullptr); + renderer->renderWorld(nullptr, gameHandler.get()); } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d19c3792..fd541d99 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -118,6 +118,16 @@ bool GameHandler::isConnected() const { } void GameHandler::update(float deltaTime) { + // Timing profiling (log every 60 frames to reduce spam) + static int profileCounter = 0; + static float socketTime = 0.0f; + static float taxiTime = 0.0f; + static float distanceCheckTime = 0.0f; + static float entityUpdateTime = 0.0f; + static float totalTime = 0.0f; + + auto updateStart = std::chrono::high_resolution_clock::now(); + // Fire deferred char-create callback (outside ImGui render) if (pendingCharCreateResult_) { pendingCharCreateResult_ = false; @@ -131,9 +141,12 @@ void GameHandler::update(float deltaTime) { } // Update socket (processes incoming data and triggers callbacks) + auto socketStart = std::chrono::high_resolution_clock::now(); if (socket) { socket->update(); } + auto socketEnd = std::chrono::high_resolution_clock::now(); + socketTime += std::chrono::duration(socketEnd - socketStart).count(); // Validate target still exists if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) { @@ -187,6 +200,9 @@ void GameHandler::update(float deltaTime) { taxiLandingCooldown_ -= deltaTime; } + // Taxi logic timing + auto taxiStart = std::chrono::high_resolution_clock::now(); + // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared if (onTaxiFlight_) { updateClientTaxi(deltaTime); @@ -286,6 +302,12 @@ void GameHandler::update(float deltaTime) { } } + auto taxiEnd = std::chrono::high_resolution_clock::now(); + taxiTime += std::chrono::duration(taxiEnd - taxiStart).count(); + + // Distance check timing + auto distanceStart = std::chrono::high_resolution_clock::now(); + // Leave combat if auto-attack target is too far away (leash range) if (autoAttacking && autoAttackTarget != 0) { auto targetEntity = entityManager.getEntity(autoAttackTarget); @@ -350,10 +372,51 @@ void GameHandler::update(float deltaTime) { } } + auto distanceEnd = std::chrono::high_resolution_clock::now(); + distanceCheckTime += std::chrono::duration(distanceEnd - distanceStart).count(); + + // Entity update timing + auto entityStart = std::chrono::high_resolution_clock::now(); + // Update entity movement interpolation (keeps targeting in sync with visuals) + // Only update entities within reasonable distance for performance + const float updateRadiusSq = 150.0f * 150.0f; // 150 unit radius + auto playerEntity = entityManager.getEntity(playerGuid); + glm::vec3 playerPos = playerEntity ? glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()) : glm::vec3(0.0f); + for (auto& [guid, entity] : entityManager.getEntities()) { - entity->updateMovement(deltaTime); + // Always update player + if (guid == playerGuid) { + entity->updateMovement(deltaTime); + continue; + } + + // Distance cull other entities + glm::vec3 entityPos(entity->getX(), entity->getY(), entity->getZ()); + float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos); + if (distSq < updateRadiusSq) { + entity->updateMovement(deltaTime); + } } + + auto entityEnd = std::chrono::high_resolution_clock::now(); + entityUpdateTime += std::chrono::duration(entityEnd - entityStart).count(); + } + + auto updateEnd = std::chrono::high_resolution_clock::now(); + totalTime += std::chrono::duration(updateEnd - updateStart).count(); + + // Log profiling every 60 frames + if (++profileCounter >= 60) { + LOG_INFO("UPDATE PROFILE (60 frames): socket=", socketTime / 60.0f, "ms taxi=", taxiTime / 60.0f, + "ms distance=", distanceCheckTime / 60.0f, "ms entity=", entityUpdateTime / 60.0f, + "ms TOTAL=", totalTime / 60.0f, "ms"); + profileCounter = 0; + socketTime = 0.0f; + taxiTime = 0.0f; + distanceCheckTime = 0.0f; + entityUpdateTime = 0.0f; + totalTime = 0.0f; } } @@ -418,6 +481,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_LOGIN_SETTIMESPEED: + // Can be received during login or at any time after + handleLoginSetTimeSpeed(packet); + break; + case Opcode::SMSG_ACCOUNT_DATA_TIMES: // Can be received at any time after authentication handleAccountDataTimes(packet); @@ -1472,6 +1540,28 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world..."); } +void GameHandler::handleLoginSetTimeSpeed(network::Packet& packet) { + // SMSG_LOGIN_SETTIMESPEED (0x042) + // Structure: uint32 gameTime, float timeScale + // gameTime: Game time in seconds since epoch + // timeScale: Time speed multiplier (typically 0.0166 for 1 day = 1 hour) + + if (packet.getSize() < 8) { + LOG_WARNING("SMSG_LOGIN_SETTIMESPEED: packet too small (", packet.getSize(), " bytes)"); + return; + } + + uint32_t gameTimePacked = packet.readUInt32(); + float timeScale = packet.readFloat(); + + // Store for celestial/sky system use + gameTime_ = static_cast(gameTimePacked); + timeSpeed_ = timeScale; + + LOG_INFO("Server time: gameTime=", gameTime_, "s, timeSpeed=", timeSpeed_); + LOG_INFO(" (1 game day = ", (1.0f / timeSpeed_) / 60.0f, " real minutes)"); +} + void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD"); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index ff35803b..6ea0d69e 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -75,6 +75,38 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) { thirdPerson = true; } +std::optional CameraController::getCachedFloorHeight(float x, float y, float z) { + // Check cache validity (position within threshold and frame count) + glm::vec2 queryPos(x, y); + glm::vec2 cachedPos(lastFloorQueryPos.x, lastFloorQueryPos.y); + float dist = glm::length(queryPos - cachedPos); + + if (dist < FLOOR_QUERY_DISTANCE_THRESHOLD && floorQueryFrameCounter < FLOOR_QUERY_FRAME_INTERVAL) { + floorQueryFrameCounter++; + return cachedFloorHeight; + } + + // Cache miss - query and update + floorQueryFrameCounter = 0; + lastFloorQueryPos = glm::vec3(x, y, z); + + std::optional result; + if (terrainManager) { + result = terrainManager->getHeightAt(x, y); + } + if (wmoRenderer) { + auto wh = wmoRenderer->getFloorHeight(x, y, z + 2.0f); + if (wh && (!result || *wh > *result)) result = wh; + } + if (m2Renderer && !externalFollow_) { + auto mh = m2Renderer->getFloorHeight(x, y, z); + if (mh && (!result || *mh > *result)) result = mh; + } + + cachedFloorHeight = result; + return result; +} + void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; @@ -342,17 +374,21 @@ void CameraController::update(float deltaTime) { float swimSpeed = speed * SWIM_SPEED_FACTOR; float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z; - glm::vec3 swimForward = glm::normalize(forward3D); - if (glm::length(swimForward) < 1e-4f) { + // For auto-run/auto-swim: use character facing (immune to camera pan) + // For manual W key: use camera direction (swim where you look) + glm::vec3 swimForward; + if (autoRunning || (leftMouseDown && rightMouseDown)) { + // Auto-running: use character's horizontal facing direction swimForward = forward; - } - glm::vec3 swimRight = camera->getRight(); - swimRight.z = 0.0f; - if (glm::length(swimRight) > 1e-4f) { - swimRight = glm::normalize(swimRight); } else { - swimRight = right; + // Manual control: use camera's 3D direction (swim where you look) + swimForward = glm::normalize(forward3D); + if (glm::length(swimForward) < 1e-4f) { + swimForward = forward; + } } + // Use character's facing direction for strafe, not camera's right vector + glm::vec3 swimRight = right; // Character's right (horizontal facing), not camera's glm::vec3 swimMove(0.0f); if (nowForward) swimMove += swimForward; @@ -396,17 +432,32 @@ void CameraController::update(float deltaTime) { } // Prevent sinking/clipping through world floor while swimming. + // Cache floor queries (update every 3 frames or 1 unit movement) std::optional floorH; - if (terrainManager) { - floorH = terrainManager->getHeightAt(targetPos.x, targetPos.y); - } - if (wmoRenderer) { - auto wh = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 2.0f); - if (wh && (!floorH || *wh > *floorH)) floorH = wh; - } - if (m2Renderer && !externalFollow_) { - auto mh = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z); - if (mh && (!floorH || *mh > *floorH)) floorH = mh; + float dist2D = glm::length(glm::vec2(targetPos.x - lastFloorQueryPos.x, + targetPos.y - lastFloorQueryPos.y)); + bool updateFloorCache = (floorQueryFrameCounter++ >= FLOOR_QUERY_FRAME_INTERVAL) || + (dist2D > FLOOR_QUERY_DISTANCE_THRESHOLD); + + if (updateFloorCache) { + floorQueryFrameCounter = 0; + lastFloorQueryPos = targetPos; + + if (terrainManager) { + floorH = terrainManager->getHeightAt(targetPos.x, targetPos.y); + } + if (wmoRenderer) { + auto wh = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 2.0f); + if (wh && (!floorH || *wh > *floorH)) floorH = wh; + } + if (m2Renderer && !externalFollow_) { + auto mh = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z); + if (mh && (!floorH || *mh > *floorH)) floorH = mh; + } + + cachedFloorHeight = floorH; + } else { + floorH = cachedFloorHeight; } if (floorH) { float swimFloor = *floorH + 0.5f; @@ -469,7 +520,7 @@ void CameraController::update(float deltaTime) { if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME; if (grounded) coyoteTimer = COYOTE_TIME; - bool canJump = (coyoteTimer > 0.0f) && (jumpBufferTimer > 0.0f); + bool canJump = (coyoteTimer > 0.0f) && (jumpBufferTimer > 0.0f) && !mounted_; if (canJump) { verticalVelocity = jumpVel; grounded = false; @@ -895,7 +946,7 @@ void CameraController::update(float deltaTime) { if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME; if (grounded) coyoteTimer = COYOTE_TIME; - if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f) { + if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f && !mounted_) { verticalVelocity = jumpVel; grounded = false; jumpBufferTimer = 0.0f; @@ -1400,5 +1451,15 @@ bool CameraController::isSprinting() const { return enabled && camera && runPace; } +void CameraController::triggerMountJump() { + // Apply physics-driven mount jump: vz = sqrt(2 * g * h) + // Desired height and gravity are configurable constants + if (grounded || coyoteTimer > 0.0f) { + verticalVelocity = getMountJumpVelocity(); + grounded = false; + coyoteTimer = 0.0f; + } +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index b58e8bf9..88a91796 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -21,7 +21,7 @@ bool Celestial::initialize() { // Create celestial shader celestialShader = std::make_unique(); - // Vertex shader - billboard facing camera + // Vertex shader - billboard facing camera (sky dome locked) const char* vertexShaderSource = R"( #version 330 core layout (location = 0) in vec3 aPos; @@ -36,13 +36,10 @@ bool Celestial::initialize() { void main() { TexCoord = aTexCoord; - // Billboard: remove rotation from view matrix, keep only translation - mat4 viewNoRotation = view; - viewNoRotation[0][0] = 1.0; viewNoRotation[0][1] = 0.0; viewNoRotation[0][2] = 0.0; - viewNoRotation[1][0] = 0.0; viewNoRotation[1][1] = 1.0; viewNoRotation[1][2] = 0.0; - viewNoRotation[2][0] = 0.0; viewNoRotation[2][1] = 0.0; viewNoRotation[2][2] = 1.0; + // Sky object: remove translation, keep rotation (skybox technique) + mat4 viewNoTranslation = mat4(mat3(view)); - gl_Position = projection * viewNoRotation * model * vec4(aPos, 1.0); + gl_Position = projection * viewNoTranslation * model * vec4(aPos, 1.0); } )"; @@ -128,21 +125,28 @@ void Celestial::shutdown() { void Celestial::render(const Camera& camera, float timeOfDay, const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) { if (!renderingEnabled || vao == 0 || !celestialShader) { + LOG_WARNING("Celestial render blocked: enabled=", renderingEnabled, " vao=", vao, " shader=", (celestialShader ? "ok" : "null")); return; } + LOG_INFO("Celestial render: timeOfDay=", timeOfDay, " gameTime=", gameTime); + // Update moon phases from game time if available (deterministic) if (gameTime >= 0.0f) { updatePhasesFromGameTime(gameTime); } - // Enable blending for celestial glow + // Enable additive blending for celestial glow (brighter against sky) glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending for brightness - // Disable depth writing (but keep depth testing) + // Disable depth testing entirely - celestial bodies render "on" the sky + glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); + // Disable culling - billboards can face either way + glDisable(GL_CULL_FACE); + // Render sun and moons (pass lighting parameters) renderSun(camera, timeOfDay, sunDir, sunColor); renderMoon(camera, timeOfDay); // White Lady (primary moon) @@ -152,34 +156,37 @@ void Celestial::render(const Camera& camera, float timeOfDay, } // Restore state + glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glDisable(GL_BLEND); + glEnable(GL_CULL_FACE); } void Celestial::renderSun(const Camera& camera, float timeOfDay, const glm::vec3* sunDir, const glm::vec3* sunColor) { // Sun visible from 5:00 to 19:00 if (timeOfDay < 5.0f || timeOfDay >= 19.0f) { + LOG_INFO("Sun not visible: timeOfDay=", timeOfDay, " (visible 5:00-19:00)"); return; } + LOG_INFO("Rendering sun: timeOfDay=", timeOfDay, " sunDir=", (sunDir ? "yes" : "no"), " sunColor=", (sunColor ? "yes" : "no")); + celestialShader->use(); - // Get sun position (use lighting direction if provided) - glm::vec3 sunPos; - if (sunDir) { - // Place sun along the lighting direction at far distance - const float sunDistance = 800.0f; - sunPos = -*sunDir * sunDistance; // Negative because light comes FROM sun - } else { - // Fallback to time-based position - sunPos = getSunPosition(timeOfDay); - } + // TESTING: Try X-up (final axis test) + glm::vec3 dir = glm::normalize(glm::vec3(1.0f, 0.0f, 0.0f)); // X-up test + LOG_INFO("Sun direction (TESTING X-UP): dir=(", dir.x, ",", dir.y, ",", dir.z, ")"); + + // Place sun on sky sphere at fixed distance + const float sunDistance = 800.0f; + glm::vec3 sunPos = dir * sunDistance; + LOG_INFO("Sun position: dir * ", sunDistance, " = (", sunPos.x, ",", sunPos.y, ",", sunPos.z, ")"); // Create model matrix glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, sunPos); - model = glm::scale(model, glm::vec3(50.0f, 50.0f, 1.0f)); // 50 unit diameter + model = glm::scale(model, glm::vec3(500.0f, 500.0f, 1.0f)); // Large and visible // Set uniforms glm::mat4 view = camera.getViewMatrix(); @@ -309,13 +316,17 @@ glm::vec3 Celestial::getSunPosition(float timeOfDay) const { // Sun rises at 6:00, peaks at 12:00, sets at 18:00 float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f); - const float radius = 800.0f; // Distance from origin - const float height = 600.0f; // Maximum height + const float radius = 800.0f; // Horizontal distance + const float height = 600.0f; // Maximum height at zenith - // Arc across sky - float x = radius * std::sin(angle); - float z = height * std::cos(angle); - float y = 0.0f; // Y is horizontal in WoW coordinates + // Arc across sky (angle 0→π maps to sunrise→noon→sunset) + // Z is vertical (matches skybox: Altitude = aPos.z) + // At angle=0: x=radius, z=0 (east horizon) + // At angle=π/2: x=0, z=height (zenith, directly overhead) + // At angle=π: x=-radius, z=0 (west horizon) + float x = radius * std::cos(angle); // Horizontal position (E→W) + float y = 0.0f; // Y is north-south (keep at 0) + float z = height * std::sin(angle); // Vertical position (Z is UP, matches skybox) return glm::vec3(x, y, z); } @@ -331,9 +342,10 @@ glm::vec3 Celestial::getMoonPosition(float timeOfDay) const { const float radius = 800.0f; const float height = 600.0f; - float x = radius * std::sin(angle); - float z = height * std::cos(angle); + // Same arc formula as sun (Z is vertical, matches skybox) + float x = radius * std::cos(angle); float y = 0.0f; + float z = height * std::sin(angle); return glm::vec3(x, y, z); } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 64cae49d..f8d93651 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -28,6 +28,10 @@ #include #include #include +#include +#include +#include +#include namespace wowee { namespace rendering { @@ -899,18 +903,21 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId, instance.currentSequenceIndex = 0; instance.currentAnimationId = model.sequences[0].id; } - core::Logger::getInstance().warning("Animation ", animationId, " not found, using default"); - // Dump available animation IDs for debugging - std::string ids; - for (size_t i = 0; i < model.sequences.size(); i++) { - if (!ids.empty()) ids += ", "; - ids += std::to_string(model.sequences[i].id); + + // Only log missing animation once per model (reduce spam) + static std::unordered_map> loggedMissingAnims; + uint32_t modelId = instance.modelId; // Use modelId as identifier + if (loggedMissingAnims[modelId].insert(animationId).second) { + // First time seeing this missing animation for this model + LOG_WARNING("Animation ", animationId, " not found in model ", modelId, ", using default"); } - core::Logger::getInstance().info("Available animation IDs (", model.sequences.size(), "): ", ids); } } -void CharacterRenderer::update(float deltaTime) { +void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { + // Distance culling for animation updates (150 unit radius) + const float animUpdateRadiusSq = 150.0f * 150.0f; + // Update fade-in opacity for (auto& [id, inst] : instances) { if (inst.fadeInDuration > 0.0f && inst.opacity < 1.0f) { @@ -940,8 +947,47 @@ void CharacterRenderer::update(float deltaTime) { } } + // Only update animations for nearby characters (performance optimization) + // Collect instances that need updates + std::vector> toUpdate; + toUpdate.reserve(instances.size()); + for (auto& pair : instances) { - updateAnimation(pair.second, deltaTime); + float distSq = glm::distance2(pair.second.position, cameraPos); + if (distSq < animUpdateRadiusSq) { + toUpdate.push_back(std::ref(pair.second)); + } + } + + int updatedCount = toUpdate.size(); + + // Thread bone calculations if we have many characters (4+) + if (updatedCount >= 4) { + std::vector> futures; + futures.reserve(updatedCount); + + for (auto& instRef : toUpdate) { + futures.push_back(std::async(std::launch::async, [this, &instRef, deltaTime]() { + updateAnimation(instRef.get(), deltaTime); + })); + } + + // Wait for all to complete + for (auto& f : futures) { + f.get(); + } + } else { + // Sequential for small counts (avoid thread overhead) + for (auto& instRef : toUpdate) { + updateAnimation(instRef.get(), deltaTime); + } + } + + static int logCounter = 0; + if (++logCounter >= 300) { // Log every 10 seconds at 30fps + LOG_INFO("CharacterRenderer: ", updatedCount, "/", instances.size(), " instances updated (", + instances.size() - updatedCount, " culled)"); + logCounter = 0; } // Update weapon attachment transforms (after all bone matrices are computed) @@ -1729,5 +1775,87 @@ void CharacterRenderer::detachWeapon(uint32_t charInstanceId, uint32_t attachmen } } +bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t attachmentId, glm::mat4& outTransform) { + auto instIt = instances.find(instanceId); + if (instIt == instances.end()) return false; + const auto& instance = instIt->second; + + auto modelIt = models.find(instance.modelId); + if (modelIt == models.end()) return false; + const auto& model = modelIt->second.data; + + // Find attachment point + uint16_t boneIndex = 0; + glm::vec3 offset(0.0f); + bool found = false; + + // Try attachment lookup first + if (attachmentId < model.attachmentLookup.size()) { + uint16_t attIdx = model.attachmentLookup[attachmentId]; + if (attIdx < model.attachments.size()) { + boneIndex = model.attachments[attIdx].bone; + offset = model.attachments[attIdx].position; + found = true; + } + } + + // Fallback: scan attachments by id + if (!found) { + for (const auto& att : model.attachments) { + if (att.id == attachmentId) { + boneIndex = att.bone; + offset = att.position; + found = true; + break; + } + } + } + + if (!found) return false; + + // Get bone matrix + glm::mat4 boneMat(1.0f); + if (boneIndex < instance.boneMatrices.size()) { + boneMat = instance.boneMatrices[boneIndex]; + } + + // Compute world transform: modelMatrix * boneMatrix * offsetTranslation + glm::mat4 modelMat = instance.hasOverrideModelMatrix + ? instance.overrideModelMatrix + : getModelMatrix(instance); + + outTransform = modelMat * boneMat * glm::translate(glm::mat4(1.0f), offset); + return true; +} + +void CharacterRenderer::dumpAnimations(uint32_t instanceId) const { + auto instIt = instances.find(instanceId); + if (instIt == instances.end()) { + core::Logger::getInstance().info("dumpAnimations: instance ", instanceId, " not found"); + return; + } + const auto& instance = instIt->second; + + auto modelIt = models.find(instance.modelId); + if (modelIt == models.end()) { + core::Logger::getInstance().info("dumpAnimations: model not found for instance ", instanceId); + return; + } + const auto& model = modelIt->second.data; + + core::Logger::getInstance().info("=== Animation dump for ", model.name, " ==="); + core::Logger::getInstance().info("Total animations: ", model.sequences.size()); + + for (size_t i = 0; i < model.sequences.size(); i++) { + const auto& seq = model.sequences[i]; + core::Logger::getInstance().info(" [", i, "] animId=", seq.id, + " variation=", seq.variationIndex, + " duration=", seq.duration, "ms", + " speed=", seq.movingSpeed, + " flags=0x", std::hex, seq.flags, std::dec); + } + core::Logger::getInstance().info("=== End animation dump ==="); +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index a3f6862f..2efb84fb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -13,6 +13,7 @@ #include "rendering/lens_flare.hpp" #include "rendering/weather.hpp" #include "rendering/lighting_manager.hpp" +#include "rendering/sky_system.hpp" #include "rendering/swim_effects.hpp" #include "rendering/mount_dust.hpp" #include "rendering/character_renderer.hpp" @@ -21,6 +22,7 @@ #include "rendering/minimap.hpp" #include "rendering/quest_marker_renderer.hpp" #include "rendering/shader.hpp" +#include "game/game_handler.hpp" #include "pipeline/m2_loader.hpp" #include #include "pipeline/asset_manager.hpp" @@ -55,6 +57,7 @@ #include #include #include +#include namespace wowee { namespace rendering { @@ -290,6 +293,17 @@ bool Renderer::initialize(core::Window* win) { lensFlare.reset(); } + // Create sky system (coordinator for sky rendering) + skySystem = std::make_unique(); + if (!skySystem->initialize()) { + LOG_WARNING("Failed to initialize sky system"); + skySystem.reset(); + } else { + // Note: SkySystem manages its own components internally + // Keep existing components for backwards compatibility (PerformanceHUD access) + LOG_INFO("Sky system initialized successfully (coordinator active)"); + } + // Create weather system weather = std::make_unique(); if (!weather->initialize()) { @@ -543,12 +557,173 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) { mountInstanceId_ = mountInstId; mountHeightOffset_ = heightOffset; + mountAction_ = MountAction::None; // Clear mount action state + mountActionPhase_ = 0; charAnimState = CharAnimState::MOUNT; if (cameraController) { cameraController->setMounted(true); cameraController->setMountHeightOffset(heightOffset); } + // Debug: dump available mount animations + if (characterRenderer && mountInstId > 0) { + characterRenderer->dumpAnimations(mountInstId); + } + + // Discover mount animation capabilities (property-based, not hardcoded IDs) + LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); + characterRenderer->dumpAnimations(mountInstId); + + // Get all sequences for property-based analysis + std::vector 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 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 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 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) + + // Ensure we have fallbacks for movement + if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found + if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run + + core::Logger::getInstance().info("Mount animation set: jumpStart=", mountAnims_.jumpStart, + " jumpLoop=", mountAnims_.jumpLoop, + " jumpEnd=", mountAnims_.jumpEnd, + " rearUp=", mountAnims_.rearUp, + " run=", mountAnims_.run, + " stand=", mountAnims_.stand); + // Notify mount sound manager if (mountSoundManager) { bool isFlying = taxiFlight_; // Taxi flights are flying mounts @@ -561,6 +736,8 @@ void Renderer::clearMount() { mountHeightOffset_ = 0.0f; mountPitch_ = 0.0f; mountRoll_ = 0.0f; + mountAction_ = MountAction::None; + mountActionPhase_ = 0; charAnimState = CharAnimState::IDLE; if (cameraController) { cameraController->setMounted(false); @@ -717,6 +894,23 @@ void Renderer::updateCharacterAnimation() { if (mountInstanceId_ > 0) { characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); float yawRad = glm::radians(characterYaw); + + // Procedural lean into turns (ground mounts only, optional enhancement) + if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { + float currentYawDeg = characterYaw; + float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_; + // Normalize to [-180, 180] for wrap-around + while (turnRate > 180.0f) turnRate -= 360.0f; + while (turnRate < -180.0f) turnRate += 360.0f; + + float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f); + mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f); + prevMountYaw_ = currentYawDeg; + } else { + // Return to upright when not turning + mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f); + } + // Apply pitch (up/down), roll (banking), and yaw for realistic flight characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad)); @@ -731,7 +925,99 @@ void Renderer::updateCharacterAnimation() { }; uint32_t mountAnimId = ANIM_STAND; - if (moving) { + + // Get current mount animation state (used throughout) + uint32_t curMountAnim = 0; + float curMountTime = 0, curMountDur = 0; + bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); + + // Check for jump trigger - use cached per-mount animation IDs + if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { + if (moving && mountAnims_.jumpLoop > 0) { + // Moving: skip JumpStart (looks like stopping), go straight to airborne loop + LOG_INFO("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountAction_ = MountAction::Jump; + mountActionPhase_ = 1; // Start in airborne phase + mountAnimId = mountAnims_.jumpLoop; + if (mountSoundManager) { + mountSoundManager->playJumpSound(); + } + if (cameraController) { + cameraController->triggerMountJump(); + } + } else if (!moving && mountAnims_.rearUp > 0) { + // Standing still: rear-up flourish + LOG_INFO("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); + mountAction_ = MountAction::RearUp; + mountActionPhase_ = 0; + mountAnimId = mountAnims_.rearUp; + // Trigger semantic rear-up sound + if (mountSoundManager) { + mountSoundManager->playRearUpSound(); + } + } + } + + // Handle active mount actions (jump chaining or rear-up) + if (mountAction_ != MountAction::None) { + bool animFinished = haveMountState && curMountDur > 0.1f && + (curMountTime >= curMountDur - 0.05f); + + if (mountAction_ == MountAction::Jump) { + // Jump sequence: start → loop → end (physics-driven) + if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { + // JumpStart finished, go to JumpLoop (airborne) + LOG_INFO("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountActionPhase_ = 1; + mountAnimId = mountAnims_.jumpLoop; + } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { + // No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose) + LOG_INFO("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); + mountActionPhase_ = 1; + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { + // Landed after airborne phase! Go to JumpEnd (grounded-triggered) + LOG_INFO("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); + mountActionPhase_ = 2; + mountAnimId = mountAnims_.jumpEnd; + // Trigger semantic landing sound + if (mountSoundManager) { + mountSoundManager->playLandSound(); + } + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { + // No JumpEnd animation, return directly to movement after landing + LOG_INFO("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else if (mountActionPhase_ == 2 && animFinished) { + // JumpEnd finished, return to movement + LOG_INFO("Mount jump: phase 2→done (JumpEnd finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; // Keep current jump animation + } + } else if (mountAction_ == MountAction::RearUp) { + // Rear-up: single animation, return to stand when done + if (animFinished) { + LOG_INFO("Mount rear-up: finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; // Keep current rear-up animation + } + } + } else if (moving) { + // Normal movement animations if (anyStrafeLeft) { mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); } else if (anyStrafeRight) { @@ -742,14 +1028,15 @@ void Renderer::updateCharacterAnimation() { mountAnimId = ANIM_RUN; } } - uint32_t curMountAnim = 0; - float curMountTime = 0, curMountDur = 0; - bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); - if (!haveMountState || curMountAnim != mountAnimId) { - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + + // Only update animation if it changed and we're not in an action sequence + if (mountAction_ == MountAction::None && (!haveMountState || curMountAnim != mountAnimId)) { + bool loop = true; // Normal movement animations loop + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop); } - // Rider bob: sinusoidal motion synced to mount's run animation + // Rider bob: sinusoidal motion synced to mount's run animation (only used in fallback positioning) + mountBob = 0.0f; if (moving && haveMountState && curMountDur > 1.0f) { float norm = std::fmod(curMountTime, curMountDur) / curMountDur; // One bounce per stride cycle @@ -758,28 +1045,35 @@ void Renderer::updateCharacterAnimation() { } } - // Character follows mount's full rotation (pitch, roll, yaw) - // This keeps the character "glued" to the mount during banking/climbing - float yawRad = glm::radians(characterYaw); + // 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]); - // Create rotation matrix from mount's orientation - glm::mat4 mountRotation = glm::mat4(1.0f); - mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); // Yaw (Z) - mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); // Roll (X) - mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); // Pitch (Y) + // 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); - // Offset in mount's local space (rider sits above mount) - glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); + // Position rider at mount seat + characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset); - // Transform offset through mount's rotation to get world-space offset - glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); - - // Character position = mount position + rotated offset - glm::vec3 playerPos = characterPosition + worldOffset; - characterRenderer->setInstancePosition(characterInstanceId, playerPos); - - // Character rotates with mount (same pitch, roll, yaw) - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); + // Rider uses character facing yaw, not mount bone rotation + // (rider faces character direction, seat bone only provides position) + float yawRad = glm::radians(characterYaw); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad)); + } else { + // Fallback to old manual positioning if attachment not found + float yawRad = glm::radians(characterYaw); + glm::mat4 mountRotation = glm::mat4(1.0f); + mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); + mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); + mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); + glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); + glm::vec3 playerPos = characterPosition + worldOffset; + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); + } return; } @@ -1184,9 +1478,18 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const { void Renderer::update(float deltaTime) { auto updateStart = std::chrono::steady_clock::now(); + lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation() + + // Renderer update profiling + static int rendProfileCounter = 0; + static float camTime = 0.0f, lightTime = 0.0f, charAnimTime = 0.0f; + static float terrainTime = 0.0f, skyTime = 0.0f, charRendTime = 0.0f; + static float audioTime = 0.0f, footstepTime = 0.0f, ambientTime = 0.0f; + if (wmoRenderer) wmoRenderer->resetQueryStats(); if (m2Renderer) m2Renderer->resetQueryStats(); + auto cam1 = std::chrono::high_resolution_clock::now(); if (cameraController) { auto cameraStart = std::chrono::steady_clock::now(); cameraController->update(deltaTime); @@ -1201,8 +1504,11 @@ void Renderer::update(float deltaTime) { } else { lastCameraUpdateMs = 0.0; } + auto cam2 = std::chrono::high_resolution_clock::now(); + camTime += std::chrono::duration(cam2 - cam1).count(); // Update lighting system + auto light1 = std::chrono::high_resolution_clock::now(); if (lightingManager) { // TODO: Get actual map ID from game state (0 = Eastern Kingdoms for now) // TODO: Get actual game time from server (use -1 for local time fallback) @@ -1214,8 +1520,11 @@ void Renderer::update(float deltaTime) { lightingManager->update(characterPosition, mapId, gameTime, isRaining, isUnderwater); } + auto light2 = std::chrono::high_resolution_clock::now(); + lightTime += std::chrono::duration(light2 - light1).count(); // Sync character model position/rotation and animation with follow target + auto charAnim1 = std::chrono::high_resolution_clock::now(); if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) { if (meleeSwingCooldown > 0.0f) { meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime); @@ -1261,13 +1570,19 @@ void Renderer::update(float deltaTime) { // Update animation based on movement state updateCharacterAnimation(); } + auto charAnim2 = std::chrono::high_resolution_clock::now(); + charAnimTime += std::chrono::duration(charAnim2 - charAnim1).count(); // Update terrain streaming + auto terrain1 = std::chrono::high_resolution_clock::now(); if (terrainManager && camera) { terrainManager->update(*camera, deltaTime); } + auto terrain2 = std::chrono::high_resolution_clock::now(); + terrainTime += std::chrono::duration(terrain2 - terrain1).count(); // Update skybox time progression + auto sky1 = std::chrono::high_resolution_clock::now(); if (skybox) { skybox->update(deltaTime); } @@ -1319,16 +1634,25 @@ void Renderer::update(float deltaTime) { } } } + auto sky2 = std::chrono::high_resolution_clock::now(); + skyTime += std::chrono::duration(sky2 - sky1).count(); // Update character animations - if (characterRenderer) { - characterRenderer->update(deltaTime); + auto charRend1 = std::chrono::high_resolution_clock::now(); + if (characterRenderer && camera) { + characterRenderer->update(deltaTime, camera->getPosition()); } + auto charRend2 = std::chrono::high_resolution_clock::now(); + charRendTime += std::chrono::duration(charRend2 - charRend1).count(); // Update AudioEngine (cleanup finished sounds, etc.) + auto audio1 = std::chrono::high_resolution_clock::now(); audio::AudioEngine::instance().update(deltaTime); + auto audio2 = std::chrono::high_resolution_clock::now(); + audioTime += std::chrono::duration(audio2 - audio1).count(); // Footsteps: animation-event driven + surface query at event time. + auto footstep1 = std::chrono::high_resolution_clock::now(); if (footstepManager) { footstepManager->update(deltaTime); cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer @@ -1448,8 +1772,11 @@ void Renderer::update(float deltaTime) { mountSoundManager->setFlying(flying); } } + auto footstep2 = std::chrono::high_resolution_clock::now(); + footstepTime += std::chrono::duration(footstep2 - footstep1).count(); // Ambient environmental sounds: fireplaces, water, birds, etc. + auto ambient1 = std::chrono::high_resolution_clock::now(); if (ambientSoundManager && camera && wmoRenderer && cameraController) { glm::vec3 camPos = camera->getPosition(); uint32_t wmoId = 0; @@ -1489,12 +1816,19 @@ void Renderer::update(float deltaTime) { ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); } + auto ambient2 = std::chrono::high_resolution_clock::now(); + ambientTime += std::chrono::duration(ambient2 - ambient1).count(); // Update M2 doodad animations (pass camera for frustum-culling bone computation) + static int m2ProfileCounter = 0; + static float m2Time = 0.0f; + auto m21 = std::chrono::high_resolution_clock::now(); if (m2Renderer && camera) { m2Renderer->update(deltaTime, camera->getPosition(), camera->getProjectionMatrix() * camera->getViewMatrix()); } + auto m22 = std::chrono::high_resolution_clock::now(); + m2Time += std::chrono::duration(m22 - m21).count(); // Update zone detection and music if (zoneManager && musicManager && terrainManager && camera) { @@ -1617,6 +1951,24 @@ void Renderer::update(float deltaTime) { auto updateEnd = std::chrono::steady_clock::now(); lastUpdateMs = std::chrono::duration(updateEnd - updateStart).count(); + + // Log renderer profiling every 60 frames + if (++rendProfileCounter >= 60) { + LOG_INFO("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f, + "ms light=", lightTime / 60.0f, "ms charAnim=", charAnimTime / 60.0f, + "ms terrain=", terrainTime / 60.0f, "ms sky=", skyTime / 60.0f, + "ms charRend=", charRendTime / 60.0f, "ms audio=", audioTime / 60.0f, + "ms footstep=", footstepTime / 60.0f, "ms ambient=", ambientTime / 60.0f, + "ms m2Anim=", m2Time / 60.0f, "ms"); + rendProfileCounter = 0; + camTime = lightTime = charAnimTime = 0.0f; + terrainTime = skyTime = charRendTime = 0.0f; + audioTime = footstepTime = ambientTime = 0.0f; + m2Time = 0.0f; + } + if (++m2ProfileCounter >= 60) { + m2ProfileCounter = 0; + } } // ============================================================ @@ -1729,7 +2081,7 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro glEnable(GL_CULL_FACE); } -void Renderer::renderWorld(game::World* world) { +void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { auto renderStart = std::chrono::steady_clock::now(); lastTerrainRenderMs = 0.0; lastWMORenderMs = 0.0; @@ -1759,50 +2111,72 @@ void Renderer::renderWorld(game::World* world) { bool underwater = false; bool canalUnderwater = false; - // Render skybox first (furthest back) - if (skybox && camera) { - skybox->render(*camera, timeOfDay); - } + // 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; - // 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; - } - - // Render stars after skybox (affected by cloud/fog density) - if (starField && camera) { - starField->render(*camera, timeOfDay, cloudDensity, fogDensity); - } - - // Render celestial bodies (sun/moon) after stars (sun uses lighting direction/color) - if (celestial && camera) { - celestial->render(*camera, timeOfDay, sunDir, sunColor); - } - - // Render clouds after celestial bodies - if (clouds && camera) { - clouds->render(*camera, timeOfDay); - } - - // Render lens flare (screen-space effect, render after celestial bodies) - if (lensFlare && camera && celestial) { - // Use lighting direction for sun position if available - glm::vec3 sunPosition; - if (sunDir) { - const float sunDistance = 800.0f; - sunPosition = -*sunDir * sunDistance; - } else { - sunPosition = celestial->getSunPosition(timeOfDay); + if (lightingManager) { + const auto& lighting = lightingManager->getLightingParams(); + skyParams.directionalDir = lighting.directionalDir; + skyParams.sunColor = lighting.diffuseColor; + skyParams.skyTopColor = lighting.skyTopColor; + skyParams.skyMiddleColor = lighting.skyMiddleColor; + skyParams.skyBand1Color = lighting.skyBand1Color; + skyParams.skyBand2Color = lighting.skyBand2Color; + skyParams.cloudDensity = lighting.cloudDensity; + skyParams.fogDensity = lighting.fogDensity; + skyParams.horizonGlow = lighting.horizonGlow; + } + + // TODO: Set skyboxModelId from LightSkybox.dbc (future) + skyParams.skyboxModelId = 0; + skyParams.skyboxHasStars = false; // Gradient skybox has no baked stars + + skySystem->render(*camera, skyParams); + } else { + // Fallback: render individual components (backwards compatibility) + if (skybox && camera) { + skybox->render(*camera, timeOfDay); + } + + // Get lighting parameters for celestial rendering + const glm::vec3* sunDir = nullptr; + const glm::vec3* sunColor = nullptr; + float cloudDensity = 0.0f; + float fogDensity = 0.0f; + if (lightingManager) { + const auto& lighting = lightingManager->getLightingParams(); + sunDir = &lighting.directionalDir; + sunColor = &lighting.diffuseColor; + cloudDensity = lighting.cloudDensity; + fogDensity = lighting.fogDensity; + } + + if (starField && camera) { + starField->render(*camera, timeOfDay, cloudDensity, fogDensity); + } + + if (celestial && camera) { + celestial->render(*camera, timeOfDay, sunDir, sunColor); + } + + if (clouds && camera) { + clouds->render(*camera, timeOfDay); + } + + if (lensFlare && camera && celestial) { + glm::vec3 sunPosition; + if (sunDir) { + const float sunDistance = 800.0f; + sunPosition = -*sunDir * sunDistance; + } else { + sunPosition = celestial->getSunPosition(timeOfDay); + } + lensFlare->render(*camera, sunPosition, timeOfDay); } - lensFlare->render(*camera, sunPosition, timeOfDay); } // Apply lighting and fog to all renderers diff --git a/src/rendering/sky_system.cpp b/src/rendering/sky_system.cpp index f830d364..920e7b61 100644 --- a/src/rendering/sky_system.cpp +++ b/src/rendering/sky_system.cpp @@ -146,9 +146,15 @@ void SkySystem::render(const Camera& camera, const SkyParams& params) { } glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const { - // Use lighting direction for sun position - const float sunDistance = 800.0f; - return -params.directionalDir * sunDistance; // Negative because light comes FROM sun + // TESTING: X-up test + glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); // X-up + glm::vec3 pos = dir * 800.0f; + + static int counter = 0; + if (counter++ % 100 == 0) { + LOG_INFO("Flare TEST X-UP dir=(", dir.x, ",", dir.y, ",", dir.z, ") pos=(", pos.x, ",", pos.y, ",", pos.z, ")"); + } + return pos; } diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index ed28217e..b49c3d0d 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -150,6 +150,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) { } // Always process ready tiles each frame (GPU uploads from background thread) + // Time budget prevents frame spikes from heavy tiles processReadyTiles(); timeSinceLastUpdate += deltaTime; @@ -641,7 +642,8 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { m2Renderer->initialize(assetManager); } - // Upload unique M2 models to GPU (stays in VRAM permanently until shutdown) + // Upload M2 models immediately (batching was causing hangs) + // The 5ms time budget in processReadyTiles() limits the spike std::unordered_set uploadedModelIds; for (auto& m2Ready : pending->m2Models) { if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) { @@ -649,7 +651,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { } } if (!uploadedModelIds.empty()) { - LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " unique M2 models to VRAM for tile [", x, ",", y, "]"); + LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " M2 models for tile [", x, ",", y, "]"); } // Create instances (deduplicate by uniqueId across tile boundaries) @@ -813,11 +815,13 @@ void TerrainManager::workerLoop() { } void TerrainManager::processReadyTiles() { - // Process up to 1 ready tile per frame to avoid main-thread stalls + // Process tiles with time budget to avoid frame spikes + // Budget: 5ms per frame (allows 3 tiles at ~1.5ms each or 1 heavy tile) + const float timeBudgetMs = 5.0f; + auto startTime = std::chrono::high_resolution_clock::now(); int processed = 0; - const int maxPerFrame = 1; - while (processed < maxPerFrame) { + while (true) { std::shared_ptr pending; { @@ -831,16 +835,48 @@ void TerrainManager::processReadyTiles() { if (pending) { TileCoord coord = pending->coord; + auto tileStart = std::chrono::high_resolution_clock::now(); + finalizeTile(pending); + + auto tileEnd = std::chrono::high_resolution_clock::now(); + float tileTimeMs = std::chrono::duration(tileEnd - tileStart).count(); + { std::lock_guard lock(queueMutex); pendingTiles.erase(coord); } processed++; + + // Check if we've exceeded time budget + float elapsedMs = std::chrono::duration(tileEnd - startTime).count(); + if (elapsedMs >= timeBudgetMs) { + if (processed > 1) { + LOG_DEBUG("Processed ", processed, " tiles in ", elapsedMs, "ms (budget: ", timeBudgetMs, "ms)"); + } + break; + } } } } +void TerrainManager::processM2UploadQueue() { + // Upload up to MAX_M2_UPLOADS_PER_FRAME models per frame + int uploaded = 0; + while (!m2UploadQueue_.empty() && uploaded < MAX_M2_UPLOADS_PER_FRAME) { + auto& upload = m2UploadQueue_.front(); + if (m2Renderer) { + m2Renderer->loadModel(upload.model, upload.modelId); + } + m2UploadQueue_.pop(); + uploaded++; + } + + if (uploaded > 0) { + LOG_DEBUG("Uploaded ", uploaded, " M2 models (", m2UploadQueue_.size(), " remaining in queue)"); + } +} + void TerrainManager::processAllReadyTiles() { while (true) { std::shared_ptr pending;