Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies

Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.

Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)

Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)

Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)

Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions

Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
This commit is contained in:
Kelsi 2026-02-10 14:36:17 -08:00
parent 159a434c60
commit 8e60d0e781
16 changed files with 1036 additions and 47 deletions

View file

@ -143,6 +143,7 @@ set(WOWEE_SOURCES
src/rendering/weather.cpp src/rendering/weather.cpp
src/rendering/lightning.cpp src/rendering/lightning.cpp
src/rendering/lighting_manager.cpp src/rendering/lighting_manager.cpp
src/rendering/sky_system.cpp
src/rendering/character_renderer.cpp src/rendering/character_renderer.cpp
src/rendering/character_preview.cpp src/rendering/character_preview.cpp
src/rendering/wmo_renderer.cpp src/rendering/wmo_renderer.cpp

View file

@ -9,10 +9,12 @@ A native C++ client for World of Warcraft 3.3.5a (Wrath of the Lich King) with a
### Rendering Engine ### Rendering Engine
- **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling - **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling
- **Water** -- Animated surfaces, reflections, refractions, Fresnel effect - **Water** -- Animated surfaces, reflections, refractions, Fresnel effect
- **Sky** -- Dynamic day/night cycle, sun/moon with orbital movement - **Sky System** -- WoW-accurate DBC-driven lighting with skybox authority
- **Stars** -- 1000+ procedurally placed stars (night-only) - **Skybox** -- Camera-locked celestial sphere (M2 model support, gradient fallback)
- **Atmosphere** -- Procedural clouds (FBM noise), lens flare with chromatic aberration - **Celestial Bodies** -- Sun (lighting-driven), White Lady + Blue Child (Azeroth's two moons)
- **Moon Phases** -- 8 realistic lunar phases with elliptical terminator - **Moon Phases** -- Server time-driven deterministic phases (30-day / 27-day cycles)
- **Stars** -- Baked into skybox assets (procedural fallback for development/debug only)
- **Atmosphere** -- Procedural clouds (FBM noise), lens flare with chromatic aberration, cloud/fog star occlusion
- **Weather** -- Rain and snow particle systems (2000 particles, camera-relative) - **Weather** -- Rain and snow particle systems (2000 particles, camera-relative)
- **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures - **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures
- **Buildings** -- WMO renderer with multi-material batches, frustum culling, 160-unit distance culling - **Buildings** -- WMO renderer with multi-material batches, frustum culling, 160-unit distance culling
@ -124,8 +126,8 @@ make -j$(nproc)
| F1 | Performance HUD | | F1 | Performance HUD |
| F2 | Wireframe mode | | F2 | Wireframe mode |
| F9 | Toggle time progression | | F9 | Toggle time progression |
| F10 | Toggle sun/moon | | F10 | Toggle celestial bodies (sun + moons) |
| F11 | Toggle stars | | F11 | Toggle procedural stars (debug mode) |
| +/- | Change time of day | | +/- | Change time of day |
| C | Toggle clouds | | C | Toggle clouds |
| L | Toggle lens flare | | L | Toggle lens flare |
@ -145,6 +147,7 @@ make -j$(nproc)
- [Authentication](docs/authentication.md) -- SRP6 auth protocol details - [Authentication](docs/authentication.md) -- SRP6 auth protocol details
- [Server Setup](docs/server-setup.md) -- Local server configuration - [Server Setup](docs/server-setup.md) -- Local server configuration
- [Single Player](docs/single-player.md) -- Offline mode - [Single Player](docs/single-player.md) -- Offline mode
- [Sky System](docs/SKY_SYSTEM.md) -- Celestial bodies, Azeroth astronomy, and WoW-accurate sky rendering
- [SRP Implementation](docs/srp-implementation.md) -- Cryptographic details - [SRP Implementation](docs/srp-implementation.md) -- Cryptographic details
- [Packet Framing](docs/packet-framing.md) -- Network protocol framing - [Packet Framing](docs/packet-framing.md) -- Network protocol framing
- [Realm List](docs/realm-list.md) -- Realm selection system - [Realm List](docs/realm-list.md) -- Realm selection system
@ -158,6 +161,13 @@ make -j$(nproc)
- **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio) - **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio)
- **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol - **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol
- **Asset Loading**: Async terrain streaming, lazy loading, MPQ archive support - **Asset Loading**: Async terrain streaming, lazy loading, MPQ archive support
- **Sky System**: WoW-accurate DBC-driven architecture
- **Skybox Authority**: Stars baked into M2 sky dome models (not procedurally generated)
- **Lore-Accurate Moons**: White Lady (30-day cycle) + Blue Child (27-day cycle)
- **Deterministic Phases**: Computed from server game time (consistent across sessions)
- **Camera-Locked**: Sky dome uses rotation-only transform (translation ignored)
- **No Latitude Math**: Per-zone artistic constants, not Earth-like planetary simulation
- **Zone Identity**: Different skyboxes per continent (Azeroth, Outland, Northrend)
## License ## License

422
docs/SKY_SYSTEM.md Normal file
View file

