mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Overhaul WMO collision: precompute normals, fix floor selection, optimize queries
- Precompute triangle normals in buildCollisionGrid, eliminating per-query cross+normalize in getFloorHeight, checkWallCollision, and raycastBoundingBoxes - Fix floor selection: remove redundant allowAbove (callers already elevate probeZ by stepUpBudget), preventing upper-story snap at doorway transitions - Align wall classification threshold (absNz < 0.35) with runtime skip check, eliminating ~30% wasted wall triangle fetches - Replace O(n log n) sort+unique dedup in range queries with O(n) visited bitset - Rename wallTriScratch to triScratch_, fix stale threshold comments
This commit is contained in:
parent
35384b2c52
commit
64879b8aab
2 changed files with 117 additions and 74 deletions
|
|
@ -390,13 +390,19 @@ private:
|
|||
std::vector<std::vector<uint32_t>> cellTriangles;
|
||||
|
||||
// Pre-classified triangle lists per cell (built at load time)
|
||||
std::vector<std::vector<uint32_t>> cellFloorTriangles; // abs(normal.z) >= 0.45
|
||||
std::vector<std::vector<uint32_t>> cellWallTriangles; // abs(normal.z) < 0.55
|
||||
std::vector<std::vector<uint32_t>> cellFloorTriangles; // abs(normal.z) >= 0.35
|
||||
std::vector<std::vector<uint32_t>> cellWallTriangles; // abs(normal.z) < 0.35
|
||||
|
||||
// Pre-computed per-triangle Z bounds for fast vertical reject
|
||||
struct TriBounds { float minZ; float maxZ; };
|
||||
std::vector<TriBounds> triBounds; // indexed by triStart/3
|
||||
|
||||
// Pre-computed per-triangle normals (unit length, indexed by triStart/3)
|
||||
std::vector<glm::vec3> triNormals;
|
||||
|
||||
// Scratch bitset for deduplicating triangle queries (sized to numTriangles)
|
||||
mutable std::vector<uint8_t> triVisited;
|
||||
|
||||
// Build the spatial grid from collision geometry
|
||||
void buildCollisionGrid();
|
||||
|
||||
|
|
@ -675,7 +681,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::vector<uint32_t> triScratch_; // Scratch for collision grid queries
|
||||
mutable std::unordered_set<uint32_t> candidateIdScratch;
|
||||
|
||||
// Parallel visibility culling
|
||||
|
|
|
|||
|
|
@ -1263,6 +1263,8 @@ void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& q
|
|||
}
|
||||
|
||||
void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
||||
++currentFrameId;
|
||||
|
||||
if (!opaquePipeline_ || instances.empty()) {
|
||||
lastDrawCalls = 0;
|
||||
return;
|
||||
|
|
@ -2474,6 +2476,8 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
|
||||
size_t numTriangles = collisionIndices.size() / 3;
|
||||
triBounds.resize(numTriangles);
|
||||
triNormals.resize(numTriangles);
|
||||
triVisited.resize(numTriangles, 0);
|
||||
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
|
@ -2494,16 +2498,23 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
float triMaxZ = std::max({v0.z, v1.z, v2.z});
|
||||
triBounds[i / 3] = { triMinZ, triMaxZ };
|
||||
|
||||
// Classify floor vs wall by normal.
|
||||
// Wall threshold matches MAX_WALK_SLOPE_DOT (cos 50° ≈ 0.6428) so that
|
||||
// surfaces too steep to walk on are always tested for wall collision.
|
||||
// Precompute and store unit normal
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
glm::vec3 edge2 = v2 - v0;
|
||||
glm::vec3 normal = glm::cross(edge1, edge2);
|
||||
float normalLen = glm::length(normal);
|
||||
float absNz = (normalLen > 0.001f) ? std::abs(normal.z / normalLen) : 0.0f;
|
||||
if (normalLen > 0.001f) {
|
||||
normal /= normalLen;
|
||||
} else {
|
||||
normal = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
triNormals[i / 3] = normal;
|
||||
|
||||
// Classify floor vs wall by normal.
|
||||
// Wall threshold matches the runtime skip in checkWallCollision (absNz >= 0.35).
|
||||
float absNz = std::abs(normal.z);
|
||||
bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs)
|
||||
bool isWall = (absNz < 0.65f); // Matches walkable slope threshold
|
||||
bool isWall = (absNz < 0.35f); // Matches checkWallCollision skip threshold
|
||||
|
||||
int cellMinX = std::max(0, static_cast<int>((triMinX - gridOrigin.x) * invCellW));
|
||||
int cellMinY = std::max(0, static_cast<int>((triMinY - gridOrigin.y) * invCellH));
|
||||
|
|
@ -2556,18 +2567,30 @@ void WMORenderer::GroupResources::getTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
// Collect unique triangle indices from all overlapping cells
|
||||
// Collect unique triangle indices using visited bitset (O(n) dedup)
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear visited bits
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2589,16 +2612,28 @@ void WMORenderer::GroupResources::getFloorTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMinX != cellMaxX || cellMinY != cellMaxY) {
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2620,22 +2655,35 @@ void WMORenderer::GroupResources::getWallTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
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, float* outNormalZ) const {
|
||||
// All floor caching disabled - even per-frame cache can return stale results
|
||||
// when player Z changes between queries, causing fall-through at stairs.
|
||||
// Per-frame cache disabled: camera and player query the same (x,y) at
|
||||
// different Z within a single frame. The allowAbove filter depends on glZ,
|
||||
// so caching by (x,y) alone returns wrong floors across Z contexts.
|
||||
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
std::optional<float> bestFloor;
|
||||
|
|
@ -2660,9 +2708,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
group.getTrianglesInRange(
|
||||
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
||||
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
||||
wallTriScratch);
|
||||
triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
|
@ -2676,18 +2724,16 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||
|
||||
float allowAbove = model.isLowPlatform ? 12.0f : 2.0f;
|
||||
if (hitWorld.z <= glZ + allowAbove) {
|
||||
// Accept floors at or below glZ (the caller already elevates
|
||||
// glZ by stepUpBudget to handle step-up range). Among those,
|
||||
// pick the highest (closest to feet).
|
||||
if (hitWorld.z <= glZ) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
bestFromLowPlatform = model.isLowPlatform;
|
||||
|
||||
// Compute local normal and transform to world space
|
||||
glm::vec3 localNormal = glm::cross(v1 - v0, v2 - v0);
|
||||
float len = glm::length(localNormal);
|
||||
if (len > 0.001f) {
|
||||
localNormal /= len;
|
||||
// Ensure normal points upward
|
||||
// Use precomputed normal, ensure upward, transform to world
|
||||
glm::vec3 localNormal = group.triNormals[triStart / 3];
|
||||
if (localNormal.z < 0.0f) localNormal = -localNormal;
|
||||
glm::vec3 worldNormal = glm::normalize(
|
||||
glm::vec3(instance.modelMatrix * glm::vec4(localNormal, 0.0f)));
|
||||
|
|
@ -2696,7 +2742,6 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fast path: current active interior group and its neighbors are usually
|
||||
|
|
@ -2735,8 +2780,8 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
}
|
||||
}
|
||||
|
||||
// Full scan: test all instances (active group fast path removed to fix
|
||||
// bridge clipping where early-return missed other WMO instances)
|
||||
// Full scan: test all instances (active group result above is not
|
||||
// early-returned because overlapping WMO instances need full coverage).
|
||||
glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 8.0f);
|
||||
glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 10.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
|
@ -2898,9 +2943,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f;
|
||||
float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f;
|
||||
float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f;
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
// Use pre-computed Z bounds for fast vertical reject
|
||||
const auto& tb = group.triBounds[triStart / 3];
|
||||
|
||||
|
|
@ -2919,13 +2964,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
||||
// Triangle normal for swept test and push fallback
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
glm::vec3 edge2 = v2 - v0;
|
||||
glm::vec3 normal = glm::cross(edge1, edge2);
|
||||
float normalLen = glm::length(normal);
|
||||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
// Use precomputed normal for swept test and push fallback
|
||||
glm::vec3 normal = group.triNormals[triStart / 3];
|
||||
if (glm::dot(normal, normal) < 0.5f) continue; // degenerate
|
||||
|
||||
// Recompute plane distances with current (possibly pushed) localTo
|
||||
float fromDist = glm::dot(localFrom - v0, normal);
|
||||
|
|
@ -3268,19 +3309,15 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
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.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
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);
|
||||
float normalLenSq = glm::dot(triNormal, triNormal);
|
||||
if (normalLenSq < 1e-8f) {
|
||||
continue;
|
||||
}
|
||||
triNormal /= std::sqrt(normalLenSq);
|
||||
// Wall list pre-filters at 0.55; apply stricter camera threshold
|
||||
glm::vec3 triNormal = group.triNormals[triStart / 3];
|
||||
if (glm::dot(triNormal, triNormal) < 0.5f) continue; // degenerate
|
||||
// Wall list pre-filters at 0.35; apply stricter camera threshold
|
||||
if (std::abs(triNormal.z) > MAX_WALKABLE_ABS_NORMAL_Z) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue