Enhanced sky atmosphere with DBC-driven colors, sun lighting, and zone weather

- Skybox now uses DBC sky colors (skyTop/skyMiddle/skyBand1/skyBand2) instead
  of hardcoded C++ color curves, with 3-band gradient and Rayleigh/Mie scattering
- Clouds receive sun direction for lit edges, self-shadowing, and silver lining
- Fixed sun quad box artifact with proper edge fade in celestial shader
- Lens flare attenuated by fog, cloud density, and weather intensity
- Replaced garish green/purple lens flare ghosts with warm natural palette
- Added zone-based weather system for single-player mode with per-zone rain/snow
  configuration, probability-based activation, and smooth intensity transitions
- Server SMSG_WEATHER remains authoritative when connected to a server
This commit is contained in:
Kelsi 2026-02-22 23:20:13 -08:00
parent 085fd09b9d
commit 6563eebb60
18 changed files with 434 additions and 252 deletions

View file

@ -26,9 +26,21 @@ float valueNoise(vec2 p) {
void main() { void main() {
vec2 uv = TexCoord - 0.5; vec2 uv = TexCoord - 0.5;
float dist = length(uv); float dist = length(uv);
float disc = smoothstep(0.42, 0.38, dist);
float glow = exp(-dist * dist * 12.0) * 0.6; // Hard disc with smooth edge
float disc = smoothstep(0.42, 0.35, dist);
// Soft glow that fades to zero well within quad bounds
// At dist=0.5 (quad edge), this should be negligible
float glow = exp(-dist * dist * 18.0) * 0.5;
// Combine disc and glow
float alpha = max(disc, glow) * push.intensity; float alpha = max(disc, glow) * push.intensity;
// Fade to zero at quad edges to prevent visible box
float edgeFade = 1.0 - smoothstep(0.4, 0.5, dist);
alpha *= edgeFade;
vec3 color = push.celestialColor.rgb; vec3 color = push.celestialColor.rgb;
// Animated haze/turbulence overlay for the sun disc // Animated haze/turbulence overlay for the sun disc

Binary file not shown.

View file

@ -1,35 +1,42 @@
#version 450 #version 450
layout(push_constant) uniform Push { layout(push_constant) uniform Push {
vec4 cloudColor; vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused
float density; vec4 sunDirDensity; // xyz = sun direction, w = density
float windOffset; vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused
} push; } push;
layout(location = 0) in vec3 vWorldDir; layout(location = 0) in vec3 vWorldDir;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
float hash(vec2 p) { // --- Gradient noise (smoother than hash-based) ---
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453);
} }
float noise(vec2 p) { float gradientNoise(vec2 p) {
vec2 i = floor(p); vec2 i = floor(p);
vec2 f = fract(p); vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i); // Quintic interpolation for smoother results
float b = hash(i + vec2(1.0, 0.0)); vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0)); float a = dot(hash2(i + vec2(0.0, 0.0)) * 2.0 - 1.0, f - vec2(0.0, 0.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); float b = dot(hash2(i + vec2(1.0, 0.0)) * 2.0 - 1.0, f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)) * 2.0 - 1.0, f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)) * 2.0 - 1.0, f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 0.5 + 0.5;
} }
float fbm(vec2 p) { float fbm(vec2 p) {
float val = 0.0; float val = 0.0;
float amp = 0.5; float amp = 0.5;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 6; i++) {
val += amp * noise(p); val += amp * gradientNoise(p);
p *= 2.0; p *= 2.0;
amp *= 0.5; amp *= 0.5;
} }
@ -38,26 +45,60 @@ float fbm(vec2 p) {
void main() { void main() {
vec3 dir = normalize(vWorldDir); vec3 dir = normalize(vWorldDir);
float altitude = dir.z; // Z is up in the Z-up world coordinate system float altitude = dir.z;
if (altitude < 0.0) discard; if (altitude < 0.0) discard;
vec2 uv = dir.xy / (altitude + 0.001); // XY is the horizontal plane vec3 sunDir = push.sunDirDensity.xyz;
uv += push.windOffset; float density = push.sunDirDensity.w;
float windOffset = push.windAndLight.x;
float sunIntensity = push.windAndLight.y;
float ambient = push.windAndLight.z;
vec2 uv = dir.xy / (altitude + 0.001);
uv += windOffset;
// --- 6-octave FBM for cloud shape ---
float cloud1 = fbm(uv * 0.8); float cloud1 = fbm(uv * 0.8);
float cloud2 = fbm(uv * 1.6 + 5.0); float cloud2 = fbm(uv * 1.6 + 5.0);
float cloud = cloud1 * 0.7 + cloud2 * 0.3; float cloud = cloud1 * 0.7 + cloud2 * 0.3;
cloud = smoothstep(0.35, 0.65, cloud) * push.density;
float edgeBreak = noise(uv * 4.0); // Coverage control: base coverage with detail erosion
cloud *= smoothstep(0.2, 0.5, edgeBreak); float baseCoverage = smoothstep(0.30, 0.55, cloud);
float detailErosion = gradientNoise(uv * 4.0);
cloud = baseCoverage * smoothstep(0.2, 0.5, detailErosion);
cloud *= density;
// Horizon fade
float horizonFade = smoothstep(0.0, 0.15, altitude); float horizonFade = smoothstep(0.0, 0.15, altitude);
cloud *= horizonFade; cloud *= horizonFade;
if (cloud < 0.01) discard;
// --- Sun lighting on clouds ---
// Sun dot product for view-relative brightness
float sunDot = max(dot(vec3(0.0, 0.0, 1.0), sunDir), 0.0);
// Self-shadowing: sample noise offset toward sun direction, darken if occluded
float lightSample = fbm((uv + sunDir.xy * 0.05) * 0.8);
float shadow = smoothstep(0.3, 0.7, lightSample);
// Base lit color: mix dark (shadow) and bright (sunlit) based on shadow and sun
vec3 baseColor = push.cloudColor.rgb;
vec3 shadowColor = baseColor * (ambient * 0.8);
vec3 litColor = baseColor * (ambient + sunIntensity * 0.6);
vec3 cloudRgb = mix(shadowColor, litColor, shadow * sunDot);
// Add ambient fill so clouds aren't too dark
cloudRgb = mix(baseColor * ambient, cloudRgb, 0.7 + 0.3 * sunIntensity);
// --- Silver lining effect at cloud edges ---
float edgeLight = smoothstep(0.0, 0.3, cloud) * (1.0 - smoothstep(0.3, 0.8, cloud));
cloudRgb += vec3(1.0, 0.95, 0.9) * edgeLight * sunDot * sunIntensity * 0.4;
// --- Edge softness for alpha ---
float edgeSoftness = smoothstep(0.0, 0.3, cloud); float edgeSoftness = smoothstep(0.0, 0.3, cloud);
float alpha = cloud * edgeSoftness; float alpha = cloud * edgeSoftness;
if (alpha < 0.01) discard; if (alpha < 0.01) discard;
outColor = vec4(push.cloudColor.rgb, alpha); outColor = vec4(cloudRgb, alpha);
} }

Binary file not shown.

View file

@ -14,9 +14,11 @@ layout(set = 0, binding = 0) uniform PerFrame {
}; };
layout(push_constant) uniform Push { layout(push_constant) uniform Push {
vec4 horizonColor; vec4 zenithColor; // DBC skyTopColor
vec4 zenithColor; vec4 midColor; // DBC skyMiddleColor
float timeOfDay; vec4 horizonColor; // DBC skyBand1Color
vec4 fogColorPush; // DBC skyBand2Color
vec4 sunDirAndTime; // xyz = sun direction, w = timeOfDay
} push; } push;
layout(location = 0) in vec2 TexCoord; layout(location = 0) in vec2 TexCoord;
@ -25,27 +27,71 @@ layout(location = 0) out vec4 outColor;
void main() { void main() {
// Reconstruct world-space ray direction from screen position. // Reconstruct world-space ray direction from screen position.
// TexCoord is [0,1]^2; convert to NDC [-1,1]^2.
float ndcX = TexCoord.x * 2.0 - 1.0; float ndcX = TexCoord.x * 2.0 - 1.0;
float ndcY = -(TexCoord.y * 2.0 - 1.0); // flip Y: Vulkan NDC Y-down, but projection already flipped float ndcY = -(TexCoord.y * 2.0 - 1.0);
// Unproject to view space using focal lengths from projection matrix.
// projection[0][0] = 2*near/(right-left) = 1/tan(fovX/2)
// projection[1][1] = 2*near/(top-bottom) (already negated for Vulkan Y-flip)
// We want the original magnitude, so take abs to get the focal length.
vec3 viewDir = vec3(ndcX / projection[0][0], vec3 viewDir = vec3(ndcX / projection[0][0],
ndcY / abs(projection[1][1]), ndcY / abs(projection[1][1]),
-1.0); -1.0);
// Rotate to world space: view = R*T, so R^-1 = R^T = transpose(mat3(view))
mat3 invViewRot = transpose(mat3(view)); mat3 invViewRot = transpose(mat3(view));
vec3 worldDir = normalize(invViewRot * viewDir); vec3 worldDir = normalize(invViewRot * viewDir);
// worldDir.z = sin(elevation); +1 = zenith, 0 = horizon, -1 = nadir vec3 sunDir = push.sunDirAndTime.xyz;
float t = clamp(worldDir.z, 0.0, 1.0); float timeOfDay = push.sunDirAndTime.w;
t = pow(t, 1.5);
vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t); // Elevation: +1 = zenith, 0 = horizon, -1 = nadir
float scatter = max(0.0, 1.0 - t * 2.0) * 0.15; float elev = worldDir.z;
sky += vec3(scatter * 0.8, scatter * 0.4, scatter * 0.1); float elevClamped = clamp(elev, 0.0, 1.0);
// --- 3-band sky gradient using DBC colors ---
// Zenith dominates upper sky, mid color fills the middle,
// horizon band at the bottom with a thin fog fringe.
vec3 sky;
if (elevClamped > 0.4) {
// Upper sky: mid -> zenith
float t = (elevClamped - 0.4) / 0.6;
sky = mix(push.midColor.rgb, push.zenithColor.rgb, t);
} else if (elevClamped > 0.05) {
// Lower sky: horizon -> mid (wide band)
float t = (elevClamped - 0.05) / 0.35;
sky = mix(push.horizonColor.rgb, push.midColor.rgb, t);
} else {
// Thin fog fringe right at horizon
float t = elevClamped / 0.05;
sky = mix(push.fogColorPush.rgb, push.horizonColor.rgb, t);
}
// --- Below-horizon darkening (nadir) ---
if (elev < 0.0) {
float nadirFade = clamp(-elev * 3.0, 0.0, 1.0);
vec3 nadirColor = push.fogColorPush.rgb * 0.3;
sky = mix(push.fogColorPush.rgb, nadirColor, nadirFade);
}
// --- Rayleigh-like scattering (subtle warm glow near sun) ---
float sunDot = max(dot(worldDir, sunDir), 0.0);
float sunAboveHorizon = clamp(sunDir.z, 0.0, 1.0);
float rayleighStrength = pow(1.0 - elevClamped, 3.0) * 0.15;
vec3 scatterColor = mix(vec3(0.8, 0.45, 0.15), vec3(0.3, 0.5, 1.0), elevClamped);
sky += scatterColor * rayleighStrength * sunDot * sunAboveHorizon;
// --- Mie-like forward scatter (sun disk glow) ---
float mieSharp = pow(sunDot, 64.0) * 0.4;
float mieSoft = pow(sunDot, 8.0) * 0.1;
vec3 sunGlowColor = mix(vec3(1.0, 0.85, 0.55), vec3(1.0, 1.0, 0.95), elevClamped);
sky += sunGlowColor * (mieSharp + mieSoft) * sunAboveHorizon;
// --- Subtle horizon haze ---
float hazeDensity = exp(-elevClamped * 12.0) * 0.06;
sky += push.horizonColor.rgb * hazeDensity * sunAboveHorizon;
// --- Night: slight moonlight tint ---
if (sunDir.z < 0.0) {
float moonlight = clamp(-sunDir.z * 0.5, 0.0, 0.15);
sky += vec3(0.02, 0.03, 0.08) * moonlight;
}
outColor = vec4(sky, 1.0); outColor = vec4(sky, 1.0);
} }

