Fix camera occlusion and stabilize WMO/M2 collision behavior

This commit is contained in:
Kelsi 2026-02-03 16:04:21 -08:00
parent d03ff5ee4c
commit a3f351f395
6 changed files with 247 additions and 127 deletions

View file

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

View file

@ -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<float>(rand()) / RAND_MAX * 10.0f; // Random start time
instances.push_back(instance);
@ -571,8 +602,7 @@ std::optional<float> 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<float> 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;
}

View file

@ -9,11 +9,19 @@
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <unordered_set>
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<float>::max());
outMax = glm::vec3(-std::numeric_limits<float>::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öllerTrumbore 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<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
std::optional<float> 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;