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.
This commit is contained in:
Kelsi
2026-07-11 11:40:49 -07:00
parent b748eba0e6
commit a36cc3553c
5 changed files with 28 additions and 0 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<std::string_view>({
"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
// ---------------------------------------------------------------

View File

@@ -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;

View File

@@ -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});
}