Binary file not shown.

View file

@ -9,45 +9,37 @@ namespace wowee {
namespace rendering { namespace rendering {
class VkContext; class VkContext;
struct SkyParams;
/** /**
* Procedural cloud renderer (Vulkan) * Procedural cloud renderer (Vulkan)
* *
* Renders animated procedural clouds on a sky hemisphere using FBM noise. * Renders animated procedural clouds on a sky hemisphere using FBM noise.
* Two noise layers at different frequencies produce realistic cloud shapes. * Sun-lit edges, self-shadowing, and DBC-driven cloud colors for realistic appearance.
* *
* Pipeline layout: * Pipeline layout:
* set 0 = perFrameLayout (camera UBO view, projection, etc.) * set 0 = perFrameLayout (camera UBO view, projection, etc.)
* push = CloudPush (vec4 cloudColor + float density + float windOffset = 24 bytes) * push = CloudPush (3 x vec4 = 48 bytes)
*
* The vertex shader reads view/projection from set 0 directly; no per-object
* model matrix is needed (clouds are locked to the sky dome).
*/ */
class Clouds { class Clouds {
public: public:
Clouds(); Clouds();
~Clouds(); ~Clouds();
/**
* Initialize the cloud system.
* @param ctx Vulkan context
* @param perFrameLayout Descriptor set layout for set 0 (camera UBO)
*/
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown(); void shutdown();
void recreatePipelines(); void recreatePipelines();
/** /**
* Render clouds. * Render clouds using DBC-driven colors and sun lighting.
* @param cmd Command buffer to record into * @param cmd Command buffer to record into
* @param perFrameSet Per-frame descriptor set (set 0, camera UBO) * @param perFrameSet Per-frame descriptor set (set 0, camera UBO)
* @param timeOfDay Time of day in hours (0-24) * @param params Sky parameters with DBC colors and sun direction
*/ */
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params);
/** /**
* Update cloud animation (wind drift). * Update cloud animation (wind drift).
* @param deltaTime Seconds since last frame
*/ */
void update(float deltaTime); void update(float deltaTime);
@ -56,7 +48,6 @@ public:
bool isEnabled() const { return enabled_; } bool isEnabled() const { return enabled_; }
// --- Cloud parameters --- // --- Cloud parameters ---
/** Cloud coverage, 0 = clear, 1 = overcast. */
void setDensity(float density); void setDensity(float density);
float getDensity() const { return density_; } float getDensity() const { return density_; }
@ -66,19 +57,16 @@ public:
private: private:
// Push constant block — must match clouds.frag.glsl // Push constant block — must match clouds.frag.glsl
struct CloudPush { struct CloudPush {
glm::vec4 cloudColor; // 16 bytes (xyz = colour, w unused) glm::vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused
float density; // 4 bytes glm::vec4 sunDirDensity; // xyz = sun direction, w = density
float windOffset; // 4 bytes glm::vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused
// total = 24 bytes
}; };
static_assert(sizeof(CloudPush) == 24, "CloudPush size mismatch"); static_assert(sizeof(CloudPush) == 48, "CloudPush size mismatch");
void generateMesh(); void generateMesh();
void createBuffers(); void createBuffers();
void destroyBuffers(); void destroyBuffers();
glm::vec3 getCloudColor(float timeOfDay) const;
// Vulkan objects // Vulkan objects
VkContext* vkCtx_ = nullptr; VkContext* vkCtx_ = nullptr;
VkPipeline pipeline_ = VK_NULL_HANDLE; VkPipeline pipeline_ = VK_NULL_HANDLE;
@ -95,14 +83,14 @@ private:
// Cloud parameters // Cloud parameters
bool enabled_ = true; bool enabled_ = true;
float density_ = 0.5f; float density_ = 0.35f;
float windSpeed_ = 1.0f; float windSpeed_ = 1.0f;
float windOffset_ = 0.0f; // Accumulated wind movement float windOffset_ = 0.0f;
// Mesh generation parameters // Mesh generation parameters
static constexpr int SEGMENTS = 32; static constexpr int SEGMENTS = 32;
static constexpr int RINGS = 8; static constexpr int RINGS = 8;
static constexpr float RADIUS = 900.0f; // Slightly smaller than skybox static constexpr float RADIUS = 900.0f;
}; };
} // namespace rendering } // namespace rendering

View file

@ -47,8 +47,13 @@ public:
* @param camera The camera to render from * @param camera The camera to render from
* @param sunPosition World-space sun position * @param sunPosition World-space sun position
* @param timeOfDay Current time (0-24 hours) * @param timeOfDay Current time (0-24 hours)
* @param fogDensity Fog density 0-1 (attenuates flare)
* @param cloudDensity Cloud density 0-1 (attenuates flare)
* @param weatherIntensity Weather intensity 0-1 (rain/snow attenuates flare)
*/ */
void render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition, float timeOfDay); void render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition,
float timeOfDay, float fogDensity = 0.0f, float cloudDensity = 0.0f,
float weatherIntensity = 0.0f);
/** /**
* @brief Enable or disable lens flare rendering * @brief Enable or disable lens flare rendering

View file

@ -28,7 +28,7 @@ struct LightingParams {
glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f}; // Sky band 1 glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f}; // Sky band 1
glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; // Sky band 2 glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; // Sky band 2
float cloudDensity = 1.0f; // Cloud density/opacity float cloudDensity = 0.3f; // Cloud density/opacity
float horizonGlow = 0.3f; // Horizon glow intensity float horizonGlow = 0.3f; // Horizon glow intensity
}; };

View file

@ -31,9 +31,10 @@ struct SkyParams {
glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f};
// Atmospheric effects // Atmospheric effects
float cloudDensity = 0.0f; // 0-1 float cloudDensity = 0.0f; // 0-1
float fogDensity = 0.0f; // 0-1 float fogDensity = 0.0f; // 0-1
float horizonGlow = 0.3f; // 0-1 float horizonGlow = 0.3f; // 0-1
float weatherIntensity = 0.0f; // 0-1 (rain/snow intensity, attenuates lens flare)
// Time // Time
float timeOfDay = 12.0f; // 0-24 hours float timeOfDay = 12.0f; // 0-24 hours

View file

@ -9,6 +9,7 @@ namespace wowee {
namespace rendering { namespace rendering {
class VkContext; class VkContext;
struct SkyParams;
/** /**
* Skybox renderer * Skybox renderer
@ -16,6 +17,9 @@ class VkContext;
* Renders an atmospheric sky gradient using a fullscreen triangle. * Renders an atmospheric sky gradient using a fullscreen triangle.
* No vertex buffer: 3 vertices cover the entire screen via gl_VertexIndex. * No vertex buffer: 3 vertices cover the entire screen via gl_VertexIndex.
* World-space ray direction is reconstructed from the inverse view+projection. * World-space ray direction is reconstructed from the inverse view+projection.
*
* Sky colors are driven by DBC data (Light.dbc / LightIntBand.dbc) via SkyParams,
* with Rayleigh/Mie atmospheric scattering for realistic appearance.
*/ */
class Skybox { class Skybox {
public: public:
@ -27,12 +31,12 @@ public:
void recreatePipelines(); void recreatePipelines();
/** /**
* Render the skybox * Render the skybox using DBC-driven sky colors.
* @param cmd Command buffer to record into * @param cmd Command buffer to record into
* @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO) * @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO)
* @param timeOfDay Time of day in hours (0-24), affects sky color * @param params Sky parameters with DBC colors and sun direction
*/ */
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay = 12.0f); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params);
/** /**
* Enable/disable skybox rendering * Enable/disable skybox rendering
@ -58,15 +62,7 @@ public:
*/ */
void update(float deltaTime); void update(float deltaTime);
/**
* Get horizon color for fog (public for fog system)
*/
glm::vec3 getHorizonColor(float time) const;
private: private:
glm::vec3 getSkyColor(float altitude, float time) const;
glm::vec3 getZenithColor(float time) const;
VkContext* vkCtx = nullptr; VkContext* vkCtx = nullptr;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;

