From 34d35881c20c3ed8bdd3aaabdb7ca3798b8b934f Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Tue, 29 Sep 2026 17:31:51 +0100
Subject: [PATCH] GFX: volumetric light (sun/moon shafts) and moonlight
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5
---
LIGHTING.md | 4 +-
README.md | 4 +-
crates/benilla-app/assets/ui/OptionsFrame.xml | 88 ++-
crates/benilla-app/src/capture/scenarios.rs | 45 ++
crates/benilla-app/src/cvars.rs | 34 +-
crates/benilla-app/src/dynamic_interior.rs | 7 +
crates/benilla-app/src/game_plugins.rs | 2 +
crates/benilla-app/src/lib.rs | 2 +
crates/benilla-app/src/pipe_warm/menagerie.rs | 2 +
.../src/ui_script/options_tests.rs | 4 +
crates/benilla-app/src/video.rs | 16 +
crates/benilla-app/src/volumetric_fog.rs | 11 +-
crates/benilla-app/src/volumetric_light.rs | 735 ++++++++++++++++++
crates/benilla-app/src/volumetric_light.wgsl | 208 +++++
crates/benilla-assets/src/materials.rs | 2 +
.../src/shaders/enhanced_water.wgsl | 4 +-
.../src/shaders/monkey_frame.wgsl | 6 +-
.../src/shaders/moonlight_hook.wgsl | 37 +
.../src/shaders/shadow_hook.wgsl | 32 +
.../benilla-assets/src/shaders/terrain.wgsl | 15 +-
.../benilla-assets/src/shaders/wow_model.wgsl | 17 +-
.../src/lighting/global_light.rs | 25 +-
crates/benilla-world/src/lighting/mod.rs | 18 +
.../src/lighting/monkey_frame.rs | 31 +-
.../benilla-world/src/lighting/moonlight.rs | 129 +++
.../benilla-world/src/shaders/static_gx.wgsl | 18 +-
26 files changed, 1467 insertions(+), 29 deletions(-)
create mode 100644 crates/benilla-app/src/volumetric_light.rs
create mode 100644 crates/benilla-app/src/volumetric_light.wgsl
create mode 100644 crates/benilla-assets/src/shaders/moonlight_hook.wgsl
create mode 100644 crates/benilla-world/src/lighting/moonlight.rs
diff --git a/LIGHTING.md b/LIGHTING.md
index 49eaa996..53e4504e 100644
--- a/LIGHTING.md
+++ b/LIGHTING.md
@@ -24,8 +24,10 @@ behind each constant.
| City day floor | `interiorDaylight`'s room floor also reaches Stormwind/Ironforge rooms that the portal graph connects to the sky (an exterior-facing portal, or one hop from one; `district_sky_rooms` in `daylight.rs`); before this no city room could take it | `interiorDaylight` (default 0, suggested High 0.10) |
| Spell and firework lights | fire, holy and fel effects light their surroundings for their lifetime; frost, nature, arcane and shadow do not | `spellLightGain`, env `WOW_SPELL_LIGHT=0` |
| Moon shadows | at night the same shadow rig re-aims at the moon and casts a faint shadow; dims only the night sky term, never point lights | `moonShadowStrength` |
+| Moonlight | GFX (moonlight): the moon as a light. The stock 1.12 night (Light.dbc night colours on the never-setting lighting sun) stays the base; a cool (0.62, 0.74, 1.0), dim directional term from the visible white moon (`daynight::moon_direction`, the disc the sky draws) is ADDED per fragment with a soft-wrapped N·L on terrain, models, WMO exteriors and retained doodads, shadowed by the moon's own map (`shadow_hook::realtime_shadow_moonlit` `.z`, faded in with the rig's hand-over ramp). It ramps with the moon's elevation (0-12°) and out with the sun, dims under storms, and never reaches interiors or the glue booth. Packed as MonkeyFrame row 16 (`moon`: direction + intensity, base 0.16 gamma at `moonLight 1`) and `misc.w` (hand-over confidence); the per-frame blob is 8800 B (`lighting/moonlight.rs`, `moonlight_hook.wgsl`) | Advanced Graphics → Moonlight, cvar `moonLight` 0..2 (0 = the reference night, bit for bit; Graphics preset Low..Ultra = 1) |
| Ground-effect spells | Flamestrike, Rain of Fire, Consecration, Flare and fire traps light the ground for their duration; frost and nature areas stay dark | `spellLightGain` |
-| Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors; the dawn curve follows the rendered clock, so a manual or capture time sets it; over an ocean a point below sea level is hazed at the sea surface, since water writes no depth) and sun/moon light shafts through gaps, sampled from the shadow map; own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` |
+| Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors; the dawn curve follows the rendered clock, so a manual or capture time sets it; over an ocean a point below sea level is hazed at the sea surface, since water writes no depth) and daytime sun shafts through gaps beyond 10 yd, sampled from the shadow map (never moon shafts: they are gated by daylight; retired while Volumetric Light is on); own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` |
+| Volumetric light | GFX (volumetric light): shadow-mapped sun and moon shafts. The directional light the shadow rig holds (sun by day, moon at night) is ray-marched along each view ray at HALF resolution: 16 (Medium) or 32 (High) samples, quadratic spacing from 0.35 yd (dense near the eye), a per-pixel interleaved-gradient-noise offset, cascade lookups with the covered-sample mean past the cascades, air thinning with height above the eye (10 yd e-fold, so shafts live under canopies and between buildings rather than in the open sky), a Henyey-Greenstein g = 0.6 phase with a quarter isotropic. A depth-aware 4x4 half-res blur removes the jitter and a depth-aware 2x2 upsample composites it with the fog pass's soft screen curve. Extinction = the volumetric fog density law (dawn mist, weather) x the zone's fog end (350 yd reference, x0.7-2.5); colour = sun diffuse (or the moon tint x moonlight intensity, faint) leaned 35 % toward the zone fog hue. No shafts in WMO interiors. While it is on, the volumetric fog pass retires its own midpoint shafts and keeps only its haze and lamp halos. Three passes after the fog node (`benilla-app/src/volumetric_light.rs` + `.wgsl`); no temporal history. Dev capture levers `WOW_VOLLIGHT_DENSITY/_HEIGHT/_GAIN/_DEBUG` (1 = the in-scatter alone) | Advanced Graphics → Volumetric Light (Off/Medium/High) + Volumetric Light Strength 0..2, cvars `volumetricLight`, `volumetricLightStrength`, env `WOW_VOLLIGHT=0\|1\|2`; Graphics preset Medium = 1, High/Ultra = 2 |
| Sky dithering | a faint screen-space dither in the FFXGlow combine so smooth sky and fog gradients do not band (MONKEY p0; was env-only) | Advanced Graphics → Sky Dithering, cvar `skyDither` 0/1 (default 0, Graphics preset High = 1), env `WOW_DITHER=1` still forces it on; bridge in `benilla-app/src/monkey_gfx.rs` |
| HDR emission and bloom | lit WMO windows, additive models/particles and magma can exceed display white; a soft-capped fullscreen halo is added before the faithful FFX clamp | Advanced Graphics → Bloom (Off/Low/High), cvar `bloom`; Off is the unchanged reference image, suggested High preset value 2 |
| Sky quality | Enhanced: the five Light.dbc sky stops through a smooth monotone curve in linear light (no bands at the rings), a soft sun glow tinted by sun and fog colour (fades at night and under cloud), a procedural star field with twinkle and a faint Milky Way over the stock `Stars.m2`. High adds domain-warped cloud detail and sun-lit clouds (self-shadow, silver lining; technique from WarcraftXL, see `THIRD-PARTY.md`). Classic is the reference sky unchanged | Advanced Graphics → Sky Quality, cvar `skyQuality` 0 Classic / 1 Enhanced / 2 High, env `WOW_SKY_QUALITY` |
diff --git a/README.md b/README.md
index 41caba37..31decb8d 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ pull request.
**Lighting and shadows**
- Realtime sun shadows for characters and the world, including foliage; moon shadows at night
+- Moonlight: at night the moon lights the world (cool, dim, from the moon's place in the sky) on top of the original night colours
- Dynamic building interiors lit by their own fixtures
- Torches, braziers and lamps emit flickering light and cast cube-map shadows; terrain blocks torch light
- Daylight through doors and windows (including Stormwind's rooms and cathedral windows)
@@ -26,7 +27,8 @@ pull request.
**Fog and atmosphere**
- Modern fog model: the world fades into the horizon, sun-coloured toward the sun
-- Volumetric fog with sun and moon light shafts
+- Volumetric fog (distance haze)
+- Volumetric light: sun and moon shafts marched through the shadow map (god rays through canopies and between buildings, faint moon shafts at night)
- Lamps glowing through fog at night
- Screen-space sun shafts
- Render distance up to 1497 yards
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index d02cfa4d..141189fe 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -510,6 +510,10 @@
ambientOcclusion = "Ambient Occlusion",
-- MONKEY (lampfog)
lampFog = "Lamp Fog",
+ -- GFX (volumetric light) / (moonlight)
+ volumetricLight = "Volumetric Light",
+ volumetricLightStrength = "Volumetric Light Strength",
+ moonLight = "Moonlight",
-- MONKEY (integration): the daylight lane's district window split.
daylightWindowSplit = "City Window Daylight",
yards = "%d yd",
@@ -518,8 +522,8 @@
GRAPHICS_QUALITY =
"Sets every graphics setting at once: Lighting Quality and everything it sets, "
.. "Render Distance, Sky Quality, Sky Dithering, Modern Fog, Wet Surfaces in Rain, "
- .. "Terrain Blocks Torchlight, the daylight through city windows, Ambient Occlusion, Lamp Fog "
- .. "and Zone Skyboxes.\n\n"
+ .. "Terrain Blocks Torchlight, the daylight through city windows, Ambient Occlusion, Lamp Fog, "
+ .. "Zone Skyboxes, Volumetric Light and Moonlight.\n\n"
.. "Classic is the original client: every enhancement off and the original 350 yd "
.. "view distance. Low and Medium add the enhancements that cost almost nothing; High "
.. "is the recommended look; Ultra raises every quality to its maximum and the view "
@@ -694,6 +698,21 @@
.. "nearest 16 lights; High uses 32. Rain and fog strengthen the halos. "
.. "The effect is unshadowed, but stops at visible scene depth.\n\n"
.. "Off is unchanged, and daylight is unchanged at every setting.",
+ -- GFX (volumetric light)
+ VOLUMETRIC_LIGHT =
+ "Sunlight and moonlight shine through the air as visible shafts: rays through "
+ .. "tree canopies, between buildings and past your character, following the "
+ .. "real shadows. Thicker at dawn, in fog and in rain. Needs World Shadows or "
+ .. "Real Character Shadows; not drawn indoors.\n\n"
+ .. "Medium uses fewer samples; High is smoother. Off is unchanged.",
+ VOLUMETRIC_LIGHT_STRENGTH =
+ "How bright the volumetric light shafts are. 100% is the default; 0 hides them.",
+ -- GFX (moonlight)
+ MOONLIGHT =
+ "The moon lights the world at night: a cool, dim light from where the moon "
+ .. "really stands in the sky, added on top of the original night colours, with "
+ .. "moon shadows when Moon Shadows is above 0.\n\n"
+ .. "At 0 the night looks exactly as the original client drew it. Costs nothing by day.",
},
},
ruRU = {
@@ -755,6 +774,10 @@
ambientOcclusion = "Затенение окружения",
-- MONKEY (lampfog)
lampFog = "Свечение фонарей в тумане",
+ -- GFX (volumetric light) / (moonlight)
+ volumetricLight = "Объёмный свет",
+ volumetricLightStrength = "Сила объёмного света",
+ moonLight = "Лунный свет",
-- MONKEY (integration): the daylight lane's district window split.
daylightWindowSplit = "Дневной свет через окна города",
yards = "%d ярд.",
@@ -764,7 +787,7 @@
"Задаёт все настройки графики сразу: качество освещения и всё, что оно задаёт, "
.. "дальность прорисовки, качество неба, дизеринг неба, современный туман, мокрые "
.. "поверхности в дождь, заслонение огней рельефом, дневной свет в окнах городов, "
- .. "затенение окружения, свечение фонарей в тумане и небосводы зон.\n\n"
+ .. "затенение окружения, свечение фонарей в тумане, небосводы зон, объёмный свет и лунный свет.\n\n"
.. "«Классика» — оригинальный клиент: все улучшения выключены, дальность обзора "
.. "прежние 350 ярдов. «Низкое» и «Среднее» добавляют почти бесплатные улучшения; "
.. "«Высокое» — рекомендуемый вид; «Ультра» поднимает всё качество до максимума, а "
@@ -949,6 +972,22 @@
.. "качество использует 16 ближайших источников, высокое — 32. Дождь и туман усиливают ореолы. "
.. "Эффект не рассчитывает тени, но обрывается на видимой глубине.\n\n"
.. "Выключено — без изменений; днём изображение не меняется при любом значении.",
+ -- GFX (volumetric light)
+ VOLUMETRIC_LIGHT =
+ "Солнечный и лунный свет виден в воздухе лучами: сквозь кроны деревьев, между "
+ .. "зданиями и рядом с персонажем, по настоящим теням. Гуще на рассвете, в тумане "
+ .. "и под дождём. Нужны тени мира или реальные тени персонажей; в помещениях не "
+ .. "рисуется.\n\n"
+ .. "«Среднее» использует меньше выборок, «Высокое» — более плавное. Выключено — "
+ .. "без изменений.",
+ VOLUMETRIC_LIGHT_STRENGTH =
+ "Яркость лучей объёмного света. 100% — по умолчанию; 0 скрывает их.",
+ -- GFX (moonlight)
+ MOONLIGHT =
+ "Луна освещает мир ночью: холодный тусклый свет с того места, где луна "
+ .. "действительно стоит на небе, поверх исходных ночных цветов, с лунными тенями, "
+ .. "если «Лунные тени» больше 0.\n\n"
+ .. "При 0 ночь выглядит точно так же, как в оригинальном клиенте. Днём не стоит ничего.",
},
},
}
@@ -1030,6 +1069,8 @@
"RowDaylightWindowSplit",
-- MONKEY (integration): the skybox lane's row (the lane left it out of this list).
"RowZoneSkyboxes",
+ -- GFX (volumetric light) / (moonlight): appended so the other rows keep their indices.
+ "RowVolumetricLight", "RowVolumetricLightStrength", "RowMoonLight",
},
-- Guild Names is a child of Player Names (UIOptionsFrame.lua:703-709).
Nameplates = {
@@ -3210,6 +3251,47 @@
"BENILLA_TOOLTIP_ZONE_SKYBOXES")
+
+
+
+
+
+
+
+ OptionsRow_OnLoad(self, "volumetricLight", BENILLA_ADVGFX.volumetricLight,
+ "BENILLA_TOOLTIP_VOLUMETRIC_LIGHT")
+ OptionsDropdownRow_Setup(self, {
+ { text = BENILLA_ADVGFX.fogOff, value = "0" },
+ { text = BENILLA_ADVGFX.qMedium, value = "1" },
+ { text = BENILLA_ADVGFX.qHigh, value = "2" },
+ })
+
+
+
+
+
+
+
+
+
+ OptionsRow_OnLoad(self, "volumetricLightStrength",
+ BENILLA_ADVGFX.volumetricLightStrength,
+ "BENILLA_TOOLTIP_VOLUMETRIC_LIGHT_STRENGTH")
+ OptionsSliderRow_Setup(self, 0, 2, 0.25)
+
+
+
+
+
+
+
+
+
+ OptionsRow_OnLoad(self, "moonLight", BENILLA_ADVGFX.moonLight,
+ "BENILLA_TOOLTIP_MOONLIGHT")
+ OptionsSliderRow_Setup(self, 0, 2, 0.25)
+
+