Implement WoW 3.3.5a DBC-driven lighting system

Add complete Blizzard-style time-of-day lighting pipeline:

Spatial Volume System (Light.dbc):
- Light volumes with position + inner/outer radius
- Distance-based weighting with smoothstep falloff
- Multi-volume blending (top 2 with normalized weights)
- X,Z,Y coordinate handling + LIGHT_COORD_SCALE for ×36 quirk
- Smooth zone transitions without popping

Profile Selection (LightParams.dbc):
- Weather variants: clear/rain/underwater
- Links to 18 color + 6 float band curves per profile
- Block indexing: LightParamsID × 18/6 + channel

Time-of-Day Band Sampling (LightIntBand/LightFloatBand):
- Half-minutes format (0-2879) with time clamping
- Keyframe interpolation with midnight wrap
- Wrap-safe initialization for edge cases
- BGR color unpacking

Multi-Volume Blending:
- Weighted sum of all lighting params
- Proper direction blending: normalize(sum(dir × weight))
- Blends ambient, diffuse, fog, sky, cloud density

Temporal Smoothing:
- Exponential blend to prevent frame snapping
- Smooths ALL parameters (colors, fog, direction, sky)

Game Time Support:
- Accepts server-sent game time (WoW standard)
- Falls back to local time if not provided
- Manual override for testing

Debug Features:
- Volume distance/weight logging
- Fog params logging
- Coordinate scale verification

Also: Move buff bar to top-left under player frame
This commit is contained in:
Kelsi 2026-02-10 13:44:22 -08:00
parent 3c13cf4b12
commit 69fa4c6e03
4 changed files with 819 additions and 4 deletions

View file

@ -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

View file

@ -0,0 +1,263 @@
#pragma once
#include <vector>
#include <map>
#include <memory>
#include <glm/glm.hpp>
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 zonelight 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<WeightedVolume> 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<uint32_t, std::vector<LightVolume>> lightVolumesByMap_;
// LightParams profiles by ID
std::map<uint32_t, LightParamsProfile> lightParamsProfiles_;
// Current state
LightingParams currentParams_;
LightingParams targetParams_; // For smooth blending
std::vector<WeightedVolume> 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

View file

@ -0,0 +1,552 @@
#include "rendering/lighting_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
#include <ctime>
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<pipeline::DBCFile>();
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<pipeline::DBCFile>();
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<pipeline::DBCFile>();
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<uint16_t>(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<pipeline::DBCFile>();
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<uint16_t>(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<uint16_t>(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::WeightedVolume> LightingManager::findLightVolumes(const glm::vec3& playerPos, uint32_t mapId) const {
auto it = lightVolumesByMap_.find(mapId);
if (it == lightVolumesByMap_.end()) {
return {};
}
const std::vector<LightVolume>& volumes = it->second;
if (volumes.empty()) {
return {};
}
// Collect all volumes with weight > 0
std::vector<WeightedVolume> 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<float>(elapsed) / static_cast<float>(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<float>(elapsed) / static_cast<float>(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

View file

@ -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<float>(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 |