View file

@ -4,6 +4,7 @@
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <vector> #include <vector>
#include <unordered_map>
namespace wowee { namespace wowee {
namespace rendering { namespace rendering {
@ -79,6 +80,35 @@ public:
*/ */
int getParticleCount() const; int getParticleCount() const;
/**
* @brief Zone weather configuration
* Provides default weather per zone for single-player mode.
* When connected to a server, SMSG_WEATHER overrides these.
*/
struct ZoneWeather {
Type type = Type::NONE;
float minIntensity = 0.0f; // Min intensity (varies over time)
float maxIntensity = 0.0f; // Max intensity
float probability = 0.0f; // Chance of weather being active (0-1)
};
/**
* @brief Set weather for a zone (used for zone-based weather configuration)
*/
void setZoneWeather(uint32_t zoneId, Type type, float minIntensity, float maxIntensity, float probability);
/**
* @brief Update weather based on current zone (single-player mode)
* @param zoneId Current zone ID
* @param deltaTime Time since last frame
*/
void updateZoneWeather(uint32_t zoneId, float deltaTime);
/**
* @brief Initialize default zone weather table
*/
void initializeZoneWeatherDefaults();
/** /**
* @brief Clean up Vulkan resources * @brief Clean up Vulkan resources
*/ */
@ -120,6 +150,15 @@ private:
static constexpr int MAX_PARTICLES = 2000; static constexpr int MAX_PARTICLES = 2000;
static constexpr float SPAWN_VOLUME_SIZE = 100.0f; // Size of spawn area around camera static constexpr float SPAWN_VOLUME_SIZE = 100.0f; // Size of spawn area around camera
static constexpr float SPAWN_HEIGHT = 80.0f; // Height above camera to spawn static constexpr float SPAWN_HEIGHT = 80.0f; // Height above camera to spawn
// Zone-based weather
std::unordered_map<uint32_t, ZoneWeather> zoneWeatherTable_;
uint32_t currentWeatherZone_ = 0;
float zoneWeatherTimer_ = 0.0f; // Time accumulator for weather cycling
float zoneWeatherCycleDuration_ = 0.0f; // Current cycle length
bool zoneWeatherActive_ = false; // Is zone weather currently active?
float targetIntensity_ = 0.0f; // Target intensity for smooth transitions
bool zoneWeatherInitialized_ = false;
}; };
} // namespace rendering } // namespace rendering

View file

@ -1,4 +1,5 @@
#include "rendering/clouds.hpp" #include "rendering/clouds.hpp"
#include "rendering/sky_system.hpp"
#include "rendering/vk_context.hpp" #include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp" #include "rendering/vk_shader.hpp"
#include "rendering/vk_pipeline.hpp" #include "rendering/vk_pipeline.hpp"
@ -40,11 +41,11 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// ------------------------------------------------------------------ push constants // ------------------------------------------------------------------ push constants
// Fragment-only push: vec4 cloudColor + float density + float windOffset = 24 bytes // Fragment-only push: 3 x vec4 = 48 bytes
VkPushConstantRange pushRange{}; VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0; pushRange.offset = 0;
pushRange.size = sizeof(CloudPush); // 24 bytes pushRange.size = sizeof(CloudPush); // 48 bytes
// ------------------------------------------------------------------ pipeline layout // ------------------------------------------------------------------ pipeline layout
pipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {pushRange}); pipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {pushRange});
@ -54,10 +55,9 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
} }
// ------------------------------------------------------------------ vertex input // ------------------------------------------------------------------ vertex input
// Vertex: vec3 pos only, stride = 12 bytes
VkVertexInputBindingDescription binding{}; VkVertexInputBindingDescription binding{};
binding.binding = 0; binding.binding = 0;
binding.stride = sizeof(glm::vec3); // 12 bytes binding.stride = sizeof(glm::vec3);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription posAttr{}; VkVertexInputAttributeDescription posAttr{};
@ -77,7 +77,7 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
.setVertexInput({binding}, {posAttr}) .setVertexInput({binding}, {posAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
.setColorBlendAttachment(PipelineBuilder::blendAlpha()) .setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx_->getMsaaSamples()) .setMultisample(vkCtx_->getMsaaSamples())
.setLayout(pipelineLayout_) .setLayout(pipelineLayout_)
@ -122,7 +122,6 @@ void Clouds::recreatePipelines() {
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex input (same as initialize)
VkVertexInputBindingDescription binding{}; VkVertexInputBindingDescription binding{};
binding.binding = 0; binding.binding = 0;
binding.stride = sizeof(glm::vec3); binding.stride = sizeof(glm::vec3);
@ -182,25 +181,35 @@ void Clouds::shutdown() {
// Render // Render
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void Clouds::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay) { void Clouds::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params) {
if (!enabled_ || pipeline_ == VK_NULL_HANDLE) { if (!enabled_ || pipeline_ == VK_NULL_HANDLE) {
return; return;
} }
glm::vec3 color = getCloudColor(timeOfDay); // Derive cloud base color from DBC horizon band, slightly brightened
glm::vec3 cloudBaseColor = params.skyBand1Color * 1.1f;
cloudBaseColor = glm::clamp(cloudBaseColor, glm::vec3(0.0f), glm::vec3(1.0f));
// Sun direction (opposite of light direction)
glm::vec3 sunDir = -glm::normalize(params.directionalDir);
float sunAboveHorizon = glm::clamp(sunDir.z, 0.0f, 1.0f);
// Sun intensity based on elevation
float sunIntensity = sunAboveHorizon;
// Ambient light — brighter during day, dimmer at night
float ambient = glm::mix(0.3f, 0.7f, sunAboveHorizon);
CloudPush push{}; CloudPush push{};
push.cloudColor = glm::vec4(color, 1.0f); push.cloudColor = glm::vec4(cloudBaseColor, 1.0f);
push.density = density_; push.sunDirDensity = glm::vec4(sunDir, density_);
push.windOffset = windOffset_; push.windAndLight = glm::vec4(windOffset_, sunIntensity, ambient, 0.0f);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
// Bind per-frame UBO (set 0 — vertex shader reads view/projection from here)
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
0, 1, &perFrameSet, 0, nullptr); 0, 1, &perFrameSet, 0, nullptr);
// Push cloud params to fragment shader
vkCmdPushConstants(cmd, pipelineLayout_, vkCmdPushConstants(cmd, pipelineLayout_,
VK_SHADER_STAGE_FRAGMENT_BIT, VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(push), &push); 0, sizeof(push), &push);
@ -223,29 +232,6 @@ void Clouds::update(float deltaTime) {
windOffset_ += deltaTime * windSpeed_ * 0.05f; // Slow drift windOffset_ += deltaTime * windSpeed_ * 0.05f; // Slow drift
} }
// ---------------------------------------------------------------------------
// Cloud colour (unchanged logic from GL version)
// ---------------------------------------------------------------------------
glm::vec3 Clouds::getCloudColor(float timeOfDay) const {
glm::vec3 dayColor(0.95f, 0.95f, 1.0f);
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
// Dawn — orange tint fading to day
float t = (timeOfDay - 5.0f) / 2.0f;
return glm::mix(glm::vec3(1.0f, 0.7f, 0.5f), dayColor, t);
} else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) {
// Dusk — day fading to orange/pink
float t = (timeOfDay - 17.0f) / 2.0f;
return glm::mix(dayColor, glm::vec3(1.0f, 0.6f, 0.4f), t);
} else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) {
// Night — dark blue-grey
return glm::vec3(0.15f, 0.15f, 0.25f);
}
return dayColor;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Density setter // Density setter
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -265,7 +251,7 @@ void Clouds::generateMesh() {
// Upper hemisphere — Z-up world: altitude goes into Z, horizontal spread in X/Y // Upper hemisphere — Z-up world: altitude goes into Z, horizontal spread in X/Y
for (int ring = 0; ring <= RINGS; ++ring) { for (int ring = 0; ring <= RINGS; ++ring) {
float phi = (ring / static_cast<float>(RINGS)) * (static_cast<float>(M_PI) * 0.5f); float phi = (ring / static_cast<float>(RINGS)) * (static_cast<float>(M_PI) * 0.5f);
float altZ = RADIUS * std::cos(phi); // altitude → world Z (up) float altZ = RADIUS * std::cos(phi);
float ringRadius = RADIUS * std::sin(phi); float ringRadius = RADIUS * std::sin(phi);
for (int seg = 0; seg <= SEGMENTS; ++seg) { for (int seg = 0; seg <= SEGMENTS; ++seg) {

View file

@ -211,28 +211,27 @@ void LensFlare::generateFlareElements() {
flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f}); flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f});
// Flare ghosts along sun-to-center axis // Flare ghosts along sun-to-center axis
// These appear at various positions between sun and opposite side
// Bright white ghost near sun // Bright white ghost near sun
flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f}); flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f});
// Blue-tinted ghost // Blue-tinted ghost
flareElements.push_back({0.4f, 0.15f, glm::vec3(0.3f, 0.5f, 1.0f), 0.4f}); flareElements.push_back({0.4f, 0.15f, glm::vec3(0.4f, 0.55f, 0.9f), 0.35f});
// Small bright spot // Small bright spot
flareElements.push_back({0.6f, 0.05f, glm::vec3(1.0f, 0.8f, 0.6f), 0.6f}); flareElements.push_back({0.6f, 0.05f, glm::vec3(1.0f, 0.8f, 0.6f), 0.5f});
// Green-tinted ghost (chromatic aberration) // Warm amber ghost (replaced oversaturated green)
flareElements.push_back({0.8f, 0.12f, glm::vec3(0.4f, 1.0f, 0.5f), 0.3f}); flareElements.push_back({0.8f, 0.10f, glm::vec3(0.9f, 0.75f, 0.5f), 0.2f});
// Large halo on opposite side // Large halo on opposite side
flareElements.push_back({-0.5f, 0.25f, glm::vec3(1.0f, 0.7f, 0.4f), 0.2f}); flareElements.push_back({-0.5f, 0.22f, glm::vec3(1.0f, 0.8f, 0.5f), 0.15f});
// Purple ghost far from sun // Faint blue ghost far from sun
flareElements.push_back({-0.8f, 0.1f, glm::vec3(0.8f, 0.4f, 1.0f), 0.25f}); flareElements.push_back({-0.8f, 0.08f, glm::vec3(0.6f, 0.5f, 0.9f), 0.15f});
// Small red ghost // Small warm ghost
flareElements.push_back({-1.2f, 0.06f, glm::vec3(1.0f, 0.3f, 0.3f), 0.3f}); flareElements.push_back({-1.2f, 0.05f, glm::vec3(1.0f, 0.6f, 0.4f), 0.2f});
} }
glm::vec2 LensFlare::worldToScreen(const Camera& camera, const glm::vec3& worldPos) const { glm::vec2 LensFlare::worldToScreen(const Camera& camera, const glm::vec3& worldPos) const {
@ -287,7 +286,9 @@ float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& s
return angleFactor * edgeFade; return angleFactor * edgeFade;
} }
void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) { void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition,
float timeOfDay, float fogDensity, float cloudDensity,
float weatherIntensity) {
if (!enabled || pipeline == VK_NULL_HANDLE) { if (!enabled || pipeline == VK_NULL_HANDLE) {
return; return;
} }
@ -312,6 +313,16 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
return; return;
} }
// Atmospheric attenuation — fog, clouds, and weather reduce lens flare
float atmosphericFactor = 1.0f;
atmosphericFactor *= (1.0f - glm::clamp(fogDensity * 0.8f, 0.0f, 0.9f)); // Heavy fog nearly kills flare
atmosphericFactor *= (1.0f - glm::clamp(cloudDensity * 0.6f, 0.0f, 0.7f)); // Clouds attenuate
atmosphericFactor *= (1.0f - glm::clamp(weatherIntensity * 0.9f, 0.0f, 0.95f)); // Rain/snow heavily attenuates
if (atmosphericFactor < 0.01f) {
return;
}
// Get sun screen position // Get sun screen position
glm::vec2 sunScreen = worldToScreen(camera, anchoredSunPos); glm::vec2 sunScreen = worldToScreen(camera, anchoredSunPos);
glm::vec2 screenCenter(0.0f, 0.0f); glm::vec2 screenCenter(0.0f, 0.0f);
@ -333,8 +344,8 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
// Calculate position along sun-to-center axis // Calculate position along sun-to-center axis
glm::vec2 position = sunScreen + sunToCenter * element.position; glm::vec2 position = sunScreen + sunToCenter * element.position;
// Apply visibility and intensity // Apply visibility, intensity, and atmospheric attenuation
float brightness = element.brightness * visibility * intensityMultiplier; float brightness = element.brightness * visibility * intensityMultiplier * atmosphericFactor;
// Set push constants // Set push constants
FlarePushConstants push{}; FlarePushConstants push{};

