mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 08:30:13 +00:00
Fix camera occlusion and stabilize WMO/M2 collision behavior
This commit is contained in:
parent
d03ff5ee4c
commit
a3f351f395
6 changed files with 247 additions and 127 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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ö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<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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue