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).
This commit is contained in:
Kelsi 2026-03-27 18:11:20 -07:00
parent cccd52b32f
commit dce11a0d3f
2 changed files with 34 additions and 3 deletions

View file

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

View file

@ -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<float>(surface.width);
const glm::vec3 extentY = surface.stepY * static_cast<float>(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);