diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 98ca64cb..9ce374a0 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -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; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 46bf008d..9029c5aa 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -270,6 +270,25 @@ private: // Collision geometry (positions only, for floor raycasting) std::vector collisionVertices; std::vector 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> cellTriangles; + + // Build the spatial grid from collision geometry + void buildCollisionGrid(); + + // Get triangle indices for a local-space XY point + const std::vector* 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& out) const; }; /** @@ -497,6 +516,7 @@ private: std::unordered_map, GridCellHash> spatialGrid; std::unordered_map instanceIndexById; mutable std::vector candidateScratch; + mutable std::vector wallTriScratch; // Scratch for wall collision grid queries mutable std::unordered_set candidateIdScratch; // Parallel visibility culling diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index eaa561ab..9f5a0367 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -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(std::ceil(moveDist / 0.20f)))); - if (deltaTime > 0.04f) { - sweepSteps = std::min(16, std::max(sweepSteps, static_cast(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(std::ceil(moveDist / 0.50f)))); glm::vec3 stepPos = startPos; glm::vec3 stepDelta = (desiredPos - startPos) / static_cast(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 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 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 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(std::ceil(moveDist / 0.20f)))); - if (deltaTime > 0.04f) { - sweepSteps = std::min(16, std::max(sweepSteps, static_cast(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(std::ceil(moveDist / 0.50f)))); glm::vec3 stepPos = startFeet; glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast(sweepSteps); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 933c113c..611417eb 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -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(std::ceil(extentX / COLLISION_CELL_SIZE))); + gridCellsY = std::max(1, static_cast(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((triMinX - gridOrigin.x) * invCellW)); + int cellMinY = std::max(0, static_cast((triMinY - gridOrigin.y) * invCellH)); + int cellMaxX = std::min(gridCellsX - 1, static_cast((triMaxX - gridOrigin.x) * invCellW)); + int cellMaxY = std::min(gridCellsY - 1, static_cast((triMaxY - gridOrigin.y) * invCellH)); + + uint32_t triIdx = static_cast(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* 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((localX - gridOrigin.x) * invCellW); + int cy = static_cast((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& 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((minX - gridOrigin.x) * invCellW)); + int cellMinY = std::max(0, static_cast((minY - gridOrigin.y) * invCellH)); + int cellMaxX = std::min(gridCellsX - 1, static_cast((maxX - gridOrigin.x) * invCellW)); + int cellMaxY = std::min(gridCellsY - 1, static_cast((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 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 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;