From cbfe7d5f4465f3bd4d2da10ad1ef860659ec5af6 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 24 Mar 2026 19:55:24 +0300 Subject: [PATCH] refactor(rendering): extract M2 classification into pure functions --- CMakeLists.txt | 1 + include/rendering/m2_model_classifier.hpp | 93 ++++++ src/rendering/m2_model_classifier.cpp | 248 +++++++++++++++ src/rendering/m2_renderer.cpp | 356 +++------------------- 4 files changed, 386 insertions(+), 312 deletions(-) create mode 100644 include/rendering/m2_model_classifier.hpp create mode 100644 src/rendering/m2_model_classifier.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 16be9564..219b88ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -529,6 +529,7 @@ set(WOWEE_SOURCES src/rendering/character_preview.cpp src/rendering/wmo_renderer.cpp src/rendering/m2_renderer.cpp + src/rendering/m2_model_classifier.cpp src/rendering/quest_marker_renderer.cpp src/rendering/minimap.cpp src/rendering/world_map.cpp diff --git a/include/rendering/m2_model_classifier.hpp b/include/rendering/m2_model_classifier.hpp new file mode 100644 index 00000000..8ef09aab --- /dev/null +++ b/include/rendering/m2_model_classifier.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace rendering { + +/** + * Output of classifyM2Model(): all name/geometry-based flags for an M2 model. + * Pure data — no Vulkan, GPU, or asset-manager dependencies. + */ +struct M2ClassificationResult { + // --- Collision shape selectors --- + bool collisionNoBlock = false; ///< Foliage/soft-trees/rugs: no blocking + bool collisionBridge = false; ///< Walk-on-top bridge/plank/walkway + bool collisionPlanter = false; ///< Low stepped planter/curb + bool collisionSteppedFountain = false; ///< Stepped fountain base + bool collisionSteppedLowPlatform = false; ///< Low stepped platform (curb/planter/bridge) + bool collisionStatue = false; ///< Statue/monument/sculpture + bool collisionSmallSolidProp = false; ///< Blockable solid prop (crate/chest/barrel) + bool collisionNarrowVerticalProp = false; ///< Narrow tall prop (lamp/post/pole) + bool collisionTreeTrunk = false; ///< Tree trunk cylinder + + // --- Rendering / effect classification --- + bool isFoliageLike = false; ///< Foliage or tree (wind sway, disabled animation) + bool isSpellEffect = false; ///< Spell effect / particle-dominated visual + bool isLavaModel = false; ///< Lava surface (UV scroll animation) + bool isInstancePortal = false; ///< Instance portal (additive, spin, no collision) + bool isWaterVegetation = false; ///< Aquatic vegetation (cattails, kelp, reeds, etc.) + bool isFireflyEffect = false; ///< Ambient creature (exempt from particle dampeners) + bool isElvenLike = false; ///< Night elf / Blood elf themed model + bool isLanternLike = false; ///< Lantern/lamp/light model + bool isKoboldFlame = false; ///< Kobold candle/torch model + bool isGroundDetail = false; ///< Ground-clutter detail doodad (always non-blocking) + bool isInvisibleTrap = false; ///< Event-object invisible trap (no render, no collision) + bool isSmoke = false; ///< Smoke model (UV scroll animation) + + // --- Animation flags --- + bool disableAnimation = false; ///< Keep visually stable (foliage, chest lids, etc.) + bool shadowWindFoliage = false; ///< Apply wind sway in shadow pass for foliage/trees +}; + +/** + * Classify an M2 model by name and geometry. + * + * Pure function — no Vulkan, VkContext, or AssetManager dependencies. + * All results are derived solely from the model name string and tight vertex bounds. + * + * @param name Full model path/name from the M2 header (any case) + * @param boundsMin Per-vertex tight bounding-box minimum + * @param boundsMax Per-vertex tight bounding-box maximum + * @param vertexCount Number of mesh vertices + * @param emitterCount Number of particle emitters + */ +M2ClassificationResult classifyM2Model( + const std::string& name, + const glm::vec3& boundsMin, + const glm::vec3& boundsMax, + std::size_t vertexCount, + std::size_t emitterCount); + +// --------------------------------------------------------------------------- +// Batch texture classification +// --------------------------------------------------------------------------- + +/** + * Per-batch texture key classification — glow / tint token flags. + * Input must be a lowercased, backslash-normalised texture path (as stored in + * M2Renderer's textureKeysLower vector). Pure data — no Vulkan dependencies. + */ +struct M2BatchTexClassification { + bool exactLanternGlowTex = false; ///< One of the known exact lantern-glow texture paths + bool hasGlowToken = false; ///< glow / flare / halo / light + bool hasFlameToken = false; ///< flame / fire / flamelick / ember + bool hasGlowCardToken = false; ///< glow / flamelick / lensflare / t_vfx / lightbeam / glowball / genericglow + bool likelyFlame = false; ///< fire / flame / torch + bool lanternFamily = false; ///< lantern / lamp / elf / silvermoon / quel / thalas + int glowTint = 0; ///< 0 = neutral, 1 = cool (blue/arcane), 2 = warm (red/scarlet) +}; + +/** + * Classify a batch texture by its lowercased path for glow/tint hinting. + * + * Pure function — no Vulkan, VkContext, or AssetManager dependencies. + * + * @param lowerTexKey Lowercased, backslash-normalised texture path (may be empty) + */ +M2BatchTexClassification classifyBatchTexture(const std::string& lowerTexKey); + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/m2_model_classifier.cpp b/src/rendering/m2_model_classifier.cpp new file mode 100644 index 00000000..424bfc42 --- /dev/null +++ b/src/rendering/m2_model_classifier.cpp @@ -0,0 +1,248 @@ +#include "rendering/m2_model_classifier.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +namespace { + +// Returns true if `lower` contains `token` as a substring. +// Caller must provide an already-lowercased string. +inline bool has(const std::string& lower, std::string_view token) noexcept { + return lower.find(token) != std::string::npos; +} + +// Returns true if any token in the compile-time array is a substring of `lower`. +template +bool hasAny(const std::string& lower, + const std::array& tokens) noexcept { + for (auto tok : tokens) + if (lower.find(tok) != std::string::npos) return true; + return false; +} + +} // namespace + +M2ClassificationResult classifyM2Model( + const std::string& name, + const glm::vec3& boundsMin, + const glm::vec3& boundsMax, + std::size_t vertexCount, + std::size_t emitterCount) +{ + // Single lowercased copy — all token checks share it. + std::string n = name; + std::transform(n.begin(), n.end(), n.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + M2ClassificationResult r; + + // --------------------------------------------------------------- + // Geometry metrics + // --------------------------------------------------------------- + const glm::vec3 dims = boundsMax - boundsMin; + const float horiz = std::max(dims.x, dims.y); + const float vert = std::max(0.0f, dims.z); + const bool lowWide = (horiz > 1.4f && vert > 0.2f && vert < horiz * 0.70f); + const bool lowPlat = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); + + // --------------------------------------------------------------- + // Simple single-token flags + // --------------------------------------------------------------- + r.isInvisibleTrap = has(n, "invisibletrap"); + r.isGroundDetail = has(n, "\\nodxt\\detail\\") || has(n, "\\detail\\"); + r.isSmoke = has(n, "smoke"); + r.isLavaModel = has(n, "forgelava") || has(n, "lavapot") || has(n, "lavaflow"); + + r.isInstancePortal = has(n, "instanceportal") || has(n, "instancenewportal") + || has(n, "portalfx") || has(n, "spellportal"); + + r.isWaterVegetation = has(n, "cattail") || has(n, "reed") || has(n, "bulrush") + || has(n, "seaweed") || has(n, "kelp") || has(n, "lilypad"); + + r.isElvenLike = has(n, "elf") || has(n, "elven") || has(n, "quel"); + r.isLanternLike = has(n, "lantern") || has(n, "lamp") || has(n, "light"); + r.isKoboldFlame = has(n, "kobold") + && (has(n, "candle") || has(n, "torch") || has(n, "mine")); + + // --------------------------------------------------------------- + // Collision: shape categories (mirrors original logic ordering) + // --------------------------------------------------------------- + const bool isPlanter = has(n, "planter"); + const bool likelyCurb = isPlanter || has(n, "curb") || has(n, "base") + || has(n, "ring") || has(n, "well"); + const bool knownSwPlanter = has(n, "stormwindplanter") + || has(n, "stormwindwindowplanter"); + const bool bridgeName = has(n, "bridge") || has(n, "plank") || has(n, "walkway"); + const bool statueName = has(n, "statue") || has(n, "monument") || has(n, "sculpture"); + const bool sittable = has(n, "chair") || has(n, "bench") || has(n, "stool") + || has(n, "seat") || has(n, "throne"); + const bool smallSolid = (statueName && !sittable) + || has(n, "crate") || has(n, "box") + || has(n, "chest") || has(n, "barrel"); + const bool chestName = has(n, "chest"); + + r.collisionSteppedFountain = has(n, "fountain"); + r.collisionSteppedLowPlatform = !r.collisionSteppedFountain + && (knownSwPlanter || bridgeName + || (likelyCurb && (lowPlat || lowWide))); + r.collisionBridge = bridgeName; + r.collisionPlanter = isPlanter; + r.collisionStatue = statueName; + + const bool narrowVertName = has(n, "lamp") || has(n, "lantern") + || has(n, "post") || has(n, "pole"); + const bool narrowVertShape = (horiz > 0.12f && horiz < 2.0f + && vert > 2.2f && vert > horiz * 1.8f); + r.collisionNarrowVerticalProp = !r.collisionSteppedFountain + && !r.collisionSteppedLowPlatform + && (narrowVertName || narrowVertShape); + + // --------------------------------------------------------------- + // Foliage token table (sorted alphabetically) + // --------------------------------------------------------------- + static constexpr auto kFoliageTokens = std::to_array({ + "algae", "bamboo", "banana", "branch", "bush", + "cactus", "canopy", "cattail", "coconut", "coral", + "corn", "crop", "dead-grass", "dead_grass", "deadgrass", + "dry-grass", "dry_grass", "drygrass", + "fern", "fireflies", "firefly", "fireflys", + "flower", "frond", "fungus", "gourd", "grass", + "hay", "hedge", "ivy", "kelp", "leaf", + "leaves", "lily", "melon", "moss", "mushroom", + "palm", "pumpkin", "reed", "root", "seaweed", + "shrub", "squash", "stalk", "thorn", "toadstool", + "vine", "watermelon", "weed", "wheat", + }); + + // "plant" is foliage unless "planter" is also present (planters are solid curbs). + const bool foliagePlant = has(n, "plant") && !isPlanter; + const bool foliageName = foliagePlant || hasAny(n, kFoliageTokens); + const bool treeLike = has(n, "tree"); + const bool hardTreePart = has(n, "trunk") || has(n, "stump") || has(n, "log"); + + // Trees wide/tall enough to have a visible trunk → solid cylinder collision. + const bool treeWithTrunk = treeLike && !hardTreePart && !foliageName + && horiz > 6.0f && vert > 4.0f; + const bool softTree = treeLike && !hardTreePart && !treeWithTrunk; + + r.collisionTreeTrunk = treeWithTrunk; + + const bool genericSolid = (horiz > 0.6f && horiz < 6.0f + && vert > 0.30f && vert < 4.0f + && vert > horiz * 0.16f) || statueName; + const bool curbLikeName = has(n, "curb") || has(n, "planter") + || has(n, "ring") || has(n, "well") || has(n, "base"); + const bool lowPlatLikeShape = lowWide || lowPlat; + + r.collisionSmallSolidProp = !r.collisionSteppedFountain + && !r.collisionSteppedLowPlatform + && !r.collisionNarrowVerticalProp + && !r.collisionTreeTrunk + && !curbLikeName + && !lowPlatLikeShape + && (smallSolid + || (genericSolid && !foliageName && !softTree)); + + const bool carpetOrRug = has(n, "carpet") || has(n, "rug"); + const bool forceSolidCurb = r.collisionSteppedLowPlatform || knownSwPlanter + || likelyCurb || r.collisionPlanter; + r.collisionNoBlock = (foliageName || softTree || carpetOrRug) && !forceSolidCurb; + // Ground-clutter detail cards are always non-blocking. + if (r.isGroundDetail) r.collisionNoBlock = true; + + // --------------------------------------------------------------- + // Ambient creatures: fireflies, dragonflies, moths, butterflies + // --------------------------------------------------------------- + static constexpr auto kAmbientTokens = std::to_array({ + "butterfly", "dragonflies", "dragonfly", + "fireflies", "firefly", "fireflys", "moth", + }); + const bool ambientCreature = hasAny(n, kAmbientTokens); + + // --------------------------------------------------------------- + // Animation / foliage rendering flags + // --------------------------------------------------------------- + const bool foliageOrTree = foliageName || treeLike; + r.isFoliageLike = foliageOrTree && !ambientCreature; + r.disableAnimation = r.isFoliageLike || chestName; + r.shadowWindFoliage = r.isFoliageLike; + r.isFireflyEffect = ambientCreature; + + // --------------------------------------------------------------- + // Spell effects (named tokens + particle-dominated geometry heuristic) + // --------------------------------------------------------------- + static constexpr auto kEffectTokens = std::to_array({ + "bubbles", "hazardlight", "instancenewportal", "instanceportal", + "lavabubble", "lavasplash", "lavasteam", "levelup", + "lightshaft", "mageportal", "particleemitter", + "spotlight", "volumetriclight", "wisps", "worldtreeportal", + }); + r.isSpellEffect = hasAny(n, kEffectTokens) + || (emitterCount >= 3 && vertexCount <= 200); + // Instance portals are spell effects too. + if (r.isInstancePortal) r.isSpellEffect = true; + + return r; +} + +// --------------------------------------------------------------------------- +// classifyBatchTexture +// --------------------------------------------------------------------------- + +M2BatchTexClassification classifyBatchTexture(const std::string& lowerTexKey) +{ + M2BatchTexClassification r; + + // Exact paths for well-known lantern / lamp glow-card textures. + static constexpr auto kExactGlowTextures = std::to_array({ + "world\\azeroth\\karazahn\\passivedoodads\\bonfire\\flamelicksmallblue.blp", + "world\\expansion06\\doodads\\nightelf\\7ne_druid_streetlamp01_light.blp", + "world\\generic\\human\\passive doodads\\stormwind\\t_vfx_glow01_64.blp", + "world\\generic\\nightelf\\passive doodads\\lamps\\glowblue32.blp", + "world\\generic\\nightelf\\passive doodads\\magicalimplements\\glow.blp", + }); + for (auto s : kExactGlowTextures) + if (lowerTexKey == s) { r.exactLanternGlowTex = true; break; } + + static constexpr auto kGlowTokens = std::to_array({ + "flare", "glow", "halo", "light", + }); + static constexpr auto kFlameTokens = std::to_array({ + "ember", "fire", "flame", "flamelick", + }); + static constexpr auto kGlowCardTokens = std::to_array({ + "flamelick", "genericglow", "glow", "glowball", + "lensflare", "lightbeam", "t_vfx", + }); + static constexpr auto kLikelyFlameTokens = std::to_array({ + "fire", "flame", "torch", + }); + static constexpr auto kLanternFamilyTokens = std::to_array({ + "elf", "lamp", "lantern", "quel", "silvermoon", "thalas", + }); + static constexpr auto kCoolTintTokens = std::to_array({ + "arcane", "blue", "nightelf", + }); + static constexpr auto kRedTintTokens = std::to_array({ + "red", "ruby", "scarlet", + }); + + r.hasGlowToken = hasAny(lowerTexKey, kGlowTokens); + r.hasFlameToken = hasAny(lowerTexKey, kFlameTokens); + r.hasGlowCardToken = hasAny(lowerTexKey, kGlowCardTokens); + r.likelyFlame = hasAny(lowerTexKey, kLikelyFlameTokens); + r.lanternFamily = hasAny(lowerTexKey, kLanternFamilyTokens); + r.glowTint = hasAny(lowerTexKey, kCoolTintTokens) ? 1 + : hasAny(lowerTexKey, kRedTintTokens) ? 2 + : 0; + + return r; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 654717ab..a7a2c42e 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1,4 +1,5 @@ #include "rendering/m2_renderer.hpp" +#include "rendering/m2_model_classifier.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_buffer.hpp" #include "rendering/vk_texture.hpp" @@ -1004,15 +1005,6 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { M2ModelGPU gpuModel; gpuModel.name = model.name; - // Detect invisible trap models (event objects that should not render or collide) - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - bool isInvisibleTrap = (lowerName.find("invisibletrap") != std::string::npos); - gpuModel.isInvisibleTrap = isInvisibleTrap; - if (isInvisibleTrap) { - LOG_INFO("Loading InvisibleTrap model: ", model.name, " (will be invisible, no collision)"); - } // Use tight bounds from actual vertices for collision/camera occlusion. // Header bounds in some M2s are overly conservative. glm::vec3 tightMin(0.0f); @@ -1025,165 +1017,40 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { tightMax = glm::max(tightMax, v.position); } } - bool foliageOrTreeLike = false; - bool chestName = false; - bool groundDetailModel = false; - { - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - gpuModel.collisionSteppedFountain = (lowerName.find("fountain") != std::string::npos); - glm::vec3 dims = tightMax - tightMin; - float horiz = std::max(dims.x, dims.y); - float vert = std::max(0.0f, dims.z); - bool lowWideShape = (horiz > 1.4f && vert > 0.2f && vert < horiz * 0.70f); - bool likelyCurbName = - (lowerName.find("planter") != std::string::npos) || - (lowerName.find("curb") != std::string::npos) || - (lowerName.find("base") != std::string::npos) || - (lowerName.find("ring") != std::string::npos) || - (lowerName.find("well") != std::string::npos); - bool knownStormwindPlanter = - (lowerName.find("stormwindplanter") != std::string::npos) || - (lowerName.find("stormwindwindowplanter") != std::string::npos); - bool lowPlatformShape = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); - bool bridgeName = - (lowerName.find("bridge") != std::string::npos) || - (lowerName.find("plank") != std::string::npos) || - (lowerName.find("walkway") != std::string::npos); - gpuModel.collisionSteppedLowPlatform = (!gpuModel.collisionSteppedFountain) && - (knownStormwindPlanter || - bridgeName || - (likelyCurbName && (lowPlatformShape || lowWideShape))); - gpuModel.collisionBridge = bridgeName; - - bool isPlanter = (lowerName.find("planter") != std::string::npos); - gpuModel.collisionPlanter = isPlanter; - bool statueName = - (lowerName.find("statue") != std::string::npos) || - (lowerName.find("monument") != std::string::npos) || - (lowerName.find("sculpture") != std::string::npos); - gpuModel.collisionStatue = statueName; - // Sittable furniture: chairs/benches/stools cause players to get stuck against - // invisible bounding boxes; WMOs already handle room collision. - bool sittableFurnitureName = - (lowerName.find("chair") != std::string::npos) || - (lowerName.find("bench") != std::string::npos) || - (lowerName.find("stool") != std::string::npos) || - (lowerName.find("seat") != std::string::npos) || - (lowerName.find("throne") != std::string::npos); - bool smallSolidPropName = - (statueName && !sittableFurnitureName) || - (lowerName.find("crate") != std::string::npos) || - (lowerName.find("box") != std::string::npos) || - (lowerName.find("chest") != std::string::npos) || - (lowerName.find("barrel") != std::string::npos); - chestName = (lowerName.find("chest") != std::string::npos); - bool foliageName = - (lowerName.find("bush") != std::string::npos) || - (lowerName.find("grass") != std::string::npos) || - (lowerName.find("drygrass") != std::string::npos) || - (lowerName.find("dry_grass") != std::string::npos) || - (lowerName.find("dry-grass") != std::string::npos) || - (lowerName.find("deadgrass") != std::string::npos) || - (lowerName.find("dead_grass") != std::string::npos) || - (lowerName.find("dead-grass") != std::string::npos) || - ((lowerName.find("plant") != std::string::npos) && !isPlanter) || - (lowerName.find("flower") != std::string::npos) || - (lowerName.find("shrub") != std::string::npos) || - (lowerName.find("fern") != std::string::npos) || - (lowerName.find("vine") != std::string::npos) || - (lowerName.find("lily") != std::string::npos) || - (lowerName.find("weed") != std::string::npos) || - (lowerName.find("wheat") != std::string::npos) || - (lowerName.find("pumpkin") != std::string::npos) || - (lowerName.find("firefly") != std::string::npos) || - (lowerName.find("fireflies") != std::string::npos) || - (lowerName.find("fireflys") != std::string::npos) || - (lowerName.find("mushroom") != std::string::npos) || - (lowerName.find("fungus") != std::string::npos) || - (lowerName.find("toadstool") != std::string::npos) || - (lowerName.find("root") != std::string::npos) || - (lowerName.find("branch") != std::string::npos) || - (lowerName.find("thorn") != std::string::npos) || - (lowerName.find("moss") != std::string::npos) || - (lowerName.find("ivy") != std::string::npos) || - (lowerName.find("seaweed") != std::string::npos) || - (lowerName.find("kelp") != std::string::npos) || - (lowerName.find("cattail") != std::string::npos) || - (lowerName.find("reed") != std::string::npos) || - (lowerName.find("palm") != std::string::npos) || - (lowerName.find("bamboo") != std::string::npos) || - (lowerName.find("banana") != std::string::npos) || - (lowerName.find("coconut") != std::string::npos) || - (lowerName.find("watermelon") != std::string::npos) || - (lowerName.find("melon") != std::string::npos) || - (lowerName.find("squash") != std::string::npos) || - (lowerName.find("gourd") != std::string::npos) || - (lowerName.find("canopy") != std::string::npos) || - (lowerName.find("hedge") != std::string::npos) || - (lowerName.find("cactus") != std::string::npos) || - (lowerName.find("leaf") != std::string::npos) || - (lowerName.find("leaves") != std::string::npos) || - (lowerName.find("stalk") != std::string::npos) || - (lowerName.find("corn") != std::string::npos) || - (lowerName.find("crop") != std::string::npos) || - (lowerName.find("hay") != std::string::npos) || - (lowerName.find("frond") != std::string::npos) || - (lowerName.find("algae") != std::string::npos) || - (lowerName.find("coral") != std::string::npos); - bool treeLike = (lowerName.find("tree") != std::string::npos); - foliageOrTreeLike = (foliageName || treeLike); - groundDetailModel = - (lowerName.find("\\nodxt\\detail\\") != std::string::npos) || - (lowerName.find("\\detail\\") != std::string::npos); - bool hardTreePart = - (lowerName.find("trunk") != std::string::npos) || - (lowerName.find("stump") != std::string::npos) || - (lowerName.find("log") != std::string::npos); - // Trees with visible trunks get collision. Threshold: canopy wider than 6 - // model units AND taller than 4 units (filters out small bushes/saplings). - bool treeWithTrunk = treeLike && !hardTreePart && !foliageName && horiz > 6.0f && vert > 4.0f; - bool softTree = treeLike && !hardTreePart && !treeWithTrunk; - bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter; - bool narrowVerticalName = - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("post") != std::string::npos) || - (lowerName.find("pole") != std::string::npos); - bool narrowVerticalShape = - (horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f); - gpuModel.collisionTreeTrunk = treeWithTrunk; - gpuModel.collisionNarrowVerticalProp = - !gpuModel.collisionSteppedFountain && - !gpuModel.collisionSteppedLowPlatform && - (narrowVerticalName || narrowVerticalShape); - bool genericSolidPropShape = - (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f) || - statueName; - bool curbLikeName = - (lowerName.find("curb") != std::string::npos) || - (lowerName.find("planter") != std::string::npos) || - (lowerName.find("ring") != std::string::npos) || - (lowerName.find("well") != std::string::npos) || - (lowerName.find("base") != std::string::npos); - bool lowPlatformLikeShape = lowWideShape || lowPlatformShape; - bool carpetOrRug = - (lowerName.find("carpet") != std::string::npos) || - (lowerName.find("rug") != std::string::npos); - gpuModel.collisionSmallSolidProp = - !gpuModel.collisionSteppedFountain && - !gpuModel.collisionSteppedLowPlatform && - !gpuModel.collisionNarrowVerticalProp && - !gpuModel.collisionTreeTrunk && - !curbLikeName && - !lowPlatformLikeShape && - (smallSolidPropName || (genericSolidPropShape && !foliageName && !softTree)); - // Disable collision for foliage, soft trees, and decorative carpets/rugs - gpuModel.collisionNoBlock = ((foliageName || softTree || carpetOrRug) && - !forceSolidCurb); + // Classify model from name and geometry — pure function, no GPU dependencies. + auto cls = classifyM2Model(model.name, tightMin, tightMax, + model.vertices.size(), + model.particleEmitters.size()); + const bool isInvisibleTrap = cls.isInvisibleTrap; + const bool groundDetailModel = cls.isGroundDetail; + if (isInvisibleTrap) { + LOG_INFO("Loading InvisibleTrap model: ", model.name, " (will be invisible, no collision)"); } + + gpuModel.isInvisibleTrap = cls.isInvisibleTrap; + gpuModel.collisionSteppedFountain = cls.collisionSteppedFountain; + gpuModel.collisionSteppedLowPlatform = cls.collisionSteppedLowPlatform; + gpuModel.collisionBridge = cls.collisionBridge; + gpuModel.collisionPlanter = cls.collisionPlanter; + gpuModel.collisionStatue = cls.collisionStatue; + gpuModel.collisionTreeTrunk = cls.collisionTreeTrunk; + gpuModel.collisionNarrowVerticalProp = cls.collisionNarrowVerticalProp; + gpuModel.collisionSmallSolidProp = cls.collisionSmallSolidProp; + gpuModel.collisionNoBlock = cls.collisionNoBlock; + gpuModel.isGroundDetail = cls.isGroundDetail; + gpuModel.isFoliageLike = cls.isFoliageLike; + gpuModel.disableAnimation = cls.disableAnimation; + gpuModel.shadowWindFoliage = cls.shadowWindFoliage; + gpuModel.isFireflyEffect = cls.isFireflyEffect; + gpuModel.isSmoke = cls.isSmoke; + gpuModel.isSpellEffect = cls.isSpellEffect; + gpuModel.isLavaModel = cls.isLavaModel; + gpuModel.isInstancePortal = cls.isInstancePortal; + gpuModel.isWaterVegetation = cls.isWaterVegetation; + gpuModel.isElvenLike = cls.isElvenLike; + gpuModel.isLanternLike = cls.isLanternLike; + gpuModel.isKoboldFlame = cls.isKoboldFlame; gpuModel.boundMin = tightMin; gpuModel.boundMax = tightMax; gpuModel.boundRadius = model.boundRadius; @@ -1201,79 +1068,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { break; } } - bool ambientCreature = - (lowerName.find("firefly") != std::string::npos) || - (lowerName.find("fireflies") != std::string::npos) || - (lowerName.find("fireflys") != std::string::npos) || - (lowerName.find("dragonfly") != std::string::npos) || - (lowerName.find("dragonflies") != std::string::npos) || - (lowerName.find("butterfly") != std::string::npos) || - (lowerName.find("moth") != std::string::npos); - gpuModel.disableAnimation = (foliageOrTreeLike && !ambientCreature) || chestName; - gpuModel.shadowWindFoliage = foliageOrTreeLike && !ambientCreature; - gpuModel.isFoliageLike = foliageOrTreeLike && !ambientCreature; - gpuModel.isElvenLike = - (lowerName.find("elf") != std::string::npos) || - (lowerName.find("elven") != std::string::npos) || - (lowerName.find("quel") != std::string::npos); - gpuModel.isLanternLike = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); - gpuModel.isKoboldFlame = - (lowerName.find("kobold") != std::string::npos) && - ((lowerName.find("candle") != std::string::npos) || - (lowerName.find("torch") != std::string::npos) || - (lowerName.find("mine") != std::string::npos)); - gpuModel.isGroundDetail = groundDetailModel; - if (groundDetailModel) { - // Ground clutter (grass/pebbles/detail cards) should never block camera/movement. - gpuModel.collisionNoBlock = true; - } - // Spell effect / pure-visual models: particle-dominated with minimal geometry, - // or named effect models (light shafts, portals, emitters, spotlights) - bool effectByName = - (lowerName.find("lightshaft") != std::string::npos) || - (lowerName.find("volumetriclight") != std::string::npos) || - (lowerName.find("instanceportal") != std::string::npos) || - (lowerName.find("instancenewportal") != std::string::npos) || - (lowerName.find("mageportal") != std::string::npos) || - (lowerName.find("worldtreeportal") != std::string::npos) || - (lowerName.find("particleemitter") != std::string::npos) || - (lowerName.find("bubbles") != std::string::npos) || - (lowerName.find("spotlight") != std::string::npos) || - (lowerName.find("hazardlight") != std::string::npos) || - (lowerName.find("lavasplash") != std::string::npos) || - (lowerName.find("lavabubble") != std::string::npos) || - (lowerName.find("lavasteam") != std::string::npos) || - (lowerName.find("wisps") != std::string::npos) || - (lowerName.find("levelup") != std::string::npos); - gpuModel.isSpellEffect = effectByName || - (hasParticles && model.vertices.size() <= 200 && - model.particleEmitters.size() >= 3); - gpuModel.isLavaModel = - (lowerName.find("forgelava") != std::string::npos) || - (lowerName.find("lavapot") != std::string::npos) || - (lowerName.find("lavaflow") != std::string::npos); - gpuModel.isInstancePortal = - (lowerName.find("instanceportal") != std::string::npos) || - (lowerName.find("instancenewportal") != std::string::npos) || - (lowerName.find("portalfx") != std::string::npos) || - (lowerName.find("spellportal") != std::string::npos); - // Instance portals are spell effects too (additive blend, no collision) - if (gpuModel.isInstancePortal) { - gpuModel.isSpellEffect = true; - } - // Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water - gpuModel.isWaterVegetation = - (lowerName.find("cattail") != std::string::npos) || - (lowerName.find("reed") != std::string::npos) || - (lowerName.find("bulrush") != std::string::npos) || - (lowerName.find("seaweed") != std::string::npos) || - (lowerName.find("kelp") != std::string::npos) || - (lowerName.find("lilypad") != std::string::npos); - // Ambient creature effects: particle-based glow (exempt from particle dampeners) - gpuModel.isFireflyEffect = ambientCreature; + // Build collision mesh + spatial grid from M2 bounding geometry gpuModel.collision.vertices = model.collisionVertices; @@ -1284,14 +1079,6 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { " tris, grid ", gpuModel.collision.gridCellsX, "x", gpuModel.collision.gridCellsY); } - // Flag smoke models for UV scroll animation (in addition to particle emitters) - { - std::string smokeName = model.name; - std::transform(smokeName.begin(), smokeName.end(), smokeName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - gpuModel.isSmoke = (smokeName.find("smoke") != std::string::npos); - } - // Identify idle variation sequences (animation ID 0 = Stand) for (int i = 0; i < static_cast(model.sequences.size()); i++) { if (model.sequences[i].id == 0 && model.sequences[i].duration > 0) { @@ -1412,14 +1199,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { static const bool kGlowDiag = envFlagEnabled("WOWEE_M2_GLOW_DIAG", false); if (kGlowDiag) { - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - const bool lanternLike = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); - if (lanternLike) { + if (gpuModel.isLanternLike) { for (size_t ti = 0; ti < model.textures.size(); ++ti) { const std::string key = (ti < textureKeysLower.size()) ? textureKeysLower[ti] : std::string(); LOG_DEBUG("M2 GLOW TEX '", model.name, "' tex[", ti, "]='", key, "' flags=0x", @@ -1561,60 +1341,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } bgpu.texture = tex; - const bool exactLanternGlowTexture = - (batchTexKeyLower == "world\\expansion06\\doodads\\nightelf\\7ne_druid_streetlamp01_light.blp") || - (batchTexKeyLower == "world\\generic\\nightelf\\passive doodads\\lamps\\glowblue32.blp") || - (batchTexKeyLower == "world\\generic\\human\\passive doodads\\stormwind\\t_vfx_glow01_64.blp") || - (batchTexKeyLower == "world\\azeroth\\karazahn\\passivedoodads\\bonfire\\flamelicksmallblue.blp") || - (batchTexKeyLower == "world\\generic\\nightelf\\passive doodads\\magicalimplements\\glow.blp"); - const bool texHasGlowToken = - (batchTexKeyLower.find("glow") != std::string::npos) || - (batchTexKeyLower.find("flare") != std::string::npos) || - (batchTexKeyLower.find("halo") != std::string::npos) || - (batchTexKeyLower.find("light") != std::string::npos); - const bool texHasFlameToken = - (batchTexKeyLower.find("flame") != std::string::npos) || - (batchTexKeyLower.find("fire") != std::string::npos) || - (batchTexKeyLower.find("flamelick") != std::string::npos) || - (batchTexKeyLower.find("ember") != std::string::npos); - const bool texGlowCardToken = - (batchTexKeyLower.find("glow") != std::string::npos) || - (batchTexKeyLower.find("flamelick") != std::string::npos) || - (batchTexKeyLower.find("lensflare") != std::string::npos) || - (batchTexKeyLower.find("t_vfx") != std::string::npos) || - (batchTexKeyLower.find("lightbeam") != std::string::npos) || - (batchTexKeyLower.find("glowball") != std::string::npos) || - (batchTexKeyLower.find("genericglow") != std::string::npos); - const bool texLikelyFlame = - (batchTexKeyLower.find("fire") != std::string::npos) || - (batchTexKeyLower.find("flame") != std::string::npos) || - (batchTexKeyLower.find("torch") != std::string::npos); - const bool texLanternFamily = - (batchTexKeyLower.find("lantern") != std::string::npos) || - (batchTexKeyLower.find("lamp") != std::string::npos) || - (batchTexKeyLower.find("elf") != std::string::npos) || - (batchTexKeyLower.find("silvermoon") != std::string::npos) || - (batchTexKeyLower.find("quel") != std::string::npos) || - (batchTexKeyLower.find("thalas") != std::string::npos); - const bool modelLanternFamily = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); + const auto tcls = classifyBatchTexture(batchTexKeyLower); + const bool modelLanternFamily = gpuModel.isLanternLike; bgpu.lanternGlowHint = - exactLanternGlowTexture || - ((texHasGlowToken || (modelLanternFamily && texHasFlameToken)) && - (texLanternFamily || modelLanternFamily) && - (!texLikelyFlame || modelLanternFamily)); - bgpu.glowCardLike = bgpu.lanternGlowHint && texGlowCardToken; - const bool texCoolTint = - (batchTexKeyLower.find("blue") != std::string::npos) || - (batchTexKeyLower.find("nightelf") != std::string::npos) || - (batchTexKeyLower.find("arcane") != std::string::npos); - const bool texRedTint = - (batchTexKeyLower.find("red") != std::string::npos) || - (batchTexKeyLower.find("scarlet") != std::string::npos) || - (batchTexKeyLower.find("ruby") != std::string::npos); - bgpu.glowTint = texCoolTint ? 1 : (texRedTint ? 2 : 0); + tcls.exactLanternGlowTex || + ((tcls.hasGlowToken || (modelLanternFamily && tcls.hasFlameToken)) && + (tcls.lanternFamily || modelLanternFamily) && + (!tcls.likelyFlame || modelLanternFamily)); + bgpu.glowCardLike = bgpu.lanternGlowHint && tcls.hasGlowCardToken; + bgpu.glowTint = tcls.glowTint; bool texHasAlpha = false; if (tex != nullptr && tex != whiteTexture_.get()) { auto ait = textureHasAlphaByPtr_.find(tex); @@ -1682,10 +1417,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } // Optional diagnostics for glow/light batches (disabled by default). - if (kGlowDiag && - (lowerName.find("light") != std::string::npos || - lowerName.find("lamp") != std::string::npos || - lowerName.find("lantern") != std::string::npos)) { + if (kGlowDiag && gpuModel.isLanternLike) { LOG_DEBUG("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), ": blend=", bgpu.blendMode, " matFlags=0x", std::hex, bgpu.materialFlags, std::dec,