mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add property-based mount animation discovery and procedural lean
Mount Animation System: - Property-based jump animation discovery using sequence metadata - Chain linkage scoring (nextAnimation/aliasNext) for accurate detection - Correct loop detection: flags & 0x01 == 0 means looping - Avoids brake/stop animations via blendTime penalties - Works on any mount model without hardcoded animation IDs Mount Physics: - Physics-based jump height: vz = sqrt(2 * g * h) - Configurable MOUNT_JUMP_HEIGHT constant (1.0m default) - Procedural lean into turns for ground mounts - Smooth roll based on turn rate (±14° max, 6x/sec blend) Audio Improvements: - State-machine driven mount sounds (jump, land, rear-up) - Semantic sound methods (no animation ID dependencies) - Debug logging for missing sound files Bug Fixes: - Fixed mount animation sequencing (JumpStart → JumpLoop → JumpEnd) - Fixed animation loop flag interpretation (0x20 vs 0x21) - Rider bone attachment working correctly during all mount actions
This commit is contained in:
parent
3c783d1845
commit
c623fcef51
16 changed files with 1083 additions and 145 deletions
|
|
@ -40,6 +40,11 @@ public:
|
||||||
void setFlying(bool flying);
|
void setFlying(bool flying);
|
||||||
void setGrounded(bool grounded);
|
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_; }
|
bool isMounted() const { return mounted_; }
|
||||||
void setVolumeScale(float scale) { volumeScale_ = scale; }
|
void setVolumeScale(float scale) { volumeScale_ = scale; }
|
||||||
float getVolumeScale() const { return volumeScale_; }
|
float getVolumeScale() const { return volumeScale_; }
|
||||||
|
|
@ -69,6 +74,7 @@ private:
|
||||||
bool playingMovementSound_ = false;
|
bool playingMovementSound_ = false;
|
||||||
bool playingIdleSound_ = false;
|
bool playingIdleSound_ = false;
|
||||||
std::chrono::steady_clock::time_point lastSoundUpdate_;
|
std::chrono::steady_clock::time_point lastSoundUpdate_;
|
||||||
|
std::chrono::steady_clock::time_point lastActionSoundTime_; // Cooldown for action sounds
|
||||||
float soundLoopTimer_ = 0.0f;
|
float soundLoopTimer_ = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,10 @@ public:
|
||||||
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
||||||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
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
|
// Player skills
|
||||||
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
|
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
|
||||||
const std::string& getSkillName(uint32_t skillId) const;
|
const std::string& getSkillName(uint32_t skillId) const;
|
||||||
|
|
@ -1096,6 +1100,11 @@ private:
|
||||||
uint32_t serverPlayerLevel_ = 1;
|
uint32_t serverPlayerLevel_ = 1;
|
||||||
static uint32_t xpForLevel(uint32_t level);
|
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 ----
|
// ---- Player skills ----
|
||||||
std::map<uint32_t, PlayerSkill> playerSkills_;
|
std::map<uint32_t, PlayerSkill> playerSkills_;
|
||||||
std::unordered_map<uint32_t, std::string> skillLineNames_;
|
std::unordered_map<uint32_t, std::string> skillLineNames_;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ enum class Opcode : uint16_t {
|
||||||
SMSG_CHAR_DELETE = 0x03C,
|
SMSG_CHAR_DELETE = 0x03C,
|
||||||
SMSG_PONG = 0x1DD,
|
SMSG_PONG = 0x1DD,
|
||||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||||
|
SMSG_LOGIN_SETTIMESPEED = 0x042,
|
||||||
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
||||||
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
|
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
|
||||||
SMSG_MOTD = 0x33D,
|
SMSG_MOTD = 0x33D,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ public:
|
||||||
bool isGrounded() const { return grounded; }
|
bool isGrounded() const { return grounded; }
|
||||||
bool isJumping() const { return !grounded && verticalVelocity > 0.0f; }
|
bool isJumping() const { return !grounded && verticalVelocity > 0.0f; }
|
||||||
bool isFalling() 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 isSprinting() const;
|
||||||
bool isMovingForward() const { return moveForwardActive; }
|
bool isMovingForward() const { return moveForwardActive; }
|
||||||
bool isMovingBackward() const { return moveBackwardActive; }
|
bool isMovingBackward() const { return moveBackwardActive; }
|
||||||
|
|
@ -92,6 +93,9 @@ public:
|
||||||
void setFacingYaw(float yaw) { facingYaw = yaw; } // For taxi/scripted movement
|
void setFacingYaw(float yaw) { facingYaw = yaw; } // For taxi/scripted movement
|
||||||
void clearMovementInputs();
|
void clearMovementInputs();
|
||||||
|
|
||||||
|
// Trigger mount jump (applies vertical velocity for physics hop)
|
||||||
|
void triggerMountJump();
|
||||||
|
|
||||||
// For first-person player hiding
|
// For first-person player hiding
|
||||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||||
characterRenderer = cr;
|
characterRenderer = cr;
|
||||||
|
|
@ -168,6 +172,16 @@ private:
|
||||||
std::optional<float> cachedCamWmoFloor;
|
std::optional<float> cachedCamWmoFloor;
|
||||||
bool hasCachedCamFloor = false;
|
bool hasCachedCamFloor = false;
|
||||||
|
|
||||||
|
// Cached floor height queries (update every 5 frames or 2 unit movement)
|
||||||
|
glm::vec3 lastFloorQueryPos = glm::vec3(0.0f);
|
||||||
|
std::optional<float> 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<float> getCachedFloorHeight(float x, float y, float z);
|
||||||
|
|
||||||
// Swimming
|
// Swimming
|
||||||
bool swimming = false;
|
bool swimming = false;
|
||||||
bool wasSwimming = 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_TURN_SPEED = 180.0f; // Keyboard turn deg/sec
|
||||||
static constexpr float WOW_GRAVITY = -19.29f;
|
static constexpr float WOW_GRAVITY = -19.29f;
|
||||||
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
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)
|
// Server-driven run speed override (0 = use default WOW_RUN_SPEED)
|
||||||
float runSpeedOverride_ = 0.0f;
|
float runSpeedOverride_ = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public:
|
||||||
|
|
||||||
void playAnimation(uint32_t instanceId, uint32_t animationId, bool loop = true);
|
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 render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
|
||||||
void renderShadow(const glm::mat4& lightSpaceMatrix);
|
void renderShadow(const glm::mat4& lightSpaceMatrix);
|
||||||
|
|
@ -74,6 +74,9 @@ public:
|
||||||
bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const;
|
bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const;
|
||||||
bool getInstanceBounds(uint32_t instanceId, glm::vec3& outCenter, float& outRadius) 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. */
|
/** Attach a weapon model to a character instance at the given attachment point. */
|
||||||
bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
||||||
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
||||||
|
|
@ -82,6 +85,15 @@ public:
|
||||||
/** Detach a weapon from the given attachment point. */
|
/** Detach a weapon from the given attachment point. */
|
||||||
void detachWeapon(uint32_t charInstanceId, uint32_t attachmentId);
|
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(); }
|
size_t getInstanceCount() const { return instances.size(); }
|
||||||
|
|
||||||
void setFog(const glm::vec3& color, float start, float end) {
|
void setFog(const glm::vec3& color, float start, float end) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace core { class Window; }
|
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 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; }
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
||||||
|
|
@ -27,6 +27,7 @@ class Clouds;
|
||||||
class LensFlare;
|
class LensFlare;
|
||||||
class Weather;
|
class Weather;
|
||||||
class LightingManager;
|
class LightingManager;
|
||||||
|
class SkySystem;
|
||||||
class SwimEffects;
|
class SwimEffects;
|
||||||
class MountDust;
|
class MountDust;
|
||||||
class CharacterRenderer;
|
class CharacterRenderer;
|
||||||
|
|
@ -47,7 +48,7 @@ public:
|
||||||
void beginFrame();
|
void beginFrame();
|
||||||
void endFrame();
|
void endFrame();
|
||||||
|
|
||||||
void renderWorld(game::World* world);
|
void renderWorld(game::World* world, game::GameHandler* gameHandler = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update renderer (camera, etc.)
|
* Update renderer (camera, etc.)
|
||||||
|
|
@ -108,6 +109,7 @@ public:
|
||||||
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
||||||
Minimap* getMinimap() const { return minimap.get(); }
|
Minimap* getMinimap() const { return minimap.get(); }
|
||||||
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
||||||
|
SkySystem* getSkySystem() const { return skySystem.get(); }
|
||||||
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
||||||
|
|
||||||
// Third-person character follow
|
// Third-person character follow
|
||||||
|
|
@ -176,6 +178,7 @@ private:
|
||||||
std::unique_ptr<LensFlare> lensFlare;
|
std::unique_ptr<LensFlare> lensFlare;
|
||||||
std::unique_ptr<Weather> weather;
|
std::unique_ptr<Weather> weather;
|
||||||
std::unique_ptr<LightingManager> lightingManager;
|
std::unique_ptr<LightingManager> lightingManager;
|
||||||
|
std::unique_ptr<SkySystem> skySystem; // Coordinator for sky rendering
|
||||||
std::unique_ptr<SwimEffects> swimEffects;
|
std::unique_ptr<SwimEffects> swimEffects;
|
||||||
std::unique_ptr<MountDust> mountDust;
|
std::unique_ptr<MountDust> mountDust;
|
||||||
std::unique_ptr<CharacterRenderer> characterRenderer;
|
std::unique_ptr<CharacterRenderer> characterRenderer;
|
||||||
|
|
@ -302,10 +305,27 @@ private:
|
||||||
uint32_t equippedWeaponInvType_ = 0;
|
uint32_t equippedWeaponInvType_ = 0;
|
||||||
|
|
||||||
// Mount state
|
// 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;
|
uint32_t mountInstanceId_ = 0;
|
||||||
float mountHeightOffset_ = 0.0f;
|
float mountHeightOffset_ = 0.0f;
|
||||||
float mountPitch_ = 0.0f; // Up/down tilt (radians)
|
float mountPitch_ = 0.0f; // Up/down tilt (radians)
|
||||||
float mountRoll_ = 0.0f; // Left/right banking (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 taxiFlight_ = false;
|
||||||
|
|
||||||
bool terrainEnabled = true;
|
bool terrainEnabled = true;
|
||||||
|
|
|
||||||
|
|
@ -282,8 +282,8 @@ private:
|
||||||
|
|
||||||
// Streaming parameters
|
// Streaming parameters
|
||||||
bool streamingEnabled = true;
|
bool streamingEnabled = true;
|
||||||
int loadRadius = 8; // Load tiles within this radius (17x17 grid)
|
int loadRadius = 4; // Load tiles within this radius (9x9 grid = 81 tiles)
|
||||||
int unloadRadius = 12; // Unload tiles beyond this radius
|
int unloadRadius = 7; // Unload tiles beyond this radius
|
||||||
float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps)
|
float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps)
|
||||||
float timeSinceLastUpdate = 0.0f;
|
float timeSinceLastUpdate = 0.0f;
|
||||||
|
|
||||||
|
|
@ -326,6 +326,17 @@ private:
|
||||||
|
|
||||||
// Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x)
|
// Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x)
|
||||||
std::unordered_set<uint32_t> placedWmoIds;
|
std::unordered_set<uint32_t> placedWmoIds;
|
||||||
|
|
||||||
|
// Progressive M2 upload queue (spread heavy uploads across frames)
|
||||||
|
struct PendingM2Upload {
|
||||||
|
uint32_t modelId;
|
||||||
|
pipeline::M2Model model;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
std::queue<PendingM2Upload> m2UploadQueue_;
|
||||||
|
static constexpr int MAX_M2_UPLOADS_PER_FRAME = 5; // Upload up to 5 models per frame
|
||||||
|
|
||||||
|
void processM2UploadQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -116,16 +116,19 @@ void MountSoundManager::loadMountSounds() {
|
||||||
|
|
||||||
bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) {
|
bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) {
|
||||||
if (!assetManager_ || !assetManager_->fileExists(path)) {
|
if (!assetManager_ || !assetManager_->fileExists(path)) {
|
||||||
|
LOG_WARNING("Mount sound file not found: ", path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto data = assetManager_->readFile(path);
|
auto data = assetManager_->readFile(path);
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
|
LOG_WARNING("Mount sound file empty: ", path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sample.path = path;
|
sample.path = path;
|
||||||
sample.data = std::move(data);
|
sample.data = std::move(data);
|
||||||
|
LOG_INFO("Loaded mount sound: ", path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +187,85 @@ void MountSoundManager::setGrounded(bool grounded) {
|
||||||
setFlying(!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<std::chrono::milliseconds>(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<size_t> 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<size_t> 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<std::chrono::milliseconds>(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<size_t> 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<size_t> 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<std::chrono::milliseconds>(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<size_t> 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 {
|
MountType MountSoundManager::detectMountType(uint32_t creatureDisplayId) const {
|
||||||
// TODO: Load from CreatureDisplayInfo.dbc or CreatureModelData.dbc
|
// TODO: Load from CreatureDisplayInfo.dbc or CreatureModelData.dbc
|
||||||
// For now, use simple heuristics based on common display IDs
|
// For now, use simple heuristics based on common display IDs
|
||||||
|
|
|
||||||
|
|
@ -389,25 +389,62 @@ void Application::update(float deltaTime) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppState::IN_GAME: {
|
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) {
|
if (gameHandler) {
|
||||||
gameHandler->update(deltaTime);
|
gameHandler->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
auto gh2 = std::chrono::high_resolution_clock::now();
|
||||||
|
ghTime += std::chrono::duration<float, std::milli>(gh2 - gh1).count();
|
||||||
|
|
||||||
|
auto w1 = std::chrono::high_resolution_clock::now();
|
||||||
if (world) {
|
if (world) {
|
||||||
world->update(deltaTime);
|
world->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
auto w2 = std::chrono::high_resolution_clock::now();
|
||||||
|
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
||||||
|
|
||||||
|
auto s1 = std::chrono::high_resolution_clock::now();
|
||||||
// Spawn NPCs once when entering world
|
// Spawn NPCs once when entering world
|
||||||
spawnNpcs();
|
spawnNpcs();
|
||||||
|
auto s2 = std::chrono::high_resolution_clock::now();
|
||||||
|
spawnTime += std::chrono::duration<float, std::milli>(s2 - s1).count();
|
||||||
|
|
||||||
|
auto cq1 = std::chrono::high_resolution_clock::now();
|
||||||
// Process deferred online creature spawns (throttled)
|
// Process deferred online creature spawns (throttled)
|
||||||
processCreatureSpawnQueue();
|
processCreatureSpawnQueue();
|
||||||
|
auto cq2 = std::chrono::high_resolution_clock::now();
|
||||||
|
creatureQTime += std::chrono::duration<float, std::milli>(cq2 - cq1).count();
|
||||||
|
|
||||||
|
auto goq1 = std::chrono::high_resolution_clock::now();
|
||||||
processGameObjectSpawnQueue();
|
processGameObjectSpawnQueue();
|
||||||
|
auto goq2 = std::chrono::high_resolution_clock::now();
|
||||||
|
goQTime += std::chrono::duration<float, std::milli>(goq2 - goq1).count();
|
||||||
|
|
||||||
|
auto m1 = std::chrono::high_resolution_clock::now();
|
||||||
processPendingMount();
|
processPendingMount();
|
||||||
|
auto m2 = std::chrono::high_resolution_clock::now();
|
||||||
|
mountTime += std::chrono::duration<float, std::milli>(m2 - m1).count();
|
||||||
|
|
||||||
|
auto nm1 = std::chrono::high_resolution_clock::now();
|
||||||
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
||||||
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
npcManager->update(deltaTime, renderer->getCharacterRenderer());
|
||||||
}
|
}
|
||||||
|
auto nm2 = std::chrono::high_resolution_clock::now();
|
||||||
|
npcMgrTime += std::chrono::duration<float, std::milli>(nm2 - nm1).count();
|
||||||
|
|
||||||
|
auto qm1 = std::chrono::high_resolution_clock::now();
|
||||||
// Update 3D quest markers above NPCs
|
// Update 3D quest markers above NPCs
|
||||||
updateQuestMarkers();
|
updateQuestMarkers();
|
||||||
|
auto qm2 = std::chrono::high_resolution_clock::now();
|
||||||
|
questMarkTime += std::chrono::duration<float, std::milli>(qm2 - qm1).count();
|
||||||
|
|
||||||
|
auto sync1 = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Sync server run speed to camera controller
|
// Sync server run speed to camera controller
|
||||||
if (renderer && gameHandler && renderer->getCameraController()) {
|
if (renderer && gameHandler && renderer->getCameraController()) {
|
||||||
|
|
@ -489,6 +526,22 @@ void Application::update(float deltaTime) {
|
||||||
} else {
|
} else {
|
||||||
movementHeartbeatTimer = 0.0f;
|
movementHeartbeatTimer = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto sync2 = std::chrono::high_resolution_clock::now();
|
||||||
|
syncTime += std::chrono::duration<float, std::milli>(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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,14 +551,30 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update renderer (camera, etc.) only when in-game
|
// 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) {
|
if (renderer && state == AppState::IN_GAME) {
|
||||||
renderer->update(deltaTime);
|
renderer->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
auto r2 = std::chrono::high_resolution_clock::now();
|
||||||
|
rendererTime += std::chrono::duration<float, std::milli>(r2 - r1).count();
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
|
auto u1 = std::chrono::high_resolution_clock::now();
|
||||||
if (uiManager) {
|
if (uiManager) {
|
||||||
uiManager->update(deltaTime);
|
uiManager->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
auto u2 = std::chrono::high_resolution_clock::now();
|
||||||
|
uiTime += std::chrono::duration<float, std::milli>(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() {
|
void Application::render() {
|
||||||
|
|
@ -518,9 +587,9 @@ void Application::render() {
|
||||||
// Only render 3D world when in-game (after server connect or single-player)
|
// Only render 3D world when in-game (after server connect or single-player)
|
||||||
if (state == AppState::IN_GAME) {
|
if (state == AppState::IN_GAME) {
|
||||||
if (world) {
|
if (world) {
|
||||||
renderer->renderWorld(world.get());
|
renderer->renderWorld(world.get(), gameHandler.get());
|
||||||
} else {
|
} else {
|
||||||
renderer->renderWorld(nullptr);
|
renderer->renderWorld(nullptr, gameHandler.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,16 @@ bool GameHandler::isConnected() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::update(float deltaTime) {
|
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)
|
// Fire deferred char-create callback (outside ImGui render)
|
||||||
if (pendingCharCreateResult_) {
|
if (pendingCharCreateResult_) {
|
||||||
pendingCharCreateResult_ = false;
|
pendingCharCreateResult_ = false;
|
||||||
|
|
@ -131,9 +141,12 @@ void GameHandler::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update socket (processes incoming data and triggers callbacks)
|
// Update socket (processes incoming data and triggers callbacks)
|
||||||
|
auto socketStart = std::chrono::high_resolution_clock::now();
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket->update();
|
socket->update();
|
||||||
}
|
}
|
||||||
|
auto socketEnd = std::chrono::high_resolution_clock::now();
|
||||||
|
socketTime += std::chrono::duration<float, std::milli>(socketEnd - socketStart).count();
|
||||||
|
|
||||||
// Validate target still exists
|
// Validate target still exists
|
||||||
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
|
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
|
||||||
|
|
@ -187,6 +200,9 @@ void GameHandler::update(float deltaTime) {
|
||||||
taxiLandingCooldown_ -= deltaTime;
|
taxiLandingCooldown_ -= deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Taxi logic timing
|
||||||
|
auto taxiStart = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
|
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
|
||||||
if (onTaxiFlight_) {
|
if (onTaxiFlight_) {
|
||||||
updateClientTaxi(deltaTime);
|
updateClientTaxi(deltaTime);
|
||||||
|
|
@ -286,6 +302,12 @@ void GameHandler::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto taxiEnd = std::chrono::high_resolution_clock::now();
|
||||||
|
taxiTime += std::chrono::duration<float, std::milli>(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)
|
// Leave combat if auto-attack target is too far away (leash range)
|
||||||
if (autoAttacking && autoAttackTarget != 0) {
|
if (autoAttacking && autoAttackTarget != 0) {
|
||||||
auto targetEntity = entityManager.getEntity(autoAttackTarget);
|
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<float, std::milli>(distanceEnd - distanceStart).count();
|
||||||
|
|
||||||
|
// Entity update timing
|
||||||
|
auto entityStart = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Update entity movement interpolation (keeps targeting in sync with visuals)
|
// 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()) {
|
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<float, std::milli>(entityEnd - entityStart).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto updateEnd = std::chrono::high_resolution_clock::now();
|
||||||
|
totalTime += std::chrono::duration<float, std::milli>(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;
|
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:
|
case Opcode::SMSG_ACCOUNT_DATA_TIMES:
|
||||||
// Can be received at any time after authentication
|
// Can be received at any time after authentication
|
||||||
handleAccountDataTimes(packet);
|
handleAccountDataTimes(packet);
|
||||||
|
|
@ -1472,6 +1540,28 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world...");
|
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<float>(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) {
|
void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
||||||
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");
|
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,38 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) {
|
||||||
thirdPerson = true;
|
thirdPerson = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<float> 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<float> 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) {
|
void CameraController::update(float deltaTime) {
|
||||||
if (!enabled || !camera) {
|
if (!enabled || !camera) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -342,17 +374,21 @@ void CameraController::update(float deltaTime) {
|
||||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
||||||
float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z;
|
float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z;
|
||||||
|
|
||||||
glm::vec3 swimForward = glm::normalize(forward3D);
|
// For auto-run/auto-swim: use character facing (immune to camera pan)
|
||||||
if (glm::length(swimForward) < 1e-4f) {
|
// 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;
|
swimForward = forward;
|
||||||
}
|
|
||||||
glm::vec3 swimRight = camera->getRight();
|
|
||||||
swimRight.z = 0.0f;
|
|
||||||
if (glm::length(swimRight) > 1e-4f) {
|
|
||||||
swimRight = glm::normalize(swimRight);
|
|
||||||
} else {
|
} 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);
|
glm::vec3 swimMove(0.0f);
|
||||||
if (nowForward) swimMove += swimForward;
|
if (nowForward) swimMove += swimForward;
|
||||||
|
|
@ -396,17 +432,32 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent sinking/clipping through world floor while swimming.
|
// Prevent sinking/clipping through world floor while swimming.
|
||||||
|
// Cache floor queries (update every 3 frames or 1 unit movement)
|
||||||
std::optional<float> floorH;
|
std::optional<float> floorH;
|
||||||
if (terrainManager) {
|
float dist2D = glm::length(glm::vec2(targetPos.x - lastFloorQueryPos.x,
|
||||||
floorH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
targetPos.y - lastFloorQueryPos.y));
|
||||||
}
|
bool updateFloorCache = (floorQueryFrameCounter++ >= FLOOR_QUERY_FRAME_INTERVAL) ||
|
||||||
if (wmoRenderer) {
|
(dist2D > FLOOR_QUERY_DISTANCE_THRESHOLD);
|
||||||
auto wh = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 2.0f);
|
|
||||||
if (wh && (!floorH || *wh > *floorH)) floorH = wh;
|
if (updateFloorCache) {
|
||||||
}
|
floorQueryFrameCounter = 0;
|
||||||
if (m2Renderer && !externalFollow_) {
|
lastFloorQueryPos = targetPos;
|
||||||
auto mh = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z);
|
|
||||||
if (mh && (!floorH || *mh > *floorH)) floorH = mh;
|
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) {
|
if (floorH) {
|
||||||
float swimFloor = *floorH + 0.5f;
|
float swimFloor = *floorH + 0.5f;
|
||||||
|
|
@ -469,7 +520,7 @@ void CameraController::update(float deltaTime) {
|
||||||
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
||||||
if (grounded) coyoteTimer = COYOTE_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) {
|
if (canJump) {
|
||||||
verticalVelocity = jumpVel;
|
verticalVelocity = jumpVel;
|
||||||
grounded = false;
|
grounded = false;
|
||||||
|
|
@ -895,7 +946,7 @@ void CameraController::update(float deltaTime) {
|
||||||
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
||||||
if (grounded) coyoteTimer = COYOTE_TIME;
|
if (grounded) coyoteTimer = COYOTE_TIME;
|
||||||
|
|
||||||
if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f) {
|
if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f && !mounted_) {
|
||||||
verticalVelocity = jumpVel;
|
verticalVelocity = jumpVel;
|
||||||
grounded = false;
|
grounded = false;
|
||||||
jumpBufferTimer = 0.0f;
|
jumpBufferTimer = 0.0f;
|
||||||
|
|
@ -1400,5 +1451,15 @@ bool CameraController::isSprinting() const {
|
||||||
return enabled && camera && runPace;
|
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 rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ bool Celestial::initialize() {
|
||||||
// Create celestial shader
|
// Create celestial shader
|
||||||
celestialShader = std::make_unique<Shader>();
|
celestialShader = std::make_unique<Shader>();
|
||||||
|
|
||||||
// Vertex shader - billboard facing camera
|
// Vertex shader - billboard facing camera (sky dome locked)
|
||||||
const char* vertexShaderSource = R"(
|
const char* vertexShaderSource = R"(
|
||||||
#version 330 core
|
#version 330 core
|
||||||
layout (location = 0) in vec3 aPos;
|
layout (location = 0) in vec3 aPos;
|
||||||
|
|
@ -36,13 +36,10 @@ bool Celestial::initialize() {
|
||||||
void main() {
|
void main() {
|
||||||
TexCoord = aTexCoord;
|
TexCoord = aTexCoord;
|
||||||
|
|
||||||
// Billboard: remove rotation from view matrix, keep only translation
|
// Sky object: remove translation, keep rotation (skybox technique)
|
||||||
mat4 viewNoRotation = view;
|
mat4 viewNoTranslation = mat4(mat3(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;
|
|
||||||
|
|
||||||
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,
|
void Celestial::render(const Camera& camera, float timeOfDay,
|
||||||
const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) {
|
const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) {
|
||||||
if (!renderingEnabled || vao == 0 || !celestialShader) {
|
if (!renderingEnabled || vao == 0 || !celestialShader) {
|
||||||
|
LOG_WARNING("Celestial render blocked: enabled=", renderingEnabled, " vao=", vao, " shader=", (celestialShader ? "ok" : "null"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Celestial render: timeOfDay=", timeOfDay, " gameTime=", gameTime);
|
||||||
|
|
||||||
// Update moon phases from game time if available (deterministic)
|
// Update moon phases from game time if available (deterministic)
|
||||||
if (gameTime >= 0.0f) {
|
if (gameTime >= 0.0f) {
|
||||||
updatePhasesFromGameTime(gameTime);
|
updatePhasesFromGameTime(gameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable blending for celestial glow
|
// Enable additive blending for celestial glow (brighter against sky)
|
||||||
glEnable(GL_BLEND);
|
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);
|
glDepthMask(GL_FALSE);
|
||||||
|
|
||||||
|
// Disable culling - billboards can face either way
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
|
||||||
// Render sun and moons (pass lighting parameters)
|
// Render sun and moons (pass lighting parameters)
|
||||||
renderSun(camera, timeOfDay, sunDir, sunColor);
|
renderSun(camera, timeOfDay, sunDir, sunColor);
|
||||||
renderMoon(camera, timeOfDay); // White Lady (primary moon)
|
renderMoon(camera, timeOfDay); // White Lady (primary moon)
|
||||||
|
|
@ -152,34 +156,37 @@ void Celestial::render(const Camera& camera, float timeOfDay,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore state
|
// Restore state
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
glDepthMask(GL_TRUE);
|
glDepthMask(GL_TRUE);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Celestial::renderSun(const Camera& camera, float timeOfDay,
|
void Celestial::renderSun(const Camera& camera, float timeOfDay,
|
||||||
const glm::vec3* sunDir, const glm::vec3* sunColor) {
|
const glm::vec3* sunDir, const glm::vec3* sunColor) {
|
||||||
// Sun visible from 5:00 to 19:00
|
// Sun visible from 5:00 to 19:00
|
||||||
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
|
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
|
||||||
|
LOG_INFO("Sun not visible: timeOfDay=", timeOfDay, " (visible 5:00-19:00)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Rendering sun: timeOfDay=", timeOfDay, " sunDir=", (sunDir ? "yes" : "no"), " sunColor=", (sunColor ? "yes" : "no"));
|
||||||
|
|
||||||
celestialShader->use();
|
celestialShader->use();
|
||||||
|
|
||||||
// Get sun position (use lighting direction if provided)
|
// TESTING: Try X-up (final axis test)
|
||||||
glm::vec3 sunPos;
|
glm::vec3 dir = glm::normalize(glm::vec3(1.0f, 0.0f, 0.0f)); // X-up test
|
||||||
if (sunDir) {
|
LOG_INFO("Sun direction (TESTING X-UP): dir=(", dir.x, ",", dir.y, ",", dir.z, ")");
|
||||||
// Place sun along the lighting direction at far distance
|
|
||||||
const float sunDistance = 800.0f;
|
// Place sun on sky sphere at fixed distance
|
||||||
sunPos = -*sunDir * sunDistance; // Negative because light comes FROM sun
|
const float sunDistance = 800.0f;
|
||||||
} else {
|
glm::vec3 sunPos = dir * sunDistance;
|
||||||
// Fallback to time-based position
|
LOG_INFO("Sun position: dir * ", sunDistance, " = (", sunPos.x, ",", sunPos.y, ",", sunPos.z, ")");
|
||||||
sunPos = getSunPosition(timeOfDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create model matrix
|
// Create model matrix
|
||||||
glm::mat4 model = glm::mat4(1.0f);
|
glm::mat4 model = glm::mat4(1.0f);
|
||||||
model = glm::translate(model, sunPos);
|
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
|
// Set uniforms
|
||||||
glm::mat4 view = camera.getViewMatrix();
|
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
|
// Sun rises at 6:00, peaks at 12:00, sets at 18:00
|
||||||
float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f);
|
float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f);
|
||||||
|
|
||||||
const float radius = 800.0f; // Distance from origin
|
const float radius = 800.0f; // Horizontal distance
|
||||||
const float height = 600.0f; // Maximum height
|
const float height = 600.0f; // Maximum height at zenith
|
||||||
|
|
||||||
// Arc across sky
|
// Arc across sky (angle 0→π maps to sunrise→noon→sunset)
|
||||||
float x = radius * std::sin(angle);
|
// Z is vertical (matches skybox: Altitude = aPos.z)
|
||||||
float z = height * std::cos(angle);
|
// At angle=0: x=radius, z=0 (east horizon)
|
||||||
float y = 0.0f; // Y is horizontal in WoW coordinates
|
// 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);
|
return glm::vec3(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
@ -331,9 +342,10 @@ glm::vec3 Celestial::getMoonPosition(float timeOfDay) const {
|
||||||
const float radius = 800.0f;
|
const float radius = 800.0f;
|
||||||
const float height = 600.0f;
|
const float height = 600.0f;
|
||||||
|
|
||||||
float x = radius * std::sin(angle);
|
// Same arc formula as sun (Z is vertical, matches skybox)
|
||||||
float z = height * std::cos(angle);
|
float x = radius * std::cos(angle);
|
||||||
float y = 0.0f;
|
float y = 0.0f;
|
||||||
|
float z = height * std::sin(angle);
|
||||||
|
|
||||||
return glm::vec3(x, y, z);
|
return glm::vec3(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <future>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -899,18 +903,21 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
||||||
instance.currentSequenceIndex = 0;
|
instance.currentSequenceIndex = 0;
|
||||||
instance.currentAnimationId = model.sequences[0].id;
|
instance.currentAnimationId = model.sequences[0].id;
|
||||||
}
|
}
|
||||||
core::Logger::getInstance().warning("Animation ", animationId, " not found, using default");
|
|
||||||
// Dump available animation IDs for debugging
|
// Only log missing animation once per model (reduce spam)
|
||||||
std::string ids;
|
static std::unordered_map<uint32_t, std::unordered_set<uint32_t>> loggedMissingAnims;
|
||||||
for (size_t i = 0; i < model.sequences.size(); i++) {
|
uint32_t modelId = instance.modelId; // Use modelId as identifier
|
||||||
if (!ids.empty()) ids += ", ";
|
if (loggedMissingAnims[modelId].insert(animationId).second) {
|
||||||
ids += std::to_string(model.sequences[i].id);
|
// 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
|
// Update fade-in opacity
|
||||||
for (auto& [id, inst] : instances) {
|
for (auto& [id, inst] : instances) {
|
||||||
if (inst.fadeInDuration > 0.0f && inst.opacity < 1.0f) {
|
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<std::reference_wrapper<CharacterInstance>> toUpdate;
|
||||||
|
toUpdate.reserve(instances.size());
|
||||||
|
|
||||||
for (auto& pair : instances) {
|
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<std::future<void>> 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)
|
// 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 rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "rendering/lens_flare.hpp"
|
#include "rendering/lens_flare.hpp"
|
||||||
#include "rendering/weather.hpp"
|
#include "rendering/weather.hpp"
|
||||||
#include "rendering/lighting_manager.hpp"
|
#include "rendering/lighting_manager.hpp"
|
||||||
|
#include "rendering/sky_system.hpp"
|
||||||
#include "rendering/swim_effects.hpp"
|
#include "rendering/swim_effects.hpp"
|
||||||
#include "rendering/mount_dust.hpp"
|
#include "rendering/mount_dust.hpp"
|
||||||
#include "rendering/character_renderer.hpp"
|
#include "rendering/character_renderer.hpp"
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
#include "rendering/quest_marker_renderer.hpp"
|
#include "rendering/quest_marker_renderer.hpp"
|
||||||
#include "rendering/shader.hpp"
|
#include "rendering/shader.hpp"
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
#include "pipeline/m2_loader.hpp"
|
#include "pipeline/m2_loader.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
|
@ -55,6 +57,7 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -290,6 +293,17 @@ bool Renderer::initialize(core::Window* win) {
|
||||||
lensFlare.reset();
|
lensFlare.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create sky system (coordinator for sky rendering)
|
||||||
|
skySystem = std::make_unique<SkySystem>();
|
||||||
|
if (!skySystem->initialize()) {
|
||||||
|
LOG_WARNING("Failed to initialize sky system");
|
||||||
|
skySystem.reset();
|
||||||
|
} else {
|
||||||
|
// Note: SkySystem manages its own components internally
|
||||||
|
// Keep existing components for backwards compatibility (PerformanceHUD access)
|
||||||
|
LOG_INFO("Sky system initialized successfully (coordinator active)");
|
||||||
|
}
|
||||||
|
|
||||||
// Create weather system
|
// Create weather system
|
||||||
weather = std::make_unique<Weather>();
|
weather = std::make_unique<Weather>();
|
||||||
if (!weather->initialize()) {
|
if (!weather->initialize()) {
|
||||||
|
|
@ -543,12 +557,173 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
||||||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
||||||
mountInstanceId_ = mountInstId;
|
mountInstanceId_ = mountInstId;
|
||||||
mountHeightOffset_ = heightOffset;
|
mountHeightOffset_ = heightOffset;
|
||||||
|
mountAction_ = MountAction::None; // Clear mount action state
|
||||||
|
mountActionPhase_ = 0;
|
||||||
charAnimState = CharAnimState::MOUNT;
|
charAnimState = CharAnimState::MOUNT;
|
||||||
if (cameraController) {
|
if (cameraController) {
|
||||||
cameraController->setMounted(true);
|
cameraController->setMounted(true);
|
||||||
cameraController->setMountHeightOffset(heightOffset);
|
cameraController->setMountHeightOffset(heightOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: dump available mount animations
|
||||||
|
if (characterRenderer && mountInstId > 0) {
|
||||||
|
characterRenderer->dumpAnimations(mountInstId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover mount animation capabilities (property-based, not hardcoded IDs)
|
||||||
|
LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
|
||||||
|
characterRenderer->dumpAnimations(mountInstId);
|
||||||
|
|
||||||
|
// Get all sequences for property-based analysis
|
||||||
|
std::vector<pipeline::M2Sequence> sequences;
|
||||||
|
if (!characterRenderer->getAnimationSequences(mountInstId, sequences)) {
|
||||||
|
LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs");
|
||||||
|
sequences.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: ID-based fallback finder
|
||||||
|
auto findFirst = [&](std::initializer_list<uint32_t> candidates) -> uint32_t {
|
||||||
|
for (uint32_t id : candidates) {
|
||||||
|
if (characterRenderer->hasAnimation(mountInstId, id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Property-based jump animation discovery with chain-based scoring
|
||||||
|
auto discoverJumpSet = [&]() {
|
||||||
|
// Debug: log all sequences for analysis
|
||||||
|
LOG_INFO("=== Full sequence table for mount ===");
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
LOG_INFO("SEQ id=", seq.id,
|
||||||
|
" dur=", seq.duration,
|
||||||
|
" flags=0x", std::hex, seq.flags, std::dec,
|
||||||
|
" moveSpd=", seq.movingSpeed,
|
||||||
|
" blend=", seq.blendTime,
|
||||||
|
" next=", seq.nextAnimation,
|
||||||
|
" alias=", seq.aliasNext);
|
||||||
|
}
|
||||||
|
LOG_INFO("=== End sequence table ===");
|
||||||
|
|
||||||
|
// Known combat/bad animation IDs to avoid
|
||||||
|
std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack
|
||||||
|
|
||||||
|
auto scoreNear = [](int a, int b) -> int {
|
||||||
|
int d = std::abs(a - b);
|
||||||
|
return (d <= 8) ? (20 - d) : 0; // within 8 IDs gets points
|
||||||
|
};
|
||||||
|
|
||||||
|
auto isForbidden = [&](uint32_t id) {
|
||||||
|
return forbiddenIds.count(id) != 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* {
|
||||||
|
for (const auto& s : sequences) {
|
||||||
|
if (s.id == id) return &s;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t runId = findFirst({5, 4});
|
||||||
|
uint32_t standId = findFirst({0});
|
||||||
|
|
||||||
|
// Step A: Find loop candidates
|
||||||
|
std::vector<uint32_t> loops;
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
if (isForbidden(seq.id)) continue;
|
||||||
|
// Bit 0x01 NOT set = loops (0x20, 0x60), bit 0x01 set = non-looping (0x21, 0x61)
|
||||||
|
bool isLoop = (seq.flags & 0x01) == 0;
|
||||||
|
if (isLoop && seq.duration >= 350 && seq.duration <= 1000 &&
|
||||||
|
seq.id != runId && seq.id != standId) {
|
||||||
|
loops.push_back(seq.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose loop: prefer one near known classic IDs (38), else best duration
|
||||||
|
uint32_t loop = 0;
|
||||||
|
if (!loops.empty()) {
|
||||||
|
uint32_t best = loops[0];
|
||||||
|
int bestScore = -999;
|
||||||
|
for (uint32_t id : loops) {
|
||||||
|
int sc = 0;
|
||||||
|
sc += scoreNear((int)id, 38); // classic hint
|
||||||
|
const auto* s = findSeqById(id);
|
||||||
|
if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0;
|
||||||
|
if (sc > bestScore) {
|
||||||
|
bestScore = sc;
|
||||||
|
best = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop = best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step B: Score start/end candidates
|
||||||
|
uint32_t start = 0, end = 0;
|
||||||
|
int bestStart = -999, bestEnd = -999;
|
||||||
|
|
||||||
|
for (const auto& seq : sequences) {
|
||||||
|
if (isForbidden(seq.id)) continue;
|
||||||
|
// Only consider non-looping animations for start/end
|
||||||
|
bool isLoop = (seq.flags & 0x01) == 0;
|
||||||
|
if (isLoop) continue;
|
||||||
|
|
||||||
|
// Start window
|
||||||
|
if (seq.duration >= 450 && seq.duration <= 1100) {
|
||||||
|
int sc = 0;
|
||||||
|
if (loop) sc += scoreNear((int)seq.id, (int)loop);
|
||||||
|
// Chain bonus: if this start points at loop or near it
|
||||||
|
if (loop && (seq.nextAnimation == (int16_t)loop || seq.aliasNext == loop)) sc += 30;
|
||||||
|
if (loop && scoreNear(seq.nextAnimation, (int)loop) > 0) sc += 10;
|
||||||
|
// Penalize "stop/brake-ish": very long blendTime can be a stop transition
|
||||||
|
if (seq.blendTime > 400) sc -= 5;
|
||||||
|
|
||||||
|
if (sc > bestStart) {
|
||||||
|
bestStart = sc;
|
||||||
|
start = seq.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End window
|
||||||
|
if (seq.duration >= 650 && seq.duration <= 1600) {
|
||||||
|
int sc = 0;
|
||||||
|
if (loop) sc += scoreNear((int)seq.id, (int)loop);
|
||||||
|
// Chain bonus: end often points to run/stand or has no next
|
||||||
|
if (seq.nextAnimation == (int16_t)runId || seq.nextAnimation == (int16_t)standId) sc += 10;
|
||||||
|
if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal
|
||||||
|
if (sc > bestEnd) {
|
||||||
|
bestEnd = sc;
|
||||||
|
end = seq.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
|
||||||
|
" scores: start=", bestStart, " end=", bestEnd);
|
||||||
|
return std::make_tuple(start, loop, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet();
|
||||||
|
|
||||||
|
// Use discovered animations, fallback to known IDs if discovery fails
|
||||||
|
mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37});
|
||||||
|
mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38});
|
||||||
|
mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39});
|
||||||
|
mountAnims_.rearUp = findFirst({94, 92, 40}); // RearUp/Special
|
||||||
|
mountAnims_.run = findFirst({5, 4}); // Run/Walk
|
||||||
|
mountAnims_.stand = findFirst({0}); // Stand (almost always 0)
|
||||||
|
|
||||||
|
// 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
|
// Notify mount sound manager
|
||||||
if (mountSoundManager) {
|
if (mountSoundManager) {
|
||||||
bool isFlying = taxiFlight_; // Taxi flights are flying mounts
|
bool isFlying = taxiFlight_; // Taxi flights are flying mounts
|
||||||
|
|
@ -561,6 +736,8 @@ void Renderer::clearMount() {
|
||||||
mountHeightOffset_ = 0.0f;
|
mountHeightOffset_ = 0.0f;
|
||||||
mountPitch_ = 0.0f;
|
mountPitch_ = 0.0f;
|
||||||
mountRoll_ = 0.0f;
|
mountRoll_ = 0.0f;
|
||||||
|
mountAction_ = MountAction::None;
|
||||||
|
mountActionPhase_ = 0;
|
||||||
charAnimState = CharAnimState::IDLE;
|
charAnimState = CharAnimState::IDLE;
|
||||||
if (cameraController) {
|
if (cameraController) {
|
||||||
cameraController->setMounted(false);
|
cameraController->setMounted(false);
|
||||||
|
|
@ -717,6 +894,23 @@ void Renderer::updateCharacterAnimation() {
|
||||||
if (mountInstanceId_ > 0) {
|
if (mountInstanceId_ > 0) {
|
||||||
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||||
float yawRad = glm::radians(characterYaw);
|
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
|
// Apply pitch (up/down), roll (banking), and yaw for realistic flight
|
||||||
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad));
|
characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad));
|
||||||
|
|
||||||
|
|
@ -731,7 +925,99 @@ void Renderer::updateCharacterAnimation() {
|
||||||
};
|
};
|
||||||
|
|
||||||
uint32_t mountAnimId = ANIM_STAND;
|
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) {
|
if (anyStrafeLeft) {
|
||||||
mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN);
|
mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN);
|
||||||
} else if (anyStrafeRight) {
|
} else if (anyStrafeRight) {
|
||||||
|
|
@ -742,14 +1028,15 @@ void Renderer::updateCharacterAnimation() {
|
||||||
mountAnimId = ANIM_RUN;
|
mountAnimId = ANIM_RUN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint32_t curMountAnim = 0;
|
|
||||||
float curMountTime = 0, curMountDur = 0;
|
// Only update animation if it changed and we're not in an action sequence
|
||||||
bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur);
|
if (mountAction_ == MountAction::None && (!haveMountState || curMountAnim != mountAnimId)) {
|
||||||
if (!haveMountState || curMountAnim != mountAnimId) {
|
bool loop = true; // Normal movement animations loop
|
||||||
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
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) {
|
if (moving && haveMountState && curMountDur > 1.0f) {
|
||||||
float norm = std::fmod(curMountTime, curMountDur) / curMountDur;
|
float norm = std::fmod(curMountTime, curMountDur) / curMountDur;
|
||||||
// One bounce per stride cycle
|
// One bounce per stride cycle
|
||||||
|
|
@ -758,28 +1045,35 @@ void Renderer::updateCharacterAnimation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character follows mount's full rotation (pitch, roll, yaw)
|
// Use mount's attachment point for proper bone-driven rider positioning
|
||||||
// This keeps the character "glued" to the mount during banking/climbing
|
glm::mat4 mountSeatTransform;
|
||||||
float yawRad = glm::radians(characterYaw);
|
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
|
// Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y)
|
||||||
glm::mat4 mountRotation = glm::mat4(1.0f);
|
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f);
|
||||||
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)
|
|
||||||
|
|
||||||
// Offset in mount's local space (rider sits above mount)
|
// Position rider at mount seat
|
||||||
glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob);
|
characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset);
|
||||||
|
|
||||||
// Transform offset through mount's rotation to get world-space offset
|
// Rider uses character facing yaw, not mount bone rotation
|
||||||
glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f));
|
// (rider faces character direction, seat bone only provides position)
|
||||||
|
float yawRad = glm::radians(characterYaw);
|
||||||
// Character position = mount position + rotated offset
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
|
||||||
glm::vec3 playerPos = characterPosition + worldOffset;
|
} else {
|
||||||
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
// Fallback to old manual positioning if attachment not found
|
||||||
|
float yawRad = glm::radians(characterYaw);
|
||||||
// Character rotates with mount (same pitch, roll, yaw)
|
glm::mat4 mountRotation = glm::mat4(1.0f);
|
||||||
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1184,9 +1478,18 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
||||||
|
|
||||||
void Renderer::update(float deltaTime) {
|
void Renderer::update(float deltaTime) {
|
||||||
auto updateStart = std::chrono::steady_clock::now();
|
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 (wmoRenderer) wmoRenderer->resetQueryStats();
|
||||||
if (m2Renderer) m2Renderer->resetQueryStats();
|
if (m2Renderer) m2Renderer->resetQueryStats();
|
||||||
|
|
||||||
|
auto cam1 = std::chrono::high_resolution_clock::now();
|
||||||
if (cameraController) {
|
if (cameraController) {
|
||||||
auto cameraStart = std::chrono::steady_clock::now();
|
auto cameraStart = std::chrono::steady_clock::now();
|
||||||
cameraController->update(deltaTime);
|
cameraController->update(deltaTime);
|
||||||
|
|
@ -1201,8 +1504,11 @@ void Renderer::update(float deltaTime) {
|
||||||
} else {
|
} else {
|
||||||
lastCameraUpdateMs = 0.0;
|
lastCameraUpdateMs = 0.0;
|
||||||
}
|
}
|
||||||
|
auto cam2 = std::chrono::high_resolution_clock::now();
|
||||||
|
camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count();
|
||||||
|
|
||||||
// Update lighting system
|
// Update lighting system
|
||||||
|
auto light1 = std::chrono::high_resolution_clock::now();
|
||||||
if (lightingManager) {
|
if (lightingManager) {
|
||||||
// TODO: Get actual map ID from game state (0 = Eastern Kingdoms for now)
|
// 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 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);
|
lightingManager->update(characterPosition, mapId, gameTime, isRaining, isUnderwater);
|
||||||
}
|
}
|
||||||
|
auto light2 = std::chrono::high_resolution_clock::now();
|
||||||
|
lightTime += std::chrono::duration<float, std::milli>(light2 - light1).count();
|
||||||
|
|
||||||
// Sync character model position/rotation and animation with follow target
|
// 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 (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
||||||
if (meleeSwingCooldown > 0.0f) {
|
if (meleeSwingCooldown > 0.0f) {
|
||||||
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
|
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
|
||||||
|
|
@ -1261,13 +1570,19 @@ void Renderer::update(float deltaTime) {
|
||||||
// Update animation based on movement state
|
// Update animation based on movement state
|
||||||
updateCharacterAnimation();
|
updateCharacterAnimation();
|
||||||
}
|
}
|
||||||
|
auto charAnim2 = std::chrono::high_resolution_clock::now();
|
||||||
|
charAnimTime += std::chrono::duration<float, std::milli>(charAnim2 - charAnim1).count();
|
||||||
|
|
||||||
// Update terrain streaming
|
// Update terrain streaming
|
||||||
|
auto terrain1 = std::chrono::high_resolution_clock::now();
|
||||||
if (terrainManager && camera) {
|
if (terrainManager && camera) {
|
||||||
terrainManager->update(*camera, deltaTime);
|
terrainManager->update(*camera, deltaTime);
|
||||||
}
|
}
|
||||||
|
auto terrain2 = std::chrono::high_resolution_clock::now();
|
||||||
|
terrainTime += std::chrono::duration<float, std::milli>(terrain2 - terrain1).count();
|
||||||
|
|
||||||
// Update skybox time progression
|
// Update skybox time progression
|
||||||
|
auto sky1 = std::chrono::high_resolution_clock::now();
|
||||||
if (skybox) {
|
if (skybox) {
|
||||||
skybox->update(deltaTime);
|
skybox->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
@ -1319,16 +1634,25 @@ void Renderer::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
auto sky2 = std::chrono::high_resolution_clock::now();
|
||||||
|
skyTime += std::chrono::duration<float, std::milli>(sky2 - sky1).count();
|
||||||
|
|
||||||
// Update character animations
|
// Update character animations
|
||||||
if (characterRenderer) {
|
auto charRend1 = std::chrono::high_resolution_clock::now();
|
||||||
characterRenderer->update(deltaTime);
|
if (characterRenderer && camera) {
|
||||||
|
characterRenderer->update(deltaTime, camera->getPosition());
|
||||||
}
|
}
|
||||||
|
auto charRend2 = std::chrono::high_resolution_clock::now();
|
||||||
|
charRendTime += std::chrono::duration<float, std::milli>(charRend2 - charRend1).count();
|
||||||
|
|
||||||
// Update AudioEngine (cleanup finished sounds, etc.)
|
// Update AudioEngine (cleanup finished sounds, etc.)
|
||||||
|
auto audio1 = std::chrono::high_resolution_clock::now();
|
||||||
audio::AudioEngine::instance().update(deltaTime);
|
audio::AudioEngine::instance().update(deltaTime);
|
||||||
|
auto audio2 = std::chrono::high_resolution_clock::now();
|
||||||
|
audioTime += std::chrono::duration<float, std::milli>(audio2 - audio1).count();
|
||||||
|
|
||||||
// Footsteps: animation-event driven + surface query at event time.
|
// Footsteps: animation-event driven + surface query at event time.
|
||||||
|
auto footstep1 = std::chrono::high_resolution_clock::now();
|
||||||
if (footstepManager) {
|
if (footstepManager) {
|
||||||
footstepManager->update(deltaTime);
|
footstepManager->update(deltaTime);
|
||||||
cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer
|
cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer
|
||||||
|
|
@ -1448,8 +1772,11 @@ void Renderer::update(float deltaTime) {
|
||||||
mountSoundManager->setFlying(flying);
|
mountSoundManager->setFlying(flying);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
auto footstep2 = std::chrono::high_resolution_clock::now();
|
||||||
|
footstepTime += std::chrono::duration<float, std::milli>(footstep2 - footstep1).count();
|
||||||
|
|
||||||
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
||||||
|
auto ambient1 = std::chrono::high_resolution_clock::now();
|
||||||
if (ambientSoundManager && camera && wmoRenderer && cameraController) {
|
if (ambientSoundManager && camera && wmoRenderer && cameraController) {
|
||||||
glm::vec3 camPos = camera->getPosition();
|
glm::vec3 camPos = camera->getPosition();
|
||||||
uint32_t wmoId = 0;
|
uint32_t wmoId = 0;
|
||||||
|
|
@ -1489,12 +1816,19 @@ void Renderer::update(float deltaTime) {
|
||||||
|
|
||||||
ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
|
ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
|
||||||
}
|
}
|
||||||
|
auto ambient2 = std::chrono::high_resolution_clock::now();
|
||||||
|
ambientTime += std::chrono::duration<float, std::milli>(ambient2 - ambient1).count();
|
||||||
|
|
||||||
// Update M2 doodad animations (pass camera for frustum-culling bone computation)
|
// 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) {
|
if (m2Renderer && camera) {
|
||||||
m2Renderer->update(deltaTime, camera->getPosition(),
|
m2Renderer->update(deltaTime, camera->getPosition(),
|
||||||
camera->getProjectionMatrix() * camera->getViewMatrix());
|
camera->getProjectionMatrix() * camera->getViewMatrix());
|
||||||
}
|
}
|
||||||
|
auto m22 = std::chrono::high_resolution_clock::now();
|
||||||
|
m2Time += std::chrono::duration<float, std::milli>(m22 - m21).count();
|
||||||
|
|
||||||
// Update zone detection and music
|
// Update zone detection and music
|
||||||
if (zoneManager && musicManager && terrainManager && camera) {
|
if (zoneManager && musicManager && terrainManager && camera) {
|
||||||
|
|
@ -1617,6 +1951,24 @@ void Renderer::update(float deltaTime) {
|
||||||
|
|
||||||
auto updateEnd = std::chrono::steady_clock::now();
|
auto updateEnd = std::chrono::steady_clock::now();
|
||||||
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
||||||
|
|
||||||
|
// Log renderer profiling every 60 frames
|
||||||
|
if (++rendProfileCounter >= 60) {
|
||||||
|
LOG_INFO("RENDERER UPDATE PROFILE (60 frames): camera=", camTime / 60.0f,
|
||||||
|
"ms light=", lightTime / 60.0f, "ms charAnim=", charAnimTime / 60.0f,
|
||||||
|
"ms terrain=", terrainTime / 60.0f, "ms sky=", skyTime / 60.0f,
|
||||||
|
"ms charRend=", charRendTime / 60.0f, "ms audio=", audioTime / 60.0f,
|
||||||
|
"ms footstep=", footstepTime / 60.0f, "ms ambient=", ambientTime / 60.0f,
|
||||||
|
"ms m2Anim=", m2Time / 60.0f, "ms");
|
||||||
|
rendProfileCounter = 0;
|
||||||
|
camTime = lightTime = charAnimTime = 0.0f;
|
||||||
|
terrainTime = skyTime = charRendTime = 0.0f;
|
||||||
|
audioTime = footstepTime = ambientTime = 0.0f;
|
||||||
|
m2Time = 0.0f;
|
||||||
|
}
|
||||||
|
if (++m2ProfileCounter >= 60) {
|
||||||
|
m2ProfileCounter = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1729,7 +2081,7 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro
|
||||||
glEnable(GL_CULL_FACE);
|
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();
|
auto renderStart = std::chrono::steady_clock::now();
|
||||||
lastTerrainRenderMs = 0.0;
|
lastTerrainRenderMs = 0.0;
|
||||||
lastWMORenderMs = 0.0;
|
lastWMORenderMs = 0.0;
|
||||||
|
|
@ -1759,50 +2111,72 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
bool underwater = false;
|
bool underwater = false;
|
||||||
bool canalUnderwater = false;
|
bool canalUnderwater = false;
|
||||||
|
|
||||||
// Render skybox first (furthest back)
|
// Render sky system (unified coordinator for skybox, stars, celestial, clouds, lens flare)
|
||||||
if (skybox && camera) {
|
if (skySystem && camera) {
|
||||||
skybox->render(*camera, timeOfDay);
|
// Populate SkyParams from lighting manager
|
||||||
}
|
rendering::SkyParams skyParams;
|
||||||
|
skyParams.timeOfDay = timeOfDay;
|
||||||
|
skyParams.gameTime = gameHandler ? gameHandler->getGameTime() : -1.0f;
|
||||||
|
|
||||||
// Get lighting parameters for celestial rendering
|
if (lightingManager) {
|
||||||
const glm::vec3* sunDir = nullptr;
|
const auto& lighting = lightingManager->getLightingParams();
|
||||||
const glm::vec3* sunColor = nullptr;
|
skyParams.directionalDir = lighting.directionalDir;
|
||||||
float cloudDensity = 0.0f;
|
skyParams.sunColor = lighting.diffuseColor;
|
||||||
float fogDensity = 0.0f;
|
skyParams.skyTopColor = lighting.skyTopColor;
|
||||||
if (lightingManager) {
|
skyParams.skyMiddleColor = lighting.skyMiddleColor;
|
||||||
const auto& lighting = lightingManager->getLightingParams();
|
skyParams.skyBand1Color = lighting.skyBand1Color;
|
||||||
sunDir = &lighting.directionalDir;
|
skyParams.skyBand2Color = lighting.skyBand2Color;
|
||||||
sunColor = &lighting.diffuseColor;
|
skyParams.cloudDensity = lighting.cloudDensity;
|
||||||
cloudDensity = lighting.cloudDensity;
|
skyParams.fogDensity = lighting.fogDensity;
|
||||||
fogDensity = lighting.fogDensity;
|
skyParams.horizonGlow = lighting.horizonGlow;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render stars after skybox (affected by cloud/fog density)
|
// TODO: Set skyboxModelId from LightSkybox.dbc (future)
|
||||||
if (starField && camera) {
|
skyParams.skyboxModelId = 0;
|
||||||
starField->render(*camera, timeOfDay, cloudDensity, fogDensity);
|
skyParams.skyboxHasStars = false; // Gradient skybox has no baked stars
|
||||||
}
|
|
||||||
|
skySystem->render(*camera, skyParams);
|
||||||
// Render celestial bodies (sun/moon) after stars (sun uses lighting direction/color)
|
} else {
|
||||||
if (celestial && camera) {
|
// Fallback: render individual components (backwards compatibility)
|
||||||
celestial->render(*camera, timeOfDay, sunDir, sunColor);
|
if (skybox && camera) {
|
||||||
}
|
skybox->render(*camera, timeOfDay);
|
||||||
|
}
|
||||||
// Render clouds after celestial bodies
|
|
||||||
if (clouds && camera) {
|
// Get lighting parameters for celestial rendering
|
||||||
clouds->render(*camera, timeOfDay);
|
const glm::vec3* sunDir = nullptr;
|
||||||
}
|
const glm::vec3* sunColor = nullptr;
|
||||||
|
float cloudDensity = 0.0f;
|
||||||
// Render lens flare (screen-space effect, render after celestial bodies)
|
float fogDensity = 0.0f;
|
||||||
if (lensFlare && camera && celestial) {
|
if (lightingManager) {
|
||||||
// Use lighting direction for sun position if available
|
const auto& lighting = lightingManager->getLightingParams();
|
||||||
glm::vec3 sunPosition;
|
sunDir = &lighting.directionalDir;
|
||||||
if (sunDir) {
|
sunColor = &lighting.diffuseColor;
|
||||||
const float sunDistance = 800.0f;
|
cloudDensity = lighting.cloudDensity;
|
||||||
sunPosition = -*sunDir * sunDistance;
|
fogDensity = lighting.fogDensity;
|
||||||
} else {
|
}
|
||||||
sunPosition = celestial->getSunPosition(timeOfDay);
|
|
||||||
|
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
|
// Apply lighting and fog to all renderers
|
||||||
|
|
|
||||||
|
|
@ -146,9 +146,15 @@ void SkySystem::render(const Camera& camera, const SkyParams& params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const {
|
glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const {
|
||||||
// Use lighting direction for sun position
|
// TESTING: X-up test
|
||||||
const float sunDistance = 800.0f;
|
glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); // X-up
|
||||||
return -params.directionalDir * sunDistance; // Negative because light comes FROM sun
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always process ready tiles each frame (GPU uploads from background thread)
|
// Always process ready tiles each frame (GPU uploads from background thread)
|
||||||
|
// Time budget prevents frame spikes from heavy tiles
|
||||||
processReadyTiles();
|
processReadyTiles();
|
||||||
|
|
||||||
timeSinceLastUpdate += deltaTime;
|
timeSinceLastUpdate += deltaTime;
|
||||||
|
|
@ -641,7 +642,8 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
|
||||||
m2Renderer->initialize(assetManager);
|
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<uint32_t> uploadedModelIds;
|
std::unordered_set<uint32_t> uploadedModelIds;
|
||||||
for (auto& m2Ready : pending->m2Models) {
|
for (auto& m2Ready : pending->m2Models) {
|
||||||
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
|
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
|
||||||
|
|
@ -649,7 +651,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!uploadedModelIds.empty()) {
|
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)
|
// Create instances (deduplicate by uniqueId across tile boundaries)
|
||||||
|
|
@ -813,11 +815,13 @@ void TerrainManager::workerLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerrainManager::processReadyTiles() {
|
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;
|
int processed = 0;
|
||||||
const int maxPerFrame = 1;
|
|
||||||
|
|
||||||
while (processed < maxPerFrame) {
|
while (true) {
|
||||||
std::shared_ptr<PendingTile> pending;
|
std::shared_ptr<PendingTile> pending;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -831,16 +835,48 @@ void TerrainManager::processReadyTiles() {
|
||||||
|
|
||||||
if (pending) {
|
if (pending) {
|
||||||
TileCoord coord = pending->coord;
|
TileCoord coord = pending->coord;
|
||||||
|
auto tileStart = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
finalizeTile(pending);
|
finalizeTile(pending);
|
||||||
|
|
||||||
|
auto tileEnd = std::chrono::high_resolution_clock::now();
|
||||||
|
float tileTimeMs = std::chrono::duration<float, std::milli>(tileEnd - tileStart).count();
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(queueMutex);
|
std::lock_guard<std::mutex> lock(queueMutex);
|
||||||
pendingTiles.erase(coord);
|
pendingTiles.erase(coord);
|
||||||
}
|
}
|
||||||
processed++;
|
processed++;
|
||||||
|
|
||||||
|
// Check if we've exceeded time budget
|
||||||
|
float elapsedMs = std::chrono::duration<float, std::milli>(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() {
|
void TerrainManager::processAllReadyTiles() {
|
||||||
while (true) {
|
while (true) {
|
||||||
std::shared_ptr<PendingTile> pending;
|
std::shared_ptr<PendingTile> pending;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue