diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 9877838a..79459e04 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -286,7 +286,7 @@ public: * @param glY World Y * @param glZ Query/reference Z (used to ignore unreachable tops) */ - std::optional getFloorHeight(float glX, float glY, float glZ) const; + std::optional getFloorHeight(float glX, float glY, float glZ, float* outNormalZ = nullptr) const; /** * Raycast against M2 bounding boxes for camera collision diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 6ea0d69e..7eee46a1 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -592,6 +592,9 @@ void CameraController::update(float deltaTime) { // 1. Center-only sample for terrain/WMO floor selection. // Using only the center prevents tunnel entrances from snapping // to terrain when offset samples miss the WMO floor geometry. + // Slope limit: reject surfaces too steep to walk (prevent clipping) + constexpr float MIN_WALKABLE_NORMAL = 0.7f; // ~45° max slope + std::optional groundH; { // Collision cache: skip expensive checks if barely moved (15cm threshold) @@ -609,9 +612,16 @@ void CameraController::update(float deltaTime) { terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y); } float wmoProbeZ = std::max(targetPos.z, lastGroundZ) + stepUpBudget + 0.5f; + float wmoNormalZ = 1.0f; if (wmoRenderer) { - wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ); + wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ, &wmoNormalZ); } + + // Reject steep WMO slopes + if (wmoH && wmoNormalZ < MIN_WALKABLE_NORMAL) { + wmoH = std::nullopt; // Treat as unwalkable + } + groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget); // Update cache @@ -636,8 +646,15 @@ void CameraController::update(float deltaTime) { }; float m2ProbeZ = std::max(targetPos.z, lastGroundZ) + 6.0f; for (const auto& o : offsets) { + float m2NormalZ = 1.0f; auto m2H = m2Renderer->getFloorHeight( - targetPos.x + o.x, targetPos.y + o.y, m2ProbeZ); + targetPos.x + o.x, targetPos.y + o.y, m2ProbeZ, &m2NormalZ); + + // Reject steep M2 slopes + if (m2H && m2NormalZ < MIN_WALKABLE_NORMAL) { + continue; // Skip unwalkable M2 surface + } + // Prefer M2 floors (ships, platforms) even if slightly lower than terrain // to prevent falling through ship decks to water below if (m2H && *m2H <= targetPos.z + stepUpBudget) { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 6f02ffde..4c505dd3 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2688,9 +2688,10 @@ uint32_t M2Renderer::getTotalTriangleCount() const { return total; } -std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) const { +std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ, float* outNormalZ) const { QueryTimer timer(&queryTimeMs, &queryCallCount); std::optional bestFloor; + float bestNormalZ = 1.0f; // Default to flat glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 6.0f); glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 8.0f); @@ -2745,12 +2746,13 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) float hitZ = rayOrigin.z - tHit; // Walkable normal check (world space) + glm::vec3 worldN(0.0f, 0.0f, 1.0f); // Default to flat glm::vec3 localN = glm::cross(v1 - v0, v2 - v0); float nLen = glm::length(localN); if (nLen > 0.001f) { localN /= nLen; if (localN.z < 0.0f) localN = -localN; - glm::vec3 worldN = glm::normalize( + worldN = glm::normalize( glm::vec3(instance.modelMatrix * glm::vec4(localN, 0.0f))); if (std::abs(worldN.z) < 0.35f) continue; // too steep (~70° max slope) } @@ -2758,6 +2760,7 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) if (hitZ <= localPos.z + 3.0f && hitZ > bestHitZ) { bestHitZ = hitZ; hitAny = true; + bestNormalZ = std::abs(worldN.z); // Store normal for output } } @@ -2822,6 +2825,11 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) } } + // Output surface normal if requested + if (outNormalZ) { + *outNormalZ = bestNormalZ; + } + return bestFloor; }