@ -0,0 +1,422 @@
# Sky System & Azeroth Astronomy
## Overview
The sky rendering system in wowee follows World of Warcraft's WotLK (3.3.5a) architecture, where skyboxes are **authoritative** and procedural elements serve as fallbacks only. This document explains the lore-accurate celestial system, implementation details, and critical anti-patterns to avoid.
---
## Architecture
### Component Hierarchy
```
SkySystem (coordinator)
├── Skybox (M2 model, AUTHORITATIVE - includes baked stars)
├── StarField (procedural, DEBUG/FALLBACK ONLY)
├── Celestial (sun + White Lady + Blue Child)
├── Clouds (atmospheric layer)
└── LensFlare (sun glow effect)
```
### Rendering Pipeline
```
LightingManager (DBC-driven)
↓ Light.dbc + LightParams.dbc + time-of-day bands
↓ produces: directionalDir, diffuseColor, skyColors, cloudDensity, fogDensity
SkyParams (interface struct)
↓ adds: gameTime, skyboxModelId, skyboxHasStars
SkySystem::render(camera, params)
├─→ Skybox first (far plane, camera-locked)
├─→ StarField (ONLY if debugMode OR skybox missing)
├─→ Celestial (sun + 2 moons, uses directionalDir + gameTime)
├─→ Clouds (atmospheric layer)
└─→ LensFlare (screen-space sun glow)
```
---
## Celestial Bodies (Lore)
### The Two Moons of Azeroth
Azeroth has **two moons** visible in the night sky, both significant to the world's lore:
#### **White Lady** (Primary Moon)
- **Appearance**: Larger, brighter, pale white color (RGB: 0.8, 0.85, 1.0)
- **Size**: 40-unit diameter billboard
- **Intensity**: Full brightness (1.0)
- **Lore**: Tied to Elune, the Night Elf moon goddess
- **Cycle**: 30 game days per phase cycle (~12 real-world hours)
#### **Blue Child** (Secondary Moon)
- **Appearance**: Smaller, dimmer, pale blue color (RGB: 0.7, 0.8, 1.0)
- **Size**: 30-unit diameter billboard
- **Intensity**: 70% of White Lady's brightness
- **Position**: Offset to the right and slightly lower (+80 X, -40 Z)
- **Cycle**: 27 game days per phase cycle (~10.8 real-world hours, slightly faster)
#### **Visibility**
- **Night hours**: 19:00 - 5:00 (both moons visible)
- **Fade transitions**:
- Fade in: 19:00 - 21:00 (dusk)
- Full intensity: 21:00 - 3:00 (night)
- Fade out: 3:00 - 5:00 (dawn)
- **Day hours**: 5:00 - 19:00 (moons not rendered)
### The Sun
- **Positioning**: Driven by `LightingManager::directionalDir`
- Placement: `sunPosition = -directionalDir * 800` (light comes FROM sun)
- Fallback: Time-based arc if no lighting manager (sunrise 6:00, peak 12:00, sunset 18:00)
- **Color**: Uses `LightingManager::diffuseColor` (DBC-driven, changes with time-of-day)
- Sunrise/sunset: Orange/red tones
- Midday: Bright yellow-white
- **Size**: 50-unit diameter billboard
- **Visibility**: 5:00 - 19:00 with intensity fade at transitions
---
## Deterministic Moon Phases
### Server Time-Driven (NOT deltaTime)
Moon phases are computed from **server game time**, ensuring:
- ✅ **Deterministic**: Same game time always produces same moon phases
- ✅ **Persistent**: Phases consistent across sessions and server restarts
- ✅ **Lore-feeling**: Realistic cycles tied to game world time, not arbitrary timers
### Calculation Formula
```cpp
float computePhaseFromGameTime(float gameTime, float cycleDays) {
constexpr float SECONDS_PER_GAME_DAY = 1440.0f; // 1 game day = 24 real minutes
float gameDays = gameTime / SECONDS_PER_GAME_DAY;
float phase = fmod(gameDays / cycleDays, 1.0f);
return (phase < 0.0f) ? phase + 1.0f : phase; // Ensure positive
}
// Applied per moon
whiteLadyPhase = computePhaseFromGameTime(gameTime, 30.0f); // 30 game days
blueChildPhase = computePhaseFromGameTime(gameTime, 27.0f); // 27 game days
```
### Phase Representation
- **Value range**: 0.0 - 1.0
- `0.0` = New moon (dark)
- `0.25` = First quarter (right half lit)
- `0.5` = Full moon (fully lit)
- `0.75` = Last quarter (left half lit)
- `1.0` = New moon (wraps to 0.0)
- **Shader-driven**: Phase uniform passed to fragment shader for crescent/gibbous rendering
### Fallback Mode (Development)
If `gameTime < 0.0` (server time unavailable):
- Uses deltaTime accumulator: `moonPhaseTimer += deltaTime`
- Short cycle durations (4 minutes / 3.5 minutes) for quick testing
- **NOT used in production**: Should always use server time
---
## Sky Dome Rendering
### Camera-Locked Behavior (WoW Standard)
```cpp
// Vertex shader transformation
mat4 viewNoTranslation = mat4(mat3(view)); // Strip translation, keep rotation
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
gl_Position = gl_Position.xyww; // Force far plane depth
```
**Why this works:**
- ✅ **Translation ignored**: Sky centered on camera (doesn't "swim" when moving)
- ✅ **Rotation applied**: Sky follows camera look direction (feels "attached to view")
- ✅ **Far plane depth**: Always renders behind world geometry
- ✅ **Celestial sphere illusion**: Stars/sky appear infinitely distant
### Time-Based Sky Drift (Optional)
Subtle rotation for atmospheric effect:
```cpp
float skyYawRotation = gameTime * skyRotationRate;
skyDomeMatrix = rotate(skyDomeMatrix, skyYawRotation, vec3(0, 0, 1)); // Yaw only
```
**Per-zone rotation rates:**
- Azeroth continents: `0.00001` rad/sec (very slow, barely noticeable)
- Outland: `0.00005` rad/sec (faster, "weird" alien sky feel)
- Northrend: `0.00002` rad/sec (subtle drift, aurora-like)
- Static zones: `0.0` (no rotation)
**Implementation status:** Not yet active (waiting for M2 skybox loading)
---
## Critical Anti-Patterns
### ❌ DO NOT: Latitude-Based Star Rotation
**Why it's wrong:**
- Azeroth is **not modeled as a spherical planet** with latitude/longitude in WoW client
- No coherent coordinate system for Earth-style celestial mechanics
- Stars are part of **authored skybox M2 models**, not dynamically positioned
- Breaks zone identity (Duskwood's gloomy sky shouldn't behave like Barrens)
**What happens if you do it anyway:**
- Stars won't match Blizzard's authored skyboxes when M2 models load
- Lore/continuity breaks (geographically close zones with different star rotation)
- "Swimming" stars during movement
- Undermines the "WoW feel"
**Correct approach:**
```cpp
// ✅ Per-zone artistic constants (NOT geography)
struct SkyProfile {
float celestialTilt; // Artistic pitch/roll (Outland = 15°, Azeroth = 0°)
float skyYawOffset; // Alignment offset for authored skybox
float skyRotationRate; // Time-based drift (0 = static)
};
```
### ❌ DO NOT: Always Render Procedural Stars
**Why it's wrong:**
- Skyboxes contain **baked stars** as part of zone mood/identity
- Procedural stars over skybox stars = double stars, visual clash
- Different zones have dramatically different skies (Outland purple nebulae, Northrend auroras)
**Correct gating logic:**
```cpp
bool renderProceduralStars = false;
if (debugSkyMode) {
renderProceduralStars = true; // Debug: force for testing fog/cloud attenuation
} else if (proceduralStarsEnabled) {
renderProceduralStars = !params.skyboxHasStars; // Fallback ONLY if skybox missing
}
```
**skyboxHasStars flag:**
- Set to `true` when M2 skybox loaded and has star layer (query materials/textures)
- Set to `false` for gradient skybox (placeholder, no baked stars)
- Prevents procedural stars from "leaking" once real skyboxes load
### ❌ DO NOT: Universal Dual Moon Setup
**Why it's wrong:**
- Not all maps/continents have same celestial bodies
- Azeroth: White Lady + Blue Child (two moons)
- Outland: Different sky (alien world, broken planet)
- Forcing two moons everywhere breaks lore
**Correct approach:**
```cpp
struct SkyProfile {
bool dualMoons; // Azeroth = true, Outland = false
// ... other per-map settings
};
// In Celestial::render()
if (dualMoonMode_ && mapUsesAzerothSky) {
renderBlueChild(camera, timeOfDay);
}
```
---
## Integration Points
### SkyParams Struct (Interface)
```cpp
struct SkyParams {
// Sun/moon positioning
glm::vec3 directionalDir; // From LightingManager (sun direction)
glm::vec3 sunColor; // From LightingManager (DBC diffuse color)
// Sky colors (for skybox tinting/blending, future)
glm::vec3 skyTopColor;
glm::vec3 skyMiddleColor;
glm::vec3 skyBand1Color;
glm::vec3 skyBand2Color;
// Atmospheric effects (star/moon occlusion)
float cloudDensity; // 0-1, from LightingManager
float fogDensity; // 0-1, from LightingManager
float horizonGlow; // 0-1, atmospheric scattering
// Time
float timeOfDay; // 0-24 hours (for sun/moon visibility)
float gameTime; // Server time in seconds (for moon phases)
// Skybox control (future: LightSkybox.dbc)
uint32_t skyboxModelId; // Which M2 skybox to load
bool skyboxHasStars; // Does skybox include baked stars?
};
```
### Star Occlusion by Weather
Clouds and fog affect star visibility:
```cpp
// In StarField::render()
float intensity = getStarIntensity(timeOfDay); // Time-based (night = 1.0, day = 0.0)
intensity *= (1.0f - glm::clamp(cloudDensity * 0.7f, 0.0f, 1.0f)); // Heavy clouds hide stars
intensity *= (1.0f - glm::clamp(fogDensity * 0.3f, 0.0f, 1.0f)); // Fog dims stars
if (intensity <= 0.01f) {
return; // Don't render invisible stars
}
```
**Result:** Cloudy/foggy nights have fewer visible stars (realistic behavior)
---
## Future: M2 Skybox System
### LightSkybox.dbc Integration
**DBC Chain:**
```
Light.dbc (spatial volumes)
↓ lightParamsId (per weather condition)
LightParams.dbc (profile mapping)
↓ skyboxId
LightSkybox.dbc (model paths)
↓ M2 model name
Environments\Stars\*.m2 (actual sky dome models)
```
**Skybox Loading Flow:**
1. Query `lightParamsId` from active light volume(s)
2. Look up `skyboxId` in LightParams.dbc
3. Load M2 model path from LightSkybox.dbc
4. Load/cache M2 skybox model
5. Query model materials → set `skyboxHasStars = true` if star textures found
6. Render skybox, disable procedural stars
### Skybox Transition Blending
**Problem:** Hard swaps between skyboxes at zone boundaries look bad
**Solution:** Blend skyboxes using same volume weighting as lighting:
```cpp
// In SkySystem::render()
if (activeVolumes.size() >= 2) {
// Render primary skybox
skybox1->render(camera, alpha = volumes[0].weight);
// Blend in secondary skybox
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
skybox2->render(camera, alpha = volumes[1].weight);
glDisable(GL_BLEND);
}
```
**Result:** Smooth crossfade between zone skies, no popping
### SkyProfile Configuration
**Per-map/continent settings:**
```cpp
std::map<uint32_t, SkyProfile> skyProfiles = {
// Azeroth (Eastern Kingdoms)
{0, {
.skyboxModelId = 123,
.celestialTilt = 0.0f, // No tilt, standard orientation
.skyYawOffset = 0.0f,
.skyRotationRate = 0.00001f, // Very slow drift
.dualMoons = true // White Lady + Blue Child
}},
// Kalimdor
{1, {
.skyboxModelId = 124,
.celestialTilt = 0.0f,
.skyYawOffset = 0.0f,
.skyRotationRate = 0.00001f,
.dualMoons = true
}},
// Outland (Burning Crusade)
{530, {
.skyboxModelId = 456,
.celestialTilt = 15.0f, // Tilted, alien feel
.skyYawOffset = 45.0f, // Rotated alignment
.skyRotationRate = 0.00005f, // Faster, "weird" drift
.dualMoons = false // Different celestial setup
}},
// Northrend (Wrath of the Lich King)
{571, {
.skyboxModelId = 789,
.celestialTilt = 0.0f,
.skyYawOffset = 0.0f,
.skyRotationRate = 0.00002f, // Subtle aurora-like drift
.dualMoons = true
}}
};
```
---
## Implementation Checklist
### ✅ Completed
- [x] SkySystem coordinator class
- [x] Skybox camera-locked rendering (translation ignored)
- [x] Procedural stars gated by `skyboxHasStars` flag
- [x] Two moons (White Lady + Blue Child) with independent phases
- [x] Deterministic moon phases from server `gameTime`
- [x] Sun positioning from lighting `directionalDir`
- [x] Star occlusion by cloud/fog density
- [x] SkyParams interface for lighting integration
### 🚧 Future Enhancements
- [ ] Load M2 skybox models (parse LightSkybox.dbc)
- [ ] Query M2 materials to detect baked stars
- [ ] Skybox transition blending (weighted crossfade)
- [ ] SkyProfile per map/continent
- [ ] Time-based sky rotation (optional drift)
- [ ] Moon position from shared sky arc system (not fixed offsets)
- [ ] Support for zone-specific celestial setups (Outland, etc.)
---
## Code References
**Key Files:**
- `include/rendering/sky_system.hpp` - Coordinator, SkyParams struct
- `src/rendering/sky_system.cpp` - Render pipeline, star gating logic
- `include/rendering/celestial.hpp` - Sun + dual moon system
- `src/rendering/celestial.cpp` - Moon phase calculations, rendering
- `include/rendering/starfield.hpp` - Procedural star fallback
- `src/rendering/starfield.cpp` - Star intensity + occlusion
- `include/rendering/skybox.hpp` - Camera-locked sky dome
- `src/rendering/skybox.cpp` - Sky dome vertex shader
**Integration Points:**
- `src/rendering/renderer.cpp` - Populates SkyParams from LightingManager
- `include/rendering/lighting_manager.hpp` - Provides directionalDir, colors, fog/cloud
---
## References
- **WoW 3.3.5a Client**: Environments\Stars\*.m2 (skybox models)
- **DBC Files**: Light.dbc, LightParams.dbc, LightSkybox.dbc, LightIntBand.dbc, LightFloatBand.dbc
- **WoWDev Wiki**: https://wowdev.wiki/Light.dbc (lighting system documentation)
- **Lore Sources**:
- White Lady / Blue Child: https://wowpedia.fandom.com/wiki/Moon
- Elune connection: https://wowpedia.fandom.com/wiki/Elune

View file

@ -27,8 +27,14 @@ public:
* Render celestial bodies (sun and moon) * Render celestial bodies (sun and moon)
* @param camera Camera for view matrix * @param camera Camera for view matrix
* @param timeOfDay Time of day in hours (0-24) * @param timeOfDay Time of day in hours (0-24)
* @param sunDir Optional sun direction from lighting system (normalized)
* @param sunColor Optional sun color from lighting system
* @param gameTime Optional server game time in seconds (for deterministic moon phases)
*/ */
void render(const Camera& camera, float timeOfDay); void render(const Camera& camera, float timeOfDay,
const glm::vec3* sunDir = nullptr,
const glm::vec3* sunColor = nullptr,
float gameTime = -1.0f);
/** /**
* Enable/disable celestial rendering * Enable/disable celestial rendering
@ -42,10 +48,16 @@ public:
void update(float deltaTime); void update(float deltaTime);
/** /**
* Set moon phase (0.0 = new moon, 0.25 = first quarter, 0.5 = full, 0.75 = last quarter, 1.0 = new) * Set White Lady phase (primary moon, 0.0 = new, 0.5 = full, 1.0 = new)
*/ */
void setMoonPhase(float phase); void setMoonPhase(float phase);
float getMoonPhase() const { return moonPhase; } float getMoonPhase() const { return whiteLadyPhase_; }
/**
* Set Blue Child phase (secondary moon, 0.0 = new, 0.5 = full, 1.0 = new)
*/
void setBlueChildPhase(float phase);
float getBlueChildPhase() const { return blueChildPhase_; }
/** /**
* Enable/disable automatic moon phase cycling * Enable/disable automatic moon phase cycling
@ -53,6 +65,12 @@ public:
void setMoonPhaseCycling(bool enabled) { moonPhaseCycling = enabled; } void setMoonPhaseCycling(bool enabled) { moonPhaseCycling = enabled; }
bool isMoonPhaseCycling() const { return moonPhaseCycling; } bool isMoonPhaseCycling() const { return moonPhaseCycling; }
/**
* Enable/disable two-moon rendering (White Lady + Blue Child)
*/
void setDualMoonMode(bool enabled) { dualMoonMode_ = enabled; }
bool isDualMoonMode() const { return dualMoonMode_; }
/** /**
* Get sun position in world space * Get sun position in world space
*/ */
@ -77,11 +95,27 @@ private:
void createCelestialQuad(); void createCelestialQuad();
void destroyCelestialQuad(); void destroyCelestialQuad();
void renderSun(const Camera& camera, float timeOfDay); void renderSun(const Camera& camera, float timeOfDay,
void renderMoon(const Camera& camera, float timeOfDay); const glm::vec3* sunDir = nullptr,
const glm::vec3* sunColor = nullptr);
void renderMoon(const Camera& camera, float timeOfDay); // White Lady (primary)
void renderBlueChild(const Camera& camera, float timeOfDay); // Blue Child (secondary)
float calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const; float calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const;
/**
* Compute moon phase from game time (deterministic)
* @param gameTime Server game time in seconds
* @param cycleDays Lunar cycle length in game days
* @return Phase 0.0-1.0 (0=new, 0.5=full, 1.0=new)
*/
float computePhaseFromGameTime(float gameTime, float cycleDays) const;
/**
* Update moon phases from game time (server-driven)
*/
void updatePhasesFromGameTime(float gameTime);
std::unique_ptr<Shader> celestialShader; std::unique_ptr<Shader> celestialShader;
uint32_t vao = 0; uint32_t vao = 0;
@ -90,11 +124,18 @@ private:
bool renderingEnabled = true; bool renderingEnabled = true;
// Moon phase system // Moon phase system (two moons in Azeroth lore)
float moonPhase = 0.5f; // 0.0-1.0 (0=new, 0.5=full) float whiteLadyPhase_ = 0.5f; // 0.0-1.0 (0=new, 0.5=full) - primary moon
float blueChildPhase_ = 0.25f; // 0.0-1.0 (0=new, 0.5=full) - secondary moon
bool moonPhaseCycling = true; bool moonPhaseCycling = true;
float moonPhaseTimer = 0.0f; float moonPhaseTimer = 0.0f; // Fallback for deltaTime mode (development)
static constexpr float MOON_CYCLE_DURATION = 240.0f; // 4 minutes for full cycle bool dualMoonMode_ = true; // Default: render both moons (Azeroth-specific)
// WoW lunar cycle constants (in game days)
// WoW day = 24 real minutes, so these are ~realistic game-world cycles
static constexpr float WHITE_LADY_CYCLE_DAYS = 30.0f; // ~12 real hours for full cycle
static constexpr float BLUE_CHILD_CYCLE_DAYS = 27.0f; // ~10.8 real hours (slightly faster)
static constexpr float MOON_CYCLE_DURATION = 240.0f; // Fallback: 4 minutes (deltaTime mode)
}; };
} // namespace rendering } // namespace rendering

