#include "rendering/lighting_manager.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_layout.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. const auto* activeLayout = pipeline::getActiveDBCLayout(); const auto* lL = activeLayout ? activeLayout->getLayout("Light") : nullptr; for (uint32_t i = 0; i < recordCount; ++i) { LightVolume volume; volume.lightId = dbc->getUInt32(i, lL ? (*lL)["ID"] : 0); volume.mapId = dbc->getUInt32(i, lL ? (*lL)["MapID"] : 1); // Position (note: DBC stores as x,z,y - need to swap!) float x = dbc->getFloat(i, lL ? (*lL)["X"] : 2); float z = dbc->getFloat(i, lL ? (*lL)["Z"] : 3); float y = dbc->getFloat(i, lL ? (*lL)["Y"] : 4); volume.position = glm::vec3(x, y, z); // Convert to x,y,z volume.innerRadius = dbc->getFloat(i, lL ? (*lL)["InnerRadius"] : 5); volume.outerRadius = dbc->getFloat(i, lL ? (*lL)["OuterRadius"] : 6); // LightParams IDs for different conditions volume.lightParamsId = dbc->getUInt32(i, lL ? (*lL)["LightParamsID"] : 7); if (dbc->getFieldCount() > 8) { volume.lightParamsIdRain = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDRain"] : 8); } if (dbc->getFieldCount() > 9) { volume.lightParamsIdUnderwater = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDUnderwater"] : 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) const auto* lpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightParams") : nullptr; for (uint32_t i = 0; i < recordCount; ++i) { uint32_t paramId = dbc->getUInt32(i, lpL ? (*lpL)["LightParamsID"] : 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 const auto* libL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightIntBand") : nullptr; for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { uint32_t blockIndex = dbc->getUInt32(i, libL ? (*libL)["BlockIndex"] : 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, libL ? (*libL)["NumKeyframes"] : 2); if (band.numKeyframes > 16) band.numKeyframes = 16; // Read time keys (field 3-18) - stored as uint16 half-minutes uint32_t timeKeyBase = libL ? (*libL)["TimeKey0"] : 3; for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k); band.times[k] = static_cast(timeValue % 2880); // Clamp to valid range } // Read color values (field 19-34) - stored as BGRA packed uint32 uint32_t valueBase = libL ? (*libL)["Value0"] : 19; for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { uint32_t colorBGRA = dbc->getUInt32(i, valueBase + 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 const auto* lfbL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightFloatBand") : nullptr; for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { uint32_t blockIndex = dbc->getUInt32(i, lfbL ? (*lfbL)["BlockIndex"] : 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, lfbL ? (*lfbL)["NumKeyframes"] : 2); if (band.numKeyframes > 16) band.numKeyframes = 16; // Read time keys (field 3-18) uint32_t timeKeyBase = lfbL ? (*lfbL)["TimeKey0"] : 3; for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k); band.times[k] = static_cast(timeValue % 2880); // Clamp to valid range } // Read float values (field 19-34) uint32_t valueBase = lfbL ? (*lfbL)["Value0"] : 19; for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) { band.values[k] = dbc->getFloat(i, valueBase + 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