From 2219ccde515931cc3a2c6d051b42ff1d1ce64818 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 25 Feb 2026 10:22:05 -0800 Subject: [PATCH] Optimize city performance and harden WMO grounding --- include/rendering/camera_controller.hpp | 2 + include/rendering/renderer.hpp | 1 + src/audio/ambient_sound_manager.cpp | 9 +- src/rendering/camera_controller.cpp | 118 ++++++++++++++++-------- src/rendering/character_renderer.cpp | 24 ++++- src/rendering/quest_marker_renderer.cpp | 9 +- src/rendering/renderer.cpp | 49 +++++++--- src/rendering/wmo_renderer.cpp | 39 ++++++++ 8 files changed, 196 insertions(+), 55 deletions(-) diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 835627bc..3431ce55 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -171,6 +171,8 @@ private: // Cached isInsideWMO result (throttled to avoid per-frame cost) bool cachedInsideWMO = false; bool cachedInsideInteriorWMO = false; + int insideStateCheckCounter_ = 0; + glm::vec3 lastInsideStateCheckPos_ = glm::vec3(0.0f); int insideWMOCheckCounter = 0; glm::vec3 lastInsideWMOCheckPos = glm::vec3(0.0f); diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 4f163e89..3e3dba7f 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -238,6 +238,7 @@ private: glm::vec3 shadowCenter = glm::vec3(0.0f); bool shadowCenterInitialized = false; bool shadowsEnabled = true; + uint32_t shadowFrameCounter_ = 0; public: diff --git a/src/audio/ambient_sound_manager.cpp b/src/audio/ambient_sound_manager.cpp index 7ab88689..5e820ef7 100644 --- a/src/audio/ambient_sound_manager.cpp +++ b/src/audio/ambient_sound_manager.cpp @@ -296,7 +296,8 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v const int MAX_ACTIVE_WATER = 3; // Max 3 water sounds at once for (auto& emitter : emitters_) { - float distance = glm::distance(emitter.position, cameraPos); + const glm::vec3 delta = emitter.position - cameraPos; + const float distSq = glm::dot(delta, delta); // Determine max distance based on type float maxDist = MAX_AMBIENT_DISTANCE; @@ -317,7 +318,8 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v } // Update active state based on distance AND limits - bool withinRange = (distance < maxDist); + const float maxDistSq = maxDist * maxDist; + const bool withinRange = (distSq < maxDistSq); if (isFire && withinRange && activeFireCount < MAX_ACTIVE_FIRE) { emitter.active = true; @@ -336,6 +338,9 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v // Update play timer emitter.lastPlayTime += deltaTime; + // We only need the true distance for volume attenuation once the emitter is active. + const float distance = std::sqrt(distSq); + // Handle different emitter types switch (emitter.type) { case AmbientType::FIREPLACE_SMALL: diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 9dbc2b71..4f04707c 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -126,6 +126,8 @@ void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; } + // Keep physics integration stable during render hitches to avoid floor tunneling. + const float physicsDeltaTime = std::min(deltaTime, 1.0f / 30.0f); // During taxi flights, skip movement logic but keep camera orbit/zoom controls. if (externalFollow_) { @@ -442,7 +444,7 @@ void CameraController::update(float deltaTime) { if (glm::length(swimMove) > 0.001f) { swimMove = glm::normalize(swimMove); - targetPos += swimMove * swimSpeed * deltaTime; + targetPos += swimMove * swimSpeed * physicsDeltaTime; } // Spacebar = swim up (continuous, not a jump) @@ -451,7 +453,7 @@ void CameraController::update(float deltaTime) { verticalVelocity = SWIM_BUOYANCY; } else { // Gentle sink when not pressing space - verticalVelocity += SWIM_GRAVITY * deltaTime; + verticalVelocity += SWIM_GRAVITY * physicsDeltaTime; if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } @@ -459,15 +461,15 @@ void CameraController::update(float deltaTime) { // you afloat unless you're intentionally diving. if (!diveIntent) { float surfaceErr = (waterSurfaceZ - targetPos.z); - verticalVelocity += surfaceErr * 7.0f * deltaTime; - verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime); + verticalVelocity += surfaceErr * 7.0f * physicsDeltaTime; + verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * physicsDeltaTime); if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) { verticalVelocity = 0.0f; } } } - targetPos.z += verticalVelocity * deltaTime; + targetPos.z += verticalVelocity * physicsDeltaTime; // Don't rise above water surface if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) { @@ -557,7 +559,7 @@ void CameraController::update(float deltaTime) { if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); - targetPos += movement * speed * deltaTime; + targetPos += movement * speed * physicsDeltaTime; } // Jump with input buffering and coyote time @@ -572,12 +574,12 @@ void CameraController::update(float deltaTime) { coyoteTimer = 0.0f; } - jumpBufferTimer -= deltaTime; - coyoteTimer -= deltaTime; + jumpBufferTimer -= physicsDeltaTime; + coyoteTimer -= physicsDeltaTime; // Apply gravity - verticalVelocity += gravity * deltaTime; - targetPos.z += verticalVelocity * deltaTime; + verticalVelocity += gravity * physicsDeltaTime; + targetPos.z += verticalVelocity * physicsDeltaTime; } } else { // External follow (e.g., taxi): trust server position without grounding. @@ -589,14 +591,21 @@ void CameraController::update(float deltaTime) { // Refresh inside-WMO state before collision/grounding so we don't use stale // terrain-first caches while entering enclosed tunnel/building spaces. if (wmoRenderer && !externalFollow_) { - bool prevInside = cachedInsideWMO; - bool prevInsideInterior = cachedInsideInteriorWMO; - cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr); - cachedInsideInteriorWMO = wmoRenderer->isInsideInteriorWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f); - if (cachedInsideWMO != prevInside || cachedInsideInteriorWMO != prevInsideInterior) { - hasCachedFloor_ = false; - hasCachedCamFloor = false; - cachedPivotLift_ = 0.0f; + const float insideDist = glm::length(targetPos - lastInsideStateCheckPos_); + if (++insideStateCheckCounter_ >= 2 || insideDist > 0.35f) { + insideStateCheckCounter_ = 0; + lastInsideStateCheckPos_ = targetPos; + + bool prevInside = cachedInsideWMO; + bool prevInsideInterior = cachedInsideInteriorWMO; + cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr); + cachedInsideInteriorWMO = cachedInsideWMO && + wmoRenderer->isInsideInteriorWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f); + if (cachedInsideWMO != prevInside || cachedInsideInteriorWMO != prevInsideInterior) { + hasCachedFloor_ = false; + hasCachedCamFloor = false; + cachedPivotLift_ = 0.0f; + } } } @@ -662,7 +671,7 @@ void CameraController::update(float deltaTime) { // Collision cache: skip expensive checks if barely moved (15cm threshold) float distMoved = glm::length(glm::vec2(targetPos.x, targetPos.y) - glm::vec2(lastCollisionCheckPos_.x, lastCollisionCheckPos_.y)); - bool useCached = hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE; + bool useCached = grounded && hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE; if (useCached) { // Never trust cached ground while actively descending or when // vertical drift from cached floor is meaningful. @@ -759,13 +768,14 @@ void CameraController::update(float deltaTime) { // Transition safety: if no reachable floor was selected, choose the higher // of terrain/WMO center surfaces when it is still near the player. // This avoids dropping into void gaps at terrain<->WMO seams. + const bool nearWmoSpace = cachedInsideWMO || centerWmoH.has_value(); if (!groundH) { auto highestCenter = selectHighestFloor(centerTerrainH, centerWmoH, std::nullopt); if (highestCenter) { float dz = targetPos.z - *highestCenter; // Keep this fallback narrow: only for WMO seam cases, or very short // transient misses while still almost touching the last floor. - bool allowFallback = cachedInsideWMO || (noGroundTimer_ < 0.10f && dz < 0.6f); + bool allowFallback = nearWmoSpace || (noGroundTimer_ < 0.10f && dz < 0.6f); if (allowFallback && dz >= -0.5f && dz < 2.0f) { groundH = highestCenter; } @@ -774,7 +784,7 @@ void CameraController::update(float deltaTime) { // Continuity guard only for WMO seam overlap: avoid instantly switching to a // much lower floor sample at tunnel mouths (bad WMO ramp chains into void). - if (groundH && hasRealGround_ && cachedInsideWMO && !cachedInsideInteriorWMO) { + if (groundH && hasRealGround_ && nearWmoSpace && !cachedInsideInteriorWMO) { float dropFromLast = lastGroundZ - *groundH; if (dropFromLast > 1.5f) { if (centerTerrainH && *centerTerrainH > *groundH + 1.5f) { @@ -785,7 +795,7 @@ void CameraController::update(float deltaTime) { // Seam stability: while overlapping WMO shells, cap how fast floor height can // step downward in a single frame to avoid following bad ramp samples into void. - if (groundH && cachedInsideWMO && !cachedInsideInteriorWMO && lastGroundZ > 1.0f) { + if (groundH && nearWmoSpace && !cachedInsideInteriorWMO && lastGroundZ > 1.0f) { float maxDropPerFrame = (verticalVelocity < -8.0f) ? 2.0f : 0.60f; float minAllowed = lastGroundZ - maxDropPerFrame; // Extra seam guard: outside interior groups, avoid accepting floors that @@ -804,7 +814,7 @@ void CameraController::update(float deltaTime) { // 1b. Multi-sample WMO floors when in/near WMO space to avoid // falling through narrow board/plank gaps where center ray misses. - if (wmoRenderer && cachedInsideWMO) { + if (wmoRenderer && nearWmoSpace) { constexpr float WMO_FOOTPRINT = 0.35f; const glm::vec2 wmoOffsets[] = { {0.0f, 0.0f}, @@ -835,6 +845,37 @@ void CameraController::update(float deltaTime) { } } + // WMO recovery probe: when no floor is found while descending, do a wider + // footprint sample around the player to catch narrow plank/stair misses. + if (!groundH && wmoRenderer && hasRealGround_ && verticalVelocity <= 0.0f) { + constexpr float RESCUE_FOOTPRINT = 0.55f; + const glm::vec2 rescueOffsets[] = { + {0.0f, 0.0f}, + { RESCUE_FOOTPRINT, 0.0f}, {-RESCUE_FOOTPRINT, 0.0f}, + {0.0f, RESCUE_FOOTPRINT}, {0.0f, -RESCUE_FOOTPRINT}, + { RESCUE_FOOTPRINT, RESCUE_FOOTPRINT}, + { RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT}, + {-RESCUE_FOOTPRINT, RESCUE_FOOTPRINT}, + {-RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT} + }; + float rescueProbeZ = std::max(lastGroundZ, targetPos.z) + stepUpBudget + 1.2f; + std::optional rescueFloor; + for (const auto& o : rescueOffsets) { + float nz = 1.0f; + auto wh = wmoRenderer->getFloorHeight(targetPos.x + o.x, targetPos.y + o.y, rescueProbeZ, &nz); + if (!wh) continue; + if (nz < MIN_WALKABLE_NORMAL_WMO) continue; + if (*wh > lastGroundZ + stepUpBudget + 0.75f) continue; + if (*wh < targetPos.z - 4.0f) continue; + if (!rescueFloor || *wh > *rescueFloor) { + rescueFloor = wh; + } + } + if (rescueFloor) { + groundH = rescueFloor; + } + } + // 2. Multi-sample for M2 objects (rugs, planks, bridges, ships) — // these are narrow and need offset probes to detect reliably. if (m2Renderer && !externalFollow_) { @@ -895,14 +936,15 @@ void CameraController::update(float deltaTime) { } } else { hasRealGround_ = false; - noGroundTimer_ += deltaTime; + noGroundTimer_ += physicsDeltaTime; float dropFromLastGround = lastGroundZ - targetPos.z; - bool seamSizedGap = dropFromLastGround <= 0.35f; + bool seamSizedGap = dropFromLastGround <= (nearWmoSpace ? 1.0f : 0.35f); if (noGroundTimer_ < NO_GROUND_GRACE && seamSizedGap) { // Micro-gap grace only: keep continuity for tiny seam misses, // but never convert air into persistent ground. - targetPos.z = std::max(targetPos.z, lastGroundZ - 0.10f); + float maxSlip = nearWmoSpace ? 0.25f : 0.10f; + targetPos.z = std::max(targetPos.z, lastGroundZ - maxSlip); grounded = false; } else { grounded = false; @@ -918,7 +960,7 @@ void CameraController::update(float deltaTime) { // Player is safely on real geometry — save periodically continuousFallTime_ = 0.0f; autoUnstuckFired_ = false; - safePosSaveTimer_ += deltaTime; + safePosSaveTimer_ += physicsDeltaTime; if (safePosSaveTimer_ >= SAFE_POS_SAVE_INTERVAL) { safePosSaveTimer_ = 0.0f; lastSafePos_ = targetPos; @@ -926,7 +968,7 @@ void CameraController::update(float deltaTime) { } } else if (!grounded && !swimming && !externalFollow_) { // Falling (or standing on nothing past grace period) — accumulate fall time - continuousFallTime_ += deltaTime; + continuousFallTime_ += physicsDeltaTime; if (continuousFallTime_ >= AUTO_UNSTUCK_FALL_TIME && !autoUnstuckFired_) { autoUnstuckFired_ = true; if (autoUnstuckCallback_) { @@ -1179,27 +1221,27 @@ void CameraController::update(float deltaTime) { if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); - newPos += movement * swimSpeed * deltaTime; + newPos += movement * swimSpeed * physicsDeltaTime; } if (nowJump) { verticalVelocity = SWIM_BUOYANCY; } else { - verticalVelocity += SWIM_GRAVITY * deltaTime; + verticalVelocity += SWIM_GRAVITY * physicsDeltaTime; if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } if (!diveIntent) { float surfaceErr = (waterSurfaceCamZ - newPos.z); - verticalVelocity += surfaceErr * 7.0f * deltaTime; - verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime); + verticalVelocity += surfaceErr * 7.0f * physicsDeltaTime; + verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * physicsDeltaTime); if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) { verticalVelocity = 0.0f; } } } - newPos.z += verticalVelocity * deltaTime; + newPos.z += verticalVelocity * physicsDeltaTime; // Don't rise above water surface (feet at water level) if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) { @@ -1213,7 +1255,7 @@ void CameraController::update(float deltaTime) { if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); - newPos += movement * speed * deltaTime; + newPos += movement * speed * physicsDeltaTime; } // Jump with input buffering and coyote time @@ -1227,12 +1269,12 @@ void CameraController::update(float deltaTime) { coyoteTimer = 0.0f; } - jumpBufferTimer -= deltaTime; - coyoteTimer -= deltaTime; + jumpBufferTimer -= physicsDeltaTime; + coyoteTimer -= physicsDeltaTime; // Apply gravity - verticalVelocity += gravity * deltaTime; - newPos.z += verticalVelocity * deltaTime; + verticalVelocity += gravity * physicsDeltaTime; + newPos.z += verticalVelocity * physicsDeltaTime; } // Wall sweep collision before grounding (skip when stationary). diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index df5bac46..bc2c77d6 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1310,8 +1310,9 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId, } void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { - // Distance culling for animation updates (150 unit radius) - const float animUpdateRadiusSq = 150.0f * 150.0f; + // Distance culling for animation updates in dense areas. + const float animUpdateRadius = static_cast(envSizeOrDefault("WOWEE_CHAR_ANIM_RADIUS", 120)); + const float animUpdateRadiusSq = animUpdateRadius * animUpdateRadius; // Update fade-in opacity for (auto& [id, inst] : instances) { @@ -1404,6 +1405,7 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { for (auto& pair : instances) { auto& instance = pair.second; if (instance.weaponAttachments.empty()) continue; + if (glm::distance2(instance.position, cameraPos) > animUpdateRadiusSq) continue; glm::mat4 charModelMat = instance.hasOverrideModelMatrix ? instance.overrideModelMatrix @@ -1614,6 +1616,12 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (instances.empty() || !opaquePipeline_) { return; } + const float renderRadius = static_cast(envSizeOrDefault("WOWEE_CHAR_RENDER_RADIUS", 130)); + const float renderRadiusSq = renderRadius * renderRadius; + const float nearNoConeCullSq = 16.0f * 16.0f; + const float backfaceDotCull = -0.30f; + const glm::vec3 camPos = camera.getPosition(); + const glm::vec3 camForward = camera.getForward(); uint32_t frameIndex = vkCtx_->getCurrentFrame(); uint32_t frameSlot = frameIndex % 2u; @@ -1647,6 +1655,18 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // Skip invisible instances (e.g., player in first-person mode) if (!instance.visible) continue; + // Character instance culling: avoid drawing far-away / strongly behind-camera + // actors in dense city scenes. + if (!instance.hasOverrideModelMatrix) { + glm::vec3 toInst = instance.position - camPos; + float distSq = glm::dot(toInst, toInst); + if (distSq > renderRadiusSq) continue; + if (distSq > nearNoConeCullSq) { + float invDist = 1.0f / std::sqrt(distSq); + float facingDot = glm::dot(toInst, camForward) * invDist; + if (facingDot < backfaceDotCull) continue; + } + } auto modelIt = models.find(instance.modelId); if (modelIt == models.end()) continue; diff --git a/src/rendering/quest_marker_renderer.cpp b/src/rendering/quest_marker_renderer.cpp index d07096e3..bc481d5a 100644 --- a/src/rendering/quest_marker_renderer.cpp +++ b/src/rendering/quest_marker_renderer.cpp @@ -363,6 +363,8 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe constexpr float MIN_DIST = 4.0f; // Near clamp constexpr float MAX_DIST = 90.0f; // Far fade-out start constexpr float FADE_RANGE = 25.0f; // Fade-out range + constexpr float CULL_DIST = MAX_DIST + FADE_RANGE; + constexpr float CULL_DIST_SQ = CULL_DIST * CULL_DIST; // Get time for bob animation float timeSeconds = SDL_GetTicks() / 1000.0f; @@ -373,6 +375,7 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe // Get camera right and up vectors for billboarding glm::vec3 cameraRight = glm::vec3(view[0][0], view[1][0], view[2][0]); glm::vec3 cameraUp = glm::vec3(view[0][1], view[1][1], view[2][1]); + const glm::vec3 cameraForward = glm::cross(cameraRight, cameraUp); // Bind pipeline vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); @@ -391,7 +394,9 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe // Calculate distance for LOD and culling glm::vec3 toCamera = cameraPos - marker.position; - float dist = glm::length(toCamera); + float distSq = glm::dot(toCamera, toCamera); + if (distSq > CULL_DIST_SQ) continue; + float dist = std::sqrt(distSq); // Calculate fade alpha float fadeAlpha = 1.0f; @@ -425,7 +430,7 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe // Billboard: align quad to face camera model[0] = glm::vec4(cameraRight * size, 0.0f); model[1] = glm::vec4(cameraUp * size, 0.0f); - model[2] = glm::vec4(glm::cross(cameraRight, cameraUp), 0.0f); + model[2] = glm::vec4(cameraForward, 0.0f); // Bind material descriptor set (set 1) for this marker's texture vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index cfd9c21b..d6d3f4b1 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -99,6 +99,15 @@ static bool envFlagEnabled(const char* key, bool defaultValue) { return !(v == "0" || v == "false" || v == "off" || v == "no"); } +static int envIntOrDefault(const char* key, int defaultValue) { + const char* raw = std::getenv(key); + if (!raw || !*raw) return defaultValue; + char* end = nullptr; + long n = std::strtol(raw, &end, 10); + if (end == raw) return defaultValue; + return static_cast(n); +} + static std::vector parseEmoteCommands(const std::string& raw) { std::vector out; std::string cur; @@ -2678,15 +2687,19 @@ void Renderer::update(float deltaTime) { } } + const bool canQueryWmo = (camera && wmoRenderer); + const glm::vec3 camPos = camera ? camera->getPosition() : glm::vec3(0.0f); + uint32_t insideWmoId = 0; + const bool insideWmo = canQueryWmo && + wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId); + // Ambient environmental sounds: fireplaces, water, birds, etc. if (ambientSoundManager && camera && wmoRenderer && cameraController) { - glm::vec3 camPos = camera->getPosition(); - uint32_t wmoId = 0; - bool isIndoor = wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoId); + bool isIndoor = insideWmo; bool isSwimming = cameraController->isSwimming(); // Check if inside blacksmith (96048 = Goldshire blacksmith) - bool isBlacksmith = (wmoId == 96048); + bool isBlacksmith = (insideWmoId == 96048); // Sync weather audio with visual weather system if (weather) { @@ -2747,9 +2760,8 @@ void Renderer::update(float deltaTime) { // Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths) if (wmoRenderer) { - glm::vec3 camPos = camera->getPosition(); - uint32_t wmoModelId = 0; - if (wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoModelId)) { + uint32_t wmoModelId = insideWmoId; + if (insideWmo) { // Check if inside Stormwind WMO (model ID 10047) if (wmoModelId == 10047) { zoneId = 1519; // Stormwind City @@ -3839,6 +3851,19 @@ void Renderer::renderShadowPass() { if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return; if (currentCmd == VK_NULL_HANDLE) return; + const int baseInterval = std::max(1, envIntOrDefault("WOWEE_SHADOW_INTERVAL", 1)); + const int denseInterval = std::max(baseInterval, envIntOrDefault("WOWEE_SHADOW_INTERVAL_DENSE", 3)); + const uint32_t denseCharThreshold = static_cast(std::max(1, envIntOrDefault("WOWEE_DENSE_CHAR_THRESHOLD", 120))); + const uint32_t denseM2Threshold = static_cast(std::max(1, envIntOrDefault("WOWEE_DENSE_M2_THRESHOLD", 900))); + const bool denseScene = + (characterRenderer && characterRenderer->getInstanceCount() >= denseCharThreshold) || + (m2Renderer && m2Renderer->getInstanceCount() >= denseM2Threshold); + const int shadowInterval = denseScene ? denseInterval : baseInterval; + if (++shadowFrameCounter_ < static_cast(shadowInterval)) { + return; + } + shadowFrameCounter_ = 0; + // Compute and store light space matrix; write to per-frame UBO lightSpaceMatrix = computeLightSpaceMatrix(); // Zero matrix means character position isn't set yet — skip shadow pass entirely. @@ -3890,15 +3915,17 @@ void Renderer::renderShadowPass() { vkCmdSetScissor(currentCmd, 0, 1, &sc); // Phase 7/8: render shadow casters - constexpr float kShadowCullRadius = 180.0f; // match kShadowHalfExtent + const float baseShadowCullRadius = static_cast(std::max(40, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS", 180))); + const float denseShadowCullRadius = static_cast(std::max(30, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS_DENSE", 90))); + const float shadowCullRadius = denseScene ? std::min(baseShadowCullRadius, denseShadowCullRadius) : baseShadowCullRadius; if (wmoRenderer) { - wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, kShadowCullRadius); + wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius); } if (m2Renderer) { - m2Renderer->renderShadow(currentCmd, lightSpaceMatrix, globalTime, shadowCenter, kShadowCullRadius); + m2Renderer->renderShadow(currentCmd, lightSpaceMatrix, globalTime, shadowCenter, shadowCullRadius); } if (characterRenderer) { - characterRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, kShadowCullRadius); + characterRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius); } vkCmdEndRenderPass(currentCmd); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 40338e11..8a3bd00e 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -2699,6 +2699,42 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ } }; + // Fast path: current active interior group and its neighbors are usually + // the right answer for player-floor queries while moving in cities/buildings. + if (activeGroup_.isValid() && activeGroup_.instanceIdx < instances.size()) { + const auto& instance = instances[activeGroup_.instanceIdx]; + auto it = loadedModels.find(instance.modelId); + if (it != loadedModels.end() && instance.modelId == activeGroup_.modelId) { + const ModelData& model = it->second; + glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f)); + glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f))); + + auto testGroupIdx = [&](uint32_t gi) { + if (gi >= model.groups.size()) return; + if (gi < instance.worldGroupBounds.size()) { + const auto& [gMin, gMax] = instance.worldGroupBounds[gi]; + if (glX < gMin.x || glX > gMax.x || + glY < gMin.y || glY > gMax.y || + glZ - 4.0f > gMax.z) { + return; + } + } + const auto& group = model.groups[gi]; + if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) { + return; + } + testGroupFloor(instance, model, group, localOrigin, localDir); + }; + + if (activeGroup_.groupIdx >= 0) { + testGroupIdx(static_cast(activeGroup_.groupIdx)); + } + for (uint32_t ngi : activeGroup_.neighborGroups) { + testGroupIdx(ngi); + } + } + } + // Full scan: test all instances (active group fast path removed to fix // bridge clipping where early-return missed other WMO instances) glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 8.0f); @@ -2720,6 +2756,9 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ float zMarginUp = model.isLowPlatform ? 20.0f : 4.0f; // Broad-phase reject in world space to avoid expensive matrix transforms. + if (bestFloor && instance.worldBoundsMax.z <= (*bestFloor + 0.05f)) { + continue; + } if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x || glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y || glZ < instance.worldBoundsMin.z - zMarginDown || glZ > instance.worldBoundsMax.z + zMarginUp) {