Skip to the content.

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)

Blue Child (Secondary Moon)

Visibility

The Sun


Deterministic Moon Phases

Server Time-Driven (NOT deltaTime)

Moon phases are computed from server game time, ensuring:

Calculation Formula

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

Fallback Mode (Development)

If gameTime < 0.0 (server time unavailable):


Sky Dome Rendering

Camera-Locked Behavior (WoW Standard)

// 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:

Time-Based Sky Drift (Optional)

Subtle rotation for atmospheric effect:

float skyYawRotation = gameTime * skyRotationRate;
skyDomeMatrix = rotate(skyDomeMatrix, skyYawRotation, vec3(0, 0, 1));  // Yaw only

Per-zone rotation rates:

Implementation status: Not yet active (waiting for M2 skybox loading)


Critical Anti-Patterns

❌ DO NOT: Latitude-Based Star Rotation

Why it’s wrong:

What happens if you do it anyway:

Correct approach:

// ✅ 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:

Correct gating logic:

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:

❌ DO NOT: Universal Dual Moon Setup

Why it’s wrong:

Correct approach:

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)

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:

// 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:

// In SkySystem::render() - Vulkan path
// Bind a sky pipeline whose VkPipelineColorBlendAttachmentState is
// configured for additive blending (srcColor=SRC_ALPHA, dstColor=ONE,
// blendOp=ADD), then render each skybox in weight order:
if (activeVolumes.size() >= 2) {
    // Primary skybox (alpha = volumes[0].weight)
    skybox1->render(camera, volumes[0].weight);
    // Secondary skybox blends additively on top
    skybox2->render(camera, volumes[1].weight);
}

Result: Smooth crossfade between zone skies, no popping

SkyProfile Configuration

Per-map/continent settings:

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

🚧 Future Enhancements


Code References

Key Files:

Integration Points:


References