MONKEY (fog): LightFogBand.dbc reader + LightCatalog::blend_chain
benilla-formats/src/light/fog_band.rs reads our LightFogBand.dbc (the LightFloatBand record, 13 rows per LightParams: height fog, curve blend, sun-fog rgb/strength/angle, end-fog rgb/distance). Absent file = empty catalog, no error. blend_chain exposes the area-light blend order so a side table folds with the same law as the stock bands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
71c50a7f4d
commit
3d13fd331e
@@ -128,6 +128,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);
|
||||
|
||||
Reference in New Issue
Block a user