Files
benilla-octo/crates/benilla-world/src/sky_order.rs
T
SamandClaude Fable 5 25c865c00a the ghost world gets its own sky, and a corpse cannot steer
- a dead player's sky is DeathClouds now — `lightSkyboxID` was named in the schema and read by nobody and `LightSkybox.dbc` was never loaded at all, so a ghost kept the ordinary gradient dome; the shipped data has exactly 5 of 426 LightParams rows carrying a skybox id, every one of them DeathClouds.mdx, reached from the death profile (slot 4) of all 374 Light rows and gated on the ghost bit, with a hardcoded weight of 1.0 that skips the WMO skybox outright; it resolves off the same (map, camera position) seed the atmosphere uses, so the ghost sky and the death-profile fog it hangs over switch on the same frame — and the lane sheds its `wmo_sky` name for `skybox`, no longer being WMO-only
- a corpse cannot steer: the reference's two movement-input predicates share a precondition benilla never modelled — `UNIT_FIELD_HEALTH > 0` — so at zero health the keyboard turn and the right-drag close together, exactly as they do inside an Ice Block, while the ghost (health 1) keeps moving; the wow-re §5 round then corrected three byte trails (the mouse turn has its own eight-conjunct predicate, the supposed on-taxi test is the Knockdown animation lockout — set on exactly one of 208 AnimationData rows — and the "charm bit" is the far-sight latch) and added three verified health gates we lacked: the jump IS health-gated, sit and stand are refused while the body reads dead (a feigner too), and a follow tears down only when BOTH predicates drop — we had that wrong in both directions
- the walk-gait cross-check folds back confirming `min(walk, run)` and the speed-only animation channel (by a 2,304-function closure census), and finds the trap we lacked: every incoming spline re-authors the gait, and the spline's RUNMODE flag is an INVERSE — a spline marked as a run clears walk mode, one not so marked SETS it — so a charge, taxi, knockback or flee hand-off leaves the body in whatever gait its path was authored with, applied once on the spline's start edge; the post-worldport zero-walk fallback is retracted on a re-run (the ctor's zero is a transient thirty instructions wide, never observable), leaving the plain minimum the bytes compute — the better failure mode too, since a fallback makes a walker silently RUN if a walk speed ever goes missing, where the transcription makes them visibly stop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:18:16 +07:00

231 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The celestial draw-order ladder — the reference's **fixed sky pass order**, expressed as
//! `Transparent3d` sort biases (wow-re `celestial-frame-anatomy`, §5-verified off the binary).
//!
//! The real client draws the frame `sky → opaque world → weather → glare`, and inside its sky pass
//! (`CSky::Render 0x6d4940`, one squashed depth slice `[0.975, 0.98]`, depth-write off, painter's
//! order): **stars → sun disc → white moon → moon02 → gradient strip (additive) → cloud dome** —
//! so the clouds composite over a setting sun, rain falls in front of the whole sky, and the glare
//! quads (their own back slice `[0.995, 1.0]`, the frame's LAST draw) paint over everything the
//! z-buffer leaves visible.
//!
//! Bevy instead sorts every transparent by `view-z of the mesh center + depth_bias`, drawn
//! back-to-front — and our celestial entities are camera-anchored, so their raw distances are
//! sort-order accidents (the camera-centred cloud dome sat at ~0 = drawn LAST, painting clouds
//! over rain and over the glare — backwards on both counts). These biases turn the accident back
//! into the reference's fixed order: spaced far below any real in-scene view-z (≳ −far plane, a
//! few ×10³) so world distances can never reorder the sky and every world transparent draws over
//! it, with the glare *above* zero so it draws after the world and the rain — but still below
//! [`Rung::NAMEPLATE`], which keeps world text on top of the flare (the
//! reference draws its text later still).
//!
//! Precipitation deliberately has NO bias: an unbiased view-z of a few tens of units lands rain
//! after the biased sky and before the glare — the reference's `weather` slot — without pinning
//! rain-vs-world-transparent order we have no byte law for.
//!
//! ## Which way the bias points — the sign, read off Bevy 0.18 (never guessed again)
//!
//! Every rung below is a *signed* number in one direction, and the direction was inverted here for
//! a week (decision 0639 — the whole ladder ran upside down; names sorted UNDER the water that
//! erased them). Verified in the bevy 0.18.1 sources, so the next edit can check rather than
//! recall:
//!
//! - `ViewRangefinder3d::distance` (`bevy_render/src/render_phase/rangefinder.rs`) returns the
//! **view-space z** of the point — *negative* in front of the camera, and MORE negative the
//! farther away (its own unit test puts points behind the eye at `+1`/`+2`).
//! - `Transparent3d` sorts **ascending** on that value (`bevy_core_pipeline/src/core_3d/mod.rs`:
//! "Values increase towards the camera. Back-to-front ordering for transparent means we need an
//! ascending sort") — so the first item drawn is the most negative = the farthest.
//! - The queue adds the material's bias to it (`bevy_pbr/src/material.rs`,
//! `RenderPhaseType::Transparent`: `rangefinder.distance(&center) + depth_bias`).
//!
//! ⇒ **A POSITIVE bias draws LATER (on top); a NEGATIVE bias draws EARLIER (behind).** Bevy's own
//! `StandardMaterial::depth_bias` doc says it in words: "a positive depth bias will render closer
//! to the camera while negative values cause the material to render behind other objects".
//!
//! The magnitude is not free, because the same field is *also* the rasterizer's depth bias
//! (`bevy_pbr/src/pbr_material.rs` packs it into the pipeline key → `DepthBiasState::constant`,
//! applied in depth ULPs ≈ 2⁻²³ *relative*, the same knob [`crate::target::ring`] uses at 8192 to
//! beat coplanar noise). It is harmless for the sky rungs (every sky fragment overwrites its own
//! depth, below) but it perturbs the depth TEST of anything depth-tested, so the world-side rungs
//! stay as small as their ordering job allows.
//!
//! ## The depth law — every sky fragment forces the far depth
//!
//! The biases above order the sky *against itself*. What orders it against the **world** is depth,
//! and there the reference gives one rule for the whole pass: the sky draws FIRST, in a squashed
//! back slice (`[0.975, 0.98]`, the glare further back at `[0.995, 1.0]`), depth-write off — so the
//! opaque world simply paints over it, and no sky element can ever land in front of world geometry.
//!
//! Our sky draws *after* the world (Bevy's transparent pass), so the depth **test** has to do that
//! job — which it only does if the sky's depth is behind everything. Each shell used to rely on its
//! own camera-anchored radius for that (`far·0.85` discs, `far·0.87` clouds, `far·0.88` stars,
//! `far·0.9` gradient dome), on the assumption that world geometry is always nearer. **It isn't:**
//! the WDL horizon ring ([`crate::wdl`]) streams ±5 tiles ≈ 2.9 km and is drawn out to the far plane
//! (3 km), so distant hills land in a band *behind* every shell — and stars, clouds and discs then
//! passed the depth test in front of terrain the reference would have occluded them with (the
//! sighting: stars showing *through* a fogged mountain range at night, decision 0588).
//!
//! So every sky fragment now writes `SKY_FAR_DEPTH = 0.0` — reverse-Z "infinitely far" — under
//! Bevy's `GreaterEqual` test (`sky.wgsl`, `star.wgsl`, `cloud.wgsl`, `celestial.wgsl`, which
//! already did it for the glare). A sky element survives only where the depth buffer still holds
//! the clear value: exactly "the world paints over the sky", independent of any shell radius. The
//! shells now decide only *screen size and sky-internal parallax*, never occlusion.
/// Stars — the first celestial draw (`0x6d4a3f`): everything else in the sky paints over them, so
/// theirs is the ladder's LOWEST rung (see the sign law above).
pub(crate) const STARS_BIAS: f32 = -1.0e6;
/// **The WMO skybox** ([`crate::skybox`]) — the building-owned painted sky, which replaces this
/// whole ladder while it draws, so its rung never competes with one above. It needs a rung at all
/// because a skybox is **an ordinary M2 and not an opaque backdrop**, which is what this module
/// asserted until decision 1264: `CavernsOfTimeSky.m2` is 21 batches across four blend modes — a
/// painted cube, six additive star sheets on the cube's own faces, five alpha-blended planet cards
/// and three alpha-tested asteroid belts on rotating bones. Drawn opaque (the old lane read
/// positions, UVs and one texture and nothing else) the additive star sheet — near-white RGB whose
/// stars live in its ALPHA — paints a flat white sheet over the painted sky. That is the director's
/// "the whole ceiling is white" in Caverns of Time.
///
/// The blended half of a skybox therefore lands in the transparent pass, camera-anchored, so its
/// view-z is a few tens of yards and it would sort *after* the world. This rung sinks it under
/// every world transparent: the deepest of those is [`FAR_SIDE_BIAS`] plus a far-plane view-z,
/// ≈ −4.3e4, and the shell's own view-z rides on top of the rung — all four properties of the band
/// are machine-checked in `model_render`'s
/// `the_skybox_band_orders_its_batches_on_one_pipeline_key`. Kept as small as that job allows (the
/// magnitude rationale above), because the rung is also what the batch-order epsilon has to
/// survive: see `model_render::SKYBOX_ORDER_EPS`.
pub(crate) const WMO_SKYBOX_BIAS: f32 = -6.0e4;
/// The sun disc — second (`0x7e5b90` via `0x6d4a47`).
pub(crate) const SUN_DISC_BIAS: f32 = -8.2e5;
/// The white moon — third; where the discs cross, the moon paints over the sun.
pub(crate) const WHITE_MOON_BIAS: f32 = -8.1e5;
/// moon02 — fourth (invisible in clear weather; ordered for its weather-seed surfacing).
pub(crate) const MOON02_BIAS: f32 = -8.0e5;
/// The cloud dome — last of the sky pass (`0x6d4a71`): clouds blend over a setting sun.
pub(crate) const CLOUDS_BIAS: f32 = -6.0e5;
/// **Far-side model transparents** — the water-plane interleave's early half (byte-VERIFIED,
/// wow-re `water-frame-straddle.md`): the reference splits M2 transparents into an above-water
/// and a below-water list per model (per **emitter** for particles — each classifies onto exactly
/// ONE list, `0x7084a0`), and `0x483460` draws the list on the eye's FAR side of the water plane
/// *before* the water pass, the near side *after*. A draw classified far-side takes this rung —
/// an effect plus its owner-last rung (capped at 32), a translucent M2 mesh batch plus its batch
/// eps (≤ ~65 × 1e-3, `model_render::BATCH_ORDER_SORT_EPS`), a zfill twin minus its 8 — so it
/// lands under [`WATER_BIAS`] with the owner-last law intact inside the band; near-side draws
/// keep their natural slot above it. The flip on submersion is the interleave inversion
/// (`0x4836d6 cmp eax,0xf`). Sort-only on every lane: the effect stream splits sort from raster
/// by construction, and the mesh lane's far twin zeroes the raster constant back in
/// `WowModelExt::specialize` (`clutter_fade.z` bit 11).
pub(crate) const FAR_SIDE_BIAS: f32 = -4.0e4;
/// **The water surface** — river/ocean/WMO water draw in a fixed frame slot between the two
/// transparent halves (`0x6701d0 → 0x6816d0`: ocean → river → WMO liquid → foam), never view-z
/// sorted against model transparents. One rung below the world band puts every unclassified
/// world transparent after the water (the reference's near-side default) and the far-side
/// effects before it. Magnitude: must clear the far plane (a few ×10³) so no world view-z can
/// reorder a water chunk across it, while staying small as a rasterizer bias (this one rides a
/// `StandardMaterial` base, so it IS also a depth-test bias — ~0.24% depth pull, half of what
/// the nameplates ship at +4e4; the water-noon/name-water capture diffs are the shoreline check).
pub(crate) const WATER_BIAS: f32 = -2.0e4;
/// The sun/moon glare quads — the frame's last render (`0x483740` tail): over the clouds and the
/// rain, under the nameplates; the z-buffer (their forced far depth, `celestial.wgsl`) is what
/// occludes them. Only ~6× the far plane, not ~10⁵: a rung this far up is also a rasterizer bias
/// (the sign law above), and nothing is gained by making it bigger than the ordering needs.
pub(crate) const GLARE_BIAS: f32 = 2.0e4;
/// **The world-side rungs** — the biases lanes *outside* this module apply to their own draw, and
/// the reason they are here rather than each next to its lane: they are one ladder. 1163 found the
/// four of them scattered across `blob_shadow`, `ground_fx`, `nameplates` and `target::ring`, each
/// documenting itself by pointing at one of the others ("the same constant, for the same reason,
/// as the ring's"), which is a ladder held together by prose. The compile-time assert below can
/// only check an order it can see.
///
/// Named as one type so a lane reaches the ladder, not a constant: `Rung::GROUND_FX`.
pub struct Rung;
impl Rung {
/// The **surface-decal** rung — sort bias AND rasterizer depth-bias constant, one number for
/// both roles. Projected decal vertices are geometrically coplanar with the drawn ground, so
/// without a bias the depth test dissolves into per-pixel f32 noise (stipple, view-dependent
/// bites); 8192 · 2⁻²³ pushes their depth ~1e-3·distance toward the camera — far above the
/// coplanar noise floor (an ulp of Elwynn's 10⁴-yard coords), far below anything visible.
/// Geometric lifts (0.1, 0.02) read as hovering at grazing angles — director-confirmed. The
/// reference fights the same coplanarity with polygon offset (trace-verified), the
/// fixed-function twin of this bias.
pub const GROUND_FX: f32 = 8192.0;
/// The **selection ring**, the same rung and the same rationale: ties against the ground-fx
/// decal do not matter — all of these write no depth, so the bias only ever competes with the
/// opaque ground.
pub const RING: f32 = 8192.0;
/// The **blob shadow**'s sort rung: half the ring's, so where both decals stack the ring draws
/// later deterministically.
pub const SHADOW_SORT: f32 = 4096.0;
/// The blob shadow's **rasterizer** margin, sized independently of its sort rung (B131 split
/// one constant into its two roles). It funds the `GreaterEqual` tie against the drawn ground
/// (`DECAL_WORLD_CLIP`, 0781). The reference's mechanism is the same class: `shadowBias` cvar
/// 0.1 → a constant-only polygon offset (−102.4 LSBs of its 24-bit buffer; no slope term
/// exists in the binary — wow-re unit-blob-shadow RE, corroborated across every shadow draw of
/// four apitraces). The *size* is ours: our residual is 0781's — the decal's CPU-baked world
/// verts vs the receiver's GPU-transformed verts diverge by ~1–3 ulps of the world coordinate,
/// millimetres at city magnitudes, landing on the depth tie where the receiver is *sloped*
/// (the confirmed B131 flicker walk was the Stormwind gate ramp). 4096 absorbed under half the
/// 3-ulp worst case at close zoom and lost the tie while moving; 32768 dominates it ≥2× at
/// every zoom ≥1 yd and stays centimetre-order at 30 yd. Sizing pinned by
/// `raised_bias_dominates_the_bake_residual` (particles/render.rs).
pub const SHADOW_RASTER: i32 = 32768;
/// **World text** — above the celestial glare, so a flare never washes a nameplate, and above
/// every sky rung. The reference draws its world text late in the frame (decision 0519).
/// Small on purpose: 6× the far plane is all the ordering needs, and this same field doubles
/// as the rasterizer depth bias on a layer that is depth-TESTED (walls must keep occluding
/// names). The sign was inverted here once and the water erased the glyphs (decision 0639).
pub const NAMEPLATE: f32 = 4.0e4;
}
/// The ladder IS the reference order — checked at compile time: monotonic through the sky pass,
/// every sky rung below the world (so world transparents draw over it) and below the world-side
/// decal rung, then rain (unbiased view-z, ≈ 0), the glare above it, and the nameplates above the
/// glare so text stays readable through a flare. Rung gaps stay wider than any view-z that could
/// reorder them: > 10⁴ inside the sky (camera-anchored shells spread ±far·0.85 ≈ 2.6e3), and
/// > 10⁴ on the world side (> the far plane, a few ×10³).
const _: () = {
assert!(SUN_DISC_BIAS - STARS_BIAS > 1.0e4);
assert!(WHITE_MOON_BIAS > SUN_DISC_BIAS && MOON02_BIAS > WHITE_MOON_BIAS);
assert!(CLOUDS_BIAS - MOON02_BIAS > 1.0e4);
// The water-plane interleave: sky < far-side transparents < water < world transparents (the
// near-side default). The far-side band is FAR_SIDE_BIAS − 8 (a zfill twin) … + owner rung;
// owner rungs are capped well under the 1e4 margin (benilla_formats::owner_last_rung's
// ceiling) and the mesh batch eps under that.
assert!(FAR_SIDE_BIAS - CLOUDS_BIAS > 1.0e4);
assert!(WATER_BIAS - FAR_SIDE_BIAS > 1.0e4);
assert!(-3.0e3 - WATER_BIAS > 1.0e4); // world view-z floor = −far (the ~3 km projection) stays above
assert!(Rung::GROUND_FX - CLOUDS_BIAS > 1.0e4);
assert!(GLARE_BIAS - Rung::GROUND_FX > 1.0e4);
assert!(Rung::NAMEPLATE - GLARE_BIAS > 1.0e4);
// The two decal rungs stack deterministically where both draw, and the shadow's raster margin
// is its own axis (B131) — not comparable to the sort rungs, only to zero.
assert!(Rung::RING > Rung::SHADOW_SORT && Rung::SHADOW_RASTER > 0);
};
/// The depth law (module doc) is a property of the **shaders**, so it is checked there: every sky
/// fragment shader must force `SKY_FAR_DEPTH`. Without this, a shell radius silently becomes
/// load-bearing again the moment someone edits one of them — the exact regression 0588 fixed, and
/// one that only shows up at night, on a mountainous horizon, past 2.6 km.
#[test]
fn every_sky_shader_forces_the_far_depth() {
for (name, src) in [
("sky.wgsl", include_str!("shaders/sky.wgsl")),
("star.wgsl", include_str!("shaders/star.wgsl")),
("cloud.wgsl", include_str!("shaders/cloud.wgsl")),
("celestial.wgsl", include_str!("shaders/celestial.wgsl")),
// The WMO skybox is no longer a shader of its own: it draws on the shared model lane, whose
// `WOW_SKY_DEPTH` branch obeys the same law and is asserted beside it
// (`benilla_assets::materials`, `the_sky_lane_forces_the_far_depth`).
] {
assert!(
src.contains("const SKY_FAR_DEPTH: f32 = 0.0;"),
"{name}: the sky pass's forced-far-depth constant is gone"
);
assert!(
src.contains("out.depth = SKY_FAR_DEPTH;"),
"{name}: a sky fragment no longer forces the far depth — its shell radius is deciding \
occlusion again (sky_order.rs, \"The depth law\")"
);
}
}