View file

@ -2417,10 +2417,21 @@ void Renderer::update(float deltaTime) {
if (weather && gh) { if (weather && gh) {
uint32_t wType = gh->getWeatherType(); uint32_t wType = gh->getWeatherType();
float wInt = gh->getWeatherIntensity(); float wInt = gh->getWeatherIntensity();
if (wType == 1) weather->setWeatherType(Weather::Type::RAIN); if (wType != 0) {
else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW); // Server-driven weather (SMSG_WEATHER) — authoritative
else weather->setWeatherType(Weather::Type::NONE); if (wType == 1) weather->setWeatherType(Weather::Type::RAIN);
weather->setIntensity(wInt); else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW);
else weather->setWeatherType(Weather::Type::NONE);
weather->setIntensity(wInt);
} else {
// No server weather — use zone-based weather configuration
weather->updateZoneWeather(currentZoneId, deltaTime);
}
weather->setEnabled(true);
} else if (weather) {
// No game handler (single-player without network) — zone weather only
weather->updateZoneWeather(currentZoneId, deltaTime);
weather->setEnabled(true);
} }
} }
auto light2 = std::chrono::high_resolution_clock::now(); auto light2 = std::chrono::high_resolution_clock::now();
@ -3200,6 +3211,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
skyParams.horizonGlow = lighting.horizonGlow; skyParams.horizonGlow = lighting.horizonGlow;
} }
// Weather attenuation for lens flare
if (gameHandler) {
skyParams.weatherIntensity = gameHandler->getWeatherIntensity();
}
skyParams.skyboxModelId = 0; skyParams.skyboxModelId = 0;
skyParams.skyboxHasStars = false; skyParams.skyboxHasStars = false;
@ -3866,6 +3882,7 @@ void Renderer::renderReflectionPass() {
skyParams.fogDensity = lp.fogDensity; skyParams.fogDensity = lp.fogDensity;
skyParams.horizonGlow = lp.horizonGlow; skyParams.horizonGlow = lp.horizonGlow;
} }
// weatherIntensity left at default 0 for reflection pass (no game handler in scope)
skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams); skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams);
} }
if (terrainRenderer && terrainEnabled) { if (terrainRenderer && terrainEnabled) {

View file

@ -105,9 +105,9 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
return; return;
} }
// --- Skybox (authoritative sky gradient) --- // --- Skybox (authoritative sky gradient, DBC-driven colors) ---
if (skybox_) { if (skybox_) {
skybox_->render(cmd, perFrameSet, params.timeOfDay); skybox_->render(cmd, perFrameSet, params);
} }
// --- Procedural stars (debug / fallback) --- // --- Procedural stars (debug / fallback) ---
@ -133,15 +133,17 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
&params.directionalDir, &params.sunColor, params.gameTime); &params.directionalDir, &params.sunColor, params.gameTime);
} }
// --- Clouds --- // --- Clouds (DBC-driven colors + sun lighting) ---
if (clouds_) { if (clouds_) {
clouds_->render(cmd, perFrameSet, params.timeOfDay); clouds_->render(cmd, perFrameSet, params);
} }
// --- Lens flare --- // --- Lens flare (attenuated by atmosphere) ---
if (lensFlare_) { if (lensFlare_) {
glm::vec3 sunPos = getSunPosition(params); glm::vec3 sunPos = getSunPosition(params);
lensFlare_->render(cmd, camera, sunPos, params.timeOfDay); lensFlare_->render(cmd, camera, sunPos, params.timeOfDay,
params.fogDensity, params.cloudDensity,
params.weatherIntensity);
} }
} }

