mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Fix WMO ramp/stair clipping with WoW-style floor snap and collision fixes
Remove active group fast path from getFloorHeight to fix bridge clipping. Replace ground smoothing with immediate step-up snap (WoW-style: snap up, smooth down). Accept upward Z from wall collision at all call sites. Skip floor-like surfaces (absNz >= 0.45) in wall collision to prevent false wall hits on ramps. Increase getFloorHeight allowAbove from 0.5 to 2.0 for ramp reacquisition. Prefer highest reachable surface in floor selection.
This commit is contained in:
parent
ef54f62df0
commit
f8aba30f2d
4 changed files with 425 additions and 253 deletions
|
|
@ -161,6 +161,11 @@ private:
|
|||
int insideWMOCheckCounter = 0;
|
||||
glm::vec3 lastInsideWMOCheckPos = glm::vec3(0.0f);
|
||||
|
||||
// Cached camera WMO floor query (skip if camera moved < 0.3 units)
|
||||
glm::vec3 lastCamFloorQueryPos = glm::vec3(0.0f);
|
||||
std::optional<float> cachedCamWmoFloor;
|
||||
bool hasCachedCamFloor = false;
|
||||
|
||||
// Swimming
|
||||
bool swimming = false;
|
||||
bool wasSwimming = false;
|
||||
|
|
|
|||
|
|
@ -190,9 +190,11 @@ public:
|
|||
void renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader);
|
||||
|
||||
/**
|
||||
* Get floor height at a GL position via ray-triangle intersection
|
||||
* Get floor height at a GL position via ray-triangle intersection.
|
||||
* @param outNormalZ If not null, receives the Z component of the floor surface normal
|
||||
* (1.0 = flat, 0.0 = vertical). Useful for slope walkability checks.
|
||||
*/
|
||||
std::optional<float> getFloorHeight(float glX, float glY, float glZ) const;
|
||||
std::optional<float> getFloorHeight(float glX, float glY, float glZ, float* outNormalZ = nullptr) const;
|
||||
|
||||
/**
|
||||
* Check wall collision and adjust position
|
||||
|
|
@ -235,6 +237,12 @@ public:
|
|||
double getQueryTimeMs() const { return queryTimeMs; }
|
||||
uint32_t getQueryCallCount() const { return queryCallCount; }
|
||||
|
||||
/**
|
||||
* Update the tracked active WMO group based on player position.
|
||||
* Called at low frequency (every ~10 frames or on significant movement).
|
||||
*/
|
||||
void updateActiveGroup(float glX, float glY, float glZ);
|
||||
|
||||
// Floor cache persistence (zone-specific files)
|
||||
void setMapName(const std::string& name) { mapName_ = name; }
|
||||
const std::string& getMapName() const { return mapName_; }
|
||||
|
|
@ -293,6 +301,14 @@ private:
|
|||
// cellTriangles[cellY * gridCellsX + cellX] = list of triangle start indices
|
||||
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
|
||||
|
||||
// Pre-computed per-triangle Z bounds for fast vertical reject
|
||||
struct TriBounds { float minZ; float maxZ; };
|
||||
std::vector<TriBounds> triBounds; // indexed by triStart/3
|
||||
|
||||
// Build the spatial grid from collision geometry
|
||||
void buildCollisionGrid();
|
||||
|
||||
|
|
@ -302,6 +318,12 @@ private:
|
|||
// 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;
|
||||
|
||||
// Get pre-classified floor/wall triangles in range
|
||||
void getFloorTrianglesInRange(float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const;
|
||||
void getWallTrianglesInRange(float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -563,6 +585,50 @@ private:
|
|||
|
||||
// Compute floor height for a single cell (expensive, done at load time)
|
||||
std::optional<float> computeFloorHeightSlow(float x, float y, float refZ) const;
|
||||
|
||||
// Active WMO group tracking — reduces per-query group iteration
|
||||
struct ActiveGroupInfo {
|
||||
uint32_t instanceIdx = UINT32_MAX;
|
||||
uint32_t modelId = 0;
|
||||
int32_t groupIdx = -1;
|
||||
std::vector<uint32_t> neighborGroups; // portal-connected groups
|
||||
bool isValid() const { return instanceIdx != UINT32_MAX && groupIdx >= 0; }
|
||||
void invalidate() { instanceIdx = UINT32_MAX; groupIdx = -1; neighborGroups.clear(); }
|
||||
};
|
||||
mutable ActiveGroupInfo activeGroup_;
|
||||
|
||||
// Per-frame floor height dedup cache (same XY queried 3-5x per frame)
|
||||
struct FrameFloorCache {
|
||||
static constexpr size_t CAPACITY = 16;
|
||||
struct Entry { uint64_t key; float resultZ; float normalZ; uint32_t frameId; };
|
||||
Entry entries[CAPACITY] = {};
|
||||
|
||||
uint64_t makeKey(float x, float y) const {
|
||||
// 0.5-unit quantized grid
|
||||
int32_t ix = static_cast<int32_t>(std::floor(x * 2.0f));
|
||||
int32_t iy = static_cast<int32_t>(std::floor(y * 2.0f));
|
||||
return (static_cast<uint64_t>(static_cast<uint32_t>(ix)) << 32) |
|
||||
static_cast<uint64_t>(static_cast<uint32_t>(iy));
|
||||
}
|
||||
|
||||
std::optional<float> get(float x, float y, uint32_t frame, float* outNormalZ = nullptr) const {
|
||||
uint64_t k = makeKey(x, y);
|
||||
size_t slot = k % CAPACITY;
|
||||
const auto& e = entries[slot];
|
||||
if (e.frameId == frame && e.key == k) {
|
||||
if (outNormalZ) *outNormalZ = e.normalZ;
|
||||
return e.resultZ;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void put(float x, float y, float result, float normalZ, uint32_t frame) {
|
||||
uint64_t k = makeKey(x, y);
|
||||
size_t slot = k % CAPACITY;
|
||||
entries[slot] = { k, result, normalZ, frame };
|
||||
}
|
||||
};
|
||||
mutable FrameFloorCache frameFloorCache_;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -26,18 +26,10 @@ std::optional<float> selectReachableFloor(const std::optional<float>& terrainH,
|
|||
if (terrainH && *terrainH <= refZ + maxStepUp) reachTerrain = terrainH;
|
||||
if (wmoH && *wmoH <= refZ + maxStepUp) reachWmo = wmoH;
|
||||
|
||||
// Avoid snapping up to higher WMO floors when entering buildings.
|
||||
if (reachTerrain && reachWmo && *reachWmo > refZ + 3.5f) {
|
||||
return reachTerrain;
|
||||
}
|
||||
|
||||
if (reachTerrain && reachWmo) {
|
||||
// Both available: prefer the one closest to the player's feet.
|
||||
// This prevents tunnels/caves from snapping the player up to the
|
||||
// terrain surface above, while still working on top of buildings.
|
||||
float distTerrain = std::abs(*reachTerrain - refZ);
|
||||
float distWmo = std::abs(*reachWmo - refZ);
|
||||
return (distWmo <= distTerrain) ? reachWmo : reachTerrain;
|
||||
// Prefer the highest surface — prevents clipping through
|
||||
// WMO floors that sit above terrain.
|
||||
return (*reachWmo >= *reachTerrain) ? reachWmo : reachTerrain;
|
||||
}
|
||||
if (reachWmo) return reachWmo;
|
||||
if (reachTerrain) return reachTerrain;
|
||||
|
|
@ -394,6 +386,7 @@ void CameraController::update(float deltaTime) {
|
|||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
candidate.z = std::max(candidate.z, adjusted.z);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -467,29 +460,10 @@ void CameraController::update(float deltaTime) {
|
|||
if (wmoRenderer) {
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
// Before blocking, check if there's a walkable floor at the
|
||||
// destination (stair step-up or ramp continuation).
|
||||
float feetZ = stepPos.z;
|
||||
float probeZ = feetZ + 2.5f;
|
||||
auto floorH = wmoRenderer->getFloorHeight(
|
||||
candidate.x, candidate.y, probeZ);
|
||||
bool walkable = floorH &&
|
||||
*floorH >= feetZ - 0.5f &&
|
||||
*floorH <= feetZ + 1.6f;
|
||||
if (!walkable) {
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
// Snap Z to floor at adjusted position to prevent fall-through
|
||||
auto adjFloor = wmoRenderer->getFloorHeight(adjusted.x, adjusted.y, feetZ + 2.5f);
|
||||
if (adjFloor && *adjFloor >= feetZ - 0.3f && *adjFloor <= feetZ + 1.6f) {
|
||||
candidate.z = *adjFloor;
|
||||
}
|
||||
} else if (floorH && *floorH > candidate.z) {
|
||||
// Snap Z to ramp surface so subsequent sweep
|
||||
// steps measure feetZ from the ramp, not the
|
||||
// starting position.
|
||||
candidate.z = *floorH;
|
||||
}
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
// Accept upward Z correction (ramps), reject downward
|
||||
candidate.z = std::max(candidate.z, adjusted.z);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -508,110 +482,6 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// WoW-style slope limiting (50 degrees, with sliding)
|
||||
// dot(normal, up) >= 0.64 is walkable, otherwise slide
|
||||
constexpr bool ENABLE_SLOPE_SLIDE = false;
|
||||
constexpr float MAX_WALK_SLOPE_DOT = 0.6428f; // cos(50°)
|
||||
constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation
|
||||
if (ENABLE_SLOPE_SLIDE) {
|
||||
glm::vec3 oldPos = *followTarget;
|
||||
float moveXY = glm::length(glm::vec2(targetPos.x - oldPos.x, targetPos.y - oldPos.y));
|
||||
if (moveXY >= 0.03f) {
|
||||
struct GroundSample {
|
||||
std::optional<float> height;
|
||||
bool fromM2 = false;
|
||||
};
|
||||
// Helper to get ground height at a position and whether M2 provided the top floor.
|
||||
auto getGroundAt = [&](float x, float y) -> GroundSample {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
float stepUpBudget = grounded ? 1.6f : 1.2f;
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + stepUpBudget + 0.5f);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2H = m2Renderer->getFloorHeight(x, y, targetPos.z);
|
||||
}
|
||||
bool firstPerson = (!thirdPerson) || (currentDistance < 0.6f);
|
||||
if (firstPerson) {
|
||||
wmoH.reset();
|
||||
}
|
||||
auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
bool fromM2 = false;
|
||||
if (m2H && *m2H <= targetPos.z + stepUpBudget && (!base || *m2H > *base)) {
|
||||
base = m2H;
|
||||
fromM2 = true;
|
||||
}
|
||||
return GroundSample{base, fromM2};
|
||||
};
|
||||
|
||||
// Get ground height at target position
|
||||
auto center = getGroundAt(targetPos.x, targetPos.y);
|
||||
bool skipSlopeCheck = center.height && center.fromM2;
|
||||
if (center.height && !skipSlopeCheck) {
|
||||
|
||||
// Calculate ground normal using height samples
|
||||
auto hPosX = getGroundAt(targetPos.x + SAMPLE_DIST, targetPos.y);
|
||||
auto hNegX = getGroundAt(targetPos.x - SAMPLE_DIST, targetPos.y);
|
||||
auto hPosY = getGroundAt(targetPos.x, targetPos.y + SAMPLE_DIST);
|
||||
auto hNegY = getGroundAt(targetPos.x, targetPos.y - SAMPLE_DIST);
|
||||
|
||||
// Estimate partial derivatives
|
||||
float dzdx = 0.0f, dzdy = 0.0f;
|
||||
if (hPosX.height && hNegX.height) {
|
||||
dzdx = (*hPosX.height - *hNegX.height) / (2.0f * SAMPLE_DIST);
|
||||
} else if (hPosX.height) {
|
||||
dzdx = (*hPosX.height - *center.height) / SAMPLE_DIST;
|
||||
} else if (hNegX.height) {
|
||||
dzdx = (*center.height - *hNegX.height) / SAMPLE_DIST;
|
||||
}
|
||||
|
||||
if (hPosY.height && hNegY.height) {
|
||||
dzdy = (*hPosY.height - *hNegY.height) / (2.0f * SAMPLE_DIST);
|
||||
} else if (hPosY.height) {
|
||||
dzdy = (*hPosY.height - *center.height) / SAMPLE_DIST;
|
||||
} else if (hNegY.height) {
|
||||
dzdy = (*center.height - *hNegY.height) / SAMPLE_DIST;
|
||||
}
|
||||
|
||||
// Ground normal = normalize(cross(tangentX, tangentY))
|
||||
// tangentX = (1, 0, dzdx), tangentY = (0, 1, dzdy)
|
||||
// cross = (-dzdx, -dzdy, 1)
|
||||
glm::vec3 groundNormal = glm::normalize(glm::vec3(-dzdx, -dzdy, 1.0f));
|
||||
float slopeDot = groundNormal.z; // dot(normal, up) where up = (0,0,1)
|
||||
|
||||
// Check if slope is too steep
|
||||
if (slopeDot < MAX_WALK_SLOPE_DOT) {
|
||||
// Slope too steep - slide instead of walk
|
||||
// Calculate slide direction (downhill, horizontal only)
|
||||
glm::vec2 slideDir = glm::normalize(glm::vec2(-groundNormal.x, -groundNormal.y));
|
||||
|
||||
// Only block uphill movement, allow downhill/across
|
||||
glm::vec2 moveDir = glm::vec2(targetPos.x - oldPos.x, targetPos.y - oldPos.y);
|
||||
float moveDist = glm::length(moveDir);
|
||||
|
||||
if (moveDist > 0.001f) {
|
||||
glm::vec2 moveDirNorm = moveDir / moveDist;
|
||||
|
||||
// How much are we trying to go uphill?
|
||||
float uphillAmount = -glm::dot(moveDirNorm, slideDir);
|
||||
|
||||
if (uphillAmount > 0.0f) {
|
||||
// Trying to go uphill on steep slope - slide back
|
||||
float slideStrength = (1.0f - slopeDot / MAX_WALK_SLOPE_DOT);
|
||||
targetPos.x = oldPos.x + slideDir.x * moveDist * slideStrength * 0.5f;
|
||||
targetPos.y = oldPos.y + slideDir.y * moveDist * slideStrength * 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ground the character to terrain or WMO floor
|
||||
// Skip entirely while swimming — the swim floor clamp handles vertical bounds.
|
||||
if (!swimming) {
|
||||
|
|
@ -656,35 +526,31 @@ void CameraController::update(float deltaTime) {
|
|||
if (groundH) {
|
||||
hasRealGround_ = true;
|
||||
noGroundTimer_ = 0.0f;
|
||||
float groundDiff = *groundH - lastGroundZ;
|
||||
if (groundDiff > 2.0f) {
|
||||
// Landing on a higher ledge - snap up
|
||||
lastGroundZ = *groundH;
|
||||
} else {
|
||||
// Smooth toward detected ground. Use a slower rate for large
|
||||
// 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);
|
||||
}
|
||||
float feetZ = targetPos.z;
|
||||
float stepUp = 1.0f;
|
||||
float fallCatch = 3.0f;
|
||||
float dz = *groundH - feetZ;
|
||||
|
||||
if (targetPos.z <= lastGroundZ + 0.1f && verticalVelocity <= 0.0f) {
|
||||
targetPos.z = lastGroundZ;
|
||||
// WoW-style: snap to floor if within step-up or fall-catch range,
|
||||
// but only when not moving upward (jumping)
|
||||
if (dz <= stepUp && dz >= -fallCatch &&
|
||||
(verticalVelocity <= 0.0f || *groundH > feetZ)) {
|
||||
targetPos.z = *groundH;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
lastGroundZ = *groundH;
|
||||
} else {
|
||||
grounded = false;
|
||||
lastGroundZ = *groundH;
|
||||
}
|
||||
} else {
|
||||
hasRealGround_ = false;
|
||||
noGroundTimer_ += deltaTime;
|
||||
if (noGroundTimer_ < NO_GROUND_GRACE) {
|
||||
// Brief grace period for terrain streaming — hold position
|
||||
targetPos.z = lastGroundZ;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
} else {
|
||||
// No geometry found for too long — let player fall
|
||||
grounded = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -734,6 +600,7 @@ void CameraController::update(float deltaTime) {
|
|||
float distFromLastCheck = glm::length(targetPos - lastInsideWMOCheckPos);
|
||||
if (++insideWMOCheckCounter >= 10 || distFromLastCheck > 2.0f) {
|
||||
cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr);
|
||||
wmoRenderer->updateActiveGroup(targetPos.x, targetPos.y, targetPos.z + 1.0f);
|
||||
insideWMOCheckCounter = 0;
|
||||
lastInsideWMOCheckPos = targetPos;
|
||||
}
|
||||
|
|
@ -781,8 +648,18 @@ void CameraController::update(float deltaTime) {
|
|||
auto camTerrainH = getTerrainFloorAt(smoothedCamPos.x, smoothedCamPos.y);
|
||||
std::optional<float> camWmoH;
|
||||
if (wmoRenderer) {
|
||||
camWmoH = wmoRenderer->getFloorHeight(
|
||||
smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z + 3.0f);
|
||||
// Skip expensive WMO floor query if camera barely moved
|
||||
float camDelta = glm::length(glm::vec2(smoothedCamPos.x - lastCamFloorQueryPos.x,
|
||||
smoothedCamPos.y - lastCamFloorQueryPos.y));
|
||||
if (camDelta < 0.3f && hasCachedCamFloor) {
|
||||
camWmoH = cachedCamWmoFloor;
|
||||
} else {
|
||||
camWmoH = wmoRenderer->getFloorHeight(
|
||||
smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z + 3.0f);
|
||||
cachedCamWmoFloor = camWmoH;
|
||||
hasCachedCamFloor = true;
|
||||
lastCamFloorQueryPos = smoothedCamPos;
|
||||
}
|
||||
}
|
||||
auto camFloorH = selectReachableFloor(
|
||||
camTerrainH, camWmoH, smoothedCamPos.z, 3.0f);
|
||||
|
|
@ -925,7 +802,9 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 candidate = stepPos + stepDelta;
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
candidate = adjusted;
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
candidate.z = std::max(candidate.z, adjusted.z);
|
||||
}
|
||||
stepPos = candidate;
|
||||
}
|
||||
|
|
@ -963,26 +842,24 @@ void CameraController::update(float deltaTime) {
|
|||
std::optional<float> groundH = sampleGround(newPos.x, newPos.y);
|
||||
|
||||
if (groundH) {
|
||||
float groundDiff = *groundH - lastGroundZ;
|
||||
if (groundDiff > 2.0f) {
|
||||
lastGroundZ = *groundH;
|
||||
} else {
|
||||
float rate = (groundDiff > -2.0f) ? 15.0f : 6.0f;
|
||||
lastGroundZ += groundDiff * std::min(1.0f, deltaTime * rate);
|
||||
}
|
||||
float feetZ = newPos.z - eyeHeight;
|
||||
float stepUp = 1.0f;
|
||||
float fallCatch = 3.0f;
|
||||
float dz = *groundH - feetZ;
|
||||
|
||||
float groundZ = lastGroundZ + eyeHeight;
|
||||
if (newPos.z <= groundZ) {
|
||||
newPos.z = groundZ;
|
||||
if (dz <= stepUp && dz >= -fallCatch &&
|
||||
(verticalVelocity <= 0.0f || *groundH > feetZ)) {
|
||||
newPos.z = *groundH + eyeHeight;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
swimming = false; // Touching ground = wading
|
||||
lastGroundZ = *groundH;
|
||||
swimming = false;
|
||||
} else if (!swimming) {
|
||||
grounded = false;
|
||||
lastGroundZ = *groundH;
|
||||
}
|
||||
} else if (!swimming) {
|
||||
float groundZ = lastGroundZ + eyeHeight;
|
||||
newPos.z = groundZ;
|
||||
newPos.z = lastGroundZ + eyeHeight;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
}
|
||||
|
|
@ -1332,6 +1209,11 @@ void CameraController::teleportTo(const glm::vec3& pos) {
|
|||
autoUnstuckFired_ = false;
|
||||
continuousFallTime_ = 0.0f;
|
||||
|
||||
// Invalidate active WMO group so it's re-detected at new position
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->updateActiveGroup(pos.x, pos.y, pos.z + 1.0f);
|
||||
}
|
||||
|
||||
if (thirdPerson && followTarget) {
|
||||
*followTarget = pos;
|
||||
camera->setRotation(yaw, pitch);
|
||||
|
|
|
|||
|
|
@ -1572,7 +1572,13 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
if (gridCellsX > 64) gridCellsX = 64;
|
||||
if (gridCellsY > 64) gridCellsY = 64;
|
||||
|
||||
cellTriangles.resize(gridCellsX * gridCellsY);
|
||||
size_t totalCells = gridCellsX * gridCellsY;
|
||||
cellTriangles.resize(totalCells);
|
||||
cellFloorTriangles.resize(totalCells);
|
||||
cellWallTriangles.resize(totalCells);
|
||||
|
||||
size_t numTriangles = collisionIndices.size() / 3;
|
||||
triBounds.resize(numTriangles);
|
||||
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
|
@ -1588,6 +1594,22 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
float triMaxX = std::max({v0.x, v1.x, v2.x});
|
||||
float triMaxY = std::max({v0.y, v1.y, v2.y});
|
||||
|
||||
// Per-triangle Z bounds
|
||||
float triMinZ = std::min({v0.z, v1.z, v2.z});
|
||||
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.
|
||||
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;
|
||||
bool isFloor = (absNz >= 0.45f);
|
||||
bool isWall = (absNz < 0.65f); // Matches walkable slope 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));
|
||||
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((triMaxX - gridOrigin.x) * invCellW));
|
||||
|
|
@ -1596,7 +1618,10 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
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);
|
||||
int cellIdx = cy * gridCellsX + cx;
|
||||
cellTriangles[cellIdx].push_back(triIdx);
|
||||
if (isFloor) cellFloorTriangles[cellIdx].push_back(triIdx);
|
||||
if (isWall) cellWallTriangles[cellIdx].push_back(triIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1651,7 +1676,73 @@ void WMORenderer::GroupResources::getTrianglesInRange(
|
|||
}
|
||||
}
|
||||
|
||||
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
|
||||
void WMORenderer::GroupResources::getFloorTrianglesInRange(
|
||||
float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const {
|
||||
out.clear();
|
||||
if (gridCellsX == 0 || gridCellsY == 0 || cellFloorTriangles.empty()) 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;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
void WMORenderer::GroupResources::getWallTrianglesInRange(
|
||||
float minX, float minY, float maxX, float maxY,
|
||||
std::vector<uint32_t>& out) const {
|
||||
out.clear();
|
||||
if (gridCellsX == 0 || gridCellsY == 0 || cellWallTriangles.empty()) 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;
|
||||
|
||||
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 {
|
||||
// Per-frame dedup cache: same (x,y) queried 3-5x per frame
|
||||
auto frameCached = frameFloorCache_.get(glX, glY, currentFrameId, outNormalZ);
|
||||
if (frameCached) return *frameCached;
|
||||
|
||||
// Check persistent grid cache first (computed lazily, never expires)
|
||||
uint64_t gridKey = floorGridKey(glX, glY);
|
||||
auto gridIt = precomputedFloorGrid.find(gridKey);
|
||||
|
|
@ -1661,18 +1752,77 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
// For multi-story buildings, the cache has the top floor - if we're on a
|
||||
// lower floor, skip cache and do full raycast to find actual floor.
|
||||
if (cachedHeight <= glZ + 2.0f && cachedHeight >= glZ - 4.0f) {
|
||||
// Persistent cache doesn't store normal — report as flat
|
||||
if (outNormalZ) *outNormalZ = 1.0f;
|
||||
frameFloorCache_.put(glX, glY, cachedHeight, 1.0f, currentFrameId);
|
||||
return cachedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
std::optional<float> bestFloor;
|
||||
float bestNormalZ = 1.0f;
|
||||
bool bestFromLowPlatform = false;
|
||||
|
||||
// World-space ray: from high above, pointing straight down
|
||||
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
||||
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
|
||||
|
||||
// Lambda to test a single group for floor hits
|
||||
auto testGroupFloor = [&](const WMOInstance& instance, const ModelData& model,
|
||||
const GroupResources& group,
|
||||
const glm::vec3& localOrigin, const glm::vec3& localDir) {
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
// Use unfiltered triangle list: a vertical ray naturally misses vertical
|
||||
// geometry via ray-triangle intersection, so pre-filtering by normal is
|
||||
// unnecessary and risks excluding legitimate floor geometry (steep ramps,
|
||||
// stair treads with non-trivial normals).
|
||||
group.getTrianglesInRange(
|
||||
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
||||
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
||||
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]];
|
||||
|
||||
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));
|
||||
|
||||
float allowAbove = model.isLowPlatform ? 12.0f : 2.0f;
|
||||
if (hitWorld.z <= glZ + allowAbove) {
|
||||
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
|
||||
if (localNormal.z < 0.0f) localNormal = -localNormal;
|
||||
glm::vec3 worldNormal = glm::normalize(
|
||||
glm::vec3(instance.modelMatrix * glm::vec4(localNormal, 0.0f)));
|
||||
bestNormalZ = std::abs(worldNormal.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Full scan: test all instances (active group fast path removed to fix
|
||||
// bridge clipping where early-return missed other WMO instances)
|
||||
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);
|
||||
|
|
@ -1733,41 +1883,7 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
continue;
|
||||
}
|
||||
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
// Use spatial grid to test triangles near the query XY.
|
||||
// Query a small range (±1 unit) to catch floor triangles at cell boundaries
|
||||
// (bridges, narrow walkways whose triangles may sit in adjacent cells).
|
||||
group.getTrianglesInRange(
|
||||
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
||||
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
||||
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]];
|
||||
|
||||
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));
|
||||
|
||||
float allowAbove = model.isLowPlatform ? 12.0f : 0.5f;
|
||||
if (hitWorld.z <= glZ + allowAbove) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
bestFromLowPlatform = model.isLowPlatform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
testGroupFloor(instance, model, group, localOrigin, localDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1781,6 +1897,11 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
}
|
||||
}
|
||||
|
||||
if (bestFloor) {
|
||||
if (outNormalZ) *outNormalZ = bestNormalZ;
|
||||
frameFloorCache_.put(glX, glY, *bestFloor, bestNormalZ, currentFrameId);
|
||||
}
|
||||
|
||||
return bestFloor;
|
||||
}
|
||||
|
||||
|
|
@ -1790,13 +1911,13 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
bool blocked = false;
|
||||
|
||||
glm::vec3 moveDir = to - from;
|
||||
float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y));
|
||||
if (moveDistXY < 0.001f) return false;
|
||||
float moveDist = glm::length(moveDir);
|
||||
if (moveDist < 0.001f) return false;
|
||||
|
||||
// Player collision parameters
|
||||
const float PLAYER_RADIUS = 0.70f; // Wider radius for better wall collision
|
||||
const float PLAYER_HEIGHT = 2.0f; // Player height for wall checks
|
||||
const float MAX_STEP_HEIGHT = 1.0f; // Allow stepping up stairs/ramps
|
||||
// Player collision parameters — WoW-style horizontal cylinder
|
||||
const float PLAYER_RADIUS = 0.50f; // Horizontal cylinder radius
|
||||
const float PLAYER_HEIGHT = 2.0f; // Cylinder height for Z bounds
|
||||
const float MAX_STEP_HEIGHT = 1.0f; // Step-up threshold
|
||||
|
||||
glm::vec3 queryMin = glm::min(from, to) - glm::vec3(8.0f, 8.0f, 5.0f);
|
||||
glm::vec3 queryMax = glm::max(from, to) + glm::vec3(8.0f, 8.0f, 5.0f);
|
||||
|
|
@ -1868,14 +1989,28 @@ 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.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
// Use pre-computed Z bounds for fast vertical reject
|
||||
const auto& tb = group.triBounds[triStart / 3];
|
||||
|
||||
// Only collide with walls in player's vertical range
|
||||
if (tb.maxZ < localFeetZ + 0.3f) continue;
|
||||
if (tb.minZ > localFeetZ + PLAYER_HEIGHT) continue;
|
||||
|
||||
// Skip low geometry that can be stepped over
|
||||
if (tb.maxZ <= localFeetZ + MAX_STEP_HEIGHT) continue;
|
||||
|
||||
// Skip very short vertical surfaces (stair risers)
|
||||
float triHeight = tb.maxZ - tb.minZ;
|
||||
if (triHeight < 1.0f && tb.maxZ <= localFeetZ + 1.2f) continue;
|
||||
|
||||
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
|
||||
// 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);
|
||||
|
|
@ -1883,32 +2018,11 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
|
||||
// Skip near-horizontal triangles (floors/ceilings/ramps).
|
||||
// Anything more horizontal than ~55° from vertical is walkable.
|
||||
if (std::abs(normal.z) > 0.55f) continue;
|
||||
|
||||
// Get triangle Z range
|
||||
float triMinZ = std::min({v0.z, v1.z, v2.z});
|
||||
float triMaxZ = std::max({v0.z, v1.z, v2.z});
|
||||
|
||||
// Only collide with walls in player's vertical range
|
||||
if (triMaxZ < localFeetZ + 0.3f) continue;
|
||||
if (triMinZ > localFeetZ + PLAYER_HEIGHT) continue;
|
||||
|
||||
// Simplified wall detection: any vertical-ish triangle above step height is a wall.
|
||||
float triHeight = triMaxZ - triMinZ;
|
||||
|
||||
// Skip low geometry that can be stepped over
|
||||
if (triMaxZ <= localFeetZ + MAX_STEP_HEIGHT) continue;
|
||||
|
||||
// Skip very short vertical surfaces (stair risers)
|
||||
if (triHeight < 1.0f && triMaxZ <= localFeetZ + 1.2f) continue;
|
||||
|
||||
// Recompute distances with current (possibly pushed) localTo
|
||||
// Recompute plane distances with current (possibly pushed) localTo
|
||||
float fromDist = glm::dot(localFrom - v0, normal);
|
||||
float toDist = glm::dot(localTo - v0, normal);
|
||||
|
||||
// Swept test: prevent tunneling when crossing a wall between frames.
|
||||
// Swept test: prevent tunneling when crossing a wall between frames
|
||||
if ((fromDist > PLAYER_RADIUS && toDist < -PLAYER_RADIUS) ||
|
||||
(fromDist < -PLAYER_RADIUS && toDist > PLAYER_RADIUS)) {
|
||||
float denom = (fromDist - toDist);
|
||||
|
|
@ -1921,11 +2035,12 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (hitErrSq <= 0.15f * 0.15f) {
|
||||
float side = fromDist > 0.0f ? 1.0f : -1.0f;
|
||||
glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f);
|
||||
glm::vec3 safeWorld = glm::vec3(instance.modelMatrix * glm::vec4(safeLocal, 1.0f));
|
||||
adjustedPos.x = safeWorld.x;
|
||||
adjustedPos.y = safeWorld.y;
|
||||
// Update localTo for subsequent triangle checks
|
||||
localTo = glm::vec3(instance.invModelMatrix * glm::vec4(adjustedPos, 1.0f));
|
||||
glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f);
|
||||
localTo.x = safeLocal.x;
|
||||
localTo.y = safeLocal.y;
|
||||
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
|
||||
adjustedPos.x += pushWorld.x;
|
||||
adjustedPos.y += pushWorld.y;
|
||||
blocked = true;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1933,23 +2048,28 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
}
|
||||
}
|
||||
|
||||
// Distance-based collision: push player out of walls
|
||||
// Horizontal cylinder collision: closest point + horizontal distance
|
||||
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) {
|
||||
// Skip floor-like surfaces — grounding handles them, not wall collision
|
||||
float absNz = std::abs(normal.z);
|
||||
if (absNz >= 0.45f) continue;
|
||||
|
||||
float pushDist = PLAYER_RADIUS - horizDist + 0.02f;
|
||||
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);
|
||||
float n2Len = glm::length(n2);
|
||||
if (n2Len < 1e-4f) continue;
|
||||
pushDir2 = n2 / n2Len;
|
||||
}
|
||||
glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f);
|
||||
|
||||
// Update localTo so subsequent triangles use corrected position
|
||||
localTo.x += pushLocal.x;
|
||||
localTo.y += pushLocal.y;
|
||||
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
|
||||
|
|
@ -1964,6 +2084,104 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
return blocked;
|
||||
}
|
||||
|
||||
void WMORenderer::updateActiveGroup(float glX, float glY, float glZ) {
|
||||
// If active group is still valid, check if player is still inside it
|
||||
if (activeGroup_.isValid() && activeGroup_.instanceIdx < instances.size()) {
|
||||
const auto& instance = instances[activeGroup_.instanceIdx];
|
||||
if (instance.modelId == activeGroup_.modelId) {
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it != loadedModels.end()) {
|
||||
const ModelData& model = it->second;
|
||||
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
||||
|
||||
// Still inside active group?
|
||||
if (activeGroup_.groupIdx >= 0 && static_cast<size_t>(activeGroup_.groupIdx) < model.groups.size()) {
|
||||
const auto& group = model.groups[activeGroup_.groupIdx];
|
||||
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
|
||||
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
|
||||
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
|
||||
return; // Still in same group
|
||||
}
|
||||
}
|
||||
|
||||
// Check portal-neighbor groups
|
||||
for (uint32_t ngi : activeGroup_.neighborGroups) {
|
||||
if (ngi < model.groups.size()) {
|
||||
const auto& group = model.groups[ngi];
|
||||
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
|
||||
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
|
||||
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
|
||||
// Moved to a neighbor group — update
|
||||
activeGroup_.groupIdx = static_cast<int32_t>(ngi);
|
||||
// Rebuild neighbors for new group
|
||||
activeGroup_.neighborGroups.clear();
|
||||
if (ngi < model.groupPortalRefs.size()) {
|
||||
auto [portalStart, portalCount] = model.groupPortalRefs[ngi];
|
||||
for (uint16_t pi = 0; pi < portalCount; pi++) {
|
||||
uint16_t refIdx = portalStart + pi;
|
||||
if (refIdx < model.portalRefs.size()) {
|
||||
uint32_t tgt = model.portalRefs[refIdx].groupIndex;
|
||||
if (tgt < model.groups.size()) {
|
||||
activeGroup_.neighborGroups.push_back(tgt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full scan: find which instance/group contains the player
|
||||
activeGroup_.invalidate();
|
||||
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||
glZ < instance.worldBoundsMin.z || glZ > instance.worldBoundsMax.z) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
|
||||
const ModelData& model = it->second;
|
||||
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
||||
|
||||
int gi = findContainingGroup(model, localPos);
|
||||
if (gi >= 0) {
|
||||
activeGroup_.instanceIdx = static_cast<uint32_t>(idx);
|
||||
activeGroup_.modelId = instance.modelId;
|
||||
activeGroup_.groupIdx = gi;
|
||||
|
||||
// Build neighbor list from portal refs
|
||||
activeGroup_.neighborGroups.clear();
|
||||
uint32_t groupIdx = static_cast<uint32_t>(gi);
|
||||
if (groupIdx < model.groupPortalRefs.size()) {
|
||||
auto [portalStart, portalCount] = model.groupPortalRefs[groupIdx];
|
||||
for (uint16_t pi = 0; pi < portalCount; pi++) {
|
||||
uint16_t refIdx = portalStart + pi;
|
||||
if (refIdx < model.portalRefs.size()) {
|
||||
uint32_t tgt = model.portalRefs[refIdx].groupIndex;
|
||||
if (tgt < model.groups.size()) {
|
||||
activeGroup_.neighborGroups.push_back(tgt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId) const {
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
|
|
@ -2063,8 +2281,8 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
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.
|
||||
// Wall list pre-filters at abs(normal.z) < 0.55, but for camera raycast we want
|
||||
// a stricter threshold to avoid ramp/stair geometry pulling the camera in.
|
||||
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;
|
||||
|
|
@ -2118,7 +2336,7 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
continue;
|
||||
}
|
||||
|
||||
// Narrow-phase: triangle raycast using spatial grid.
|
||||
// Narrow-phase: triangle raycast using spatial grid (wall-only).
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
|
|
@ -2129,7 +2347,7 @@ 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.getTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, wallTriScratch);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
|
|
@ -2141,6 +2359,7 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
continue;
|
||||
}
|
||||
triNormal /= std::sqrt(normalLenSq);
|
||||
// Wall list pre-filters at 0.55; 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