From a36cc3553c19405d87a3136e6556ea5fc1cf8986 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 11 Jul 2026 11:40:49 -0700 Subject: [PATCH] fix(rendering): hide sky birds until close enough for animation Bird and bat doodads appeared frozen mid-flap in the sky because bone computation stops beyond 150 units (LOD3 distance) but the model was still rendered in its bind pose. Now classified as sky birds and their render distance is capped to the bone-update range, fading in smoothly as the player approaches. --- include/rendering/m2_model_classifier.hpp | 1 + include/rendering/m2_renderer.hpp | 2 ++ src/rendering/m2_model_classifier.cpp | 12 ++++++++++++ src/rendering/m2_renderer.cpp | 1 + src/rendering/m2_renderer_render.cpp | 12 ++++++++++++ 5 files changed, 28 insertions(+) diff --git a/include/rendering/m2_model_classifier.hpp b/include/rendering/m2_model_classifier.hpp index 6d6faeb5..9b0ee247 100644 --- a/include/rendering/m2_model_classifier.hpp +++ b/include/rendering/m2_model_classifier.hpp @@ -51,6 +51,7 @@ struct M2ClassificationResult { bool isWaterfall = false; ///< Waterfall model (ambient sound + splash particles) bool isBrazierOrFire = false; ///< Brazier / campfire / bonfire model bool isTorch = false; ///< Wall-mounted or standing torch + bool isSkyBird = false; ///< Flying bird/bat doodad (hide until animation range) // --- Ambient emitter type (for sound system) --- AmbientEmitterType ambientEmitterType = AmbientEmitterType::None; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 741436a6..24edcd70 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -130,6 +130,7 @@ struct M2ModelGPU { bool isLanternLike = false; // Model name matches lantern/lamp/light (precomputed) bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed) bool isLavaModel = false; // Model name contains lava/molten/magma (UV scroll fallback) + bool isSkyBird = false; // Flying bird/bat doodad — hide until animation range bool hasTextureAnimation = false; // True if any batch has UV animation bool hasTransparentBatches = false; // True if any batch uses alpha-blend or additive (blendMode >= 2) uint8_t availableLODs = 0; // Bitmask: bit N set if any batch has submeshLevel==N @@ -215,6 +216,7 @@ struct M2Instance { bool cachedIsGroundDetail = false; bool cachedIsInvisibleTrap = false; bool cachedIsInstancePortal = false; + bool cachedIsSkyBird = false; bool cachedIsValid = false; bool skipCollision = false; // WMO interior doodads — skip player wall collision float cachedBoundRadius = 0.0f; diff --git a/src/rendering/m2_model_classifier.cpp b/src/rendering/m2_model_classifier.cpp index 1e8044ec..b72395d1 100644 --- a/src/rendering/m2_model_classifier.cpp +++ b/src/rendering/m2_model_classifier.cpp @@ -200,6 +200,18 @@ M2ClassificationResult classifyM2Model( }); const bool ambientCreature = hasAny(n, kAmbientTokens); + // --------------------------------------------------------------- + // Sky birds / bats: animated flying doodads that look frozen beyond bone range + // --------------------------------------------------------------- + static constexpr auto kSkyBirdTokens = std::to_array({ + "albatross", "carrionbird", "crane", "crow", + "eagle", "gull", "hawk", "osprey", + "owl", "parrot", "pelican", + "raven", "seagull", "vulture", + }); + r.isSkyBird = hasAny(n, kSkyBirdTokens) || has(n, "\\bird") + || has(n, "\\bat\\") || has(n, "\\bat."); + // --------------------------------------------------------------- // Animation / foliage rendering flags // --------------------------------------------------------------- diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 33a7b1b5..4827ce16 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1245,6 +1245,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.isWaterfall = cls.isWaterfall; gpuModel.isBrazierOrFire = cls.isBrazierOrFire; gpuModel.isTorch = cls.isTorch; + gpuModel.isSkyBird = cls.isSkyBird; gpuModel.ambientEmitterType = cls.ambientEmitterType; gpuModel.boundMin = tightMin; gpuModel.boundMax = tightMax; diff --git a/src/rendering/m2_renderer_render.cpp b/src/rendering/m2_renderer_render.cpp index be415c13..d39b3d43 100644 --- a/src/rendering/m2_renderer_render.cpp +++ b/src/rendering/m2_renderer_render.cpp @@ -90,6 +90,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, instance.cachedIsGroundDetail = mdlRef.isGroundDetail; instance.cachedIsInvisibleTrap = mdlRef.isInvisibleTrap; instance.cachedIsInstancePortal = mdlRef.isInstancePortal; + instance.cachedIsSkyBird = mdlRef.isSkyBird; instance.cachedIsValid = mdlRef.isValid(); instance.cachedModel = &mdlRef; instance.recomputeCachedCullFactors(); @@ -212,6 +213,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& instance.cachedBoundRadius = mdl2.boundRadius; instance.cachedIsGroundDetail = mdl2.isGroundDetail; instance.cachedIsInvisibleTrap = mdl2.isInvisibleTrap; + instance.cachedIsSkyBird = mdl2.isSkyBird; instance.cachedIsValid = mdl2.isValid(); instance.cachedModel = &mdl2; instance.recomputeCachedCullFactors(); @@ -870,6 +872,16 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const if (paddedRadius > 0.0f && !frustum.intersectsSphere(instance.position, paddedRadius)) continue; } + // Animated sky birds: cap render distance to bone-update range so they + // fade in only when close enough for animation, hiding frozen poses. + if (instance.cachedIsSkyBird && instance.cachedHasAnimation && !instance.cachedDisableAnimation) { + constexpr float kBirdMaxDistSq = rendering::M2_LOD3_DISTANCE * rendering::M2_LOD3_DISTANCE; + if (effectiveMaxDistSq > kBirdMaxDistSq) { + effectiveMaxDistSq = kBirdMaxDistSq; + if (distSq > effectiveMaxDistSq) continue; + } + } + sortedVisible_.push_back({i, instance.modelId, distSq, effectiveMaxDistSq}); }