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 c7c84413..d5875791 100644 Binary files a/assets/shaders/celestial.frag.spv and b/assets/shaders/celestial.frag.spv differ 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 7d09eb46..70b318db 100644 Binary files a/assets/shaders/clouds.frag.spv and b/assets/shaders/clouds.frag.spv differ diff --git a/assets/shaders/skybox.frag.glsl b/assets/shaders/skybox.frag.glsl index b2054e15..3c4709ee 100644 --- a/assets/shaders/skybox.frag.glsl +++ b/assets/shaders/skybox.frag.glsl @@ -14,9 +14,11 @@ layout(set = 0, binding = 0) uniform PerFrame { }; layout(push_constant) uniform Push { - vec4 horizonColor; - vec4 zenithColor; - float timeOfDay; + vec4 zenithColor; // DBC skyTopColor + vec4 midColor; // DBC skyMiddleColor + vec4 horizonColor; // DBC skyBand1Color + vec4 fogColorPush; // DBC skyBand2Color + vec4 sunDirAndTime; // xyz = sun direction, w = timeOfDay } push; layout(location = 0) in vec2 TexCoord; @@ -25,27 +27,71 @@ layout(location = 0) out vec4 outColor; void main() { // 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 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], ndcY / abs(projection[1][1]), -1.0); - // Rotate to world space: view = R*T, so R^-1 = R^T = transpose(mat3(view)) mat3 invViewRot = transpose(mat3(view)); vec3 worldDir = normalize(invViewRot * viewDir); - // worldDir.z = sin(elevation); +1 = zenith, 0 = horizon, -1 = nadir - float t = clamp(worldDir.z, 0.0, 1.0); - t = pow(t, 1.5); - vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t); - float scatter = max(0.0, 1.0 - t * 2.0) * 0.15; - sky += vec3(scatter * 0.8, scatter * 0.4, scatter * 0.1); + vec3 sunDir = push.sunDirAndTime.xyz; + float timeOfDay = push.sunDirAndTime.w; + + // Elevation: +1 = zenith, 0 = horizon, -1 = nadir + float elev = worldDir.z; + 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); } diff --git a/assets/shaders/skybox.frag.spv b/assets/shaders/skybox.frag.spv index 555b6c96..29865049 100644 Binary files a/assets/shaders/skybox.frag.spv and b/assets/shaders/skybox.frag.spv differ 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