mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +00:00
Add per-group spatial grid for WMO collision and reduce collision call frequency
Build a 2D triangle grid per WMO group at load time so getFloorHeight and checkWallCollision only test triangles in nearby cells instead of brute-forcing all triangles. Also reduce sweep steps (12→4), ground probes (3→1), camera floor probes (5→2), throttle isInsideWMO to every 10 frames, and early-out wall collision on first hit.
This commit is contained in:
parent
751e6fdbde
commit
974384c725
4 changed files with 179 additions and 73 deletions
|
|
@ -133,6 +133,11 @@ private:
|
|||
static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer
|
||||
static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground
|
||||
|
||||
// Cached isInsideWMO result (throttled to avoid per-frame cost)
|
||||
bool cachedInsideWMO = false;
|
||||
int insideWMOCheckCounter = 0;
|
||||
glm::vec3 lastInsideWMOCheckPos = glm::vec3(0.0f);
|
||||
|
||||
// Swimming
|
||||
bool swimming = false;
|
||||
bool wasSwimming = false;
|
||||
|
|
|
|||
|
|
@ -270,6 +270,25 @@ private:
|
|||
// Collision geometry (positions only, for floor raycasting)
|
||||
std::vector<glm::vec3> collisionVertices;
|
||||
std::vector<uint16_t> collisionIndices;
|
||||
|
||||
// 2D spatial grid for fast triangle lookup (built at load time).
|
||||
// Bins triangles by their XY bounding box into grid cells.
|
||||
static constexpr float COLLISION_CELL_SIZE = 4.0f;
|
||||
int gridCellsX = 0;
|
||||
int gridCellsY = 0;
|
||||
glm::vec2 gridOrigin; // XY of bounding box min
|
||||
// cellTriangles[cellY * gridCellsX + cellX] = list of triangle start indices
|
||||
std::vector<std::vector<uint32_t>> cellTriangles;
|
||||
|
||||
// Build the spatial grid from collision geometry
|
||||
void buildCollisionGrid();
|
||||
|
||||
// Get triangle indices for a local-space XY point
|
||||
const std::vector<uint32_t>* getTrianglesAtLocal(float localX, float localY) const;
|
||||
|
||||
// Get triangle indices for a local-space XY range (for wall collision)
|
||||
void getTrianglesInRange(float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -497,6 +516,7 @@ private:
|
|||
std::unordered_map<GridCell, std::vector<uint32_t>, GridCellHash> spatialGrid;
|
||||
std::unordered_map<uint32_t, size_t> instanceIndexById;
|
||||
mutable std::vector<size_t> candidateScratch;
|
||||
mutable std::vector<uint32_t> wallTriScratch; // Scratch for wall collision grid queries
|
||||
mutable std::unordered_set<uint32_t> candidateIdScratch;
|
||||
|
||||
// Parallel visibility culling
|
||||
|
|
|
|||
|
|
@ -415,11 +415,8 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 startPos = *followTarget;
|
||||
glm::vec3 desiredPos = targetPos;
|
||||
float moveDist = glm::length(desiredPos - startPos);
|
||||
// Adaptive CCD: keep per-step movement short to avoid clipping through walls.
|
||||
int sweepSteps = std::max(1, std::min(12, static_cast<int>(std::ceil(moveDist / 0.20f))));
|
||||
if (deltaTime > 0.04f) {
|
||||
sweepSteps = std::min(16, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f))));
|
||||
}
|
||||
// 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);
|
||||
|
||||
|
|
@ -577,25 +574,8 @@ void CameraController::update(float deltaTime) {
|
|||
return base;
|
||||
};
|
||||
|
||||
// Sample center + movement-aligned offsets to avoid slipping through narrow floor pieces.
|
||||
// Use 3 samples instead of 5 — center plus two movement-direction probes.
|
||||
std::optional<float> groundH;
|
||||
groundH = sampleGround(targetPos.x, targetPos.y);
|
||||
{
|
||||
constexpr float FOOTPRINT = 0.4f;
|
||||
glm::vec2 moveXY(targetPos.x - followTarget->x, targetPos.y - followTarget->y);
|
||||
float moveLen = glm::length(moveXY);
|
||||
if (moveLen > 0.01f) {
|
||||
glm::vec2 moveDir2 = moveXY / moveLen;
|
||||
glm::vec2 perpDir(-moveDir2.y, moveDir2.x);
|
||||
auto h1 = sampleGround(targetPos.x + moveDir2.x * FOOTPRINT,
|
||||
targetPos.y + moveDir2.y * FOOTPRINT);
|
||||
if (h1 && (!groundH || *h1 > *groundH)) groundH = h1;
|
||||
auto h2 = sampleGround(targetPos.x + perpDir.x * FOOTPRINT,
|
||||
targetPos.y + perpDir.y * FOOTPRINT);
|
||||
if (h2 && (!groundH || *h2 > *groundH)) groundH = h2;
|
||||
}
|
||||
}
|
||||
// Single center probe — extra probes are too expensive in WMO-heavy areas.
|
||||
std::optional<float> groundH = sampleGround(targetPos.x, targetPos.y);
|
||||
|
||||
if (groundH) {
|
||||
float groundDiff = *groundH - lastGroundZ;
|
||||
|
|
@ -640,9 +620,16 @@ void CameraController::update(float deltaTime) {
|
|||
currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
|
||||
|
||||
// Limit max zoom when inside a WMO (building interior)
|
||||
// Throttle: only recheck every 10 frames or when position changes >2 units.
|
||||
static constexpr float WMO_MAX_DISTANCE = 5.0f;
|
||||
if (wmoRenderer && wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr)) {
|
||||
if (currentDistance > WMO_MAX_DISTANCE) {
|
||||
if (wmoRenderer) {
|
||||
float distFromLastCheck = glm::length(targetPos - lastInsideWMOCheckPos);
|
||||
if (++insideWMOCheckCounter >= 10 || distFromLastCheck > 2.0f) {
|
||||
cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr);
|
||||
insideWMOCheckCounter = 0;
|
||||
lastInsideWMOCheckPos = targetPos;
|
||||
}
|
||||
if (cachedInsideWMO && currentDistance > WMO_MAX_DISTANCE) {
|
||||
currentDistance = WMO_MAX_DISTANCE;
|
||||
}
|
||||
}
|
||||
|
|
@ -676,17 +663,13 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Intentionally ignore M2 doodads for camera collision to match WoW feel.
|
||||
|
||||
// Check floor collision along the camera path
|
||||
// Sample a few points to find where camera would go underground
|
||||
for (int i = 1; i <= 2; i++) {
|
||||
float testDist = collisionDistance * (float(i) / 2.0f);
|
||||
glm::vec3 testPos = pivot + camDir * testDist;
|
||||
// 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) {
|
||||
// Camera would be underground at this distance
|
||||
collisionDistance = std::max(MIN_DISTANCE, testDist - CAM_SPHERE_RADIUS);
|
||||
break;
|
||||
collisionDistance = std::max(MIN_DISTANCE, collisionDistance - CAM_SPHERE_RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -710,22 +693,8 @@ void CameraController::update(float deltaTime) {
|
|||
smoothedCamPos += (actualCam - smoothedCamPos) * camLerp;
|
||||
|
||||
// ===== Final floor clearance check =====
|
||||
// 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.7f, FLOOR_SAMPLE_R * 0.7f},
|
||||
{-FLOOR_SAMPLE_R * 0.7f, -FLOOR_SAMPLE_R * 0.7f}
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z);
|
||||
if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) {
|
||||
smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE;
|
||||
}
|
||||
|
|
@ -854,11 +823,8 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
|
||||
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
|
||||
float moveDist = glm::length(desiredFeet - startFeet);
|
||||
// Adaptive CCD: keep per-step movement short to avoid clipping through walls.
|
||||
int sweepSteps = std::max(1, std::min(12, static_cast<int>(std::ceil(moveDist / 0.20f))));
|
||||
if (deltaTime > 0.04f) {
|
||||
sweepSteps = std::min(16, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f))));
|
||||
}
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1089,6 +1089,9 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes
|
|||
}
|
||||
}
|
||||
|
||||
// Build 2D spatial grid for fast collision triangle lookup
|
||||
resources.buildCollisionGrid();
|
||||
|
||||
// Create batches
|
||||
if (!group.batches.empty()) {
|
||||
for (const auto& batch : group.batches) {
|
||||
|
|
@ -1510,6 +1513,105 @@ static glm::vec3 closestPointOnTriangle(const glm::vec3& p, const glm::vec3& a,
|
|||
return a + ab * v + ac * w;
|
||||
}
|
||||
|
||||
// ---- Per-group 2D collision grid ----
|
||||
|
||||
void WMORenderer::GroupResources::buildCollisionGrid() {
|
||||
if (collisionVertices.empty() || collisionIndices.size() < 3) {
|
||||
gridCellsX = 0;
|
||||
gridCellsY = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
gridOrigin = glm::vec2(boundingBoxMin.x, boundingBoxMin.y);
|
||||
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
||||
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
||||
|
||||
gridCellsX = std::max(1, static_cast<int>(std::ceil(extentX / COLLISION_CELL_SIZE)));
|
||||
gridCellsY = std::max(1, static_cast<int>(std::ceil(extentY / COLLISION_CELL_SIZE)));
|
||||
|
||||
// Cap grid size to avoid excessive memory for huge groups
|
||||
if (gridCellsX > 64) gridCellsX = 64;
|
||||
if (gridCellsY > 64) gridCellsY = 64;
|
||||
|
||||
cellTriangles.resize(gridCellsX * gridCellsY);
|
||||
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
||||
for (size_t i = 0; i + 2 < collisionIndices.size(); i += 3) {
|
||||
const glm::vec3& v0 = collisionVertices[collisionIndices[i]];
|
||||
const glm::vec3& v1 = collisionVertices[collisionIndices[i + 1]];
|
||||
const glm::vec3& v2 = collisionVertices[collisionIndices[i + 2]];
|
||||
|
||||
// Triangle XY bounding box
|
||||
float triMinX = std::min({v0.x, v1.x, v2.x});
|
||||
float triMinY = std::min({v0.y, v1.y, v2.y});
|
||||
float triMaxX = std::max({v0.x, v1.x, v2.x});
|
||||
float triMaxY = std::max({v0.y, v1.y, v2.y});
|
||||
|
||||
int cellMinX = std::max(0, static_cast<int>((triMinX - gridOrigin.x) * invCellW));
|
||||
int cellMinY = std::max(0, static_cast<int>((triMinY - gridOrigin.y) * invCellH));
|
||||
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((triMaxX - gridOrigin.x) * invCellW));
|
||||
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((triMaxY - gridOrigin.y) * invCellH));
|
||||
|
||||
uint32_t triIdx = static_cast<uint32_t>(i);
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
cellTriangles[cy * gridCellsX + cx].push_back(triIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint32_t>* WMORenderer::GroupResources::getTrianglesAtLocal(float localX, float localY) const {
|
||||
if (gridCellsX == 0 || gridCellsY == 0) return nullptr;
|
||||
|
||||
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
||||
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
||||
int cx = static_cast<int>((localX - gridOrigin.x) * invCellW);
|
||||
int cy = static_cast<int>((localY - gridOrigin.y) * invCellH);
|
||||
|
||||
if (cx < 0 || cx >= gridCellsX || cy < 0 || cy >= gridCellsY) return nullptr;
|
||||
|
||||
return &cellTriangles[cy * gridCellsX + cx];
|
||||
}
|
||||
|
||||
void WMORenderer::GroupResources::getTrianglesInRange(
|
||||
float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const {
|
||||
out.clear();
|
||||
if (gridCellsX == 0 || gridCellsY == 0) return;
|
||||
|
||||
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
||||
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
||||
int cellMinX = std::max(0, static_cast<int>((minX - gridOrigin.x) * invCellW));
|
||||
int cellMinY = std::max(0, static_cast<int>((minY - gridOrigin.y) * invCellH));
|
||||
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((maxX - gridOrigin.x) * invCellW));
|
||||
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((maxY - gridOrigin.y) * invCellH));
|
||||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
// Collect unique triangle indices from all overlapping cells
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates (triangles spanning multiple cells)
|
||||
if (cellMinX != cellMaxX || cellMinY != cellMaxY) {
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
|
||||
// Check persistent grid cache first (computed lazily, never expires)
|
||||
uint64_t gridKey = floorGridKey(glX, glY);
|
||||
|
|
@ -1592,23 +1694,27 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const glm::vec3& v0 = verts[indices[i]];
|
||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||
// Use spatial grid to only test triangles near the query XY
|
||||
const auto* cellTris = group.getTrianglesAtLocal(localOrigin.x, localOrigin.y);
|
||||
if (cellTris) {
|
||||
for (uint32_t triStart : *cellTris) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
||||
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
||||
if (t <= 0.0f) {
|
||||
t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
||||
}
|
||||
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
||||
if (t <= 0.0f) {
|
||||
t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
||||
}
|
||||
|
||||
if (t > 0.0f) {
|
||||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||
if (t > 0.0f) {
|
||||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||
|
||||
if (hitWorld.z <= glZ + 0.5f) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
if (hitWorld.z <= glZ + 0.5f) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1645,6 +1751,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
if (blocked) break; // Early-out once a wall is found
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -1682,7 +1789,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
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;
|
||||
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
||||
for (size_t gi = 0; gi < model.groups.size() && !blocked; ++gi) {
|
||||
// World-space group cull
|
||||
if (gi < instance.worldGroupBounds.size()) {
|
||||
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||
|
|
@ -1705,10 +1812,18 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const glm::vec3& v0 = verts[indices[i]];
|
||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||
// Use spatial grid: query range covering the movement segment + player radius
|
||||
float rangeMinX = std::min(localFrom.x, localTo.x) - PLAYER_RADIUS - 1.0f;
|
||||
float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.0f;
|
||||
float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.0f;
|
||||
float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.0f;
|
||||
group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
if (blocked) break;
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
||||
// Get triangle normal
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue