Optimize collision further: skip when stationary, cache floor height, fix drop bug

- Skip wall collision sweep entirely when player isn't moving (saves all
  collision calls when standing still)
- Reduce max sweep steps from 4 to 2 with 1.0f step size (all paths:
  follow, free-fly, swimming)
- Cache floor height between frames, reuse when position changes <0.5 units
- Fix floor height not updating after walking off tall objects (fountain etc)
  by always smoothing toward detected ground instead of ignoring drops >2 units
- Reduce free-fly ground probes from 5 to 1
- Disable WMO camera collision (raycast + floor probes) for performance
- Add spatial grid to raycastBoundingBoxes for when camera collision is re-enabled
This commit is contained in:
Kelsi 2026-02-07 15:54:33 -08:00
parent 974384c725
commit 6516fd777d
3 changed files with 110 additions and 118 deletions

View file

@ -4,6 +4,7 @@
#include "core/input.hpp" #include "core/input.hpp"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <functional> #include <functional>
#include <optional>
namespace wowee { namespace wowee {
namespace rendering { namespace rendering {
@ -133,6 +134,10 @@ private:
static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer
static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground 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<float> cachedFloorHeight;
glm::vec2 cachedFloorPos = glm::vec2(0.0f);
// Cached isInsideWMO result (throttled to avoid per-frame cost) // Cached isInsideWMO result (throttled to avoid per-frame cost)
bool cachedInsideWMO = false; bool cachedInsideWMO = false;
int insideWMOCheckCounter = 0; int insideWMOCheckCounter = 0;

View file

@ -344,36 +344,38 @@ void CameraController::update(float deltaTime) {
} }
} }
// Enforce collision while swimming too (horizontal only), so we don't // Enforce collision while swimming too (horizontal only), skip when stationary.
// pass through walls/props when underwater or at waterline.
{ {
glm::vec3 swimFrom = *followTarget; glm::vec3 swimFrom = *followTarget;
glm::vec3 swimTo = targetPos; glm::vec3 swimTo = targetPos;
float swimMoveDist = glm::length(swimTo - swimFrom); float swimMoveDist = glm::length(swimTo - swimFrom);
int swimSteps = std::max(1, std::min(4, static_cast<int>(std::ceil(swimMoveDist / 0.5f))));
glm::vec3 stepPos = swimFrom; glm::vec3 stepPos = swimFrom;
glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast<float>(swimSteps);
for (int i = 0; i < swimSteps; i++) { if (swimMoveDist > 0.01f) {
glm::vec3 candidate = stepPos + stepDelta; int swimSteps = std::max(1, std::min(2, static_cast<int>(std::ceil(swimMoveDist / 1.0f))));
glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast<float>(swimSteps);
if (wmoRenderer) { for (int i = 0; i < swimSteps; i++) {
glm::vec3 adjusted; glm::vec3 candidate = stepPos + stepDelta;
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
candidate.x = adjusted.x; if (wmoRenderer) {
candidate.y = adjusted.y; glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
candidate.x = adjusted.x;
candidate.y = adjusted.y;
}
} }
}
if (m2Renderer) { if (m2Renderer) {
glm::vec3 adjusted; glm::vec3 adjusted;
if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) { if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) {
candidate.x = adjusted.x; candidate.x = adjusted.x;
candidate.y = adjusted.y; candidate.y = adjusted.y;
}
} }
}
stepPos = candidate; stepPos = candidate;
}
} }
targetPos.x = stepPos.x; 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. // 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 startPos = *followTarget;
glm::vec3 desiredPos = targetPos; glm::vec3 desiredPos = targetPos;
float moveDist = glm::length(desiredPos - startPos); 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<int>(std::ceil(moveDist / 0.50f))));
glm::vec3 stepPos = startPos;
glm::vec3 stepDelta = (desiredPos - startPos) / static_cast<float>(sweepSteps);
for (int i = 0; i < sweepSteps; i++) { if (moveDist > 0.01f) {
glm::vec3 candidate = stepPos + stepDelta; // Adaptive CCD: 1.0f step size, max 2 steps.
int sweepSteps = std::max(1, std::min(2, static_cast<int>(std::ceil(moveDist / 1.0f))));
glm::vec3 stepPos = startPos;
glm::vec3 stepDelta = (desiredPos - startPos) / static_cast<float>(sweepSteps);
if (wmoRenderer) { for (int i = 0; i < sweepSteps; i++) {
glm::vec3 adjusted; glm::vec3 candidate = stepPos + stepDelta;
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
// Keep vertical motion from physics/grounding; only block horizontal wall penetration. if (wmoRenderer) {
candidate.x = adjusted.x; glm::vec3 adjusted;
candidate.y = adjusted.y; 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) { targetPos = stepPos;
glm::vec3 adjusted;
if (m2Renderer->checkCollision(stepPos, candidate, adjusted)) {
candidate.x = adjusted.x;
candidate.y = adjusted.y;
}
}
stepPos = candidate;
} }
targetPos = stepPos;
} }
// WoW-style slope limiting (50 degrees, with sliding) // WoW-style slope limiting (50 degrees, with sliding)
@ -574,21 +579,29 @@ void CameraController::update(float deltaTime) {
return base; return base;
}; };
// Single center probe — extra probes are too expensive in WMO-heavy areas. // Use cached floor height if player hasn't moved much horizontally.
std::optional<float> groundH = sampleGround(targetPos.x, targetPos.y); float floorPosDist = glm::length(glm::vec2(targetPos.x, targetPos.y) - cachedFloorPos);
std::optional<float> 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) { if (groundH) {
float groundDiff = *groundH - lastGroundZ; float groundDiff = *groundH - lastGroundZ;
if (groundDiff > 2.0f) { if (groundDiff > 2.0f) {
// Landing on a higher ledge - snap up // Landing on a higher ledge - snap up
lastGroundZ = *groundH; lastGroundZ = *groundH;
} else if (groundDiff > -2.0f) { } else {
// Small height difference - smooth it // Smooth toward detected ground. Use a slower rate for large
lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f); // 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) { if (targetPos.z <= lastGroundZ + 0.1f && verticalVelocity <= 0.0f) {
targetPos.z = lastGroundZ; targetPos.z = lastGroundZ;
@ -638,41 +651,15 @@ void CameraController::update(float deltaTime) {
// Find max safe distance using raycast + sphere radius // Find max safe distance using raycast + sphere radius
collisionDistance = currentDistance; collisionDistance = currentDistance;
// Helper to get floor height for camera collision. // Camera collision: terrain-only floor clamping (skip expensive WMO raycasts).
// Use the player's ground level as reference to avoid locking the camera // The camera may clip through WMO walls but won't go underground.
// to upper floors in multi-story buildings. auto getTerrainFloorAt = [&](float x, float y) -> std::optional<float> {
auto getFloorAt = [&](float x, float y, float /*z*/) -> std::optional<float> {
std::optional<float> terrainH;
std::optional<float> wmoH;
if (terrainManager) { if (terrainManager) {
terrainH = terrainManager->getHeightAt(x, y); return terrainManager->getHeightAt(x, y);
} }
if (wmoRenderer) { return std::nullopt;
wmoH = wmoRenderer->getFloorHeight(x, y, lastGroundZ + 2.5f);
}
return selectReachableFloor(terrainH, wmoH, lastGroundZ, 2.0f);
}; };
// 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) // Use collision distance (don't exceed user target)
float actualDist = std::min(currentDistance, collisionDistance); 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); float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime);
smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; smoothedCamPos += (actualCam - smoothedCamPos) * camLerp;
// ===== Final floor clearance check ===== // ===== Final floor clearance check (terrain only) =====
constexpr float MIN_FLOOR_CLEARANCE = 0.35f; 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) { if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) {
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; 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) { if (wmoRenderer) {
glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight); glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight); glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
float moveDist = glm::length(desiredFeet - startFeet); 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<int>(std::ceil(moveDist / 0.50f))));
glm::vec3 stepPos = startFeet;
glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast<float>(sweepSteps);
for (int i = 0; i < sweepSteps; i++) { if (moveDist > 0.01f) {
glm::vec3 candidate = stepPos + stepDelta; int sweepSteps = std::max(1, std::min(2, static_cast<int>(std::ceil(moveDist / 1.0f))));
glm::vec3 adjusted; glm::vec3 stepPos = startFeet;
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast<float>(sweepSteps);
candidate = adjusted;
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 // Ground to terrain or WMO floor
@ -865,29 +854,17 @@ void CameraController::update(float deltaTime) {
return base; return base;
}; };
std::optional<float> groundH; // Single center probe.
constexpr float FOOTPRINT = 0.4f; // Larger footprint for better floor detection std::optional<float> groundH = sampleGround(newPos.x, newPos.y);
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;
}
}
if (groundH) { if (groundH) {
float groundDiff = *groundH - lastGroundZ; float groundDiff = *groundH - lastGroundZ;
if (groundDiff > 2.0f) { if (groundDiff > 2.0f) {
// Landing on a higher ledge - snap up
lastGroundZ = *groundH;
} else if (groundDiff > -2.0f) {
// Small difference - accept it
lastGroundZ = *groundH; 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; float groundZ = lastGroundZ + eyeHeight;
if (newPos.z <= groundZ) { if (newPos.z <= groundZ) {

View file

@ -2019,13 +2019,23 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
continue; continue;
} }
// Narrow-phase: triangle raycast for accurate camera collision. // Narrow-phase: triangle raycast using spatial grid.
const auto& verts = group.collisionVertices; const auto& verts = group.collisionVertices;
const auto& indices = group.collisionIndices; const auto& indices = group.collisionIndices;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const glm::vec3& v0 = verts[indices[i]]; // Compute local-space ray endpoint and query grid for XY range
const glm::vec3& v1 = verts[indices[i + 1]]; glm::vec3 localEnd = localOrigin + localDir * (closestHit / glm::length(
const glm::vec3& v2 = verts[indices[i + 2]]; 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); glm::vec3 triNormal = glm::cross(v1 - v0, v2 - v0);
float normalLenSq = glm::dot(triNormal, triNormal); float normalLenSq = glm::dot(triNormal, triNormal);
if (normalLenSq < 1e-8f) { if (normalLenSq < 1e-8f) {