diff --git a/crates/benilla-assets/src/shaders/wow_model.wgsl b/crates/benilla-assets/src/shaders/wow_model.wgsl index 744502bb..d4810031 100644 --- a/crates/benilla-assets/src/shaders/wow_model.wgsl +++ b/crates/benilla-assets/src/shaders/wow_model.wgsl @@ -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 diff --git a/crates/benilla-world/src/mesh_tag.rs b/crates/benilla-world/src/mesh_tag.rs index 85339128..d6ddc327 100644 --- a/crates/benilla-world/src/mesh_tag.rs +++ b/crates/benilla-world/src/mesh_tag.rs @@ -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); diff --git a/crates/benilla-world/src/model_render.rs b/crates/benilla-world/src/model_render.rs index 7dc4bcfc..a521b40d 100644 --- a/crates/benilla-world/src/model_render.rs +++ b/crates/benilla-world/src/model_render.rs @@ -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, +) -> Handle { + 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, + texture: Option>, + 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>, + rgb_anim: Option<&std::sync::Arc>, + wmo_class: Option, + 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, +) -> Handle { + 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, + texture: Option>, + 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>, + rgb_anim: Option<&std::sync::Arc>, + wmo_class: Option, + 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, + foliage_wind: bool, ) -> Handle { 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, diff --git a/crates/benilla-world/src/static_gx/cull.rs b/crates/benilla-world/src/static_gx/cull.rs index 8db109b1..814bb594 100644 --- a/crates/benilla-world/src/static_gx/cull.rs +++ b/crates/benilla-world/src/static_gx/cull.rs @@ -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, diff --git a/crates/benilla-world/src/static_gx/mod.rs b/crates/benilla-world/src/static_gx/mod.rs index e860e688..f61d4cd4 100644 --- a/crates/benilla-world/src/static_gx/mod.rs +++ b/crates/benilla-world/src/static_gx/mod.rs @@ -215,8 +215,6 @@ struct GxFaderBatch { blend: Handle, blend_mode: ModelBlend, geometry: Arc, - /// 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; } diff --git a/crates/benilla-world/src/terrain_stream/spawn/assemble.rs b/crates/benilla-world/src/terrain_stream/spawn/assemble.rs index fa58849d..8327b925 100644 --- a/crates/benilla-world/src/terrain_stream/spawn/assemble.rs +++ b/crates/benilla-world/src/terrain_stream/spawn/assemble.rs @@ -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())