- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
517 lines
38 KiB
Rust
517 lines
38 KiB
Rust
//! 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(¢er) + 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 vertex pins 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 VERTEX pins its clip z to `SKY_FAR_CLIP_Z = 0.0` — reverse-Z "infinitely far" —
|
||
//! under Bevy's `GreaterEqual` test, in the one vertex stage every sky material shares
|
||
//! (`sky_vertex.wgsl`, [`SKY_VERTEX_SHADER`]; `sky.wgsl`, `star.wgsl`, `cloud.wgsl` and
|
||
//! `celestial.wgsl` are fragment stages that write colour only). Clip z = 0 interpolates to NDC
|
||
//! depth 0 for every fragment whatever w is, so 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.
|
||
//!
|
||
//! It was a `@builtin(frag_depth)` write in each fragment shader until decision 2016. Same number,
|
||
//! wrong stage: a pipeline whose fragment decides its own depth cannot be early-Z rejected, so
|
||
//! every dome fragment under a hill, a wall or a leaf was shaded in full and discarded late — a
|
||
//! full-screen gradient (and the cloud dome, and the star patches) paid for the covered fraction
|
||
//! of the screen on every frame. Pinned at the vertex the rasterizer rejects them first. The
|
||
//! rasterizer's `DepthBiasState` constant then has to be zero on these pipelines
|
||
//! ([`sky_pipeline_state`]): the rung each sky material carries in `depth_bias` is a SORT key,
|
||
//! and the base `StandardMaterial` packs the same f32 into the bias constant — harmless while
|
||
//! the fragment overwrote its depth, a perturbation of the pinned zero once it doesn't.
|
||
|
||
use bevy::render::render_resource::RenderPipelineDescriptor;
|
||
|
||
/// The one vertex stage every sky material draws through — the far-depth pin (module doc, "The
|
||
/// depth law"). A sky `MaterialExtension` returns this from `vertex_shader()`; the test below
|
||
/// holds each of them to it.
|
||
pub(crate) const SKY_VERTEX_SHADER: &str = "embedded://benilla_world/shaders/sky_vertex.wgsl";
|
||
|
||
/// The pipeline state every sky material's `specialize` applies (module doc, "The depth law"):
|
||
/// the rasterizer depth-bias constant back to zero, so the sort rung the material carries in
|
||
/// `StandardMaterial::depth_bias` stays sort-only and the vertex stage's pinned far depth reaches
|
||
/// the depth test untouched. Same split the model lane makes for its far-side and skybox twins
|
||
/// (`benilla_assets::materials`, `WowModelExt::specialize`).
|
||
pub(crate) fn sky_pipeline_state(descriptor: &mut RenderPipelineDescriptor) {
|
||
if let Some(ds) = descriptor.depth_stencil.as_mut() {
|
||
ds.bias.constant = 0;
|
||
}
|
||
}
|
||
|
||
/// 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 water pass's own foam** — `CWater0Ripple`'s wade wake / standing ring, which the reference
|
||
/// draws INSIDE the water group and AFTER every liquid queue has drained (`0x6816d0`: ocean → river
|
||
/// → WMO liquid → foam). So foam is unconditionally over every water surface in the frame and still
|
||
/// under the near-side world transparents that follow the pass.
|
||
///
|
||
/// It rode [`WATER_BIAS`]` + 1.0` until B348: `+1` is a *coplanar* tie-break, and the thing it has to
|
||
/// beat is not one surface but the whole water band. A water chunk's key is `WATER_BIAS + its own
|
||
/// view-z`, so the chunk one lattice over — 33 yd nearer the eye — outranks a foam patch by ~33,
|
||
/// thirty times the tie-break, and paints over it: the director's "the wake goes dull, like another
|
||
/// layer without the effect is over it". A rung, not an epsilon, is the fix, and the window is
|
||
/// arithmetic: above every water key (`WATER_BIAS` + 0) even from the far plane, below every
|
||
/// unbiased near-side transparent (view-z ≥ [`WORLD_VIEW_Z_FLOOR`]) even at the eye — i.e.
|
||
/// `(WATER_BIAS − WORLD_VIEW_Z_FLOOR, WORLD_VIEW_Z_FLOOR)` = `(−1.7e4, −3e3)`, taken centred with
|
||
/// 7e3 of margin at each end. Both ends are asserted below; the blanket 1e4 does not fit a 1.4e4
|
||
/// window and is not claimed (the decal band's precedent).
|
||
pub(crate) const FOAM_BIAS: f32 = -1.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 pinned far depth, `sky_vertex.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 floor of any world draw's view-z: the projection's ~3 km far plane, negated (the sign law
|
||
/// above — view-z is negative in front of the eye, more negative the farther away). Two rungs
|
||
/// order two draws *unconditionally* only when their gap exceeds this, so it is the unit the
|
||
/// assert block measures the ladder's margins in.
|
||
pub(crate) const WORLD_VIEW_Z_FLOOR: f32 = -3.0e3;
|
||
|
||
/// **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 — and 1163 left two out of view: the footprint print and the
|
||
/// ground-target reticle each kept a private constant in its own lane, copied from a neighbour's
|
||
/// number by the same prose 1163 came to end. Both are rungs now (B347).
|
||
///
|
||
/// Named as one type so a lane reaches the ladder, not a constant: `Rung::GROUND_FX`.
|
||
pub struct Rung;
|
||
|
||
impl Rung {
|
||
/// The **coplanarity** rung — sort bias AND rasterizer depth-bias constant, one number for
|
||
/// both roles, and the home of the rationale the whole decal family shares. 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.
|
||
///
|
||
/// It stays a *sort* rung only for **ground-fx spell decals**, which have no reference frame
|
||
/// slot to take: the real client draws those quads as ordinary M2 batches with no ground
|
||
/// conform anywhere in the spell-visual chain (`ground_fx`'s header) — conforming them is our
|
||
/// own improvement, so "wherever that M2 transparent sorts" is the honest answer and the
|
||
/// near-side default is after the water. Everything with a *known* slot moved to the
|
||
/// pre-water band below (B347).
|
||
pub const GROUND_FX: f32 = 8192.0;
|
||
/// **The one rasterizer settle every ground decal shares** — every client of the shared
|
||
/// surface-decal projector (`decal.rs`): the selection ring, the unit blob shadow, spell
|
||
/// ground-fx quads, the footprints and the ground-target reticle. It funds the `GreaterEqual`
|
||
/// tie against the drawn ground (`DECAL_WORLD_CLIP`, 0781).
|
||
///
|
||
/// **It used to be two constants, and the split was wrong** (1817, superseding B131's
|
||
/// `SHADOW_RASTER`): the ring and reticle rode +8192 on the claim that "the blob shadow is the
|
||
/// one lane that needs more". The binary groups them the other way — the blob shadow reaches
|
||
/// the SAME EGxRs id-`0` arming site as the ring, corpse marker, click marker and sky decal
|
||
/// (`0x6d7920` → `0x6d8047 call 0x6d7330` → `0x6d6fa0` → `0x6d7480`), and it is the
|
||
/// **footprints** that are a lane of their own (`0x69a54a` in `0x69a3e0`), at 10× (wow-re
|
||
/// `gx/scratch/decal-depth-bias-lanes.md`, §5 five-way). And our own measurement agrees with
|
||
/// the grouping: the residual is 0781's bake error — 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 — which is a property of the bake, identical on every lane that uses it.
|
||
/// `ground_fx` was a third value again, and not even a raster one: it passed `GROUND_FX as
|
||
/// i32`, a **sort** rung borrowed for a rasterizer field because the numbers coincided — 1806's
|
||
/// conflation, in the one place it had survived.
|
||
/// So one number, and it is the measured one: **the residual is 2.59× the +4096 margin and
|
||
/// therefore 1.30× the +8192 one**, i.e. the ring and the reticle were riding *under* their own
|
||
/// conflict and had simply not been walked on a slope yet. Pinned by
|
||
/// `raised_bias_dominates_the_bake_residual` (particles/render.rs), which now asserts both ends.
|
||
///
|
||
/// **What is deliberately NOT ported: the reference's magnitudes, or its 1 : 10 : 20 ratio.**
|
||
/// Both are properties of a 24-bit *fixed-point forward-Z* buffer at near 0.1 / far 777 /
|
||
/// depth-range [0, 0.94], where a constant normalized-depth bias is worth `k · d²` yards —
|
||
/// 0.0065 yd at 10 yd on the ring lane, 0.16 yd at 50. Ours is reversed-Z **float**, where the
|
||
/// bias unit is the ULP at the primitive's own depth, so the same constant is worth `z · C ·
|
||
/// 2⁻²³` — **linear**, not quadratic. Neither profile converts into the other, and the quantity
|
||
/// that sets ours is a millimetre-scale bake residual that does not grow with distance at all.
|
||
/// The reference's 10× footprint step has no counterpart here for the same reason: the residual
|
||
/// is the bake's, and the bake is one path.
|
||
pub const DECAL_RASTER: i32 = 32768;
|
||
/// **The pre-water decal band, rung 1: the selection ring** (and, when they are built, the
|
||
/// corpse marker and the click-to-move marker — the same `+0x30` slot and the same call site).
|
||
///
|
||
/// This rung shipped at **+8192** for months and was moved on a guess's opposite: 1785 left it
|
||
/// alone because the ring's frame slot was *unread*, and the RE it dispatched came back with a
|
||
/// slot earlier than anything guessed. `[node+0xb4] = 0x481540` → `0x4815d0` →
|
||
/// `0x48160c call [obj vt+0x38]` → `0x614ada call [obj vt+0x30]` → `0x608e00`: the ring is
|
||
/// emitted **from inside PHASE 1's own M2 node drain** (`0x6812c5 call 0x683dd0`), the same
|
||
/// loop body that then calls `0x6d78f0` for that node's blob shadow — so the family is not
|
||
/// merely before the water, it is before the footprints and before the M2 opaque pass too.
|
||
///
|
||
/// **And the ring draws UNDER the shadow, not over it.** Per node the `+0x38` tick runs first
|
||
/// (`0x48160c`) and the shadow gate second (`0x683ec3`), so the additive ring goes down and the
|
||
/// modulate shadow darkens it. This ladder asserted the opposite for as long as both rungs have
|
||
/// existed — "where both decals stack the ring draws later deterministically" was a tie-break
|
||
/// invented here, not a fact read anywhere, and it was backwards.
|
||
pub const RING: f32 = -5.4e4;
|
||
/// **The pre-water decal band, rung 2: the unit blob shadow.**
|
||
///
|
||
/// The reference draws its ground decals *before* the water, and B347 is what it costs not
|
||
/// to. The frame driver `0x483460` emits this one inside PHASE 1's opaque drain row
|
||
/// (`0x6812c5 call 0x683dd0` → `0x6d78f0` → `0x6d7920`; wow-re `unit-blob-shadow.md` Q1) —
|
||
/// in the same node-drain loop body as the ring above and immediately after it, before the
|
||
/// footprints, before the M2 opaque pass (`0x4836a6`), and long before the water surfaces,
|
||
/// which draw in phase 3 *between* the two M2 transparent passes (`water-frame-straddle.md`
|
||
/// §1). Every transparent in the world paints over a shadow there.
|
||
///
|
||
/// Here it did the exact opposite. At the old **+4096** the shadow's key (`view-z + bias`)
|
||
/// beat every world transparent — [`WATER_BIAS`], [`FAR_SIDE_BIAS`], and the unbiased
|
||
/// near-side default — making it the one thing in the scene the water did not attenuate: a
|
||
/// crocolisk's blob read through a Stranglethorn river that hid the crocolisk itself, and the
|
||
/// player's read through the Stormwind mage-tower portal (B347, both shots).
|
||
///
|
||
/// **Where the band fits.** Its keys must clear the world-transparent band below it and the
|
||
/// WMO-skybox band above it, each by more than a world view-z can travel (`WORLD_VIEW_Z_FLOOR`
|
||
/// — two rungs are only separated if their gap exceeds it). The deepest world transparent
|
||
/// sorts at `FAR_SIDE_BIAS − 8 − far ≈ −4.3e4`; the shallowest skybox batch at
|
||
/// `WMO_SKYBOX_BIAS − 0.09 ≈ −6.0e4` — a window of `(−5.7e4, −4.3e4)`, which the four rungs
|
||
/// take 2e3 apart, clearing the skybox by 3e3 below and the world band by ~5e3 above. The
|
||
/// assert block below is the
|
||
/// check, and it is arithmetic on the neighbours rather than a blanket 1e4: the window is only
|
||
/// 1.4e4 wide, so the ladder's usual margin does not fit and pretending it does would be the
|
||
/// lie. **The depth of the rung costs nothing at the rasterizer** — unlike [`WATER_BIAS`] and
|
||
/// the two rungs above, the effect stream carries sort and raster as separate fields
|
||
/// ([`DECAL_RASTER`](Rung::DECAL_RASTER) is this lane's other half).
|
||
pub const SHADOW_SORT: f32 = -5.2e4;
|
||
/// **The pre-water band, rung 3: footprints** — the reference draws them at
|
||
/// `0x483654` (`0x670240` → `0x69a3e0`), one slot AFTER the shadow pass and still before the
|
||
/// M2 opaque pass, so a print paints over a shadow rather than under it. That was inverted
|
||
/// here: the print rode +2048 *below* the shadow's +4096, on a comment calling the reference's
|
||
/// frame order an open RE item — `water-frame-straddle.md` §1 had already closed it.
|
||
pub const FOOTPRINT: f32 = -5.0e4;
|
||
/// **The pre-water band, rung 4: the ground-target reticle** — the reference's solid-receiver
|
||
/// pass (`0x4836c5`, flags `0x200122`) is the last decal before the water. Its *liquid* pass
|
||
/// (`0x483727`, flags `0xf0000`) is the family's one genuinely post-water draw, and it has no
|
||
/// counterpart here: liquid is not a `GroundDecalSurface` yet, so nothing we project can land
|
||
/// on a water surface at all. When it can, that half wants a rung above [`WATER_BIAS`] — not
|
||
/// this one.
|
||
pub const RETICLE: f32 = -4.8e4;
|
||
/// **The wade-foam decal's rasterizer settle.** `0x68fd0f` arms EGxRs id `0` for the foam draw
|
||
/// (`0x68fae0`), which is the client's third and largest ground-decal bias lane.
|
||
///
|
||
/// **The shipped value is `D3DRS_DEPTHBIAS = −1.2207217514514923e−04`** =
|
||
/// `−f32(footstepBias(0.125) × [0x810390])` — **−2048.03 ULPs** of the reference's 24-bit
|
||
/// buffer, 20× the ring/shadow lane and 2× the footprints' (VERIFIED, wow-re
|
||
/// `gx/scratch/decal-depth-bias-lanes.md`, §5 five-way, commit `6335d165`).
|
||
///
|
||
/// **1809's `−8.0001` and its `factor = −4.0` were both dead code.** `gxApi` defaults to
|
||
/// `"direct3d"` (`0x63a81d`) and no shipped `WTF` overrides it, so `GxDevCreate` builds
|
||
/// `CGxDeviceD3d` and the OpenGL arm — the only place in the image where the `−4.0` slope
|
||
/// factor exists — never runs. The GL arm also hard-codes a `×65536` *16-bit* ULP conversion,
|
||
/// which is why every number derived from it came out **256× too small**. Measured from a
|
||
/// fully independent state: `factor = 0` on 2485 of 2485 client-originated `glPolygonOffset`
|
||
/// calls across four captures, which WoW's GL arm cannot emit.
|
||
///
|
||
/// **The signs invert here.** Reference depth runs 0 = near, so its negative offset pulls
|
||
/// toward the eye; ours is reversed-Z (`CompareFunction::GreaterEqual`, near = 1), where toward
|
||
/// the eye is *larger* depth — so a ported term is positive.
|
||
///
|
||
/// **We spend almost none of it, on purpose.** Our constant is in ULPs of a **float** buffer
|
||
/// (per-primitive — the quantisation 1806 is about), so its world pull is `z · C · 2⁻²³`:
|
||
/// ~1 µm per yard of view distance at `C = 8`. The reference's is `1.29847e−3 · d²` — 0.13 yd
|
||
/// at 10 yd, 0.52 at 20, 3.2 at 50 — and wow-re **measured, from framebuffer pixels on two
|
||
/// Northshire wade frames, that it does paint the splash decal onto dry bank**, by ~0.3–1.0 yd
|
||
/// at an 11.9-yd camera. That bleed is the thing the director reported twice (B348), so the
|
||
/// fidelity is available and deliberately not taken; the reference's own bound on it is not
|
||
/// the depth test but the texgen box plus the decal texture's alpha-0 border under CLAMP.
|
||
/// What ours funds instead is a guard against a driver rounding one pipeline's coplanar
|
||
/// arithmetic differently from another's — [`FOAM_RASTER_SLOPE`](Rung::FOAM_RASTER_SLOPE) has
|
||
/// the reason nothing more is needed.
|
||
pub const FOAM_RASTER: i32 = 8;
|
||
/// **The slope half is deliberately NOT armed — the tie it settles does not exist here** (B348
|
||
/// second round, 1811; supersedes 1809 §3's transcription).
|
||
///
|
||
/// **The reference has no slope term at all** — 1811 argued this from our own geometry and the
|
||
/// bytes then confirmed it outright: `D3DRS_SLOPESCALEDEPTHBIAS` is written nowhere in the
|
||
/// image (`0xaf` never pushed, census of all 24 `call [reg+0xe4]` sites), and the `-4.0` that
|
||
/// 1809 ported lives only in the OpenGL arm the shipped client never constructs (see
|
||
/// [`FOAM_RASTER`](Rung::FOAM_RASTER)). A re-implementation that ports it is transcribing dead
|
||
/// code, and a grazing view of a ground plane is precisely where the two arms diverge most.
|
||
///
|
||
/// 1811's own reason stands underneath, and is the one that would hold even if the term were
|
||
/// live: our foam patch is not a decal *over* the liquid surface — it **is** the
|
||
/// liquid surface's own triangles: [`water_fx::build_patch`] emits the wet cells straight out
|
||
/// of [`WaterChunkInfo`]'s grid, in the liquid mesh's own winding, through the same
|
||
/// `clip_from_world` (`DECAL_WORLD_CLIP`, 0781) against a mesh whose `Transform` is
|
||
/// `IDENTITY`. Same vertices, same matrix, same arithmetic — so the depths agree exactly and
|
||
/// `GreaterEqual` passes the tie on its own. That holds for the fullbright kinds too, which
|
||
/// are the only liquids that write depth at all (magma/slime are `AlphaMode::Opaque`; water is
|
||
/// `Blend`, depth-write off, so it never competes at all).
|
||
///
|
||
/// What a slope term *does* buy, measured: a near-horizontal decal at a grazing angle has a
|
||
/// window-depth gradient of `n / (h · f)` per pixel (eye height `h` above the plane, focal
|
||
/// length `f` in pixels), so `factor · m` is a world-space pull of `factor · z² / (h · f)` —
|
||
/// **growing as the square of view distance, and the near plane cancels out**. Whole wet cells
|
||
/// are the unit of liquid geometry, so a shoreline carries a skirt of liquid-lattice triangles
|
||
/// over dry sand (up to 7 yd of it at the reported beach — `benilla-formats --example
|
||
/// water_here`), held back by nothing but the terrain having drawn first. Any pull toward the
|
||
/// eye spends that budget: at `4.0` the wake visibly washed onto the beach, which is what the
|
||
/// director reported twice.
|
||
pub const FOAM_RASTER_SLOPE: f32 = 0.0;
|
||
/// **The underwater drift cloud** — the mote field, at the reference's own frame slot
|
||
/// `0x483731`: after the water surface AND both M2 transparent passes, immediately before the
|
||
/// glare dispatch `0x483740`. The last world content in the frame (decision 1814).
|
||
///
|
||
/// The window is narrow and this rung is checked against its actual NEIGHBOURS rather than the
|
||
/// blanket 1e4, exactly as the decal band is: it has to clear
|
||
/// [`GROUND_FX`](Rung::GROUND_FX) below and [`GLARE_BIAS`] above, and those are only 1.18e4
|
||
/// apart. Centred, that leaves ~5.8e3 each side — comfortably more than the
|
||
/// [`WORLD_VIEW_Z_FLOOR`] spread that is the most two anchors here can differ by, since the
|
||
/// cloud's anchor is the eye itself and a ground-fx decal's is at most a far plane away.
|
||
///
|
||
/// One batch for the whole field, so there is no intra-cloud sort — which is right rather than
|
||
/// merely cheap: the reference submits all its survivors as **one indexed triangle list**
|
||
/// (`0x68f3c9`) in slot order, with depth WRITE off and no per-mote ordering anywhere.
|
||
pub const DRIFT_CLOUD: f32 = 1.4e4;
|
||
/// **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,
|
||
/// then the **pre-water decal band** (the reference's own decal slots, B347), then the water-plane
|
||
/// interleave, 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 — over 10⁴ inside the sky (camera-anchored shells spread ±far·0.85 ≈ 2.6e3) and
|
||
/// over 10⁴ on the world side (more than the far plane, a few ×10³) — except inside the decal
|
||
/// band, which lives in a 1.4e4 window between two bands it must not touch and is therefore
|
||
/// checked against its actual neighbours rather than the blanket.
|
||
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
|
||
// Foam sits in the water pass, over every liquid surface and under the near-side default,
|
||
// whatever the two draws' distances are (B348).
|
||
assert!(FOAM_BIAS + WORLD_VIEW_Z_FLOOR - WATER_BIAS > 1.0e3);
|
||
assert!(WORLD_VIEW_Z_FLOOR - FOAM_BIAS > 1.0e3);
|
||
assert!(Rung::GROUND_FX - CLOUDS_BIAS > 1.0e4);
|
||
assert!(GLARE_BIAS - Rung::GROUND_FX > 1.0e4);
|
||
assert!(Rung::NAMEPLATE - GLARE_BIAS > 1.0e4);
|
||
// The drift cloud sits between the ground-fx decals and the glare — the reference's `0x483731`,
|
||
// the frame's last world draw. Its window is 1.18e4 wide, so like the decal band it is measured
|
||
// against its neighbours rather than the blanket: the margin only has to beat the view-z spread
|
||
// two anchors here can differ by, and the cloud's anchor IS the eye.
|
||
assert!(Rung::DRIFT_CLOUD - Rung::GROUND_FX > -WORLD_VIEW_Z_FLOOR);
|
||
assert!(GLARE_BIAS - Rung::DRIFT_CLOUD > -WORLD_VIEW_Z_FLOOR);
|
||
// The raster margins are their own axis (B131) — not comparable to the sort rungs above, only
|
||
// to zero and to each other. One number for every ground decal (1817); the foam is not one of
|
||
// them, and takes far less.
|
||
assert!(Rung::DECAL_RASTER > 0 && Rung::FOAM_RASTER < Rung::DECAL_RASTER);
|
||
// The foam's settle is deliberately the SMALL one: unlike a ground decal its patch is the
|
||
// liquid mesh's own triangles, so it has no bake residual to dominate (1811). It pulls toward
|
||
// the eye under reversed-Z, so it is positive; a negative here would push it INTO its receiver.
|
||
assert!(Rung::FOAM_RASTER > 0);
|
||
// The slope half stays disarmed on this lane (1811): its pull grows as z² and it spends the
|
||
// wet-lattice skirt onto the beach, while the tie it would settle is already won by the patch
|
||
// being the liquid mesh's own triangles. A nonzero value here is a decision, not a tune.
|
||
assert!(Rung::FOAM_RASTER_SLOPE == 0.0);
|
||
|
||
// ─── The pre-water decal band (B347, 1785/1789) ─────────────────────────────────────────
|
||
// Internally ordered as the reference's frame emits them — ring then shadow, both from
|
||
// PHASE 1's node drain (`0x6812c5 call 0x683dd0`: the `+0x38` tick at `0x48160c`, then the
|
||
// shadow gate at `0x683ec3`) → footprints (`0x483654`) → the reticle's solid pass
|
||
// (`0x4836c5`) — with a step far wider than the view-z two *stacked* decals can differ by
|
||
// (they share a patch of ground, so their anchors are yards apart, not kilometres).
|
||
assert!(Rung::SHADOW_SORT - Rung::RING > 1.0e3);
|
||
assert!(Rung::FOOTPRINT - Rung::SHADOW_SORT > 1.0e3);
|
||
assert!(Rung::RETICLE - Rung::FOOTPRINT > 1.0e3);
|
||
// And below EVERY world transparent whatever its distance — the deepest being a far-side
|
||
// zfill twin (−8) out at the far plane. This is the assert B347 was missing: at the old
|
||
// positive rungs the band sat above the water and above every M2 transparent, and the shadow
|
||
// was the one thing in the scene the water did not attenuate.
|
||
assert!(FAR_SIDE_BIAS - 8.0 + WORLD_VIEW_Z_FLOOR - Rung::RETICLE > 1.0e3);
|
||
// ...and above the WMO-skybox band whatever the decal's distance, so a building's painted sky
|
||
// can never sort over a decal (it is depth-forced to the far plane as well, but the ladder
|
||
// does not get to lean on a second mechanism to state its own order). Measured at the band's
|
||
// FLOOR, which is the ring's rung.
|
||
assert!(Rung::RING + WORLD_VIEW_Z_FLOOR - WMO_SKYBOX_BIAS > 1.0e3);
|
||
};
|
||
|
||
/// The depth law (module doc) is a property of the **shaders**, so it is checked there: the shared
|
||
/// vertex stage must pin the far depth, every sky material must draw through it, and no sky
|
||
/// fragment shader may take the depth back (a `frag_depth` write is the same number at the cost of
|
||
/// the pipeline's early-Z — decision 2016). 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 the_sky_depth_is_pinned_at_the_vertex_and_nowhere_else() {
|
||
use bevy::pbr::Material;
|
||
use bevy::shader::ShaderRef;
|
||
|
||
let vertex = include_str!("shaders/sky_vertex.wgsl");
|
||
assert!(
|
||
vertex.contains("const SKY_FAR_CLIP_Z: f32 = 0.0;")
|
||
&& vertex.contains("out.position.z = SKY_FAR_CLIP_Z;"),
|
||
"sky_vertex.wgsl no longer pins the far depth — every shell radius is deciding occlusion \
|
||
again (sky_order.rs, \"The depth law\")"
|
||
);
|
||
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_pins_the_far_depth_at_the_vertex`).
|
||
] {
|
||
assert!(
|
||
!src.contains("@builtin(frag_depth)"),
|
||
"{name}: a sky fragment writes its depth again — the pin is the vertex stage's, and a \
|
||
fragment write costs the pipeline its early-Z (sky_order.rs, \"The depth law\")"
|
||
);
|
||
}
|
||
fn shared(name: &str, shader: ShaderRef) {
|
||
match shader {
|
||
ShaderRef::Path(p) => assert_eq!(
|
||
p.to_string(),
|
||
SKY_VERTEX_SHADER,
|
||
"{name}: not drawing through the shared sky vertex stage"
|
||
),
|
||
_ => panic!("{name}: vertex shader is not a path — not the shared sky vertex stage"),
|
||
}
|
||
}
|
||
shared(
|
||
"SkyMaterial",
|
||
<crate::sky::SkyMaterial as Material>::vertex_shader(),
|
||
);
|
||
shared(
|
||
"CloudMaterial",
|
||
<crate::clouds::CloudMaterial as Material>::vertex_shader(),
|
||
);
|
||
shared(
|
||
"StarMaterial",
|
||
<crate::sun::StarMaterial as Material>::vertex_shader(),
|
||
);
|
||
shared(
|
||
"CelestialMaterial",
|
||
<crate::sun::CelestialMaterial as Material>::vertex_shader(),
|
||
);
|
||
}
|