diff --git a/CMakeLists.txt b/CMakeLists.txt index 026b4e31..2ea402fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ set(WOWEE_SOURCES src/rendering/lens_flare.cpp src/rendering/weather.cpp src/rendering/lightning.cpp + src/rendering/lighting_manager.cpp src/rendering/character_renderer.cpp src/rendering/character_preview.cpp src/rendering/wmo_renderer.cpp diff --git a/include/rendering/lighting_manager.hpp b/include/rendering/lighting_manager.hpp new file mode 100644 index 00000000..f6b23627 --- /dev/null +++ b/include/rendering/lighting_manager.hpp @@ -0,0 +1,263 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { class DBCFile; class AssetManager; } + +namespace rendering { + +/** + * Time-of-day lighting parameters sampled from DBC curves + */ +struct LightingParams { + glm::vec3 ambientColor{0.4f, 0.4f, 0.5f}; // Fill lighting + glm::vec3 diffuseColor{1.0f, 0.95f, 0.8f}; // Directional sun color + glm::vec3 directionalDir{0.0f, -1.0f, 0.5f}; // Sun direction (normalized) + + glm::vec3 fogColor{0.5f, 0.6f, 0.7f}; // Fog color + float fogStart = 100.0f; // Fog start distance + float fogEnd = 1000.0f; // Fog end distance + float fogDensity = 0.001f; // Fog density + + glm::vec3 skyTopColor{0.5f, 0.7f, 1.0f}; // Sky zenith color + glm::vec3 skyMiddleColor{0.7f, 0.85f, 1.0f}; // Sky horizon color + glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f}; // Sky band 1 + glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; // Sky band 2 + + float cloudDensity = 1.0f; // Cloud density/opacity + float horizonGlow = 0.3f; // Horizon glow intensity +}; + +/** + * Light set keyframe for time-of-day interpolation + */ +struct LightKeyframe { + uint32_t time; // Time in minutes since midnight (0-1439) + + // Colors stored as RGB int tuples (0-255) in DBC + glm::vec3 ambientColor; + glm::vec3 diffuseColor; + glm::vec3 fogColor; + glm::vec3 skyTopColor; + glm::vec3 skyMiddleColor; + glm::vec3 skyBand1Color; + glm::vec3 skyBand2Color; + + float fogStart; + float fogEnd; + float fogDensity; + float cloudDensity; + float horizonGlow; +}; + +/** + * Light volume from Light.dbc (spatial lighting) + */ +struct LightVolume { + uint32_t lightId = 0; + uint32_t mapId = 0; + glm::vec3 position{0.0f}; // World position (note: DBC stores as x,z,y!) + float innerRadius = 0.0f; // Full weight radius + float outerRadius = 0.0f; // Fade-out radius + + // LightParams IDs for different conditions + uint32_t lightParamsId = 0; // Normal/clear weather + uint32_t lightParamsIdRain = 0; // Rainy weather + uint32_t lightParamsIdUnderwater = 0; + // More variants exist for phases, death, etc. +}; + +/** + * Color band with time-of-day keyframes + */ +struct ColorBand { + uint8_t numKeyframes = 0; + uint16_t times[16]; // Time keyframes (half-minutes since midnight) + glm::vec3 colors[16]; // Color values (RGB 0-1) +}; + +/** + * Float band with time-of-day keyframes + */ +struct FloatBand { + uint8_t numKeyframes = 0; + uint16_t times[16]; // Time keyframes (half-minutes since midnight) + float values[16]; // Float values +}; + +/** + * LightParams profile with 18 color bands + 6 float bands + */ +struct LightParamsProfile { + uint32_t lightParamsId = 0; + + // 18 color channels (IntBand) + enum ColorChannel { + AMBIENT_COLOR = 0, + DIFFUSE_COLOR = 1, + SKY_TOP_COLOR = 2, + SKY_MIDDLE_COLOR = 3, + SKY_BAND1_COLOR = 4, + SKY_BAND2_COLOR = 5, + FOG_COLOR = 6, + // ... more channels exist (ocean, river, shadow, etc.) + COLOR_CHANNEL_COUNT = 18 + }; + + ColorBand colorBands[COLOR_CHANNEL_COUNT]; + + // 6 float channels (FloatBand) + enum FloatChannel { + FOG_END = 0, + FOG_START_SCALAR = 1, // Multiplier for fog start + CLOUD_DENSITY = 2, + FOG_DENSITY = 3, + // ... more channels + FLOAT_CHANNEL_COUNT = 6 + }; + + FloatBand floatBands[FLOAT_CHANNEL_COUNT]; +}; + +/** + * WoW DBC-driven lighting manager + * + * Implements WotLK's time-of-day lighting system: + * - Loads Light.dbc, LightParams.dbc, LightIntBand.dbc, LightFloatBand.dbc + * - Samples lighting curves based on time-of-day + * - Interpolates between keyframes + * - Provides lighting parameters for rendering + */ +class LightingManager { +public: + LightingManager(); + ~LightingManager(); + + /** + * Initialize lighting system and load DBCs + */ + bool initialize(pipeline::AssetManager* assetManager); + + /** + * Update lighting for current time and player position + * @param playerPos Player world position + * @param mapId Current map ID + * @param gameTime Optional game time in seconds (use -1 for real time) + * @param isRaining Whether it's raining + * @param isUnderwater Whether player is underwater + * + * Note: WoW uses server-sent game time, not local PC time. + * Pass gameTime from SMSG_LOGIN_SETTIMESPEED or similar. + */ + void update(const glm::vec3& playerPos, uint32_t mapId, + float gameTime = -1.0f, + bool isRaining = false, bool isUnderwater = false); + + /** + * Get current lighting parameters + */ + const LightingParams& getLightingParams() const { return currentParams_; } + + /** + * Set whether player is indoors (disables outdoor lighting) + */ + void setIndoors(bool indoors) { isIndoors_ = indoors; } + + /** + * Get current time of day (0.0-1.0) + */ + float getTimeOfDay() const { return timeOfDay_; } + + /** + * Manually set time of day for testing + */ + void setTimeOfDay(float tod) { timeOfDay_ = tod; manualTime_ = true; } + + /** + * Use real time for day/night cycle + */ + void useRealTime(bool use) { manualTime_ = !use; } + +private: + /** + * Load Light.dbc + */ + bool loadLightDbc(pipeline::AssetManager* assetManager); + + /** + * Load LightParams.dbc for zone→light mapping + */ + bool loadLightParamsDbc(pipeline::AssetManager* assetManager); + + /** + * Load LightIntBand.dbc and LightFloatBand.dbc for time curves + */ + bool loadLightBandDbcs(pipeline::AssetManager* assetManager); + + /** + * Weighted light volume for blending + */ + struct WeightedVolume { + const LightVolume* volume = nullptr; + float weight = 0.0f; + }; + + /** + * Find light volumes for blending (up to 4 with weight > 0) + */ + std::vector findLightVolumes(const glm::vec3& playerPos, uint32_t mapId) const; + + /** + * Get LightParams ID based on conditions + */ + uint32_t selectLightParamsId(const LightVolume* volume, bool isRaining, bool isUnderwater) const; + + /** + * Sample lighting from LightParams profile + */ + LightingParams sampleLightParams(const LightParamsProfile* profile, uint16_t timeHalfMinutes) const; + + /** + * Sample color from band + */ + glm::vec3 sampleColorBand(const ColorBand& band, uint16_t timeHalfMinutes) const; + + /** + * Sample float from band + */ + float sampleFloatBand(const FloatBand& band, uint16_t timeHalfMinutes) const; + + /** + * Convert DBC BGR color to RGB vec3 + */ + glm::vec3 dbcColorToVec3(uint32_t dbcColor) const; + + pipeline::AssetManager* assetManager_ = nullptr; + + // Light volumes by map + std::map> lightVolumesByMap_; + + // LightParams profiles by ID + std::map lightParamsProfiles_; + + // Current state + LightingParams currentParams_; + LightingParams targetParams_; // For smooth blending + std::vector activeVolumes_; + glm::vec3 currentPlayerPos_{0.0f}; + uint32_t currentMapId_ = 0; + float timeOfDay_ = 0.5f; // Start at noon + bool isIndoors_ = false; + bool manualTime_ = false; + bool initialized_ = false; + + // Fallback lighting + LightingParams fallbackParams_; +}; + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/lighting_manager.cpp b/src/rendering/lighting_manager.cpp new file mode 100644 index 00000000..5195defb --- /dev/null +++ b/src/rendering/lighting_manager.cpp @@ -0,0 +1,552 @@ +#include "rendering/lighting_manager.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace rendering { + +// Light coordinate scaling (test with 1.0f first, then try 36.0f if distances seem off) +constexpr float LIGHT_COORD_SCALE = 1.0f; + +// Maximum volumes to blend (top 2-4) +constexpr size_t MAX_BLEND_VOLUMES = 2; + +LightingManager::LightingManager() { + // Set fallback lighting (Elwynn Forest-ish outdoor daytime) + fallbackParams_.ambientColor = glm::vec3(0.5f, 0.5f, 0.6f); + fallbackParams_.diffuseColor = glm::vec3(1.0f, 0.95f, 0.85f); + fallbackParams_.directionalDir = glm::normalize(glm::vec3(0.3f, -0.7f, 0.6f)); + fallbackParams_.fogColor = glm::vec3(0.6f, 0.7f, 0.85f); + fallbackParams_.fogStart = 300.0f; + fallbackParams_.fogEnd = 1500.0f; + fallbackParams_.skyTopColor = glm::vec3(0.4f, 0.6f, 0.9f); + fallbackParams_.skyMiddleColor = glm::vec3(0.6f, 0.75f, 0.95f); + + currentParams_ = fallbackParams_; +} + +LightingManager::~LightingManager() { +} + +bool LightingManager::initialize(pipeline::AssetManager* assetManager) { + if (!assetManager) { + LOG_ERROR("LightingManager::initialize: null AssetManager"); + return false; + } + + assetManager_ = assetManager; + + // Load DBCs (non-fatal if missing, will use fallback lighting) + loadLightDbc(assetManager); + loadLightParamsDbc(assetManager); + loadLightBandDbcs(assetManager); + + initialized_ = true; + LOG_INFO("LightingManager initialized: ", lightVolumesByMap_.size(), " maps with lighting"); + return true; +} + +bool LightingManager::loadLightDbc(pipeline::AssetManager* assetManager) { + auto dbcData = assetManager->readFile("DBFilesClient\\Light.dbc"); + if (dbcData.empty()) { + LOG_WARNING("Light.dbc not found, using fallback lighting"); + return false; + } + + auto dbc = std::make_unique(); + if (!dbc->load(dbcData)) { + LOG_ERROR("Failed to load Light.dbc"); + return false; + } + + uint32_t recordCount = dbc->getRecordCount(); + LOG_INFO("Loading Light.dbc: ", recordCount, " light volumes"); + + // Parse light volumes + // Light.dbc structure (WotLK 3.3.5a): + // 0: uint32 ID + // 1: uint32 MapID + // 2-4: float X, Z, Y (note: z and y swapped!) + // 5: float FalloffStart (inner radius) + // 6: float FalloffEnd (outer radius) + // 7: uint32 LightParamsID (clear weather) + // 8: uint32 LightParamsID (overcast/rain) + // 9: uint32 LightParamsID (underwater) + // ... more params for death, phases, etc. + + for (uint32_t i = 0; i < recordCount; ++i) { + LightVolume volume; + volume.lightId = dbc->getUInt32(i, 0); + volume.mapId = dbc->getUInt32(i, 1); + + // Position (note: DBC stores as x,z,y - need to swap!) + float x = dbc->getFloat(i, 2); + float z = dbc->getFloat(i, 3); + float y = dbc->getFloat(i, 4); + volume.position = glm::vec3(x, y, z); // Convert to x,y,z + + volume.innerRadius = dbc->getFloat(i, 5); + volume.outerRadius = dbc->getFloat(i, 6); + + // LightParams IDs for different conditions + volume.lightParamsId = dbc->getUInt32(i, 7); + if (dbc->getFieldCount() > 8) { + volume.lightParamsIdRain = dbc->getUInt32(i, 8); + } + if (dbc->getFieldCount() > 9) { + volume.lightParamsIdUnderwater = dbc->getUInt32(i, 9); + } + + // Add to map-specific list + lightVolumesByMap_[volume.mapId].push_back(volume); + } + + LOG_INFO("Loaded ", lightVolumesByMap_.size(), " maps with lighting volumes"); + return true; +} + +bool LightingManager::loadLightParamsDbc(pipeline::AssetManager* assetManager) { + auto dbcData = assetManager->readFile("DBFilesClient\\LightParams.dbc"); + if (dbcData.empty()) { + LOG_WARNING("LightParams.dbc not found"); + return false; + } + + auto dbc = std::make_unique(); + if (!dbc->load(dbcData)) { + LOG_ERROR("Failed to load LightParams.dbc"); + return false; + } + + uint32_t recordCount = dbc->getRecordCount(); + LOG_INFO("Loaded LightParams.dbc: ", recordCount, " profiles"); + + // Create profile entries (will be populated by band loading) + for (uint32_t i = 0; i < recordCount; ++i) { + uint32_t paramId = dbc->getUInt32(i, 0); + LightParamsProfile profile; + profile.lightParamsId = paramId; + lightParamsProfiles_[paramId] = profile; + } + + return true; +} + +bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) { + // Load LightIntBand.dbc for RGB color curves (18 channels per LightParams) + auto intBandData = assetManager->readFile("DBFilesClient\\LightIntBand.dbc"); + if (!intBandData.empty()) { + auto dbc = std::make_unique(); + if (dbc->load(intBandData)) { + LOG_INFO("Loaded LightIntBand.dbc: ", dbc->getRecordCount(), " color bands"); + + // Parse int bands + // Structure: ID, Entry (block index), NumValues, Time[16], Color[16] + // Block index = LightParamsID * 18 + channel + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t blockIndex = dbc->getUInt32(i, 1); + uint32_t lightParamsId = blockIndex / 18; + uint32_t channelIndex = blockIndex % 18; + + auto it = lightParamsProfiles_.find(lightParamsId); + if (it == lightParamsProfiles_.end()) continue; + + if (channelIndex >= LightParamsProfile::COLOR_CHANNEL_COUNT) continue; + + ColorBand& band = it->second.colorBands[channelIndex]; + band.numKeyframes = dbc->getUInt32(i, 2); + if (band.numKeyframes > 16) band.numKeyframes = 16; + + // Read time keys (field 3-18) - stored as uint16 half-minutes + for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { + uint32_t timeValue = dbc->getUInt32(i, 3 + k); + band.times[k] = static_cast(timeValue % 2880); // Clamp to valid range + } + + // Read color values (field 19-34) - stored as BGRA packed uint32 + for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { + uint32_t colorBGRA = dbc->getUInt32(i, 19 + k); + band.colors[k] = dbcColorToVec3(colorBGRA); + } + } + } + } + + // Load LightFloatBand.dbc for fog/intensity curves (6 channels per LightParams) + auto floatBandData = assetManager->readFile("DBFilesClient\\LightFloatBand.dbc"); + if (!floatBandData.empty()) { + auto dbc = std::make_unique(); + if (dbc->load(floatBandData)) { + LOG_INFO("Loaded LightFloatBand.dbc: ", dbc->getRecordCount(), " float bands"); + + // Parse float bands + // Structure: ID, Entry (block index), NumValues, Time[16], Value[16] + // Block index = LightParamsID * 6 + channel + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t blockIndex = dbc->getUInt32(i, 1); + uint32_t lightParamsId = blockIndex / 6; + uint32_t channelIndex = blockIndex % 6; + + auto it = lightParamsProfiles_.find(lightParamsId); + if (it == lightParamsProfiles_.end()) continue; + + if (channelIndex >= LightParamsProfile::FLOAT_CHANNEL_COUNT) continue; + + FloatBand& band = it->second.floatBands[channelIndex]; + band.numKeyframes = dbc->getUInt32(i, 2); + if (band.numKeyframes > 16) band.numKeyframes = 16; + + // Read time keys (field 3-18) + for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { + uint32_t timeValue = dbc->getUInt32(i, 3 + k); + band.times[k] = static_cast(timeValue % 2880); // Clamp to valid range + } + + // Read float values (field 19-34) + for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { + band.values[k] = dbc->getFloat(i, 19 + k); + } + } + } + } + + LOG_INFO("Loaded bands for ", lightParamsProfiles_.size(), " LightParams profiles"); + return true; +} + +void LightingManager::update(const glm::vec3& playerPos, uint32_t mapId, + float gameTime, + bool isRaining, bool isUnderwater) { + if (!initialized_) return; + + // Update time + if (!manualTime_) { + if (gameTime >= 0.0f) { + // Use server-sent game time (preferred!) + // gameTime is typically seconds since midnight + timeOfDay_ = std::fmod(gameTime / 86400.0f, 1.0f); // 0.0-1.0 + } else { + // Fallback: use real time for day/night cycle + std::time_t now = std::time(nullptr); + std::tm* localTime = std::localtime(&now); + float secondsSinceMidnight = localTime->tm_hour * 3600.0f + + localTime->tm_min * 60.0f + + localTime->tm_sec; + timeOfDay_ = secondsSinceMidnight / 86400.0f; // 0.0-1.0 + } + } + // else: manualTime_ is set, use timeOfDay_ as-is + + // Convert time to half-minutes (WoW DBC format: 0-2879) + uint16_t timeHalfMinutes = static_cast(timeOfDay_ * 2880.0f) % 2880; + + // Update player position and map + currentPlayerPos_ = playerPos; + currentMapId_ = mapId; + + // Find light volumes for blending + activeVolumes_ = findLightVolumes(playerPos, mapId); + + // Sample and blend lighting + LightingParams newParams; + + if (isIndoors_) { + // Indoor lighting: static ambient-heavy + newParams.ambientColor = glm::vec3(0.6f, 0.6f, 0.65f); + newParams.diffuseColor = glm::vec3(0.3f, 0.3f, 0.35f); + newParams.fogColor = glm::vec3(0.3f, 0.3f, 0.4f); + newParams.fogStart = 50.0f; + newParams.fogEnd = 300.0f; + } else if (!activeVolumes_.empty()) { + // Outdoor with DBC lighting - blend multiple volumes + newParams = {}; // Zero-initialize + glm::vec3 blendedDir(0.0f); // Accumulate weighted directions + + for (const auto& wv : activeVolumes_) { + uint32_t lightParamsId = selectLightParamsId(wv.volume, isRaining, isUnderwater); + auto it = lightParamsProfiles_.find(lightParamsId); + + if (it != lightParamsProfiles_.end()) { + LightingParams params = sampleLightParams(&it->second, timeHalfMinutes); + + // Blend this volume's contribution + newParams.ambientColor += params.ambientColor * wv.weight; + newParams.diffuseColor += params.diffuseColor * wv.weight; + newParams.fogColor += params.fogColor * wv.weight; + newParams.skyTopColor += params.skyTopColor * wv.weight; + newParams.skyMiddleColor += params.skyMiddleColor * wv.weight; + newParams.skyBand1Color += params.skyBand1Color * wv.weight; + newParams.skyBand2Color += params.skyBand2Color * wv.weight; + + newParams.fogStart += params.fogStart * wv.weight; + newParams.fogEnd += params.fogEnd * wv.weight; + newParams.fogDensity += params.fogDensity * wv.weight; + newParams.cloudDensity += params.cloudDensity * wv.weight; + newParams.horizonGlow += params.horizonGlow * wv.weight; + + // Blend direction weighted (normalize at end) + blendedDir += params.directionalDir * wv.weight; + } + } + + // Normalize blended direction + if (glm::length(blendedDir) > 0.001f) { + newParams.directionalDir = glm::normalize(blendedDir); + } else { + // Fallback if all directions cancelled out + newParams.directionalDir = glm::vec3(0.3f, -0.7f, 0.6f); + } + } else { + // No light volume, use fallback with time-based animation + newParams = fallbackParams_; + + // Animate sun direction + float angle = timeOfDay_ * 2.0f * 3.14159f; + newParams.directionalDir = glm::normalize(glm::vec3( + std::sin(angle) * 0.6f, + -0.6f + std::cos(angle) * 0.4f, + std::cos(angle) * 0.6f + )); + + // Time-of-day color adjustments + if (timeOfDay_ < 0.25f || timeOfDay_ > 0.75f) { + // Night: darker, bluer + float nightness = (timeOfDay_ < 0.25f) ? (0.25f - timeOfDay_) * 4.0f + : (timeOfDay_ - 0.75f) * 4.0f; + newParams.ambientColor *= (0.3f + 0.7f * (1.0f - nightness)); + newParams.diffuseColor *= (0.2f + 0.8f * (1.0f - nightness)); + newParams.ambientColor.b += nightness * 0.1f; + } + } + + // Smooth temporal blending to avoid snapping (5.0 = blend rate) + float deltaTime = 0.016f; // Assume ~60 FPS for now + float blendFactor = 1.0f - std::exp(-deltaTime * 5.0f); + + currentParams_.ambientColor = glm::mix(currentParams_.ambientColor, newParams.ambientColor, blendFactor); + currentParams_.diffuseColor = glm::mix(currentParams_.diffuseColor, newParams.diffuseColor, blendFactor); + currentParams_.fogColor = glm::mix(currentParams_.fogColor, newParams.fogColor, blendFactor); + currentParams_.skyTopColor = glm::mix(currentParams_.skyTopColor, newParams.skyTopColor, blendFactor); + currentParams_.skyMiddleColor = glm::mix(currentParams_.skyMiddleColor, newParams.skyMiddleColor, blendFactor); + currentParams_.skyBand1Color = glm::mix(currentParams_.skyBand1Color, newParams.skyBand1Color, blendFactor); + currentParams_.skyBand2Color = glm::mix(currentParams_.skyBand2Color, newParams.skyBand2Color, blendFactor); + currentParams_.fogStart = glm::mix(currentParams_.fogStart, newParams.fogStart, blendFactor); + currentParams_.fogEnd = glm::mix(currentParams_.fogEnd, newParams.fogEnd, blendFactor); + currentParams_.fogDensity = glm::mix(currentParams_.fogDensity, newParams.fogDensity, blendFactor); + currentParams_.cloudDensity = glm::mix(currentParams_.cloudDensity, newParams.cloudDensity, blendFactor); + currentParams_.horizonGlow = glm::mix(currentParams_.horizonGlow, newParams.horizonGlow, blendFactor); + currentParams_.directionalDir = glm::normalize(glm::mix(currentParams_.directionalDir, newParams.directionalDir, blendFactor)); +} + +std::vector LightingManager::findLightVolumes(const glm::vec3& playerPos, uint32_t mapId) const { + auto it = lightVolumesByMap_.find(mapId); + if (it == lightVolumesByMap_.end()) { + return {}; + } + + const std::vector& volumes = it->second; + if (volumes.empty()) { + return {}; + } + + // Collect all volumes with weight > 0 + std::vector weighted; + weighted.reserve(volumes.size()); + + for (const auto& volume : volumes) { + // Apply coordinate scaling (test with 1.0f, try 36.0f if distances are off) + glm::vec3 scaledPos = volume.position * LIGHT_COORD_SCALE; + float dist = glm::length(playerPos - scaledPos); + + float weight = 0.0f; + if (dist <= volume.innerRadius) { + // Inside inner radius: full weight + weight = 1.0f; + } else if (dist < volume.outerRadius) { + // Between inner and outer: fade out with smoothstep + float t = (dist - volume.innerRadius) / (volume.outerRadius - volume.innerRadius); + t = glm::clamp(t, 0.0f, 1.0f); + weight = 1.0f - (t * t * (3.0f - 2.0f * t)); // Smoothstep + } + + if (weight > 0.0f) { + weighted.push_back({&volume, weight}); + + // Debug logging for first few volumes + if (weighted.size() <= 3) { + LOG_INFO("Light volume ", volume.lightId, ": dist=", dist, + " inner=", volume.innerRadius, " outer=", volume.outerRadius, + " weight=", weight); + } + } + } + + if (weighted.empty()) { + return {}; + } + + // Sort by weight descending + std::sort(weighted.begin(), weighted.end(), + [](const WeightedVolume& a, const WeightedVolume& b) { + return a.weight > b.weight; + }); + + // Keep top N volumes + if (weighted.size() > MAX_BLEND_VOLUMES) { + weighted.resize(MAX_BLEND_VOLUMES); + } + + // Normalize weights to sum to 1.0 + float totalWeight = 0.0f; + for (const auto& wv : weighted) { + totalWeight += wv.weight; + } + + if (totalWeight > 0.0f) { + for (auto& wv : weighted) { + wv.weight /= totalWeight; + } + } + + return weighted; +} + +uint32_t LightingManager::selectLightParamsId(const LightVolume* volume, bool isRaining, bool isUnderwater) const { + if (!volume) return 0; + + // Select appropriate LightParams based on conditions + if (isUnderwater && volume->lightParamsIdUnderwater != 0) { + return volume->lightParamsIdUnderwater; + } else if (isRaining && volume->lightParamsIdRain != 0) { + return volume->lightParamsIdRain; + } else { + return volume->lightParamsId; + } +} + +LightingParams LightingManager::sampleLightParams(const LightParamsProfile* profile, uint16_t timeHalfMinutes) const { + if (!profile) return fallbackParams_; + + LightingParams params; + + // Sample color bands + params.ambientColor = sampleColorBand(profile->colorBands[LightParamsProfile::AMBIENT_COLOR], timeHalfMinutes); + params.diffuseColor = sampleColorBand(profile->colorBands[LightParamsProfile::DIFFUSE_COLOR], timeHalfMinutes); + params.fogColor = sampleColorBand(profile->colorBands[LightParamsProfile::FOG_COLOR], timeHalfMinutes); + params.skyTopColor = sampleColorBand(profile->colorBands[LightParamsProfile::SKY_TOP_COLOR], timeHalfMinutes); + params.skyMiddleColor = sampleColorBand(profile->colorBands[LightParamsProfile::SKY_MIDDLE_COLOR], timeHalfMinutes); + params.skyBand1Color = sampleColorBand(profile->colorBands[LightParamsProfile::SKY_BAND1_COLOR], timeHalfMinutes); + params.skyBand2Color = sampleColorBand(profile->colorBands[LightParamsProfile::SKY_BAND2_COLOR], timeHalfMinutes); + + // Sample float bands + params.fogEnd = sampleFloatBand(profile->floatBands[LightParamsProfile::FOG_END], timeHalfMinutes); + float fogStartScalar = sampleFloatBand(profile->floatBands[LightParamsProfile::FOG_START_SCALAR], timeHalfMinutes); + params.fogStart = params.fogEnd * fogStartScalar; // Start is a scalar of end distance + params.fogDensity = sampleFloatBand(profile->floatBands[LightParamsProfile::FOG_DENSITY], timeHalfMinutes); + params.cloudDensity = sampleFloatBand(profile->floatBands[LightParamsProfile::CLOUD_DENSITY], timeHalfMinutes); + + // Debug logging for fog params (first few samples only) + static int debugCount = 0; + if (debugCount < 3) { + LOG_INFO("Fog params: start=", params.fogStart, " end=", params.fogEnd, + " color=(", params.fogColor.r, ",", params.fogColor.g, ",", params.fogColor.b, ")"); + debugCount++; + } + + // Compute sun direction from time + float angle = (timeHalfMinutes / 2880.0f) * 2.0f * 3.14159f; + params.directionalDir = glm::normalize(glm::vec3( + std::sin(angle) * 0.6f, + -0.6f + std::cos(angle) * 0.4f, + std::cos(angle) * 0.6f + )); + + return params; +} + +glm::vec3 LightingManager::sampleColorBand(const ColorBand& band, uint16_t timeHalfMinutes) const { + if (band.numKeyframes == 0) { + return glm::vec3(0.5f); // Fallback gray + } + + if (band.numKeyframes == 1) { + return band.colors[0]; // Single keyframe + } + + // Safer initialization: default to wrapping last→first + uint8_t idx1 = band.numKeyframes - 1; + uint8_t idx2 = 0; + + // Find surrounding keyframes + for (uint8_t i = 0; i < band.numKeyframes; ++i) { + if (timeHalfMinutes < band.times[i]) { + idx2 = i; + idx1 = (i > 0) ? (i - 1) : (band.numKeyframes - 1); // Wrap to last + break; + } + } + + // Calculate interpolation factor + uint16_t t1 = band.times[idx1]; + uint16_t t2 = band.times[idx2]; + + // Handle midnight wrap + uint16_t timeSpan = (t2 > t1) ? (t2 - t1) : (2880 - t1 + t2); + uint16_t elapsed = (timeHalfMinutes >= t1) ? (timeHalfMinutes - t1) : (2880 - t1 + timeHalfMinutes); + + float t = (timeSpan > 0) ? (static_cast(elapsed) / static_cast(timeSpan)) : 0.0f; + t = glm::clamp(t, 0.0f, 1.0f); + + // Linear interpolation + return glm::mix(band.colors[idx1], band.colors[idx2], t); +} + +float LightingManager::sampleFloatBand(const FloatBand& band, uint16_t timeHalfMinutes) const { + if (band.numKeyframes == 0) { + return 1.0f; // Fallback + } + + if (band.numKeyframes == 1) { + return band.values[0]; + } + + // Safer initialization: default to wrapping last→first + uint8_t idx1 = band.numKeyframes - 1; + uint8_t idx2 = 0; + + // Find surrounding keyframes + for (uint8_t i = 0; i < band.numKeyframes; ++i) { + if (timeHalfMinutes < band.times[i]) { + idx2 = i; + idx1 = (i > 0) ? (i - 1) : (band.numKeyframes - 1); + break; + } + } + + uint16_t t1 = band.times[idx1]; + uint16_t t2 = band.times[idx2]; + + uint16_t timeSpan = (t2 > t1) ? (t2 - t1) : (2880 - t1 + t2); + uint16_t elapsed = (timeHalfMinutes >= t1) ? (timeHalfMinutes - t1) : (2880 - t1 + timeHalfMinutes); + + float t = (timeSpan > 0) ? (static_cast(elapsed) / static_cast(timeSpan)) : 0.0f; + t = glm::clamp(t, 0.0f, 1.0f); + + return glm::mix(band.values[idx1], band.values[idx2], t); +} + +glm::vec3 LightingManager::dbcColorToVec3(uint32_t dbcColor) const { + // DBC colors are stored as BGR (0x00BBGGRR on little-endian) + uint8_t b = (dbcColor >> 16) & 0xFF; + uint8_t g = (dbcColor >> 8) & 0xFF; + uint8_t r = dbcColor & 0xFF; + + return glm::vec3(r / 255.0f, g / 255.0f, b / 255.0f); +} + +} // namespace rendering +} // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e0377c9f..ce7cb8a2 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -3010,15 +3010,14 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { } if (activeCount == 0) return; - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; auto* assetMgr = core::Application::getInstance().getAssetManager(); - // Position below the minimap (minimap is 200px + 10px margin from top-right) + // Position below the player frame in top-left constexpr float ICON_SIZE = 32.0f; constexpr int ICONS_PER_ROW = 8; float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f; - ImGui::SetNextWindowPos(ImVec2(screenW - barW - 10.0f, 220.0f), ImGuiCond_Always); + // Dock under player frame in top-left (player frame is at 10, 30 with ~110px height) + ImGui::SetNextWindowPos(ImVec2(10.0f, 145.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |