Optimize city performance and harden WMO grounding

This commit is contained in:
Kelsi 2026-02-25 10:22:05 -08:00
parent dd4b72e046
commit 2219ccde51
8 changed files with 196 additions and 55 deletions

View file

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

View file

@ -238,6 +238,7 @@ private:
glm::vec3 shadowCenter = glm::vec3(0.0f);
bool shadowCenterInitialized = false;
bool shadowsEnabled = true;
uint32_t shadowFrameCounter_ = 0;
public:

View file

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

View file

@ -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<float> 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).

View file

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

View file

@ -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_,

View file

@ -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<int>(n);
}
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
std::vector<std::string> 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<uint32_t>(std::max(1, envIntOrDefault("WOWEE_DENSE_CHAR_THRESHOLD", 120)));
const uint32_t denseM2Threshold = static_cast<uint32_t>(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<uint32_t>(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<float>(std::max(40, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS", 180)));
const float denseShadowCullRadius = static_cast<float>(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);

View file

@ -2699,6 +2699,42 @@ std::optional<float> 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<uint32_t>(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<float> 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) {