From f00d13bfc0809d5e1fc1ae0a554a94051b7fb962 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 3 Feb 2026 17:21:04 -0800 Subject: [PATCH] Improve performance and tune ramp/planter collision behavior --- include/audio/footstep_manager.hpp | 2 + include/rendering/camera_controller.hpp | 4 +- include/rendering/m2_renderer.hpp | 1 + include/rendering/minimap.hpp | 6 + include/rendering/terrain_manager.hpp | 10 +- src/audio/footstep_manager.cpp | 10 + src/pipeline/m2_loader.cpp | 5 +- src/rendering/camera_controller.cpp | 77 +++---- src/rendering/m2_renderer.cpp | 32 ++- src/rendering/minimap.cpp | 18 +- src/rendering/terrain_manager.cpp | 280 +++++++++++++++--------- src/rendering/wmo_renderer.cpp | 29 ++- 12 files changed, 310 insertions(+), 164 deletions(-) diff --git a/include/audio/footstep_manager.hpp b/include/audio/footstep_manager.hpp index 0c4e7321..6efa3d9c 100644 --- a/include/audio/footstep_manager.hpp +++ b/include/audio/footstep_manager.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace wowee { namespace pipeline { class AssetManager; } @@ -57,6 +58,7 @@ private: std::string tempFilePath = "/tmp/wowee_footstep.wav"; pid_t playerPid = -1; + std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{}; std::mt19937 rng; }; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index b2d6c207..35e571b6 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -94,8 +94,8 @@ private: static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height static constexpr float CAM_SPHERE_RADIUS = 0.32f; // Keep camera farther from geometry to avoid clipping-through surfaces static constexpr float CAM_EPSILON = 0.22f; // Extra wall offset to avoid near-plane clipping artifacts - static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 90.0f; - static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 70.0f; + static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 42.0f; + static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 34.0f; static constexpr float MIN_PITCH = -88.0f; // Look almost straight down static constexpr float MAX_PITCH = 35.0f; // Limited upward look glm::vec3* followTarget = nullptr; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index b345d393..27af8f45 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -44,6 +44,7 @@ struct M2ModelGPU { float boundRadius = 0.0f; bool collisionSteppedFountain = false; bool collisionSteppedLowPlatform = false; + bool collisionPlanter = false; bool collisionNoBlock = false; std::string name; diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp index 6654a52e..e91423fd 100644 --- a/include/rendering/minimap.hpp +++ b/include/rendering/minimap.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace wowee { @@ -48,6 +49,11 @@ private: int mapSize = 200; float viewRadius = 500.0f; bool enabled = false; + float updateIntervalSec = 0.25f; + float updateDistance = 6.0f; + std::chrono::steady_clock::time_point lastUpdateTime = std::chrono::steady_clock::time_point{}; + glm::vec3 lastUpdatePos = glm::vec3(0.0f); + bool hasCachedFrame = false; }; } // namespace rendering diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 34777538..5b712894 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -246,8 +247,8 @@ private: // Streaming parameters bool streamingEnabled = true; - int loadRadius = 2; // Load tiles within this radius (5x5 grid for performance) - int unloadRadius = 3; // Unload tiles beyond this radius + int loadRadius = 1; // Load tiles within this radius (3x3 grid for better CPU/GPU perf) + int unloadRadius = 2; // Unload tiles beyond this radius float updateInterval = 0.1f; // Check streaming every 0.1 seconds float timeSinceLastUpdate = 0.0f; @@ -257,8 +258,9 @@ private: static constexpr float TILE_SIZE = 533.33333f; // One tile = 533.33 units static constexpr float CHUNK_SIZE = 33.33333f; // One chunk = 33.33 units - // Background loading thread - std::thread workerThread; + // Background loading worker pool + std::vector workerThreads; + int workerCount = 0; std::mutex queueMutex; std::condition_variable queueCV; std::queue loadQueue; diff --git a/src/audio/footstep_manager.cpp b/src/audio/footstep_manager.cpp index 12d490aa..f856f194 100644 --- a/src/audio/footstep_manager.cpp +++ b/src/audio/footstep_manager.cpp @@ -135,6 +135,15 @@ void FootstepManager::reapFinishedProcess() { } bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) { + auto now = std::chrono::steady_clock::now(); + if (lastPlayTime.time_since_epoch().count() != 0) { + float elapsed = std::chrono::duration(now - lastPlayTime).count(); + float minInterval = sprinting ? 0.09f : 0.14f; + if (elapsed < minInterval) { + return false; + } + } + auto& list = surfaces[static_cast(surface)].clips; if (list.empty()) { list = surfaces[static_cast(FootstepSurface::STONE)].clips; @@ -181,6 +190,7 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) { _exit(1); } else if (pid > 0) { playerPid = pid; + lastPlayTime = now; return true; } diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index d2124d18..eb0ae976 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -523,7 +523,10 @@ M2Model M2Loader::load(const std::vector& m2Data) { model.attachmentLookup = readArray(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup); } - core::Logger::getInstance().debug("M2 model loaded: ", model.name); + static int m2LoadLogBudget = 200; + if (m2LoadLogBudget-- > 0) { + core::Logger::getInstance().debug("M2 model loaded: ", model.name); + } return model; } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index cc08bf7c..7a6ec073 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -235,9 +235,9 @@ void CameraController::update(float deltaTime) { glm::vec3 desiredPos = targetPos; float moveDist = glm::length(desiredPos - startPos); // Adaptive CCD: keep per-step movement short, especially on low FPS spikes. - int sweepSteps = std::max(1, std::min(24, static_cast(std::ceil(moveDist / 0.18f)))); + int sweepSteps = std::max(1, std::min(14, static_cast(std::ceil(moveDist / 0.24f)))); if (deltaTime > 0.04f) { - sweepSteps = std::min(28, std::max(sweepSteps, static_cast(std::ceil(deltaTime / 0.016f)) * 3)); + sweepSteps = std::min(16, std::max(sweepSteps, static_cast(std::ceil(deltaTime / 0.016f)) * 2)); } glm::vec3 stepPos = startPos; glm::vec3 stepDelta = (desiredPos - startPos) / static_cast(sweepSteps); @@ -270,42 +270,44 @@ void CameraController::update(float deltaTime) { // WoW-style slope limiting (50 degrees, with sliding) // dot(normal, up) >= 0.64 is walkable, otherwise slide + constexpr bool ENABLE_SLOPE_SLIDE = false; constexpr float MAX_WALK_SLOPE_DOT = 0.6428f; // cos(50°) constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation - { + if (ENABLE_SLOPE_SLIDE) { glm::vec3 oldPos = *followTarget; + float moveXY = glm::length(glm::vec2(targetPos.x - oldPos.x, targetPos.y - oldPos.y)); + if (moveXY >= 0.03f) { + struct GroundSample { + std::optional height; + bool fromM2 = false; + }; + // Helper to get ground height at a position and whether M2 provided the top floor. + auto getGroundAt = [&](float x, float y) -> GroundSample { + std::optional terrainH; + std::optional wmoH; + std::optional m2H; + if (terrainManager) { + terrainH = terrainManager->getHeightAt(x, y); + } + if (wmoRenderer) { + wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f); + } + if (m2Renderer) { + m2H = m2Renderer->getFloorHeight(x, y, targetPos.z); + } + auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f); + bool fromM2 = false; + if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) { + base = m2H; + fromM2 = true; + } + return GroundSample{base, fromM2}; + }; - struct GroundSample { - std::optional height; - bool fromM2 = false; - }; - // Helper to get ground height at a position and whether M2 provided the top floor. - auto getGroundAt = [&](float x, float y) -> GroundSample { - std::optional terrainH; - std::optional wmoH; - std::optional m2H; - if (terrainManager) { - terrainH = terrainManager->getHeightAt(x, y); - } - if (wmoRenderer) { - wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f); - } - if (m2Renderer) { - m2H = m2Renderer->getFloorHeight(x, y, targetPos.z); - } - auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f); - bool fromM2 = false; - if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) { - base = m2H; - fromM2 = true; - } - return GroundSample{base, fromM2}; - }; - - // Get ground height at target position - auto center = getGroundAt(targetPos.x, targetPos.y); - bool skipSlopeCheck = center.height && center.fromM2; - if (center.height && !skipSlopeCheck) { + // Get ground height at target position + auto center = getGroundAt(targetPos.x, targetPos.y); + bool skipSlopeCheck = center.height && center.fromM2; + if (center.height && !skipSlopeCheck) { // Calculate ground normal using height samples auto hPosX = getGroundAt(targetPos.x + SAMPLE_DIST, targetPos.y); @@ -361,6 +363,7 @@ void CameraController::update(float deltaTime) { } } } + } } } @@ -390,7 +393,7 @@ void CameraController::update(float deltaTime) { std::optional groundH; constexpr float FOOTPRINT = 0.28f; const glm::vec2 offsets[] = { - {0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT} + {0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT} }; for (const auto& o : offsets) { auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y); @@ -603,9 +606,9 @@ void CameraController::update(float deltaTime) { glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight); glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight); float moveDist = glm::length(desiredFeet - startFeet); - int sweepSteps = std::max(1, std::min(24, static_cast(std::ceil(moveDist / 0.18f)))); + int sweepSteps = std::max(1, std::min(14, static_cast(std::ceil(moveDist / 0.24f)))); if (deltaTime > 0.04f) { - sweepSteps = std::min(28, std::max(sweepSteps, static_cast(std::ceil(deltaTime / 0.016f)) * 3)); + sweepSteps = std::min(16, std::max(sweepSteps, static_cast(std::ceil(deltaTime / 0.016f)) * 2)); } glm::vec3 stepPos = startFeet; glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast(sweepSteps); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 7a31c1b4..cadc04df 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -70,9 +70,9 @@ float getEffectiveCollisionTopLocal(const M2ModelGPU& model, // use edge distance (not radial) so corner blocks don't become too low and // clip-through at diagonals. float edge = std::max(std::abs(nx), std::abs(ny)); - if (edge > 0.92f) return localMin.z + h * 0.10f; - if (edge > 0.72f) return localMin.z + h * 0.46f; - return localMin.z + h * 0.74f; + if (edge > 0.92f) return localMin.z + h * 0.06f; + if (edge > 0.72f) return localMin.z + h * 0.30f; + return localMin.z + h * 0.62f; } bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to, @@ -351,6 +351,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (likelyCurbName && (lowPlatformShape || lowWideShape))); bool isPlanter = (lowerName.find("planter") != std::string::npos); + gpuModel.collisionPlanter = isPlanter; bool foliageName = (lowerName.find("bush") != std::string::npos) || (lowerName.find("grass") != std::string::npos) || @@ -371,7 +372,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bool softTree = treeLike && !hardTreePart && (canopyLike || vert > horiz * 1.35f); bool smallSoftShape = (horiz < 2.2f && vert < 2.4f); bool mediumFoliageShape = (horiz < 4.5f && vert < 4.5f); - bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName; + bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter; gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) && !forceSolidCurb); } @@ -593,7 +594,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; // Distance-based culling threshold for M2 models - const float maxRenderDistance = 400.0f; // Balance between performance and visibility + const float maxRenderDistance = 180.0f; // Aggressive culling for city performance const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const glm::vec3 camPos = camera.getPosition(); @@ -609,7 +610,12 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: float distSq = glm::dot(toCam, toCam); float worldRadius = model.boundRadius * instance.scale; // Cull small objects (radius < 20) at distance, keep larger objects visible longer - float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 10.0f); + float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 12.0f); + if (worldRadius < 0.8f) { + effectiveMaxDistSq = std::min(effectiveMaxDistSq, 65.0f * 65.0f); + } else if (worldRadius < 1.5f) { + effectiveMaxDistSq = std::min(effectiveMaxDistSq, 95.0f * 95.0f); + } if (distSq > effectiveMaxDistSq) { continue; } @@ -886,7 +892,7 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) glm::vec3 worldTop = glm::vec3(instance.modelMatrix * glm::vec4(localTop, 1.0f)); // Reachability filter: allow a bit more climb for stepped low platforms. - float maxStepUp = model.collisionSteppedLowPlatform ? 1.8f : 1.0f; + float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.2f : 1.8f) : 1.0f; if (worldTop.z > glZ + maxStepUp) continue; if (!bestFloor || worldTop.z > *bestFloor) { @@ -948,12 +954,12 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, // Swept hard clamp for taller blockers only. // Low/stepable objects should be climbable and not "shove" the player off. - float maxStepUp = model.collisionSteppedLowPlatform ? 2.0f : 1.20f; + float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.8f : 2.4f) : 1.20f; bool stepableLowObject = (effectiveTop <= localFrom.z + maxStepUp); bool climbingAttempt = (localPos.z > localFrom.z + 0.18f); bool nearTop = (localFrom.z >= effectiveTop - 0.30f); - bool climbingTowardTop = climbingAttempt && (localFrom.z + 0.35f >= effectiveTop); - bool forceHardLateral = model.collisionSteppedLowPlatform && !nearTop && !climbingTowardTop; + bool climbingTowardTop = climbingAttempt && (localFrom.z + (model.collisionPlanter ? 0.95f : 0.60f) >= effectiveTop); + bool forceHardLateral = model.collisionSteppedLowPlatform && !model.collisionPlanter && !nearTop && !climbingTowardTop; if (!stepableLowObject || forceHardLateral) { float tEnter = 0.0f; glm::vec3 sweepMax = localMax; @@ -988,7 +994,11 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, // Gentle fallback push for overlapping cases. float pushAmount; if (model.collisionSteppedLowPlatform) { - pushAmount = std::clamp(minPush * 0.18f, 0.006f, 0.020f); + if (model.collisionPlanter && stepableLowObject) { + pushAmount = std::clamp(minPush * 0.06f, 0.001f, 0.006f); + } else { + pushAmount = std::clamp(minPush * 0.12f, 0.003f, 0.012f); + } } else if (stepableLowObject) { pushAmount = std::clamp(minPush * 0.12f, 0.002f, 0.015f); } else { diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index b40c29eb..6139cb66 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -140,8 +140,22 @@ void Minimap::shutdown() { void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) { if (!enabled || !terrainRenderer || !fbo) return; - // 1. Render terrain from top-down into FBO - renderTerrainToFBO(playerCamera); + const auto now = std::chrono::steady_clock::now(); + glm::vec3 playerPos = playerCamera.getPosition(); + bool needsRefresh = !hasCachedFrame; + if (!needsRefresh) { + float moved = glm::length(glm::vec2(playerPos.x - lastUpdatePos.x, playerPos.y - lastUpdatePos.y)); + float elapsed = std::chrono::duration(now - lastUpdateTime).count(); + needsRefresh = (moved >= updateDistance) || (elapsed >= updateIntervalSec); + } + + // 1. Render terrain from top-down into FBO (throttled) + if (needsRefresh) { + renderTerrainToFBO(playerCamera); + lastUpdateTime = now; + lastUpdatePos = playerPos; + hasCachedFrame = true; + } // 2. Draw the minimap quad on screen renderQuad(screenWidth, screenHeight); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index dc44f3ec..bbd4235e 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -89,9 +89,12 @@ TerrainManager::~TerrainManager() { if (workerRunning.load()) { workerRunning.store(false); queueCV.notify_all(); - if (workerThread.joinable()) { - workerThread.join(); + for (auto& t : workerThreads) { + if (t.joinable()) { + t.join(); + } } + workerThreads.clear(); } } @@ -109,14 +112,20 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* return false; } - // Start background worker thread + // Start background worker pool workerRunning.store(true); - workerThread = std::thread(&TerrainManager::workerLoop, this); + unsigned hc = std::thread::hardware_concurrency(); + workerCount = static_cast(hc > 0 ? std::min(4u, std::max(2u, hc - 1)) : 2u); + workerThreads.reserve(workerCount); + for (int i = 0; i < workerCount; i++) { + workerThreads.emplace_back(&TerrainManager::workerLoop, this); + } LOG_INFO("Terrain manager initialized (async loading enabled)"); LOG_INFO(" Map: ", mapName); LOG_INFO(" Load radius: ", loadRadius, " tiles"); LOG_INFO(" Unload radius: ", unloadRadius, " tiles"); + LOG_INFO(" Workers: ", workerCount); return true; } @@ -152,7 +161,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) { // Stream tiles if we've moved significantly or initial load if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) { - LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z, + LOG_DEBUG("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z, ") tile=[", newTile.x, ",", newTile.y, "] loaded=", loadedTiles.size()); streamTiles(); @@ -189,7 +198,7 @@ bool TerrainManager::loadTile(int x, int y) { std::unique_ptr TerrainManager::prepareTile(int x, int y) { TileCoord coord = {x, y}; - LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)"); + LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)"); // Load ADT file std::string adtPath = getADTPath(coord); @@ -445,7 +454,7 @@ std::unique_ptr TerrainManager::prepareTile(int x, int y) { } } - LOG_INFO("Prepared tile [", x, ",", y, "]: ", + LOG_DEBUG("Prepared tile [", x, ",", y, "]: ", pending->m2Models.size(), " M2 models, ", pending->m2Placements.size(), " M2 placements, ", pending->wmoModels.size(), " WMOs, ", @@ -459,7 +468,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { int y = pending->coord.y; TileCoord coord = pending->coord; - LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)"); + LOG_DEBUG("Finalizing tile [", x, ",", y, "] (GPU upload)"); // Check if tile was already loaded (race condition guard) or failed if (loadedTiles.find(coord) != loadedTiles.end()) { @@ -517,7 +526,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { } } - LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ", + LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ", loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ", skippedDedup, " dedup skipped)"); } @@ -558,7 +567,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { } } if (loadedLiquids > 0) { - LOG_INFO(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); + LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); } // Upload WMO doodad M2 models @@ -572,7 +581,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { } if (loadedWMOs > 0) { - LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs); + LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs); } } @@ -591,7 +600,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { loadedTiles[coord] = std::move(tile); - LOG_INFO(" Finalized tile [", x, ",", y, "]"); + LOG_DEBUG(" Finalized tile [", x, ",", y, "]"); } void TerrainManager::workerLoop() { @@ -728,9 +737,12 @@ void TerrainManager::unloadAll() { if (workerRunning.load()) { workerRunning.store(false); queueCV.notify_all(); - if (workerThread.joinable()) { - workerThread.join(); + for (auto& t : workerThreads) { + if (t.joinable()) { + t.join(); + } } + workerThreads.clear(); } // Clear queues @@ -809,59 +821,88 @@ std::optional TerrainManager::getHeightAt(float glX, float glY) const { const float unitSize = CHUNK_SIZE / 8.0f; - // Query coordinates are the same as what we pass (terrain uses identity model matrix) - float queryX = glX; - float queryY = glY; + auto sampleTileHeight = [&](const TerrainTile* tile) -> std::optional { + if (!tile || !tile->loaded) return std::nullopt; - for (const auto& [coord, tile] : loadedTiles) { - if (!tile || !tile->loaded) continue; + auto sampleChunk = [&](int cx, int cy) -> std::optional { + if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt; + const auto& chunk = tile->terrain.getChunk(cx, cy); + if (!chunk.hasHeightMap()) return std::nullopt; - for (int cy = 0; cy < 16; cy++) { - for (int cx = 0; cx < 16; cx++) { - const auto& chunk = tile->terrain.getChunk(cx, cy); - if (!chunk.hasHeightMap()) continue; + float chunkMaxX = chunk.position[0]; + float chunkMinX = chunk.position[0] - 8.0f * unitSize; + float chunkMaxY = chunk.position[1]; + float chunkMinY = chunk.position[1] - 8.0f * unitSize; - float chunkMaxX = chunk.position[0]; - float chunkMinX = chunk.position[0] - 8.0f * unitSize; - float chunkMaxY = chunk.position[1]; - float chunkMinY = chunk.position[1] - 8.0f * unitSize; + if (glX < chunkMinX || glX > chunkMaxX || + glY < chunkMinY || glY > chunkMaxY) { + return std::nullopt; + } - if (queryX < chunkMinX || queryX > chunkMaxX || - queryY < chunkMinY || queryY > chunkMaxY) { - continue; - } + // Fractional position within chunk (0-8 range) + float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY + float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX - // Fractional position within chunk (0-8 range) - float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY - float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX + fracX = glm::clamp(fracX, 0.0f, 8.0f); + fracY = glm::clamp(fracY, 0.0f, 8.0f); - fracX = glm::clamp(fracX, 0.0f, 8.0f); - fracY = glm::clamp(fracY, 0.0f, 8.0f); + // Bilinear interpolation on 9x9 outer grid + int gx0 = static_cast(std::floor(fracX)); + int gy0 = static_cast(std::floor(fracY)); + int gx1 = std::min(gx0 + 1, 8); + int gy1 = std::min(gy0 + 1, 8); - // Bilinear interpolation on 9x9 outer grid - int gx0 = static_cast(std::floor(fracX)); - int gy0 = static_cast(std::floor(fracY)); - int gx1 = std::min(gx0 + 1, 8); - int gy1 = std::min(gy0 + 1, 8); + float tx = fracX - gx0; + float ty = fracY - gy0; - float tx = fracX - gx0; - float ty = fracY - gy0; + float h00 = chunk.heightMap.heights[gy0 * 17 + gx0]; + float h10 = chunk.heightMap.heights[gy0 * 17 + gx1]; + float h01 = chunk.heightMap.heights[gy1 * 17 + gx0]; + float h11 = chunk.heightMap.heights[gy1 * 17 + gx1]; - // Outer vertex heights from the 9x17 layout - // Outer vertex (gx, gy) is at index: gy * 17 + gx - float h00 = chunk.heightMap.heights[gy0 * 17 + gx0]; - float h10 = chunk.heightMap.heights[gy0 * 17 + gx1]; - float h01 = chunk.heightMap.heights[gy1 * 17 + gx0]; - float h11 = chunk.heightMap.heights[gy1 * 17 + gx1]; + float h = h00 * (1 - tx) * (1 - ty) + + h10 * tx * (1 - ty) + + h01 * (1 - tx) * ty + + h11 * tx * ty; - float h = h00 * (1 - tx) * (1 - ty) + - h10 * tx * (1 - ty) + - h01 * (1 - tx) * ty + - h11 * tx * ty; + return chunk.position[2] + h; + }; - return chunk.position[2] + h; + // Fast path: infer likely chunk index and probe 3x3 neighborhood. + int guessCy = glm::clamp(static_cast(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15); + int guessCx = glm::clamp(static_cast(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15); + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + auto h = sampleChunk(guessCx + dx, guessCy + dy); + if (h) return h; } } + + // Fallback full scan for robustness at seams/unusual coords. + for (int cy = 0; cy < 16; cy++) { + for (int cx = 0; cx < 16; cx++) { + auto h = sampleChunk(cx, cy); + if (h) { + return h; + } + } + } + return std::nullopt; + }; + + // Fast path: sample the expected containing tile first. + TileCoord tc = worldToTile(glX, glY); + auto it = loadedTiles.find(tc); + if (it != loadedTiles.end()) { + auto h = sampleTileHeight(it->second.get()); + if (h) return h; + } + + // Fallback: check all loaded tiles (handles seam/edge coordinate ambiguity). + for (const auto& [coord, tile] : loadedTiles) { + if (coord == tc) continue; + auto h = sampleTileHeight(tile.get()); + if (h) return h; } return std::nullopt; @@ -870,61 +911,92 @@ std::optional TerrainManager::getHeightAt(float glX, float glY) const { std::optional TerrainManager::getDominantTextureAt(float glX, float glY) const { const float unitSize = CHUNK_SIZE / 8.0f; std::vector alphaScratch; + auto sampleTileTexture = [&](const TerrainTile* tile) -> std::optional { + if (!tile || !tile->loaded) return std::nullopt; - for (const auto& [coord, tile] : loadedTiles) { - (void)coord; - if (!tile || !tile->loaded) continue; + auto sampleChunkTexture = [&](int cx, int cy) -> std::optional { + if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt; + const auto& chunk = tile->terrain.getChunk(cx, cy); + if (!chunk.hasHeightMap() || chunk.layers.empty()) return std::nullopt; + + float chunkMaxX = chunk.position[0]; + float chunkMinX = chunk.position[0] - 8.0f * unitSize; + float chunkMaxY = chunk.position[1]; + float chunkMinY = chunk.position[1] - 8.0f * unitSize; + if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) { + return std::nullopt; + } + + float fracY = (chunk.position[0] - glX) / unitSize; + float fracX = (chunk.position[1] - glY) / unitSize; + fracX = glm::clamp(fracX, 0.0f, 8.0f); + fracY = glm::clamp(fracY, 0.0f, 8.0f); + + int alphaX = glm::clamp(static_cast((fracX / 8.0f) * 63.0f), 0, 63); + int alphaY = glm::clamp(static_cast((fracY / 8.0f) * 63.0f), 0, 63); + int alphaIndex = alphaY * 64 + alphaX; + + std::vector weights(chunk.layers.size(), 0); + int accum = 0; + for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) { + int alpha = 0; + if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast(alphaScratch.size())) { + alpha = alphaScratch[alphaIndex]; + } + weights[layerIdx] = alpha; + accum += alpha; + } + weights[0] = glm::clamp(255 - accum, 0, 255); + + size_t bestLayer = 0; + int bestWeight = weights[0]; + for (size_t i = 1; i < weights.size(); i++) { + if (weights[i] > bestWeight) { + bestWeight = weights[i]; + bestLayer = i; + } + } + + uint32_t texId = chunk.layers[bestLayer].textureId; + if (texId < tile->terrain.textures.size()) { + return tile->terrain.textures[texId]; + } + return std::nullopt; + }; + + int guessCy = glm::clamp(static_cast(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15); + int guessCx = glm::clamp(static_cast(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15); + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + auto tex = sampleChunkTexture(guessCx + dx, guessCy + dy); + if (tex) return tex; + } + } for (int cy = 0; cy < 16; cy++) { for (int cx = 0; cx < 16; cx++) { - const auto& chunk = tile->terrain.getChunk(cx, cy); - if (!chunk.hasHeightMap() || chunk.layers.empty()) continue; - - float chunkMaxX = chunk.position[0]; - float chunkMinX = chunk.position[0] - 8.0f * unitSize; - float chunkMaxY = chunk.position[1]; - float chunkMinY = chunk.position[1] - 8.0f * unitSize; - if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) { - continue; + auto tex = sampleChunkTexture(cx, cy); + if (tex) { + return tex; } - - float fracY = (chunk.position[0] - glX) / unitSize; - float fracX = (chunk.position[1] - glY) / unitSize; - fracX = glm::clamp(fracX, 0.0f, 8.0f); - fracY = glm::clamp(fracY, 0.0f, 8.0f); - - int alphaX = glm::clamp(static_cast((fracX / 8.0f) * 63.0f), 0, 63); - int alphaY = glm::clamp(static_cast((fracY / 8.0f) * 63.0f), 0, 63); - int alphaIndex = alphaY * 64 + alphaX; - - std::vector weights(chunk.layers.size(), 0); - int accum = 0; - for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) { - int alpha = 0; - if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast(alphaScratch.size())) { - alpha = alphaScratch[alphaIndex]; - } - weights[layerIdx] = alpha; - accum += alpha; - } - weights[0] = glm::clamp(255 - accum, 0, 255); - - size_t bestLayer = 0; - int bestWeight = weights[0]; - for (size_t i = 1; i < weights.size(); i++) { - if (weights[i] > bestWeight) { - bestWeight = weights[i]; - bestLayer = i; - } - } - - uint32_t texId = chunk.layers[bestLayer].textureId; - if (texId < tile->terrain.textures.size()) { - return tile->terrain.textures[texId]; - } - return std::nullopt; } } + return std::nullopt; + }; + + // Fast path: check expected containing tile first. + TileCoord tc = worldToTile(glX, glY); + auto it = loadedTiles.find(tc); + if (it != loadedTiles.end()) { + auto tex = sampleTileTexture(it->second.get()); + if (tex) return tex; + } + + // Fallback: seam/edge case. + for (const auto& [coord, tile] : loadedTiles) { + if (coord == tc) continue; + auto tex = sampleTileTexture(tile.get()); + if (tex) return tex; } return std::nullopt; @@ -958,8 +1030,8 @@ void TerrainManager::streamTiles() { } } - // Notify worker thread that there's work - queueCV.notify_one(); + // Notify workers that there's work + queueCV.notify_all(); // Unload tiles beyond unload radius (well past the camera far clip) std::vector tilesToUnload; diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 1a4b64a4..d110f07d 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -460,7 +460,7 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: // Render all instances with instance-level culling const glm::vec3 camPos = camera.getPosition(); - const float maxRenderDistance = 500.0f; // Reduced for performance + const float maxRenderDistance = 320.0f; // More aggressive culling for city performance const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; for (const auto& instance : instances) { @@ -1094,7 +1094,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, if (normalLen < 0.001f) continue; normal /= normalLen; - // Skip near-horizontal triangles (floors/ceilings), keep sloped tunnel walls. + // Skip near-horizontal triangles (floors/ceilings). if (std::abs(normal.z) > 0.85f) continue; // Get triangle Z range @@ -1104,7 +1104,30 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Only collide with walls in player's vertical range if (triMaxZ < localFeetZ + 0.3f) continue; if (triMinZ > localFeetZ + PLAYER_HEIGHT) continue; - if (triMaxZ <= localFeetZ + MAX_STEP_HEIGHT) continue; // Treat as step-up, not hard wall + + // Lower parts of ramps should be stepable from the side. + // Allow a larger step-up budget for ramp-like triangles. + // Allow running off/onto lower ramp side geometry without invisible wall blocks. + if (normal.z > 0.20f && triMaxZ <= localFeetZ + 1.60f) continue; + // Ignore short near-vertical side strips around ramps/edges. + // These commonly act like invisible side guard rails. + float triHeight = triMaxZ - triMinZ; + if (std::abs(normal.z) < 0.25f && + triHeight < 1.8f && + triMaxZ <= localFeetZ + 1.4f) { + continue; + } + // Let players run off ramp sides: ignore lower side-wall strips + // that sit around foot height and are not true tall building walls. + if (std::abs(normal.z) < 0.45f && + triMinZ <= localFeetZ + 0.20f && + triHeight < 4.0f && + triMaxZ <= localFeetZ + 4.0f) { + continue; + } + + float stepHeightLimit = MAX_STEP_HEIGHT; + if (triMaxZ <= localFeetZ + stepHeightLimit) continue; // Treat as step-up, not hard wall // Swept test: prevent tunneling when crossing a wall between frames. float fromDist = glm::dot(localFrom - v0, normal);