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)
This commit is contained in:
pkuzic
2026-10-03 11:48:53 +01:00
co-authored by GPT-6 Astra (Codex)
parent fbeb0f02f6
commit f5547a63bd
3 changed files with 58 additions and 4 deletions
+9 -1
View File
@@ -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);
}
+20 -3
View File
@@ -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();
+29
View File
@@ -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();