a lighting matrix joins the goldens, and captures stop drifting with LOD
- A creature and a chest, photographed in sunlight, in baked shade and indoors, from both sides — a matrix of golden frames that pins how every light lane renders a model, so a lighting regression in any one lane shows up as exactly one failing cell. Building it found two real races. - Captures pin the animation-LOD gate off now — the third discovered source of run-to-run drift, where a slow capture run dropped bones from a skeleton and the golden image quietly disagreed with itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,18 @@ const WOLF_FACTION: u32 = 32;
|
||||
/// onto the water *beyond* it.
|
||||
const NAME_WATER_POS: [f32; 3] = [-9512.97, -331.29, 61.4];
|
||||
|
||||
/// The lighting matrix's chest (decision 0744): `GameObjectDisplayInfo` 259,
|
||||
/// `World\SkillActivated\Containers\TreasureChest01.mdx`. GameObject guids carry the `0xF110` high
|
||||
/// word, and the descriptor is left at its defaults — an unstated `GAMEOBJECT_STATE` holds the
|
||||
/// closed rest pose, which is the frame we want.
|
||||
const CHEST_DISPLAY: u32 = 259;
|
||||
const CHEST_GUID: u64 = (0xF110u64 << 48) | 0x744;
|
||||
|
||||
/// Which way a lighting-matrix subject faces (Bevy yaw). The matrix's `front` camera sits on the
|
||||
/// lighting sun's bearing (azimuth 45°), so the subject is turned to meet it: `front` reads the
|
||||
/// face, `rear` the tail — the same body, its lit and unlit sides.
|
||||
const SUBJECT_YAW: f32 = 2.36;
|
||||
|
||||
/// Seed the fixture window's state resources once, right after the scene goes resident — the real
|
||||
/// feeds then push it into the VM during the settle window exactly as live wire data would. Item
|
||||
/// icons resolve through the offline `ItemDisplayCatalog` (display ids chosen from entries that
|
||||
@@ -1025,6 +1037,53 @@ pub(super) fn seed_ui_fixture(
|
||||
// draws one — the ShouldShowName exclusivity) even though enemy plates boot ON.
|
||||
vplates.enemies = false;
|
||||
}
|
||||
// The lighting matrix (decision 0744). One spawn, through the same component set a streamed
|
||||
// entity gets, at a position whose light lane was read out of the data (see the matrix note
|
||||
// in `scenarios`). Deliberately ANONYMOUS: no name is registered and plates are forced off,
|
||||
// so no glyph rides over the body — the diff of these cells must be about light and nothing
|
||||
// else.
|
||||
UiFixture::Subject { kind, at } => {
|
||||
use benilla_protocol::messages::ObjectFields;
|
||||
let transform = Transform {
|
||||
translation: wow_to_bevy(at),
|
||||
rotation: Quat::from_rotation_y(SUBJECT_YAW),
|
||||
..default()
|
||||
};
|
||||
match kind {
|
||||
SubjectKind::Creature => {
|
||||
commands.spawn((
|
||||
crate::net::Guid(WOLF_GUID),
|
||||
crate::net::NetEntity {
|
||||
kind: benilla_protocol::EntityKind::Unit,
|
||||
display_id: Some(WOLF_DISPLAY),
|
||||
scale: 1.0,
|
||||
},
|
||||
crate::net::ObjectStore(ObjectFields::from_pairs(&[
|
||||
(22, 100), // UNIT_FIELD_HEALTH
|
||||
(28, 100), // UNIT_FIELD_MAXHEALTH
|
||||
(34, 2), // UNIT_FIELD_LEVEL
|
||||
(35, WOLF_FACTION), // UNIT_FIELD_FACTIONTEMPLATE
|
||||
])),
|
||||
transform,
|
||||
Visibility::default(),
|
||||
));
|
||||
}
|
||||
SubjectKind::Chest => {
|
||||
commands.spawn((
|
||||
crate::net::Guid(CHEST_GUID),
|
||||
crate::net::NetEntity {
|
||||
kind: benilla_protocol::EntityKind::GameObject,
|
||||
display_id: Some(CHEST_DISPLAY),
|
||||
scale: 1.0,
|
||||
},
|
||||
crate::net::ObjectStore(ObjectFields::default()),
|
||||
transform,
|
||||
Visibility::default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
vplates.enemies = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) use probes::{
|
||||
EntityCensusPlugin, LiveFpsPlugin, NodeProbePlugin, ParticleCensusPlugin, ProbeChatPlugin,
|
||||
ProbeExitPlugin, ProbeKeyPlugin, ProbeLuaPlugin, ProbeResizePlugin,
|
||||
};
|
||||
use scenarios::{Scenario, UiFixture, GROUND_EYE, SCENARIOS};
|
||||
use scenarios::{Scenario, SubjectKind, UiFixture, GROUND_EYE, SCENARIOS};
|
||||
|
||||
/// Is the app running a capture (`$WOW_CAPTURE` set)? Read by `main` to disable net + add the harness.
|
||||
pub(crate) fn scenario_active() -> bool {
|
||||
|
||||
@@ -96,6 +96,27 @@ pub(super) enum UiFixture {
|
||||
/// sweep covered this — decision 0519 wrote the law and shipped it with the sort bias
|
||||
/// pointing the wrong way, invisible to every gate. The name must read at full strength.
|
||||
NameWater,
|
||||
/// One cell of the **lighting matrix** (decision 0744): a creature or a GameObject spawned
|
||||
/// through the live path at `at`, so each lane an object's light can take is photographed from
|
||||
/// two sides. See [`SubjectKind`] and the matrix note above [`SUBJECT_SUN`].
|
||||
Subject {
|
||||
kind: SubjectKind,
|
||||
/// Where the subject stands, raw WoW coords — its FEET, not its body centre.
|
||||
at: [f32; 3],
|
||||
},
|
||||
}
|
||||
|
||||
/// What the lighting matrix puts in frame. Both spawn with the same live component set a streamed
|
||||
/// entity gets (`NetEntity` + descriptor), differing only in `EntityKind` — so the matrix exercises
|
||||
/// the real unit and GameObject paths rather than a bespoke preview.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(super) enum SubjectKind {
|
||||
/// The Timber Wolf (entry 69, display 604) — the same subject `vplates`/`name-water` use, so a
|
||||
/// defect seen here can be A/B'd against them directly.
|
||||
Creature,
|
||||
/// `World\SkillActivated\Containers\TreasureChest01.mdx` (`GameObjectDisplayInfo` 259) — the
|
||||
/// canonical world chest, at its closed rest pose.
|
||||
Chest,
|
||||
}
|
||||
|
||||
/// The ON-DEMAND Northshire framings, anchored at the Human start (around `SPAWN_XY`
|
||||
@@ -217,6 +238,116 @@ pub(super) const SCENARIOS: &[Scenario] = &[
|
||||
minute: 0,
|
||||
ui: None,
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-sun-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9496.46, 59.54, 58.28],
|
||||
look: [-9500.00, 56.00, 57.28],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_SUN,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-sun-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9503.54, 52.46, 58.28],
|
||||
look: [-9500.00, 56.00, 57.28],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_SUN,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-shade-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9496.46, 47.54, 57.75],
|
||||
look: [-9500.00, 44.00, 56.75],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_SHADE,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-shade-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9503.54, 40.46, 57.75],
|
||||
look: [-9500.00, 44.00, 56.75],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_SHADE,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-indoor-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9466.57, 34.73, 59.70],
|
||||
look: [-9469.40, 31.90, 58.70],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_INDOOR,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-indoor-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9472.23, 29.07, 59.70],
|
||||
look: [-9469.40, 31.90, 58.70],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Creature,
|
||||
at: SUBJECT_INDOOR,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-sun-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9496.82, 59.18, 57.98],
|
||||
look: [-9500.00, 56.00, 56.93],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_SUN,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-sun-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9503.18, 52.82, 57.98],
|
||||
look: [-9500.00, 56.00, 56.93],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_SUN,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-shade-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9496.82, 47.18, 57.45],
|
||||
look: [-9500.00, 44.00, 56.40],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_SHADE,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-shade-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9503.18, 40.82, 57.45],
|
||||
look: [-9500.00, 44.00, 56.40],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_SHADE,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
/// Director shot 1 — the Northshire overlook, north-east of the Abbey looking at it: terrain,
|
||||
@@ -261,10 +392,73 @@ pub(super) const INN_LOOK: [f32; 3] = [-9458.8, -7.5, 48.2];
|
||||
pub(super) const FELWOOD_EYE: [f32; 3] = [4060.9, -944.3, 256.8];
|
||||
pub(super) const FELWOOD_LOOK: [f32; 3] = [4014.0, -954.4, 242.9];
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The LIGHTING MATRIX (decision 0744) — one subject, three lanes, two sides.
|
||||
//
|
||||
// The golden spots are landscape: they photograph terrain, buildings and water, and (captures being
|
||||
// server-less) contain no creature or GameObject at all. So the *object* light path — the one every
|
||||
// creature, player, pet and chest in the game is lit by — had no regression coverage whatsoever,
|
||||
// while the bug ledger's unit reports (blacked-out textures, mis-lit NPCs across city WMOs) sit
|
||||
// squarely on it.
|
||||
//
|
||||
// The three positions are the three lanes an object's light can take, and they were CHOSEN FROM THE
|
||||
// DATA, not by eye — `WOW_LIGHT_AT`/`WOW_LIGHT_GRID` (`wmo_portal::audit::light_probe`) reports the
|
||||
// lane and the terrain MCSH bit at any world point:
|
||||
//
|
||||
// SUN (-9500, 56) terrain z 56.48 MCSH false lane exterior-on-terrain — sun term at full
|
||||
// SHADE (-9500, 44) terrain z 55.95 MCSH true lane exterior-on-terrain — sun term dimmed
|
||||
// INDOOR (-9469.4, 31.9) terrain z none zone-text indoor 5, lane BAKE g05 — no sun at all
|
||||
//
|
||||
// SUN and SHADE are twelve yards apart on the same open ground, so between that pair the ONLY
|
||||
// changed input is the baked shadow bit — exactly the discriminator `entity_shade` ramps on
|
||||
// (2.5 lit → 0.5 shadowed). INDOOR stands in the Goldshire inn's common room, where the interior
|
||||
// classifier owns the tag instead and the sun never reaches.
|
||||
//
|
||||
// Two sides per lane, straddling the light: the game's LIGHTING sun is near-fixed at azimuth 45°
|
||||
// (`sun::follow`), so `front` puts the camera on that bearing (sun behind us — the subject's lit
|
||||
// face) and `rear` puts it opposite (the shadowed face). Indoors there is no sun, so the pair simply
|
||||
// samples the bake from two sides. Cameras sit at a fixed offset off the subject's feet, eye a
|
||||
// little above and aimed at the body, so every cell frames the subject identically and a diff is
|
||||
// about light, never framing.
|
||||
|
||||
/// Lighting-matrix subject positions (feet, raw WoW coords) — see the note above.
|
||||
pub(super) const SUBJECT_SUN: [f32; 3] = [-9500.0, 56.0, 56.48];
|
||||
pub(super) const SUBJECT_SHADE: [f32; 3] = [-9500.0, 44.0, 55.95];
|
||||
pub(super) const SUBJECT_INDOOR: [f32; 3] = [-9469.4, 31.9, 57.9];
|
||||
|
||||
/// Every remaining named viewpoint — the UI look-pass fixtures, the sun/moon/sky regression
|
||||
/// fixtures, the house-compass and street scenes. Capturable by name (`WOW_CAPTURE=<name>`) for
|
||||
/// debugging and look passes, but NOT part of the blessed baseline sweep.
|
||||
pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// The chest INDOORS is out of the blessed sweep and in here instead: measured over two runs of
|
||||
// one build it is not reproducible (front MAE 1.551 / 9.2 % of pixels, rear 0.529 / 7.6 %), and
|
||||
// the diff is the whole body shifting brightness in perfect registration — LIGHT, not pose. The
|
||||
// GameObject's interior light lane does not converge by the shutter, while the CREATURE at the
|
||||
// same spot is bit-identical. That is the ledger's "mis-lit objects inside city WMOs" class, and
|
||||
// it is open (decision 0746). Capture them by name to work on it; they rejoin the sweep the day
|
||||
// they are deterministic.
|
||||
Scenario {
|
||||
name: "chest-indoor-front",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9466.85, 34.45, 59.40],
|
||||
look: [-9469.40, 31.90, 58.35],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_INDOOR,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-indoor-rear",
|
||||
map: MAP_AZEROTH,
|
||||
eye: [-9471.95, 29.35, 59.40],
|
||||
look: [-9469.40, 31.90, 58.35],
|
||||
minute: 720,
|
||||
ui: Some(UiFixture::Subject {
|
||||
kind: SubjectKind::Chest,
|
||||
at: SUBJECT_INDOOR,
|
||||
}),
|
||||
},
|
||||
Scenario {
|
||||
name: "house-north",
|
||||
map: MAP_AZEROTH,
|
||||
|
||||
@@ -235,6 +235,22 @@ fn main() -> AppExit {
|
||||
// It is not the only source of per-run drift, though it was long documented as such: the
|
||||
// other is the frame clock itself, frozen in `capture` (decision 0723).
|
||||
std::env::set_var("WOW_CLUTTER_DENSITY", "0");
|
||||
// Third source of per-run drift, and the one the lighting matrix (decision 0746) hit: the
|
||||
// anim-LOD park/wake gate (`creature_anim::lod::gate_rig_animation`). Whether a rig is
|
||||
// parked, and which pose it wakes into, depends on when its model finished loading relative
|
||||
// to the frustum/room evaluation — asset-load timing, which the frozen clock does not
|
||||
// control. MEASURED: the seeded wolf lands in one of exactly two poses, the pair always
|
||||
// MAE 4.123 apart, and three runs with the gate off are bit-identical.
|
||||
//
|
||||
// Off for captures, unless explicitly overridden. A still frame is meant to contain only
|
||||
// rigs that are IN view, and a rig in view is one the gate should never park — so what a
|
||||
// capture loses is the gate's own correctness, not the shot's subject. That loss is named,
|
||||
// not free: a rig wrongly parked in frame stays invisible to the sweep, and the deeper
|
||||
// question the measurement raises — why a woken rig does not converge on the absolute-clock
|
||||
// pose the gate promises — is open in 0746 and worth its own hunt.
|
||||
if std::env::var("WOW_NO_ANIM_LOD").is_err() {
|
||||
std::env::set_var("WOW_NO_ANIM_LOD", "1");
|
||||
}
|
||||
}
|
||||
|
||||
// The `mpq://` asset source must be registered BEFORE `AssetPlugin` (inside `DefaultPlugins`)
|
||||
|
||||
@@ -47,6 +47,9 @@ impl BoothRig {
|
||||
let Some(buffer) = self.buffer.clone() else {
|
||||
// No booth buffer at all (headless tests) — there is nothing to wait FOR, so this
|
||||
// stays a plain fallback rather than an unready signal.
|
||||
if super::booth_log() {
|
||||
eprintln!("[booth] variant NO-BUFFER -> world lane (unlit in a booth)");
|
||||
}
|
||||
return world.clone();
|
||||
};
|
||||
match material_variant(&mut self.variants, &buffer, world, materials, false) {
|
||||
|
||||
@@ -299,6 +299,24 @@ fn booth_anchors(
|
||||
creatures?.display_anchors(display_id)
|
||||
}
|
||||
|
||||
/// `WOW_BOOTH_LOG=1` — the resolved framing for a bake: which anchors it had and where the camera
|
||||
/// ended up. An opaque near-black pane is what a camera parked *inside* the model renders, so
|
||||
/// "black" and "zoomed to max" may be one symptom; this is the line that tells them apart.
|
||||
fn log_frame(token: &str, a: &PortraitAnchors, cam: &Transform) {
|
||||
if booth_log() {
|
||||
eprintln!(
|
||||
"[booth] {token} frame eye=({:.3},{:.3},{:.3}) authored_cam={} pivot={:.3} head_y={:.3} gr={:.3}",
|
||||
cam.translation.x,
|
||||
cam.translation.y,
|
||||
cam.translation.z,
|
||||
a.camera.is_some(),
|
||||
a.pivot_height,
|
||||
a.head.map_or(f32::NAN, |h| h.y),
|
||||
a.ground_radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Arm `booth` after a content edge: render the settle window, plus every frame until each twin
|
||||
/// material's texture is resident. `twins` = the material handles the bake just installed.
|
||||
fn wake_booth<'a>(
|
||||
@@ -799,6 +817,7 @@ fn sync_portraits(
|
||||
);
|
||||
// Frame through the display's authored portrait camera (heuristic anchors for the
|
||||
// camera-less few), resolved above before anything was torn down.
|
||||
log_frame(token, &anchors, &frame(&anchors).0);
|
||||
aim(&mut cams, token, &frame(&anchors));
|
||||
log_bake(token, "bake", parts.len(), riders.len(), billboards.len());
|
||||
wake_booth(
|
||||
@@ -1042,6 +1061,7 @@ fn sync_body_booth(
|
||||
);
|
||||
// Body framing from the display's bounds — the full standing figure, feet-to-crown.
|
||||
// Resolved before the teardown above; see the portrait site for why it cannot be faked.
|
||||
log_frame(slot, &anchors, &body_frame(&anchors).0);
|
||||
aim(cams, slot, &body_frame(&anchors));
|
||||
log_bake(slot, "bake", parts.len(), riders.len(), billboards.len());
|
||||
wake_booth(
|
||||
|
||||
Reference in New Issue
Block a user