the cooldown goes stock, early-Z comes back, and lists reach the end
- 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>
This commit is contained in:
@@ -246,18 +246,18 @@ pub use models::{
|
||||
parse_m2_global_sequence_bones, parse_m2_lights, parse_m2_playable_animation_lookup,
|
||||
parse_m2_portrait_camera, parse_m2_render_submeshes, parse_m2_skeleton,
|
||||
parse_m2_string_anchors, parse_wmo_fogs, parse_wmo_lights, parse_wmo_portals, parse_wmo_root,
|
||||
shipped_glue_art_extent, wmo_group_doodad_refs, wmo_group_fixed_colors,
|
||||
wmo_group_footprint_tris, wmo_group_header, wmo_group_light_refs, wmo_group_liquid_mesh,
|
||||
wmo_group_raw_colors, wmo_group_submeshes, wmo_root_id, AlphaAnim, AlphaMap, AlphaSeq,
|
||||
AnimEvent, ArtExtent, BatchFootprint, Billboard, BillboardKind, BoneKeys, BoneScaleAnim,
|
||||
BoneSpin, CharSkinSlot, CollisionMesh, Coverage, CoverageReader, EmitterBoneLink, EventMarker,
|
||||
FogPolicy, FootprintTris, GlobalSeqBone, GlobalSeqChannel, GroundQuad, KeyAnim, M2AnimSummary,
|
||||
M2Attachment, M2Bounds, M2Light, M2PortraitCamera, ModelAnimation, ModelBlend, ParentArm,
|
||||
ParentBasis, PlayableAnim, RenderSubmesh, RgbAnim, ScalarAnim, SeqLoops, Skeleton,
|
||||
SkeletonBone, StringAnchors, UvAnim, WmoBatchClass, WmoDoodad, WmoDoodadSet, WmoFog,
|
||||
WmoGroupHeader, WmoGroupInfo, WmoLight, WmoPortalInfo, WmoPortalRef, WmoPortals, WmoRoot,
|
||||
ALPHA_KEY_REF, DEGENERATE_RING_FOOTPRINT, GLUE_AUTHORED_ASPECT, NO_GROUP_LIQUID,
|
||||
OWNER_RUNG_BUCKETS,
|
||||
rotation_2x2, shipped_glue_art_extent, uv_transform, wmo_group_doodad_refs,
|
||||
wmo_group_fixed_colors, wmo_group_footprint_tris, wmo_group_header, wmo_group_light_refs,
|
||||
wmo_group_liquid_mesh, wmo_group_raw_colors, wmo_group_submeshes, wmo_root_id, AlphaAnim,
|
||||
AlphaMap, AlphaSeq, AnimEvent, ArtExtent, BatchFootprint, Billboard, BillboardKind, BoneKeys,
|
||||
BoneScaleAnim, BoneSpin, CharSkinSlot, CollisionMesh, Coverage, CoverageReader,
|
||||
EmitterBoneLink, EventMarker, FogPolicy, FootprintTris, GlobalSeqBone, GlobalSeqChannel,
|
||||
GroundQuad, KeyAnim, M2AnimSummary, M2Attachment, M2Bounds, M2Light, M2PortraitCamera,
|
||||
ModelAnimation, ModelBlend, ParentArm, ParentBasis, PlayableAnim, RenderSubmesh, RgbAnim,
|
||||
ScalarAnim, SeqLoops, Skeleton, SkeletonBone, StringAnchors, UvAnim, UvRotAnim, WmoBatchClass,
|
||||
WmoDoodad, WmoDoodadSet, WmoFog, WmoGroupHeader, WmoGroupInfo, WmoLight, WmoPortalInfo,
|
||||
WmoPortalRef, WmoPortals, WmoRoot, ALPHA_KEY_REF, DEGENERATE_RING_FOOTPRINT,
|
||||
GLUE_AUTHORED_ASPECT, NO_GROUP_LIQUID, OWNER_RUNG_BUCKETS,
|
||||
};
|
||||
mod terrain;
|
||||
pub use terrain::{
|
||||
|
||||
@@ -578,6 +578,12 @@ pub fn parse_m2_render_submeshes(
|
||||
// the lane it has always taken.
|
||||
let uv_seq = tex_anim::bake_uv_seqs(model, batch.texture_transform_combo_index, &seq_slots)
|
||||
.filter(|set| set.uniform().is_none());
|
||||
// The rotation and scaling channels, per slot (decision 2019): consumed only by a lane
|
||||
// that owns its materials per instance (the UI model tiles), so they are carried whole.
|
||||
let uv_rot_seq =
|
||||
tex_anim::bake_uv_rot_seqs(model, batch.texture_transform_combo_index, &seq_slots);
|
||||
let uv_scale_seq =
|
||||
tex_anim::bake_uv_scale_seqs(model, batch.texture_transform_combo_index, &seq_slots);
|
||||
// The batch's animated RGB tint (the M2Color colour track's runtime half): only a
|
||||
// time-varying track bakes — a `Some` here *replaces* the static vertex tint below (a
|
||||
// spell effect's white-hot flash cooling to red would otherwise freeze on its first key).
|
||||
@@ -724,6 +730,8 @@ pub fn parse_m2_render_submeshes(
|
||||
sub.alpha_anim = alpha_anim.clone(); // every billboard-split group shares the batch's loops
|
||||
sub.uv_anim = uv_anim.clone();
|
||||
sub.uv_seq = uv_seq.clone();
|
||||
sub.uv_rot_seq = uv_rot_seq.clone();
|
||||
sub.uv_scale_seq = uv_scale_seq.clone();
|
||||
sub.rgb_anim = rgb_anim.clone();
|
||||
sub.rgb_seq = rgb_seq.clone();
|
||||
out.push(sub);
|
||||
|
||||
@@ -40,7 +40,7 @@ pub use key_anim::{KeyAnim, SeqLoops};
|
||||
pub use m2_batches::*;
|
||||
pub use mat_anim::{AlphaAnim, AlphaSeq, RgbAnim, ScalarAnim};
|
||||
pub use records::*;
|
||||
pub use tex_anim::UvAnim;
|
||||
pub use tex_anim::{rotation_2x2, uv_transform, UvAnim, UvRotAnim};
|
||||
pub use types::*;
|
||||
pub use wmo::*;
|
||||
|
||||
@@ -107,11 +107,13 @@ fn remap_submesh(
|
||||
alpha_anim: None, // set by the M2 path from the batch's colour/weight tracks
|
||||
uv_anim: None, // set by the M2 path from the batch's texture transform
|
||||
uv_seq: None, // …and its per-sequence set, when the slots disagree (1408)
|
||||
rgb_anim: None, // set by the M2 path from the batch's colour RGB track
|
||||
rgb_seq: None, // …ditto (1408)
|
||||
wmo_batch: None, // set by the WMO path from the MOGP batch-section counts
|
||||
env_map: false, // set by the M2 path from texture_unit_lookup[texCoordSet] > 2
|
||||
section: None, // set by the M2 path from the batch's skin_section_index
|
||||
uv_rot_seq: None,
|
||||
uv_scale_seq: None,
|
||||
rgb_anim: None, // set by the M2 path from the batch's colour RGB track
|
||||
rgb_seq: None, // …ditto (1408)
|
||||
wmo_batch: None, // set by the WMO path from the MOGP batch-section counts
|
||||
env_map: false, // set by the M2 path from texture_unit_lookup[texCoordSet] > 2
|
||||
section: None, // set by the M2 path from the batch's skin_section_index
|
||||
},
|
||||
globals,
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
//! `0xffff`/out-of-range = none) → the record's **translation** track, baked to a loopable UV-offset
|
||||
//! loop on the same clocks as the material-alpha bake (gseq wrap / seq-0 band).
|
||||
//!
|
||||
//! Translation only, deliberately: the full 7968-model 1.12 corpus sweep found 281 of 286
|
||||
//! transforms translation-keyed, and every rotation/scaling user is a UI/spell-effect/goober model
|
||||
//! — **zero placed world doodads** — so the doodad lane (0130's scope) needs no rotation/scaling.
|
||||
//! The baked offset is the track's raw x/y; how the real client maps it onto U/V (sign/axis, and
|
||||
//! the application mechanism) is under RE in wow-re — the renderer applies the convention once,
|
||||
//! when it consumes this.
|
||||
//! The translation channel is what the world's shared-material lane consumes: the full 7968-model
|
||||
//! 1.12 corpus sweep found 281 of 286 transforms translation-keyed, and every rotation/scaling
|
||||
//! user is a UI/spell-effect/goober model — **zero placed world doodads**. The rotation and
|
||||
//! scaling channels bake per file sequence slot ([`bake_uv_rot_seqs`] / [`bake_uv_scale_seqs`],
|
||||
//! decision 2019) for the lanes that own their materials per instance — the UI model tiles, whose
|
||||
//! cooldown indicator is four quadrant quads turned by their rotation tracks. The baked values are
|
||||
//! the tracks' raw numbers; the law that composes them is [`uv_transform`], and the shader is its
|
||||
//! twin.
|
||||
|
||||
use benilla_m2::M2Model;
|
||||
|
||||
@@ -26,6 +28,18 @@ impl KeyAnim<[f32; 2]> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One baked texture-transform **rotation** loop — the raw quaternion keys, component-lerped
|
||||
/// (see [`bake_uv_rot_seqs`]).
|
||||
pub type UvRotAnim = KeyAnim<[f32; 4]>;
|
||||
|
||||
impl KeyAnim<[f32; 4]> {
|
||||
/// The raw quaternion at `elapsed` seconds on the loop clock (the identity `(0, 0, 0, 1)`
|
||||
/// for a defensively-empty loop the bake never emits).
|
||||
pub fn sample(&self, elapsed: f32) -> [f32; 4] {
|
||||
self.sample_or(elapsed, [0.0, 0.0, 0.0, 1.0])
|
||||
}
|
||||
}
|
||||
|
||||
/// `(x, y)` within a hair of zero — the offset that moves nothing.
|
||||
fn is_zero(v: [f32; 2]) -> bool {
|
||||
v[0].abs() < 1e-6 && v[1].abs() < 1e-6
|
||||
@@ -90,11 +104,177 @@ pub(super) fn bake_uv_seqs(
|
||||
)
|
||||
}
|
||||
|
||||
/// A rotation quaternion within a hair of the identity `(0, 0, 0, 1)`.
|
||||
fn is_quat_identity(q: [f32; 4]) -> bool {
|
||||
q[0].abs() < 1e-6 && q[1].abs() < 1e-6 && q[2].abs() < 1e-6 && (q[3] - 1.0).abs() < 1e-6
|
||||
}
|
||||
|
||||
/// A scale within a hair of `(1, 1)`.
|
||||
fn is_scale_identity(v: [f32; 2]) -> bool {
|
||||
(v[0] - 1.0).abs() < 1e-6 && (v[1] - 1.0).abs() < 1e-6
|
||||
}
|
||||
|
||||
/// Bake the batch's texture-transform **rotation** loop per file sequence slot — the raw
|
||||
/// quaternion keys, **component-lerped and never normalised**, which is what the reference does
|
||||
/// (`0x713ea0` lerps each component and `0x7bddb0` consumes the result as is: between two 22.5°
|
||||
/// keys `|q|` dips to cos 11.25° and the block is a rotation with a slight shrink — wow-re
|
||||
/// `modelframe-texanim-and-sequence-law.md` §3.4). `None` when the transform is absent or its
|
||||
/// rotation never leaves the identity.
|
||||
pub(super) fn bake_uv_rot_seqs(
|
||||
model: &M2Model,
|
||||
combo_index: u16,
|
||||
slots: &[SeqSlot],
|
||||
) -> Option<SeqLoops<[f32; 4]>> {
|
||||
let ti = *model.texture_transform_lookup.get(combo_index as usize)?;
|
||||
let t = model.texture_transforms.get(ti as usize)?;
|
||||
SeqLoops::new(
|
||||
slots
|
||||
.iter()
|
||||
.map(|&slot| {
|
||||
bake_track(
|
||||
&t.rotation,
|
||||
&model.global_sequences,
|
||||
Some(slot),
|
||||
|q| q,
|
||||
is_quat_identity,
|
||||
is_quat_identity,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Bake the batch's texture-transform **scaling** loop per file sequence slot (`(x, y)` of the
|
||||
/// track's vec3). `None` when the transform is absent or its scale never leaves `(1, 1)`.
|
||||
pub(super) fn bake_uv_scale_seqs(
|
||||
model: &M2Model,
|
||||
combo_index: u16,
|
||||
slots: &[SeqSlot],
|
||||
) -> Option<SeqLoops<[f32; 2]>> {
|
||||
let ti = *model.texture_transform_lookup.get(combo_index as usize)?;
|
||||
let t = model.texture_transforms.get(ti as usize)?;
|
||||
SeqLoops::new(
|
||||
slots
|
||||
.iter()
|
||||
.map(|&slot| {
|
||||
bake_track(
|
||||
&t.scaling,
|
||||
&model.global_sequences,
|
||||
Some(slot),
|
||||
|v| [v[0], v[1]],
|
||||
is_scale_identity,
|
||||
is_scale_identity,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// **The texture transform's law**, from the file to the texel (wow-re
|
||||
/// `modelframe-texanim-and-sequence-law.md` §3.4, VERIFIED trio-convergent):
|
||||
///
|
||||
/// > `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`
|
||||
///
|
||||
/// — the translation is added first, the scale is applied about the pivot, then the rotation
|
||||
/// about the pivot. `R_q` is the standard active rotation of the raw (unnormalised) quaternion;
|
||||
/// for the z-only quaternions every shipped UI asset authors, `c = 1 − 2z²`, `s = 2zw`, and
|
||||
/// `+θ` turns counter-clockwise in the `u`-right / `v`-up frame. A quaternion with x/y components
|
||||
/// would turn the plane out of itself; this reads its z-rotation alone (the shipped population
|
||||
/// has none). `wow_model.wgsl`'s UV fold is this function's twin: keep the two in step.
|
||||
pub fn uv_transform(uv: [f32; 2], t: [f32; 2], q: [f32; 4], s: [f32; 2]) -> [f32; 2] {
|
||||
let (c, sn) = rotation_2x2(q);
|
||||
let dx = (uv[0] + t[0] - 0.5) * s[0];
|
||||
let dy = (uv[1] + t[1] - 0.5) * s[1];
|
||||
[0.5 + dx * c - dy * sn, 0.5 + dx * sn + dy * c]
|
||||
}
|
||||
|
||||
/// The `(cos, sin)` of a raw quaternion's z-rotation, as the reference's matrix builder yields
|
||||
/// them — `1 − 2z²`, `2zw`, unnormalised (see [`uv_transform`]).
|
||||
pub fn rotation_2x2(q: [f32; 4]) -> (f32, f32) {
|
||||
let (z, w) = (q[2], q[3]);
|
||||
(1.0 - 2.0 * z * z, 2.0 * z * w)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use benilla_m2::M2Vec3Track;
|
||||
|
||||
/// The law's handedness, on the note's own worked example: `θ = +90°` (`z = w = √½`),
|
||||
/// `p = (½, ½)`, no translation, unit scale — `(1, 0.5) ↦ (0.5, 1.0)`, a counter-clockwise
|
||||
/// turn in the `u`-right / `v`-up frame; and a `z < 0` key (every key of the cooldown model)
|
||||
/// turns the other way.
|
||||
#[test]
|
||||
fn the_uv_law_turns_counter_clockwise_for_a_positive_z() {
|
||||
let r = std::f32::consts::FRAC_1_SQRT_2;
|
||||
let got = uv_transform([1.0, 0.5], [0.0, 0.0], [0.0, 0.0, r, r], [1.0, 1.0]);
|
||||
assert!(
|
||||
(got[0] - 0.5).abs() < 1e-6 && (got[1] - 1.0).abs() < 1e-6,
|
||||
"{got:?}"
|
||||
);
|
||||
let cw = uv_transform([1.0, 0.5], [0.0, 0.0], [0.0, 0.0, -r, r], [1.0, 1.0]);
|
||||
assert!(
|
||||
(cw[0] - 0.5).abs() < 1e-6 && (cw[1] - 0.0).abs() < 1e-6,
|
||||
"{cw:?}"
|
||||
);
|
||||
// Identity in, identity out — the static path's whole population.
|
||||
let id = uv_transform([0.2, 0.7], [0.0, 0.0], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0]);
|
||||
assert!((id[0] - 0.2).abs() < 1e-6 && (id[1] - 0.7).abs() < 1e-6);
|
||||
// A translation is added BEFORE the pivoted rotation: `(uv + t − p)` turns.
|
||||
let tr = uv_transform([0.5, 0.5], [0.5, 0.0], [0.0, 0.0, r, r], [1.0, 1.0]);
|
||||
assert!(
|
||||
(tr[0] - 0.5).abs() < 1e-6 && (tr[1] - 1.0).abs() < 1e-6,
|
||||
"{tr:?}"
|
||||
);
|
||||
// The lerped, unnormalised midpoint between 0° and 45° keys shrinks: |q| < 1 ⇒ the
|
||||
// block's scale is below 1 (the note's cos 11.25° effect, at a coarser pair here).
|
||||
let (c, sn) = rotation_2x2([
|
||||
0.0,
|
||||
0.0,
|
||||
0.5 * (0.0 + (22.5f32).to_radians().sin()),
|
||||
0.5 * (1.0 + (22.5f32).to_radians().cos()),
|
||||
]);
|
||||
assert!((c * c + sn * sn).sqrt() < 1.0);
|
||||
}
|
||||
|
||||
/// The cooldown indicator's own rotation tracks, straight off the client data: transform 0's
|
||||
/// keys turn `0 → −90°` over the first 250 ms of sequence 0 (`z` from 0 to `−√½`) and hold
|
||||
/// there to the sequence's end; sequence 1's window is the identity throughout (its keys at
|
||||
/// 1167 and 2167 ms are both `(0, 0, 0, 1)`). Skips without client data.
|
||||
#[test]
|
||||
fn the_cooldown_indicators_rotation_bakes_per_sequence() {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
let mut chain = crate::open_chain(&data).expect("open chain");
|
||||
let bytes = chain
|
||||
.read_file(r"Interface\Cooldown\UI-Cooldown-Indicator.m2")
|
||||
.expect("read the cooldown indicator");
|
||||
let subs = super::super::parse_m2_render_submeshes(&bytes, "", &[]).expect("parse");
|
||||
let rot = subs[0]
|
||||
.uv_rot_seq
|
||||
.as_ref()
|
||||
.expect("the top-right quadrant's rotation loop");
|
||||
let seq0 = rot.seq(Some(0)).expect("sequence 0 keys it");
|
||||
assert!(!seq0.wrap, "sequence 0 clamps (flags 0x1)");
|
||||
let r = std::f32::consts::FRAC_1_SQRT_2;
|
||||
let at = |t: f32| seq0.sample(t);
|
||||
assert!(at(0.0)[2].abs() < 1e-5, "{:?}", at(0.0));
|
||||
assert!(
|
||||
(at(0.25)[2] + r).abs() < 1e-3,
|
||||
"−90° at 250 ms: {:?}",
|
||||
at(0.25)
|
||||
);
|
||||
assert!(
|
||||
(at(0.9)[2] + r).abs() < 1e-3,
|
||||
"held to the end: {:?}",
|
||||
at(0.9)
|
||||
);
|
||||
// The lerp between the 0 and 62 ms keys is component-wise on the raw quaternion.
|
||||
let mid = at(0.031);
|
||||
assert!(mid[2] < 0.0 && mid[2] > -r);
|
||||
assert!(subs[0].uv_scale_seq.is_none(), "no scale track");
|
||||
assert!(subs[4].uv_rot_seq.is_none(), "the star has no transform");
|
||||
}
|
||||
|
||||
fn track(gseq: u16, interp: u16, keys: &[(u32, [f32; 3])]) -> M2Vec3Track {
|
||||
M2Vec3Track {
|
||||
interp,
|
||||
|
||||
@@ -493,6 +493,15 @@ pub struct RenderSubmesh {
|
||||
/// only slot the shared-material registry can read — is a dead hold). `None` for every batch
|
||||
/// whose slots agree, which is the shared lane unchanged.
|
||||
pub uv_seq: Option<SeqLoops<[f32; 2]>>,
|
||||
/// The batch's texture-transform **rotation** loop per file sequence slot — the raw
|
||||
/// quaternion keys ([`tex_anim::bake_uv_rot_seqs`](super::tex_anim::bake_uv_rot_seqs),
|
||||
/// decision 2019), for the lanes that own a material per instance (the UI model tiles;
|
||||
/// the cooldown indicator's sweep is this channel). `None` for a transform that never
|
||||
/// rotates — every placed world doodad — and all of WMO.
|
||||
pub uv_rot_seq: Option<SeqLoops<[f32; 4]>>,
|
||||
/// The batch's texture-transform **scaling** loop per file sequence slot (`(x, y)`), on the
|
||||
/// same rule as [`Self::uv_rot_seq`].
|
||||
pub uv_scale_seq: Option<SeqLoops<[f32; 2]>>,
|
||||
/// The batch's **animated RGB tint** (the M2Color colour track, time-varying only — a spell
|
||||
/// effect's white-hot flash cooling to red): baked by [`mat_anim`](super::mat_anim). When
|
||||
/// `Some`, the static vertex-colour tint is **skipped** for this batch (the two would
|
||||
@@ -616,6 +625,8 @@ impl Default for RenderSubmesh {
|
||||
alpha_anim: None,
|
||||
uv_anim: None,
|
||||
uv_seq: None,
|
||||
uv_rot_seq: None,
|
||||
uv_scale_seq: None,
|
||||
rgb_anim: None,
|
||||
rgb_seq: None,
|
||||
wmo_batch: None,
|
||||
|
||||
Reference in New Issue
Block a user