diff --git a/include/rendering/camera.hpp b/include/rendering/camera.hpp index f4e5ab84..0464007f 100644 --- a/include/rendering/camera.hpp +++ b/include/rendering/camera.hpp @@ -41,8 +41,8 @@ private: float pitch = 0.0f; float fov = 45.0f; float aspectRatio = 16.0f / 9.0f; - float nearPlane = 0.1f; - float farPlane = 200000.0f; // Large draw distance for terrain visibility + float nearPlane = 0.05f; + float farPlane = 30000.0f; // Improves depth precision vs extremely large far clip glm::mat4 viewMatrix = glm::mat4(1.0f); glm::mat4 projectionMatrix = glm::mat4(1.0f); diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 749718a1..a1721daf 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -92,8 +92,8 @@ private: static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height - static constexpr float CAM_SPHERE_RADIUS = 0.2f; // Collision sphere radius - static constexpr float CAM_EPSILON = 0.05f; // Offset from walls + 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 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 8373c7d5..b14b727f 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -57,6 +57,7 @@ struct M2Instance { glm::vec3 rotation; // Euler angles in degrees float scale; glm::mat4 modelMatrix; + glm::mat4 invModelMatrix; // Animation state float animTime = 0.0f; // Current animation time diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 10c79eec..ed82730b 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -165,41 +165,6 @@ void CameraController::update(float deltaTime) { if (thirdPerson && followTarget) { // Move the follow target (character position) instead of the camera glm::vec3 targetPos = *followTarget; - auto clampMoveAgainstWMO = [&](const glm::vec3& fromFeet, glm::vec3& toFeet) { - if (!wmoRenderer) return; - glm::vec3 move = toFeet - fromFeet; - move.z = 0.0f; - float moveDist = glm::length(glm::vec2(move)); - if (moveDist < 0.001f) return; - - glm::vec3 dir = glm::normalize(move); - glm::vec3 perp(-dir.y, dir.x, 0.0f); - constexpr float BODY_RADIUS = 0.38f; - constexpr float SAFETY = 0.08f; - constexpr float HEIGHTS[3] = {0.4f, 1.0f, 1.6f}; - const glm::vec3 probes[3] = { - fromFeet, - fromFeet + perp * BODY_RADIUS, - fromFeet - perp * BODY_RADIUS - }; - - float bestHit = moveDist + 1.0f; - for (const glm::vec3& probeBase : probes) { - for (float h : HEIGHTS) { - glm::vec3 origin = probeBase + glm::vec3(0.0f, 0.0f, h); - float hit = wmoRenderer->raycastBoundingBoxes(origin, dir, moveDist + SAFETY); - if (hit < bestHit) { - bestHit = hit; - } - } - } - - if (bestHit < moveDist + SAFETY) { - float allowed = std::max(0.0f, bestHit - SAFETY); - toFeet.x = fromFeet.x + dir.x * allowed; - toFeet.y = fromFeet.y + dir.y * allowed; - } - }; // Check for water at current position std::optional waterH; @@ -269,7 +234,6 @@ void CameraController::update(float deltaTime) { for (int i = 0; i < sweepSteps; i++) { glm::vec3 candidate = stepPos + stepDelta; - clampMoveAgainstWMO(stepPos, candidate); if (wmoRenderer) { glm::vec3 adjusted; @@ -471,7 +435,9 @@ void CameraController::update(float deltaTime) { if (wmoRenderer) { wmoH = wmoRenderer->getFloorHeight(x, y, z + 5.0f); } - return selectReachableFloor(terrainH, wmoH, z, 0.5f); + // Camera floor clamp must allow larger step-up on ramps/stairs. + // Too-small limits let the camera slip below rising ground and see through floors. + return selectReachableFloor(terrainH, wmoH, z, 2.0f); }; // Raycast against WMO bounding boxes @@ -518,13 +484,26 @@ void CameraController::update(float deltaTime) { smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; // ===== Final floor clearance check ===== - // After smoothing, ensure camera is above the floor at its final position - // This prevents camera clipping through ground in Stormwind and similar areas - constexpr float MIN_FLOOR_CLEARANCE = 0.20f; // Keep camera at least 20cm above floor - auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z); + // Sample a small footprint around the camera to avoid peeking through ramps/stairs + // when zoomed out and pitched down. + constexpr float MIN_FLOOR_CLEARANCE = 0.35f; + constexpr float FLOOR_SAMPLE_R = 0.35f; + std::optional finalFloorH; + const glm::vec2 floorOffsets[] = { + {0.0f, 0.0f}, {FLOOR_SAMPLE_R, 0.0f}, {-FLOOR_SAMPLE_R, 0.0f}, + {0.0f, FLOOR_SAMPLE_R}, {0.0f, -FLOOR_SAMPLE_R} + }; + for (const auto& o : floorOffsets) { + auto h = getFloorAt(smoothedCamPos.x + o.x, smoothedCamPos.y + o.y, smoothedCamPos.z); + if (h && (!finalFloorH || *h > *finalFloorH)) { + finalFloorH = h; + } + } if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) { smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE; } + // Never let camera sink below the character's feet plane. + smoothedCamPos.z = std::max(smoothedCamPos.z, targetPos.z + 0.15f); camera->setPosition(smoothedCamPos); @@ -538,41 +517,6 @@ void CameraController::update(float deltaTime) { } else { // Free-fly camera mode (original behavior) glm::vec3 newPos = camera->getPosition(); - auto clampMoveAgainstWMO = [&](const glm::vec3& fromFeet, glm::vec3& toFeet) { - if (!wmoRenderer) return; - glm::vec3 move = toFeet - fromFeet; - move.z = 0.0f; - float moveDist = glm::length(glm::vec2(move)); - if (moveDist < 0.001f) return; - - glm::vec3 dir = glm::normalize(move); - glm::vec3 perp(-dir.y, dir.x, 0.0f); - constexpr float BODY_RADIUS = 0.38f; - constexpr float SAFETY = 0.08f; - constexpr float HEIGHTS[3] = {0.4f, 1.0f, 1.6f}; - const glm::vec3 probes[3] = { - fromFeet, - fromFeet + perp * BODY_RADIUS, - fromFeet - perp * BODY_RADIUS - }; - - float bestHit = moveDist + 1.0f; - for (const glm::vec3& probeBase : probes) { - for (float h : HEIGHTS) { - glm::vec3 origin = probeBase + glm::vec3(0.0f, 0.0f, h); - float hit = wmoRenderer->raycastBoundingBoxes(origin, dir, moveDist + SAFETY); - if (hit < bestHit) { - bestHit = hit; - } - } - } - - if (bestHit < moveDist + SAFETY) { - float allowed = std::max(0.0f, bestHit - SAFETY); - toFeet.x = fromFeet.x + dir.x * allowed; - toFeet.y = fromFeet.y + dir.y * allowed; - } - }; float feetZ = newPos.z - eyeHeight; // Check for water at feet position @@ -640,7 +584,6 @@ void CameraController::update(float deltaTime) { for (int i = 0; i < sweepSteps; i++) { glm::vec3 candidate = stepPos + stepDelta; - clampMoveAgainstWMO(stepPos, candidate); glm::vec3 adjusted; if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { candidate = adjusted; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8cec777d..589f1365 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -22,14 +22,43 @@ void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::ve glm::vec3 half = (model.boundMax - model.boundMin) * 0.5f; // Tighten footprint to reduce overly large object blockers. - half.x *= 0.60f; - half.y *= 0.60f; - half.z *= 0.90f; + half.x *= 0.72f; + half.y *= 0.72f; + half.z *= 0.78f; outMin = center - half; outMax = center + half; } +bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to, + const glm::vec3& bmin, const glm::vec3& bmax, + float& outEnterT) { + glm::vec3 d = to - from; + float tEnter = 0.0f; + float tExit = 1.0f; + + for (int axis = 0; axis < 3; axis++) { + if (std::abs(d[axis]) < 1e-6f) { + if (from[axis] < bmin[axis] || from[axis] > bmax[axis]) { + return false; + } + continue; + } + + float inv = 1.0f / d[axis]; + float t0 = (bmin[axis] - from[axis]) * inv; + float t1 = (bmax[axis] - from[axis]) * inv; + if (t0 > t1) std::swap(t0, t1); + + tEnter = std::max(tEnter, t0); + tExit = std::min(tExit, t1); + if (tEnter > tExit) return false; + } + + outEnterT = tEnter; + return tExit >= 0.0f && tEnter <= 1.0f; +} + } // namespace void M2Instance::updateModelMatrix() { @@ -42,6 +71,7 @@ void M2Instance::updateModelMatrix() { modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); modelMatrix = glm::scale(modelMatrix, glm::vec3(scale)); + invModelMatrix = glm::inverse(modelMatrix); } M2Renderer::M2Renderer() { @@ -341,6 +371,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& instance.rotation = glm::vec3(0.0f); instance.scale = 1.0f; instance.modelMatrix = modelMatrix; + instance.invModelMatrix = glm::inverse(modelMatrix); instance.animTime = static_cast(rand()) / RAND_MAX * 10.0f; // Random start time instances.push_back(instance); @@ -571,8 +602,7 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) glm::vec3 localMin, localMax; getTightCollisionBounds(model, localMin, localMax); - glm::mat4 invModel = glm::inverse(instance.modelMatrix); - glm::vec3 localPos = glm::vec3(invModel * glm::vec4(glX, glY, glZ, 1.0f)); + glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f)); // Must be within doodad footprint in local XY. if (localPos.x < localMin.x || localPos.x > localMax.x || @@ -597,7 +627,6 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos, float playerRadius) const { - (void)from; adjustedPos = to; bool collided = false; @@ -609,8 +638,8 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, const M2ModelGPU& model = it->second; if (instance.scale <= 0.001f) continue; - glm::mat4 invModel = glm::inverse(instance.modelMatrix); - glm::vec3 localPos = glm::vec3(invModel * glm::vec4(adjustedPos, 1.0f)); + glm::vec3 localFrom = glm::vec3(instance.invModelMatrix * glm::vec4(from, 1.0f)); + glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(adjustedPos, 1.0f)); float localRadius = playerRadius / instance.scale; glm::vec3 localMin, localMax; @@ -624,6 +653,23 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, continue; } + // Swept hard clamp for taller blockers only. + // Low/stepable objects should be climbable and not "shove" the player off. + constexpr float MAX_STEP_UP = 1.20f; + bool stepableLowObject = (localMax.z <= localFrom.z + MAX_STEP_UP); + if (!stepableLowObject) { + float tEnter = 0.0f; + if (segmentIntersectsAABB(localFrom, localPos, localMin, localMax, tEnter)) { + float tSafe = std::clamp(tEnter - 0.03f, 0.0f, 1.0f); + glm::vec3 localSafe = localFrom + (localPos - localFrom) * tSafe; + glm::vec3 worldSafe = glm::vec3(instance.modelMatrix * glm::vec4(localSafe, 1.0f)); + adjustedPos.x = worldSafe.x; + adjustedPos.y = worldSafe.y; + collided = true; + continue; + } + } + if (localPos.x < localMin.x || localPos.x > localMax.x || localPos.y < localMin.y || localPos.y > localMax.y) { continue; @@ -635,10 +681,12 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, float pushFront = localMax.y - localPos.y; float minPush = std::min({pushLeft, pushRight, pushBack, pushFront}); - // Soft pushback to avoid large snapping when grazing doodads. - float pushAmount = std::min(0.05f, std::max(0.0f, minPush * 0.30f)); - if (pushAmount <= 0.0f) { - continue; + // Gentle fallback push for overlapping cases. + float pushAmount; + if (stepableLowObject) { + pushAmount = std::clamp(minPush * 0.12f, 0.002f, 0.015f); + } else { + pushAmount = std::clamp(minPush * 0.28f, 0.010f, 0.045f); } glm::vec3 localPush(0.0f); if (minPush == pushLeft) { @@ -674,9 +722,8 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& glm::vec3 extents = (localMax - localMin) * instance.scale; if (glm::length(extents) < 0.75f) continue; - glm::mat4 invModel = glm::inverse(instance.modelMatrix); - glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(origin, 1.0f)); - glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(direction, 0.0f))); + glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(origin, 1.0f)); + glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(direction, 0.0f))); if (!std::isfinite(localDir.x) || !std::isfinite(localDir.y) || !std::isfinite(localDir.z)) { continue; } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 188ae6ea..94189aad 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -9,11 +9,19 @@ #include #include #include +#include +#include #include namespace wowee { namespace rendering { +static void transformAABB(const glm::mat4& modelMatrix, + const glm::vec3& localMin, + const glm::vec3& localMax, + glm::vec3& outMin, + glm::vec3& outMax); + WMORenderer::WMORenderer() { } @@ -377,12 +385,12 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: for (const auto& group : model.groups) { // Proper frustum culling using AABB test if (frustumCulling) { - // Transform group bounding box to world space - glm::vec3 worldMin = glm::vec3(instance.modelMatrix * glm::vec4(group.boundingBoxMin, 1.0f)); - glm::vec3 worldMax = glm::vec3(instance.modelMatrix * glm::vec4(group.boundingBoxMax, 1.0f)); - // Ensure min/max are correct after transform (rotation can swap them) - glm::vec3 actualMin = glm::min(worldMin, worldMax); - glm::vec3 actualMax = glm::max(worldMin, worldMax); + // Transform all AABB corners to avoid false culling on rotated groups. + glm::vec3 actualMin, actualMax; + transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, actualMin, actualMax); + // Small pad reduces edge flicker from precision/camera jitter. + actualMin -= glm::vec3(0.5f); + actualMax += glm::vec3(0.5f); if (!frustum.intersectsAABB(actualMin, actualMax)) { continue; } @@ -694,6 +702,31 @@ static bool rayIntersectsAABB(const glm::vec3& origin, const glm::vec3& dir, return tmax >= 0.0f; // At least part of the ray is forward } +static void transformAABB(const glm::mat4& modelMatrix, + const glm::vec3& localMin, + const glm::vec3& localMax, + glm::vec3& outMin, + glm::vec3& outMax) { + const glm::vec3 corners[8] = { + {localMin.x, localMin.y, localMin.z}, + {localMin.x, localMin.y, localMax.z}, + {localMin.x, localMax.y, localMin.z}, + {localMin.x, localMax.y, localMax.z}, + {localMax.x, localMin.y, localMin.z}, + {localMax.x, localMin.y, localMax.z}, + {localMax.x, localMax.y, localMin.z}, + {localMax.x, localMax.y, localMax.z} + }; + + outMin = glm::vec3(std::numeric_limits::max()); + outMax = glm::vec3(-std::numeric_limits::max()); + for (const glm::vec3& corner : corners) { + glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(corner, 1.0f)); + outMin = glm::min(outMin, world); + outMax = glm::max(outMax, world); + } +} + // Möller–Trumbore ray-triangle intersection // Returns distance along ray if hit, or negative if miss static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir, @@ -718,6 +751,50 @@ static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir, return t > EPSILON ? t : -1.0f; } +// Closest point on triangle (from Real-Time Collision Detection). +static glm::vec3 closestPointOnTriangle(const glm::vec3& p, const glm::vec3& a, + const glm::vec3& b, const glm::vec3& c) { + glm::vec3 ab = b - a; + glm::vec3 ac = c - a; + glm::vec3 ap = p - a; + float d1 = glm::dot(ab, ap); + float d2 = glm::dot(ac, ap); + if (d1 <= 0.0f && d2 <= 0.0f) return a; + + glm::vec3 bp = p - b; + float d3 = glm::dot(ab, bp); + float d4 = glm::dot(ac, bp); + if (d3 >= 0.0f && d4 <= d3) return b; + + float vc = d1 * d4 - d3 * d2; + if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) { + float v = d1 / (d1 - d3); + return a + v * ab; + } + + glm::vec3 cp = p - c; + float d5 = glm::dot(ab, cp); + float d6 = glm::dot(ac, cp); + if (d6 >= 0.0f && d5 <= d6) return c; + + float vb = d5 * d2 - d1 * d6; + if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) { + float w = d2 / (d2 - d6); + return a + w * ac; + } + + float va = d3 * d6 - d5 * d4; + if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) { + float w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + return b + w * (c - b); + } + + float denom = 1.0f / (va + vb + vc); + float v = vb * denom; + float w = vc * denom; + return a + ab * v + ac * w; +} + std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ) const { std::optional bestFloor; @@ -808,6 +885,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Player collision parameters const float PLAYER_RADIUS = 0.6f; // Character collision radius const float PLAYER_HEIGHT = 2.0f; // Player height for wall checks + const float MAX_STEP_HEIGHT = 0.85f; // Balanced step-up without wall pass-through // Debug logging static int wallDebugCounter = 0; @@ -821,6 +899,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, const ModelData& model = it->second; // Transform positions into local space using cached inverse + glm::vec3 localFrom = glm::vec3(instance.invModelMatrix * glm::vec4(from, 1.0f)); glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f)); float localFeetZ = localTo.z; @@ -850,9 +929,8 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, if (normalLen < 0.001f) continue; normal /= normalLen; - // Skip mostly-horizontal triangles (floors/ceilings) - // Only collide with walls (vertical surfaces) - if (std::abs(normal.z) > 0.5f) continue; + // Skip near-horizontal triangles (floors/ceilings), keep sloped tunnel walls. + if (std::abs(normal.z) > 0.85f) continue; // Get triangle Z range float triMinZ = std::min({v0.z, v1.z, v2.z}); @@ -861,34 +939,55 @@ 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 - // Signed distance from player to triangle plane - float planeDist = glm::dot(localTo - v0, normal); - float absPlaneDist = std::abs(planeDist); - if (absPlaneDist > PLAYER_RADIUS) continue; + // Swept test: prevent tunneling when crossing a wall between frames. + float fromDist = glm::dot(localFrom - v0, normal); + float toDist = glm::dot(localTo - v0, normal); + if ((fromDist > PLAYER_RADIUS && toDist < -PLAYER_RADIUS) || + (fromDist < -PLAYER_RADIUS && toDist > PLAYER_RADIUS)) { + float denom = (fromDist - toDist); + if (std::abs(denom) > 1e-6f) { + float tHit = fromDist / denom; // Segment param [0,1] + if (tHit >= 0.0f && tHit <= 1.0f) { + glm::vec3 hitPoint = localFrom + (localTo - localFrom) * tHit; + glm::vec3 hitClosest = closestPointOnTriangle(hitPoint, v0, v1, v2); + float hitErrSq = glm::dot(hitClosest - hitPoint, hitClosest - hitPoint); + bool insideHit = (hitErrSq <= 0.04f * 0.04f); + if (insideHit) { + float side = fromDist > 0.0f ? 1.0f : -1.0f; + glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.03f); + glm::vec3 safeWorld = glm::vec3(instance.modelMatrix * glm::vec4(safeLocal, 1.0f)); + adjustedPos.x = safeWorld.x; + adjustedPos.y = safeWorld.y; + blocked = true; + continue; + } + } + } + } - // Project point onto plane - glm::vec3 projected = localTo - normal * planeDist; - - // Check if projected point is inside triangle (or near edge) - float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal); - float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal); - float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal); - - // Allow small negative values for edge tolerance - const float edgeTolerance = -0.1f; - bool insideTriangle = (d0 >= edgeTolerance && d1 >= edgeTolerance && d2 >= edgeTolerance); - - if (insideTriangle) { + glm::vec3 closest = closestPointOnTriangle(localTo, v0, v1, v2); + glm::vec3 delta = localTo - closest; + float horizDist = glm::length(glm::vec2(delta.x, delta.y)); + if (horizDist <= PLAYER_RADIUS) { wallsHit++; - // Push player away from wall (horizontal only) - float pushDist = PLAYER_RADIUS - absPlaneDist; + // Push player away from wall (horizontal only, from closest point). + float pushDist = PLAYER_RADIUS - horizDist; if (pushDist > 0.0f) { + glm::vec2 pushDir2; + if (horizDist > 1e-4f) { + pushDir2 = glm::normalize(glm::vec2(delta.x, delta.y)); + } else { + glm::vec2 n2(normal.x, normal.y); + if (glm::length(n2) < 1e-4f) continue; + pushDir2 = glm::normalize(n2); + } + // Soft pushback avoids hard side-snaps when skimming walls. - pushDist = std::min(0.06f, pushDist * 0.30f); + pushDist = std::min(0.06f, pushDist * 0.35f); if (pushDist <= 0.0f) continue; - float sign = planeDist > 0.0f ? 1.0f : -1.0f; - glm::vec3 pushLocal = normal * sign * pushDist; + glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f); // Transform push vector back to world space glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f)); @@ -935,6 +1034,13 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const { float closestHit = maxDistance; + // Camera collision should primarily react to walls. + // Treat near-horizontal triangles as floor/ceiling and ignore them here so + // ramps/stairs don't constantly pull the camera in and clip the floor view. + constexpr float MAX_WALKABLE_ABS_NORMAL_Z = 0.20f; + constexpr float MAX_HIT_BELOW_ORIGIN = 0.90f; + constexpr float MAX_HIT_ABOVE_ORIGIN = 0.80f; + constexpr float MIN_SURFACE_ALIGNMENT = 0.25f; for (const auto& instance : instances) { auto it = loadedModels.find(instance.modelId); @@ -959,6 +1065,20 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 const glm::vec3& v0 = verts[indices[i]]; const glm::vec3& v1 = verts[indices[i + 1]]; const glm::vec3& v2 = verts[indices[i + 2]]; + glm::vec3 triNormal = glm::cross(v1 - v0, v2 - v0); + float normalLenSq = glm::dot(triNormal, triNormal); + if (normalLenSq < 1e-8f) { + continue; + } + triNormal /= std::sqrt(normalLenSq); + if (std::abs(triNormal.z) > MAX_WALKABLE_ABS_NORMAL_Z) { + continue; + } + // Ignore near-grazing intersections that tend to come from ramps/arches + // and cause camera pull-in even when no meaningful wall is behind the player. + if (std::abs(glm::dot(triNormal, localDir)) < MIN_SURFACE_ALIGNMENT) { + continue; + } float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2); if (t <= 0.0f) { @@ -969,6 +1089,15 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 glm::vec3 localHit = localOrigin + localDir * t; glm::vec3 worldHit = glm::vec3(instance.modelMatrix * glm::vec4(localHit, 1.0f)); + // Ignore low hits; camera floor handling already keeps the camera above ground. + // This avoids gate/ramp floor geometry pulling the camera in too aggressively. + if (worldHit.z < origin.z - MAX_HIT_BELOW_ORIGIN) { + continue; + } + // Ignore very high hits (arches/ceilings) that should not clamp normal chase-cam distance. + if (worldHit.z > origin.z + MAX_HIT_ABOVE_ORIGIN) { + continue; + } float worldDist = glm::length(worldHit - origin); if (worldDist > 0.0f && worldDist < closestHit && worldDist <= maxDistance) { closestHit = worldDist;