From f5547a63bd3aa2a7036f790d04c268a2daddabf6 Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Sat, 3 Oct 2026 11:48:53 +0100
Subject: [PATCH] MONKEY (gfx): keep named rock doodads and ground clutter
rigid in wind
Reject rock, stone, boulder, pebble, gravel and rubble model filenames from retained foliage. Give matching ground-detail models zero bend height so neither grass wind nor player parting moves them. Limit the name veto to the filename to preserve vegetation in stone-named zones.
Verified on Windows: two foliage classifier tests and one rigid-model test pass; benilla-app lib tests have 4313 passed, 0 failed, 10 ignored (no-install run); play build passes. Elwynn wind captures at 0 and 3 seconds preserve coloured foliage motion. The tester's specific small-rock assets still need an in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
---
crates/benilla-world/src/clutter.rs | 10 +++++++-
crates/benilla-world/src/static_gx/mod.rs | 23 +++++++++++++++---
crates/benilla-world/src/wind/mod.rs | 29 +++++++++++++++++++++++
3 files changed, 58 insertions(+), 4 deletions(-)
diff --git a/crates/benilla-world/src/clutter.rs b/crates/benilla-world/src/clutter.rs
index b43113a8..8fa2cdbd 100644
--- a/crates/benilla-world/src/clutter.rs
+++ b/crates/benilla-world/src/clutter.rs
@@ -334,6 +334,9 @@ fn build_chunk_clutter(
(lo.min(p[2]), hi.max(p[2]))
});
let height_span = (max_z - min_z).max(1.0e-4);
+ // MONKEY (wind): the detail table includes stones as well as grass. Zero bend weight
+ // keeps their geometry fixed under both wind and the player-parting field.
+ let bends = !crate::wind::rigid_model(model_path);
for sub in subs.iter() {
let vcount = sub.positions.len() * placements.len();
let mut positions = Vec::with_capacity(vcount);
@@ -359,7 +362,12 @@ fn build_chunk_clutter(
for (i, p) in sub.positions.iter().enumerate() {
positions.push((rot * (wow_to_bevy(*p) * inst_scale) + origin).to_array());
uvs.push(sub.uvs[i]);
- wind.push([((p[2] - min_z) / height_span).clamp(0.0, 1.0), phase]);
+ let bend_height = if bends {
+ ((p[2] - min_z) / height_span).clamp(0.0, 1.0)
+ } else {
+ 0.0
+ };
+ wind.push([bend_height, phase]);
normals.push(sp.ground_normal);
colors.push(tint);
}
diff --git a/crates/benilla-world/src/static_gx/mod.rs b/crates/benilla-world/src/static_gx/mod.rs
index 737443b8..a9dbc73b 100644
--- a/crates/benilla-world/src/static_gx/mod.rs
+++ b/crates/benilla-world/src/static_gx/mod.rs
@@ -700,11 +700,11 @@ 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
+/// MONKEY (wind): conservative leaf classification. Alpha test alone does not exclude rock cards;
+/// rigid model names veto plant-family directories. Animated
/// doodads never reach this function because `assemble.rs` excludes them before `StaticGx::divert`.
pub(crate) fn foliage_wind_batch(path: &str, blend: ModelBlend, wmo_geometry: bool) -> bool {
- if wmo_geometry || blend != ModelBlend::AlphaTest {
+ if wmo_geometry || blend != ModelBlend::AlphaTest || crate::wind::rigid_model(path) {
return false;
}
let p = path.to_ascii_lowercase();
@@ -920,6 +920,23 @@ mod tests {
));
}
+ #[test]
+ fn foliage_classifier_vetoes_rigid_cards_in_plant_families() {
+ for path in [
+ "World\\Generic\\PassiveDoodads\\Bush\\BushRock01.m2",
+ "World\\Azeroth\\Elwynn\\PassiveDoodads\\Trees\\Stone01.m2",
+ "World/Generic/Foliage/Boulder01.m2",
+ "World/Generic/Plants/Pebble01.m2",
+ "World/Generic/Foliage/Gravel01.m2",
+ "World/Generic/Foliage/Rubble01.m2",
+ ] {
+ assert!(
+ !foliage_wind_batch(path, ModelBlend::AlphaTest, false),
+ "{path}"
+ );
+ }
+ }
+
#[test]
fn a_fader_divert_registers_its_placement_seed() {
let mut gx = StaticGx::default();
diff --git a/crates/benilla-world/src/wind/mod.rs b/crates/benilla-world/src/wind/mod.rs
index 1f88610c..9fd631b3 100644
--- a/crates/benilla-world/src/wind/mod.rs
+++ b/crates/benilla-world/src/wind/mod.rs
@@ -15,6 +15,19 @@ const TAU: f64 = std::f64::consts::TAU;
const RATIOS: [f64; 3] = [1.0, 0.437, 0.1913];
const WEIGHTS: [f64; 3] = [0.60, 0.28, 0.12];
+/// MONKEY (wind): rigid detail models can have alpha-tested cards and live in plant directories.
+/// Inspect the filename so a zone such as Stonetalon does not disable all its vegetation.
+pub(crate) fn rigid_model(path: &str) -> bool {
+ let name = path
+ .rsplit(['/', '\\'])
+ .next()
+ .unwrap_or(path)
+ .to_ascii_lowercase();
+ ["rock", "stone", "boulder", "pebble", "gravel", "rubble"]
+ .iter()
+ .any(|term| name.contains(term))
+}
+
/// The WarcraftXL wind profile. Units are yards, seconds and degrees.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindProfile {
@@ -352,6 +365,22 @@ impl Plugin for WindPlugin {
mod tests {
use super::*;
+ #[test]
+ fn rigid_details_ignore_case_and_plant_directory_names() {
+ for path in [
+ "World/Plants/BushRock01.m2",
+ "World\\Trees\\STONE01.mdx",
+ "Boulder02.m2",
+ "Pebble01.m2",
+ "Gravel01.m2",
+ "Rubble01.m2",
+ ] {
+ assert!(rigid_model(path), "{path}");
+ }
+ assert!(!rigid_model("World/Stonetalon/Trees/Pine01.m2"));
+ assert!(!rigid_model("World/RockyZone/Grass01.m2"));
+ }
+
#[test]
fn defaults_are_the_warcraft_xl_profile() {
let p = WindProfile::default();