View file

@ -88,6 +88,13 @@ public:
fogColor = color; fogStart = start; fogEnd = end; fogColor = color; fogStart = start; fogEnd = end;
} }
void setLighting(const float lightDirIn[3], const float lightColorIn[3],
const float ambientColorIn[3]) {
lightDir = glm::vec3(lightDirIn[0], lightDirIn[1], lightDirIn[2]);
lightColor = glm::vec3(lightColorIn[0], lightColorIn[1], lightColorIn[2]);
ambientColor = glm::vec3(ambientColorIn[0], ambientColorIn[1], ambientColorIn[2]);
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) { void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true; shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
} }
@ -206,6 +213,11 @@ private:
float fogStart = 400.0f; float fogStart = 400.0f;
float fogEnd = 1200.0f; float fogEnd = 1200.0f;
// Lighting parameters
glm::vec3 lightDir = glm::vec3(0.0f, -1.0f, 0.3f);
glm::vec3 lightColor = glm::vec3(1.5f, 1.4f, 1.3f);
glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f);
// Shadow mapping // Shadow mapping
GLuint shadowDepthTex = 0; GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);

View file

@ -307,6 +307,13 @@ public:
fogColor = color; fogStart = start; fogEnd = end; fogColor = color; fogStart = start; fogEnd = end;
} }
void setLighting(const float lightDirIn[3], const float lightColorIn[3],
const float ambientColorIn[3]) {
lightDir = glm::vec3(lightDirIn[0], lightDirIn[1], lightDirIn[2]);
lightColor = glm::vec3(lightColorIn[0], lightColorIn[1], lightColorIn[2]);
ambientColor = glm::vec3(ambientColorIn[0], ambientColorIn[1], ambientColorIn[2]);
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) { void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true; shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
} }
@ -334,6 +341,7 @@ private:
// Lighting uniforms // Lighting uniforms
glm::vec3 lightDir = glm::vec3(0.5f, 0.5f, 1.0f); glm::vec3 lightDir = glm::vec3(0.5f, 0.5f, 1.0f);
glm::vec3 lightColor = glm::vec3(1.5f, 1.4f, 1.3f);
glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f); glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f);
// Fog parameters // Fog parameters