View file

@ -1,4 +1,5 @@
#include "rendering/skybox.hpp" #include "rendering/skybox.hpp"
#include "rendering/sky_system.hpp"
#include "rendering/vk_context.hpp" #include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp" #include "rendering/vk_shader.hpp"
#include "rendering/vk_pipeline.hpp" #include "rendering/vk_pipeline.hpp"
@ -10,6 +11,16 @@
namespace wowee { namespace wowee {
namespace rendering { namespace rendering {
// Push constant struct — must match skybox.frag.glsl layout
struct SkyPushConstants {
glm::vec4 zenithColor; // DBC skyTopColor
glm::vec4 midColor; // DBC skyMiddleColor
glm::vec4 horizonColor; // DBC skyBand1Color
glm::vec4 fogColor; // DBC skyBand2Color / fogColor blend
glm::vec4 sunDirAndTime; // xyz = sun direction, w = timeOfDay
};
static_assert(sizeof(SkyPushConstants) == 80, "SkyPushConstants size mismatch");
Skybox::Skybox() = default; Skybox::Skybox() = default;
Skybox::~Skybox() { Skybox::~Skybox() {
@ -39,11 +50,11 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// Push constant range: horizonColor (vec4) + zenithColor (vec4) + timeOfDay (float) = 36 bytes // Push constant range: 5 x vec4 = 80 bytes
VkPushConstantRange pushRange{}; VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0; pushRange.offset = 0;
pushRange.size = sizeof(glm::vec4) + sizeof(glm::vec4) + sizeof(float); // 36 bytes pushRange.size = sizeof(SkyPushConstants); // 80 bytes
// Create pipeline layout with perFrameLayout (set 0) + push constants // Create pipeline layout with perFrameLayout (set 0) + push constants
pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange});
@ -148,24 +159,20 @@ void Skybox::shutdown() {
vkCtx = nullptr; vkCtx = nullptr;
} }
void Skybox::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float time) { void Skybox::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params) {
if (pipeline == VK_NULL_HANDLE || !renderingEnabled) { if (pipeline == VK_NULL_HANDLE || !renderingEnabled) {
return; return;
} }
// Push constant data // Compute sun direction from directionalDir (light points toward scene, sun is opposite)
struct SkyPushConstants { glm::vec3 sunDir = -glm::normalize(params.directionalDir);
glm::vec4 horizonColor;
glm::vec4 zenithColor;
float timeOfDay;
};
SkyPushConstants push{}; SkyPushConstants push{};
glm::vec3 horizon = getHorizonColor(time); push.zenithColor = glm::vec4(params.skyTopColor, 1.0f);
glm::vec3 zenith = getZenithColor(time); push.midColor = glm::vec4(params.skyMiddleColor, 1.0f);
push.horizonColor = glm::vec4(horizon, 1.0f); push.horizonColor = glm::vec4(params.skyBand1Color, 1.0f);
push.zenithColor = glm::vec4(zenith, 1.0f); push.fogColor = glm::vec4(params.skyBand2Color, 1.0f);
push.timeOfDay = time; push.sunDirAndTime = glm::vec4(sunDir, params.timeOfDay);
// Bind pipeline // Bind pipeline
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
@ -202,103 +209,5 @@ void Skybox::setTimeOfDay(float time) {
timeOfDay = time; timeOfDay = time;
} }
glm::vec3 Skybox::getHorizonColor(float time) const {
// Time-based horizon colors
// 0-6: Night (dark blue)
// 6-8: Dawn (orange/pink)
// 8-16: Day (light blue)
// 16-18: Dusk (orange/red)
// 18-24: Night (dark blue)
if (time < 5.0f || time >= 21.0f) {
// Night - dark blue/purple horizon
return glm::vec3(0.05f, 0.05f, 0.15f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to orange
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend from orange to blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - light blue horizon
return glm::vec3(0.6f, 0.7f, 0.9f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend from blue to orange/red
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend from orange to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getZenithColor(float time) const {
// Zenith (top of sky) colors
if (time < 5.0f || time >= 21.0f) {
// Night - very dark blue, almost black
return glm::vec3(0.01f, 0.01f, 0.05f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to light blue
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend to bright blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - bright blue zenith
return glm::vec3(0.2f, 0.5f, 1.0f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend to darker blue
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getSkyColor(float altitude, float time) const {
// Blend between horizon and zenith based on altitude
glm::vec3 horizon = getHorizonColor(time);
glm::vec3 zenith = getZenithColor(time);
// Use power curve for more natural gradient
float t = std::pow(std::max(altitude, 0.0f), 0.5f);
return glm::mix(horizon, zenith, t);
}
} // namespace rendering } // namespace rendering
} // namespace wowee } // namespace wowee

View file

@ -369,5 +369,134 @@ void Weather::shutdown() {
particlePositions.clear(); particlePositions.clear();
} }
// ---------------------------------------------------------------------------
// Zone-based weather configuration
// ---------------------------------------------------------------------------
void Weather::setZoneWeather(uint32_t zoneId, Type type, float minIntensity, float maxIntensity, float probability) {
zoneWeatherTable_[zoneId] = {type, minIntensity, maxIntensity, probability};
}
void Weather::initializeZoneWeatherDefaults() {
if (zoneWeatherInitialized_) return;
zoneWeatherInitialized_ = true;
// Eastern Kingdoms zones
setZoneWeather(10, Type::RAIN, 0.2f, 0.6f, 0.3f); // Duskwood — frequent rain
setZoneWeather(11, Type::RAIN, 0.1f, 0.4f, 0.15f); // Wetlands — moderate rain
setZoneWeather(8, Type::RAIN, 0.1f, 0.5f, 0.2f); // Swamp of Sorrows
setZoneWeather(33, Type::RAIN, 0.2f, 0.7f, 0.25f); // Stranglethorn Vale
setZoneWeather(44, Type::RAIN, 0.1f, 0.3f, 0.1f); // Redridge Mountains — light rain
setZoneWeather(36, Type::RAIN, 0.1f, 0.4f, 0.15f); // Alterac Mountains
setZoneWeather(45, Type::RAIN, 0.1f, 0.3f, 0.1f); // Arathi Highlands
setZoneWeather(267, Type::RAIN, 0.2f, 0.5f, 0.2f); // Hillsbrad Foothills
setZoneWeather(28, Type::RAIN, 0.1f, 0.3f, 0.1f); // Western Plaguelands — occasional rain
setZoneWeather(139, Type::RAIN, 0.1f, 0.3f, 0.1f); // Eastern Plaguelands
// Snowy zones
setZoneWeather(1, Type::SNOW, 0.2f, 0.6f, 0.3f); // Dun Morogh
setZoneWeather(51, Type::SNOW, 0.1f, 0.5f, 0.2f); // Searing Gorge (occasional)
setZoneWeather(41, Type::SNOW, 0.1f, 0.4f, 0.15f); // Deadwind Pass
setZoneWeather(2817, Type::SNOW, 0.3f, 0.7f, 0.4f); // Crystalsong Forest
setZoneWeather(67, Type::SNOW, 0.2f, 0.6f, 0.35f); // Storm Peaks
setZoneWeather(65, Type::SNOW, 0.2f, 0.5f, 0.3f); // Dragonblight
setZoneWeather(394, Type::SNOW, 0.1f, 0.4f, 0.2f); // Grizzly Hills
setZoneWeather(495, Type::SNOW, 0.3f, 0.8f, 0.5f); // Howling Fjord
setZoneWeather(210, Type::SNOW, 0.2f, 0.5f, 0.25f); // Icecrown
setZoneWeather(3537, Type::SNOW, 0.2f, 0.6f, 0.3f); // Borean Tundra
setZoneWeather(4742, Type::SNOW, 0.2f, 0.5f, 0.3f); // Hrothgar's Landing
// Kalimdor zones
setZoneWeather(15, Type::RAIN, 0.1f, 0.4f, 0.15f); // Dustwallow Marsh
setZoneWeather(16, Type::RAIN, 0.1f, 0.3f, 0.1f); // Azshara
setZoneWeather(148, Type::RAIN, 0.1f, 0.4f, 0.15f); // Darkshore
setZoneWeather(331, Type::RAIN, 0.1f, 0.3f, 0.1f); // Ashenvale
setZoneWeather(405, Type::RAIN, 0.1f, 0.3f, 0.1f); // Desolace
setZoneWeather(15, Type::RAIN, 0.2f, 0.5f, 0.2f); // Dustwallow Marsh
setZoneWeather(490, Type::RAIN, 0.1f, 0.4f, 0.15f); // Un'Goro Crater
setZoneWeather(493, Type::RAIN, 0.1f, 0.3f, 0.1f); // Moonglade
// Winterspring is snowy
setZoneWeather(618, Type::SNOW, 0.2f, 0.6f, 0.3f); // Winterspring
// Outland
setZoneWeather(3483, Type::RAIN, 0.1f, 0.3f, 0.1f); // Hellfire Peninsula (occasional)
setZoneWeather(3521, Type::RAIN, 0.1f, 0.4f, 0.15f); // Zangarmarsh
setZoneWeather(3519, Type::RAIN, 0.1f, 0.3f, 0.1f); // Terokkar Forest
}
void Weather::updateZoneWeather(uint32_t zoneId, float deltaTime) {
if (!zoneWeatherInitialized_) {
initializeZoneWeatherDefaults();
}
// Zone changed — reset weather cycle
if (zoneId != currentWeatherZone_) {
currentWeatherZone_ = zoneId;
zoneWeatherTimer_ = 0.0f;
auto it = zoneWeatherTable_.find(zoneId);
if (it == zoneWeatherTable_.end()) {
// Zone has no configured weather — clear gradually
targetIntensity_ = 0.0f;
} else {
// Roll whether weather is active based on probability
float roll = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
zoneWeatherActive_ = (roll < it->second.probability);
if (zoneWeatherActive_) {
weatherType = it->second.type;
// Random intensity within configured range
float t = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
targetIntensity_ = glm::mix(it->second.minIntensity, it->second.maxIntensity, t);
// Random cycle duration: 3-8 minutes
zoneWeatherCycleDuration_ = 180.0f + static_cast<float>(rand()) / static_cast<float>(RAND_MAX) * 300.0f;
} else {
targetIntensity_ = 0.0f;
zoneWeatherCycleDuration_ = 120.0f + static_cast<float>(rand()) / static_cast<float>(RAND_MAX) * 180.0f;
}
}
}
// Smooth intensity transitions
float transitionSpeed = 0.15f * deltaTime; // ~7 seconds to full transition
if (intensity < targetIntensity_) {
intensity = std::min(intensity + transitionSpeed, targetIntensity_);
} else if (intensity > targetIntensity_) {
intensity = std::max(intensity - transitionSpeed, targetIntensity_);
}
// If intensity reached zero and target is zero, clear weather type
if (intensity <= 0.01f && targetIntensity_ <= 0.01f) {
if (weatherType != Type::NONE) {
weatherType = Type::NONE;
particles.clear();
}
}
// Weather cycling — periodically re-roll weather
zoneWeatherTimer_ += deltaTime;
if (zoneWeatherTimer_ >= zoneWeatherCycleDuration_ && zoneWeatherCycleDuration_ > 0.0f) {
zoneWeatherTimer_ = 0.0f;
auto it = zoneWeatherTable_.find(zoneId);
if (it != zoneWeatherTable_.end()) {
float roll = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
zoneWeatherActive_ = (roll < it->second.probability);
if (zoneWeatherActive_) {
weatherType = it->second.type;
float t = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
targetIntensity_ = glm::mix(it->second.minIntensity, it->second.maxIntensity, t);
} else {
targetIntensity_ = 0.0f;
}
// New cycle duration
zoneWeatherCycleDuration_ = 180.0f + static_cast<float>(rand()) / static_cast<float>(RAND_MAX) * 300.0f;
}
}
}
} // namespace rendering } // namespace rendering
} // namespace wowee } // namespace wowee