Merge gfx/fog into integration
# Conflicts: # LIGHTING.md # crates/benilla-app/assets/ui/OptionsFrame.xml # crates/benilla-app/src/cvars.rs # crates/benilla-app/src/monkey_gfx.rs # crates/benilla-assets/src/shaders/enhanced_water.wgsl # crates/benilla-assets/src/shaders/fog_hook.wgsl # crates/benilla-assets/src/shaders/monkey_frame.wgsl # crates/benilla-world/src/lighting/monkey_frame.rs # crates/benilla-world/src/shaders/sky.wgsl # crates/benilla-world/src/sky.rs
This commit is contained in:
+2
-1
@@ -29,6 +29,7 @@ behind each constant.
|
||||
| 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` |
|
||||
| Modern fog | MONKEY (fog): radial distance fog with a gradual exponential curve, a daylight sun lobe, and an end colour shared by the world, WDL hull, sky horizon and volumetric haze. Each zone keeps its authored fog end up to the reference view-distance limit; optional `LightFogBand.dbc` rows provide height, sun and end-fog controls. Interior WMO fog stays classic | Advanced Graphics → Modern Fog, cvar `fogModel` 0 Classic (default) / 1 Modern (High = 1), env `WOW_FOGMODEL=0\|1`; `fog_hook.wgsl`, `lighting/fog_model.rs`, `light/fog_band.rs` |
|
||||
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND`; field and benders in `benilla-world/src/wind/` |
|
||||
| Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` |
|
||||
|
||||
@@ -81,7 +82,7 @@ is its own module, documented in `WATER.md`.
|
||||
| `fog_a` | height_fog_density | height_fog_height | height_fog_falloff | curve_blend |
|
||||
| `fog_b` | sun_fog r | g | b | sun_fog_strength |
|
||||
| `fog_c` | end_fog r | g | b | end_fog_distance |
|
||||
| `fog_d` | fog_model (0 classic, 1 modern) | sun_fog_angle | 0 | 0 |
|
||||
| `fog_d` | fog_model (0 classic; Modern = scene fog end in yd) | sun_fog_angle | sun direction octahedral x | y |
|
||||
| `wind_a` | dir_x | dir_y | speed | gust |
|
||||
| `wind_b` | time_s | sway_strength | grass_strength | tree_strength |
|
||||
| `wet_a` | rain_rate | wetness | ripple_time_s | snow |
|
||||
|
||||
@@ -528,6 +528,8 @@
|
||||
interiorDarkness = "Interior Darkness",
|
||||
-- MONKEY (p0 skyDither)
|
||||
skyDither = "Sky Dithering",
|
||||
-- MONKEY (fog)
|
||||
fogModel = "Modern Fog",
|
||||
-- MONKEY (wind)
|
||||
foliageWind = "Foliage Wind",
|
||||
windGrass = "Grass",
|
||||
@@ -648,7 +650,14 @@
|
||||
SKY_DITHER =
|
||||
"Adds a faint noise to the final image so smooth sky and fog gradients do not "
|
||||
.. "break into visible bands. Off is the original client's look. Practically free.",
|
||||
-- MONKEY (wind)
|
||||
-- MONKEY (fog)
|
||||
FOG_MODEL =
|
||||
"Softer distance fog in the style of the modern client: it thickens gradually "
|
||||
.. "instead of in a straight ramp, glows toward the sun by day and shifts to the "
|
||||
.. "sky's colour in the distance, so the land melts into the horizon. Each zone "
|
||||
.. "keeps its own fog distance; above the original view-distance limit the fog "
|
||||
.. "recedes with it.\n\nOff is the original client's fog. Practically free.",
|
||||
-- MONKEY (wind)
|
||||
FOLIAGE_WIND =
|
||||
"Grass bends in two travelling wind waves and parts around nearby characters. "
|
||||
.. "Grass + Trees also gives classified tree and bush foliage a gentle sway.\n\n"
|
||||
@@ -694,6 +703,8 @@
|
||||
interiorDarkness = "Темнота помещений",
|
||||
-- MONKEY (p0 skyDither)
|
||||
skyDither = "Дизеринг неба",
|
||||
-- MONKEY (fog)
|
||||
fogModel = "Современный туман",
|
||||
-- MONKEY (wind)
|
||||
foliageWind = "Ветер листвы",
|
||||
windGrass = "Трава",
|
||||
@@ -824,7 +835,14 @@
|
||||
"Добавляет едва заметный шум в итоговое изображение, чтобы плавные переходы цвета "
|
||||
.. "неба и тумана не распадались на полосы. Выключено — вид оригинального клиента. "
|
||||
.. "Почти ничего не стоит.",
|
||||
-- MONKEY (wind)
|
||||
-- MONKEY (fog)
|
||||
FOG_MODEL =
|
||||
"Более мягкий туман в духе современного клиента: он сгущается постепенно, а не "
|
||||
.. "по прямой, днём светлеет в сторону солнца, а вдали принимает цвет неба, "
|
||||
.. "так что земля растворяется в горизонте. Дальность тумана в каждой зоне "
|
||||
.. "прежняя; за пределом дальности обзора оригинала туман отступает вместе с ней."
|
||||
.. "\n\nВыключено — туман оригинального клиента. Почти ничего не стоит.",
|
||||
-- MONKEY (wind)
|
||||
FOLIAGE_WIND =
|
||||
"Трава гнётся двумя бегущими волнами ветра и расступается вокруг персонажей. "
|
||||
.. "Трава + деревья также добавляет мягкое качание листвы деревьев и кустов.\n\n"
|
||||
@@ -895,6 +913,8 @@
|
||||
"RowNightDarkness", "RowInteriorDarkness",
|
||||
-- MONKEY (p0 skyDither)
|
||||
"RowSkyDither",
|
||||
-- MONKEY (fog)
|
||||
"RowFogModel",
|
||||
-- MONKEY (wind): appended for conflict-free lane merges.
|
||||
"RowFoliageWind",
|
||||
},
|
||||
@@ -3005,12 +3025,24 @@
|
||||
"BENILLA_TOOLTIP_SKY_DITHER")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- MONKEY (wind): three foliage tiers; appended at the end of the page. -->
|
||||
<Frame name="$parentRowFoliageWind" inherits="OptionsDropdownRowTemplate">
|
||||
<!-- MONKEY (fog): the distance-fog model. -->
|
||||
<Frame name="$parentRowFogModel" inherits="OptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowSkyDither" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowSkyDither" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
OptionsRow_OnLoad(self, "fogModel",
|
||||
BENILLA_ADVGFX.fogModel,
|
||||
"BENILLA_TOOLTIP_FOG_MODEL")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- MONKEY (wind): three foliage tiers; appended at the end of the page. -->
|
||||
<Frame name="$parentRowFoliageWind" inherits="OptionsDropdownRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowFogModel" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowFogModel" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
OptionsRow_OnLoad(self, "foliageWind",
|
||||
BENILLA_ADVGFX.foliageWind,
|
||||
|
||||
@@ -1592,6 +1592,41 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// a world-image instrument, and UI script errors must not cover the grass it measures.
|
||||
ui: None,
|
||||
},
|
||||
// MONKEY (fog): Duskwood's short zone fog (Light 15, LightParams 14) from above the canopy west
|
||||
// of Darkshire toward the town, for the Classic/Modern fog A/B (`WOW_FOGMODEL=0|1`).
|
||||
Scenario {
|
||||
name: "fog-duskwood-noon",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-10700.0, -900.0, 160.0],
|
||||
look: [-10560.0, -1180.0, 60.0],
|
||||
minute: 720,
|
||||
ui: None,
|
||||
},
|
||||
// MONKEY (fog): a long Elwynn vista from the Northshire ridge south over Goldshire's forest.
|
||||
Scenario {
|
||||
name: "fog-vista-noon",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9000.0, -100.0, 200.0],
|
||||
look: [-9500.0, -100.0, 90.0],
|
||||
minute: 720,
|
||||
ui: None,
|
||||
},
|
||||
Scenario {
|
||||
name: "fog-vista-dusk",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9000.0, -100.0, 200.0],
|
||||
look: [-9500.0, -100.0, 90.0],
|
||||
minute: 1170,
|
||||
ui: None,
|
||||
},
|
||||
Scenario {
|
||||
name: "fog-duskwood-dusk",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-10700.0, -900.0, 160.0],
|
||||
look: [-10560.0, -1180.0, 60.0],
|
||||
minute: 1170,
|
||||
ui: None,
|
||||
},
|
||||
];
|
||||
|
||||
/// MONKEY (p0 baseline): the Elwynn programme view, above the canopy east of Goldshire looking
|
||||
|
||||
@@ -944,6 +944,14 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
"2",
|
||||
"benilla's own: foliage wind, 0 Off / 1 Grass / 2 Grass + trees",
|
||||
),
|
||||
// MONKEY (fog): the distance-fog model. 0 = the 1.12 linear fog (byte-identical), 1 = Modern
|
||||
// (exponential, sun/horizon colour, end-fog shift, fog end decoupled from farclip past 777).
|
||||
// Default 0; the Graphics preset's High sets 1.
|
||||
ours(
|
||||
"fogModel",
|
||||
"0",
|
||||
"benilla's own: distance fog, 0 Classic (1.12 linear) / 1 Modern (soft, sun-coloured horizon)",
|
||||
),
|
||||
ours(
|
||||
"waterQuality",
|
||||
"1",
|
||||
@@ -3781,6 +3789,8 @@ mod tests {
|
||||
// MONKEY (p0 graphics programme)
|
||||
|app| {
|
||||
app.init_resource::<benilla_world::ffx_glow::SkyDither>();
|
||||
// MONKEY (fog)
|
||||
app.init_resource::<benilla_world::lighting::FogModelSetting>();
|
||||
app.add_observer(crate::monkey_gfx::on_cvar);
|
||||
},
|
||||
|app| {
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
//! |---|---|---|---|---|
|
||||
//! | `skyDither` | 0 Off / 1 On | 0 | 1 | `benilla_world::ffx_glow::SkyDither` |
|
||||
//! | `foliageWind` | 0 Off / 1 Grass / 2 Grass + trees | 2 | 2 | `benilla_world::wind::FoliageWind` |
|
||||
//! | `fogModel` | 0 Classic / 1 Modern | 0 | 1 | `benilla_world::lighting::FogModelSetting` |
|
||||
|
||||
use benilla_world::ffx_glow::SkyDither;
|
||||
use benilla_world::lighting::FogModelSetting;
|
||||
use benilla_world::wind::FoliageWind;
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -16,6 +18,8 @@ pub(crate) fn on_cvar(
|
||||
ev: On<crate::cvars::CvarChanged>,
|
||||
mut dither: ResMut<SkyDither>,
|
||||
mut foliage_wind: ResMut<FoliageWind>,
|
||||
// MONKEY (fog)
|
||||
mut fog_model: ResMut<FogModelSetting>,
|
||||
) {
|
||||
match ev.key().as_str() {
|
||||
"skydither" => {
|
||||
@@ -37,6 +41,8 @@ pub(crate) fn on_cvar(
|
||||
foliage_wind.0 = want;
|
||||
}
|
||||
}
|
||||
// MONKEY (fog): `WOW_FOGMODEL` still wins for the session (captures).
|
||||
"fogmodel" => fog_model.set_from_cvar(ev.num()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +55,7 @@ impl Plugin for MonkeyGfxPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<SkyDither>()
|
||||
.init_resource::<FoliageWind>()
|
||||
.init_resource::<FogModelSetting>()
|
||||
.add_observer(on_cvar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ struct FogView {
|
||||
sun_strength: Vec4,
|
||||
// Actual celestial direction and raymarch step count.
|
||||
direction_steps: Vec4,
|
||||
// MONKEY (fog): MonkeyFrame fog rows 1-3. Modern (fog3.x > 0) colours the haze with the shared
|
||||
// fog colour function and keeps sky pixels (depth 0) out of the distance haze.
|
||||
mf_fog1: Vec4,
|
||||
mf_fog2: Vec4,
|
||||
mf_fog3: Vec4,
|
||||
}
|
||||
|
||||
impl Plugin for VolumetricFogPlugin {
|
||||
@@ -138,6 +143,8 @@ fn update_fog(
|
||||
clock: Res<WorldTime>,
|
||||
weather: Res<WeatherState>,
|
||||
interior: Res<CameraInteriorClaim>,
|
||||
// MONKEY (fog): the Modern fog rows (Option: absent in unit tests).
|
||||
monkey: Option<Res<benilla_world::lighting::MonkeyFrame>>,
|
||||
mut cameras: Query<(Entity, &Camera, &mut Camera3d), With<WorldCamera>>,
|
||||
suns: Query<(Entity, &DirectionalLight), With<ShadowSun>>,
|
||||
) {
|
||||
@@ -161,7 +168,12 @@ fn update_fog(
|
||||
let sun = lighting.celestial_dir();
|
||||
let daylight = ((sun.y + 0.02) / 0.15).clamp(0.0, 1.0);
|
||||
let fog = Vec3::from_array(lighting.fog_color);
|
||||
// MONKEY (fog): rows 1-3 as packed for the light buffer.
|
||||
let rows = monkey.as_ref().map_or([[0.0; 4]; 16], |m| m.pack(0.0, 0.0));
|
||||
commands.entity(entity).insert(FogView {
|
||||
mf_fog1: Vec4::from_array(rows[1]),
|
||||
mf_fog2: Vec4::from_array(rows[2]),
|
||||
mf_fog3: Vec4::from_array(rows[3]),
|
||||
color_density: fog.extend(
|
||||
density(
|
||||
clock.minute_f,
|
||||
@@ -371,6 +383,7 @@ impl ViewNode for FogNode {
|
||||
const FOG_SHADER: &str = r#"
|
||||
#import bevy_render::view::View
|
||||
#import bevy_pbr::mesh_view_types::Lights
|
||||
#import benilla::fog_hook
|
||||
@group(0) @binding(0) var scene: texture_2d<f32>;
|
||||
#ifdef MULTISAMPLED
|
||||
@group(0) @binding(1) var depth: texture_depth_multisampled_2d;
|
||||
@@ -381,7 +394,11 @@ const FOG_SHADER: &str = r#"
|
||||
@group(0) @binding(3) var<uniform> lights: Lights;
|
||||
@group(0) @binding(4) var shadows: texture_depth_2d_array;
|
||||
@group(0) @binding(5) var shadow_sampler: sampler_comparison;
|
||||
struct Fog { color_density: vec4<f32>, sun_strength: vec4<f32>, direction_steps: vec4<f32> }
|
||||
struct Fog {
|
||||
color_density: vec4<f32>, sun_strength: vec4<f32>, direction_steps: vec4<f32>,
|
||||
// MONKEY (fog): MonkeyFrame fog1..fog3.
|
||||
mf_fog1: vec4<f32>, mf_fog2: vec4<f32>, mf_fog3: vec4<f32>,
|
||||
}
|
||||
@group(0) @binding(6) var<uniform> fog: Fog;
|
||||
|
||||
// Integral of smoothstep(10, 30, distance): zero in the first ten yards,
|
||||
@@ -420,9 +437,17 @@ fn fragment(@builtin(position) pixel: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
if (distance <= 10.0) { return source; }
|
||||
let ray = normalize(delta);
|
||||
let optical_depth = fog.color_density.w * fog_path(distance);
|
||||
let opacity = 1.0 - exp(-optical_depth);
|
||||
var opacity = 1.0 - exp(-optical_depth);
|
||||
var haze_rgb = fog.color_density.rgb;
|
||||
if (fog_hook::fog_is_modern(fog.mf_fog3)) {
|
||||
// MONKEY (fog): the sky (depth 0, clamped to 150 yd above) already carries its own
|
||||
// horizon fog, so it takes no distance haze; the haze colour is the shared fog colour.
|
||||
if (z <= 0.0) { opacity = 0.0; }
|
||||
haze_rgb = fog_hook::fog_modern_colour(fog.color_density.rgb, ray, length(delta),
|
||||
fog.mf_fog1, fog.mf_fog2, fog.mf_fog3);
|
||||
}
|
||||
let luma = vec3(0.2126, 0.7152, 0.0722);
|
||||
var haze = mix(source.rgb, fog.color_density.rgb, opacity);
|
||||
var haze = mix(source.rgb, haze_rgb, opacity);
|
||||
// A darker zone colour may change hue, never the pixel's luminance. Add
|
||||
// neutral headroom proportionally so saturated channels cannot clip dark.
|
||||
let missing = max(0.0, dot(source.rgb - haze, luma));
|
||||
|
||||
@@ -15,6 +15,21 @@
|
||||
//
|
||||
// Extension point for the FOG lane: `fog_sample` branches on `mf.fog_d.x` (`MonkeyFrame.fog_model`,
|
||||
// 0 classic / 1 modern). The modern arm currently returns the classic result.
|
||||
//
|
||||
// MONKEY (fog): the Modern model (cvar `fogModel` 1; CPU half `lighting/fog_model.rs`). `fog_d.x`
|
||||
// holds the scene fog end when Modern is on (0 = Classic, every path below is then untouched):
|
||||
// - survival: radial distance, exponential from the zone's start, fitted so 60 % survives at the
|
||||
// middle of the 1.12 span, closed linearly over the last stretch (from max(start, 0.3·end), the
|
||||
// modern client's 1/0.7 end fade) so the fog still ends where the zone authored it; optional
|
||||
// height fog below `fog_a.y` (Bevy Y); `fog_a.w` blends back toward the 1.12 linear ramp.
|
||||
// - colour (scene colour only, never the black/white/grey blend policies): shifts to the end-fog
|
||||
// colour `fog_c` by (d / end)³, then leans to the sun-fog colour `fog_b` inside a cubic lobe around
|
||||
// the sun (cosine threshold `fog_d.y`, direction octahedral in `fog_d.zw`; strength already faded
|
||||
// for night on the CPU).
|
||||
// - only spans ending at the scene fog end take it: an interior WMO fog pair stays classic.
|
||||
// - the WDL hull's own (0, 1) pair asks for the horizon: fully fogged, in the far colour.
|
||||
// The sky dome (`fog_sky_horizon`) and the volumetric haze (`fog_modern_colour`) call the same
|
||||
// colour function, so the fogged world, the far hull and the sky's horizon meet in one colour.
|
||||
|
||||
#import benilla::monkey_frame::MonkeyFrame
|
||||
|
||||
@@ -24,6 +39,86 @@ fn fog_linear(eye_z: f32, span: vec2<f32>) -> f32 {
|
||||
return clamp((span.y - eye_z) / denom, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// MONKEY (fog): exp survival at the span midpoint = 0.6, i.e. k·(end − start) = −2·ln 0.6.
|
||||
const FOG_EXP_MID: f32 = 1.0216512;
|
||||
|
||||
// MONKEY (fog): the unit vector packed by `fog_model::oct_encode`.
|
||||
fn fog_oct_decode(e: vec2<f32>) -> vec3<f32> {
|
||||
var v = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
|
||||
let t = max(-v.y, 0.0);
|
||||
v.x += select(t, -t, v.x >= 0.0);
|
||||
v.z += select(t, -t, v.z >= 0.0);
|
||||
return normalize(v);
|
||||
}
|
||||
|
||||
// MONKEY (fog): whether the Modern model is on for this frame.
|
||||
fn fog_is_modern(sel_row: vec4<f32>) -> bool {
|
||||
return sel_row.x > 0.5;
|
||||
}
|
||||
|
||||
// MONKEY (fog): the Modern fog colour seen along unit `dir` at radial distance `d` (yd), from the
|
||||
// scene fog colour `base`: end-fog shift, then the sun lobe. `d` may be huge (the sky).
|
||||
fn fog_modern_colour(
|
||||
base: vec3<f32>,
|
||||
dir: vec3<f32>,
|
||||
d: f32,
|
||||
sun_row: vec4<f32>,
|
||||
end_row: vec4<f32>,
|
||||
sel_row: vec4<f32>,
|
||||
) -> vec3<f32> {
|
||||
var c = base;
|
||||
if (end_row.w > 0.0) {
|
||||
let t = clamp(d / end_row.w, 0.0, 1.0);
|
||||
c = mix(c, end_row.rgb, t * t * t);
|
||||
}
|
||||
if (sun_row.w > 0.0) {
|
||||
let sun = fog_oct_decode(sel_row.zw);
|
||||
let a = sel_row.y;
|
||||
let s = clamp((dot(dir, sun) - a) / max(1.0 - a, 0.001), 0.0, 1.0);
|
||||
c = mix(c, sun_row.rgb, sun_row.w * s * s * s);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// MONKEY (fog): the Modern survival (1 = no fog) at radial distance `d` for the pair `span`.
|
||||
fn fog_modern_survival(span: vec2<f32>, d: f32, world_y: f32, height_row: vec4<f32>) -> f32 {
|
||||
let len = max(span.y - span.x, 0.001);
|
||||
let k = FOG_EXP_MID / len;
|
||||
let x = max(d - span.x, 0.0);
|
||||
var expo = exp(-k * x);
|
||||
if (height_row.x > 0.0) {
|
||||
// Extra density at and below the plane, fading out `1 / falloff` yd above it.
|
||||
let hf = clamp(1.0 - (world_y - height_row.y) * height_row.z, 0.0, 1.0);
|
||||
expo = mix(expo, exp(-k * (1.0 + height_row.x) * x), hf);
|
||||
}
|
||||
let fade_from = max(span.x, 0.3 * span.y);
|
||||
let end_fade = clamp((span.y - d) / max(span.y - fade_from, 0.001), 0.0, 1.0);
|
||||
let linear = clamp((span.y - d) / len, 0.0, 1.0);
|
||||
return mix(min(expo, end_fade), linear, clamp(height_row.w, 0.0, 1.0));
|
||||
}
|
||||
|
||||
// MONKEY (fog): the sky dome's low band under Modern. `col` is the dome's own colour at `dir`,
|
||||
// `horizon_rgb` the colour it uses at 0° (the scene fog colour); the far fog colour along `dir`
|
||||
// replaces the horizon and eases into the dome's own gradient by 15° of elevation (a mix, never an
|
||||
// offset, so an already warm dusk ring is not pushed past itself).
|
||||
// Classic returns `col` untouched.
|
||||
fn fog_sky_horizon(
|
||||
col: vec3<f32>,
|
||||
horizon_rgb: vec3<f32>,
|
||||
dir: vec3<f32>,
|
||||
sun_row: vec4<f32>,
|
||||
end_row: vec4<f32>,
|
||||
sel_row: vec4<f32>,
|
||||
) -> vec3<f32> {
|
||||
if (!fog_is_modern(sel_row)) {
|
||||
return col;
|
||||
}
|
||||
let far = fog_modern_colour(horizon_rgb, dir, 1.0e9, sun_row, end_row, sel_row);
|
||||
let elev = asin(clamp(dir.y, -1.0, 1.0));
|
||||
let w = 1.0 - smoothstep(0.0, 0.26179939, elev);
|
||||
return mix(col, far, w);
|
||||
}
|
||||
|
||||
// The fog a surface stands in: rgb = the fog colour, w = how much of the surface survives (1 = no
|
||||
// fog). `fog_rgb` is the colour the caller's policy picked; `scene_colour` is true when that is the
|
||||
// scene's own fog colour (false for the forced black/white/grey policies of additive, Mod and Mod2x
|
||||
@@ -40,9 +135,23 @@ fn fog_sample(
|
||||
) -> vec4<f32> {
|
||||
let classic = vec4<f32>(fog_rgb, fog_linear(eye_z, span));
|
||||
if (mf.fog_d.x > 0.5) {
|
||||
// FOG lane: the modern model (height / sun / end-colour / curve fog) goes here, reading
|
||||
// mf.fog_a..fog_d. Until it lands, the modern arm is the classic law.
|
||||
return classic;
|
||||
// MONKEY (fog): the Modern model (header).
|
||||
let delta = world_pos - camera_pos;
|
||||
let d = length(delta);
|
||||
let dir = delta / max(d, 0.0001);
|
||||
if (span.x == 0.0 && span.y == 1.0) {
|
||||
// The WDL hull's own pair: the horizon, fully fogged in the far colour.
|
||||
return vec4<f32>(fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d), 0.0);
|
||||
}
|
||||
if (abs(span.y - mf.fog_d.x) > 0.01) {
|
||||
// Not the scene fog (an interior WMO pair): classic.
|
||||
return classic;
|
||||
}
|
||||
var rgb = fog_rgb;
|
||||
if (scene_colour) {
|
||||
rgb = fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d);
|
||||
}
|
||||
return vec4<f32>(rgb, fog_modern_survival(span, d, world_pos.y, mf.fog_a));
|
||||
}
|
||||
return classic;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct MonkeyFrame {
|
||||
fog_a: vec4<f32>, // height_fog_density, height_fog_height (world Y, yd), height_fog_falloff, curve_blend
|
||||
fog_b: vec4<f32>, // sun_fog_rgb (gamma 0..1), sun_fog_strength
|
||||
fog_c: vec4<f32>, // end_fog_rgb (gamma 0..1), end_fog_distance (yd)
|
||||
fog_d: vec4<f32>, // fog_model (0 classic, 1 modern), sun_fog_angle (cos), 0, 0
|
||||
fog_d: vec4<f32>, // 0 classic or scene fog end for Modern, sun_fog_angle, sun direction octahedral xy
|
||||
wind_a: vec4<f32>, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), speed (yd/s), gust 0..1
|
||||
wind_b: vec4<f32>, // time_s, sway_strength, grass_strength, tree_strength
|
||||
wet_a: vec4<f32>, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1
|
||||
|
||||
@@ -132,6 +132,8 @@ pub use ground_effects::{
|
||||
};
|
||||
mod light;
|
||||
pub use light::{Atmosphere, LightCatalog, Submersion, ZERO_KEY_COLOR, ZERO_KEY_SCALAR};
|
||||
// MONKEY (fog): `LightFogBand.dbc`.
|
||||
pub use light::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
|
||||
mod loading_screen;
|
||||
pub use loading_screen::{load_loading_screens, LoadingScreenCatalog};
|
||||
mod liquid;
|
||||
|
||||
@@ -11,9 +11,12 @@ use crate::dbc::{f32_at, parse, str_at, u32_at};
|
||||
use crate::Chain;
|
||||
|
||||
mod atmosphere;
|
||||
// MONKEY (fog): `LightFogBand.dbc`, the modern fog fields.
|
||||
mod fog_band;
|
||||
mod tables;
|
||||
|
||||
pub use atmosphere::Atmosphere;
|
||||
pub use fog_band::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
|
||||
use atmosphere::{
|
||||
FB_CLOUD_DENSITY, FB_FOG_END, FB_FOG_START_MULT, IB_AMBIENT, IB_CLOUD_GBASE, IB_CLOUD_SLOPE,
|
||||
IB_CLOUD_SUN, IB_DIFFUSE, IB_FOG_COLOR, IB_OCEAN_DEEP, IB_OCEAN_SHALLOW, IB_RIVER_DEEP,
|
||||
@@ -509,6 +512,66 @@ impl LightCatalog {
|
||||
acc
|
||||
}
|
||||
|
||||
/// MONKEY (fog): the `LightParams` ids [`Self::sample_blended`] folds, in its order, each with its
|
||||
/// lerp weight (the base first at 1.0). A side table keyed by `LightParams` (`LightFogBand.dbc`)
|
||||
/// blends through this with the same law. Magma and slime give their fixed row alone.
|
||||
pub fn blend_chain(
|
||||
&self,
|
||||
map: u32,
|
||||
pos: [f32; 3],
|
||||
stormy: bool,
|
||||
submersion: Submersion,
|
||||
ghost: bool,
|
||||
) -> Vec<(u32, f32)> {
|
||||
if let Some(p) = submersion.fixed_param() {
|
||||
if self.has_param(p) {
|
||||
return vec![(p, 1.0)];
|
||||
}
|
||||
}
|
||||
let slot = weather_slot(ghost, stormy, submersion.is_water());
|
||||
let param_of = |l: &Light| match l.params[slot] {
|
||||
0 => l.params[SLOT_CLEAR],
|
||||
p => p,
|
||||
};
|
||||
let mut chain = Vec::new();
|
||||
let map_has_no_light = !self.lights.iter().any(|l| l.map == map);
|
||||
if let Some(base) = self
|
||||
.lights
|
||||
.iter()
|
||||
.find(|l| l.map == map && l.global)
|
||||
.or_else(|| {
|
||||
map_has_no_light
|
||||
.then(|| self.lights.iter().find(|l| l.id == FALLBACK_LIGHT_ID))
|
||||
.flatten()
|
||||
})
|
||||
{
|
||||
let p = param_of(base);
|
||||
if p >= 1 {
|
||||
chain.push((p, 1.0));
|
||||
}
|
||||
}
|
||||
let mut locals: Vec<(f32, &Light)> = self
|
||||
.lights
|
||||
.iter()
|
||||
.filter(|l| l.map == map && !l.global)
|
||||
.filter_map(|l| {
|
||||
let d = (0..3)
|
||||
.map(|i| (l.pos[i] - pos[i]).powi(2))
|
||||
.sum::<f32>()
|
||||
.sqrt();
|
||||
(d <= l.falloff_end).then_some((d, l))
|
||||
})
|
||||
.collect();
|
||||
locals.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
for (dist, l) in locals {
|
||||
let p = param_of(l);
|
||||
if p >= 1 {
|
||||
chain.push((p, blend_alpha(dist, l.falloff_start, l.falloff_end)));
|
||||
}
|
||||
}
|
||||
chain
|
||||
}
|
||||
|
||||
/// Debug: the spheres near `pos`, nearest first (the blend applies them farthest first), then
|
||||
/// the blended result against the `pick_light` sample and the water swatch endpoints.
|
||||
pub fn debug_blend(&self, map: u32, pos: [f32; 3], time: u32) {
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//! MONKEY (fog): `LightFogBand.dbc`, the modern fog fields 1.12 has no column for.
|
||||
//!
|
||||
//! The file is ours (DESIGN-Benilla-Graphics §2a). It mirrors `LightFloatBand.dbc` exactly: the
|
||||
//! same 34-field, 136-byte band record (`ID, num, time[16], value[16]`, times in half-minutes on
|
||||
//! the 2880 day), interpolated the same way across midnight. `LightParams` id `P` keys
|
||||
//! [`FOG_BANDS_PER_PARAM`] rows, band `b` at id `(P − 1)·13 + b + 1`, the `LightFloatBand`
|
||||
//! indexing with its own stride, so the stock `paramID·6 + k` tables stay untouched.
|
||||
//!
|
||||
//! | band | field | unit |
|
||||
//! |---|---|---|
|
||||
//! | 0 | height-fog density | extra extinction below the plane, × the distance fog's (0 = off) |
|
||||
//! | 1 | height-fog height | WoW world Z, yards |
|
||||
//! | 2 | height-fog falloff | 1/yd: how fast the extra density fades above the plane |
|
||||
//! | 3 | curve blend | 0 = exponential (modern), 1 = the 1.12 linear ramp |
|
||||
//! | 4-6 | sun-fog colour r, g, b | gamma 0..1 |
|
||||
//! | 7 | sun-fog strength | 0..1 |
|
||||
//! | 8 | sun-fog angle | cosine threshold of the lobe around the sun |
|
||||
//! | 9-11 | end-fog colour r, g, b | gamma 0..1 |
|
||||
//! | 12 | end-fog distance | yards (0 = the scene fog end) |
|
||||
//!
|
||||
//! A missing file, or a `LightParams` id without rows, is not an error: the caller falls back to
|
||||
//! the colours it derives from the 1.12 bands.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use benilla_dbc::FieldType;
|
||||
|
||||
use super::tables::{band_schema, load_float_bands, sample_float, Band};
|
||||
use crate::Chain;
|
||||
|
||||
const FOG_BAND: &str = "DBFilesClient\\LightFogBand.dbc";
|
||||
|
||||
/// Rows per `LightParams` id.
|
||||
pub const FOG_BANDS_PER_PARAM: u32 = 13;
|
||||
|
||||
/// One `LightParams` id's fog fields at one time of day (see the module table).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FogBand {
|
||||
pub height_density: f32,
|
||||
/// WoW world Z, yards.
|
||||
pub height: f32,
|
||||
pub height_falloff: f32,
|
||||
pub curve_blend: f32,
|
||||
pub sun_rgb: [f32; 3],
|
||||
pub sun_strength: f32,
|
||||
pub sun_angle: f32,
|
||||
pub end_rgb: [f32; 3],
|
||||
/// Yards; 0 = the scene fog end.
|
||||
pub end_distance: f32,
|
||||
}
|
||||
|
||||
impl FogBand {
|
||||
/// Componentwise `self + (o − self)·t`, the area and storm blend.
|
||||
pub fn lerp(&self, o: &FogBand, t: f32) -> FogBand {
|
||||
let l = |a: f32, b: f32| a + (b - a) * t;
|
||||
let l3 = |a: [f32; 3], b: [f32; 3]| [l(a[0], b[0]), l(a[1], b[1]), l(a[2], b[2])];
|
||||
FogBand {
|
||||
height_density: l(self.height_density, o.height_density),
|
||||
height: l(self.height, o.height),
|
||||
height_falloff: l(self.height_falloff, o.height_falloff),
|
||||
curve_blend: l(self.curve_blend, o.curve_blend),
|
||||
sun_rgb: l3(self.sun_rgb, o.sun_rgb),
|
||||
sun_strength: l(self.sun_strength, o.sun_strength),
|
||||
sun_angle: l(self.sun_angle, o.sun_angle),
|
||||
end_rgb: l3(self.end_rgb, o.end_rgb),
|
||||
end_distance: l(self.end_distance, o.end_distance),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The parsed `LightFogBand.dbc`; empty when the file is absent.
|
||||
#[derive(Default)]
|
||||
pub struct FogBandCatalog {
|
||||
bands: HashMap<u32, Band<f32>>,
|
||||
}
|
||||
|
||||
impl FogBandCatalog {
|
||||
/// Read `LightFogBand.dbc` off the chain. Absent or unreadable → an empty catalog (every
|
||||
/// lookup `None`), which is the 1.12 data set.
|
||||
pub fn load(chain: &mut Chain) -> Self {
|
||||
let bands = load_float_bands(
|
||||
chain,
|
||||
FOG_BAND,
|
||||
band_schema("LightFogBand", FieldType::Float32),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
FogBandCatalog { bands }
|
||||
}
|
||||
|
||||
/// Whether the file carried any row.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bands.is_empty()
|
||||
}
|
||||
|
||||
/// `LightParams` id `p` at `time` (half-minutes), or `None` when `p` has no rows. A single
|
||||
/// missing band of a present id reads 0.
|
||||
pub fn sample(&self, p: u32, time: u32) -> Option<FogBand> {
|
||||
if p < 1 {
|
||||
return None;
|
||||
}
|
||||
let base = (p - 1) * FOG_BANDS_PER_PARAM + 1;
|
||||
let rows: Vec<Option<f32>> = (0..FOG_BANDS_PER_PARAM)
|
||||
.map(|b| self.bands.get(&(base + b)).and_then(|band| sample_float(band, time)))
|
||||
.collect();
|
||||
if rows.iter().all(Option::is_none) {
|
||||
return None;
|
||||
}
|
||||
let v = |b: usize| rows[b].unwrap_or(0.0);
|
||||
Some(FogBand {
|
||||
height_density: v(0),
|
||||
height: v(1),
|
||||
height_falloff: v(2),
|
||||
curve_blend: v(3),
|
||||
sun_rgb: [v(4), v(5), v(6)],
|
||||
sun_strength: v(7),
|
||||
sun_angle: v(8),
|
||||
end_rgb: [v(9), v(10), v(11)],
|
||||
end_distance: v(12),
|
||||
})
|
||||
}
|
||||
|
||||
/// The area/storm blend of [`super::LightCatalog::blend_chain`]: each id's sample, or
|
||||
/// `fallback` for an id without rows, lerped in chain order. `None` when no id in the chain has
|
||||
/// rows, so the caller keeps its own derivation untouched.
|
||||
pub fn sample_chain(
|
||||
&self,
|
||||
chain: &[(u32, f32)],
|
||||
time: u32,
|
||||
fallback: &FogBand,
|
||||
) -> Option<FogBand> {
|
||||
let mut any = false;
|
||||
let mut acc = *fallback;
|
||||
for &(p, w) in chain {
|
||||
let v = match self.sample(p, time) {
|
||||
Some(v) => {
|
||||
any = true;
|
||||
v
|
||||
}
|
||||
None => *fallback,
|
||||
};
|
||||
acc = acc.lerp(&v, w);
|
||||
}
|
||||
any.then_some(acc)
|
||||
}
|
||||
|
||||
/// Test/tool constructor: a catalog from `(row id, [(time, value)])` bands.
|
||||
pub fn from_rows(rows: &[(u32, &[(u32, f32)])]) -> Self {
|
||||
let mut bands = HashMap::new();
|
||||
for (id, keys) in rows {
|
||||
bands.insert(*id, Band::from_keys(keys));
|
||||
}
|
||||
FogBandCatalog { bands }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn neutral() -> FogBand {
|
||||
FogBand {
|
||||
height_density: 0.0,
|
||||
height: 0.0,
|
||||
height_falloff: 0.0,
|
||||
curve_blend: 0.0,
|
||||
sun_rgb: [0.0; 3],
|
||||
sun_strength: 0.0,
|
||||
sun_angle: 1.0,
|
||||
end_rgb: [0.0; 3],
|
||||
end_distance: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_catalog_is_absent() {
|
||||
let c = FogBandCatalog::default();
|
||||
assert!(c.is_empty());
|
||||
assert_eq!(c.sample(12, 1440), None);
|
||||
assert_eq!(c.sample_chain(&[(12, 1.0)], 1440, &neutral()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_land_at_the_param_stride_and_interpolate() {
|
||||
// Param 12, band 7 (sun strength): 0 at 06:00, 1 at 18:00.
|
||||
let id = 11 * FOG_BANDS_PER_PARAM + 7 + 1;
|
||||
let c = FogBandCatalog::from_rows(&[(id, &[(720, 0.0), (2160, 1.0)])]);
|
||||
let noon = c.sample(12, 1440).unwrap();
|
||||
assert!((noon.sun_strength - 0.5).abs() < 1e-6);
|
||||
assert_eq!(noon.height_density, 0.0);
|
||||
assert_eq!(c.sample(13, 1440), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_blends_rows_against_the_fallback() {
|
||||
let id = 11 * FOG_BANDS_PER_PARAM + 7 + 1;
|
||||
let c = FogBandCatalog::from_rows(&[(id, &[(0, 1.0)])]);
|
||||
// Base 13 (no rows → fallback strength 0), then param 12 at weight 0.25.
|
||||
let out = c.sample_chain(&[(13, 1.0), (12, 0.25)], 100, &neutral()).unwrap();
|
||||
assert!((out.sun_strength - 0.25).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,16 @@ pub(super) struct Band<T> {
|
||||
values: Vec<T>,
|
||||
}
|
||||
|
||||
// MONKEY (fog): a band from `(time, value)` keys, for `fog_band`'s tests and tools.
|
||||
impl<T: Copy> Band<T> {
|
||||
pub(super) fn from_keys(keys: &[(u32, T)]) -> Self {
|
||||
Band {
|
||||
times: keys.iter().map(|k| k.0).collect(),
|
||||
values: keys.iter().map(|k| k.1).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The shared band record: `ID, num, time[16], value[16]`, 34 fields, 136 bytes.
|
||||
pub(super) fn band_schema(name: &str, value: FieldType) -> Schema {
|
||||
let mut s = Schema::new(name);
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
//! MONKEY (fog): the Modern fog model's CPU half — fills the FOG rows of [`MonkeyFrame`] every
|
||||
//! frame. The law itself lives in `benilla-assets/src/shaders/fog_hook.wgsl`.
|
||||
//!
|
||||
//! Setting: cvar `fogModel` (0 Classic = the 1.12 linear fog, byte-identical; 1 Modern), bridged by
|
||||
//! `benilla-app/src/monkey_gfx.rs` into [`FogModelSetting`]; `WOW_FOGMODEL=0|1` overrides it for a
|
||||
//! session (captures).
|
||||
//!
|
||||
//! Modern, per frame:
|
||||
//! - **Distances** stay the zone's: the shader's exponential curve is fitted to the 1.12 start/end
|
||||
//! pair it already receives. Only past the reference's farclip ceiling does the end grow with the
|
||||
//! view distance ([`modern_fog_end`], called from `resolve.rs`), so a larger `farclip` shows more.
|
||||
//! - **Colours** without `LightFogBand.dbc` rows are derived from the 1.12 bands
|
||||
//! ([`derived_band`]): the sun-fog lobe leans the fog toward the sun colour (IntBand 9) around the
|
||||
//! sun and fades out at night; the far fog shifts toward the sky's lowest ring (1.8°) so the
|
||||
//! fogged world, the WDL hull and the sky dome's horizon meet in one colour. Height fog is off.
|
||||
//! - With `LightFogBand.dbc` rows, those override the derivation per `LightParams`, blended over
|
||||
//! the same area-light chain as the stock bands.
|
||||
//!
|
||||
//! Packed rows (see `monkey_frame.rs`): `fog_a` height density / height (Bevy Y) / falloff / curve
|
||||
//! blend; `fog_b` sun rgb / strength × day; `fog_c` end rgb / end distance; `fog_d` the scene fog end
|
||||
//! (> 0 = Modern; the shader applies the model only to spans ending there, so an interior WMO fog
|
||||
//! stays classic) / sun-fog cosine / the sun direction, octahedral-encoded.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_assets::{LockRecover, WorldAssets};
|
||||
use benilla_formats::{FogBand, FogBandCatalog};
|
||||
|
||||
use super::{FogModel, LightSampler, MonkeyFrame, WowLighting};
|
||||
use crate::terrain_stream::SPAWN_XY;
|
||||
use crate::view::WorldCamera;
|
||||
use crate::world_map::CurrentMap;
|
||||
use benilla_assets::coords::bevy_to_wow;
|
||||
|
||||
/// The live fog-model choice (cvar `fogModel`). Default Classic; `WOW_FOGMODEL` overrides.
|
||||
#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct FogModelSetting(pub FogModel);
|
||||
|
||||
impl Default for FogModelSetting {
|
||||
fn default() -> Self {
|
||||
FogModelSetting(env_override().unwrap_or(FogModel::Classic))
|
||||
}
|
||||
}
|
||||
|
||||
/// `WOW_FOGMODEL=0|1`, read once.
|
||||
fn env_override() -> Option<FogModel> {
|
||||
static ENV: std::sync::OnceLock<Option<FogModel>> = std::sync::OnceLock::new();
|
||||
*ENV.get_or_init(|| {
|
||||
std::env::var("WOW_FOGMODEL").ok().and_then(|v| match v.trim() {
|
||||
"0" => Some(FogModel::Classic),
|
||||
"1" => Some(FogModel::Modern),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl FogModelSetting {
|
||||
/// Apply a cvar write; the env override, when set, wins for the session.
|
||||
pub fn set_from_cvar(&mut self, v: f32) {
|
||||
let want = env_override().unwrap_or(if v >= 0.5 {
|
||||
FogModel::Modern
|
||||
} else {
|
||||
FogModel::Classic
|
||||
});
|
||||
if self.0 != want {
|
||||
self.0 = want;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modern(&self) -> bool {
|
||||
self.0 == FogModel::Modern
|
||||
}
|
||||
}
|
||||
|
||||
/// `LightFogBand.dbc`, loaded once; empty without the file.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct FogBandTable(pub FogBandCatalog);
|
||||
|
||||
/// The reference client's farclip ceiling (`FARCLIP_RANGE` end). Up to it Modern keeps the 1.12
|
||||
/// fog pair exactly; beyond it the fog end scales with the view distance.
|
||||
pub const FOG_REF_FARCLIP: f32 = 777.0;
|
||||
|
||||
/// The Modern scene fog pair: the zone's end, stretched by `farclip / 777` once the view distance
|
||||
/// passes the reference ceiling, and never past `farclip` (the wall). Classic is
|
||||
/// `(frac·min(end, farclip), min(end, farclip))`; at `farclip ≤ 777` the two are identical.
|
||||
pub fn modern_fog_end(fog_end_raw: f32, fog_start_frac: f32, farclip: f32) -> (f32, f32) {
|
||||
let stretch = (farclip / FOG_REF_FARCLIP).max(1.0);
|
||||
let end = (fog_end_raw * stretch).min(farclip);
|
||||
(fog_start_frac * end, end)
|
||||
}
|
||||
|
||||
/// Sun-fog lobe defaults (no `LightFogBand` row): strength and the cosine the lobe starts at.
|
||||
pub const DERIVED_SUN_FOG_STRENGTH: f32 = 0.25;
|
||||
pub const DERIVED_SUN_FOG_ANGLE: f32 = 0.4;
|
||||
/// How far the sun-fog colour leans from the fog colour toward the sun colour (IntBand 9).
|
||||
const DERIVED_SUN_FOG_LEAN: f32 = 0.4;
|
||||
/// How far the end-fog colour leans from the fog colour toward the 1.8° sky ring.
|
||||
const DERIVED_END_FOG_LEAN: f32 = 0.25;
|
||||
|
||||
fn mix3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
|
||||
[
|
||||
a[0] + (b[0] - a[0]) * t,
|
||||
a[1] + (b[1] - a[1]) * t,
|
||||
a[2] + (b[2] - a[2]) * t,
|
||||
]
|
||||
}
|
||||
|
||||
/// The Modern fields from 1.12 data alone: no height fog, the exponential curve, a sun-fog lobe in
|
||||
/// the sun's colour and an end colour leaning to the sky's lowest ring, ending at the scene fog end.
|
||||
pub fn derived_band(l: &WowLighting) -> FogBand {
|
||||
FogBand {
|
||||
height_density: 0.0,
|
||||
height: 0.0,
|
||||
height_falloff: 0.0,
|
||||
curve_blend: 0.0,
|
||||
sun_rgb: mix3(l.fog_color, l.spec, DERIVED_SUN_FOG_LEAN),
|
||||
sun_strength: DERIVED_SUN_FOG_STRENGTH,
|
||||
sun_angle: DERIVED_SUN_FOG_ANGLE,
|
||||
end_rgb: mix3(l.fog_color, l.sky[4], DERIVED_END_FOG_LEAN),
|
||||
end_distance: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The sun-fog day factor from the visible sun's height (`sin(elevation)`): full from ~3° up,
|
||||
/// fading through the horizon to 0 by ~6° below it (the modern client's SunAngleBlend ramp).
|
||||
pub fn sun_fog_day(sun_y: f32) -> f32 {
|
||||
let t = ((sun_y + 0.1) / 0.15).clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
/// Octahedral encoding of a unit vector into `[-1, 1]²` (the shader's `oct_decode`).
|
||||
pub fn oct_encode(v: Vec3) -> [f32; 2] {
|
||||
let v = v.normalize_or(Vec3::Y);
|
||||
let n = v / (v.x.abs() + v.y.abs() + v.z.abs());
|
||||
if n.y >= 0.0 {
|
||||
[n.x, n.z]
|
||||
} else {
|
||||
let sx = if n.x >= 0.0 { 1.0 } else { -1.0 };
|
||||
let sz = if n.z >= 0.0 { 1.0 } else { -1.0 };
|
||||
[(1.0 - n.z.abs()) * sx, (1.0 - n.x.abs()) * sz]
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the FOG rows of `frame` for `band` under the resolved light. Classic zeroes them.
|
||||
pub fn fill_frame(frame: &mut MonkeyFrame, model: FogModel, l: &WowLighting, band: &FogBand) {
|
||||
if model == FogModel::Classic {
|
||||
frame.fog_model = FogModel::Classic;
|
||||
frame.fog_scene_end = 0.0;
|
||||
frame.height_fog_density = 0.0;
|
||||
frame.height_fog_height = 0.0;
|
||||
frame.height_fog_falloff = 0.0;
|
||||
frame.fog_curve_blend = 0.0;
|
||||
frame.sun_fog_rgb = [0.0; 3];
|
||||
frame.sun_fog_strength = 0.0;
|
||||
frame.sun_fog_angle = 0.0;
|
||||
frame.sun_fog_dir = [0.0; 2];
|
||||
frame.end_fog_rgb = [0.0; 3];
|
||||
frame.end_fog_distance = 0.0;
|
||||
return;
|
||||
}
|
||||
let sun = l.celestial_dir;
|
||||
frame.fog_model = FogModel::Modern;
|
||||
frame.fog_scene_end = l.fog_end;
|
||||
frame.height_fog_density = band.height_density.max(0.0);
|
||||
// WoW Z is Bevy Y.
|
||||
frame.height_fog_height = band.height;
|
||||
frame.height_fog_falloff = band.height_falloff.max(0.0);
|
||||
frame.fog_curve_blend = band.curve_blend.clamp(0.0, 1.0);
|
||||
frame.sun_fog_rgb = band.sun_rgb;
|
||||
frame.sun_fog_strength = band.sun_strength.clamp(0.0, 1.0) * sun_fog_day(sun.y);
|
||||
frame.sun_fog_angle = band.sun_angle.clamp(-1.0, 0.999);
|
||||
frame.sun_fog_dir = oct_encode(sun);
|
||||
frame.end_fog_rgb = band.end_rgb;
|
||||
frame.end_fog_distance = if band.end_distance > 0.0 {
|
||||
band.end_distance
|
||||
} else {
|
||||
l.fog_end
|
||||
};
|
||||
}
|
||||
|
||||
/// Startup: read `LightFogBand.dbc` (absent → empty).
|
||||
pub(super) fn load_fog_bands(mut commands: Commands, world_assets: Option<ResMut<WorldAssets>>) {
|
||||
let Some(world_assets) = world_assets else {
|
||||
return;
|
||||
};
|
||||
let mut chain = world_assets.chain.lock_recover();
|
||||
let cat = FogBandCatalog::load(&mut chain);
|
||||
if !cat.is_empty() {
|
||||
info!("LightFogBand.dbc loaded: Modern fog uses its authored fields");
|
||||
}
|
||||
commands.insert_resource(FogBandTable(cat));
|
||||
}
|
||||
|
||||
/// Per frame: the Modern fog fields into [`MonkeyFrame`] (after the light resolve).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn update_fog_model(
|
||||
setting: Res<FogModelSetting>,
|
||||
lighting: Res<WowLighting>,
|
||||
table: Option<Res<FogBandTable>>,
|
||||
sampler: Option<Res<LightSampler>>,
|
||||
clock: Res<super::GameClock>,
|
||||
cam: Query<&GlobalTransform, With<WorldCamera>>,
|
||||
current_map: Option<Res<CurrentMap>>,
|
||||
eye_liquid: crate::liquid::EyeLiquid,
|
||||
viewer: Res<crate::view::Viewer>,
|
||||
weather: Option<Res<crate::weather::WeatherState>>,
|
||||
mut frame: ResMut<MonkeyFrame>,
|
||||
) {
|
||||
let derived = derived_band(&lighting);
|
||||
let band = match (&table, &sampler, setting.modern()) {
|
||||
(Some(t), Some(s), true) if !t.0.is_empty() => {
|
||||
let pos = match cam.single() {
|
||||
Ok(t) => bevy_to_wow(t.translation()),
|
||||
Err(_) => [SPAWN_XY.0, SPAWN_XY.1, 83.5],
|
||||
};
|
||||
let map = current_map.as_ref().map_or(0, |m| m.0);
|
||||
let sub = eye_liquid.submersion();
|
||||
let time = clock.minute * 2;
|
||||
let storm = weather
|
||||
.as_ref()
|
||||
.map_or(0.0, |w| crate::weather::storm_blend(w.sky_density));
|
||||
let chain = |stormy| s.0.blend_chain(map, pos, stormy, sub, viewer.ghost);
|
||||
let clear = t.0.sample_chain(&chain(false), time, &derived);
|
||||
let stormy = (storm > 0.0)
|
||||
.then(|| t.0.sample_chain(&chain(true), time, &derived))
|
||||
.flatten();
|
||||
match (clear, stormy) {
|
||||
(None, None) => derived,
|
||||
(c, s) => c.unwrap_or(derived).lerp(&s.unwrap_or(derived), storm),
|
||||
}
|
||||
}
|
||||
_ => derived,
|
||||
};
|
||||
let mut next = *frame;
|
||||
fill_frame(&mut next, setting.0, &lighting, &band);
|
||||
if *frame != next {
|
||||
*frame = next;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn modern_end_matches_classic_up_to_the_reference_farclip() {
|
||||
for far in [177.0, 350.0, 500.0, 777.0] {
|
||||
for end in [120.0, 444.0, 900.0] {
|
||||
let classic_end: f32 = f32::min(end, far);
|
||||
let (s, e) = modern_fog_end(end, 0.25, far);
|
||||
assert_eq!(e, classic_end);
|
||||
assert_eq!(s, 0.25 * classic_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modern_end_grows_past_the_reference_farclip() {
|
||||
let (_, e) = modern_fog_end(444.0, 0.25, 1554.0);
|
||||
assert!((e - 888.0).abs() < 1e-3);
|
||||
// Never past the wall.
|
||||
let (_, e) = modern_fog_end(900.0, 0.25, 1000.0);
|
||||
assert_eq!(e, 1000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_leaves_the_fog_rows_zero() {
|
||||
let mut f = MonkeyFrame::default();
|
||||
let l = WowLighting::default();
|
||||
fill_frame(&mut f, FogModel::Classic, &l, &derived_band(&l));
|
||||
assert_eq!(f, MonkeyFrame::default());
|
||||
assert!(f.pack(0.0, 0.0)[..4].iter().flatten().all(|v| *v == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modern_marks_the_scene_end_and_fades_the_sun_at_night() {
|
||||
let mut l = WowLighting::default();
|
||||
l.fog_end = 444.0;
|
||||
l.celestial_dir = Vec3::new(0.0, 0.8, 0.6);
|
||||
let mut f = MonkeyFrame::default();
|
||||
fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l));
|
||||
assert_eq!(f.fog_scene_end, 444.0);
|
||||
assert_eq!(f.end_fog_distance, 444.0);
|
||||
assert!((f.sun_fog_strength - DERIVED_SUN_FOG_STRENGTH).abs() < 1e-6);
|
||||
l.celestial_dir = Vec3::new(0.0, -0.5, 0.86);
|
||||
fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l));
|
||||
assert_eq!(f.sun_fog_strength, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oct_encoding_round_trips() {
|
||||
fn decode(e: [f32; 2]) -> Vec3 {
|
||||
let mut v = Vec3::new(e[0], 1.0 - e[0].abs() - e[1].abs(), e[1]);
|
||||
let t = (-v.y).max(0.0);
|
||||
v.x += if v.x >= 0.0 { -t } else { t };
|
||||
v.z += if v.z >= 0.0 { -t } else { t };
|
||||
v.normalize()
|
||||
}
|
||||
for d in [
|
||||
Vec3::Y,
|
||||
-Vec3::Y,
|
||||
Vec3::new(0.3, 0.2, -0.9),
|
||||
Vec3::new(-0.7, -0.1, 0.7),
|
||||
Vec3::new(0.1, -0.9, -0.4),
|
||||
] {
|
||||
let d = d.normalize();
|
||||
assert!(decode(oct_encode(d)).dot(d) > 0.9999, "{d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ mod flicker; // MONKEY (flame flicker): the per-light fire wobble folded in at p
|
||||
mod global_light; // the one shared global-light storage buffer (replaces the per-material push)
|
||||
mod monkey_frame; // MONKEY (p0 MonkeyFrame): the programme's per-frame block after the point table
|
||||
pub use monkey_frame::{FogModel, MonkeyFrame, MAX_BENDERS, MONKEY_FRAME_ROWS};
|
||||
pub mod fog_model; // MONKEY (fog): the Modern fog model's CPU half (MonkeyFrame fog rows)
|
||||
pub use fog_model::FogModelSetting;
|
||||
mod lava_light; // MONKEY (lava light): magma surface fixtures and their independent gain
|
||||
pub use lava_light::{LavaLight, LavaLightGain};
|
||||
mod prop_probes; // the per-instance interior-prop SH probe table (slot ↔ MeshTag payload)
|
||||
@@ -322,6 +324,16 @@ impl Plugin for LightingPlugin {
|
||||
// This frame's submersion verdict, which the sky-pass suppression also reads.
|
||||
.after(crate::liquid::SubmersionVerdict),
|
||||
);
|
||||
// MONKEY (fog): the fog-model setting, LightFogBand.dbc and the per-frame fog rows.
|
||||
app.init_resource::<fog_model::FogModelSetting>()
|
||||
.init_resource::<fog_model::FogBandTable>()
|
||||
.add_systems(Startup, fog_model::load_fog_bands.after(AssetSet::Open))
|
||||
.add_systems(
|
||||
Update,
|
||||
fog_model::update_fog_model
|
||||
.after(update_time_lighting)
|
||||
.in_set(LightingResolveSet),
|
||||
);
|
||||
// The shared light buffer, packed after the resolve and uploaded in the render world.
|
||||
global_light::register(app);
|
||||
// MONKEY (daylight fixtures): the per-frame re-aim, ordered before the packer's own set.
|
||||
@@ -399,6 +411,11 @@ mod ordering_tests {
|
||||
"the celestial follows: PostUpdate, BillboardPlace",
|
||||
),
|
||||
("weather/precip/mod.rs", "push_precip: PostUpdate"),
|
||||
// MONKEY (fog)
|
||||
(
|
||||
"lighting/fog_model.rs",
|
||||
"update_fog_model: in the resolve set, .after(update_time_lighting)",
|
||||
),
|
||||
// MONKEY (daylight fixtures / portal bleed): both systems are PostUpdate,
|
||||
// chained before `global_light::classify_light_lanes`.
|
||||
(
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! | 0 `fog_a` | height_fog_density, height_fog_height, height_fog_falloff | curve_blend |
|
||||
//! | 1 `fog_b` | sun_fog_rgb | sun_fog_strength |
|
||||
//! | 2 `fog_c` | end_fog_rgb | end_fog_distance |
|
||||
//! | 3 `fog_d` | fog_model (0 classic, 1 modern), sun_fog_angle, 0 | 0 |
|
||||
//! | 3 `fog_d` | fog_model (0 classic; Modern = the scene fog end, yd, ≥ 1), sun_fog_angle, sun dir oct.x | sun dir oct.y |
|
||||
//! | 4 `wind_a` | dir_x, dir_y, speed | gust |
|
||||
//! | 5 `wind_b` | time_s, sway_strength, grass_strength | tree_strength |
|
||||
//! | 6 `wet_a` | rain_rate, wetness, ripple_time_s | snow |
|
||||
@@ -64,6 +64,11 @@ pub struct MonkeyFrame {
|
||||
pub sun_fog_strength: f32,
|
||||
/// Cosine threshold of the sun-fog lobe.
|
||||
pub sun_fog_angle: f32,
|
||||
/// MONKEY (fog): the sun direction (Bevy space), octahedral-encoded (`fog_model::oct_encode`).
|
||||
pub sun_fog_dir: [f32; 2],
|
||||
/// MONKEY (fog): the scene fog end (yd) the Modern law applies to; packed as `fog_d.x`, so an
|
||||
/// interior WMO span (a different end) stays classic.
|
||||
pub fog_scene_end: f32,
|
||||
/// Gamma 0..1.
|
||||
pub end_fog_rgb: [f32; 3],
|
||||
pub end_fog_distance: f32,
|
||||
@@ -94,9 +99,10 @@ impl MonkeyFrame {
|
||||
/// The 16 packed rows. `time_of_day` (0..1) and `night` (0..1) are the packer's own inputs.
|
||||
pub fn pack(&self, time_of_day: f32, night: f32) -> [[f32; 4]; MONKEY_FRAME_ROWS] {
|
||||
let mut rows = [[0.0f32; 4]; MONKEY_FRAME_ROWS];
|
||||
// MONKEY (fog): Modern packs the scene fog end (never below 1) as its flag.
|
||||
let model = match self.fog_model {
|
||||
FogModel::Classic => 0.0,
|
||||
FogModel::Modern => 1.0,
|
||||
FogModel::Modern => self.fog_scene_end.max(1.0),
|
||||
};
|
||||
rows[0] = [
|
||||
self.height_fog_density,
|
||||
@@ -108,7 +114,7 @@ impl MonkeyFrame {
|
||||
rows[1] = [s[0], s[1], s[2], self.sun_fog_strength];
|
||||
let e = self.end_fog_rgb;
|
||||
rows[2] = [e[0], e[1], e[2], self.end_fog_distance];
|
||||
rows[3] = [model, self.sun_fog_angle, 0.0, 0.0];
|
||||
rows[3] = [model, self.sun_fog_angle, self.sun_fog_dir[0], self.sun_fog_dir[1]];
|
||||
rows[4] = [self.wind_dir[0], self.wind_dir[1], self.wind_speed, self.wind_gust];
|
||||
rows[5] = [self.wind_time_s, self.sway_strength, self.grass_strength, self.tree_strength];
|
||||
rows[6] = [self.rain_rate, self.wetness, self.ripple_time_s, self.snow];
|
||||
@@ -144,6 +150,8 @@ mod tests {
|
||||
end_fog_rgb: [9.0, 10.0, 11.0],
|
||||
end_fog_distance: 12.0,
|
||||
sun_fog_angle: 13.0,
|
||||
sun_fog_dir: [0.5, -0.5],
|
||||
fog_scene_end: 444.0,
|
||||
wind_dir: [14.0, 15.0],
|
||||
wind_speed: 16.0,
|
||||
wind_gust: 17.0,
|
||||
@@ -162,7 +170,7 @@ mod tests {
|
||||
assert_eq!(r[0], [1.0, 2.0, 3.0, 4.0]);
|
||||
assert_eq!(r[1], [5.0, 6.0, 7.0, 8.0]);
|
||||
assert_eq!(r[2], [9.0, 10.0, 11.0, 12.0]);
|
||||
assert_eq!(r[3], [1.0, 13.0, 0.0, 0.0]);
|
||||
assert_eq!(r[3], [444.0, 13.0, 0.5, -0.5]);
|
||||
assert_eq!(r[4], [14.0, 15.0, 16.0, 17.0]);
|
||||
assert_eq!(r[5], [18.0, 19.0, 20.0, 21.0]);
|
||||
assert_eq!(r[6], [22.0, 23.0, 24.0, 25.0]);
|
||||
|
||||
@@ -177,7 +177,9 @@ pub(super) fn update_time_lighting(
|
||||
eye_liquid: crate::liquid::EyeLiquid,
|
||||
viewer: Res<crate::view::Viewer>,
|
||||
weather: Option<Res<crate::weather::WeatherState>>,
|
||||
view: Res<crate::view::ViewDistance>,
|
||||
// MONKEY (fog): paired with the fog-model setting (Modern decouples the fog end from farclip
|
||||
// past the reference ceiling); a tuple keeps the system under Bevy's 16-parameter limit.
|
||||
(view, fog_setting): (Res<crate::view::ViewDistance>, Res<super::FogModelSetting>),
|
||||
time: Res<Time>,
|
||||
wmo_fog: Res<crate::wmo_portal::CameraWmoFog>,
|
||||
mut wmo_ramp: ResMut<WmoCrossfade>,
|
||||
@@ -250,7 +252,12 @@ pub(super) fn update_time_lighting(
|
||||
|
||||
// Elwynn commits 125/500 clear and −139/278 at full storm (`0x6cee30`): the negative start is
|
||||
// the near veil, and the short storm end whites out the middle distance.
|
||||
let (fog_start, fog_end) = scene_fog(atmo.fog_end, atmo.fog_start_frac, view.farclip);
|
||||
let (fog_start, fog_end) = if fog_setting.modern() {
|
||||
// MONKEY (fog): identical up to farclip 777, then the end grows with the view distance.
|
||||
super::fog_model::modern_fog_end(atmo.fog_end, atmo.fog_start_frac, view.farclip)
|
||||
} else {
|
||||
scene_fog(atmo.fog_end, atmo.fog_start_frac, view.farclip)
|
||||
};
|
||||
// The interior crossfade is its own triple; the scene fog stays untouched. Submerged, the
|
||||
// underwater param owns the fog and the ramp is bypassed, as the reference snaps it for magma
|
||||
// and slime inside an MFOG interior (`0x6cef6f`); the reference's MFOG underwater block
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#ifdef SKY_FX
|
||||
#import benilla_world::sky_fx
|
||||
#endif
|
||||
// MONKEY (fog): the Modern fog colour at the horizon.
|
||||
#import benilla::fog_hook
|
||||
|
||||
struct SkyColors {
|
||||
sky0: vec4<f32>, // zenith (90°)
|
||||
@@ -26,6 +28,10 @@ struct SkyColors {
|
||||
sun: vec4<f32>,
|
||||
// MONKEY (sky): rgb = glow colour (gamma), w unused.
|
||||
glow: vec4<f32>,
|
||||
// MONKEY (fog): MonkeyFrame rows B-D (all zero = Classic).
|
||||
mf_fog_b: vec4<f32>,
|
||||
mf_fog_c: vec4<f32>,
|
||||
mf_fog_d: vec4<f32>,
|
||||
};
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> sky: SkyColors;
|
||||
|
||||
@@ -78,6 +84,22 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
s4 = mix(warp_one(sky.sky4.rgb, g0, warp_s), warp_one(sky.sky4.rgb, g1, warp_s), f);
|
||||
}
|
||||
|
||||
// Elevation gradient, linear between rings like the reference's Gouraud-shaded dome.
|
||||
var col: vec3<f32>;
|
||||
if (elev <= 0.0) {
|
||||
col = sky.fog.rgb; // horizon and below = fog colour (row 7), unwarped
|
||||
} else if (elev < 1.8) {
|
||||
col = mix(sky.fog.rgb, s4, elev / 1.8);
|
||||
} else if (elev < 3.7) {
|
||||
col = mix(s4, s3, (elev - 1.8) / (3.7 - 1.8));
|
||||
} else if (elev < 9.8) {
|
||||
col = mix(s3, s2c, (elev - 3.7) / (9.8 - 3.7));
|
||||
} else if (elev < 16.8) {
|
||||
col = mix(s2c, s1, (elev - 9.8) / (16.8 - 9.8));
|
||||
} else {
|
||||
col = mix(s1, sky.sky0.rgb, (elev - 16.8) / (90.0 - 16.8));
|
||||
}
|
||||
|
||||
// MONKEY (sky): Enhanced/High. The same stops through a monotone cubic in linear light, then
|
||||
// the sun glow and the night sky added in linear light, back to gamma, dithered.
|
||||
#ifdef SKY_FX
|
||||
@@ -111,27 +133,15 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
lin += (stars * 0.85 + milky) * night;
|
||||
}
|
||||
let out = sky_fx::linear_to_srgb(lin) + vec3<f32>(sky_fx::dither_tri(in.position.xy));
|
||||
return vec4<f32>(clamp(out, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
|
||||
col = clamp(out, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Elevation gradient, linear between rings like the reference's Gouraud-shaded dome.
|
||||
var col: vec3<f32>;
|
||||
if (elev <= 0.0) {
|
||||
col = sky.fog.rgb; // horizon and below = fog colour (row 7), unwarped
|
||||
} else if (elev < 1.8) {
|
||||
col = mix(sky.fog.rgb, s4, elev / 1.8);
|
||||
} else if (elev < 3.7) {
|
||||
col = mix(s4, s3, (elev - 1.8) / (3.7 - 1.8));
|
||||
} else if (elev < 9.8) {
|
||||
col = mix(s3, s2c, (elev - 3.7) / (9.8 - 3.7));
|
||||
} else if (elev < 16.8) {
|
||||
col = mix(s2c, s1, (elev - 9.8) / (16.8 - 9.8));
|
||||
} else {
|
||||
col = mix(s1, sky.sky0.rgb, (elev - 16.8) / (90.0 - 16.8)); // warped ring1 to the raw apex
|
||||
}
|
||||
|
||||
// Raw gamma out: the reference draws the sky as raw DBC bytes, sRGB off (`0x6d4940`).
|
||||
let rgb = col;
|
||||
// MONKEY (fog): Modern eases the low band into the world's far fog colour after either the
|
||||
// Classic or Enhanced/High gradient path; Classic returns `col` unchanged.
|
||||
let rgb = fog_hook::fog_sky_horizon(
|
||||
col, sky.fog.rgb, dir, sky.mf_fog_b, sky.mf_fog_c, sky.mf_fog_d,
|
||||
);
|
||||
return vec4<f32>(rgb, 1.0);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ pub struct SkyExt {
|
||||
/// MONKEY (sky): the glow colour, gamma (`rgb`).
|
||||
#[uniform(100)]
|
||||
pub(crate) glow: Vec4,
|
||||
/// MONKEY (fog): MonkeyFrame fog rows B-D so the dome horizon uses the world's far colour.
|
||||
#[uniform(100)]
|
||||
pub(crate) mf_fog_b: Vec4,
|
||||
#[uniform(100)]
|
||||
pub(crate) mf_fog_c: Vec4,
|
||||
#[uniform(100)]
|
||||
pub(crate) mf_fog_d: Vec4,
|
||||
}
|
||||
|
||||
/// MONKEY (sky): the pipeline key: Enhanced/High compile `SKY_FX` into `sky.wgsl`; Classic keeps
|
||||
@@ -217,6 +224,10 @@ fn setup_sky(
|
||||
fx: Vec4::ZERO,
|
||||
sun: Vec4::Y,
|
||||
glow: Vec4::ZERO,
|
||||
// MONKEY (fog): zero rows leave Classic unchanged.
|
||||
mf_fog_b: Vec4::ZERO,
|
||||
mf_fog_c: Vec4::ZERO,
|
||||
mf_fog_d: Vec4::ZERO,
|
||||
},
|
||||
});
|
||||
commands.spawn((
|
||||
@@ -283,6 +294,8 @@ fn update_sky_colors(
|
||||
quality: Res<crate::sky_fx::SkyQuality>,
|
||||
clock: Res<crate::sky_fx::SkyClock>,
|
||||
clouds: Res<crate::clouds::CloudCoverage>,
|
||||
// MONKEY (fog): the Modern fog rows for the shared horizon colour.
|
||||
monkey: Res<crate::lighting::MonkeyFrame>,
|
||||
dome: Query<&MeshMaterial3d<SkyMaterial>, With<Sky>>,
|
||||
mut materials: ResMut<Assets<SkyMaterial>>,
|
||||
) {
|
||||
@@ -311,6 +324,13 @@ fn update_sky_colors(
|
||||
);
|
||||
// MONKEY (sky): the Enhanced/High inputs; all zero at Classic so the write gate stays quiet.
|
||||
let (fx, sun_v, glow) = sky_fx_inputs(&light, *quality, clock.secs, &clouds);
|
||||
// MONKEY (fog): rows B-D as packed for the light buffer; clock lanes are not read here.
|
||||
let rows = monkey.pack(0.0, 0.0);
|
||||
let mf_fog = [
|
||||
Vec4::from_array(rows[1]),
|
||||
Vec4::from_array(rows[2]),
|
||||
Vec4::from_array(rows[3]),
|
||||
];
|
||||
benilla_assets::write_gated(
|
||||
&mut materials,
|
||||
&handle.0,
|
||||
@@ -318,6 +338,9 @@ fn update_sky_colors(
|
||||
m.extension.fx != fx
|
||||
|| m.extension.sun != sun_v
|
||||
|| m.extension.glow != glow
|
||||
|| m.extension.mf_fog_b != mf_fog[0]
|
||||
|| m.extension.mf_fog_c != mf_fog[1]
|
||||
|| m.extension.mf_fog_d != mf_fog[2]
|
||||
|| m.extension.sky0 != sky[0]
|
||||
|| m.extension.sky1 != sky[1]
|
||||
|| m.extension.sky2 != sky[2]
|
||||
@@ -337,6 +360,9 @@ fn update_sky_colors(
|
||||
m.extension.fx = fx;
|
||||
m.extension.sun = sun_v;
|
||||
m.extension.glow = glow;
|
||||
m.extension.mf_fog_b = mf_fog[0];
|
||||
m.extension.mf_fog_c = mf_fog[1];
|
||||
m.extension.mf_fog_d = mf_fog[2];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user