View file

@ -0,0 +1,131 @@
#pragma once
#include <memory>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering {
class Camera;
class Skybox;
class Celestial;
class StarField;
class Clouds;
class LensFlare;
class LightingManager;
/**
* Sky rendering parameters (extracted from LightingManager)
*/
struct SkyParams {
// Sun/moon positioning
glm::vec3 directionalDir{0.0f, -1.0f, 0.3f};
glm::vec3 sunColor{1.0f, 1.0f, 0.9f};
// Sky colors (for skybox tinting/blending)
glm::vec3 skyTopColor{0.5f, 0.7f, 1.0f};
glm::vec3 skyMiddleColor{0.7f, 0.85f, 1.0f};
glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f};
glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f};
// Atmospheric effects
float cloudDensity = 0.0f; // 0-1
float fogDensity = 0.0f; // 0-1
float horizonGlow = 0.3f; // 0-1
// Time
float timeOfDay = 12.0f; // 0-24 hours
float gameTime = -1.0f; // Server game time in seconds (-1 = use fallback)
// Skybox selection (future: from LightSkybox.dbc)
uint32_t skyboxModelId = 0;
bool skyboxHasStars = false; // Does loaded skybox include baked stars?
};
/**
* Unified sky rendering system
*
* Coordinates skybox (authoritative), celestial bodies (sun + 2 moons),
* and fallback procedural stars. Driven by lighting system data.
*
* Architecture:
* - Skybox is PRIMARY (includes baked stars from M2 models)
* - Celestial renders sun + White Lady + Blue Child
* - StarField is DEBUG/FALLBACK only (disabled when skybox has stars)
*/
class SkySystem {
public:
SkySystem();
~SkySystem();
/**
* Initialize sky system components
*/
bool initialize();
void shutdown();
/**
* Update sky system (time, moon phases, etc.)
*/
void update(float deltaTime);
/**
* Render complete sky
* @param camera Camera for view/projection
* @param params Sky parameters from lighting system
*/
void render(const Camera& camera, const SkyParams& params);
/**
* Enable/disable procedural stars (DEBUG/FALLBACK)
* Default: OFF (stars come from skybox)
*/
void setProceduralStarsEnabled(bool enabled) { proceduralStarsEnabled_ = enabled; }
bool isProceduralStarsEnabled() const { return proceduralStarsEnabled_; }
/**
* Enable/disable debug sky mode (forces procedural stars even with skybox)
*/
void setDebugSkyMode(bool enabled) { debugSkyMode_ = enabled; }
bool isDebugSkyMode() const { return debugSkyMode_; }
/**
* Get sun position in world space (for lens flare, shadows, etc.)
*/
glm::vec3 getSunPosition(const SkyParams& params) const;
/**
* Enable/disable moon phase cycling
*/
void setMoonPhaseCycling(bool enabled);
/**
* Set moon phases manually (0.0-1.0 each)
*/
void setWhiteLadyPhase(float phase);
void setBlueChildPhase(float phase);
float getWhiteLadyPhase() const;
float getBlueChildPhase() const;
// Component accessors (for direct control if needed)
Skybox* getSkybox() const { return skybox_.get(); }
Celestial* getCelestial() const { return celestial_.get(); }
StarField* getStarField() const { return starField_.get(); }
Clouds* getClouds() const { return clouds_.get(); }
LensFlare* getLensFlare() const { return lensFlare_.get(); }
private:
std::unique_ptr<Skybox> skybox_; // Authoritative sky (gradient now, M2 models later)
std::unique_ptr<Celestial> celestial_; // Sun + 2 moons
std::unique_ptr<StarField> starField_; // Fallback procedural stars
std::unique_ptr<Clouds> clouds_; // Cloud layer
std::unique_ptr<LensFlare> lensFlare_; // Sun lens flare
bool proceduralStarsEnabled_ = false; // Default: OFF (skybox is authoritative)
bool debugSkyMode_ = false; // Force procedural stars for debugging
bool initialized_ = false;
};
} // namespace rendering
} // namespace wowee

