lava: Ironforge's magma gets its own palette

grom: "this red looks nice in the burning steps but terrible in Ironforge in
contrast to the lava pouring from the forge [...] still keep the kinda colouring
and tone of a per zone lava".

Measured first, because the render looked wrong and was not: the shipped
XTextures\lava frames really are that saturated -- mean RGB (176, 23, 0) on
frame 1, maxima (255, 134, 0) -- and over the Great Forge pool we draw
(202, 23, 0), the extra being the interior ambient. Nothing is broken. Outdoors
the zone's own fog carries the sheet, which is why Burning Steppes reads well;
a city interior has no haze, so the same frames land as a flat slab against
brown rock, beside a near-white molten pour.

So: one tint multiply on the magma body, before fog and before the emissive
boost so a tinted pool glows its own colour. Keyed on MOHD.wmoID -- the
WMOAreaTable key, a building rather than a box of world coordinates -- so ADT
magma never reaches it and the Steppes are untouched, which a re-capture
confirms pixel for pixel. Ironforge (208) takes red x0.82, green x1.6: deeper
and crustier, and the pour becomes the brightest thing in the room. Measured
over `ironforge-lava`, (202, 23, 0) -> (160, 36, 0).

The palette is art, not a setting: always on, no CVar, nothing hidden.

