From dce11a0d3fbab77691ac78e2b19bded5d2fcb6d8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Mar 2026 18:11:20 -0700 Subject: [PATCH] perf: skip bone animation for LOD3 models, frustum-cull water surfaces M2 renderer: skip bone matrix computation for instances beyond 150 units (LOD 3 threshold). These models use minimal static geometry with no visible skeletal animation. Last-computed bone matrices are retained for GPU upload. Removes unnecessary float matrix operations for hundreds of distant NPCs in crowded zones. Water renderer: add per-surface AABB frustum culling before draw calls. Computes tight AABB from surface corners and height range, tests against camera frustum. Skips descriptor binding and vkCmdDrawIndexed for surfaces outside the view. Handles both ADT and WMO water (rotated step vectors). --- src/rendering/m2_renderer.cpp | 9 +++++++-- src/rendering/water_renderer.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index e487f64a..916b0ff0 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2058,10 +2058,15 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f); if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue; + // LOD 3 skip: models beyond 150 units use the lowest LOD mesh which has + // no visible skeletal animation. Keep their last-computed bone matrices + // (always valid — seeded on spawn) and avoid the expensive per-bone work. + constexpr float kLOD3DistSq = 150.0f * 150.0f; + if (distSq > kLOD3DistSq) continue; + // Distance-based frame skipping: update distant bones less frequently uint32_t boneInterval = 1; - if (distSq > 200.0f * 200.0f) boneInterval = 8; - else if (distSq > 100.0f * 100.0f) boneInterval = 4; + if (distSq > 100.0f * 100.0f) boneInterval = 4; else if (distSq > 50.0f * 50.0f) boneInterval = 2; instance.frameSkipCounter++; if ((instance.frameSkipCounter % boneInterval) != 0) continue; diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 88c22879..6d9245f6 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -5,6 +5,7 @@ #include "rendering/vk_utils.hpp" #include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" +#include "rendering/frustum.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/wmo_loader.hpp" #include "core/logger.hpp" @@ -1039,7 +1040,7 @@ void WaterRenderer::clear() { // ============================================================== void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, - const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) { + const Camera& camera, float /*time*/, bool use1x, uint32_t frameIndex) { VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline; if (!renderingEnabled || surfaces.empty() || !pipeline) { if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) { @@ -1059,6 +1060,10 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, return; } + // Frustum culling setup + Frustum frustum; + frustum.extractFromMatrix(camera.getViewProjectionMatrix()); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, @@ -1070,6 +1075,27 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; if (!surface.materialSet) continue; + // Frustum cull: compute AABB from surface origin + step vectors + { + const glm::vec3 extentX = surface.stepX * static_cast(surface.width); + const glm::vec3 extentY = surface.stepY * static_cast(surface.height); + const glm::vec3 c0 = surface.origin; + const glm::vec3 c1 = surface.origin + extentX; + const glm::vec3 c2 = surface.origin + extentY; + const glm::vec3 c3 = surface.origin + extentX + extentY; + const glm::vec3 aabbMin( + std::min({c0.x, c1.x, c2.x, c3.x}), + std::min({c0.y, c1.y, c2.y, c3.y}), + surface.minHeight + ); + const glm::vec3 aabbMax( + std::max({c0.x, c1.x, c2.x, c3.x}), + std::max({c0.y, c1.y, c2.y, c3.y}), + surface.maxHeight + ); + if (!frustum.intersectsAABB(aabbMin, aabbMax)) continue; + } + bool isWmoWater = (surface.wmoId != 0); bool canalProfile = isWmoWater || (surface.liquidType == 5); uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);