MONKEY (wind): sway classified tree foliage

Classify static alpha-tested tree and bush leaf batches, log accepted model paths, and apply gentle anchor-phased crown sway and flutter in both retained and fade-twin render paths. Keep animated doodads and tree shadows unchanged, and add wind capture instruments.

Co-Authored-By: GPT-5.6 Sol (Codex)
This commit is contained in:
pkuzic
2026-09-25 08:06:56 +01:00
co-authored by GPT-5.6 Sol (Codex)
parent d85c0718c1
commit 0fdc8e1ad4
12 changed files with 283 additions and 12 deletions
+6
View File
@@ -25,6 +25,7 @@ behind each constant.
| Ground-effect spells | Flamestrike, Rain of Fire, Consecration, Flare and fire traps light the ground for their duration; frost and nature areas stay dark | `spellLightGain` |
| Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors) and sun/moon light shafts through gaps, sampled from the shadow map; own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` |
| Sky dithering | a faint screen-space dither in the FFXGlow combine so smooth sky and fog gradients do not band (MONKEY p0; was env-only) | Advanced Graphics → Sky Dithering, cvar `skyDither` 0/1 (default 0, Graphics preset High = 1), env `WOW_DITHER=1` still forces it on; bridge in `benilla-app/src/monkey_gfx.rs` |
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND`; field and benders in `benilla-world/src/wind/` |
| Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` |
Players reach all of it from **Options -> Advanced Graphics** (a Lighting Quality preset Off / Low / Medium / High plus the individual rows; Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default /
@@ -52,6 +53,9 @@ is its own module, documented in `WATER.md`.
`character_shadow.rs`, `world_shadow.rs`, `blob_shadow.rs`, `entities/carried_light.rs`,
`entities/spell_fx/lifecycle.rs`, `dynamic_interior.rs`, `debug_panel/lighting_controls.rs`, and
the cvars in `cvars.rs` / `video.rs`.
- **Wind**: `wind/mod.rs` resolves the WXL-derived shared field and nearest-unit benders;
`clutter.rs` authors blade weights/phases; `static_gx` classifies and marks alpha-tested leaf
batches. `wind_hook.wgsl` owns the displacement shared by clutter, retained trees and fade twins.
- **Tools**: `benilla-extract <Data> wmolights <wmo> [--verts <group>]`, `wmolamps`, `m2firescan`
print the inputs the system works from (groups, batch classes, portals, claims, flame emitters).
@@ -80,6 +84,8 @@ is its own module, documented in `WATER.md`.
(`benilla-world/src/lighting/monkey_frame.rs`); `global_light::pack_monkey_frame` packs it after
the point table and fills `time_of_day` / `night` itself. All zero = no visual change.
- World lights use upstream's `WorldPointLight`, never Bevy's `PointLight`.
- Foliage wind is vertex-only. Tree and leaf shadow meshes stay static; the small mismatch is the
accepted first-stage cost/complexity tradeoff.
- WGSL only fails at pipeline creation, so a shader edit is verified by running a world scene, not
by `cargo check`.
- Exterior point lights are selected per draw unit (12 slots, ranked against the chunk's box); the
@@ -29,6 +29,65 @@ const CHEST_GUID: u64 = (0xF110u64 << 48) | 0x744;
/// face, `rear` the tail — the same body, its lit and unlit sides.
const SUBJECT_YAW: f32 = 2.36;
/// MONKEY (wind): stand one real player in the grass-parting world capture without loading the
/// player UI. `UiFixture::Subject` intentionally opts the whole HUD in; parting needs the live
/// entity path but a pristine world framebuffer. `look` is the scenario's documented feet point.
pub(super) fn seed_wind_player(
mut commands: Commands,
ctx: Res<CaptureCtx>,
progress: Res<WorldLoadProgress>,
mut seeded: Local<bool>,
) {
let Some(scenario) = ctx.scenario else {
return;
};
if *seeded
|| scenario.name != "wind-player-parting"
|| !(progress.total > 0 && progress.ready == progress.total)
{
return;
}
*seeded = true;
use benilla_protocol::messages::ObjectFields;
commands.spawn((
crate::net::Guid(0x51),
crate::net::NetEntity {
kind: benilla_protocol::EntityKind::Player,
// HumanMale.m2; the normal entity visual path supplies the body.
display_id: Some(49),
scale: 1.0,
},
crate::net::ObjectStore(ObjectFields::from_pairs(&[
(22, 100), // UNIT_FIELD_HEALTH
(28, 100), // UNIT_FIELD_MAXHEALTH
(34, 12), // UNIT_FIELD_LEVEL
(35, 1), // UNIT_FIELD_FACTIONTEMPLATE
(36, 1 | 1 << 8), // UNIT_FIELD_BYTES_0: human warrior
])),
// A player visual waits for its equipment resolution handshake before attaching. This
// fixture is intentionally naked, so its final resolved set is known immediately.
crate::entities::Equipment {
settled: true,
..default()
},
// Mark this visible remote body as the capture's viewer. The real client publishes the
// local body through `Viewer::at`; this server-less instrument has no active Player.
benilla_world::world_unit::ViewerUnit,
benilla_world::world_unit::WorldUnit {
wades: true,
scale: 1.0,
height: crate::entities::CollisionHeight::default().0,
bound: None,
},
Transform {
translation: wow_to_bevy(scenario.look),
rotation: Quat::from_rotation_y(SUBJECT_YAW),
..default()
},
Visibility::default(),
));
}
/// Seed the fixture window's state resources once, right after the scene goes resident — the real
/// feeds then push it into the VM during the settle window exactly as live wire data would. Item
/// icons resolve through the offline `ItemDisplayCatalog` (display ids chosen from entries that
+2 -1
View File
@@ -119,7 +119,7 @@ mod probes;
mod scenarios;
use crate::run_mode::CaptureMode;
pub(crate) use depth_probe::DepthProbePlugin;
use fixtures::seed_ui_fixture;
use fixtures::{seed_ui_fixture, seed_wind_player};
pub(crate) use live_shot::LiveShotPlugin;
pub(crate) use phase_probe::PhaseProbePlugin;
pub(crate) use pick_probe::PickProbePlugin;
@@ -904,6 +904,7 @@ impl Plugin for CapturePlugin {
bailed: false,
})
.add_systems(Update, pin_scene.in_set(WorldStage::Present))
.add_systems(Update, seed_wind_player)
// Before the UnitFeed pass: the seed stands in for wire data that in live play
// filled the app caches on EARLIER frames, so the same frame's feeds (item-template
// / player-req pushes, then the merchant paint) must all see it. Unordered, the
@@ -1379,6 +1379,36 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
minute: 0,
ui: None,
},
// MONKEY wind: keep these at the END of the table. They are the three visual instruments for
// W1: authored grass weight/lean, static-tree classification/sway, and the viewer bender.
Scenario {
name: "wind-grass-elwynn",
map: Some(MAP_AZEROTH),
// Inside the grassy hill that the earlier long shot put beyond clutter's 70 yd horizon.
eye: [-9460.0, 45.0, 60.5],
look: [-9442.0, 27.0, 57.5],
minute: 720,
ui: None,
},
Scenario {
name: "wind-forest-elwynn",
map: Some(MAP_AZEROTH),
// East of Goldshire looking south-west across the lake and Elwynn forest.
eye: [-9380.0, -30.0, 80.0],
look: [-9600.0, -200.0, 62.0],
minute: 720,
ui: None,
},
Scenario {
name: "wind-player-parting",
map: Some(MAP_AZEROTH),
eye: [-9472.0, 57.0, 60.5],
look: [-9460.0, 45.0, 57.5],
minute: 720,
// The capture plugin stands a real player at `look` without opting into the HUD: this is
// a world-image instrument, and UI script errors must not cover the grass it measures.
ui: None,
},
];
/// The owner's reported vantage for the lava-glow report: `(-7048.8, -1000.6, 242.0)` facing
+7 -6
View File
@@ -311,12 +311,13 @@ pub fn run(build: BuildId) -> AppExit {
// never fights the director's screen (decision 0703; `WOW_BG` overrides). See `bgwin`.
let background = benilla_world::bgwin::background_run();
if capturing {
// Ground clutter scatters with per-run randomness, so disable it for byte-stable baselines
// — clutter isn't what the lighting rework validates, and the regression diff must not be
// masked by grass wobble. Set before plugins build so `ClutterConfig::from_env` reads it.
// It is not the only source of per-run drift, though it was long documented as such: the
// other is the frame clock itself, frozen in `capture` (decision 0723).
std::env::set_var("WOW_CLUTTER_DENSITY", "0");
// Ordinary baselines omit clutter, but a feature-specific capture may explicitly ask for
// it (MONKEY wind does). Scatter is now chunk-seeded and deterministic; the default stays
// zero so unrelated lighting diffs are not masked by grass. Set before plugins build so
// `ClutterConfig::default` reads it.
if std::env::var_os("WOW_CLUTTER_DENSITY").is_none() {
std::env::set_var("WOW_CLUTTER_DENSITY", "0");
}
// Third source of per-run drift, and the one the lighting matrix (decision 0746) hit: the
// anim-LOD park/wake gate (`creature_anim::lod::gate_rig_animation`). Whether a rig is
// parked, and which pose it wakes into, depends on when its model finished loading relative
@@ -59,3 +59,36 @@ fn grass_offset(
}
return vec3<f32>(xz.x, 0.0, xz.y);
}
// MONKEY (wind): our tree/bush design (WarcraftXL has no tree wind). Only leaf-card batches call
// this: a low-frequency crown sway, small leaf flutter, stable anchor phase and distance fade.
fn tree_offset(
world: vec3<f32>,
anchor: vec3<f32>,
camera: vec3<f32>,
frame: MonkeyFrame,
) -> vec3<f32> {
if (frame.wind1.w <= 0.0) {
return vec3<f32>(0.0);
}
let h = max(world.y - anchor.y, 0.0);
// A one-yard bush still needs a crown; tree leaf cards generally begin above this range and
// therefore reach full weight, while the first 0.15 yd remains a planted base.
let crown = smoothstep(0.15, 1.5, h);
let weight = crown * crown;
let dir = normalize(frame.wind0.xy + vec2<f32>(1.0e-6, 0.0));
let cross = vec2<f32>(-dir.y, dir.x);
let phase = fract(sin(dot(anchor.xz, vec2<f32>(0.173, 0.317))) * 43758.5453) * WIND_TAU;
let travel = frame.wind0.z * 0.08 * frame.wind1.x;
let slow = sin(dot(dir, world.xz) * (WIND_TAU / 34.0) - travel + phase);
let side = sin(dot(cross, world.xz) * (WIND_TAU / 21.0) - travel * 0.61 + phase * 1.7);
let flutter = sin(frame.wind1.x * 2.3 + dot(world.xz, vec2<f32>(1.7, 2.1)) + phase * 2.0);
let gust = 0.75 + frame.wind0.w * 0.5;
let fade = 1.0 / (1.0 + distance(camera, world) * 0.006);
let xz = (
dir * (0.045 + slow * 0.055)
+ cross * side * 0.025
+ dir * flutter * 0.012
) * weight * gust * fade * frame.wind1.y * frame.wind1.w;
return vec3<f32>(xz.x, 0.0, xz.y);
}
@@ -1091,6 +1091,15 @@ 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) {
let world = p_cam + view.world_position;
p_cam += wind_hook::tree_offset(
world, frame_origin, view.world_position, wow_light.monkey
);
}
out.world_position = vec4<f32>(p_cam + view.world_position, 1.0);
let view_rot = mat3x3<f32>(
view.view_from_world[0].xyz,
+25
View File
@@ -90,6 +90,10 @@ 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
@@ -204,6 +208,16 @@ 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 {
@@ -352,6 +366,17 @@ 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);
@@ -14,6 +14,8 @@
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
// MONKEY (wind): shared foliage vertex displacement; tree shadows intentionally stay static.
#import benilla::wind_hook
// Group 0 is Bevy's standard mesh-view bind group (view matrices, directional-light records and
// the shadow textures the retained pass reads).
@@ -103,6 +105,8 @@ const WORD_HAS_VC: u32 = 134217728u; // 1 << 27: the batch authors vertex colo
// INTERIOR without WMO is an interior M2 prop, the entity shader's `interior_prop =
// flags.z && !flags.x`: probe lighting, interior fog, no point lights.
const WORD_MATTE: u32 = 268435456u; // 1 << 28: ShadeSel::Matte, fixed intensity 1.0
// MONKEY (wind): alpha-tested leaf batch of a classified static tree/bush model.
const WORD_FOLIAGE_WIND: u32 = 536870912u; // 1 << 29
// MONKEY (room gate): record column `w` bits 15..=26 — this item's ROOM KEY, packed as
// `group + 1`, so **0 means the item names no room** and takes every fixture (a terrain-cell item,
@@ -1282,8 +1286,15 @@ fn vertex(v: GxVertex) -> GxVsOut {
return out;
}
// Camera-relative for f32 precision: the recentred vertex plus (cell origin - camera).
let p_cam = v.position + (cell.origin.xyz - view.world_position);
let world = v.position + cell.origin.xyz;
var p_cam = v.position + (cell.origin.xyz - view.world_position);
var world = v.position + cell.origin.xyz;
// MONKEY (wind): placement anchor is already baked per vertex. Only classified leaf-card
// batches carry the bit; animated doodads were rejected before this retained path.
if ((v.word & WORD_FOLIAGE_WIND) != 0u) {
let offset = wind_hook::tree_offset(world, v.anchor, view.world_position, wow_light.monkey);
world += offset;
p_cam += offset;
}
out.world_position = vec4<f32>(world, 1.0);
let view_rot = mat3x3<f32>(
view.view_from_world[0].xyz,
+5 -2
View File
@@ -12,8 +12,9 @@ use super::{
MAX_DIRTY_FRAMES, REBAKE_FRAMES,
};
use super::{
WORD_CLASS_INT, WORD_CLASS_TRANS, WORD_FOG_OFF, WORD_HAS_VC, WORD_INTERIOR, WORD_MATTE,
WORD_SHADE_LIT, WORD_TEXTURED, WORD_UNLIT, WORD_WINDOW, WORD_WMO, WORD_WRAP_X, WORD_WRAP_Y,
WORD_CLASS_INT, WORD_CLASS_TRANS, WORD_FOG_OFF, WORD_FOLIAGE_WIND, WORD_HAS_VC, WORD_INTERIOR,
WORD_MATTE, WORD_SHADE_LIT, WORD_TEXTURED, WORD_UNLIT, WORD_WINDOW, WORD_WMO, WORD_WRAP_X,
WORD_WRAP_Y,
};
/// Print the declined-batch census, beside the accepted count, once the counts have sat still for
@@ -208,6 +209,8 @@ fn bake_cell(items: &[GxItem], meshes: &mut Assets<Mesh>) -> render::GxCellDraw
| (u32::from(item.matte) * WORD_MATTE)
| (u32::from(item.texture.is_some()) * WORD_TEXTURED)
| (u32::from(has_vc) * WORD_HAS_VC)
// MONKEY (wind): only classified alpha-tested tree/bush leaf cards carry this bit.
| (u32::from(item.foliage_wind) * WORD_FOLIAGE_WIND)
// An interior prop is WORD_INTERIOR without WORD_WMO; a slot-less prop keeps the
// exterior law, as on the entity path.
| item.prop.as_ref().map_or(0, |p| {
+5 -1
View File
@@ -100,7 +100,11 @@ fn spawn_exile(commands: &mut Commands, f: &GxFader, alpha: f32, admitted: bool)
},
crate::model_render::EntityPathWhy("exile"),
crate::interact::PickMesh(b.geometry.clone()),
bevy::mesh::MeshTag(crate::mesh_tag::alpha_bits(alpha)),
// 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,
)),
crate::model_fade::DoodadFade {
radius: f.radius,
local_center: f.local_center,
+89
View File
@@ -76,6 +76,8 @@ const WORD_HAS_VC: u32 = 1 << 27;
// `ShadeSel::Matte`, fixed 1.0: a map doodad never reaches the 2.5 site (`0x69e4ad`). Not
// `WORD_SHADE_LIT`, so lifting the shader's `min(I, 1)` cap leaves this at 1.0.
const WORD_MATTE: u32 = 1 << 28;
// MONKEY (wind): classified alpha-tested tree/bush foliage; bits 30-31 remain free.
const WORD_FOLIAGE_WIND: u32 = 1 << 29;
// An interior M2 prop is WORD_INTERIOR with WORD_WMO clear, the entity shader's `interior_prop =
// flags.z && !flags.x`: SH-probe light from its record slot, interior fog, no live point lights.
@@ -173,6 +175,8 @@ struct GxItem {
prop: Option<GxItemProp>,
/// `Some(uid)` on a fader item: the placement it exiles with.
fader: Option<u32>,
/// MONKEY (wind): an alpha-tested leaf-card batch of a static tree/bush model.
foliage_wind: bool,
}
/// A WMO-prop item's referrer set (an index into [`GxCell::sets`]) and folded SH-probe slot;
@@ -211,6 +215,8 @@ 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,
@@ -478,6 +484,10 @@ impl StaticGx {
return false;
}
};
let foliage_wind = foliage_wind_batch(&b.object.label, b.blend, b.wmo.is_some());
if foliage_wind {
log_foliage_classification(&b.object.label);
}
let wmo_key = b.wmo.as_ref().map(|w| w.instance);
let wmo = b.wmo.map(|w| GxItemWmo {
group: w.group,
@@ -556,6 +566,7 @@ 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.
@@ -591,6 +602,7 @@ impl StaticGx {
wmo,
prop,
fader: fader_uid,
foliage_wind,
});
if !entry.dirty {
entry.dirty_since = self.frame;
@@ -688,6 +700,44 @@ 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 {
if wmo_geometry || blend != ModelBlend::AlphaTest {
return false;
}
let p = path.to_ascii_lowercase();
// Substrings alone otherwise catch StreetLamp, tree huts, dead stumps/logs and painted tree
// facades. Those can carry alpha-tested cards, but moving the whole card is not foliage sway.
if ["street", "treehut", "stump", "log", "facade", "fallen"]
.iter()
.any(|term| p.contains(term))
{
return false;
}
[
"tree", "bush", "shrub", "foliage", "fern", "palm", "plant", "willow", "canopy", "hedge",
"thorn", "cactus", "vine", "reed", "kelp",
]
.iter()
.any(|term| p.contains(term))
}
/// Log each classified model once so false positives can be audited from an ordinary scene load.
fn log_foliage_classification(path: &str) {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let mut seen = SEEN
.get_or_init(|| Mutex::new(HashSet::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if seen.insert(path.to_string()) {
info!("MONKEY wind: classified foliage leaf batches for {path}");
}
}
pub struct StaticGxPlugin;
impl Plugin for StaticGxPlugin {
@@ -831,6 +881,45 @@ mod tests {
assert_eq!(gx.cells.len(), 1);
}
#[test]
fn foliage_classifier_requires_a_leaf_batch_and_a_plant_name() {
assert!(foliage_wind_batch(
"World\\Azeroth\\Elwynn\\PassiveDoodads\\Trees\\ElwynnTree01.m2",
ModelBlend::AlphaTest,
false,
));
assert!(foliage_wind_batch(
"World\\Generic\\PassiveDoodads\\Bush\\Bush01.mdx",
ModelBlend::AlphaTest,
false,
));
assert!(!foliage_wind_batch(
"World\\Azeroth\\Elwynn\\PassiveDoodads\\Trees\\ElwynnTree01.m2",
ModelBlend::Opaque,
false,
));
assert!(!foliage_wind_batch(
"World\\Stormwind\\StreetLamp01.m2",
ModelBlend::AlphaTest,
false,
));
assert!(!foliage_wind_batch(
"World\\Azeroth\\SwampOfSorrow\\PassiveDoodads\\TreeHuts\\LostTreeHuts03.m2",
ModelBlend::AlphaTest,
false,
));
assert!(!foliage_wind_batch(
"World\\Azeroth\\Elwynn\\PassiveDoodads\\Tree\\ElwynnLog02.m2",
ModelBlend::AlphaTest,
false,
));
assert!(!foliage_wind_batch(
"World\\Wmo\\TreeHouse.wmo",
ModelBlend::AlphaTest,
true,
));
}
#[test]
fn a_fader_divert_registers_its_placement_seed() {
let mut gx = StaticGx::default();