`MagmaTone::for_wmo`'s 208 is pinned against the shipped root, since a constant
like that would otherwise rot in silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-09 09:48:36 -07:00
co-authored by Claude Opus 5
parent e2257f84e4
commit ac36f7c8f3
4 changed files with 144 additions and 11 deletions
+5
View File
@@ -473,6 +473,11 @@ pub struct LiquidExt {
/// `y` = frame count, `z` = scroll flag, `w` = clock enable.
#[uniform(102)]
pub anim: Vec4,
/// `rgb` multiplies the magma/slime body before fog; `w` unread. White on everything 1.12
/// draws its own way — see `benilla_world::liquid::surface::MagmaTone`, the one deliberate
/// departure from the shipped palette.
#[uniform(102)]
pub tint: Vec4,
/// GFX (planar water): the mirrored view of the world
/// (`benilla_world::liquid::reflect`), filtered, at half the viewport. Sampled only where the
/// screen-space march missed and only when `water.mirror.x` says the target was rendered for
@@ -48,6 +48,8 @@ struct LiquidParams {
// y = frame count; z = scroll flag (WMO magma/slime only, liquid nibbles 6/7); w = clock
// enable (0 on a deterministic run).
anim: vec4<f32>,
// rgb multiplies the magma/slime body before fog; white everywhere 1.12 is left alone.
tint: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> w: LiquidParams;
@@ -243,10 +245,14 @@ fn fragment(in: LiquidVsOut) -> @location(0) vec4<f32> {
// Magma/slime: the sheet is the opaque body, unmodulated (the ADT vertex has no colour, the WMO
// one is `0xffffffff`) and unlit (lighting off on both paths), but fogged.
if (w.kind.x > 0.5) {
// The per-zone palette, applied to the body before fog and before the emissive boost, so
// a tinted pool's glow is its own colour and not the sheet's. White leaves the sheet
// exactly as the reference draws it.
let body = detail.rgb * w.tint.rgb;
// MONKEY (post): distant magma keeps some authored body and crosses 1.0 for bloom.
let fogged = apply_fog(detail.rgb, in.world_position.xyz, in.room_fog);
let fogged = apply_fog(body, in.world_position.xyz, in.room_fog);
let hot = emissive_hook::emissive_boost(
detail.rgb, emissive_hook::EMISSIVE_MAGMA, wow_light.light_diffuse.w, 1.0);
body, emissive_hook::EMISSIVE_MAGMA, wow_light.light_diffuse.w, 1.0);
return vec4<f32>(mix(fogged, hot,
emissive_hook::magma_fog_resist(wow_light.light_diffuse.w)), 1.0);
}
+129 -9
View File
@@ -78,14 +78,70 @@ impl LiquidPath {
}
}
/// Which palette a magma pool draws its body with: 1.12's own everywhere, bar the buildings
/// named here.
///
/// **This is the one deliberate departure from the shipped art in the liquid renderer.** The
/// `XTextures\\lava` frames are a saturated red — mean RGB (176, 23, 0), maxima (255, 134, 0) on
/// frame 1 — and outdoors the zone's own fog carries them: Burning Steppes is a red haze and the
/// lava sits inside it. A city interior has no such haze, so the same sheet reads as a flat
/// neon slab against brown rock, and next to the Great Forge's own molten pour, which is a
/// near-white WMO material, it reads wrong twice over. grom, 2026-11-09: *"this red looks nice
/// in the burning steps but terrible in Ironforge in contrast to the lava pouring from the
/// forge [...] still keep the kinda colouring and tone of a per zone lava"*.
///
/// Keyed on `MOHD.wmoID` (the `WMOAreaTable` key), so it names a building rather than a box of
/// world coordinates: it survives a placement moving and it cannot catch a neighbour's pool.
/// ADT magma never reaches it — open-world lava has no WMO — which is what leaves the Steppes
/// alone.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub(crate) enum MagmaTone {
/// The shipped frames, untouched. Every liquid in the world but the list below.
#[default]
AsShipped,
/// Ironforge (`wmoID` 208). Red down, green up: the body goes deeper and crustier and the
/// pour above it becomes the brightest thing in the room, which is what a forge should look
/// like. Measured over the `ironforge-lava` frame, mean RGB (202, 23, 0) -> (166, 37, 0).
Ironforge,
}
impl MagmaTone {
/// The building's palette, by its `MOHD.wmoID`.
fn for_wmo(wmo_id: u32) -> Self {
match wmo_id {
208 => Self::Ironforge,
_ => Self::AsShipped,
}
}
/// The multiplier on the body, `rgb` in the shader's gamma space.
fn tint(self) -> Vec4 {
match self {
Self::AsShipped => Vec4::ONE,
Self::Ironforge => Vec4::new(0.82, 1.6, 1.0, 1.0),
}
}
/// Every palette a material is built for. Only magma takes more than the shipped one.
fn all_for(kind: LiquidKind) -> &'static [Self] {
if kind == LiquidKind::Magma {
&[Self::AsShipped, Self::Ironforge]
} else {
&[Self::AsShipped]
}
}
}
/// Which shared material a surface takes. The scroll is per nibble and per path, which
/// [`LiquidKind`] collapses (2 and 6 are both `Magma`), so it keys the material too.
/// [`LiquidKind`] collapses (2 and 6 are both `Magma`), so it keys the material too; so does the
/// palette, which is per building.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct LiquidKey {
kind: LiquidKind,
path: LiquidPath,
/// This surface takes the animated stage-0 texture matrix ([`scrolls`]).
scroll: bool,
tone: MagmaTone,
}
/// Does a WMO MLIQ surface of this type nibble take the reference's animated stage-0 texture
@@ -110,9 +166,15 @@ impl LiquidAssets {
kind: LiquidKind,
path: LiquidPath,
scroll: bool,
tone: MagmaTone,
) -> Option<Handle<LiquidMaterial>> {
self.materials
.get(&LiquidKey { kind, path, scroll })
.get(&LiquidKey {
kind,
path,
scroll,
tone,
})
.map(|e| e.material.clone())
}
}
@@ -154,7 +216,8 @@ pub(crate) fn spawn_liquids<'a>(
for lq in liquids {
// The scene fog (the ADT passes submit none and draw under the scene submit `0x66ff20`),
// and never a scroll, though ADT magma is nibble 6: its kernel zeroes the v-translate.
let Some(material) = liquid.material(lq.kind, LiquidPath::Adt, false) else {
let Some(material) = liquid.material(lq.kind, LiquidPath::Adt, false, MagmaTone::AsShipped)
else {
continue; // this kind's frames failed to load (warned at setup)
};
let info = wet_footprint(lq, &Transform::IDENTITY, LiquidSource::AdtChunk);
@@ -253,6 +316,8 @@ pub(crate) fn spawn_wmo_liquids<'a>(
transform: Transform,
interior: bool,
pool: WmoPool,
// The owning root's `MOHD.wmoID`, which picks the magma palette (`MagmaTone`).
wmo_id: u32,
// The owning root's MOMT `diffColor` table: MOMT lives in the root, not the group file.
material_diff_color: &[[f32; 3]],
entities: &mut Vec<Entity>,
@@ -261,6 +326,7 @@ pub(crate) fn spawn_wmo_liquids<'a>(
return;
};
let path = LiquidPath::wmo(interior);
let tone = MagmaTone::for_wmo(wmo_id);
for lq in liquids {
// MONKEY (water): opt-in evidence for WMO-liquid classification and the raw MLIQ opacity
// contract. The byte is not a depth; logging it beside the group and world centre makes a
@@ -302,7 +368,7 @@ pub(crate) fn spawn_wmo_liquids<'a>(
.copied()
})
.flatten();
let Some(material) = liquid.material(lq.kind, path, scroll) else {
let Some(material) = liquid.material(lq.kind, path, scroll, tone) else {
continue; // this kind's frames failed to load (warned at setup)
};
if scroll {
@@ -415,13 +481,14 @@ pub(super) fn setup_liquid(
} else {
&[false]
};
for (path, &scroll) in [
for ((path, &scroll), &tone) in [
LiquidPath::Adt,
LiquidPath::WmoExterior,
LiquidPath::WmoInterior,
]
.into_iter()
.flat_map(|p| scroll_lanes.iter().map(move |s| (p, s)))
.flat_map(|ps| MagmaTone::all_for(kind).iter().map(move |t| (ps, t)))
{
let material = materials.add(ExtendedMaterial {
base: StandardMaterial {
@@ -464,6 +531,8 @@ pub(super) fn setup_liquid(
if scroll { 1.0 } else { 0.0 },
if deterministic { 0.0 } else { 1.0 },
),
// The per-building magma palette; `Vec4::ONE` for everything else.
tint: tone.tint(),
// MONKEY (enhanced water): the module's uniform; the sky rows and the
// quality follow at runtime (`liquid/scene_depth.rs`).
water: benilla_assets::WaterUniform {
@@ -498,9 +567,15 @@ pub(super) fn setup_liquid(
light_buf: world_assets.shared_light.clone(),
},
});
assets
.materials
.insert(LiquidKey { kind, path, scroll }, LiquidEntry { material });
assets.materials.insert(
LiquidKey {
kind,
path,
scroll,
tone,
},
LiquidEntry { material },
);
}
}
// Frame sets, not materials.
@@ -508,7 +583,11 @@ pub(super) fn setup_liquid(
"liquid: loaded {} water frame set(s)",
FRAME_SETS
.iter()
.filter(|(k, ..)| assets.material(*k, LiquidPath::Adt, false).is_some())
.filter(|(k, ..)| {
assets
.material(*k, LiquidPath::Adt, false, MagmaTone::AsShipped)
.is_some()
})
.count()
);
commands.insert_resource(assets);
@@ -621,6 +700,7 @@ mod tests {
kind: LiquidKind::Still,
path: LiquidPath::Adt,
scroll: false,
tone: MagmaTone::AsShipped,
},
LiquidEntry {
material: Handle::default(),
@@ -682,6 +762,46 @@ mod real_data {
use benilla_formats::{parse_wmo_root, wmo_group_liquid_mesh};
use std::collections::HashMap;
/// The magma palette's key is a real `MOHD.wmoID`, read back off the shipped root.
///
/// `MagmaTone::for_wmo` names 208 and nothing else. A constant like that rots in silence --
/// the pool would simply stop being tinted and nobody would know which end moved -- so it is
/// pinned against the file it came from.
#[test]
fn ironforges_magma_palette_is_keyed_on_its_own_wmo_id() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let bytes = chain
.read_file("world\\wmo\\khazmodan\\cities\\ironforge\\ironforge.wmo")
.expect("root readable");
let wmo_id = benilla_formats::wmo_root_id(&bytes);
assert_eq!(
wmo_id, 208,
"Ironforge's MOHD.wmoID moved; MagmaTone::for_wmo names the old one"
);
assert_eq!(
super::MagmaTone::for_wmo(wmo_id),
super::MagmaTone::Ironforge,
);
// Every other building in the city block keeps the shipped palette.
for other in [0, 1, 207, 209, 10047] {
assert_eq!(
super::MagmaTone::for_wmo(other),
super::MagmaTone::AsShipped,
"wmoID {other} must take the shipped frames",
);
}
assert_eq!(
super::MagmaTone::AsShipped.tint(),
bevy::prelude::Vec4::ONE,
"the shipped palette must be a no-op multiply, or every pool in the world moves",
);
// Only magma is built in more than one palette: a second water material per tone would
// be a draw call's worth of nothing.
assert_eq!(super::MagmaTone::all_for(LiquidKind::Magma).len(), 2);
assert_eq!(super::MagmaTone::all_for(LiquidKind::Still).len(), 1);
}
/// The scroll gate over the shipped pools, where both behaviours share buildings: Ironforge has
/// 9 static nibble-2 and 2 scrolling nibble-6 magma groups, Blackrock is nibble 6 throughout,
/// Undercity's 38 slime canals are nibble 3 and still, and Stratholme's are nibble 7 and flow.
@@ -513,6 +513,8 @@ pub(super) fn spawn_loaded_placements(
&p.transform,
m.group_bounds.get(gi),
),
// The building's own id, which picks its magma palette.
m.wmo_id,
// The root's MOMT diffColor table: an interior pool's body colour.
&m.material_diff_color,
&mut ents,