From 6563eebb60dc206ffa4154c98d540e0209aee303 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Feb 2026 23:20:13 -0800 Subject: [PATCH] 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 --- assets/shaders/celestial.frag.glsl | 16 ++- assets/shaders/celestial.frag.spv | Bin 5160 -> 5340 bytes assets/shaders/clouds.frag.glsl | 83 +++++++++++---- assets/shaders/clouds.frag.spv | Bin 5372 -> 8476 bytes assets/shaders/skybox.frag.glsl | 78 ++++++++++++--- assets/shaders/skybox.frag.spv | Bin 3568 -> 7332 bytes include/rendering/clouds.hpp | 38 +++---- include/rendering/lens_flare.hpp | 7 +- include/rendering/lighting_manager.hpp | 2 +- include/rendering/sky_system.hpp | 7 +- include/rendering/skybox.hpp | 18 ++-- include/rendering/weather.hpp | 39 ++++++++ src/rendering/clouds.cpp | 60 +++++------ src/rendering/lens_flare.cpp | 37 ++++--- src/rendering/renderer.cpp | 25 ++++- src/rendering/sky_system.cpp | 14 +-- src/rendering/skybox.cpp | 133 ++++--------------------- src/rendering/weather.cpp | 129 ++++++++++++++++++++++++ 18 files changed, 434 insertions(+), 252 deletions(-) diff --git a/assets/shaders/celestial.frag.glsl b/assets/shaders/celestial.frag.glsl index 1dee3a05..8ccd7eb7 100644 --- a/assets/shaders/celestial.frag.glsl +++ b/assets/shaders/celestial.frag.glsl @@ -26,9 +26,21 @@ float valueNoise(vec2 p) { void main() { vec2 uv = TexCoord - 0.5; 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; + + // 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; // Animated haze/turbulence overlay for the sun disc diff --git a/assets/shaders/celestial.frag.spv b/assets/shaders/celestial.frag.spv index c7c84413e6d893311638561e2ed964e489febe86..d587579183b2d35a26739bf45c1511e28b8ee324 100644 GIT binary patch literal 5340 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 V9yzWAM-FQ?r!lYc4(apFdJSHY&>H{% diff --git a/assets/shaders/clouds.frag.glsl b/assets/shaders/clouds.frag.glsl index fec28507..1e443a2f 100644 --- a/assets/shaders/clouds.frag.glsl +++ b/assets/shaders/clouds.frag.glsl @@ -1,35 +1,42 @@ #version 450 layout(push_constant) uniform Push { - vec4 cloudColor; - float density; - float windOffset; + vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused + vec4 sunDirDensity; // xyz = sun direction, w = density + vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused } push; layout(location = 0) in vec3 vWorldDir; layout(location = 0) out vec4 outColor; -float hash(vec2 p) { - return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +// --- Gradient noise (smoother than hash-based) --- +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 f = fract(p); - f = f * f * (3.0 - 2.0 * f); - float a = hash(i); - float b = hash(i + vec2(1.0, 0.0)); - float c = hash(i + vec2(0.0, 1.0)); - float d = hash(i + vec2(1.0, 1.0)); - return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); + + // Quintic interpolation for smoother results + vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + + float a = dot(hash2(i + vec2(0.0, 0.0)) * 2.0 - 1.0, f - vec2(0.0, 0.0)); + 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 val = 0.0; float amp = 0.5; - for (int i = 0; i < 4; i++) { - val += amp * noise(p); + for (int i = 0; i < 6; i++) { + val += amp * gradientNoise(p); p *= 2.0; amp *= 0.5; } @@ -38,26 +45,60 @@ float fbm(vec2 p) { void main() { 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; - vec2 uv = dir.xy / (altitude + 0.001); // XY is the horizontal plane - uv += push.windOffset; + vec3 sunDir = push.sunDirDensity.xyz; + 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 cloud2 = fbm(uv * 1.6 + 5.0); float cloud = cloud1 * 0.7 + cloud2 * 0.3; - cloud = smoothstep(0.35, 0.65, cloud) * push.density; - float edgeBreak = noise(uv * 4.0); - cloud *= smoothstep(0.2, 0.5, edgeBreak); + // Coverage control: base coverage with detail erosion + 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); 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 alpha = cloud * edgeSoftness; if (alpha < 0.01) discard; - outColor = vec4(push.cloudColor.rgb, alpha); + outColor = vec4(cloudRgb, alpha); } diff --git a/assets/shaders/clouds.frag.spv b/assets/shaders/clouds.frag.spv index 7d09eb467a3823429dcd040c36878a0ecf280f69..70b318db25fe1f5010d293d77dcd5466ce533839 100644 GIT binary patch literal 8476 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 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); } diff --git a/assets/shaders/skybox.frag.spv b/assets/shaders/skybox.frag.spv index 555b6c96c7b4da105f2b324478cf3700c3f37837..2986504922beb75d809c4f7c6cb8f26aecd1645e 100644 GIT binary patch literal 7332 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 c-p|{5)T#*b!*y0Sw$UXnX+E zE4cOr3@_jV6d%Al-$3JyHzs~-W_~nIva1MvJ#W3^YM^Hz zGB$=*==?}!&$-r8e30Y#j>|EE4wLDxUV9Kf6Y9}X6sL*DfgTs%3!~2?XEg!ekD*+j znkno-AnisR>l)&uKiK#G_IzP%bZ5JN*bOO@wAY;&aj}-W*X{1>OHn+ehO78V-~$a* zT({1bL+j^xmEmiXm2?KHSLb5cx!oZ1Gtrhi21Y>+43VxY-2vh=b|8%J;(K5aNYi?< zV3;%L3z6<>C-B8(7^q&8K(!}<8>{R=C2(+_{K+SyYG!UQXAg8g9Vs5&# zbu4>DRWm?U7l5mpjd@q~m{{Cw=UetfyNIU)7C4@IJ^|t!bZPNVfewBdXs)^_SBKX= zrMpW%!&lGu+(nmu4#XV=?_zEqpQW`!(QT&$sUequwmJ`7lLdU~d??{i{S3spp)c{p zx%@?Zar%y2&MW+Xa{}g@NPS!`>Ggj}%f#aJ!MQE1;A=ntq^nk2sse3ei(4wL#hmip zro_4YMw37VQ}J39H-H|;=^sa&HsiRpnA1N-fp5#lt^?I8Upngzht{#ue$(M9_ybUl BdAk4r diff --git a/include/rendering/clouds.hpp b/include/rendering/clouds.hpp index fb8008ba..800b5c0f 100644 --- a/include/rendering/clouds.hpp +++ b/include/rendering/clouds.hpp @@ -9,45 +9,37 @@ namespace wowee { namespace rendering { class VkContext; +struct SkyParams; /** * Procedural cloud renderer (Vulkan) * * 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: * set 0 = perFrameLayout (camera UBO — view, projection, etc.) - * push = CloudPush (vec4 cloudColor + float density + float windOffset = 24 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). + * push = CloudPush (3 x vec4 = 48 bytes) */ class Clouds { public: 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); void shutdown(); void recreatePipelines(); /** - * Render clouds. + * Render clouds using DBC-driven colors and sun lighting. * @param cmd Command buffer to record into * @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). - * @param deltaTime Seconds since last frame */ void update(float deltaTime); @@ -56,7 +48,6 @@ public: bool isEnabled() const { return enabled_; } // --- Cloud parameters --- - /** Cloud coverage, 0 = clear, 1 = overcast. */ void setDensity(float density); float getDensity() const { return density_; } @@ -66,19 +57,16 @@ public: private: // Push constant block — must match clouds.frag.glsl struct CloudPush { - glm::vec4 cloudColor; // 16 bytes (xyz = colour, w unused) - float density; // 4 bytes - float windOffset; // 4 bytes - // total = 24 bytes + glm::vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused + glm::vec4 sunDirDensity; // xyz = sun direction, w = density + glm::vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused }; - static_assert(sizeof(CloudPush) == 24, "CloudPush size mismatch"); + static_assert(sizeof(CloudPush) == 48, "CloudPush size mismatch"); void generateMesh(); void createBuffers(); void destroyBuffers(); - glm::vec3 getCloudColor(float timeOfDay) const; - // Vulkan objects VkContext* vkCtx_ = nullptr; VkPipeline pipeline_ = VK_NULL_HANDLE; @@ -95,14 +83,14 @@ private: // Cloud parameters bool enabled_ = true; - float density_ = 0.5f; + float density_ = 0.35f; float windSpeed_ = 1.0f; - float windOffset_ = 0.0f; // Accumulated wind movement + float windOffset_ = 0.0f; // Mesh generation parameters static constexpr int SEGMENTS = 32; static constexpr int RINGS = 8; - static constexpr float RADIUS = 900.0f; // Slightly smaller than skybox + static constexpr float RADIUS = 900.0f; }; } // namespace rendering diff --git a/include/rendering/lens_flare.hpp b/include/rendering/lens_flare.hpp index 1578dd76..f4dac6fd 100644 --- a/include/rendering/lens_flare.hpp +++ b/include/rendering/lens_flare.hpp @@ -47,8 +47,13 @@ public: * @param camera The camera to render from * @param sunPosition World-space sun position * @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 diff --git a/include/rendering/lighting_manager.hpp b/include/rendering/lighting_manager.hpp index f6b23627..11c079a5 100644 --- a/include/rendering/lighting_manager.hpp +++ b/include/rendering/lighting_manager.hpp @@ -28,7 +28,7 @@ struct LightingParams { glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f}; // Sky band 1 glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; // Sky band 2 - float cloudDensity = 1.0f; // Cloud density/opacity + float cloudDensity = 0.3f; // Cloud density/opacity float horizonGlow = 0.3f; // Horizon glow intensity }; diff --git a/include/rendering/sky_system.hpp b/include/rendering/sky_system.hpp index 740e3870..6b3d9c47 100644 --- a/include/rendering/sky_system.hpp +++ b/include/rendering/sky_system.hpp @@ -31,9 +31,10 @@ struct SkyParams { 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 + float cloudDensity = 0.0f; // 0-1 + float fogDensity = 0.0f; // 0-1 + float horizonGlow = 0.3f; // 0-1 + float weatherIntensity = 0.0f; // 0-1 (rain/snow intensity, attenuates lens flare) // Time float timeOfDay = 12.0f; // 0-24 hours diff --git a/include/rendering/skybox.hpp b/include/rendering/skybox.hpp index 5e20c28c..51e098cd 100644 --- a/include/rendering/skybox.hpp +++ b/include/rendering/skybox.hpp @@ -9,6 +9,7 @@ namespace wowee { namespace rendering { class VkContext; +struct SkyParams; /** * Skybox renderer @@ -16,6 +17,9 @@ class VkContext; * Renders an atmospheric sky gradient using a fullscreen triangle. * No vertex buffer: 3 vertices cover the entire screen via gl_VertexIndex. * 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 { public: @@ -27,12 +31,12 @@ public: void recreatePipelines(); /** - * Render the skybox + * Render the skybox using DBC-driven sky colors. * @param cmd Command buffer to record into * @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 @@ -58,15 +62,7 @@ public: */ void update(float deltaTime); - /** - * Get horizon color for fog (public for fog system) - */ - glm::vec3 getHorizonColor(float time) const; - private: - glm::vec3 getSkyColor(float altitude, float time) const; - glm::vec3 getZenithColor(float time) const; - VkContext* vkCtx = nullptr; VkPipeline pipeline = VK_NULL_HANDLE; diff --git a/include/rendering/weather.hpp b/include/rendering/weather.hpp index a971e02c..b92c963d 100644 --- a/include/rendering/weather.hpp +++ b/include/rendering/weather.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace wowee { namespace rendering { @@ -79,6 +80,35 @@ public: */ 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 */ @@ -120,6 +150,15 @@ private: static constexpr int MAX_PARTICLES = 2000; 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 + + // Zone-based weather + std::unordered_map 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 diff --git a/src/rendering/clouds.cpp b/src/rendering/clouds.cpp index 22dcf342..eb2a5a25 100644 --- a/src/rendering/clouds.cpp +++ b/src/rendering/clouds.cpp @@ -1,4 +1,5 @@ #include "rendering/clouds.hpp" +#include "rendering/sky_system.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_shader.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); // ------------------------------------------------------------------ push constants - // Fragment-only push: vec4 cloudColor + float density + float windOffset = 24 bytes + // Fragment-only push: 3 x vec4 = 48 bytes VkPushConstantRange pushRange{}; pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.offset = 0; - pushRange.size = sizeof(CloudPush); // 24 bytes + pushRange.size = sizeof(CloudPush); // 48 bytes // ------------------------------------------------------------------ pipeline layout pipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {pushRange}); @@ -54,10 +55,9 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { } // ------------------------------------------------------------------ vertex input - // Vertex: vec3 pos only, stride = 12 bytes VkVertexInputBindingDescription binding{}; binding.binding = 0; - binding.stride = sizeof(glm::vec3); // 12 bytes + binding.stride = sizeof(glm::vec3); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; VkVertexInputAttributeDescription posAttr{}; @@ -77,7 +77,7 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setVertexInput({binding}, {posAttr}) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .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()) .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) @@ -122,7 +122,6 @@ void Clouds::recreatePipelines() { VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); - // Vertex input (same as initialize) VkVertexInputBindingDescription binding{}; binding.binding = 0; binding.stride = sizeof(glm::vec3); @@ -182,25 +181,35 @@ void Clouds::shutdown() { // 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) { 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{}; - push.cloudColor = glm::vec4(color, 1.0f); - push.density = density_; - push.windOffset = windOffset_; + push.cloudColor = glm::vec4(cloudBaseColor, 1.0f); + push.sunDirDensity = glm::vec4(sunDir, density_); + push.windAndLight = glm::vec4(windOffset_, sunIntensity, ambient, 0.0f); 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_, 0, 1, &perFrameSet, 0, nullptr); - // Push cloud params to fragment shader vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); @@ -223,29 +232,6 @@ void Clouds::update(float deltaTime) { 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 // --------------------------------------------------------------------------- @@ -265,7 +251,7 @@ void Clouds::generateMesh() { // Upper hemisphere — Z-up world: altitude goes into Z, horizontal spread in X/Y for (int ring = 0; ring <= RINGS; ++ring) { float phi = (ring / static_cast(RINGS)) * (static_cast(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); for (int seg = 0; seg <= SEGMENTS; ++seg) { diff --git a/src/rendering/lens_flare.cpp b/src/rendering/lens_flare.cpp index 07b3b3fd..820641af 100644 --- a/src/rendering/lens_flare.cpp +++ b/src/rendering/lens_flare.cpp @@ -211,28 +211,27 @@ void LensFlare::generateFlareElements() { flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f}); // Flare ghosts along sun-to-center axis - // These appear at various positions between sun and opposite side // Bright white ghost near sun flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f}); // 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 - 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) - flareElements.push_back({0.8f, 0.12f, glm::vec3(0.4f, 1.0f, 0.5f), 0.3f}); + // Warm amber ghost (replaced oversaturated green) + flareElements.push_back({0.8f, 0.10f, glm::vec3(0.9f, 0.75f, 0.5f), 0.2f}); // 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 - flareElements.push_back({-0.8f, 0.1f, glm::vec3(0.8f, 0.4f, 1.0f), 0.25f}); + // Faint blue ghost far from sun + flareElements.push_back({-0.8f, 0.08f, glm::vec3(0.6f, 0.5f, 0.9f), 0.15f}); - // Small red ghost - flareElements.push_back({-1.2f, 0.06f, glm::vec3(1.0f, 0.3f, 0.3f), 0.3f}); + // Small warm ghost + 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 { @@ -287,7 +286,9 @@ float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& s 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) { return; } @@ -312,6 +313,16 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec 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 glm::vec2 sunScreen = worldToScreen(camera, anchoredSunPos); 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 glm::vec2 position = sunScreen + sunToCenter * element.position; - // Apply visibility and intensity - float brightness = element.brightness * visibility * intensityMultiplier; + // Apply visibility, intensity, and atmospheric attenuation + float brightness = element.brightness * visibility * intensityMultiplier * atmosphericFactor; // Set push constants FlarePushConstants push{}; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 598f3741..f6b2387b 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2417,10 +2417,21 @@ void Renderer::update(float deltaTime) { if (weather && gh) { uint32_t wType = gh->getWeatherType(); float wInt = gh->getWeatherIntensity(); - if (wType == 1) weather->setWeatherType(Weather::Type::RAIN); - else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW); - else weather->setWeatherType(Weather::Type::NONE); - weather->setIntensity(wInt); + if (wType != 0) { + // Server-driven weather (SMSG_WEATHER) — authoritative + if (wType == 1) weather->setWeatherType(Weather::Type::RAIN); + 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(); @@ -3200,6 +3211,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { skyParams.horizonGlow = lighting.horizonGlow; } + // Weather attenuation for lens flare + if (gameHandler) { + skyParams.weatherIntensity = gameHandler->getWeatherIntensity(); + } + skyParams.skyboxModelId = 0; skyParams.skyboxHasStars = false; @@ -3866,6 +3882,7 @@ void Renderer::renderReflectionPass() { skyParams.fogDensity = lp.fogDensity; skyParams.horizonGlow = lp.horizonGlow; } + // weatherIntensity left at default 0 for reflection pass (no game handler in scope) skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams); } if (terrainRenderer && terrainEnabled) { diff --git a/src/rendering/sky_system.cpp b/src/rendering/sky_system.cpp index 3e419f77..9509cdc2 100644 --- a/src/rendering/sky_system.cpp +++ b/src/rendering/sky_system.cpp @@ -105,9 +105,9 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, return; } - // --- Skybox (authoritative sky gradient) --- + // --- Skybox (authoritative sky gradient, DBC-driven colors) --- if (skybox_) { - skybox_->render(cmd, perFrameSet, params.timeOfDay); + skybox_->render(cmd, perFrameSet, params); } // --- Procedural stars (debug / fallback) --- @@ -133,15 +133,17 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, ¶ms.directionalDir, ¶ms.sunColor, params.gameTime); } - // --- Clouds --- + // --- Clouds (DBC-driven colors + sun lighting) --- if (clouds_) { - clouds_->render(cmd, perFrameSet, params.timeOfDay); + clouds_->render(cmd, perFrameSet, params); } - // --- Lens flare --- + // --- Lens flare (attenuated by atmosphere) --- if (lensFlare_) { 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); } } diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index e85ce38c..3e0e7de6 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -1,4 +1,5 @@ #include "rendering/skybox.hpp" +#include "rendering/sky_system.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_shader.hpp" #include "rendering/vk_pipeline.hpp" @@ -10,6 +11,16 @@ namespace wowee { 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() { @@ -39,11 +50,11 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_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{}; pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; 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 pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); @@ -148,24 +159,20 @@ void Skybox::shutdown() { 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) { return; } - // Push constant data - struct SkyPushConstants { - glm::vec4 horizonColor; - glm::vec4 zenithColor; - float timeOfDay; - }; + // Compute sun direction from directionalDir (light points toward scene, sun is opposite) + glm::vec3 sunDir = -glm::normalize(params.directionalDir); SkyPushConstants push{}; - glm::vec3 horizon = getHorizonColor(time); - glm::vec3 zenith = getZenithColor(time); - push.horizonColor = glm::vec4(horizon, 1.0f); - push.zenithColor = glm::vec4(zenith, 1.0f); - push.timeOfDay = time; + push.zenithColor = glm::vec4(params.skyTopColor, 1.0f); + push.midColor = glm::vec4(params.skyMiddleColor, 1.0f); + push.horizonColor = glm::vec4(params.skyBand1Color, 1.0f); + push.fogColor = glm::vec4(params.skyBand2Color, 1.0f); + push.sunDirAndTime = glm::vec4(sunDir, params.timeOfDay); // Bind pipeline vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); @@ -202,103 +209,5 @@ void Skybox::setTimeOfDay(float 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 wowee diff --git a/src/rendering/weather.cpp b/src/rendering/weather.cpp index 20af6ab7..fed604dc 100644 --- a/src/rendering/weather.cpp +++ b/src/rendering/weather.cpp @@ -369,5 +369,134 @@ void Weather::shutdown() { 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(rand()) / static_cast(RAND_MAX); + zoneWeatherActive_ = (roll < it->second.probability); + + if (zoneWeatherActive_) { + weatherType = it->second.type; + // Random intensity within configured range + float t = static_cast(rand()) / static_cast(RAND_MAX); + targetIntensity_ = glm::mix(it->second.minIntensity, it->second.maxIntensity, t); + // Random cycle duration: 3-8 minutes + zoneWeatherCycleDuration_ = 180.0f + static_cast(rand()) / static_cast(RAND_MAX) * 300.0f; + } else { + targetIntensity_ = 0.0f; + zoneWeatherCycleDuration_ = 120.0f + static_cast(rand()) / static_cast(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(rand()) / static_cast(RAND_MAX); + zoneWeatherActive_ = (roll < it->second.probability); + + if (zoneWeatherActive_) { + weatherType = it->second.type; + float t = static_cast(rand()) / static_cast(RAND_MAX); + targetIntensity_ = glm::mix(it->second.minIntensity, it->second.maxIntensity, t); + } else { + targetIntensity_ = 0.0f; + } + + // New cycle duration + zoneWeatherCycleDuration_ = 180.0f + static_cast(rand()) / static_cast(RAND_MAX) * 300.0f; + } + } +} + } // namespace rendering } // namespace wowee