MONKEY (fix-wind): tree exile sway rides a material marker, not MeshTag bit 18

Bit 18 aliases the top lane-weight bit and bit 12 of the interior probe slot,
so doorway units and probe-lit interior props swayed like trees. A classified
leaf batch's fade-band exile now gets marked copies of its cutout/blend
materials (clutter_fade.z bit 14, FOLIAGE_WIND_MARKER, not a key axis) and
wow_model.wgsl sways only those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-25 16:54:08 +01:00
co-authored by Claude Opus 5.5
parent 5f7237aa98
commit 57e21bfc2a
6 changed files with 197 additions and 41 deletions
@@ -1095,10 +1095,11 @@ fn vertex(vertex: WowVertex) -> WowVsOut {
);
}
#endif
// MONKEY (wind): a retained tree inside its fade band renders here. StaticGx marks only its
// alpha-tested leaf batches in MeshTag bit 18; use the same anchor/field maths to avoid a pop.
let wind_tag = mesh_functions::get_tag(vertex.instance_index);
if ((wind_tag & 262144u) != 0u && m.clutter_fade.w <= 0.5) {
// MONKEY (wind): a retained tree inside its fade band renders here with the same anchor/field
// maths, to avoid a pop. MONKEY (fix-wind): the leaf-batch marker is material bit 14
// (`model_render::FOLIAGE_WIND_MARKER`), never a MeshTag bit (bit 18 aliases the lane weight
// and the interior probe slot, so doorway units and interior props swayed).
if ((u32(m.clutter_fade.z) & 16384u) != 0u && m.clutter_fade.w <= 0.5) {
let world = p_cam + view.world_position;
p_cam += wind_hook::tree_offset(
world, frame_origin, view.world_position, wow_light.monkey
-25
View File
@@ -90,10 +90,6 @@ const SHADE_SHIFT: u32 = 6;
/// Sits in the exterior payload's reserved bits, so [`with_shade`]/[`with_alpha`]/[`with_rig`]
/// carry it through and the shader's 8-bit shade decode never sees it.
pub(crate) const MATTE_INDOOR_BIT: u32 = 0x0000_4000;
/// MONKEY (wind): bit 18 on an exterior, non-Matte static doodad exile marks a classified
/// tree/bush leaf batch. It aliases the top lane-weight bit safely: lane weight is read only with
/// [`MATTE_INDOOR_BIT`], which these exterior fade twins never carry.
const FOLIAGE_WIND_BIT: u32 = 0x0004_0000;
/// Bits 15..=18 of the exterior payload (MONKEY, portal lane fade): the **lane weight**, a 4-bit
/// crossfade `0` (fully exterior-lit) … [`LANE_MAX`] (fully room-lit) that the shader mixes the
/// two lanes by. It rides beside [`MATTE_INDOOR_BIT`] — the bit says "this part is on the room
@@ -208,16 +204,6 @@ pub fn alpha_bits(alpha: f32) -> u32 {
}
}
/// MONKEY (wind): marks or clears the fade-twin foliage receiver without disturbing its live
/// alpha. Only exterior static doodad exiles use this payload convention.
pub(crate) fn with_foliage_wind(tag: u32, on: bool) -> u32 {
if on {
tag | FOLIAGE_WIND_BIT
} else {
tag & !FOLIAGE_WIND_BIT
}
}
/// Sets or clears [`INTERIOR_FOG_BIT`] alone: the probe slot says how a prop is lit, the
/// reference's `[0xca7f00]` only which fog triple its batch gets.
pub(crate) fn with_interior_fog(tag: u32, on: bool) -> u32 {
@@ -366,17 +352,6 @@ mod tests {
assert_eq!(shade_of(t, ext()), 10);
}
// MONKEY (wind): the fade-twin marker is orthogonal to the live fade field.
#[test]
fn foliage_marker_survives_alpha_writes_and_clears_cleanly() {
let alpha = alpha_bits(0.5);
let marked = with_foliage_wind(alpha, true);
assert_eq!(marked & FOLIAGE_WIND_BIT, FOLIAGE_WIND_BIT);
assert_eq!(marked & ALPHA_MASK, alpha);
assert_eq!(with_alpha(marked, 0.25) & FOLIAGE_WIND_BIT, FOLIAGE_WIND_BIT);
assert_eq!(with_foliage_wind(marked, false), alpha);
}
#[test]
fn probe_bits_compose_with_the_alpha_field() {
let t = probe_bits(6660);
+144 -1
View File
@@ -75,6 +75,9 @@ pub struct MatKey {
window: bool,
/// A depth-prime twin ([`zfill_material`], the reference's `M2UseZFill` clone at `0x707f7d`).
zfill: bool,
/// MONKEY (fix-wind): a classified tree/bush leaf batch's exile copy, marker bit 14
/// ([`FOLIAGE_WIND_MARKER`]); `wow_model.wgsl` sways only these.
foliage_wind: bool,
/// A WMO skybox batch ([`crate::skybox`]), with its own pipeline and sort rung.
sky_depth: bool,
/// The placement owning this material when its animated loop follows the instance's sequence
@@ -154,6 +157,137 @@ pub fn model_material(
// the sequence its instance is playing (decision 1408): the animated-material registries are
// keyed by material, so such a batch cannot share one and be right. See `MatKey::instance`.
instance: Option<Entity>,
) -> Handle<WowModelMaterial> {
model_material_impl(
cache,
materials,
texture,
blend,
two_sided,
is_wmo,
is_interior,
is_emissive,
is_additive,
fade_variant,
no_depth_write,
no_depth_test,
fog_policy,
env_map,
shade,
batch_order,
uv_anim,
rgb_anim,
wmo_class,
sidn,
window,
sky_depth,
light,
torch,
instance,
false,
)
}
/// MONKEY (fix-wind): [`model_material`] for a classified tree/bush leaf batch's exile copy: the
/// same material plus [`FOLIAGE_WIND_MARKER`], so the entity draw sways like the retained batch.
#[allow(clippy::too_many_arguments)]
pub fn foliage_model_material(
cache: &mut MaterialCache,
materials: &mut Assets<WowModelMaterial>,
texture: Option<Handle<Image>>,
blend: ModelBlend,
two_sided: bool,
is_wmo: bool,
is_interior: bool,
is_emissive: bool,
is_additive: bool,
fade_variant: bool,
no_depth_write: bool,
no_depth_test: bool,
fog_policy: benilla_formats::FogPolicy,
env_map: bool,
shade: ShadeSel,
batch_order: u16,
uv_anim: Option<&std::sync::Arc<benilla_formats::UvAnim>>,
rgb_anim: Option<&std::sync::Arc<benilla_formats::RgbAnim>>,
wmo_class: Option<WmoBatchClass>,
sidn: Option<[u8; 3]>,
window: bool,
sky_depth: bool,
light: &Buffer,
// MONKEY (torch shadows Phase 3A): the shared torch depth image + table buffer, cloned into the
// material beside `light` (not a key axis — one pair for the whole scene, like the light).
torch: &TorchBinds,
// The ONE placement this material belongs to, or `None` for the shared batch material every
// instance of the model reuses. `Some` only for a batch whose animated UV/tint loop depends on
// the sequence its instance is playing (decision 1408): the animated-material registries are
// keyed by material, so such a batch cannot share one and be right. See `MatKey::instance`.
instance: Option<Entity>,
) -> Handle<WowModelMaterial> {
model_material_impl(
cache,
materials,
texture,
blend,
two_sided,
is_wmo,
is_interior,
is_emissive,
is_additive,
fade_variant,
no_depth_write,
no_depth_test,
fog_policy,
env_map,
shade,
batch_order,
uv_anim,
rgb_anim,
wmo_class,
sidn,
window,
sky_depth,
light,
torch,
instance,
true,
)
}
#[allow(clippy::too_many_arguments)]
fn model_material_impl(
cache: &mut MaterialCache,
materials: &mut Assets<WowModelMaterial>,
texture: Option<Handle<Image>>,
blend: ModelBlend,
two_sided: bool,
is_wmo: bool,
is_interior: bool,
is_emissive: bool,
is_additive: bool,
fade_variant: bool,
no_depth_write: bool,
no_depth_test: bool,
fog_policy: benilla_formats::FogPolicy,
env_map: bool,
shade: ShadeSel,
batch_order: u16,
uv_anim: Option<&std::sync::Arc<benilla_formats::UvAnim>>,
rgb_anim: Option<&std::sync::Arc<benilla_formats::RgbAnim>>,
wmo_class: Option<WmoBatchClass>,
sidn: Option<[u8; 3]>,
window: bool,
sky_depth: bool,
light: &Buffer,
// MONKEY (torch shadows Phase 3A): the shared torch depth image + table buffer, cloned into the
// material beside `light` (not a key axis — one pair for the whole scene, like the light).
torch: &TorchBinds,
// The ONE placement this material belongs to, or `None` for the shared batch material every
// instance of the model reuses. `Some` only for a batch whose animated UV/tint loop depends on
// the sequence its instance is playing (decision 1408): the animated-material registries are
// keyed by material, so such a batch cannot share one and be right. See `MatKey::instance`.
instance: Option<Entity>,
foliage_wind: bool,
) -> Handle<WowModelMaterial> {
let key = MatKey {
light: light.id(),
@@ -177,6 +311,7 @@ pub fn model_material(
sidn,
window,
zfill: false,
foliage_wind,
sky_depth,
instance,
};
@@ -263,7 +398,8 @@ pub fn model_material(
| (u16::from(blend == ModelBlend::Mod2x) << 8)
| (u16::from(fade_variant && source_cutout) * TWIN_CUTOUT_MARKER)
| (u16::from(env_map) * ENV_MAP_MARKER)
| (u16::from(sky_depth) * SKY_DEPTH_MARKER),
| (u16::from(sky_depth) * SKY_DEPTH_MARKER)
| (u16::from(foliage_wind) * FOLIAGE_WIND_MARKER),
),
0.0,
),
@@ -349,6 +485,12 @@ pub(crate) const ENV_MAP_MARKER: u16 = 1 << 12;
/// vertex with no raster bias; a `WowModelKey` axis, so every other draw keeps its real depth.
pub(crate) const SKY_DEPTH_MARKER: u16 = 1 << 13;
/// MONKEY (fix-wind): `clutter_fade.z` marker bit 14: a classified tree/bush leaf batch's exile
/// copy, the only entity draw `wow_model.wgsl` applies `tree_offset` to. Vertex-only uniform data,
/// not a `WowModelKey` axis. (It replaced MeshTag bit 18, which aliases the lane weight and the
/// interior probe slot, so doorway units and interior props swayed.)
pub(crate) const FOLIAGE_WIND_MARKER: u16 = 1 << 14;
/// [`BATCH_ORDER_SORT_EPS`] for the WMO skybox lane: at the −6e4 rung the f32 ulp is 0.0039, so
/// 1e-3 would round away; this step is 4 ulps. Every skybox batch pair ties, being eye-anchored.
pub(crate) const SKYBOX_ORDER_EPS: f32 = 0.015625;
@@ -412,6 +554,7 @@ pub fn zfill_material(
sidn: None,
window: false,
zfill: true,
foliage_wind: false,
// No skybox batch has a twin: the sky writes no depth.
sky_depth: false,
instance: None,
+2 -5
View File
@@ -100,11 +100,8 @@ fn spawn_exile(commands: &mut Commands, f: &GxFader, alpha: f32, admitted: bool)
},
crate::model_render::EntityPathWhy("exile"),
crate::interact::PickMesh(b.geometry.clone()),
// MONKEY (wind): bit 18 is the fade-twin leaf marker in exterior payload mode.
bevy::mesh::MeshTag(crate::mesh_tag::with_foliage_wind(
crate::mesh_tag::alpha_bits(alpha),
b.foliage_wind,
)),
// MONKEY (fix-wind): a leaf batch's sway marker rides its material (`FOLIAGE_WIND_MARKER`).
bevy::mesh::MeshTag(crate::mesh_tag::alpha_bits(alpha)),
crate::model_fade::DoodadFade {
radius: f.radius,
local_center: f.local_center,
+1 -4
View File
@@ -215,8 +215,6 @@ struct GxFaderBatch {
blend: Handle<benilla_assets::materials::WowModelMaterial>,
blend_mode: ModelBlend,
geometry: Arc<RenderSubmesh>,
/// MONKEY (wind): mirror the retained batch's sway while this placement fades as an entity.
foliage_wind: bool,
}
/// A fader's exile state. Steady: drawn retained at fade 1. Exiled: drawn as ordinary entities,
@@ -569,7 +567,6 @@ impl StaticGx {
blend: seed.blend,
blend_mode: b.blend,
geometry: b.geometry.clone(),
foliage_wind,
});
if is_new {
// A placement's later batches share its sphere: the caches move only on a new one.
@@ -706,7 +703,7 @@ impl StaticGx {
/// MONKEY (wind): conservative leaf classification. Alpha test excludes trunks/rocks even when a
/// whole model path says tree; the name terms cover vanilla's tree/bush/plant families. Animated
/// doodads never reach this function because `assemble.rs` excludes them before `StaticGx::divert`.
fn foliage_wind_batch(path: &str, blend: ModelBlend, wmo_geometry: bool) -> bool {
pub(crate) fn foliage_wind_batch(path: &str, blend: ModelBlend, wmo_geometry: bool) -> bool {
if wmo_geometry || blend != ModelBlend::AlphaTest {
return false;
}
@@ -233,6 +233,44 @@ pub fn spawn_model_entities(
seq_owner,
)
};
// MONKEY (fix-wind): a classified exterior tree/bush leaf batch gets marked copies of both
// materials for its fade-band exile (`FOLIAGE_WIND_MARKER`), so only those entity draws
// sway; the plain pair stays on every other path.
let wind_seed = (!interior
&& !steady_interior_prop
&& crate::static_gx::foliage_wind_batch(&object.label, sub.blend, is_wmo))
.then(|| {
let mut wind_mat = |fade: bool| {
crate::model_render::foliage_model_material(
mat_cache,
materials,
sub.texture.clone(),
sub.blend,
two_sided,
is_wmo,
interior,
sub.emissive,
sub.additive,
fade,
sub.no_depth_write,
sub.no_depth_test,
sub.fog_policy,
sub.env_map,
shade,
batch_order,
sub.uv_anim.as_ref(),
sub.rgb_anim.as_ref(),
sub.wmo_batch,
sub.sidn,
sub.window,
false,
light,
torch,
seq_owner,
)
};
(wind_mat(false), wind_mat(true))
});
// One classification for the census tally and the diverts, so the two cannot drift.
let class = crate::static_merge::BatchClass {
excluded: animated
@@ -311,8 +349,13 @@ pub fn spawn_model_entities(
local_center,
stat_mesh: stat_mesh.clone(),
aabb: *stat_aabb,
cutout: cutout.clone(),
blend: blend.clone(),
// MONKEY (fix-wind): the marked pair for a leaf batch.
cutout: wind_seed
.as_ref()
.map_or_else(|| cutout.clone(), |w| w.0.clone()),
blend: wind_seed
.as_ref()
.map_or_else(|| blend.clone(), |w| w.1.clone()),
});
// A never-fader enters bare, a fader only with its seed.
(class.never_fade || fade_seed.is_some())