From 023bb10cc4c474320ffc491af2e67143fb7a8b72 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 10:22:29 +0000 Subject: [PATCH 01/12] Add movable lunar disk and moon directional light Add moon support to the scene and editor: - Add moon parameters to WeatherComponent (direction, color, intensity, angular radius, phase, texture, mip bias). - Create EnsureMoonLight to auto-create/sync a directional moon light entity (created automatically when terrain/sun is added). - Render lunar disk in the sky shader and expose a moon texture picker in the Weather window. - Expose moon controls to Lua and serialize moon settings with the scene. - Wire editor UI (Weather panel) to control moon orientation and appearance. --- Editor/TerrainWindow.cpp | 2 + Editor/WeatherWindow.cpp | 197 ++++++++++++++++++- Editor/WeatherWindow.h | 10 + WickedEngine/ArchiveVersionHistory.txt | 4 + WickedEngine/shaders/ShaderInterop_Weather.h | 8 +- WickedEngine/shaders/globals.hlsli | 13 ++ WickedEngine/shaders/skyHF.hlsli | 46 +++++ WickedEngine/wiArchive.cpp | 2 +- WickedEngine/wiScene.cpp | 194 ++++++++++++++++-- WickedEngine/wiScene.h | 3 +- WickedEngine/wiScene_BindLua.cpp | 7 + WickedEngine/wiScene_BindLua.h | 21 ++ WickedEngine/wiScene_Components.h | 14 ++ WickedEngine/wiScene_Serializers.cpp | 85 +++++++- 14 files changed, 589 insertions(+), 17 deletions(-) diff --git a/Editor/TerrainWindow.cpp b/Editor/TerrainWindow.cpp index 66a0652743..ce52c52a3a 100644 --- a/Editor/TerrainWindow.cpp +++ b/Editor/TerrainWindow.cpp @@ -1790,6 +1790,8 @@ void TerrainWindow::SetupAssets() transform.Translate(XMFLOAT3(0, 4, 0)); } + // currentScene.EnsureMoonLight(currentScene.weather); + presetCombo.SetSelected(0); editor->paintToolWnd.RecreateTerrainMaterialButtons(); diff --git a/Editor/WeatherWindow.cpp b/Editor/WeatherWindow.cpp index 50e7c389ae..83c06c1dc5 100644 --- a/Editor/WeatherWindow.cpp +++ b/Editor/WeatherWindow.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "WeatherWindow.h" +#include using namespace wi::ecs; using namespace wi::scene; @@ -9,7 +10,7 @@ void WeatherWindow::Create(EditorComponent* _editor) { editor = _editor; wi::gui::Window::Create(ICON_WEATHER " Weather", wi::gui::Window::WindowControls::COLLAPSE | wi::gui::Window::WindowControls::CLOSE | wi::gui::Window::WindowControls::FIT_ALL_WIDGETS_VERTICAL); - SetSize(XMFLOAT2(660, 2140)); + SetSize(XMFLOAT2(660, 2260)); closeButton.SetTooltip("Delete WeatherComponent"); OnClose([=](wi::gui::EventArgs args) { @@ -63,6 +64,7 @@ void WeatherWindow::Create(EditorComponent* _editor) colorComboBox.AddItem("Cloud extinction 1"); colorComboBox.AddItem("Cloud extinction 2"); colorComboBox.AddItem("Rain color"); + colorComboBox.AddItem("Moon color"); colorComboBox.SetTooltip("Choose the destination data of the color picker."); colorComboBox.SetMaxVisibleItemCount(100); AddWidget(&colorComboBox); @@ -106,6 +108,11 @@ void WeatherWindow::Create(EditorComponent* _editor) case 9: weather.rain_color = args.color.toFloat4(); break; + case 10: + weather.moonColor = args.color.toFloat3(); + editor->GetCurrentScene().EnsureMoonLight(weather); + InvalidateProbes(); + break; } }); AddWidget(&colorPicker); @@ -233,6 +240,113 @@ void WeatherWindow::Create(EditorComponent* _editor) }); AddWidget(&starsSlider); + moonAzimuthSlider.Create(0, 360, 0, 10000, "Moon Azimuth: "); + moonAzimuthSlider.SetTooltip("Horizontal moon orientation in degrees (0° faces +Z)."); + moonAzimuthSlider.SetSize(XMFLOAT2(wid, hei)); + moonAzimuthSlider.SetPos(XMFLOAT2(x, y += step)); + moonAzimuthSlider.OnSlide([this](wi::gui::EventArgs args) { + UpdateMoonDirection(); + }); + AddWidget(&moonAzimuthSlider); + + moonElevationSlider.Create(-90, 90, -10, 10000, "Moon Elevation: "); + moonElevationSlider.SetTooltip("Vertical moon orientation in degrees (0° on horizon)."); + moonElevationSlider.SetSize(XMFLOAT2(wid, hei)); + moonElevationSlider.SetPos(XMFLOAT2(x, y += step)); + moonElevationSlider.OnSlide([this](wi::gui::EventArgs args) { + UpdateMoonDirection(); + }); + AddWidget(&moonElevationSlider); + + moonSizeSlider.Create(0.05f, 5.0f, 0.5f, 10000, "Moon Size (deg): "); + moonSizeSlider.SetTooltip("Apparent angular size of the moon."); + moonSizeSlider.SetSize(XMFLOAT2(wid, hei)); + moonSizeSlider.SetPos(XMFLOAT2(x, y += step)); + moonSizeSlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonSize = wi::math::DegreesToRadians(args.fValue); + }); + AddWidget(&moonSizeSlider); + + moonGlowSizeSlider.Create(0.0f, 20.0f, 2.0f, 10000, "Moon Glow (deg): "); + moonGlowSizeSlider.SetTooltip("Additional halo radius around the moon."); + moonGlowSizeSlider.SetSize(XMFLOAT2(wid, hei)); + moonGlowSizeSlider.SetPos(XMFLOAT2(x, y += step)); + moonGlowSizeSlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonGlowSize = wi::math::DegreesToRadians(args.fValue); + }); + AddWidget(&moonGlowSizeSlider); + + moonGlowSharpnessSlider.Create(0.2f, 8.0f, 2.0f, 10000, "Moon Glow Sharpness: "); + moonGlowSharpnessSlider.SetTooltip("Controls how quickly the halo fades."); + moonGlowSharpnessSlider.SetSize(XMFLOAT2(wid, hei)); + moonGlowSharpnessSlider.SetPos(XMFLOAT2(x, y += step)); + moonGlowSharpnessSlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonGlowSharpness = args.fValue; + }); + AddWidget(&moonGlowSharpnessSlider); + + moonGlowIntensitySlider.Create(0.0f, 2.0f, 0.25f, 10000, "Moon Glow Intensity: "); + moonGlowIntensitySlider.SetTooltip("Halo brightness multiplier."); + moonGlowIntensitySlider.SetSize(XMFLOAT2(wid, hei)); + moonGlowIntensitySlider.SetPos(XMFLOAT2(x, y += step)); + moonGlowIntensitySlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonGlowIntensity = args.fValue; + }); + AddWidget(&moonGlowIntensitySlider); + + moonLightIntensitySlider.Create(0.0f, 5.0f, 0.05f, 1000, "Moon Light Intensity: "); + moonLightIntensitySlider.SetTooltip("Illuminance emitted by the moon directional light (in lux). Set to 0 to disable moon lighting."); + moonLightIntensitySlider.SetSize(XMFLOAT2(wid, hei)); + moonLightIntensitySlider.SetPos(XMFLOAT2(x, y += step)); + moonLightIntensitySlider.OnSlide([this](wi::gui::EventArgs args) { + auto& weather = GetWeather(); + weather.moonLightIntensity = args.fValue; + editor->GetCurrentScene().EnsureMoonLight(weather); + InvalidateProbes(); + }); + AddWidget(&moonLightIntensitySlider); + + moonTextureButton.Create("Load Moon Texture"); + moonTextureButton.SetTooltip("Load a dedicated texture for the moon disk. Click again to clear."); + moonTextureButton.SetSize(XMFLOAT2(mod_wid, hei)); + moonTextureButton.SetPos(XMFLOAT2(mod_x, y += step)); + moonTextureButton.OnClick([=](wi::gui::EventArgs args) { + auto& weather = GetWeather(); + if (!weather.moonTexture.IsValid()) + { + wi::helper::FileDialogParams params; + params.type = wi::helper::FileDialogParams::OPEN; + params.description = "Texture"; + params.extensions = wi::resourcemanager::GetSupportedImageExtensions(); + wi::helper::FileDialog(params, [=](std::string fileName) { + wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) { + auto& weather = GetWeather(); + weather.moonTextureName = fileName; + weather.moonTexture = wi::resourcemanager::Load(fileName); + moonTextureButton.SetText(wi::helper::GetFileNameFromPath(fileName)); + InvalidateProbes(); + }); + }); + } + else + { + weather.moonTexture = {}; + weather.moonTextureName.clear(); + moonTextureButton.SetText("Load Moon Texture"); + InvalidateProbes(); + } + }); + AddWidget(&moonTextureButton); + + moonTextureMipBiasSlider.Create(-4.0f, 4.0f, 0.0f, 1000, "Moon Texture Mip Bias: "); + moonTextureMipBiasSlider.SetTooltip("Adjust mip bias when sampling the moon texture for sharper or softer results."); + moonTextureMipBiasSlider.SetSize(XMFLOAT2(wid, hei)); + moonTextureMipBiasSlider.SetPos(XMFLOAT2(x, y += step)); + moonTextureMipBiasSlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonTextureMipBias = args.fValue; + }); + AddWidget(&moonTextureMipBiasSlider); + skyRotationSlider.Create(0, 360, 0, 10000, "Sky Texture Rotation: "); skyRotationSlider.SetTooltip("Rotate the sky texture horizontally. (If using a sky texture)"); skyRotationSlider.SetSize(XMFLOAT2(wid, hei)); @@ -1053,6 +1167,15 @@ void WeatherWindow::UpdateData() colorgradingButton.SetText(wi::helper::GetFileNameFromPath(weather.colorGradingMapName)); } + if (!weather.moonTextureName.empty()) + { + moonTextureButton.SetText(wi::helper::GetFileNameFromPath(weather.moonTextureName)); + } + else + { + moonTextureButton.SetText("Load Moon Texture"); + } + if (!weather.volumetricCloudsWeatherMapFirstName.empty()) { volumetricCloudsWeatherMapFirstButton.SetText(wi::helper::GetFileNameFromPath(weather.volumetricCloudsWeatherMapFirstName)); @@ -1075,6 +1198,31 @@ void WeatherWindow::UpdateData() windRandomnessSlider.SetValue(weather.windRandomness); skyExposureSlider.SetValue(weather.skyExposure); starsSlider.SetValue(weather.stars); + + { + XMFLOAT3 moonDir = weather.moonDirection; + if (wi::math::LengthSquared(moonDir) < 1e-6f) + { + moonDir = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + } + XMVECTOR moonDirVec = XMVector3Normalize(XMLoadFloat3(&moonDir)); + XMFLOAT3 moonDirNorm; + XMStoreFloat3(&moonDirNorm, moonDirVec); + float azimuth = wi::math::RadiansToDegrees(std::atan2(moonDirNorm.x, moonDirNorm.z)); + if (azimuth < 0) + { + azimuth += 360.0f; + } + moonAzimuthSlider.SetValue(azimuth); + float elevation = wi::math::RadiansToDegrees(std::asin(wi::math::Clamp(moonDirNorm.y, -1.0f, 1.0f))); + moonElevationSlider.SetValue(elevation); + } + moonSizeSlider.SetValue(wi::math::RadiansToDegrees(weather.moonSize)); + moonGlowSizeSlider.SetValue(wi::math::RadiansToDegrees(weather.moonGlowSize)); + moonGlowSharpnessSlider.SetValue(weather.moonGlowSharpness); + moonGlowIntensitySlider.SetValue(weather.moonGlowIntensity); + moonLightIntensitySlider.SetValue(weather.moonLightIntensity); + moonTextureMipBiasSlider.SetValue(weather.moonTextureMipBias); skyRotationSlider.SetValue(wi::math::RadiansToDegrees(weather.sky_rotation)); rainAmountSlider.SetValue(weather.rain_amount); rainLengthSlider.SetValue(weather.rain_length); @@ -1116,6 +1264,9 @@ void WeatherWindow::UpdateData() case 9: colorPicker.SetPickColor(wi::Color::fromFloat4(weather.rain_color)); break; + case 10: + colorPicker.SetPickColor(wi::Color::fromFloat3(weather.moonColor)); + break; } realisticskyCheckBox.SetCheck(weather.IsRealisticSky()); @@ -1204,6 +1355,15 @@ void WeatherWindow::UpdateData() colorgradingButton.SetText("Load Color Grading LUT"); } + if (weather.moonTexture.IsValid()) + { + moonTextureButton.SetText(wi::helper::GetFileNameFromPath(weather.moonTextureName)); + } + else + { + moonTextureButton.SetText("Load Moon Texture"); + } + if (weather.volumetricCloudsWeatherMapFirst.IsValid()) { volumetricCloudsWeatherMapFirstButton.SetText(wi::helper::GetFileNameFromPath(weather.volumetricCloudsWeatherMapFirstName)); @@ -1230,6 +1390,14 @@ void WeatherWindow::UpdateData() scene.weather.horizon = default_sky_horizon; scene.weather.fogStart = std::numeric_limits::max(); scene.weather.fogDensity = 0; + scene.weather.moonColor = XMFLOAT3(0.04f, 0.04f, 0.05f); + scene.weather.moonDirection = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + scene.weather.moonSize = 0.0095f; + scene.weather.moonGlowSize = 0.03f; + scene.weather.moonGlowSharpness = 2.0f; + scene.weather.moonGlowIntensity = 0.25f; + scene.weather.moonLightIntensity = 0.05f; + scene.weather.moonTextureMipBias = 0; } } @@ -1263,6 +1431,24 @@ void WeatherWindow::UpdateWind() XMStoreFloat3(&GetWeather().windDirection, dir); } +void WeatherWindow::UpdateMoonDirection() +{ + float azimuth = wi::math::DegreesToRadians(moonAzimuthSlider.GetValue()); + float elevation = wi::math::DegreesToRadians(moonElevationSlider.GetValue()); + float cosElev = std::cos(elevation); + XMFLOAT3 dir = { + std::sin(azimuth) * cosElev, + std::sin(elevation), + std::cos(azimuth) * cosElev + }; + XMVECTOR dirVec = XMVector3Normalize(XMLoadFloat3(&dir)); + XMFLOAT3 normalized; + XMStoreFloat3(&normalized, dirVec); + GetWeather().moonDirection = normalized; + editor->GetCurrentScene().EnsureMoonLight(GetWeather()); + InvalidateProbes(); +} + void WeatherWindow::ResizeLayout() { wi::gui::Window::ResizeLayout(); @@ -1291,6 +1477,15 @@ void WeatherWindow::ResizeLayout() layout.add(windRandomnessSlider); layout.add(skyExposureSlider); layout.add(starsSlider); + layout.add(moonAzimuthSlider); + layout.add(moonElevationSlider); + layout.add(moonSizeSlider); + layout.add(moonGlowSizeSlider); + layout.add(moonGlowSharpnessSlider); + layout.add(moonGlowIntensitySlider); + layout.add(moonLightIntensitySlider); + layout.add_fullwidth(moonTextureButton); + layout.add(moonTextureMipBiasSlider); layout.add(skyRotationSlider); layout.add(rainAmountSlider); layout.add(rainLengthSlider); diff --git a/Editor/WeatherWindow.h b/Editor/WeatherWindow.h index aacb7e541e..11fd5e96d3 100644 --- a/Editor/WeatherWindow.h +++ b/Editor/WeatherWindow.h @@ -4,6 +4,7 @@ class EditorComponent; class WeatherWindow : public wi::gui::Window { void UpdateWind(); + void UpdateMoonDirection(); public: void Create(EditorComponent* editor); @@ -35,6 +36,14 @@ class WeatherWindow : public wi::gui::Window wi::gui::Slider skyExposureSlider; wi::gui::Slider starsSlider; wi::gui::Slider skyRotationSlider; + wi::gui::Slider moonAzimuthSlider; + wi::gui::Slider moonElevationSlider; + wi::gui::Slider moonSizeSlider; + wi::gui::Slider moonGlowSizeSlider; + wi::gui::Slider moonGlowSharpnessSlider; + wi::gui::Slider moonGlowIntensitySlider; + wi::gui::Slider moonLightIntensitySlider; + wi::gui::Slider moonTextureMipBiasSlider; wi::gui::Slider rainAmountSlider; wi::gui::Slider rainLengthSlider; wi::gui::Slider rainSpeedSlider; @@ -45,6 +54,7 @@ class WeatherWindow : public wi::gui::Window wi::gui::CheckBox realisticskyHighQualityCheckBox; wi::gui::CheckBox realisticskyReceiveShadowCheckBox; wi::gui::Button skyButton; + wi::gui::Button moonTextureButton; wi::gui::Button colorgradingButton; // ocean params: diff --git a/WickedEngine/ArchiveVersionHistory.txt b/WickedEngine/ArchiveVersionHistory.txt index 97d23220d7..a53ce089c3 100644 --- a/WickedEngine/ArchiveVersionHistory.txt +++ b/WickedEngine/ArchiveVersionHistory.txt @@ -1,5 +1,9 @@ This file contains changelog of wi::Archive versions +97: serialized WeatherComponent moon light intensity +96: serialized WeatherComponent moon texture mip bias +95: serialized WeatherComponent moon texture path +94: serialized WeatherComponent moon parameters 93: DDGI changed to store irradiance in spherical harmonics instead of octahedral atlas 92: added support for compressed archive 91: thumbnail image support for Archive diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index 53e04ae733..57fc8b152b 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -357,6 +357,8 @@ struct alignas(16) ShaderWeather { uint2 sun_direction; // packed half3 uint2 sun_color; // packed half3 + uint2 moon_direction; // packed half3 + uint2 moon_color; // packed half3 uint2 ambient; // packed half3 uint most_important_light_index; @@ -366,8 +368,10 @@ struct alignas(16) ShaderWeather uint2 zenith; // packed half3 float4 stars_rotation; // quaternion - - float3 padding_stars; + float4 moon_params; // x=size(rad), y=halo size(rad), z=halo sharpness, w=halo intensity + int moon_texture; // bindless descriptor index for moon texture (SRV), -1 if unused + float moon_texture_mip_bias; // optional mip bias when sampling moon texture + float2 moon_texture_padding; float sky_rotation_sin; float sky_rotation_cos; diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index d2e98f9734..9d2780c29f 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -973,6 +973,19 @@ float acosFastPositive(float x) } inline half3 GetSunColor() { return unpack_half3(GetWeather().sun_color); } // sun color with intensity applied +inline float3 GetMoonDirection() +{ + float3 dir = unpack_half3(GetWeather().moon_direction); + float len_sq = dot(dir, dir); + return len_sq > 0 ? dir * rsqrt(len_sq) : float3(0.0f, 0.5f, 0.8660254f); +} +inline half3 GetMoonColor() { return unpack_half3(GetWeather().moon_color); } +inline float GetMoonSize() { return GetWeather().moon_params.x; } +inline float GetMoonHaloSize() { return GetWeather().moon_params.y; } +inline float GetMoonHaloSharpness() { return GetWeather().moon_params.z; } +inline float GetMoonHaloIntensity() { return GetWeather().moon_params.w; } +inline bool HasMoonTexture() { return GetWeather().moon_texture >= 0; } +inline float GetMoonTextureMipBias() { return GetWeather().moon_texture_mip_bias; } inline half3 GetSunDirection() { return normalize(unpack_half3(GetWeather().sun_direction)); } inline half3 GetHorizonColor() { return unpack_half3(GetWeather().horizon); } inline half3 GetZenithColor() { return unpack_half3(GetWeather().zenith); } diff --git a/WickedEngine/shaders/skyHF.hlsli b/WickedEngine/shaders/skyHF.hlsli index 2a882b63ba..577230d5d4 100644 --- a/WickedEngine/shaders/skyHF.hlsli +++ b/WickedEngine/shaders/skyHF.hlsli @@ -143,6 +143,52 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, sky = lerp(GetHorizonColor(), GetZenithColor(), saturate(V.y * 0.5f + 0.5f)); } + if (!dark_enabled) + { + float moonSize = GetMoonSize(); + float3 moonColor = GetMoonColor(); + if (moonSize > 0 && dot(moonColor, moonColor) > 0) + { + float3 moonDir = GetMoonDirection(); + float cosAngle = dot(V, moonDir); + float innerEdge = cos(moonSize); + float core = smoothstep(innerEdge, cos(moonSize * 0.8f), cosAngle); + float diskMask = core; + float3 diskColor = moonColor; + if (HasMoonTexture()) + { + float3 referenceUp = abs(moonDir.y) > 0.95f ? float3(1, 0, 0) : float3(0, 1, 0); + float3 moonRight = normalize(cross(referenceUp, moonDir)); + float3 moonUp = normalize(cross(moonDir, moonRight)); + float2 local = float2(dot(V, moonRight), dot(V, moonUp)); + float invRadius = 0.5f / max(sin(moonSize), 0.0001f); + float2 moonUV = local * invRadius + 0.5f; + if (all(moonUV >= 0.0f) && all(moonUV <= 1.0f)) + { + float4 tex = bindless_textures[NonUniformResourceIndex(descriptor_index(GetWeather().moon_texture))].SampleLevel(sampler_linear_clamp, moonUV, GetMoonTextureMipBias()); + diskMask *= tex.a; + diskColor *= tex.rgb; + } + else + { + diskMask = 0.0f; + } + } + float haloContribution = 0; + float haloIntensity = GetMoonHaloIntensity(); + if (haloIntensity > 0) + { + float haloSize = max(GetMoonHaloSize(), 0.0001f); + float haloRadius = moonSize + haloSize; + float halo = smoothstep(cos(haloRadius), innerEdge, cosAngle); + halo = pow(saturate(halo), max(GetMoonHaloSharpness(), 0.0001f)); + haloContribution = halo * haloIntensity; + } + sky += moonColor * haloContribution; + sky += diskColor * diskMask; + } + } + sky *= GetWeather().sky_exposure; if (clouds_enabled && V.y > 0 && GetScene().texture_cloudmap >= 0 && !stationary) diff --git a/WickedEngine/wiArchive.cpp b/WickedEngine/wiArchive.cpp index 6e44ce9a9b..a429bedb11 100644 --- a/WickedEngine/wiArchive.cpp +++ b/WickedEngine/wiArchive.cpp @@ -15,7 +15,7 @@ namespace wi { // this should always be only INCREMENTED and only if a new serialization is implemeted somewhere! - static constexpr uint64_t __archiveVersion = 93; + static constexpr uint64_t __archiveVersion = 97; // this is the version number of which below the archive is not compatible with the current version static constexpr uint64_t __archiveVersionBarrier = 22; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 3cf899a783..c280bd7da5 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -16,6 +16,8 @@ #include "shaders/ShaderInterop_SurfelGI.h" #include "shaders/ShaderInterop_DDGI.h" +#include + #if __has_include() #include #else @@ -930,6 +932,22 @@ namespace wi::scene shaderscene.weather.sun_color = wi::math::pack_half3(weather.sunColor); shaderscene.weather.sun_direction = wi::math::pack_half3(weather.sunDirection); + auto moonDir = weather.moonDirection; + if (wi::math::LengthSquared(moonDir) < 1e-6f) + { + moonDir = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + } + else + { + XMVECTOR moonDirVec = XMVector3Normalize(XMLoadFloat3(&moonDir)); + XMStoreFloat3(&moonDir, moonDirVec); + } + shaderscene.weather.moon_color = wi::math::pack_half3(weather.moonColor); + shaderscene.weather.moon_direction = wi::math::pack_half3(moonDir); + shaderscene.weather.moon_params = XMFLOAT4(weather.moonSize, weather.moonGlowSize, weather.moonGlowSharpness, weather.moonGlowIntensity); + shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; + shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; + shaderscene.weather.moon_texture_padding = XMFLOAT2(0, 0); shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); shaderscene.weather.sky_rotation_sin = std::sin(weather.sky_rotation); @@ -1393,6 +1411,150 @@ namespace wi::scene return entity; } + + void Scene::EnsureMoonLight(WeatherComponent& weather_component) + { + if (weather_component.moonLight != INVALID_ENTITY) + { + if (!lights.Contains(weather_component.moonLight) || !transforms.Contains(weather_component.moonLight)) + { + weather_component.moonLight = INVALID_ENTITY; + } + } + + auto adopt_candidate = [&](Entity candidate) + { + if (candidate == INVALID_ENTITY) + { + return false; + } + if (!lights.Contains(candidate) || !transforms.Contains(candidate)) + { + return false; + } + LightComponent& light = *lights.GetComponent(candidate); + if (light.GetType() != LightComponent::DIRECTIONAL) + { + return false; + } + light.SetMoonLight(true); + weather_component.moonLight = candidate; + return true; + }; + + if (weather_component.moonLight == INVALID_ENTITY) + { + for (size_t i = 0; i < lights.GetCount(); ++i) + { + Entity entity = lights.GetEntity(i); + LightComponent& light = lights[i]; + if (light.IsMoonLight() && adopt_candidate(entity)) + { + break; + } + } + } + + if (weather_component.moonLight == INVALID_ENTITY) + { + for (size_t i = 0; i < lights.GetCount(); ++i) + { + Entity entity = lights.GetEntity(i); + const NameComponent* name = names.GetComponent(entity); + if (name != nullptr) + { + if (wi::helper::toLower(name->name) == "moon") + { + if (adopt_candidate(entity)) + { + break; + } + } + } + } + } + + const bool moon_active = weather_component.moonLightIntensity > 0.0f; + + if (weather_component.moonLight == INVALID_ENTITY) + { + weather_component.moonLight = Entity_CreateLight("moon"); + LightComponent& light = *lights.GetComponent(weather_component.moonLight); + light.SetType(LightComponent::DIRECTIONAL); + light.SetMoonLight(true); + light.SetCastShadow(moon_active); + light.SetVolumetricsEnabled(moon_active); + light.intensity = weather_component.moonLightIntensity; + light.color = weather_component.moonColor; + TransformComponent& transform = *transforms.GetComponent(weather_component.moonLight); + transform.Translate(XMFLOAT3(0, 4, 0)); + transform.UpdateTransform(); + } + + LightComponent* light = lights.GetComponent(weather_component.moonLight); + if (light == nullptr) + { + weather_component.moonLight = INVALID_ENTITY; + return; + } + light->SetType(LightComponent::DIRECTIONAL); + light->SetMoonLight(true); + light->SetCastShadow(moon_active); + light->SetVolumetricsEnabled(moon_active); + light->color = weather_component.moonColor; + light->intensity = weather_component.moonLightIntensity; + light->SetVolumetricCloudsEnabled(moon_active); + + TransformComponent* transform = transforms.GetComponent(weather_component.moonLight); + if (transform == nullptr) + { + return; + } + + XMVECTOR target_dir = XMLoadFloat3(&weather_component.moonDirection); + XMFLOAT3 target_dir_f; + XMStoreFloat3(&target_dir_f, target_dir); + float dir_length_sq = wi::math::LengthSquared(target_dir_f); + if (!std::isfinite(dir_length_sq) || dir_length_sq < 1e-6f) + { + target_dir = XMVectorSet(0.0f, 0.5f, 0.8660254f, 0); + } + else + { + target_dir = XMVector3Normalize(target_dir); + } + target_dir = XMVector3Normalize(target_dir); + XMVECTOR default_dir = XMVectorSet(0, 1, 0, 0); + float dot = XMVectorGetX(XMVector3Dot(default_dir, target_dir)); + dot = wi::math::Clamp(dot, -1.0f, 1.0f); + XMVECTOR rotationQuat; + if (dot > 0.9999f) + { + rotationQuat = XMQuaternionIdentity(); + } + else if (dot < -0.9999f) + { + XMVECTOR axis = XMVector3Cross(default_dir, XMVectorSet(1, 0, 0, 0)); + if (XMVectorGetX(XMVector3LengthSq(axis)) < 1e-6f) + { + axis = XMVectorSet(0, 0, 1, 0); + } + axis = XMVector3Normalize(axis); + rotationQuat = XMQuaternionRotationAxis(axis, XM_PI); + } + else + { + XMVECTOR axis = XMVector3Normalize(XMVector3Cross(default_dir, target_dir)); + float angle = std::acos(dot); + rotationQuat = XMQuaternionRotationAxis(axis, angle); + } + + XMFLOAT4 rotation; + XMStoreFloat4(&rotation, rotationQuat); + transform->rotation_local = rotation; + transform->SetDirty(); + transform->UpdateTransform(); + } Entity Scene::Entity_CreateForce( const std::string& name, const XMFLOAT3& position @@ -4972,18 +5134,21 @@ namespace wi::scene case LightComponent::DIRECTIONAL: XMStoreFloat3(&light.direction, XMVector3Normalize(XMVector3TransformNormal(XMVectorSet(0, 1, 0, 0), W))); aabb.createFromHalfWidth(XMFLOAT3(0, 0, 0), XMFLOAT3(FLT_MAX, FLT_MAX, FLT_MAX)); - locker.lock(); - if (args.jobIndex < weather.most_important_light_index) + if (!light.IsMoonLight()) { - weather.most_important_light_index = args.jobIndex; - weather.sunColor = light.color; - weather.sunColor.x *= light.intensity; - weather.sunColor.y *= light.intensity; - weather.sunColor.z *= light.intensity; - weather.sunDirection = light.direction; - weather.stars_rotation_quaternion = light.rotation; + locker.lock(); + if (args.jobIndex < weather.most_important_light_index) + { + weather.most_important_light_index = args.jobIndex; + weather.sunColor = light.color; + weather.sunColor.x *= light.intensity; + weather.sunColor.y *= light.intensity; + weather.sunColor.z *= light.intensity; + weather.sunDirection = light.direction; + weather.stars_rotation_quaternion = light.rotation; + } + locker.unlock(); } - locker.unlock(); break; case LightComponent::SPOT: XMStoreFloat3(&light.direction, XMVector3Normalize(XMVector3TransformNormal(XMVectorSet(0, 1, 0, 0), W))); @@ -5268,7 +5433,9 @@ namespace wi::scene { if (weathers.GetCount() > 0) { - weather = weathers[0]; + WeatherComponent& primary_weather = weathers[0]; + EnsureMoonLight(primary_weather); + weather = primary_weather; weather.most_important_light_index = ~0; if (weather.IsOceanEnabled() && !ocean.IsValid()) @@ -5300,6 +5467,11 @@ namespace wi::scene } ocean.occlusionQueries[queryheap_idx] = -1; // invalidate query } + // else + // { + // EnsureMoonLight(weather); + // weather.most_important_light_index = ~0; + // } if (ocean.IsValid()) { diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index fda44fe3e8..315a714c52 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -47,7 +47,7 @@ namespace wi::scene wi::ecs::ComponentManager& animation_datas = componentLibrary.Register("wi::scene::Scene::animation_datas"); wi::ecs::ComponentManager& emitters = componentLibrary.Register("wi::scene::Scene::emitters", 2); // version = 2 wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 3); // version = 3 - wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 6); // version = 6 + wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 10); // version = 10 wi::ecs::ComponentManager& sounds = componentLibrary.Register("wi::scene::Scene::sounds", 1); // version = 1 wi::ecs::ComponentManager& videos = componentLibrary.Register("wi::scene::Scene::videos", 1); // version = 1 wi::ecs::ComponentManager& inverse_kinematics = componentLibrary.Register("wi::scene::Scene::inverse_kinematics"); @@ -396,6 +396,7 @@ namespace wi::scene float outerConeAngle = XM_PIDIV4, float innerConeAngle = 0 ); + void EnsureMoonLight(WeatherComponent& weather_component); wi::ecs::Entity Entity_CreateForce( const std::string& name, const XMFLOAT3& position = XMFLOAT3(0, 0, 0) diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index 4722a0ce2a..a262977c32 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -6843,6 +6843,8 @@ Luna::FunctionType WeatherComponent_BindLua::methods[] Luna::PropertyType WeatherComponent_BindLua::properties[] = { lunaproperty(WeatherComponent_BindLua, sunColor), lunaproperty(WeatherComponent_BindLua, sunDirection), + lunaproperty(WeatherComponent_BindLua, moonColor), + lunaproperty(WeatherComponent_BindLua, moonDirection), lunaproperty(WeatherComponent_BindLua, skyExposure), lunaproperty(WeatherComponent_BindLua, horizon), lunaproperty(WeatherComponent_BindLua, zenith), @@ -6864,6 +6866,11 @@ Luna::PropertyType WeatherComponent_BindLua::propertie lunaproperty(WeatherComponent_BindLua, windWaveSize), lunaproperty(WeatherComponent_BindLua, windSpeed), lunaproperty(WeatherComponent_BindLua, stars), + lunaproperty(WeatherComponent_BindLua, moonSize), + lunaproperty(WeatherComponent_BindLua, moonGlowSize), + lunaproperty(WeatherComponent_BindLua, moonGlowSharpness), + lunaproperty(WeatherComponent_BindLua, moonGlowIntensity), + lunaproperty(WeatherComponent_BindLua, moonLightIntensity), lunaproperty(WeatherComponent_BindLua, rainAmount), lunaproperty(WeatherComponent_BindLua, rainLength), lunaproperty(WeatherComponent_BindLua, rainSpeed), diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index c62fea2059..3e91e86462 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -1589,6 +1589,13 @@ namespace wi::lua::scene { sunColor = VectorProperty(&component->sunColor); sunDirection = VectorProperty(&component->sunDirection); + moonColor = VectorProperty(&component->moonColor); + moonDirection = VectorProperty(&component->moonDirection); + moonSize = FloatProperty(&component->moonSize); + moonGlowSize = FloatProperty(&component->moonGlowSize); + moonGlowSharpness = FloatProperty(&component->moonGlowSharpness); + moonGlowIntensity = FloatProperty(&component->moonGlowIntensity); + moonLightIntensity = FloatProperty(&component->moonLightIntensity); skyExposure = FloatProperty(&component->skyExposure); horizon = VectorProperty(&component->horizon); zenith = VectorProperty(&component->zenith); @@ -1630,6 +1637,8 @@ namespace wi::lua::scene VectorProperty sunColor; VectorProperty sunDirection; + VectorProperty moonColor; + VectorProperty moonDirection; FloatProperty skyExposure; VectorProperty horizon; VectorProperty zenith; @@ -1651,6 +1660,11 @@ namespace wi::lua::scene FloatProperty windWaveSize; FloatProperty windSpeed; FloatProperty stars; + FloatProperty moonSize; + FloatProperty moonGlowSize; + FloatProperty moonGlowSharpness; + FloatProperty moonGlowIntensity; + FloatProperty moonLightIntensity; FloatProperty rainAmount; FloatProperty rainLength; FloatProperty rainSpeed; @@ -1659,6 +1673,8 @@ namespace wi::lua::scene PropertyFunction(sunColor) PropertyFunction(sunDirection) + PropertyFunction(moonColor) + PropertyFunction(moonDirection) PropertyFunction(skyExposure) PropertyFunction(horizon) PropertyFunction(zenith) @@ -1680,6 +1696,11 @@ namespace wi::lua::scene PropertyFunction(windWaveSize) PropertyFunction(windSpeed) PropertyFunction(stars) + PropertyFunction(moonSize) + PropertyFunction(moonGlowSize) + PropertyFunction(moonGlowSharpness) + PropertyFunction(moonGlowIntensity) + PropertyFunction(moonLightIntensity) PropertyFunction(rainAmount) PropertyFunction(rainLength) PropertyFunction(rainSpeed) diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index e6d7e31171..6ae79eaa6d 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1328,6 +1328,7 @@ namespace wi::scene VISUALIZER = 1 << 2, LIGHTMAPONLY_STATIC = 1 << 3, VOLUMETRICCLOUDS = 1 << 4, + MOON_LIGHT = 1 << 5, }; uint32_t _flags = EMPTY; @@ -1373,12 +1374,14 @@ namespace wi::scene constexpr void SetVisualizerEnabled(bool value) { set_flag(_flags, VISUALIZER, value); } constexpr void SetStatic(bool value) { set_flag(_flags, LIGHTMAPONLY_STATIC, value); } constexpr void SetVolumetricCloudsEnabled(bool value) { set_flag(_flags, VOLUMETRICCLOUDS, value); } + constexpr void SetMoonLight(bool value) { set_flag(_flags, MOON_LIGHT, value); } constexpr bool IsCastingShadow() const { return _flags & CAST_SHADOW; } constexpr bool IsVolumetricsEnabled() const { return _flags & VOLUMETRICS; } constexpr bool IsVisualizerEnabled() const { return _flags & VISUALIZER; } constexpr bool IsStatic() const { return _flags & LIGHTMAPONLY_STATIC; } constexpr bool IsVolumetricCloudsEnabled() const { return _flags & VOLUMETRICCLOUDS; } + constexpr bool IsMoonLight() const { return _flags & MOON_LIGHT; } constexpr bool IsInactive() const { return intensity < 0.0001f || range < 0.0001f; } constexpr float GetRange() const @@ -1856,6 +1859,14 @@ namespace wi::scene XMFLOAT3 sunColor = XMFLOAT3(0, 0, 0); XMFLOAT3 sunDirection = XMFLOAT3(0, 1, 0); + XMFLOAT3 moonColor = XMFLOAT3(0.04f, 0.04f, 0.05f); + XMFLOAT3 moonDirection = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + float moonSize = 0.0095f; // radians + float moonGlowSize = 0.03f; // additional radius in radians for halo falloff + float moonGlowSharpness = 2.0f; + float moonGlowIntensity = 0.25f; + float moonLightIntensity = 0.05f; + float moonTextureMipBias = 0; float skyExposure = 1; XMFLOAT3 horizon = XMFLOAT3(0.0f, 0.0f, 0.0f); XMFLOAT3 zenith = XMFLOAT3(0.0f, 0.0f, 0.0f); @@ -1882,18 +1893,21 @@ namespace wi::scene AtmosphereParameters atmosphereParameters; VolumetricCloudParameters volumetricCloudParameters; + std::string moonTextureName; std::string skyMapName; std::string colorGradingMapName; std::string volumetricCloudsWeatherMapFirstName; std::string volumetricCloudsWeatherMapSecondName; // Non-serialized attributes: + wi::Resource moonTexture; wi::Resource skyMap; wi::Resource colorGradingMap; wi::Resource volumetricCloudsWeatherMapFirst; wi::Resource volumetricCloudsWeatherMapSecond; XMFLOAT4 stars_rotation_quaternion = XMFLOAT4(0, 0, 0, 1); uint32_t most_important_light_index = ~0u; + wi::ecs::Entity moonLight = wi::ecs::INVALID_ENTITY; void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); }; diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index ee4b5f49b1..0794b8b0b1 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -1613,6 +1613,64 @@ namespace wi::scene archive >> _flags; archive >> sunDirection; archive >> sunColor; + moonLight = wi::ecs::INVALID_ENTITY; + const bool supports_moon_params = (seri.GetVersion() >= 7) || (archive.GetVersion() >= 94); + const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); + const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); + const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); + if (supports_moon_params) + { + archive >> moonDirection; + archive >> moonColor; + archive >> moonSize; + archive >> moonGlowSize; + archive >> moonGlowSharpness; + archive >> moonGlowIntensity; + if (supports_moon_light_intensity) + { + archive >> moonLightIntensity; + } + } + else + { + moonDirection = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + moonColor = XMFLOAT3(0.04f, 0.04f, 0.05f); + moonSize = 0.0095f; + moonGlowSize = 0.03f; + moonGlowSharpness = 2.0f; + moonGlowIntensity = 0.25f; + moonLightIntensity = 0.05f; + } + if (!supports_moon_light_intensity) + { + moonLightIntensity = 0.05f; + } + if (supports_moon_texture) + { + archive >> moonTextureName; + if (!moonTextureName.empty()) + { + moonTextureName = dir + moonTextureName; + moonTexture = wi::resourcemanager::Load(moonTextureName); + } + else + { + moonTexture = {}; + } + } + else + { + moonTextureName.clear(); + moonTexture = {}; + } + if (supports_moon_texture_bias) + { + archive >> moonTextureMipBias; + } + else + { + moonTextureMipBias = 0; + } archive >> horizon; archive >> zenith; archive >> ambient; @@ -1894,14 +1952,39 @@ namespace wi::scene } else { + const bool supports_moon_params = (seri.GetVersion() >= 7) || (archive.GetVersion() >= 94); + const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); + const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); + const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); + seri.RegisterResource(moonTextureName); seri.RegisterResource(skyMapName); seri.RegisterResource(colorGradingMapName); seri.RegisterResource(volumetricCloudsWeatherMapFirstName); seri.RegisterResource(volumetricCloudsWeatherMapSecondName); - archive << _flags; archive << sunDirection; archive << sunColor; + if (supports_moon_params) + { + archive << moonDirection; + archive << moonColor; + archive << moonSize; + archive << moonGlowSize; + archive << moonGlowSharpness; + archive << moonGlowIntensity; + if (supports_moon_light_intensity) + { + archive << moonLightIntensity; + } + } + if (supports_moon_texture) + { + archive << wi::helper::GetPathRelative(dir, moonTextureName); + } + if (supports_moon_texture_bias) + { + archive << moonTextureMipBias; + } archive << horizon; archive << zenith; archive << ambient; From d5a7a0119652af50016450ce37db33c6ed3e873a Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 10:27:21 +0000 Subject: [PATCH 02/12] Add moonlight contribution to volumetric cloud lighting Enable the moon to illuminate volumetric clouds by streaming moon lighting parameters to the GPU and sampling them during the volumetric cloud scattering pass: - Export moon direction/color/intensity/phase to GPU via ShaderInterop_Weather. - Add moon light helpers to globals.hlsli for shareable math/phase evaluation. - Update volumetricCloud_renderCS.hlsl to include moon scattering, rim-lighting, and simple self-shadowing from the lunar direction. - Ensure wiScene/Weather updates push moon parameters to shader constant buffers; add minor serialization/UI sync where needed. Note: this change affects cloud appearance at night; tune moon intensity and phase controls and rebuild shader cache after merging. --- WickedEngine/shaders/ShaderInterop_Weather.h | 3 +- WickedEngine/shaders/globals.hlsli | 2 + .../shaders/volumetricCloud_renderCS.hlsl | 79 ++++++++++++++----- WickedEngine/wiScene.cpp | 3 +- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index 57fc8b152b..55fed55821 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -371,7 +371,8 @@ struct alignas(16) ShaderWeather float4 moon_params; // x=size(rad), y=halo size(rad), z=halo sharpness, w=halo intensity int moon_texture; // bindless descriptor index for moon texture (SRV), -1 if unused float moon_texture_mip_bias; // optional mip bias when sampling moon texture - float2 moon_texture_padding; + float moon_light_intensity; // illuminance emitted by moon directional light + float moon_padding0; float sky_rotation_sin; float sky_rotation_cos; diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index 9d2780c29f..fec1726979 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -986,6 +986,8 @@ inline float GetMoonHaloSharpness() { return GetWeather().moon_params.z; } inline float GetMoonHaloIntensity() { return GetWeather().moon_params.w; } inline bool HasMoonTexture() { return GetWeather().moon_texture >= 0; } inline float GetMoonTextureMipBias() { return GetWeather().moon_texture_mip_bias; } +inline float GetMoonLightIntensity() { return GetWeather().moon_light_intensity; } +inline float3 GetMoonIlluminance() { return GetMoonColor() * GetMoonLightIntensity(); } inline half3 GetSunDirection() { return normalize(unpack_half3(GetWeather().sun_direction)); } inline half3 GetHorizonColor() { return unpack_half3(GetWeather().horizon); } inline half3 GetZenithColor() { return unpack_half3(GetWeather().zenith); } diff --git a/WickedEngine/shaders/volumetricCloud_renderCS.hlsl b/WickedEngine/shaders/volumetricCloud_renderCS.hlsl index 0d590c2cdb..ab53cd5478 100644 --- a/WickedEngine/shaders/volumetricCloud_renderCS.hlsl +++ b/WickedEngine/shaders/volumetricCloud_renderCS.hlsl @@ -434,7 +434,10 @@ float3 SampleLocalLights(float3 worldPosition) return localLightLuminance; } -void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPosition, float3 worldPosition, float3 sunDirection, float3 sunIlluminance, float cosTheta, float stepSize, float heightFraction, +void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPosition, float3 worldPosition, + float3 sunDirection, float3 sunIlluminance, float cosThetaSun, + float3 moonDirection, float3 moonIlluminance, float cosThetaMoon, + float stepSize, float heightFraction, float cloudDensityFirst, float cloudDensitySecond, float3 weatherDataFirst, float3 weatherDataSecond, LayerParameters layerParametersFirst, LayerParameters layerParametersSecond, float lod, inout float3 luminance, inout float3 transmittanceToView, inout float depthWeightedSum, inout float depthWeightsSum) @@ -443,45 +446,75 @@ void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPositi float3 albedo = SampleAlbedo(cloudDensityFirst, cloudDensitySecond, weatherDataFirst, weatherDataSecond); float3 extinction = SampleExtinction(cloudDensityFirst, cloudDensitySecond); - float3 atmosphereTransmittanceToLight = 1.0; + float3 sunAtmosphereTransmittanceToLight = 1.0; if (GetFrame().options & OPTION_BIT_REALISTIC_SKY) { - atmosphereTransmittanceToLight = GetAtmosphericLightTransmittance(atmosphere, worldPosition, sunDirection, texture_transmittancelut); // Has to be in meters + sunAtmosphereTransmittanceToLight = GetAtmosphericLightTransmittance(atmosphere, worldPosition, sunDirection, texture_transmittancelut); // Has to be in meters } - + + float3 moonAtmosphereTransmittanceToLight = 1.0; + if (GetFrame().options & OPTION_BIT_REALISTIC_SKY) + { + moonAtmosphereTransmittanceToLight = GetAtmosphericLightTransmittance(atmosphere, worldPosition, moonDirection, texture_transmittancelut); + } + // Sample participating media with multiple scattering - ParticipatingMedia participatingMedia = SampleParticipatingMedia(albedo, extinction, GetWeather().volumetric_clouds.multiScatteringScattering, GetWeather().volumetric_clouds.multiScatteringExtinction, atmosphereTransmittanceToLight); + ParticipatingMedia participatingMediaSun = SampleParticipatingMedia(albedo, extinction, GetWeather().volumetric_clouds.multiScatteringScattering, GetWeather().volumetric_clouds.multiScatteringExtinction, sunAtmosphereTransmittanceToLight); + ParticipatingMedia participatingMediaMoon = participatingMediaSun; + + [unroll] + for (int ms = 0; ms < MS_COUNT; ++ms) + { + participatingMediaMoon.transmittanceToLight[ms] = moonAtmosphereTransmittanceToLight; + } + const bool moonEnabled = dot(moonIlluminance, moonIlluminance) > 0.0; + // Sample environment lighting float3 environmentLuminance = SampleAmbientLight(heightFraction); // Only render if there is any sign of scattering (albedo * extinction) - if (any(participatingMedia.scatteringCoefficients[0] > 0.0)) + if (any(participatingMediaSun.scatteringCoefficients[0] > 0.0)) { // Sample shadows from opaque shadow maps if (GetFrame().options & OPTION_BIT_VOLUMETRICCLOUDS_RECEIVE_SHADOW) { - OpaqueShadow(participatingMedia, worldPosition); + OpaqueShadow(participatingMediaSun, worldPosition); } // Calcualte volumetric shadow - VolumetricShadow(participatingMedia, atmosphere, worldPosition, sunDirection, layerParametersFirst, layerParametersSecond, lod); + VolumetricShadow(participatingMediaSun, atmosphere, worldPosition, sunDirection, layerParametersFirst, layerParametersSecond, lod); + if (moonEnabled) + { + VolumetricShadow(participatingMediaMoon, atmosphere, worldPosition, moonDirection, layerParametersFirst, layerParametersSecond, lod); + } // Calculate bounced light from ground onto clouds const float maxTransmittanceToView = max(max(transmittanceToView.x, transmittanceToView.y), transmittanceToView.z); if (maxTransmittanceToView > 0.01f) { - VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, sunDirection, sunIlluminance, atmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); + VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, sunDirection, sunIlluminance, sunAtmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); + if (moonEnabled) + { + VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, moonDirection, moonIlluminance, moonAtmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); + } } } // Sample dual lob phase with multiple scattering - float phaseFunction = DualLobPhase(GetWeather().volumetric_clouds.phaseG, GetWeather().volumetric_clouds.phaseG2, GetWeather().volumetric_clouds.phaseBlend, -cosTheta); - ParticipatingMediaPhase participatingMediaPhase = SampleParticipatingMediaPhase(phaseFunction, GetWeather().volumetric_clouds.multiScatteringEccentricity); + float phaseFunctionSun = DualLobPhase(GetWeather().volumetric_clouds.phaseG, GetWeather().volumetric_clouds.phaseG2, GetWeather().volumetric_clouds.phaseBlend, -cosThetaSun); + ParticipatingMediaPhase participatingMediaPhaseSun = SampleParticipatingMediaPhase(phaseFunctionSun, GetWeather().volumetric_clouds.multiScatteringEccentricity); + + ParticipatingMediaPhase participatingMediaPhaseMoon; + if (moonEnabled) + { + float phaseFunctionMoon = DualLobPhase(GetWeather().volumetric_clouds.phaseG, GetWeather().volumetric_clouds.phaseG2, GetWeather().volumetric_clouds.phaseBlend, -cosThetaMoon); + participatingMediaPhaseMoon = SampleParticipatingMediaPhase(phaseFunctionMoon, GetWeather().volumetric_clouds.multiScatteringEccentricity); + } // Update depth sampling @@ -499,11 +532,13 @@ void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPositi [unroll] for (int ms = MS_COUNT - 1; ms >= 0; ms--) // Should terminate at 0 { - const float3 scatteringCoefficients = participatingMedia.scatteringCoefficients[ms]; - const float3 extinctionCoefficients = participatingMedia.extinctionCoefficients[ms]; - const float3 transmittanceToLight = participatingMedia.transmittanceToLight[ms]; - - float3 lightLuminance = transmittanceToLight * sunIlluminance * participatingMediaPhase.phase[ms]; + const float3 scatteringCoefficients = participatingMediaSun.scatteringCoefficients[ms]; + const float3 extinctionCoefficients = participatingMediaSun.extinctionCoefficients[ms]; + float3 lightLuminance = participatingMediaSun.transmittanceToLight[ms] * sunIlluminance * participatingMediaPhaseSun.phase[ms]; + if (moonEnabled) + { + lightLuminance += participatingMediaMoon.transmittanceToLight[ms] * moonIlluminance * participatingMediaPhaseMoon.phase[ms]; + } lightLuminance += (ms == 0 ? environmentLuminance : float3(0.0, 0.0, 0.0)); // only apply at last lightLuminance += localLightLuminance; @@ -624,8 +659,11 @@ void RenderClouds(uint3 DTid, float2 uv, float depth, float3 depthWorldPosition, float3 sunIlluminance = GetSunColor(); float3 sunDirection = GetSunDirection(); - - float cosTheta = dot(rayDirection, sunDirection); + float3 moonIlluminance = GetMoonIlluminance(); + float3 moonDirection = GetMoonDirection(); + + float cosThetaSun = dot(rayDirection, sunDirection); + float cosThetaMoon = dot(rayDirection, moonDirection); float3 luminance = 0.0; @@ -666,7 +704,10 @@ void RenderClouds(uint3 DTid, float2 uv, float depth, float3 depthWorldPosition, if (saturate(cloudDensityFirst + cloudDensitySecond) > CLOUD_DENSITY_THRESHOLD) { - VolumetricCloudLighting(atmosphere, rayOrigin, sampleWorldPosition, sunDirection, sunIlluminance, cosTheta, stepSize, heightFraction, + VolumetricCloudLighting(atmosphere, rayOrigin, sampleWorldPosition, + sunDirection, sunIlluminance, cosThetaSun, + moonDirection, moonIlluminance, cosThetaMoon, + stepSize, heightFraction, cloudDensityFirst, cloudDensitySecond, weatherDataFirst, weatherDataSecond, layerParametersFirst, layerParametersSecond, lod, luminance, transmittanceToView, depthWeightedSum, depthWeightsSum); diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index c280bd7da5..7ba2f48c08 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -947,7 +947,8 @@ namespace wi::scene shaderscene.weather.moon_params = XMFLOAT4(weather.moonSize, weather.moonGlowSize, weather.moonGlowSharpness, weather.moonGlowIntensity); shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; - shaderscene.weather.moon_texture_padding = XMFLOAT2(0, 0); + shaderscene.weather.moon_light_intensity = weather.moonLightIntensity; + shaderscene.weather.moon_padding0 = 0; shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); shaderscene.weather.sky_rotation_sin = std::sin(weather.sky_rotation); From 359f98c48e935235641c44c903dcf253bbf5f68a Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 11:25:30 +0000 Subject: [PATCH 03/12] Add moon shadow map support to directional lighting pipeline Extend moon lighting with shadow map support: track its directional light index alongside the sun, feed it through the renderer's light buffer, and sample the cascade array in sky/cloud shaders so moonlit scenes receive proper shadowing. --- WickedEngine/shaders/ShaderInterop_Weather.h | 2 +- WickedEngine/shaders/shadowHF.hlsli | 24 ++++++++++----- .../shaders/volumetricCloud_renderCS.hlsl | 30 ++++++++++++++----- WickedEngine/wiRenderer.cpp | 17 +++++++++++ WickedEngine/wiScene.cpp | 12 +++++++- WickedEngine/wiScene_Components.h | 1 + 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index 55fed55821..44100c5f01 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -372,7 +372,7 @@ struct alignas(16) ShaderWeather int moon_texture; // bindless descriptor index for moon texture (SRV), -1 if unused float moon_texture_mip_bias; // optional mip bias when sampling moon texture float moon_light_intensity; // illuminance emitted by moon directional light - float moon_padding0; + uint moon_light_index; float sky_rotation_sin; float sky_rotation_cos; diff --git a/WickedEngine/shaders/shadowHF.hlsli b/WickedEngine/shaders/shadowHF.hlsli index e3108a44cb..a9c2625b17 100644 --- a/WickedEngine/shaders/shadowHF.hlsli +++ b/WickedEngine/shaders/shadowHF.hlsli @@ -350,20 +350,30 @@ inline half shadow_2D_volumetricclouds(float3 P) // Sample light and furthest cascade for large mediums (volumetrics) // Used with SkyAtmosphere and Volumetric Clouds -inline bool furthest_cascade_volumetrics(inout ShaderEntity light, inout uint furthestCascade) +inline bool furthest_cascade_volumetrics(inout ShaderEntity light, inout uint furthestCascade, uint light_index) { - light = load_entity(lights().first_item() + GetWeather().most_important_light_index); - furthestCascade = light.GetShadowCascadeCount() - 1; - - if (!light.IsStaticLight() && light.IsCastingShadow() && furthestCascade >= 0) + ShaderEntityIterator dir_lights = directional_lights(); + if (light_index == ~0u || light_index >= dir_lights.item_count()) + { + return false; + } + + light = load_entity(dir_lights.first_item() + light_index); + const uint cascade_count = light.GetShadowCascadeCount(); + if (!light.IsStaticLight() && light.IsCastingShadow() && cascade_count > 0) { - // We consider this light useless if it is static, is not casting shadow and if there are no available cascades + furthestCascade = cascade_count - 1; return true; } - + return false; } +inline bool furthest_cascade_volumetrics(inout ShaderEntity light, inout uint furthestCascade) +{ + return furthest_cascade_volumetrics(light, furthestCascade, GetWeather().most_important_light_index); +} + static const float rain_blocker_head_size = 1; static const float rain_blocker_head_size_sq = rain_blocker_head_size * rain_blocker_head_size; diff --git a/WickedEngine/shaders/volumetricCloud_renderCS.hlsl b/WickedEngine/shaders/volumetricCloud_renderCS.hlsl index ab53cd5478..75f89c7315 100644 --- a/WickedEngine/shaders/volumetricCloud_renderCS.hlsl +++ b/WickedEngine/shaders/volumetricCloud_renderCS.hlsl @@ -150,7 +150,7 @@ ParticipatingMedia SampleParticipatingMedia(float3 baseAlbedo, float3 baseExtinc return participatingMedia; } -void OpaqueShadow(inout ParticipatingMedia participatingMedia, float3 worldPosition) +void OpaqueShadow(inout ParticipatingMedia participatingMedia, float3 worldPosition, uint light_index) { float shadow = 1.0f; @@ -158,7 +158,7 @@ void OpaqueShadow(inout ParticipatingMedia participatingMedia, float3 worldPosit ShaderEntity light = (ShaderEntity) 0; uint furthestCascade = 0; - if (furthest_cascade_volumetrics(light, furthestCascade)) + if (furthest_cascade_volumetrics(light, furthestCascade, light_index)) { float3 shadow_pos = mul(load_entitymatrix(light.GetMatrixIndex() + furthestCascade), float4(worldPosition, 1)).xyz; // ortho matrix, no divide by .w float3 shadow_uv = clipspace_to_uv(shadow_pos.xyz); @@ -176,7 +176,7 @@ void OpaqueShadow(inout ParticipatingMedia participatingMedia, float3 worldPosit } } -void VolumetricShadow(inout ParticipatingMedia participatingMedia, in AtmosphereParameters atmosphere, float3 worldPosition, float3 sunDirection, LayerParameters layerParametersFirst, LayerParameters layerParametersSecond, float lod) +void VolumetricShadow(inout ParticipatingMedia participatingMedia, in AtmosphereParameters atmosphere, float3 worldPosition, float3 lightDirection, LayerParameters layerParametersFirst, LayerParameters layerParametersSecond, float lod) { int ms = 0; float3 extinctionAccumulation[MS_COUNT]; @@ -204,7 +204,7 @@ void VolumetricShadow(inout ParticipatingMedia participatingMedia, in Atmosphere float t = t0 + delta * sampleSegmentT; // 5 samples: 0.02, 0.1, 0.26, 0.5, 0.82 float shadowSampleT = GetWeather().volumetric_clouds.shadowStepLength * t; - float3 samplePoint = worldPosition + sunDirection * shadowSampleT; // Step futher towards the light + float3 samplePoint = worldPosition + lightDirection * shadowSampleT; // Step futher towards the light float heightFraction = GetHeightFractionForPoint(atmosphere, samplePoint); if (heightFraction < 0.0 || heightFraction > 1.0) @@ -469,7 +469,8 @@ void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPositi } - const bool moonEnabled = dot(moonIlluminance, moonIlluminance) > 0.0; + const bool sunEnabled = GetWeather().most_important_light_index != ~0u && dot(sunIlluminance, sunIlluminance) > 0.0; + const bool moonEnabled = GetWeather().moon_light_index != ~0u && dot(moonIlluminance, moonIlluminance) > 0.0; // Sample environment lighting float3 environmentLuminance = SampleAmbientLight(heightFraction); @@ -481,11 +482,21 @@ void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPositi // Sample shadows from opaque shadow maps if (GetFrame().options & OPTION_BIT_VOLUMETRICCLOUDS_RECEIVE_SHADOW) { - OpaqueShadow(participatingMediaSun, worldPosition); + if (sunEnabled) + { + OpaqueShadow(participatingMediaSun, worldPosition, GetWeather().most_important_light_index); + } + if (moonEnabled) + { + OpaqueShadow(participatingMediaMoon, worldPosition, GetWeather().moon_light_index); + } } // Calcualte volumetric shadow - VolumetricShadow(participatingMediaSun, atmosphere, worldPosition, sunDirection, layerParametersFirst, layerParametersSecond, lod); + if (sunEnabled) + { + VolumetricShadow(participatingMediaSun, atmosphere, worldPosition, sunDirection, layerParametersFirst, layerParametersSecond, lod); + } if (moonEnabled) { VolumetricShadow(participatingMediaMoon, atmosphere, worldPosition, moonDirection, layerParametersFirst, layerParametersSecond, lod); @@ -496,7 +507,10 @@ void VolumetricCloudLighting(AtmosphereParameters atmosphere, float3 startPositi const float maxTransmittanceToView = max(max(transmittanceToView.x, transmittanceToView.y), transmittanceToView.z); if (maxTransmittanceToView > 0.01f) { - VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, sunDirection, sunIlluminance, sunAtmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); + if (sunEnabled) + { + VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, sunDirection, sunIlluminance, sunAtmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); + } if (moonEnabled) { VolumetricGroundContribution(environmentLuminance, atmosphere, worldPosition, moonDirection, moonIlluminance, moonAtmosphereTransmittanceToLight, layerParametersFirst, layerParametersSecond, lod); diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index a137fa827d..faac0f78e5 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -4662,6 +4662,10 @@ void UpdatePerFrameData( } const XMFLOAT2 atlas_dim_rcp = XMFLOAT2(1.0f / float(shadowMapAtlas.desc.width), 1.0f / float(shadowMapAtlas.desc.height)); + const uint32_t most_important_light_component_index = vis.scene->weather.most_important_light_index; + frameCB.scene.weather.most_important_light_index = ~0u; + frameCB.scene.weather.moon_light_index = ~0u; + uint32_t moon_directional_entity_index = ~0u; // Write directional lights into entity array: lightarray_offset = entityCounter; @@ -4696,6 +4700,8 @@ void UpdatePerFrameData( shaderentity.SetDirection(light.direction); shaderentity.SetColor(float4(light.color.x * light.intensity, light.color.y * light.intensity, light.color.z * light.intensity, 1)); + const uint32_t directional_entity_index = lightarray_count_directional; + const bool shadowmap = IsShadowsEnabled() && light.IsCastingShadow() && !light.IsStatic(); const wi::rectpacker::Rect& shadow_rect = vis.visibleLightShadowRects[lightIndex]; @@ -4736,11 +4742,22 @@ void UpdatePerFrameData( shaderentity.SetFlags(ENTITY_FLAG_LIGHT_VOLUMETRICCLOUDS); } + if (lightIndex == most_important_light_component_index) + { + frameCB.scene.weather.most_important_light_index = directional_entity_index; + } + if (light.IsMoonLight()) + { + moon_directional_entity_index = directional_entity_index; + } + std::memcpy(entityArray + entityCounter, &shaderentity, sizeof(ShaderEntity)); entityCounter++; lightarray_count_directional++; } + frameCB.scene.weather.moon_light_index = moon_directional_entity_index; + // Write spot lights into entity array: lightarray_offset_spot = entityCounter; for (uint32_t lightIndex : vis.visibleLights) diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 7ba2f48c08..f06862e18b 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -948,7 +948,7 @@ namespace wi::scene shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; shaderscene.weather.moon_light_intensity = weather.moonLightIntensity; - shaderscene.weather.moon_padding0 = 0; + shaderscene.weather.moon_light_index = weather.moon_light_index; shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); shaderscene.weather.sky_rotation_sin = std::sin(weather.sky_rotation); @@ -5150,6 +5150,15 @@ namespace wi::scene } locker.unlock(); } + else + { + locker.lock(); + if (args.jobIndex < weather.moon_light_index) + { + weather.moon_light_index = args.jobIndex; + } + locker.unlock(); + } break; case LightComponent::SPOT: XMStoreFloat3(&light.direction, XMVector3Normalize(XMVector3TransformNormal(XMVectorSet(0, 1, 0, 0), W))); @@ -5438,6 +5447,7 @@ namespace wi::scene EnsureMoonLight(primary_weather); weather = primary_weather; weather.most_important_light_index = ~0; + weather.moon_light_index = ~0; if (weather.IsOceanEnabled() && !ocean.IsValid()) { diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 6ae79eaa6d..a9f9e15cd5 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1907,6 +1907,7 @@ namespace wi::scene wi::Resource volumetricCloudsWeatherMapSecond; XMFLOAT4 stars_rotation_quaternion = XMFLOAT4(0, 0, 0, 1); uint32_t most_important_light_index = ~0u; + uint32_t moon_light_index = ~0u; wi::ecs::Entity moonLight = wi::ecs::INVALID_ENTITY; void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); From f1e6d966e32e8b325fbd9e889646cb27ac882d52 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 12:24:25 +0000 Subject: [PATCH 04/12] Add phase-aware shading for sky and lighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement dynamic moon phases so the visible disk, halo, and moonlight intensity depend on sun/moon alignment. The renderer now derives a phase visibility factor per frame, scales moon directional light output, and the sky shader uses that same value to shade crescents and gibbous shapes procedurally, providing consistent night lighting as sun–moon directions change. --- WickedEngine/shaders/globals.hlsli | 16 ++++++++++++- WickedEngine/shaders/skyHF.hlsli | 38 ++++++++++++++++++++++-------- WickedEngine/wiScene.cpp | 27 +++++++++++++++++---- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index fec1726979..1e7132af95 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -987,8 +987,22 @@ inline float GetMoonHaloIntensity() { return GetWeather().moon_params.w; } inline bool HasMoonTexture() { return GetWeather().moon_texture >= 0; } inline float GetMoonTextureMipBias() { return GetWeather().moon_texture_mip_bias; } inline float GetMoonLightIntensity() { return GetWeather().moon_light_intensity; } -inline float3 GetMoonIlluminance() { return GetMoonColor() * GetMoonLightIntensity(); } inline half3 GetSunDirection() { return normalize(unpack_half3(GetWeather().sun_direction)); } +inline float GetMoonPhaseVisibility() +{ + float3 sunDir = (float3)GetSunDirection(); + float3 moonDir = GetMoonDirection(); + float sunLenSq = dot(sunDir, sunDir); + float moonLenSq = dot(moonDir, moonDir); + if (sunLenSq < 1e-6f || moonLenSq < 1e-6f) + { + return 1.0f; + } + sunDir *= rsqrt(sunLenSq); + moonDir *= rsqrt(moonLenSq); + return saturate(0.5f * (1.0f - dot(sunDir, moonDir))); +} +inline float3 GetMoonIlluminance() { return GetMoonColor() * GetMoonLightIntensity(); } inline half3 GetHorizonColor() { return unpack_half3(GetWeather().horizon); } inline half3 GetZenithColor() { return unpack_half3(GetWeather().zenith); } inline half3 GetAmbientColor() { return unpack_half3(GetWeather().ambient); } diff --git a/WickedEngine/shaders/skyHF.hlsli b/WickedEngine/shaders/skyHF.hlsli index 577230d5d4..6041f02367 100644 --- a/WickedEngine/shaders/skyHF.hlsli +++ b/WickedEngine/shaders/skyHF.hlsli @@ -150,12 +150,22 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, if (moonSize > 0 && dot(moonColor, moonColor) > 0) { float3 moonDir = GetMoonDirection(); + float3 sunDir = (float3)GetSunDirection(); + float sunLenSq = dot(sunDir, sunDir); + float3 sunToMoonDir = moonDir; + float phaseVisibility = 1.0f; + if (sunLenSq > 1e-6f) + { + float invSunLen = rsqrt(sunLenSq); + sunToMoonDir = -sunDir * invSunLen; + phaseVisibility = saturate(0.5f * (1.0f + dot(sunToMoonDir, moonDir))); + } float cosAngle = dot(V, moonDir); float innerEdge = cos(moonSize); float core = smoothstep(innerEdge, cos(moonSize * 0.8f), cosAngle); - float diskMask = core; float3 diskColor = moonColor; - if (HasMoonTexture()) + float diskMask = 0.0f; + if (phaseVisibility > 0.0f) { float3 referenceUp = abs(moonDir.y) > 0.95f ? float3(1, 0, 0) : float3(0, 1, 0); float3 moonRight = normalize(cross(referenceUp, moonDir)); @@ -165,13 +175,21 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, float2 moonUV = local * invRadius + 0.5f; if (all(moonUV >= 0.0f) && all(moonUV <= 1.0f)) { - float4 tex = bindless_textures[NonUniformResourceIndex(descriptor_index(GetWeather().moon_texture))].SampleLevel(sampler_linear_clamp, moonUV, GetMoonTextureMipBias()); - diskMask *= tex.a; - diskColor *= tex.rgb; - } - else - { - diskMask = 0.0f; + float2 diskCoord = (moonUV - 0.5f) * 2.0f; + float radialSq = dot(diskCoord, diskCoord); + float localZ = sqrt(saturate(1.0f - radialSq)); + float3 localNormal = normalize(moonRight * diskCoord.x + moonUp * diskCoord.y + moonDir * localZ); + float lit = saturate(dot(localNormal, sunToMoonDir)); + if (lit > 0.0f) + { + diskMask = core * lit; + if (HasMoonTexture()) + { + float4 tex = bindless_textures[NonUniformResourceIndex(descriptor_index(GetWeather().moon_texture))].SampleLevel(sampler_linear_clamp, moonUV, GetMoonTextureMipBias()); + diskMask *= tex.a; + diskColor *= tex.rgb; + } + } } } float haloContribution = 0; @@ -182,7 +200,7 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, float haloRadius = moonSize + haloSize; float halo = smoothstep(cos(haloRadius), innerEdge, cosAngle); halo = pow(saturate(halo), max(GetMoonHaloSharpness(), 0.0001f)); - haloContribution = halo * haloIntensity; + haloContribution = halo * haloIntensity * phaseVisibility; } sky += moonColor * haloContribution; sky += diskColor * diskMask; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index f06862e18b..bebe4958cb 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -33,6 +33,22 @@ namespace wi::scene { static constexpr uint32_t small_subtask_groupsize = 256u; + static float ComputeMoonPhaseVisibility(const XMFLOAT3& sun_dir, const XMFLOAT3& moon_dir) + { + XMVECTOR sun = XMLoadFloat3(&sun_dir); + XMVECTOR moon = XMLoadFloat3(&moon_dir); + const float sun_len_sq = XMVectorGetX(XMVector3LengthSq(sun)); + const float moon_len_sq = XMVectorGetX(XMVector3LengthSq(moon)); + if (!std::isfinite(sun_len_sq) || !std::isfinite(moon_len_sq) || sun_len_sq < 1e-6f || moon_len_sq < 1e-6f) + { + return 1.0f; + } + sun = XMVector3Normalize(sun); + moon = XMVector3Normalize(moon); + const float dot_sm = XMVectorGetX(XMVector3Dot(sun, moon)); + return wi::math::Clamp(0.5f * (1.0f - dot_sm), 0.0f, 1.0f); + } + void Scene::Update(float dt) { GraphicsDevice* device = wi::graphics::GetDevice(); @@ -947,7 +963,8 @@ namespace wi::scene shaderscene.weather.moon_params = XMFLOAT4(weather.moonSize, weather.moonGlowSize, weather.moonGlowSharpness, weather.moonGlowIntensity); shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; - shaderscene.weather.moon_light_intensity = weather.moonLightIntensity; + const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather.sunDirection, moonDir); + shaderscene.weather.moon_light_intensity = weather.moonLightIntensity * moon_phase_visibility; shaderscene.weather.moon_light_index = weather.moon_light_index; shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); @@ -1475,7 +1492,9 @@ namespace wi::scene } } - const bool moon_active = weather_component.moonLightIntensity > 0.0f; + const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather_component.sunDirection, weather_component.moonDirection); + const float moon_effective_intensity = weather_component.moonLightIntensity * moon_phase_visibility; + const bool moon_active = moon_effective_intensity > 0.0f; if (weather_component.moonLight == INVALID_ENTITY) { @@ -1485,7 +1504,7 @@ namespace wi::scene light.SetMoonLight(true); light.SetCastShadow(moon_active); light.SetVolumetricsEnabled(moon_active); - light.intensity = weather_component.moonLightIntensity; + light.intensity = moon_effective_intensity; light.color = weather_component.moonColor; TransformComponent& transform = *transforms.GetComponent(weather_component.moonLight); transform.Translate(XMFLOAT3(0, 4, 0)); @@ -1503,7 +1522,7 @@ namespace wi::scene light->SetCastShadow(moon_active); light->SetVolumetricsEnabled(moon_active); light->color = weather_component.moonColor; - light->intensity = weather_component.moonLightIntensity; + light->intensity = moon_effective_intensity; light->SetVolumetricCloudsEnabled(moon_active); TransformComponent* transform = transforms.GetComponent(weather_component.moonLight); From 814565a64800f2f200c3800cfb4e7d33ba1aaf7a Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 12:55:48 +0000 Subject: [PATCH 05/12] Add procedural phase animation in sky shader Extend the dynamic sky moon disk shading to animate its lit hemisphere. The shader now warps the local normal toward the sun, layers time-driven crater detail, and adds a subtle rim glow so crescents evolve smoothly as phase visibility changes. --- WickedEngine/shaders/skyHF.hlsli | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/WickedEngine/shaders/skyHF.hlsli b/WickedEngine/shaders/skyHF.hlsli index 6041f02367..619b1f00e7 100644 --- a/WickedEngine/shaders/skyHF.hlsli +++ b/WickedEngine/shaders/skyHF.hlsli @@ -189,6 +189,19 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, diskMask *= tex.a; diskColor *= tex.rgb; } + float phaseBlend = saturate(phaseVisibility); + float3 shadingNormal = normalize(lerp(moonDir, localNormal, phaseBlend)); + float diffuse = saturate(dot(shadingNormal, sunToMoonDir)); + float rim = pow(saturate(1.0f - dot(localNormal, V)), 4.0f); + float wave = dot(diskCoord, float2(21.7f, -14.9f)); + float time = GetTime(); + float anim0 = sin(wave + time * 0.35f); + float anim1 = sin((diskCoord.x * -18.3f + diskCoord.y * 11.1f) - time * 0.22f); + float craterMask = 0.85f + 0.15f * (anim0 * 0.6f + anim1 * 0.4f); + float detailMask = lerp(1.0f, craterMask, phaseBlend); + float shading = lerp(0.35f, 1.0f, diffuse); + diskColor *= shading * detailMask; + diskColor += moonColor * rim * 0.08f * phaseBlend; } } } From a117a271693d1d8e569dbaad3adde782a00f8832 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 13:36:52 +0000 Subject: [PATCH 06/12] Add weather-driven lunar eclipse control Add an eclipse strength scalar to WeatherComponent, serialize it, and feed it to the sky shader so designers can dim the moon disk, halo, and moonlight on demand. --- Editor/WeatherWindow.cpp | 12 ++++++++++++ Editor/WeatherWindow.h | 1 + WickedEngine/ArchiveVersionHistory.txt | 1 + WickedEngine/shaders/ShaderInterop_Weather.h | 2 ++ WickedEngine/shaders/globals.hlsli | 1 + WickedEngine/shaders/skyHF.hlsli | 4 +++- WickedEngine/wiArchive.cpp | 2 +- WickedEngine/wiScene.cpp | 7 +++++-- WickedEngine/wiScene.h | 2 +- WickedEngine/wiScene_BindLua.cpp | 1 + WickedEngine/wiScene_BindLua.h | 3 +++ WickedEngine/wiScene_Components.h | 1 + WickedEngine/wiScene_Serializers.cpp | 14 ++++++++++++++ 13 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Editor/WeatherWindow.cpp b/Editor/WeatherWindow.cpp index 83c06c1dc5..8a9b27f346 100644 --- a/Editor/WeatherWindow.cpp +++ b/Editor/WeatherWindow.cpp @@ -306,6 +306,16 @@ void WeatherWindow::Create(EditorComponent* _editor) }); AddWidget(&moonLightIntensitySlider); + moonEclipseSlider.Create(0.0f, 1.0f, 0.0f, 1000, "Moon Eclipse: "); + moonEclipseSlider.SetTooltip("Fraction of the moon darkened by Earth's shadow (0 = none, 1 = total eclipse)."); + moonEclipseSlider.SetSize(XMFLOAT2(wid, hei)); + moonEclipseSlider.SetPos(XMFLOAT2(x, y += step)); + moonEclipseSlider.OnSlide([this](wi::gui::EventArgs args) { + GetWeather().moonEclipseStrength = args.fValue; + InvalidateProbes(); + }); + AddWidget(&moonEclipseSlider); + moonTextureButton.Create("Load Moon Texture"); moonTextureButton.SetTooltip("Load a dedicated texture for the moon disk. Click again to clear."); moonTextureButton.SetSize(XMFLOAT2(mod_wid, hei)); @@ -1222,6 +1232,7 @@ void WeatherWindow::UpdateData() moonGlowSharpnessSlider.SetValue(weather.moonGlowSharpness); moonGlowIntensitySlider.SetValue(weather.moonGlowIntensity); moonLightIntensitySlider.SetValue(weather.moonLightIntensity); + moonEclipseSlider.SetValue(weather.moonEclipseStrength); moonTextureMipBiasSlider.SetValue(weather.moonTextureMipBias); skyRotationSlider.SetValue(wi::math::RadiansToDegrees(weather.sky_rotation)); rainAmountSlider.SetValue(weather.rain_amount); @@ -1484,6 +1495,7 @@ void WeatherWindow::ResizeLayout() layout.add(moonGlowSharpnessSlider); layout.add(moonGlowIntensitySlider); layout.add(moonLightIntensitySlider); + layout.add(moonEclipseSlider); layout.add_fullwidth(moonTextureButton); layout.add(moonTextureMipBiasSlider); layout.add(skyRotationSlider); diff --git a/Editor/WeatherWindow.h b/Editor/WeatherWindow.h index 11fd5e96d3..ea7a48e322 100644 --- a/Editor/WeatherWindow.h +++ b/Editor/WeatherWindow.h @@ -43,6 +43,7 @@ class WeatherWindow : public wi::gui::Window wi::gui::Slider moonGlowSharpnessSlider; wi::gui::Slider moonGlowIntensitySlider; wi::gui::Slider moonLightIntensitySlider; + wi::gui::Slider moonEclipseSlider; wi::gui::Slider moonTextureMipBiasSlider; wi::gui::Slider rainAmountSlider; wi::gui::Slider rainLengthSlider; diff --git a/WickedEngine/ArchiveVersionHistory.txt b/WickedEngine/ArchiveVersionHistory.txt index a53ce089c3..eede599592 100644 --- a/WickedEngine/ArchiveVersionHistory.txt +++ b/WickedEngine/ArchiveVersionHistory.txt @@ -1,5 +1,6 @@ This file contains changelog of wi::Archive versions +98: serialized WeatherComponent moon eclipse strength 97: serialized WeatherComponent moon light intensity 96: serialized WeatherComponent moon texture mip bias 95: serialized WeatherComponent moon texture path diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index 44100c5f01..24a59eb6b5 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -372,7 +372,9 @@ struct alignas(16) ShaderWeather int moon_texture; // bindless descriptor index for moon texture (SRV), -1 if unused float moon_texture_mip_bias; // optional mip bias when sampling moon texture float moon_light_intensity; // illuminance emitted by moon directional light + float moon_eclipse_strength; // 0-1 multiplier describing earth shadow on moon uint moon_light_index; + uint padding_moon0; float sky_rotation_sin; float sky_rotation_cos; diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index 1e7132af95..012f455429 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -1002,6 +1002,7 @@ inline float GetMoonPhaseVisibility() moonDir *= rsqrt(moonLenSq); return saturate(0.5f * (1.0f - dot(sunDir, moonDir))); } +inline float GetMoonEclipseStrength() { return saturate(GetWeather().moon_eclipse_strength); } inline float3 GetMoonIlluminance() { return GetMoonColor() * GetMoonLightIntensity(); } inline half3 GetHorizonColor() { return unpack_half3(GetWeather().horizon); } inline half3 GetZenithColor() { return unpack_half3(GetWeather().zenith); } diff --git a/WickedEngine/shaders/skyHF.hlsli b/WickedEngine/shaders/skyHF.hlsli index 619b1f00e7..2c9ce65a6e 100644 --- a/WickedEngine/shaders/skyHF.hlsli +++ b/WickedEngine/shaders/skyHF.hlsli @@ -160,6 +160,8 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, sunToMoonDir = -sunDir * invSunLen; phaseVisibility = saturate(0.5f * (1.0f + dot(sunToMoonDir, moonDir))); } + float eclipseStrength = GetMoonEclipseStrength(); + phaseVisibility *= saturate(1.0f - eclipseStrength); float cosAngle = dot(V, moonDir); float innerEdge = cos(moonSize); float core = smoothstep(innerEdge, cos(moonSize * 0.8f), cosAngle); @@ -216,7 +218,7 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, haloContribution = halo * haloIntensity * phaseVisibility; } sky += moonColor * haloContribution; - sky += diskColor * diskMask; + sky += diskColor * diskMask * saturate(1.0f - eclipseStrength * 0.9f); } } diff --git a/WickedEngine/wiArchive.cpp b/WickedEngine/wiArchive.cpp index a429bedb11..35c3a145c4 100644 --- a/WickedEngine/wiArchive.cpp +++ b/WickedEngine/wiArchive.cpp @@ -15,7 +15,7 @@ namespace wi { // this should always be only INCREMENTED and only if a new serialization is implemeted somewhere! - static constexpr uint64_t __archiveVersion = 97; + static constexpr uint64_t __archiveVersion = 98; // this is the version number of which below the archive is not compatible with the current version static constexpr uint64_t __archiveVersionBarrier = 22; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index bebe4958cb..5249bb6ab1 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -964,7 +964,9 @@ namespace wi::scene shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather.sunDirection, moonDir); - shaderscene.weather.moon_light_intensity = weather.moonLightIntensity * moon_phase_visibility; + const float moon_eclipse_scale = wi::math::Clamp(1.0f - weather.moonEclipseStrength, 0.0f, 1.0f); + shaderscene.weather.moon_light_intensity = weather.moonLightIntensity * moon_phase_visibility * moon_eclipse_scale; + shaderscene.weather.moon_eclipse_strength = weather.moonEclipseStrength; shaderscene.weather.moon_light_index = weather.moon_light_index; shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); @@ -1493,7 +1495,8 @@ namespace wi::scene } const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather_component.sunDirection, weather_component.moonDirection); - const float moon_effective_intensity = weather_component.moonLightIntensity * moon_phase_visibility; + const float moon_eclipse_scale = wi::math::Clamp(1.0f - weather_component.moonEclipseStrength, 0.0f, 1.0f); + const float moon_effective_intensity = weather_component.moonLightIntensity * moon_phase_visibility * moon_eclipse_scale; const bool moon_active = moon_effective_intensity > 0.0f; if (weather_component.moonLight == INVALID_ENTITY) diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 315a714c52..850530c037 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -47,7 +47,7 @@ namespace wi::scene wi::ecs::ComponentManager& animation_datas = componentLibrary.Register("wi::scene::Scene::animation_datas"); wi::ecs::ComponentManager& emitters = componentLibrary.Register("wi::scene::Scene::emitters", 2); // version = 2 wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 3); // version = 3 - wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 10); // version = 10 + wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 11); // version = 11 wi::ecs::ComponentManager& sounds = componentLibrary.Register("wi::scene::Scene::sounds", 1); // version = 1 wi::ecs::ComponentManager& videos = componentLibrary.Register("wi::scene::Scene::videos", 1); // version = 1 wi::ecs::ComponentManager& inverse_kinematics = componentLibrary.Register("wi::scene::Scene::inverse_kinematics"); diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index a262977c32..e18da9597a 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -6871,6 +6871,7 @@ Luna::PropertyType WeatherComponent_BindLua::propertie lunaproperty(WeatherComponent_BindLua, moonGlowSharpness), lunaproperty(WeatherComponent_BindLua, moonGlowIntensity), lunaproperty(WeatherComponent_BindLua, moonLightIntensity), + lunaproperty(WeatherComponent_BindLua, moonEclipseStrength), lunaproperty(WeatherComponent_BindLua, rainAmount), lunaproperty(WeatherComponent_BindLua, rainLength), lunaproperty(WeatherComponent_BindLua, rainSpeed), diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index 3e91e86462..92fc827088 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -1596,6 +1596,7 @@ namespace wi::lua::scene moonGlowSharpness = FloatProperty(&component->moonGlowSharpness); moonGlowIntensity = FloatProperty(&component->moonGlowIntensity); moonLightIntensity = FloatProperty(&component->moonLightIntensity); + moonEclipseStrength = FloatProperty(&component->moonEclipseStrength); skyExposure = FloatProperty(&component->skyExposure); horizon = VectorProperty(&component->horizon); zenith = VectorProperty(&component->zenith); @@ -1665,6 +1666,7 @@ namespace wi::lua::scene FloatProperty moonGlowSharpness; FloatProperty moonGlowIntensity; FloatProperty moonLightIntensity; + FloatProperty moonEclipseStrength; FloatProperty rainAmount; FloatProperty rainLength; FloatProperty rainSpeed; @@ -1701,6 +1703,7 @@ namespace wi::lua::scene PropertyFunction(moonGlowSharpness) PropertyFunction(moonGlowIntensity) PropertyFunction(moonLightIntensity) + PropertyFunction(moonEclipseStrength) PropertyFunction(rainAmount) PropertyFunction(rainLength) PropertyFunction(rainSpeed) diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index a9f9e15cd5..59a0895dcf 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1867,6 +1867,7 @@ namespace wi::scene float moonGlowIntensity = 0.25f; float moonLightIntensity = 0.05f; float moonTextureMipBias = 0; + float moonEclipseStrength = 0.0f; float skyExposure = 1; XMFLOAT3 horizon = XMFLOAT3(0.0f, 0.0f, 0.0f); XMFLOAT3 zenith = XMFLOAT3(0.0f, 0.0f, 0.0f); diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index 0794b8b0b1..714103dfa1 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -1618,6 +1618,7 @@ namespace wi::scene const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); + const bool supports_moon_eclipse = (seri.GetVersion() >= 11) || (archive.GetVersion() >= 98); if (supports_moon_params) { archive >> moonDirection; @@ -1629,6 +1630,10 @@ namespace wi::scene if (supports_moon_light_intensity) { archive >> moonLightIntensity; + if (supports_moon_eclipse) + { + archive >> moonEclipseStrength; + } } } else @@ -1645,6 +1650,10 @@ namespace wi::scene { moonLightIntensity = 0.05f; } + if (!supports_moon_eclipse) + { + moonEclipseStrength = 0.0f; + } if (supports_moon_texture) { archive >> moonTextureName; @@ -1956,6 +1965,7 @@ namespace wi::scene const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); + const bool supports_moon_eclipse = (seri.GetVersion() >= 11) || (archive.GetVersion() >= 98); seri.RegisterResource(moonTextureName); seri.RegisterResource(skyMapName); seri.RegisterResource(colorGradingMapName); @@ -1975,6 +1985,10 @@ namespace wi::scene if (supports_moon_light_intensity) { archive << moonLightIntensity; + if (supports_moon_eclipse) + { + archive << moonEclipseStrength; + } } } if (supports_moon_texture) From 0eca4c18a9184dbb61b7f5de6cc04009bfaa5929 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 14:14:24 +0000 Subject: [PATCH 07/12] Add automatic lunar eclipse support Add a weather flag and UI toggle that detects when the Moon moves into Earth's shadow, dims the disk/light automatically, and serializes the state with archive version 99. The editor slider can still override the effect when automation is off, and Lua bindings expose the new control for scripts. --- Editor/WeatherWindow.cpp | 22 ++++++++++- Editor/WeatherWindow.h | 1 + WickedEngine/ArchiveVersionHistory.txt | 1 + WickedEngine/wiArchive.cpp | 2 +- WickedEngine/wiScene.cpp | 51 ++++++++++++++++++++++++-- WickedEngine/wiScene.h | 2 +- WickedEngine/wiScene_BindLua.cpp | 1 + WickedEngine/wiScene_BindLua.h | 3 ++ WickedEngine/wiScene_Components.h | 1 + WickedEngine/wiScene_Serializers.cpp | 14 +++++++ 10 files changed, 91 insertions(+), 7 deletions(-) diff --git a/Editor/WeatherWindow.cpp b/Editor/WeatherWindow.cpp index 8a9b27f346..634adea877 100644 --- a/Editor/WeatherWindow.cpp +++ b/Editor/WeatherWindow.cpp @@ -306,12 +306,27 @@ void WeatherWindow::Create(EditorComponent* _editor) }); AddWidget(&moonLightIntensitySlider); + moonEclipseAutoCheckBox.Create("Auto Moon Eclipse"); + moonEclipseAutoCheckBox.SetTooltip("Automatically darken the moon when it enters Earth's shadow."); + moonEclipseAutoCheckBox.SetSize(XMFLOAT2(hei, hei)); + moonEclipseAutoCheckBox.SetPos(XMFLOAT2(x, y += step)); + moonEclipseAutoCheckBox.OnClick([this](wi::gui::EventArgs args) { + auto& weather = GetWeather(); + weather.moonEclipseAutomatic = args.bValue; + moonEclipseSlider.SetEnabled(!args.bValue); + editor->GetCurrentScene().EnsureMoonLight(weather); + InvalidateProbes(); + }); + AddWidget(&moonEclipseAutoCheckBox); + moonEclipseSlider.Create(0.0f, 1.0f, 0.0f, 1000, "Moon Eclipse: "); - moonEclipseSlider.SetTooltip("Fraction of the moon darkened by Earth's shadow (0 = none, 1 = total eclipse)."); + moonEclipseSlider.SetTooltip("Fraction of the moon darkened by Earth's shadow (0 = none, 1 = total eclipse). Disabled when automation is enabled."); moonEclipseSlider.SetSize(XMFLOAT2(wid, hei)); moonEclipseSlider.SetPos(XMFLOAT2(x, y += step)); moonEclipseSlider.OnSlide([this](wi::gui::EventArgs args) { - GetWeather().moonEclipseStrength = args.fValue; + auto& weather = GetWeather(); + weather.moonEclipseStrength = args.fValue; + editor->GetCurrentScene().EnsureMoonLight(weather); InvalidateProbes(); }); AddWidget(&moonEclipseSlider); @@ -1233,6 +1248,8 @@ void WeatherWindow::UpdateData() moonGlowIntensitySlider.SetValue(weather.moonGlowIntensity); moonLightIntensitySlider.SetValue(weather.moonLightIntensity); moonEclipseSlider.SetValue(weather.moonEclipseStrength); + moonEclipseAutoCheckBox.SetCheck(weather.moonEclipseAutomatic); + moonEclipseSlider.SetEnabled(!weather.moonEclipseAutomatic); moonTextureMipBiasSlider.SetValue(weather.moonTextureMipBias); skyRotationSlider.SetValue(wi::math::RadiansToDegrees(weather.sky_rotation)); rainAmountSlider.SetValue(weather.rain_amount); @@ -1495,6 +1512,7 @@ void WeatherWindow::ResizeLayout() layout.add(moonGlowSharpnessSlider); layout.add(moonGlowIntensitySlider); layout.add(moonLightIntensitySlider); + layout.add(moonEclipseAutoCheckBox); layout.add(moonEclipseSlider); layout.add_fullwidth(moonTextureButton); layout.add(moonTextureMipBiasSlider); diff --git a/Editor/WeatherWindow.h b/Editor/WeatherWindow.h index ea7a48e322..dab6752252 100644 --- a/Editor/WeatherWindow.h +++ b/Editor/WeatherWindow.h @@ -44,6 +44,7 @@ class WeatherWindow : public wi::gui::Window wi::gui::Slider moonGlowIntensitySlider; wi::gui::Slider moonLightIntensitySlider; wi::gui::Slider moonEclipseSlider; + wi::gui::CheckBox moonEclipseAutoCheckBox; wi::gui::Slider moonTextureMipBiasSlider; wi::gui::Slider rainAmountSlider; wi::gui::Slider rainLengthSlider; diff --git a/WickedEngine/ArchiveVersionHistory.txt b/WickedEngine/ArchiveVersionHistory.txt index eede599592..5bb2b12add 100644 --- a/WickedEngine/ArchiveVersionHistory.txt +++ b/WickedEngine/ArchiveVersionHistory.txt @@ -1,5 +1,6 @@ This file contains changelog of wi::Archive versions +99: serialized WeatherComponent moon eclipse automation 98: serialized WeatherComponent moon eclipse strength 97: serialized WeatherComponent moon light intensity 96: serialized WeatherComponent moon texture mip bias diff --git a/WickedEngine/wiArchive.cpp b/WickedEngine/wiArchive.cpp index 35c3a145c4..9d93155cac 100644 --- a/WickedEngine/wiArchive.cpp +++ b/WickedEngine/wiArchive.cpp @@ -15,7 +15,7 @@ namespace wi { // this should always be only INCREMENTED and only if a new serialization is implemeted somewhere! - static constexpr uint64_t __archiveVersion = 98; + static constexpr uint64_t __archiveVersion = 99; // this is the version number of which below the archive is not compatible with the current version static constexpr uint64_t __archiveVersionBarrier = 22; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 5249bb6ab1..974228824e 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -49,6 +49,49 @@ namespace wi::scene return wi::math::Clamp(0.5f * (1.0f - dot_sm), 0.0f, 1.0f); } + static float ResolveMoonEclipseStrength(const WeatherComponent& weather) + { + const float manual = wi::math::Clamp(weather.moonEclipseStrength, 0.0f, 1.0f); + if (!weather.moonEclipseAutomatic) + { + return manual; + } + + XMFLOAT3 sun_dir = weather.sunDirection; + XMFLOAT3 moon_dir = weather.moonDirection; + XMVECTOR sun = XMLoadFloat3(&sun_dir); + XMVECTOR moon = XMLoadFloat3(&moon_dir); + const float sun_len_sq = XMVectorGetX(XMVector3LengthSq(sun)); + if (!std::isfinite(sun_len_sq) || sun_len_sq < 1e-6f) + { + return manual; + } + float moon_len_sq = XMVectorGetX(XMVector3LengthSq(moon)); + if (!std::isfinite(moon_len_sq) || moon_len_sq < 1e-6f) + { + moon_dir = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + moon = XMLoadFloat3(&moon_dir); + moon_len_sq = XMVectorGetX(XMVector3LengthSq(moon)); + } + + sun = XMVector3Normalize(sun); + moon = XMVector3Normalize(moon); + const float opposition = wi::math::Clamp(-XMVectorGetX(XMVector3Dot(sun, moon)), -1.0f, 1.0f); + constexpr float sun_apparent_radius = wi::math::DegreesToRadians(0.27f); + constexpr float penumbra_padding = wi::math::DegreesToRadians(0.35f); + const float totality_angle = std::max(weather.moonSize + sun_apparent_radius, 0.0f); + const float penumbra_angle = totality_angle + penumbra_padding; + const float cos_totality = std::cos(totality_angle); + const float cos_penumbra = std::cos(penumbra_angle); + if (cos_totality <= cos_penumbra) + { + return manual; + } + + const float eclipse = wi::math::Clamp((opposition - cos_penumbra) / (cos_totality - cos_penumbra), 0.0f, 1.0f); + return eclipse; + } + void Scene::Update(float dt) { GraphicsDevice* device = wi::graphics::GetDevice(); @@ -964,9 +1007,10 @@ namespace wi::scene shaderscene.weather.moon_texture = weather.moonTexture.IsValid() ? device->GetDescriptorIndex(&weather.moonTexture.GetTexture(), SubresourceType::SRV, weather.moonTexture.GetTextureSRGBSubresource()) : -1; shaderscene.weather.moon_texture_mip_bias = weather.moonTextureMipBias; const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather.sunDirection, moonDir); - const float moon_eclipse_scale = wi::math::Clamp(1.0f - weather.moonEclipseStrength, 0.0f, 1.0f); + const float resolved_moon_eclipse = ResolveMoonEclipseStrength(weather); + const float moon_eclipse_scale = wi::math::Clamp(1.0f - resolved_moon_eclipse, 0.0f, 1.0f); shaderscene.weather.moon_light_intensity = weather.moonLightIntensity * moon_phase_visibility * moon_eclipse_scale; - shaderscene.weather.moon_eclipse_strength = weather.moonEclipseStrength; + shaderscene.weather.moon_eclipse_strength = resolved_moon_eclipse; shaderscene.weather.moon_light_index = weather.moon_light_index; shaderscene.weather.most_important_light_index = weather.most_important_light_index; shaderscene.weather.ambient = wi::math::pack_half3(weather.ambient); @@ -1495,7 +1539,8 @@ namespace wi::scene } const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather_component.sunDirection, weather_component.moonDirection); - const float moon_eclipse_scale = wi::math::Clamp(1.0f - weather_component.moonEclipseStrength, 0.0f, 1.0f); + const float resolved_moon_eclipse = ResolveMoonEclipseStrength(weather_component); + const float moon_eclipse_scale = wi::math::Clamp(1.0f - resolved_moon_eclipse, 0.0f, 1.0f); const float moon_effective_intensity = weather_component.moonLightIntensity * moon_phase_visibility * moon_eclipse_scale; const bool moon_active = moon_effective_intensity > 0.0f; diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 850530c037..3b00a14f47 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -47,7 +47,7 @@ namespace wi::scene wi::ecs::ComponentManager& animation_datas = componentLibrary.Register("wi::scene::Scene::animation_datas"); wi::ecs::ComponentManager& emitters = componentLibrary.Register("wi::scene::Scene::emitters", 2); // version = 2 wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 3); // version = 3 - wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 11); // version = 11 + wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 12); // version = 12 wi::ecs::ComponentManager& sounds = componentLibrary.Register("wi::scene::Scene::sounds", 1); // version = 1 wi::ecs::ComponentManager& videos = componentLibrary.Register("wi::scene::Scene::videos", 1); // version = 1 wi::ecs::ComponentManager& inverse_kinematics = componentLibrary.Register("wi::scene::Scene::inverse_kinematics"); diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index e18da9597a..a5b257a71f 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -6872,6 +6872,7 @@ Luna::PropertyType WeatherComponent_BindLua::propertie lunaproperty(WeatherComponent_BindLua, moonGlowIntensity), lunaproperty(WeatherComponent_BindLua, moonLightIntensity), lunaproperty(WeatherComponent_BindLua, moonEclipseStrength), + lunaproperty(WeatherComponent_BindLua, moonEclipseAutomatic), lunaproperty(WeatherComponent_BindLua, rainAmount), lunaproperty(WeatherComponent_BindLua, rainLength), lunaproperty(WeatherComponent_BindLua, rainSpeed), diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index 92fc827088..078009cea6 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -1597,6 +1597,7 @@ namespace wi::lua::scene moonGlowIntensity = FloatProperty(&component->moonGlowIntensity); moonLightIntensity = FloatProperty(&component->moonLightIntensity); moonEclipseStrength = FloatProperty(&component->moonEclipseStrength); + moonEclipseAutomatic = BoolProperty(&component->moonEclipseAutomatic); skyExposure = FloatProperty(&component->skyExposure); horizon = VectorProperty(&component->horizon); zenith = VectorProperty(&component->zenith); @@ -1667,6 +1668,7 @@ namespace wi::lua::scene FloatProperty moonGlowIntensity; FloatProperty moonLightIntensity; FloatProperty moonEclipseStrength; + BoolProperty moonEclipseAutomatic; FloatProperty rainAmount; FloatProperty rainLength; FloatProperty rainSpeed; @@ -1704,6 +1706,7 @@ namespace wi::lua::scene PropertyFunction(moonGlowIntensity) PropertyFunction(moonLightIntensity) PropertyFunction(moonEclipseStrength) + PropertyFunction(moonEclipseAutomatic) PropertyFunction(rainAmount) PropertyFunction(rainLength) PropertyFunction(rainSpeed) diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 59a0895dcf..52abb40631 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1868,6 +1868,7 @@ namespace wi::scene float moonLightIntensity = 0.05f; float moonTextureMipBias = 0; float moonEclipseStrength = 0.0f; + bool moonEclipseAutomatic = false; float skyExposure = 1; XMFLOAT3 horizon = XMFLOAT3(0.0f, 0.0f, 0.0f); XMFLOAT3 zenith = XMFLOAT3(0.0f, 0.0f, 0.0f); diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index 714103dfa1..8528557fe0 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -1619,6 +1619,7 @@ namespace wi::scene const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); const bool supports_moon_eclipse = (seri.GetVersion() >= 11) || (archive.GetVersion() >= 98); + const bool supports_moon_eclipse_auto = (seri.GetVersion() >= 12) || (archive.GetVersion() >= 99); if (supports_moon_params) { archive >> moonDirection; @@ -1633,6 +1634,10 @@ namespace wi::scene if (supports_moon_eclipse) { archive >> moonEclipseStrength; + if (supports_moon_eclipse_auto) + { + archive >> moonEclipseAutomatic; + } } } } @@ -1654,6 +1659,10 @@ namespace wi::scene { moonEclipseStrength = 0.0f; } + if (!supports_moon_eclipse_auto) + { + moonEclipseAutomatic = false; + } if (supports_moon_texture) { archive >> moonTextureName; @@ -1966,6 +1975,7 @@ namespace wi::scene const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); const bool supports_moon_light_intensity = (seri.GetVersion() >= 10) || (archive.GetVersion() >= 97); const bool supports_moon_eclipse = (seri.GetVersion() >= 11) || (archive.GetVersion() >= 98); + const bool supports_moon_eclipse_auto = (seri.GetVersion() >= 12) || (archive.GetVersion() >= 99); seri.RegisterResource(moonTextureName); seri.RegisterResource(skyMapName); seri.RegisterResource(colorGradingMapName); @@ -1988,6 +1998,10 @@ namespace wi::scene if (supports_moon_eclipse) { archive << moonEclipseStrength; + if (supports_moon_eclipse_auto) + { + archive << moonEclipseAutomatic; + } } } } From 25f8c7d46915dd61845f4203081145a4d2c58687 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 5 Nov 2025 15:17:44 +0000 Subject: [PATCH 08/12] Add solar eclipse pipeline Add solar eclipse pipeline including weather controls, shaders, renderer, and serialization - extend WeatherComponent, serialization, and Lua bindings with sun eclipse automation - thread eclipse factor through shader interop, realistic sky shading, and directional light scaling - expose solar eclipse sliders/toggles in the editor and reset defaults when weather is missing - bump archive/component versions and update resolver logic to keep moon lighting consistent --- Editor/WeatherWindow.cpp | 30 +++++++++++ Editor/WeatherWindow.h | 2 + WickedEngine/ArchiveVersionHistory.txt | 1 + WickedEngine/shaders/ShaderInterop_Weather.h | 2 + WickedEngine/shaders/globals.hlsli | 8 ++- WickedEngine/shaders/skyAtmosphere.hlsli | 36 ++++++++++++- WickedEngine/wiArchive.cpp | 2 +- WickedEngine/wiRenderer.cpp | 11 +++- WickedEngine/wiScene.cpp | 55 ++++++++++++++++++++ WickedEngine/wiScene.h | 2 +- WickedEngine/wiScene_BindLua.cpp | 2 + WickedEngine/wiScene_BindLua.h | 6 +++ WickedEngine/wiScene_Components.h | 4 ++ WickedEngine/wiScene_Serializers.cpp | 17 ++++++ 14 files changed, 172 insertions(+), 6 deletions(-) diff --git a/Editor/WeatherWindow.cpp b/Editor/WeatherWindow.cpp index 634adea877..15559283fe 100644 --- a/Editor/WeatherWindow.cpp +++ b/Editor/WeatherWindow.cpp @@ -240,6 +240,29 @@ void WeatherWindow::Create(EditorComponent* _editor) }); AddWidget(&starsSlider); + sunEclipseAutoCheckBox.Create("Auto Solar Eclipse"); + sunEclipseAutoCheckBox.SetTooltip("Automatically attenuate the sun when the moon passes in front of it."); + sunEclipseAutoCheckBox.SetSize(XMFLOAT2(hei, hei)); + sunEclipseAutoCheckBox.SetPos(XMFLOAT2(x, y += step)); + sunEclipseAutoCheckBox.OnClick([this](wi::gui::EventArgs args) { + auto& weather = GetWeather(); + weather.sunEclipseAutomatic = args.bValue; + sunEclipseSlider.SetEnabled(!args.bValue); + InvalidateProbes(); + }); + AddWidget(&sunEclipseAutoCheckBox); + + sunEclipseSlider.Create(0.0f, 1.0f, 0.0f, 1000, "Solar Eclipse: "); + sunEclipseSlider.SetTooltip("Fraction of the sun obscured by the moon (0 = none, 1 = total eclipse). Disabled when automation is enabled."); + sunEclipseSlider.SetSize(XMFLOAT2(wid, hei)); + sunEclipseSlider.SetPos(XMFLOAT2(x, y += step)); + sunEclipseSlider.OnSlide([this](wi::gui::EventArgs args) { + auto& weather = GetWeather(); + weather.sunEclipseStrength = args.fValue; + InvalidateProbes(); + }); + AddWidget(&sunEclipseSlider); + moonAzimuthSlider.Create(0, 360, 0, 10000, "Moon Azimuth: "); moonAzimuthSlider.SetTooltip("Horizontal moon orientation in degrees (0° faces +Z)."); moonAzimuthSlider.SetSize(XMFLOAT2(wid, hei)); @@ -1223,6 +1246,9 @@ void WeatherWindow::UpdateData() windRandomnessSlider.SetValue(weather.windRandomness); skyExposureSlider.SetValue(weather.skyExposure); starsSlider.SetValue(weather.stars); + sunEclipseSlider.SetValue(weather.sunEclipseStrength); + sunEclipseAutoCheckBox.SetCheck(weather.sunEclipseAutomatic); + sunEclipseSlider.SetEnabled(!weather.sunEclipseAutomatic); { XMFLOAT3 moonDir = weather.moonDirection; @@ -1418,6 +1444,8 @@ void WeatherWindow::UpdateData() scene.weather.horizon = default_sky_horizon; scene.weather.fogStart = std::numeric_limits::max(); scene.weather.fogDensity = 0; + scene.weather.sunEclipseStrength = 0.0f; + scene.weather.sunEclipseAutomatic = false; scene.weather.moonColor = XMFLOAT3(0.04f, 0.04f, 0.05f); scene.weather.moonDirection = XMFLOAT3(0.0f, 0.5f, 0.8660254f); scene.weather.moonSize = 0.0095f; @@ -1505,6 +1533,8 @@ void WeatherWindow::ResizeLayout() layout.add(windRandomnessSlider); layout.add(skyExposureSlider); layout.add(starsSlider); + layout.add(sunEclipseAutoCheckBox); + layout.add(sunEclipseSlider); layout.add(moonAzimuthSlider); layout.add(moonElevationSlider); layout.add(moonSizeSlider); diff --git a/Editor/WeatherWindow.h b/Editor/WeatherWindow.h index dab6752252..b2432d7046 100644 --- a/Editor/WeatherWindow.h +++ b/Editor/WeatherWindow.h @@ -35,6 +35,8 @@ class WeatherWindow : public wi::gui::Window wi::gui::Slider windRandomnessSlider; wi::gui::Slider skyExposureSlider; wi::gui::Slider starsSlider; + wi::gui::Slider sunEclipseSlider; + wi::gui::CheckBox sunEclipseAutoCheckBox; wi::gui::Slider skyRotationSlider; wi::gui::Slider moonAzimuthSlider; wi::gui::Slider moonElevationSlider; diff --git a/WickedEngine/ArchiveVersionHistory.txt b/WickedEngine/ArchiveVersionHistory.txt index 5bb2b12add..2a1a3d028e 100644 --- a/WickedEngine/ArchiveVersionHistory.txt +++ b/WickedEngine/ArchiveVersionHistory.txt @@ -1,5 +1,6 @@ This file contains changelog of wi::Archive versions +100: serialized WeatherComponent sun eclipse controls 99: serialized WeatherComponent moon eclipse automation 98: serialized WeatherComponent moon eclipse strength 97: serialized WeatherComponent moon light intensity diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index 24a59eb6b5..eac38b6948 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -357,6 +357,8 @@ struct alignas(16) ShaderWeather { uint2 sun_direction; // packed half3 uint2 sun_color; // packed half3 + float sun_eclipse_strength; // 0-1 multiplier describing moon shadow on sun + float padding_sun0; uint2 moon_direction; // packed half3 uint2 moon_color; // packed half3 diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index 012f455429..0bd9d0a30d 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -972,7 +972,13 @@ float acosFastPositive(float x) return p * sqrt(1.0 - x); } -inline half3 GetSunColor() { return unpack_half3(GetWeather().sun_color); } // sun color with intensity applied +inline float GetSunEclipseStrength() { return saturate(GetWeather().sun_eclipse_strength); } +inline half3 GetSunColor() +{ + const half3 rawColor = unpack_half3(GetWeather().sun_color); + const half eclipseScale = (half)(1.0f - GetSunEclipseStrength()); + return rawColor * eclipseScale; // sun color with eclipse dimming applied +} inline float3 GetMoonDirection() { float3 dir = unpack_half3(GetWeather().moon_direction); diff --git a/WickedEngine/shaders/skyAtmosphere.hlsli b/WickedEngine/shaders/skyAtmosphere.hlsli index 98c150712e..903b78e6f9 100644 --- a/WickedEngine/shaders/skyAtmosphere.hlsli +++ b/WickedEngine/shaders/skyAtmosphere.hlsli @@ -424,17 +424,49 @@ half3 GetSunLuminance(float3 worldPosition, float3 worldDirection, float3 sunDir float sunHalfApexAngleRadian = 0.5 * sunApexAngleDegree * PI / 180.0; float sunCosHalfApexAngle = cos(sunHalfApexAngleRadian); + float3 sunDirNorm = normalize(sunDirection); + float sunEclipseStrength = GetSunEclipseStrength(); float3 retval = 0; float t = RaySphereIntersectNearest(worldPosition, worldDirection, 0, atmosphere.bottomRadius); if (t < 0) // no intersection { - float VdotL = dot(worldDirection, normalize(sunDirection)); // weird... the sun disc shrinks near the horizon if we don't normalize sun direction + float VdotL = dot(worldDirection, sunDirNorm); // weird... the sun disc shrinks near the horizon if we don't normalize sun direction if (VdotL > sunCosHalfApexAngle) { // Edge fade const float halfCosHalfApex = sunCosHalfApexAngle + (1.0 - sunCosHalfApexAngle) * 0.25; // Start fading when at 75% distance from light disk center - const float weight = 1.0 - saturate((halfCosHalfApex - VdotL) / (halfCosHalfApex - sunCosHalfApexAngle)); + float weight = 1.0 - saturate((halfCosHalfApex - VdotL) / (halfCosHalfApex - sunCosHalfApexAngle)); + + if (sunEclipseStrength > 0.0f) + { + float3 moonDir = GetMoonDirection(); + float moonLenSq = dot(moonDir, moonDir); + if (moonLenSq > 1e-6f) + { + float3 moonDirNorm = moonDir * rsqrt(moonLenSq); + if (dot(moonDirNorm, sunDirNorm) > 0.0f) + { + float3 referenceUp = abs(sunDirNorm.y) > 0.95f ? float3(1, 0, 0) : float3(0, 1, 0); + float3 sunRight = normalize(cross(referenceUp, sunDirNorm)); + float3 sunUp = normalize(cross(sunDirNorm, sunRight)); + + float2 sunPlane = float2(dot(worldDirection, sunRight), dot(worldDirection, sunUp)); + float2 moonPlane = float2(dot(moonDirNorm, sunRight), dot(moonDirNorm, sunUp)); + float sunRadius = sin(sunHalfApexAngleRadian); + float moonRadius = sin(GetMoonSize()); + float2 delta = sunPlane - moonPlane; + float distSq = dot(delta, delta); + float moonRadiusSq = moonRadius * moonRadius; + if (moonRadiusSq > 1e-8f) + { + float coverage = saturate(1.0f - distSq / moonRadiusSq); + coverage = pow(coverage, 1.5f); + weight *= saturate(1.0f - sunEclipseStrength * coverage); + } + } + } + } retval = weight * sunIlluminance; } diff --git a/WickedEngine/wiArchive.cpp b/WickedEngine/wiArchive.cpp index 9d93155cac..a04b06a71d 100644 --- a/WickedEngine/wiArchive.cpp +++ b/WickedEngine/wiArchive.cpp @@ -15,7 +15,7 @@ namespace wi { // this should always be only INCREMENTED and only if a new serialization is implemeted somewhere! - static constexpr uint64_t __archiveVersion = 99; + static constexpr uint64_t __archiveVersion = 100; // this is the version number of which below the archive is not compatible with the current version static constexpr uint64_t __archiveVersionBarrier = 22; diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index faac0f78e5..97e5ab3264 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -4698,7 +4698,16 @@ void UpdatePerFrameData( shaderentity.SetRadius(light.radius); shaderentity.SetLength(light.length); shaderentity.SetDirection(light.direction); - shaderentity.SetColor(float4(light.color.x * light.intensity, light.color.y * light.intensity, light.color.z * light.intensity, 1)); + XMFLOAT3 directional_color = light.color; + float light_intensity_scale = 1.0f; + if (lightIndex == most_important_light_component_index) + { + light_intensity_scale = wi::math::Clamp(1.0f - vis.scene->weather.resolvedSunEclipseStrength, 0.0f, 1.0f); + } + directional_color.x *= light.intensity * light_intensity_scale; + directional_color.y *= light.intensity * light_intensity_scale; + directional_color.z *= light.intensity * light_intensity_scale; + shaderentity.SetColor(float4(directional_color.x, directional_color.y, directional_color.z, 1)); const uint32_t directional_entity_index = lightarray_count_directional; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 974228824e..8f51cd2a6b 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -49,6 +49,52 @@ namespace wi::scene return wi::math::Clamp(0.5f * (1.0f - dot_sm), 0.0f, 1.0f); } + static float ResolveSunEclipseStrength(const WeatherComponent& weather) + { + const float manual = wi::math::Clamp(weather.sunEclipseStrength, 0.0f, 1.0f); + if (!weather.sunEclipseAutomatic) + { + return manual; + } + + XMVECTOR sun = XMLoadFloat3(&weather.sunDirection); + XMVECTOR moon = XMLoadFloat3(&weather.moonDirection); + const float sun_len_sq = XMVectorGetX(XMVector3LengthSq(sun)); + const float moon_len_sq = XMVectorGetX(XMVector3LengthSq(moon)); + if (!std::isfinite(sun_len_sq) || sun_len_sq < 1e-6f) + { + return manual; + } + XMFLOAT3 default_moon_dir = XMFLOAT3(0.0f, 0.5f, 0.8660254f); + if (!std::isfinite(moon_len_sq) || moon_len_sq < 1e-6f) + { + moon = XMLoadFloat3(&default_moon_dir); + } + + sun = XMVector3Normalize(sun); + moon = XMVector3Normalize(moon); + const float alignment = wi::math::Clamp(XMVectorGetX(XMVector3Dot(sun, moon)), -1.0f, 1.0f); + if (alignment <= 0.0f) + { + return manual; + } + + constexpr float sun_apparent_radius = wi::math::DegreesToRadians(0.27f); + constexpr float penumbra_padding = wi::math::DegreesToRadians(0.35f); + const float moon_radius = std::max(weather.moonSize, 0.0f); + const float totality_angle = std::max(moon_radius + sun_apparent_radius, 0.0f); + const float penumbra_angle = totality_angle + penumbra_padding; + const float cos_totality = std::cos(totality_angle); + const float cos_penumbra = std::cos(penumbra_angle); + if (cos_totality <= cos_penumbra) + { + return manual; + } + + const float eclipse = wi::math::Clamp((alignment - cos_penumbra) / (cos_totality - cos_penumbra), 0.0f, 1.0f); + return eclipse; + } + static float ResolveMoonEclipseStrength(const WeatherComponent& weather) { const float manual = wi::math::Clamp(weather.moonEclipseStrength, 0.0f, 1.0f); @@ -989,8 +1035,16 @@ namespace wi::scene shaderscene.aabb_extents_rcp.y = 1.0f / shaderscene.aabb_extents.y; shaderscene.aabb_extents_rcp.z = 1.0f / shaderscene.aabb_extents.z; + const float resolved_sun_eclipse = ResolveSunEclipseStrength(weather); + weather.resolvedSunEclipseStrength = resolved_sun_eclipse; + if (weathers.GetCount() > 0) + { + weathers[0].resolvedSunEclipseStrength = weather.resolvedSunEclipseStrength; + } shaderscene.weather.sun_color = wi::math::pack_half3(weather.sunColor); shaderscene.weather.sun_direction = wi::math::pack_half3(weather.sunDirection); + shaderscene.weather.sun_eclipse_strength = resolved_sun_eclipse; + shaderscene.weather.padding_sun0 = 0.0f; auto moonDir = weather.moonDirection; if (wi::math::LengthSquared(moonDir) < 1e-6f) { @@ -1540,6 +1594,7 @@ namespace wi::scene const float moon_phase_visibility = ComputeMoonPhaseVisibility(weather_component.sunDirection, weather_component.moonDirection); const float resolved_moon_eclipse = ResolveMoonEclipseStrength(weather_component); + weather_component.resolvedSunEclipseStrength = ResolveSunEclipseStrength(weather_component); const float moon_eclipse_scale = wi::math::Clamp(1.0f - resolved_moon_eclipse, 0.0f, 1.0f); const float moon_effective_intensity = weather_component.moonLightIntensity * moon_phase_visibility * moon_eclipse_scale; const bool moon_active = moon_effective_intensity > 0.0f; diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 3b00a14f47..e8e4a39d07 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -47,7 +47,7 @@ namespace wi::scene wi::ecs::ComponentManager& animation_datas = componentLibrary.Register("wi::scene::Scene::animation_datas"); wi::ecs::ComponentManager& emitters = componentLibrary.Register("wi::scene::Scene::emitters", 2); // version = 2 wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 3); // version = 3 - wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 12); // version = 12 + wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 13); // version = 13 wi::ecs::ComponentManager& sounds = componentLibrary.Register("wi::scene::Scene::sounds", 1); // version = 1 wi::ecs::ComponentManager& videos = componentLibrary.Register("wi::scene::Scene::videos", 1); // version = 1 wi::ecs::ComponentManager& inverse_kinematics = componentLibrary.Register("wi::scene::Scene::inverse_kinematics"); diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index a5b257a71f..758eb3b72d 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -6866,6 +6866,8 @@ Luna::PropertyType WeatherComponent_BindLua::propertie lunaproperty(WeatherComponent_BindLua, windWaveSize), lunaproperty(WeatherComponent_BindLua, windSpeed), lunaproperty(WeatherComponent_BindLua, stars), + lunaproperty(WeatherComponent_BindLua, sunEclipseStrength), + lunaproperty(WeatherComponent_BindLua, sunEclipseAutomatic), lunaproperty(WeatherComponent_BindLua, moonSize), lunaproperty(WeatherComponent_BindLua, moonGlowSize), lunaproperty(WeatherComponent_BindLua, moonGlowSharpness), diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index 078009cea6..d50c934354 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -1611,6 +1611,8 @@ namespace wi::lua::scene windWaveSize = FloatProperty(&component->windWaveSize); windSpeed = FloatProperty(&component->windSpeed); stars = FloatProperty(&component->stars); + sunEclipseStrength = FloatProperty(&component->sunEclipseStrength); + sunEclipseAutomatic = BoolProperty(&component->sunEclipseAutomatic); rainAmount = FloatProperty(&component->rain_amount); rainLength = FloatProperty(&component->rain_length); rainSpeed = FloatProperty(&component->rain_speed); @@ -1662,6 +1664,8 @@ namespace wi::lua::scene FloatProperty windWaveSize; FloatProperty windSpeed; FloatProperty stars; + FloatProperty sunEclipseStrength; + BoolProperty sunEclipseAutomatic; FloatProperty moonSize; FloatProperty moonGlowSize; FloatProperty moonGlowSharpness; @@ -1700,6 +1704,8 @@ namespace wi::lua::scene PropertyFunction(windWaveSize) PropertyFunction(windSpeed) PropertyFunction(stars) + PropertyFunction(sunEclipseStrength) + PropertyFunction(sunEclipseAutomatic) PropertyFunction(moonSize) PropertyFunction(moonGlowSize) PropertyFunction(moonGlowSharpness) diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 52abb40631..09abf698ef 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1859,6 +1859,9 @@ namespace wi::scene XMFLOAT3 sunColor = XMFLOAT3(0, 0, 0); XMFLOAT3 sunDirection = XMFLOAT3(0, 1, 0); + float sunEclipseStrength = 0.0f; + bool sunEclipseAutomatic = false; + uint8_t padding_sun0[3] = {}; XMFLOAT3 moonColor = XMFLOAT3(0.04f, 0.04f, 0.05f); XMFLOAT3 moonDirection = XMFLOAT3(0.0f, 0.5f, 0.8660254f); float moonSize = 0.0095f; // radians @@ -1907,6 +1910,7 @@ namespace wi::scene wi::Resource colorGradingMap; wi::Resource volumetricCloudsWeatherMapFirst; wi::Resource volumetricCloudsWeatherMapSecond; + float resolvedSunEclipseStrength = 0.0f; XMFLOAT4 stars_rotation_quaternion = XMFLOAT4(0, 0, 0, 1); uint32_t most_important_light_index = ~0u; uint32_t moon_light_index = ~0u; diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index 8528557fe0..0fc5d12bee 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -1613,6 +1613,17 @@ namespace wi::scene archive >> _flags; archive >> sunDirection; archive >> sunColor; + const bool supports_sun_eclipse = (seri.GetVersion() >= 13) || (archive.GetVersion() >= 100); + if (supports_sun_eclipse) + { + archive >> sunEclipseStrength; + archive >> sunEclipseAutomatic; + } + else + { + sunEclipseStrength = 0.0f; + sunEclipseAutomatic = false; + } moonLight = wi::ecs::INVALID_ENTITY; const bool supports_moon_params = (seri.GetVersion() >= 7) || (archive.GetVersion() >= 94); const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); @@ -1970,6 +1981,7 @@ namespace wi::scene } else { + const bool supports_sun_eclipse = (seri.GetVersion() >= 13) || (archive.GetVersion() >= 100); const bool supports_moon_params = (seri.GetVersion() >= 7) || (archive.GetVersion() >= 94); const bool supports_moon_texture = (seri.GetVersion() >= 8) || (archive.GetVersion() >= 95); const bool supports_moon_texture_bias = (seri.GetVersion() >= 9) || (archive.GetVersion() >= 96); @@ -1984,6 +1996,11 @@ namespace wi::scene archive << _flags; archive << sunDirection; archive << sunColor; + if (supports_sun_eclipse) + { + archive << sunEclipseStrength; + archive << sunEclipseAutomatic; + } if (supports_moon_params) { archive << moonDirection; From 87490de0dcfcbd4ba45607e846698e298dedfbbb Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 11 Feb 2026 22:53:20 +0000 Subject: [PATCH 09/12] Fix ShaderWeather struct packing --- WickedEngine/shaders/ShaderInterop_Weather.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WickedEngine/shaders/ShaderInterop_Weather.h b/WickedEngine/shaders/ShaderInterop_Weather.h index eac38b6948..225cfbf767 100644 --- a/WickedEngine/shaders/ShaderInterop_Weather.h +++ b/WickedEngine/shaders/ShaderInterop_Weather.h @@ -357,8 +357,6 @@ struct alignas(16) ShaderWeather { uint2 sun_direction; // packed half3 uint2 sun_color; // packed half3 - float sun_eclipse_strength; // 0-1 multiplier describing moon shadow on sun - float padding_sun0; uint2 moon_direction; // packed half3 uint2 moon_color; // packed half3 @@ -378,6 +376,9 @@ struct alignas(16) ShaderWeather uint moon_light_index; uint padding_moon0; + float sun_eclipse_strength; // 0-1 multiplier describing moon shadow on sun + float padding_sun0; + float sky_rotation_sin; float sky_rotation_cos; float sky_exposure; From 625782fa0c85f3cec8e6726da4edb7717d6cbbdd Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Thu, 12 Feb 2026 19:38:08 +0000 Subject: [PATCH 10/12] Mask moon halo to the lit hemisphere --- WickedEngine/shaders/skyHF.hlsli | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WickedEngine/shaders/skyHF.hlsli b/WickedEngine/shaders/skyHF.hlsli index 2c9ce65a6e..211db47397 100644 --- a/WickedEngine/shaders/skyHF.hlsli +++ b/WickedEngine/shaders/skyHF.hlsli @@ -215,7 +215,11 @@ float3 GetDynamicSkyColor(in float2 pixel, in float3 V, bool sun_enabled = true, float haloRadius = moonSize + haloSize; float halo = smoothstep(cos(haloRadius), innerEdge, cosAngle); halo = pow(saturate(halo), max(GetMoonHaloSharpness(), 0.0001f)); - haloContribution = halo * haloIntensity * phaseVisibility; + + // Mask halo to the lit hemisphere so crescents don't get a full circular glow. + // Use the view direction V and sunToMoonDir (direction toward lit side). + float litHaloMask = saturate(dot(V, sunToMoonDir)); + haloContribution = halo * haloIntensity * phaseVisibility * litHaloMask; } sky += moonColor * haloContribution; sky += diskColor * diskMask * saturate(1.0f - eclipseStrength * 0.9f); From 3cf3ed9c624c8e367882ad6b35a5d40d259e8634 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Thu, 12 Feb 2026 19:52:31 +0000 Subject: [PATCH 11/12] Make sure the moon occludes stars --- WickedEngine/shaders/skyAtmosphere.hlsli | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/WickedEngine/shaders/skyAtmosphere.hlsli b/WickedEngine/shaders/skyAtmosphere.hlsli index 903b78e6f9..d07d391e32 100644 --- a/WickedEngine/shaders/skyAtmosphere.hlsli +++ b/WickedEngine/shaders/skyAtmosphere.hlsli @@ -480,6 +480,22 @@ half3 GetSunLuminance(float3 worldPosition, float3 worldDirection, float3 sunDir float stars_exposure = lerp(0, 512, stars_visibility); // modifies the overall strength of the stars float stars = saturate(pow(noise_gradient_3D(stars_direction * 300), stars_threshold)) * stars_exposure; stars *= lerp(0.4, 1.4, noise_gradient_3D(stars_direction * 256 + GetTime())); // time based flickering + + // Ensure the moon disk always occludes stars (moon silhouette in + // front of starfield). + float moonSize = GetMoonSize(); + if (moonSize > 0.0) + { + float3 moonDir = GetMoonDirection(); + // soft-edge mask for nicer transition (1.0 inside disk, 0.0 outside) + float edgeSoftness = 0.0025; // radians (very small) + float cosInner = cos(moonSize - edgeSoftness); + float cosOuter = cos(moonSize + edgeSoftness); + float moonDot = dot(worldDirection, moonDir); + float moonMask = smoothstep(cosOuter, cosInner, moonDot); + stars *= (1.0 - moonMask); + } + retval += stars; } From 6b2695fe095cf0b77d9cc2114ebaa568977b5602 Mon Sep 17 00:00:00 2001 From: Anthony Gaudino Date: Wed, 25 Feb 2026 21:36:05 +0000 Subject: [PATCH 12/12] Add wi::Math::AngularDisk class --- .../wiMath/AngularDisk/AngularDisk.cpp | 227 ++++++ WickedEngine/wiMath/AngularDisk/AngularDisk.h | 644 ++++++++++++++++++ 2 files changed, 871 insertions(+) create mode 100644 WickedEngine/wiMath/AngularDisk/AngularDisk.cpp create mode 100644 WickedEngine/wiMath/AngularDisk/AngularDisk.h diff --git a/WickedEngine/wiMath/AngularDisk/AngularDisk.cpp b/WickedEngine/wiMath/AngularDisk/AngularDisk.cpp new file mode 100644 index 0000000000..d85eb0e5a1 --- /dev/null +++ b/WickedEngine/wiMath/AngularDisk/AngularDisk.cpp @@ -0,0 +1,227 @@ +#include "AngularDisk.h" +#include "wiMath.h" +#include +#include +#include +#include + +#if __has_include() + // In this case, DirectXMath is coming from Windows SDK. + // It is better to use this on Windows as some Windows libraries could + // depend on the same DirectXMath headers + #include +#else + // In this case, DirectXMath is coming from supplied source code + // On platforms that don't have Windows SDK, the source code for DirectXMath + // is provided as part of the engine utilities + #include "Utility/DirectXMath/DirectXMath.h" +#endif + +using namespace wi::math; + +/* +################################################################################ +Public +################################################################################ +*/ + +// Getters +//============================================================================== + +const XMVECTOR &AngularDisk::GetDirection() const noexcept { + return props.direction; +} + +float AngularDisk::GetRadius() const noexcept { + return props.radius; +} + +// Methods +//============================================================================== + +const AngularDisk::AngularDiskProps &AngularDisk::GetProps() const noexcept { + return props; +} + +float AngularDisk::Diameter() const noexcept { + return GetRadius() * 2; +} + +float AngularDisk::Perimeter() const noexcept { + return 2 * XM_PI * GetRadius(); +} + +float AngularDisk::Area() const noexcept { + return XM_PI * GetRadius() * GetRadius(); +} + +bool AngularDisk::HasSameRadius(const AngularDisk &other) const noexcept { + return FP_Equal(GetRadius(), other.GetRadius()); +} + +bool AngularDisk::HasSameDirection(const AngularDisk &other) const noexcept { + return FP_Equal(CosAngularSeparation(other.props.direction), 1.0F); +} + +float AngularDisk::AngularDistanceToPoint(const XMVECTOR &point) const { + assert(IsNormalized(point)); + + return XMVectorGetX( + XMVector3AngleBetweenNormals(GetDirection(), point) + ); +}; + +float AngularDisk::CosAngularSeparation(const XMVECTOR &point) const noexcept { + assert(IsNormalized(point)); + + return XMVectorGetX(XMVector3Dot(GetDirection(), point)); +} + +float AngularDisk::DisksOverlapArea(const AngularDisk &other) const { + const float radius_a = GetRadius(); + const float radius_b = other.GetRadius(); + + const float angular_separation + = AngularDistanceToPoint(other.GetDirection()); + + // No overlap + if (angular_separation >= radius_a + radius_b) + { + return 0.0F; + } + + // Same radius fast path + if (HasSameRadius(other)) + { + return SameRadiusDisksOverlapArea( + radius_a, angular_separation + ); + } + + return DifferentRadiusDisksOverlapArea( + radius_a, angular_separation, radius_b + ); +} + +float AngularDisk::DisksOverlapRatio(const AngularDisk &other) const { + return Clamp(DisksOverlapArea(other) / Area(), 0.0F, 1.0F); +} + +float AngularDisk::DisksOverlapRatioEst(const AngularDisk &other) const { + // Make sure radii are the same! + assert(HasSameRadius(other)); + + const float radius_a = GetRadius(); + const float radius_b = other.GetRadius(); + + const XMVECTOR dir_a = GetDirection(); + const XMVECTOR dir_b = other.GetDirection(); + + // Cosine of angular separation between disk centers + const float cos_theta = CosAngularSeparation(dir_b); + + // Maximum and minimum cosines defining overlap range + const float cos_max = XMScalarCosEst(2.0F * radius_a); // completely separate + const float cos_min = 1.0F; // completely overlapping + + // Linear interpolation of overlap fraction + const float overlap_factor = (cos_theta - cos_max) / (cos_min - cos_max); + + // Clamp to [0,1] + return Clamp(overlap_factor, 0.0F, 1.0F); +} + +// Operator overloading +//============================================================================== + +[[nodiscard]] bool AngularDisk::operator==( + const AngularDisk &other +) const noexcept { + return HasSameRadius(other) && HasSameDirection(other); +} + +[[nodiscard]] bool AngularDisk::operator!=( + const AngularDisk &other +) const noexcept { + return !(*this == other); +} + +/* +################################################################################ +Private +################################################################################ +*/ + +// Methods +//============================================================================== + +float AngularDisk::SameRadiusDisksOverlapArea( + const float radius, + const float distance +) const { + const float radius_sq = radius * radius; + + // Fully overlapping (identical centers) + if (distance <= 0.0F) + { + return XM_PI * radius_sq; + } + + const float distance_ratio = + Clamp(distance / (2.0F * radius), -1.0F, 1.0F); + + const float intersection_angle = XMScalarACos(distance_ratio); + + const float sqrt_term = std::sqrt( + std::max(0.0F, (4.0F * radius_sq) - (distance * distance)) + ); + + return (2.0F * radius_sq * intersection_angle) + - (0.5F * distance * sqrt_term ); +} + +float AngularDisk::DifferentRadiusDisksOverlapArea( + const float radius_a, + const float distance, + const float radius_b +) const { + const float ra_sq = radius_a * radius_a; + const float rb_sq = radius_b * radius_b; + const float distance_sq = distance * distance; + + const float alpha = + LawOfCosinesAngle(distance, radius_a, radius_b); + const float beta = + LawOfCosinesAngle(distance, radius_b, radius_a); + + return + (ra_sq * alpha) + + (rb_sq * beta) - + (0.5F * std::sqrt( + std::max(0.0F, + (-distance + radius_a + radius_b) + * ( distance + radius_a - radius_b) + * ( distance - radius_a + radius_b) + * ( distance + radius_a + radius_b)) + )); +} + +/* +################################################################################ +Free functions +################################################################################ +*/ + +std::ostream &operator<<( + std::ostream &output_stream, + const AngularDisk &disk +) { + XMFLOAT3 dir; + XMStoreFloat3(&dir, disk.GetDirection()); + + output_stream << "AngularDisk(direction=(" + << dir.x << ", " << dir.y << ", " << dir.z + << "), radius=" << disk.GetRadius() << ")"; + + return output_stream; +} diff --git a/WickedEngine/wiMath/AngularDisk/AngularDisk.h b/WickedEngine/wiMath/AngularDisk/AngularDisk.h new file mode 100644 index 0000000000..1750aa535d --- /dev/null +++ b/WickedEngine/wiMath/AngularDisk/AngularDisk.h @@ -0,0 +1,644 @@ +#pragma once + +#include "wiMath.h" +#include +#include +#if __has_include() + // In this case, DirectXMath is coming from Windows SDK. + // It is better to use this on Windows as some Windows libraries could + // depend on the same DirectXMath headers + #include +#else + // In this case, DirectXMath is coming from supplied source code + // On platforms that don't have Windows SDK, the source code for DirectXMath + // is provided as part of the engine utilities + #include "Utility/DirectXMath/DirectXMath.h" +#endif + +using namespace DirectX; + +namespace wi::math { + class AngularDisk; +} // namespace wi::math + +/** + * @brief Represents an angular disk and its operations. + * + * This class models an **angular disk**, which is a 2D circular approximation + * of a spherical cap on the unit sphere. It is defined by a **center direction + * vector** (normalized) and an **angular radius** (in radians). Although it + * resembles a spherical cap, the computations (like overlap area) are performed + * in 2D for simplicity and efficiency. + * + * Typical applications include: + * - Approximating sun or moon disks and computing eclipses. + * - Field-of-view or visibility cones. + * - Any angular region on a unit sphere for rendering or gameplay logic. + * + * References: + * https://en.wikipedia.org/wiki/Spherical_cap + * https://mathworld.wolfram.com/SphericalCap.html + * + * Example usage: + * @code + * XMVECTOR dirA = XMVectorSet(1, 0, 0, 0); + * XMVECTOR dirB = XMVectorSet(0.707f, 0.707f, 0, 0); + * AngularDisk diskA(dirA, XM_PIDIV4); // 45 degrees + * AngularDisk diskB(dirB, XM_PIDIV4); + * + * float area = diskA.ComputeDiskOverlapArea(diskB); + * float ratio = diskA.ComputeDiskOverlapRatio(diskB); + * @endcode + */ +class wi::math::AngularDisk { + /* + ############################################################################ + Private + ############################################################################ + */ + private: + + // Properties + //========================================================================== + + /** + * @brief Stores the internal representation of an angular disk. + * + * This can be passed to shaders. + */ + struct alignas(16) AngularDiskProps { + /** + * @brief The normalized direction vector of the disk center. + * + * Represents the central orientation of the angular disk in 3D space. + * Must always be normalized (unit length) to ensure correct geometric + * calculations. Defaults to pointing along the positive X axis. + */ + XMVECTOR direction = XMVectorSet(1,0,0,0); + + /** + * @brief The angular radius of the disk in radians. + * + * Defines the size of the disk on the unit sphere. + * Must be non-negative. A value of 0 represents a disk with no area. + */ + float radius = 0.0F; + } props; + + /* + ############################################################################ + Public + ############################################################################ + */ + public: + + // Constructors + //========================================================================== + + /** + * @brief Constructs an empty AngularDisk with zero radius and default + * direction. + * + * Direction is set to (1,0,0) and radius to 0.0f. + * + * Example usage: + * @code + * AngularDisk disk; // direction = (1,0,0), radius = 0 + * @endcode + * + * @note The disk is effectively invalid until a proper radius and direction + * are set. + */ + explicit AngularDisk() noexcept = default; + + /** + * @brief Constructs an AngularDisk from a non-normalized direction vector. + * + * The direction will be normalized internally. + * + * Example usage: + * @code + * XMFLOAT3 dir{1.0f, 2.0f, 3.0f}; + * AngularDisk disk(dir, XM_PIDIV4); // direction normalized internally + * @endcode + * + * @param[in] dir - Non-normalized 3D direction vector (XMFLOAT3). + * @param[in] radius - Angular radius in radians. Must be positive. + */ + explicit AngularDisk(const XMFLOAT3 &dir, float radius) noexcept + : props{ wi::math::NormalizeDirection(dir), radius } + { + assert(radius > 0.0F && "AngularDisk radius must be positive"); + } + + /** + * @brief Constructs an AngularDisk from an already normalized direction + * vector. + * + * Example usage: + * @code + * XMVECTOR dir = XMVectorSet(0.707f, 0.707f, 0, 0); + * AngularDisk disk(dir, XM_PIDIV4); // direction must be normalized + * @endcode + * + * @param[in] dir - Normalized 3D direction vector (XMVECTOR). + * @param[in] radius - Angular radius in radians. Must be positive. + */ + explicit AngularDisk(const XMVECTOR &dir, float radius) noexcept + : props{ dir, radius } + { + assert(radius > 0.0F && "AngularDisk radius must be positive"); + assert(wi::math::IsNormalized(dir)); + } + + // Getters + //========================================================================== + + /** + * @brief Returns the normalized center direction of this disk. + * + * The returned vector is guaranteed to be unit length. + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); + * XMVECTOR dir = disk.GetDirection(); + * @endcode + * + * @return A normalized direction vector representing the disk’s center. + */ + [[nodiscard]] const XMVECTOR &GetDirection() const noexcept; + + /** + * @brief Returns the angular radius of this disk. + * + * The radius is expressed in radians and is guaranteed to be non-negative. + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); + * float radius = disk.GetRadius(); // XM_PIDIV4 + * @endcode + * + * @return The angular radius (in radians). + */ + [[nodiscard]] float GetRadius() const noexcept; + + // Methods + //========================================================================== + + /** + * @brief Returns the internal representation of this angular disk. + * + * The returned `AngularDiskProps` struct contains the normalized center + * direction and angular radius of the disk. This can be useful for: + * - Passing disk data directly to shaders. + * - Serializing or logging disk properties. + * - Performing calculations without repeatedly calling individual getters. + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); + * const auto& props = disk.GetProps(); + * + * // Access direction and radius + * XMVECTOR centerDir = props.direction; + * float radius = props.radius; + * + * // Can pass props directly to a GPU buffer + * shader.SetAngularDisk(props); + * @endcode + * + * @return A constant reference to the internal AngularDiskProps struct. + */ + [[nodiscard]] const AngularDiskProps &GetProps() const noexcept; + + /** + * @brief Computes the diameter of this angular disk. + * + * The diameter is defined as twice the angular radius: + * \[ + * D = 2 r + * \] + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); // 45° radius + * float diameter = disk.Diameter(); // 90° in radians + * @endcode + * + * References: + * https://en.wikipedia.org/wiki/Circle + * + * @return The diameter of this angular disk (in radians). + */ + [[nodiscard]] float Diameter() const noexcept; + + /** + * @brief Computes the perimeter of this angular disk. + * + * The perimeter is the circumference of the circle: + * \[ + * P = 2 \pi r + * \] + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); // 45° radius + * float perimeter = disk.Perimeter(); + * @endcode + * + * References: + * https://en.wikipedia.org/wiki/Circle + * + * @return The perimeter of this angular disk (in radians). + */ + [[nodiscard]] float Perimeter() const noexcept; + + /** + * @brief Computes the area of this angular disk. + * + * The area is computed using the standard circle area formula: + * \[ + * A = \pi r^2 + * \] + * + * Example usage: + * @code + * AngularDisk disk(XMVectorSet(1,0,0,0), XM_PIDIV4); // 45° radius + * float area = disk.Area(); + * @endcode + * + * References: + * https://en.wikipedia.org/wiki/Circle + * + * @return The area of this angular disk (in steradians). + */ + [[nodiscard]] float Area() const noexcept; + + /** + * @brief Checks if this disk `a` has approximately the same radius as + * another disk `b`. + * + * Example usage: + * @code + * AngularDisk diskA(XMVectorSet(1,0,0,0), XM_PIDIV4); + * AngularDisk diskB(XMVectorSet(0,1,0,0), XM_PIDIV4 + 0.0001f); + * + * bool same = diskA.HasSameRadius(diskB); // likely returns true + * @endcode + * + * @param[in] other - The other disk `b`. + * + * @return `true` if both disks have approximately the same radius, `false` + * otherwise. + */ + [[nodiscard]] bool HasSameRadius(const AngularDisk &other) const noexcept; + + /** + * @brief Checks if this angular disk has the same center direction as + * another disk. + * + * Two disks are considered to have the same direction if the angle between + * their normalized center vectors is effectively zero. Internally, this is + * done by comparing the cosine of the angular separation to 1.0 using a + * floating-point tolerant comparison. + * + * Example usage: + * @code + * AngularDisk diskA(XMVectorSet(1,0,0,0), XM_PIDIV4); + * AngularDisk diskB(XMVectorSet(0.999999f,0.0f,0.0f,0.0f), XM_PIDIV4); + * + * // true, almost identical + * bool sameDirection = diskA.HasSameDirection(diskB); + * @endcode + * + * @param[in] other - The other disk `b` to compare with. + * + * @return `true` if the center directions are approximately equal, `false` + * otherwise. + */ + [[nodiscard]] bool HasSameDirection( + const AngularDisk &other + ) const noexcept; + + /** + * @brief Computes the angular distance between this disk's center direction + * and a given point on the unit sphere. + * + * The angular distance is defined as the angle between two normalized + * vectors on the unit sphere and is computed using the dot product: + * + * \[ + * \theta = \arccos\!\left( \mathbf{c} \cdot \mathbf{p} \right) + * \] + * + * Where: + * - \(\mathbf{c}\) is the normalized center direction of this disk, + * - \(\mathbf{p}\) is the normalized input direction, + * - \(\theta\) is the angular separation in radians. + * + * This corresponds to the geodesic distance between two points on the + * unit sphere. + * + * Example usage: + * @code + * XMVECTOR center = XMVectorSet(1, 0, 0, 0); + * XMVECTOR point = XMVectorSet(0, 1, 0, 0); + * + * AngularDisk disk(center, XM_PIDIV4); + * + * // returns ~1.5708 rad (90 degrees) + * float angle = disk.AngularDistanceToPoint(point); + * @endcode + * + * References: + * https://en.wikipedia.org/wiki/Dot_product + * https://en.wikipedia.org/wiki/Great-circle_distance + * + * @param[in] point - Normalized direction vector representing the point + * on the unit sphere. + * + * @return The angular distance (in radians) between this disk’s center + * and the input point. + */ + [[nodiscard]] float AngularDistanceToPoint(const XMVECTOR &point) const; + + /** + * @brief Computes the cosine of the angular separation between the disk + * center and a given normalized point. + * + * Faster than AngularDistanceToPoint if you only need the cosine. + * + * Example usage: + * @code + * float cos_theta = disk.CosAngularSeparation(point); + * if (cos_theta > some_threshold) { ... } + * @endcode + * + * @param[in] point - Normalized direction vector + * + * @return Cosine of the angular separation + */ + [[nodiscard]] float CosAngularSeparation( + const XMVECTOR &point + ) const noexcept; + + /** + * @brief Computes the area of this angular disk `a` that is overlapped by + * another angular disk `b`. + * + * If the disks have approximately equal angular radii, a faster + * approximation is used to compute the overlap area. + * + * Example usage: + * @code + * XMVECTOR dirA = XMVectorSet(1, 0, 0, 0); + * XMVECTOR dirB = XMVectorSet(0.707f, 0.707f, 0, 0); + * + * AngularDisk diskA(dirA, XM_PIDIV4); // 45 degrees + * AngularDisk diskB(dirB, XM_PIDIV4); + * + * // area of A overlapped by B + * float overlapArea = diskA.DisksOverlapArea(diskB); + * @endcode + * + * @param[in] other - The other disk `b`. + * + * @return The area of disk `a` that is overlapped by disk `b` (same units + * as the disk’s angular radius squared). + */ + [[nodiscard]] float DisksOverlapArea(const AngularDisk &other) const; + + /** + * @brief Computes the fraction of this angular disk `a` overlapped by + * another angular disk `b` ($|a \cap b| / |a|$). + * + * If the disks have approximately equal angular radii, a faster + * approximation is used to compute the overlap area. + * + * Example usage: + * @code + * XMVECTOR dirA = XMVectorSet(1, 0, 0, 0); + * XMVECTOR dirB = XMVectorSet(0.707f, 0.707f, 0, 0); + * + * AngularDisk diskA(dirA, XM_PIDIV4); // 45 degrees + * AngularDisk diskB(dirB, XM_PIDIV4); + * + * // fraction of A overlapped by B + * float overlapRatio = diskA.DisksOverlapRatio(diskB); + * @endcode + * + * @param[in] other - The other disk `b`. + * + * @return A value in the range [0, 1]: + * - 0.0f if disk `b` does not overlap disk `a`. + * - 1.0f if disk a is fully covered by disk b. + * - A value in (0, 1) if disk `b` partially overlaps disk `a`. + */ + [[nodiscard]] float DisksOverlapRatio(const AngularDisk &other) const; + + /** + * @brief Estimates the fraction of overlap between this angular disk `a` + * and another angular disk `b` of same radius. + * + * This is a fast linear approximation based on the angular separation + * between disk centers. Only valid when both disks have the same radius! + * + * Since the disk radii are assumed to be approximately equal, the overlap + * ratio is symmetric and can be interpreted as either $|a \cap b| / |a|$ or + * $|a \cap b| / |b|$. + * + * Example usage: + * @code + * XMVECTOR dirA = XMVectorSet(1, 0, 0, 0); + * XMVECTOR dirB = XMVectorSet(0.707f, 0.707f, 0, 0); + * + * AngularDisk diskA(dirA, XM_PIDIV4); + * AngularDisk diskB(dirB, XM_PIDIV4); + * + * float ratio = diskA.DisksOverlapRatioEst(diskB); // estimated overlap fraction + * @endcode + * + * @param[in] other - The other disk `b`. + * + * @return A value in the range [0, 1]: + * - 0.0f if disks do not overlap. + * - 1.0f if disks fully overlap. + * - A value in (0, 1) if disks partially overlap. + */ + [[nodiscard]] float DisksOverlapRatioEst(const AngularDisk &other) const; + + // Operator overloading + //========================================================================== + + /** + * @brief Checks if two angular disks are equal. + * + * Two disks are considered equal if both their normalized directions and + * angular radii are approximately equal. + * + * Example usage: + * @code + * AngularDisk diskA(XMVectorSet(1,0,0,0), XM_PIDIV4); + * AngularDisk diskB(XMVectorSet(0.707f,0.707f,0,0), XM_PIDIV4); + * + * bool equal = (diskA == diskB); // false + * @endcode + * + * @param[in] other - The other disk to compare. + * + * @return true if disks are approximately equal, false otherwise. + */ + [[nodiscard]] bool operator==(const AngularDisk &other) const noexcept; + + /** + * @brief Checks if two angular disks are not equal. + * + * Example usage: + * @code + * AngularDisk diskA(XMVectorSet(1,0,0,0), XM_PIDIV4); + * AngularDisk diskB(XMVectorSet(0.707f,0.707f,0,0), XM_PIDIV4); + * + * bool notEqual = (diskA != diskB); // true + * @endcode + * + * @param[in] other - The other disk to compare. + * + * @return true if disks differ in radius or direction, false otherwise. + */ + [[nodiscard]] bool operator!=(const AngularDisk &other) const noexcept; + + /* + ############################################################################ + Private + ############################################################################ + */ + private: + + // Methods + //========================================================================== + + /** + * @brief Computes the overlap area between this angular disk `a` and + * another angular disk `b` where both have approximately the same radius. + * + * Since the disk radii are assumed to be approximately equal, the overlap + * ratio is symmetric and can be interpreted as either $|a \cap b| / |a|$ or + * $|a \cap b| / |b|$. + * + * This function only requires the **common angular radius** of the disks + * and the **angular distance** between their centers (both in radians). The + * full direction vectors of the disks are **not needed**. + * + * Equation: + * \[ + * A_{\text{intersection}}(r, d) = + * 2 r^2 \arccos\Big(\frac{d}{2r}\Big) - \frac{d}{2} \sqrt{4 r^2 - d^2} + * \] + * + * Example usage: + * @code + * float radius = XM_PIDIV4; // 45 degrees + * float distance = XM_PIDIV4 / 2; // angular separation + * + * float overlapArea = disk.SameRadiusDisksOverlapArea(radius, distance); + * @endcode + * + * References: + * https://mathworld.wolfram.com/Circle-CircleIntersection.html + * + * @param[in] radius - Angular radius of the disks (in radians). + * @param[in] distance - Angular distance between disk centers (in radians). + * + * @return Area of the disks’ overlap. + */ + [[nodiscard]] float SameRadiusDisksOverlapArea( + float radius, + float distance + ) const; + + /** + * @brief Computes the overlap area of another angular disk `b` on this + * angular disk `a` when the disks have different angular radii (in + * radians). + * + * The computation is based on the **circle–circle intersection formula**: + * \[ + * A_{\text{intersection}} = + * r_a^2 \alpha + r_b^2 \beta - \frac{1}{2} \sqrt{(-d+r_a+r_b)(d+r_a-r_b)(d-r_a+r_b)(d+r_a+r_b)} + * \] + * + * Where: + * - \(r_a, r_b\) are the angular radii of disks `a` and `b` + * - \(d\) is the angular distance between the disk centers + * - \(\alpha = \arccos \frac{d^2 + r_a^2 - r_b^2}{2 d r_a}\) + * - \(\beta = \arccos \frac{d^2 + r_b^2 - r_a^2}{2 d r_b}\) + * + * Example usage: + * @code + * float radiusA = 0.5f; // radians + * float radiusB = 0.3f; // radians + * float distance = 0.4f; // radians between centers + * + * float area = diskA.DifferentRadiusDisksOverlapArea( + * radiusA, + * distance, + * radiusB + * ); + * + * // 'area' contains the 2D circular overlap area approximation + * // expressed in radians^2. + * @endcode + * + * References: + * https://mathworld.wolfram.com/Circle-CircleIntersection.html + * + * @param[in] radius_a - Angular radius of disk `a` (in radians). + * @param[in] distance - Angular distance between disk centers (in radians). + * @param[in] radius_b - Angular radius of disk `b` (in radians). + * + * @return Area of disk `a` that is overlapped by disk `b`. + */ + [[nodiscard]] float DifferentRadiusDisksOverlapArea( + float radius_a, + float distance, + float radius_b + ) const; +}; + +/* +################################################################################ +Free functions +################################################################################ +*/ + +/** + * @brief Writes a textual representation of an AngularDisk to an output stream. + * + * Format: + * AngularDisk(direction=(x, y, z), radius=r) + * + * Example usage: + * @code + * wi::math::AngularDisk disk( + * XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f), + * 0.5f + * ); + * + * std::cout << disk << std::endl; + * + * // Possible output: + * // AngularDisk(direction=(1, 0, 0), radius=0.5) + * @endcode + * + * @param[in,out] output_stream - The output stream to which the disk + * representation will be written. + * @param[in] disk - The AngularDisk instance to print. + * + * @return A reference to the same output stream, allowing chained + * stream operations (e.g., std::cout << disk << std::endl). + */ +std::ostream& operator<<( + std::ostream& output_stream, const wi::math::AngularDisk &disk +);