diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 9ce374a0..0eabf76f 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -4,6 +4,7 @@ #include "core/input.hpp" #include #include +#include namespace wowee { namespace rendering { @@ -133,6 +134,10 @@ private: static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground + // Cached floor height between frames (skip expensive probes when barely moving) + std::optional cachedFloorHeight; + glm::vec2 cachedFloorPos = glm::vec2(0.0f); + // Cached isInsideWMO result (throttled to avoid per-frame cost) bool cachedInsideWMO = false; int insideWMOCheckCounter = 0; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 9f5a0367..a7153444 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -344,36 +344,38 @@ void CameraController::update(float deltaTime) { } } - // Enforce collision while swimming too (horizontal only), so we don't - // pass through walls/props when underwater or at waterline. + // Enforce collision while swimming too (horizontal only), skip when stationary. { glm::vec3 swimFrom = *followTarget; glm::vec3 swimTo = targetPos; float swimMoveDist = glm::length(swimTo - swimFrom); - int swimSteps = std::max(1, std::min(4, static_cast(std::ceil(swimMoveDist / 0.5f)))); glm::vec3 stepPos = swimFrom; - glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast(swimSteps); - for (int i = 0; i < swimSteps; i++) { - glm::vec3 candidate = stepPos + stepDelta; + if (swimMoveDist > 0.01f) { + int swimSteps = std::max(1, std::min(2, static_cast(std::ceil(swimMoveDist / 1.0f)))); + glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast(swimSteps); - if (wmoRenderer) { - glm::vec3 adjusted; - if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { - candidate.x = adjusted.x; - candidate.y = adjusted.y; + for (int i = 0; i < swimSteps; i++) { + glm::vec3 candidate = stepPos + stepDelta; + + if (wmoRenderer) { + glm::vec3 adjusted; + if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { + candidate.x = adjusted.x; + candidate.y = adjusted.y; + } } - } - if (m2Renderer) { - glm::vec3 adjusted; - if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) { - candidate.x = adjusted.x; - candidate.y = adjusted.y; + if (m2Renderer) { + glm::vec3 adjusted; + if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) { + candidate.x = adjusted.x; + candidate.y = adjusted.y; + } } - } - stepPos = candidate; + stepPos = candidate; + } } targetPos.x = stepPos.x; @@ -411,39 +413,42 @@ void CameraController::update(float deltaTime) { } // Sweep collisions in small steps to reduce tunneling through thin walls/floors. + // Skip entirely when stationary to avoid wasting collision calls. { glm::vec3 startPos = *followTarget; glm::vec3 desiredPos = targetPos; float moveDist = glm::length(desiredPos - startPos); - // Adaptive CCD: larger step size to reduce collision call count. - int sweepSteps = std::max(1, std::min(4, static_cast(std::ceil(moveDist / 0.50f)))); - glm::vec3 stepPos = startPos; - glm::vec3 stepDelta = (desiredPos - startPos) / static_cast(sweepSteps); - for (int i = 0; i < sweepSteps; i++) { - glm::vec3 candidate = stepPos + stepDelta; + if (moveDist > 0.01f) { + // Adaptive CCD: 1.0f step size, max 2 steps. + int sweepSteps = std::max(1, std::min(2, static_cast(std::ceil(moveDist / 1.0f)))); + glm::vec3 stepPos = startPos; + glm::vec3 stepDelta = (desiredPos - startPos) / static_cast(sweepSteps); - if (wmoRenderer) { - glm::vec3 adjusted; - if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { - // Keep vertical motion from physics/grounding; only block horizontal wall penetration. - candidate.x = adjusted.x; - candidate.y = adjusted.y; + for (int i = 0; i < sweepSteps; i++) { + glm::vec3 candidate = stepPos + stepDelta; + + if (wmoRenderer) { + glm::vec3 adjusted; + if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { + candidate.x = adjusted.x; + candidate.y = adjusted.y; + } } + + if (m2Renderer) { + glm::vec3 adjusted; + if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) { + candidate.x = adjusted.x; + candidate.y = adjusted.y; + } + } + + stepPos = candidate; } - if (m2Renderer) { - glm::vec3 adjusted; - if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) { - candidate.x = adjusted.x; - candidate.y = adjusted.y; - } - } - - stepPos = candidate; + targetPos = stepPos; } - - targetPos = stepPos; } // WoW-style slope limiting (50 degrees, with sliding) @@ -574,21 +579,29 @@ void CameraController::update(float deltaTime) { return base; }; - // Single center probe — extra probes are too expensive in WMO-heavy areas. - std::optional groundH = sampleGround(targetPos.x, targetPos.y); + // Use cached floor height if player hasn't moved much horizontally. + float floorPosDist = glm::length(glm::vec2(targetPos.x, targetPos.y) - cachedFloorPos); + std::optional groundH; + if (cachedFloorHeight && floorPosDist < 0.5f) { + groundH = cachedFloorHeight; + } else { + groundH = sampleGround(targetPos.x, targetPos.y); + cachedFloorHeight = groundH; + cachedFloorPos = glm::vec2(targetPos.x, targetPos.y); + } if (groundH) { float groundDiff = *groundH - lastGroundZ; if (groundDiff > 2.0f) { // Landing on a higher ledge - snap up lastGroundZ = *groundH; - } else if (groundDiff > -2.0f) { - // Small height difference - smooth it - lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f); + } else { + // Smooth toward detected ground. Use a slower rate for large + // drops so multi-story buildings don't snap to the wrong floor, + // but always converge so walking off a fountain works. + float rate = (groundDiff > -2.0f) ? 15.0f : 6.0f; + lastGroundZ += groundDiff * std::min(1.0f, deltaTime * rate); } - // If groundDiff < -2.0f (floor much lower), ignore it - we're likely - // on an upper floor and detecting ground floor through a gap. - // Let gravity handle actual falls. if (targetPos.z <= lastGroundZ + 0.1f && verticalVelocity <= 0.0f) { targetPos.z = lastGroundZ; @@ -638,41 +651,15 @@ void CameraController::update(float deltaTime) { // Find max safe distance using raycast + sphere radius collisionDistance = currentDistance; - // Helper to get floor height for camera collision. - // Use the player's ground level as reference to avoid locking the camera - // to upper floors in multi-story buildings. - auto getFloorAt = [&](float x, float y, float /*z*/) -> std::optional { - std::optional terrainH; - std::optional wmoH; + // Camera collision: terrain-only floor clamping (skip expensive WMO raycasts). + // The camera may clip through WMO walls but won't go underground. + auto getTerrainFloorAt = [&](float x, float y) -> std::optional { if (terrainManager) { - terrainH = terrainManager->getHeightAt(x, y); + return terrainManager->getHeightAt(x, y); } - if (wmoRenderer) { - wmoH = wmoRenderer->getFloorHeight(x, y, lastGroundZ + 2.5f); - } - return selectReachableFloor(terrainH, wmoH, lastGroundZ, 2.0f); + return std::nullopt; }; - // Raycast against WMO bounding boxes - if (wmoRenderer && collisionDistance > MIN_DISTANCE) { - float wmoHit = wmoRenderer->raycastBoundingBoxes(pivot, camDir, collisionDistance); - if (wmoHit < collisionDistance) { - collisionDistance = std::max(MIN_DISTANCE, wmoHit - CAM_SPHERE_RADIUS - CAM_EPSILON); - } - } - - // Intentionally ignore M2 doodads for camera collision to match WoW feel. - - // Check floor collision at the camera's target position - { - glm::vec3 testPos = pivot + camDir * collisionDistance; - auto floorH = getFloorAt(testPos.x, testPos.y, testPos.z); - - if (floorH && testPos.z < *floorH + CAM_SPHERE_RADIUS + CAM_EPSILON) { - collisionDistance = std::max(MIN_DISTANCE, collisionDistance - CAM_SPHERE_RADIUS); - } - } - // Use collision distance (don't exceed user target) float actualDist = std::min(currentDistance, collisionDistance); @@ -692,9 +679,9 @@ void CameraController::update(float deltaTime) { float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; - // ===== Final floor clearance check ===== + // ===== Final floor clearance check (terrain only) ===== constexpr float MIN_FLOOR_CLEARANCE = 0.35f; - auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z); + auto finalFloorH = getTerrainFloorAt(smoothedCamPos.x, smoothedCamPos.y); if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) { smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE; } @@ -818,26 +805,28 @@ void CameraController::update(float deltaTime) { newPos.z += verticalVelocity * deltaTime; } - // Wall sweep collision before grounding (reduces tunneling at low FPS/high speed). + // Wall sweep collision before grounding (skip when stationary). if (wmoRenderer) { 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); - // Adaptive CCD: larger step size to reduce collision call count. - int sweepSteps = std::max(1, std::min(4, static_cast(std::ceil(moveDist / 0.50f)))); - glm::vec3 stepPos = startFeet; - glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast(sweepSteps); - for (int i = 0; i < sweepSteps; i++) { - glm::vec3 candidate = stepPos + stepDelta; - glm::vec3 adjusted; - if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { - candidate = adjusted; + if (moveDist > 0.01f) { + int sweepSteps = std::max(1, std::min(2, static_cast(std::ceil(moveDist / 1.0f)))); + glm::vec3 stepPos = startFeet; + glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast(sweepSteps); + + for (int i = 0; i < sweepSteps; i++) { + glm::vec3 candidate = stepPos + stepDelta; + glm::vec3 adjusted; + if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { + candidate = adjusted; + } + stepPos = candidate; } - stepPos = candidate; - } - newPos = stepPos + glm::vec3(0, 0, eyeHeight); + newPos = stepPos + glm::vec3(0, 0, eyeHeight); + } } // Ground to terrain or WMO floor @@ -865,29 +854,17 @@ void CameraController::update(float deltaTime) { return base; }; - std::optional groundH; - constexpr float FOOTPRINT = 0.4f; // Larger footprint for better floor detection - const glm::vec2 offsets[] = { - {0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT} - }; - for (const auto& o : offsets) { - auto h = sampleGround(newPos.x + o.x, newPos.y + o.y); - if (h && (!groundH || *h > *groundH)) { - groundH = h; - } - } + // Single center probe. + std::optional groundH = sampleGround(newPos.x, newPos.y); if (groundH) { float groundDiff = *groundH - lastGroundZ; if (groundDiff > 2.0f) { - // Landing on a higher ledge - snap up - lastGroundZ = *groundH; - } else if (groundDiff > -2.0f) { - // Small difference - accept it lastGroundZ = *groundH; + } else { + float rate = (groundDiff > -2.0f) ? 15.0f : 6.0f; + lastGroundZ += groundDiff * std::min(1.0f, deltaTime * rate); } - // If groundDiff < -2.0f (floor much lower), ignore it - we're likely - // on an upper floor and detecting ground floor through a gap. float groundZ = lastGroundZ + eyeHeight; if (newPos.z <= groundZ) { diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 611417eb..84d56cd5 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -2019,13 +2019,23 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 continue; } - // Narrow-phase: triangle raycast for accurate camera collision. + // Narrow-phase: triangle raycast using spatial grid. const auto& verts = group.collisionVertices; const auto& indices = group.collisionIndices; - for (size_t i = 0; i + 2 < indices.size(); i += 3) { - const glm::vec3& v0 = verts[indices[i]]; - const glm::vec3& v1 = verts[indices[i + 1]]; - const glm::vec3& v2 = verts[indices[i + 2]]; + + // Compute local-space ray endpoint and query grid for XY range + glm::vec3 localEnd = localOrigin + localDir * (closestHit / glm::length( + glm::vec3(instance.modelMatrix * glm::vec4(localDir, 0.0f)))); + float rMinX = std::min(localOrigin.x, localEnd.x) - 1.0f; + float rMinY = std::min(localOrigin.y, localEnd.y) - 1.0f; + float rMaxX = std::max(localOrigin.x, localEnd.x) + 1.0f; + float rMaxY = std::max(localOrigin.y, localEnd.y) + 1.0f; + group.getTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, wallTriScratch); + + for (uint32_t triStart : wallTriScratch) { + const glm::vec3& v0 = verts[indices[triStart]]; + const glm::vec3& v1 = verts[indices[triStart + 1]]; + const glm::vec3& v2 = verts[indices[triStart + 2]]; glm::vec3 triNormal = glm::cross(v1 - v0, v2 - v0); float normalLenSq = glm::dot(triNormal, triNormal); if (normalLenSq < 1e-8f) {