View file

@ -28,8 +28,11 @@ public:
* Render the star field * Render the star field
* @param camera Camera for view matrix * @param camera Camera for view matrix
* @param timeOfDay Time of day in hours (0-24) * @param timeOfDay Time of day in hours (0-24)
* @param cloudDensity Optional cloud density from lighting (0-1, reduces star visibility)
* @param fogDensity Optional fog density from lighting (reduces star visibility)
*/ */
void render(const Camera& camera, float timeOfDay); void render(const Camera& camera, float timeOfDay,
float cloudDensity = 0.0f, float fogDensity = 0.0f);
/** /**
* Update star twinkle animation * Update star twinkle animation

View file

@ -179,6 +179,9 @@ public:
fogColor = color; fogStart = start; fogEnd = end; fogColor = color; fogStart = start; fogEnd = end;
} }
void setLighting(const float lightDir[3], const float lightColor[3],
const float ambientColor[3]);
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) { void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true; shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
} }
@ -537,6 +540,11 @@ private:
float fogStart = 3000.0f; // Increased to allow clearer visibility at distance float fogStart = 3000.0f; // Increased to allow clearer visibility at distance
float fogEnd = 4000.0f; // Increased to match extended view distance float fogEnd = 4000.0f; // Increased to match extended view distance
// Lighting parameters
float lightDir[3] = {-0.3f, -0.7f, -0.6f};
float lightColor[3] = {1.5f, 1.4f, 1.3f};
float ambientColor[3] = {0.55f, 0.55f, 0.6f};
// Shadow mapping // Shadow mapping
GLuint shadowDepthTex = 0; GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);

View file

@ -125,11 +125,17 @@ void Celestial::shutdown() {
celestialShader.reset(); celestialShader.reset();
} }
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) {
if (!renderingEnabled || vao == 0 || !celestialShader) { if (!renderingEnabled || vao == 0 || !celestialShader) {
return; return;
} }
// Update moon phases from game time if available (deterministic)
if (gameTime >= 0.0f) {
updatePhasesFromGameTime(gameTime);
}
// Enable blending for celestial glow // Enable blending for celestial glow
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@ -137,16 +143,21 @@ void Celestial::render(const Camera& camera, float timeOfDay) {
// Disable depth writing (but keep depth testing) // Disable depth writing (but keep depth testing)
glDepthMask(GL_FALSE); glDepthMask(GL_FALSE);
// Render sun and moon // Render sun and moons (pass lighting parameters)
renderSun(camera, timeOfDay); renderSun(camera, timeOfDay, sunDir, sunColor);
renderMoon(camera, timeOfDay); renderMoon(camera, timeOfDay); // White Lady (primary moon)
if (dualMoonMode_) {
renderBlueChild(camera, timeOfDay); // Blue Child (secondary moon)
}
// Restore state // Restore state
glDepthMask(GL_TRUE); glDepthMask(GL_TRUE);
glDisable(GL_BLEND); glDisable(GL_BLEND);
} }
void Celestial::renderSun(const Camera& camera, float timeOfDay) { void Celestial::renderSun(const Camera& camera, float timeOfDay,
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) {
return; return;
@ -154,8 +165,16 @@ void Celestial::renderSun(const Camera& camera, float timeOfDay) {
celestialShader->use(); celestialShader->use();
// Get sun position // Get sun position (use lighting direction if provided)
glm::vec3 sunPos = getSunPosition(timeOfDay); glm::vec3 sunPos;
if (sunDir) {
// Place sun along the lighting direction at far distance
const float sunDistance = 800.0f;
sunPos = -*sunDir * sunDistance; // Negative because light comes FROM sun
} else {
// Fallback to time-based position
sunPos = getSunPosition(timeOfDay);
}
// Create model matrix // Create model matrix
glm::mat4 model = glm::mat4(1.0f); glm::mat4 model = glm::mat4(1.0f);
@ -170,8 +189,8 @@ void Celestial::renderSun(const Camera& camera, float timeOfDay) {
celestialShader->setUniform("view", view); celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection); celestialShader->setUniform("projection", projection);
// Sun color and intensity // Sun color and intensity (use lighting color if provided)
glm::vec3 color = getSunColor(timeOfDay); glm::vec3 color = sunColor ? *sunColor : getSunColor(timeOfDay);
float intensity = getSunIntensity(timeOfDay); float intensity = getSunIntensity(timeOfDay);
celestialShader->setUniform("celestialColor", color); celestialShader->setUniform("celestialColor", color);
@ -224,7 +243,61 @@ void Celestial::renderMoon(const Camera& camera, float timeOfDay) {
celestialShader->setUniform("celestialColor", color); celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity); celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", moonPhase); celestialShader->setUniform("moonPhase", whiteLadyPhase_);
// Render quad
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
void Celestial::renderBlueChild(const Camera& camera, float timeOfDay) {
// Blue Child visible from 19:00 to 5:00 (night, same as White Lady)
if (timeOfDay >= 5.0f && timeOfDay < 19.0f) {
return;
}
celestialShader->use();
// Get moon position (offset slightly from White Lady)
glm::vec3 moonPos = getMoonPosition(timeOfDay);
// Offset Blue Child to the right and slightly lower
moonPos.x += 80.0f; // Right offset
moonPos.z -= 40.0f; // Slightly lower
// Create model matrix (smaller than White Lady)
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, moonPos);
model = glm::scale(model, glm::vec3(30.0f, 30.0f, 1.0f)); // 30 unit diameter (smaller)
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
celestialShader->setUniform("model", model);
celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection);
// Blue Child color (pale blue tint)
glm::vec3 color = glm::vec3(0.7f, 0.8f, 1.0f);
// Fade in/out at transitions (same as White Lady)
float intensity = 1.0f;
if (timeOfDay >= 19.0f && timeOfDay < 21.0f) {
// Fade in (19:00-21:00)
intensity = (timeOfDay - 19.0f) / 2.0f;
}
else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) {
// Fade out (3:00-5:00)
intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f;
}
// Blue Child is dimmer than White Lady
intensity *= 0.7f;
celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", blueChildPhase_);
// Render quad // Render quad
glBindVertexArray(vao); glBindVertexArray(vao);
@ -396,16 +469,49 @@ void Celestial::update(float deltaTime) {
// Update moon phase timer // Update moon phase timer
moonPhaseTimer += deltaTime; moonPhaseTimer += deltaTime;
// Moon completes full cycle in MOON_CYCLE_DURATION seconds // White Lady completes full cycle in MOON_CYCLE_DURATION seconds
moonPhase = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f); whiteLadyPhase_ = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f);
// Blue Child has a different cycle rate (slightly faster, 3.5 minutes)
constexpr float BLUE_CHILD_CYCLE = 210.0f;
blueChildPhase_ = std::fmod(moonPhaseTimer / BLUE_CHILD_CYCLE, 1.0f);
} }
void Celestial::setMoonPhase(float phase) { void Celestial::setMoonPhase(float phase) {
// Clamp phase to 0.0-1.0 // Set White Lady phase (primary moon)
moonPhase = glm::clamp(phase, 0.0f, 1.0f); whiteLadyPhase_ = glm::clamp(phase, 0.0f, 1.0f);
// Update timer to match phase // Update timer to match White Lady phase
moonPhaseTimer = moonPhase * MOON_CYCLE_DURATION; moonPhaseTimer = whiteLadyPhase_ * MOON_CYCLE_DURATION;
}
void Celestial::setBlueChildPhase(float phase) {
// Set Blue Child phase (secondary moon)
blueChildPhase_ = glm::clamp(phase, 0.0f, 1.0f);
}
float Celestial::computePhaseFromGameTime(float gameTime, float cycleDays) const {
// WoW game time: 1 game day = 24 real minutes = 1440 seconds
constexpr float SECONDS_PER_GAME_DAY = 1440.0f;
// Convert game time to game days
float gameDays = gameTime / SECONDS_PER_GAME_DAY;
// Compute phase as fraction of lunar cycle (0.0-1.0)
float phase = std::fmod(gameDays / cycleDays, 1.0f);
// Ensure positive (fmod can return negative for negative input)
if (phase < 0.0f) {
phase += 1.0f;
}
return phase;
}
void Celestial::updatePhasesFromGameTime(float gameTime) {
// Compute deterministic phases from server game time
whiteLadyPhase_ = computePhaseFromGameTime(gameTime, WHITE_LADY_CYCLE_DAYS);
blueChildPhase_ = computePhaseFromGameTime(gameTime, BLUE_CHILD_CYCLE_DAYS);
} }
} // namespace rendering } // namespace rendering

View file

@ -1163,8 +1163,8 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
characterShader->use(); characterShader->use();
characterShader->setUniform("uView", view); characterShader->setUniform("uView", view);
characterShader->setUniform("uProjection", projection); characterShader->setUniform("uProjection", projection);
characterShader->setUniform("uLightDir", glm::vec3(0.0f, -1.0f, 0.3f)); characterShader->setUniform("uLightDir", lightDir);
characterShader->setUniform("uLightColor", glm::vec3(1.5f, 1.4f, 1.3f)); characterShader->setUniform("uLightColor", lightColor);
characterShader->setUniform("uSpecularIntensity", 0.5f); characterShader->setUniform("uSpecularIntensity", 0.5f);
characterShader->setUniform("uViewPos", camera.getPosition()); characterShader->setUniform("uViewPos", camera.getPosition());

View file

@ -1652,7 +1652,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
shader->setUniform("uView", view); shader->setUniform("uView", view);
shader->setUniform("uProjection", projection); shader->setUniform("uProjection", projection);
shader->setUniform("uLightDir", lightDir); shader->setUniform("uLightDir", lightDir);
shader->setUniform("uLightColor", glm::vec3(1.5f, 1.4f, 1.3f)); shader->setUniform("uLightColor", lightColor);
shader->setUniform("uSpecularIntensity", onTaxi_ ? 0.0f : 0.5f); // Disable specular during taxi for performance shader->setUniform("uSpecularIntensity", onTaxi_ ? 0.0f : 0.5f); // Disable specular during taxi for performance
shader->setUniform("uAmbientColor", ambientColor); shader->setUniform("uAmbientColor", ambientColor);
shader->setUniform("uViewPos", camera.getPosition()); shader->setUniform("uViewPos", camera.getPosition());

View file

@ -1764,14 +1764,27 @@ void Renderer::renderWorld(game::World* world) {
skybox->render(*camera, timeOfDay); skybox->render(*camera, timeOfDay);
} }
// Render stars after skybox // Get lighting parameters for celestial rendering
if (starField && camera) { const glm::vec3* sunDir = nullptr;
starField->render(*camera, timeOfDay); const glm::vec3* sunColor = nullptr;
float cloudDensity = 0.0f;
float fogDensity = 0.0f;
if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
sunDir = &lighting.directionalDir;
sunColor = &lighting.diffuseColor;
cloudDensity = lighting.cloudDensity;
fogDensity = lighting.fogDensity;
} }
// Render celestial bodies (sun/moon) after stars // Render stars after skybox (affected by cloud/fog density)
if (starField && camera) {
starField->render(*camera, timeOfDay, cloudDensity, fogDensity);
}
// Render celestial bodies (sun/moon) after stars (sun uses lighting direction/color)
if (celestial && camera) { if (celestial && camera) {
celestial->render(*camera, timeOfDay); celestial->render(*camera, timeOfDay, sunDir, sunColor);
} }
// Render clouds after celestial bodies // Render clouds after celestial bodies
@ -1781,12 +1794,43 @@ void Renderer::renderWorld(game::World* world) {
// Render lens flare (screen-space effect, render after celestial bodies) // Render lens flare (screen-space effect, render after celestial bodies)
if (lensFlare && camera && celestial) { if (lensFlare && camera && celestial) {
glm::vec3 sunPosition = celestial->getSunPosition(timeOfDay); // Use lighting direction for sun position if available
glm::vec3 sunPosition;
if (sunDir) {
const float sunDistance = 800.0f;
sunPosition = -*sunDir * sunDistance;
} else {
sunPosition = celestial->getSunPosition(timeOfDay);
}
lensFlare->render(*camera, sunPosition, timeOfDay); lensFlare->render(*camera, sunPosition, timeOfDay);
} }
// Update fog across all renderers based on time of day (match sky color) // Apply lighting and fog to all renderers
if (skybox) { if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams();
float lightDir[3] = {lighting.directionalDir.x, lighting.directionalDir.y, lighting.directionalDir.z};
float lightColor[3] = {lighting.diffuseColor.r, lighting.diffuseColor.g, lighting.diffuseColor.b};
float ambientColor[3] = {lighting.ambientColor.r, lighting.ambientColor.g, lighting.ambientColor.b};
float fogColorArray[3] = {lighting.fogColor.r, lighting.fogColor.g, lighting.fogColor.b};
if (wmoRenderer) {
wmoRenderer->setLighting(lightDir, lightColor, ambientColor);
wmoRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
if (m2Renderer) {
m2Renderer->setLighting(lightDir, lightColor, ambientColor);
m2Renderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
if (characterRenderer) {
characterRenderer->setLighting(lightDir, lightColor, ambientColor);
characterRenderer->setFog(glm::vec3(fogColorArray[0], fogColorArray[1], fogColorArray[2]),
lighting.fogStart, lighting.fogEnd);
}
} else if (skybox) {
// Fallback to skybox-based fog if no lighting manager
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay); glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
if (wmoRenderer) wmoRenderer->setFog(horizonColor, 100.0f, 600.0f); if (wmoRenderer) wmoRenderer->setFog(horizonColor, 100.0f, 600.0f);
if (m2Renderer) m2Renderer->setFog(horizonColor, 100.0f, 600.0f); if (m2Renderer) m2Renderer->setFog(horizonColor, 100.0f, 600.0f);

View file

@ -0,0 +1,183 @@
#include "rendering/sky_system.hpp"
#include "rendering/skybox.hpp"
#include "rendering/celestial.hpp"
#include "rendering/starfield.hpp"
#include "rendering/clouds.hpp"
#include "rendering/lens_flare.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace rendering {
SkySystem::SkySystem() = default;
SkySystem::~SkySystem() {
shutdown();
}
bool SkySystem::initialize() {
if (initialized_) {
LOG_WARNING("SkySystem already initialized");
return true;
}
LOG_INFO("Initializing sky system");
// Initialize skybox (authoritative)
skybox_ = std::make_unique<Skybox>();
if (!skybox_->initialize()) {
LOG_ERROR("Failed to initialize skybox");
return false;
}
// Initialize celestial bodies (sun + 2 moons)
celestial_ = std::make_unique<Celestial>();
if (!celestial_->initialize()) {
LOG_ERROR("Failed to initialize celestial bodies");
return false;
}
// Initialize procedural stars (FALLBACK only)
starField_ = std::make_unique<StarField>();
if (!starField_->initialize()) {
LOG_ERROR("Failed to initialize star field");
return false;
}
// Default: disabled (skybox is authoritative)
starField_->setEnabled(false);
// Initialize clouds
clouds_ = std::make_unique<Clouds>();
if (!clouds_->initialize()) {
LOG_ERROR("Failed to initialize clouds");
return false;
}
// Initialize lens flare
lensFlare_ = std::make_unique<LensFlare>();
if (!lensFlare_->initialize()) {
LOG_ERROR("Failed to initialize lens flare");
return false;
}
initialized_ = true;
LOG_INFO("Sky system initialized successfully");
return true;
}
void SkySystem::shutdown() {
if (!initialized_) {
return;
}
LOG_INFO("Shutting down sky system");
// Shutdown components that have explicit shutdown methods
if (starField_) starField_->shutdown();
if (celestial_) celestial_->shutdown();
if (skybox_) skybox_->shutdown();
// Reset all (destructors handle cleanup for clouds/lensFlare)
lensFlare_.reset();
clouds_.reset();
starField_.reset();
celestial_.reset();
skybox_.reset();
initialized_ = false;
}
void SkySystem::update(float deltaTime) {
if (!initialized_) {
return;
}
// Update time-based systems
if (skybox_) skybox_->update(deltaTime);
if (celestial_) celestial_->update(deltaTime);
if (starField_) starField_->update(deltaTime);
}
void SkySystem::render(const Camera& camera, const SkyParams& params) {
if (!initialized_) {
return;
}
// Render skybox first (authoritative, includes baked stars)
if (skybox_) {
skybox_->render(camera, params.timeOfDay);
}
// Decide whether to render procedural stars
bool renderProceduralStars = false;
if (debugSkyMode_) {
// Debug mode: always show procedural stars
renderProceduralStars = true;
} else if (proceduralStarsEnabled_) {
// Fallback mode: show only if skybox doesn't have stars
renderProceduralStars = !params.skyboxHasStars;
}
// Render procedural stars (FALLBACK or DEBUG only)
if (renderProceduralStars && starField_) {
starField_->setEnabled(true);
starField_->render(camera, params.timeOfDay, params.cloudDensity, params.fogDensity);
} else if (starField_) {
starField_->setEnabled(false);
}
// Render celestial bodies (sun + White Lady + Blue Child)
// Pass gameTime for deterministic moon phases
if (celestial_) {
celestial_->render(camera, params.timeOfDay, &params.directionalDir, &params.sunColor, params.gameTime);
}
// Render clouds
if (clouds_) {
clouds_->render(camera, params.timeOfDay);
}
// Render lens flare (sun glow effect)
if (lensFlare_) {
glm::vec3 sunPos = getSunPosition(params);
lensFlare_->render(camera, sunPos, params.timeOfDay);
}
}
glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const {
// Use lighting direction for sun position
const float sunDistance = 800.0f;
return -params.directionalDir * sunDistance; // Negative because light comes FROM sun
}
void SkySystem::setMoonPhaseCycling(bool enabled) {
if (celestial_) {
celestial_->setMoonPhaseCycling(enabled);
}
}
void SkySystem::setWhiteLadyPhase(float phase) {
if (celestial_) {
celestial_->setMoonPhase(phase); // White Lady is primary moon
}
}
void SkySystem::setBlueChildPhase(float phase) {
if (celestial_) {
celestial_->setBlueChildPhase(phase);
}
}
float SkySystem::getWhiteLadyPhase() const {
return celestial_ ? celestial_->getMoonPhase() : 0.5f;
}
float SkySystem::getBlueChildPhase() const {
// TODO: Second moon support
return 0.25f; // Placeholder phase
}
} // namespace rendering
} // namespace wowee

View file

@ -98,7 +98,8 @@ void StarField::shutdown() {
stars.clear(); stars.clear();
} }
void StarField::render(const Camera& camera, float timeOfDay) { void StarField::render(const Camera& camera, float timeOfDay,
float cloudDensity, float fogDensity) {
if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) { if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) {
return; return;
} }
@ -106,6 +107,10 @@ void StarField::render(const Camera& camera, float timeOfDay) {
// Get star intensity based on time of day // Get star intensity based on time of day
float intensity = getStarIntensity(timeOfDay); float intensity = getStarIntensity(timeOfDay);
// Reduce intensity based on cloud density and fog (more clouds/fog = fewer visible stars)
intensity *= (1.0f - glm::clamp(cloudDensity * 0.7f, 0.0f, 1.0f));
intensity *= (1.0f - glm::clamp(fogDensity * 0.3f, 0.0f, 1.0f));
// Don't render if stars would be invisible // Don't render if stars would be invisible
if (intensity <= 0.01f) { if (intensity <= 0.01f) {
return; return;

View file

@ -583,6 +583,21 @@ void WMORenderer::clearCollisionFocus() {
collisionFocusEnabled = false; collisionFocusEnabled = false;
} }
void WMORenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3],
const float ambientColorIn[3]) {
lightDir[0] = lightDirIn[0];
lightDir[1] = lightDirIn[1];
lightDir[2] = lightDirIn[2];
lightColor[0] = lightColorIn[0];
lightColor[1] = lightColorIn[1];
lightColor[2] = lightColorIn[2];
ambientColor[0] = ambientColorIn[0];
ambientColor[1] = ambientColorIn[1];
ambientColor[2] = ambientColorIn[2];
}
void WMORenderer::resetQueryStats() { void WMORenderer::resetQueryStats() {
queryTimeMs = 0.0; queryTimeMs = 0.0;
queryCallCount = 0; queryCallCount = 0;
@ -802,10 +817,10 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
shader->setUniform("uView", view); shader->setUniform("uView", view);
shader->setUniform("uProjection", projection); shader->setUniform("uProjection", projection);
shader->setUniform("uViewPos", camera.getPosition()); shader->setUniform("uViewPos", camera.getPosition());
shader->setUniform("uLightDir", glm::vec3(-0.3f, -0.7f, -0.6f)); // Default sun direction shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2]));
shader->setUniform("uLightColor", glm::vec3(1.5f, 1.4f, 1.3f)); shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2]));
shader->setUniform("uSpecularIntensity", 0.5f); shader->setUniform("uSpecularIntensity", 0.5f);
shader->setUniform("uAmbientColor", glm::vec3(0.55f, 0.55f, 0.6f)); shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2]));
shader->setUniform("uFogColor", fogColor); shader->setUniform("uFogColor", fogColor);
shader->setUniform("uFogStart", fogStart); shader->setUniform("uFogStart", fogStart);
shader->setUniform("uFogEnd", fogEnd); shader->setUniform("uFogEnd", fogEnd);