render: the hover highlight is the scene ambient, and unlit M2 parts take it

The 1.12 highlight is not a fixed 64/255 lift. SetHighlight samples the
scene's committed ambient (read at 0x614576-0x6145bd), and every batch
adds it to its colour before the final clamp, lit or unlit.
0xff404040 is only the fallback before a map's light loads. The
highlight now follows the time of day, zone and weather: roughly
(29, 60, 84)/255 at night to (104, 130, 154)/255 at noon, where it was
always (64, 64, 64)/255.

Unlit M2 batches in blend modes 0 to 4 now draw
texel × clamp(tint·M2Color + highlight), the unlit program's c28 + c29.
They took no highlight before. The reference holds the ambient sampled
when a highlight begins; benilla samples it live, a marked deviation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-24 00:11:09 +07:00
co-authored by Claude Opus 5.5
parent 35ed67520a
commit 693b313b1c
3 changed files with 36 additions and 20 deletions
+5 -5
View File
@@ -4,13 +4,13 @@
//! The reference pushes it on hover/target *change*, never a per-frame compare: the mouseover
//! publisher and the target setter call `SetHighlight 0x614550` / `ClearHighlight 0x6144f0` with a
//! per-object reason bitmask (bit 0 = target, bit 1 = mouseover — hover + target **stack**, and the
//! glow drops only when the last reason clears). `SetHighlight` writes the config RGB — shipped
//! default `0xff404040` ⇒ **+64/255 per channel** — into the model, and the animate kernel adds it
//! to the material emissive (`glMaterialfv(GL_EMISSION)`): a flat additive lift inside the GL
//! lighting sum, pre-texture-modulate, riding every lit material of the model *and* its attachments.
//! glow drops only when the last reason clears). `SetHighlight` samples the scene's committed
//! ambient (`[0xce9cd8]`, read at `0x614576`-`0x6145bd`) into the model and holds it until the last
//! reason clears; each batch adds it to its colour before the final clamp (`c29`), lit or unlit,
//! the model's attachments included. `0xff404040` is only the fallback before a map's light loads.
//!
//! benilla carries the flag in **bit 31 of the per-instance `MeshTag`** (the convention home is
//! `benilla_world::mesh_tag`); `wow_model.wgsl` adds the 64/255 lift to its lighting factor when set. This
//! `benilla_world::mesh_tag`); `wow_model.wgsl` adds the scene ambient when it is set. This
//! system is the bit's only writer: each frame (PostUpdate — after every Update payload writer, so
//! their whole-`u32` overwrites can't strand the bit) it ORs the flag onto every part of the
//! hovered + selected roots and clears it on roots that left the set. The reason bitmask collapses
@@ -676,9 +676,9 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
let lit = select(select(lit_exterior, lit_interior, is_interior), lit_m2_interior, is_rig);
// Gamma-space albedo: lighting runs on the authored byte values. `m.tint` plus its mat-anim
// delta is the animated M2Color tint.
let albedo = base.rgb * (m.tint.rgb + wow_light.matanim[u32(m.anim_slots.y)].xyz);
// Unlit fullbright replaces the lit path and takes no emission: with lighting off, GL_EMISSION
// is dead.
let anim_tint = m.tint.rgb + wow_light.matanim[u32(m.anim_slots.y)].xyz;
let albedo = base.rgb * anim_tint;
// An unlit batch replaces the lit path: fullbright, with only the highlight added (below).
let is_emissive = m.model_flags.w > 0.5;
// SIDN night glow: the emissive × the night fraction (ramping 20:30→21:30, 06:00→07:00), a
// GL_EMISSION term inside the clamped lit sum, on lit lanes only.
@@ -693,9 +693,11 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
let sidn_e = m.sidn.rgb * (wow_light.grade.x * sidn_w);
let point_diffuse = in.point_lit;
// The hover/target highlight (tag bit 31): `glMaterialfv(GL_EMISSION)` of 64/255 per channel
// (config default 0xff404040), inside the lighting clamp; the unlit path never gets it.
let highlight = select(0.0, 0.2509804, highlighted);
// The hover/target highlight (tag bit 31): the scene's committed ambient
// (`0x614576`-`0x6145bd`), added to the batch colour inside the final clamp, lit or unlit
// (`c29`). Deviation: sampled live, where the reference holds the value sampled when the
// highlight began; a unit's tag has no slot to hold a colour, and the ambient moves slowly.
let highlight = select(vec3<f32>(0.0), wow_light.light_ambient.rgb, highlighted);
// The FFP combine: the light sum (lit, point lights, emission) clamps first and the texture
// modulates it, `tex × clamp(C·sum + emission)`, with C the GL_COLOR_MATERIAL colour (MOCV on
// WMO, the body tint on M2). The WMO branch divides Bevy's MOCV fold back out, guarded; a dim
@@ -705,7 +707,7 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
if (is_wmo) {
let vc = in.color.rgb;
let primary = clamp(
vc * (lit + point_diffuse) + sidn_e + vec3<f32>(highlight),
vc * (lit + point_diffuse) + sidn_e + highlight,
vec3<f32>(0.0),
vec3<f32>(1.0),
);
@@ -724,7 +726,7 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
// An M2: the body tint multiplies the light terms as MOCV does above. A WMO surface has
// no tint slot, so the WMO branch leaves it out.
let primary = clamp(
inst_tint * (lit + point_diffuse) + sidn_e + vec3<f32>(highlight),
inst_tint * (lit + point_diffuse) + sidn_e + highlight,
vec3<f32>(0.0),
vec3<f32>(1.0),
);
@@ -733,15 +735,29 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
#else
// A WMO batch without MOCV lands here too; its tint slot is the identity slot 0.
let primary = clamp(
inst_tint * (lit + point_diffuse) + sidn_e + vec3<f32>(highlight),
inst_tint * (lit + point_diffuse) + sidn_e + highlight,
vec3<f32>(0.0),
vec3<f32>(1.0),
);
lit_rgb = albedo * primary;
#endif
// The unlit path takes the body tint: with GL_LIGHTING off, gx SetState(1) is a plain
// `glColor` modulate.
var rgb = select(lit_rgb, albedo * inst_tint, is_emissive);
// The unlit path: an M2's unlit program outputs `c28 + c29` (`0x70c663`-`0x70c693` fold the
// tint·M2Color term into `c29` beside the highlight), so the texel modulates
// `clamp(C·tint + highlight)`, C the M2Color. A WMO keeps the plain modulate.
var unlit_rgb = albedo * inst_tint;
if (!is_wmo) {
#ifdef VERTEX_COLORS
// The constant M2Color rides the vertex colour, which Bevy folded into `base`.
let unlit_c = in.color.rgb * anim_tint;
let unlit_tex = base.rgb / max(in.color.rgb, vec3<f32>(1.0 / 255.0));
#else
let unlit_c = anim_tint;
let unlit_tex = base.rgb;
#endif
let unlit_sum = unlit_c * inst_tint + highlight;
unlit_rgb = unlit_tex * clamp(unlit_sum, vec3<f32>(0.0), vec3<f32>(1.0));
}
var rgb = select(lit_rgb, unlit_rgb, is_emissive);
// An M2 Mod or Mod2x batch draws the bare texel: the reference zeroes its tint·M2Color term and
// forces the primary colour to the blend identity (`0x70c507`/`0x70c5b8`), so neither the
// animated M2Color nor the body tint reaches it. Its alpha still does, through the lerp below.
+3 -3
View File
@@ -17,9 +17,9 @@
//! the device-init `GL_COLOR_MATERIAL` / `GL_AMBIENT_AND_DIFFUSE` current-colour path. So the tint
//! is the **material ambient+diffuse colour**: it multiplies the light sum *inside* the [0,1] clamp
//! and NOT the emission terms — structurally identical to what a WMO batch's MOCV already does in
//! `wow_model.wgsl`, which is where the shader multiplies it. With lighting off (an UNLIT batch) the
//! same state is a plain `glColor` modulate on the texture, so the fullbright path takes it too —
//! unlike the highlight, whose GL_EMISSION is dead there.
//! `wow_model.wgsl`, which is where the shader multiplies it. An UNLIT batch's program adds the
//! tint·M2Color term and the highlight inside one clamp (`c28 + c29`), so the fullbright path takes
//! the tint and the highlight both.
//!
//! (wow-re's `ghost-death-visuals.md` §4 still flags this combine as its one INFERRED hop and asks
//! for a models-node follow-up; `models.md` is that follow-up, and it is VERIFIED — two independent