From 871172d63e4b42ac3405370279939ff8de117485 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 3 Feb 2026 19:10:22 -0800 Subject: [PATCH] Tune collision behavior for ramps, props, and structural walls --- include/rendering/m2_renderer.hpp | 2 + src/rendering/camera_controller.cpp | 14 ++-- src/rendering/m2_renderer.cpp | 114 ++++++++++++++++++++++++---- src/rendering/wmo_renderer.cpp | 80 +++++++++++++++++-- 4 files changed, 186 insertions(+), 24 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 27af8f45..dec4fa77 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -45,6 +45,8 @@ struct M2ModelGPU { bool collisionSteppedFountain = false; bool collisionSteppedLowPlatform = false; bool collisionPlanter = false; + bool collisionSmallSolidProp = false; + bool collisionNarrowVerticalProp = false; bool collisionNoBlock = false; std::string name; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 7a6ec073..5c62ea80 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -295,9 +295,10 @@ void CameraController::update(float deltaTime) { if (m2Renderer) { m2H = m2Renderer->getFloorHeight(x, y, targetPos.z); } - auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f); + float stepUpBudget = grounded ? 1.6f : 1.2f; + auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget); bool fromM2 = false; - if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) { + if (m2H && *m2H <= targetPos.z + stepUpBudget && (!base || *m2H > *base)) { base = m2H; fromM2 = true; } @@ -382,8 +383,9 @@ void CameraController::update(float deltaTime) { if (m2Renderer) { m2H = m2Renderer->getFloorHeight(x, y, targetPos.z); } - auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f); - if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) { + float stepUpBudget = grounded ? 1.6f : 1.2f; + auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget); + if (m2H && *m2H <= targetPos.z + stepUpBudget && (!base || *m2H > *base)) { base = m2H; } return base; @@ -393,7 +395,9 @@ void CameraController::update(float deltaTime) { std::optional groundH; constexpr float FOOTPRINT = 0.28f; const glm::vec2 offsets[] = { - {0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT} + {0.0f, 0.0f}, + {FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f}, + {0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT} }; for (const auto& o : offsets) { auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index cadc04df..52e5fb33 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -24,11 +24,23 @@ void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::ve glm::vec3 half = (model.boundMax - model.boundMin) * 0.5f; // Per-shape collision fitting: + // - small solid props (boxes/crates/chests): tighter than full mesh, but + // larger than default to prevent walk-through on narrow objects // - default: tighter fit (avoid oversized blockers) // - stepped low platforms (tree curbs/planters): wider XY + lower Z - if (model.collisionSteppedLowPlatform) { - half.x *= 0.92f; - half.y *= 0.92f; + if (model.collisionNarrowVerticalProp) { + // Tall thin props (lamps/posts): keep passable gaps near walls. + half.x *= 0.30f; + half.y *= 0.30f; + half.z *= 0.96f; + } else if (model.collisionSmallSolidProp) { + // Keep full tight mesh bounds for small solid props to avoid clip-through. + half.x *= 1.00f; + half.y *= 1.00f; + half.z *= 1.00f; + } else if (model.collisionSteppedLowPlatform) { + half.x *= 0.98f; + half.y *= 0.98f; half.z *= 0.52f; } else { half.x *= 0.66f; @@ -352,6 +364,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bool isPlanter = (lowerName.find("planter") != std::string::npos); gpuModel.collisionPlanter = isPlanter; + bool smallSolidPropName = + (lowerName.find("crate") != std::string::npos) || + (lowerName.find("box") != std::string::npos) || + (lowerName.find("chest") != std::string::npos) || + (lowerName.find("barrel") != std::string::npos); bool foliageName = (lowerName.find("bush") != std::string::npos) || (lowerName.find("grass") != std::string::npos) || @@ -373,6 +390,33 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bool smallSoftShape = (horiz < 2.2f && vert < 2.4f); bool mediumFoliageShape = (horiz < 4.5f && vert < 4.5f); bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter; + bool narrowVerticalName = + (lowerName.find("lamp") != std::string::npos) || + (lowerName.find("lantern") != std::string::npos) || + (lowerName.find("post") != std::string::npos) || + (lowerName.find("pole") != std::string::npos); + bool narrowVerticalShape = + (horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f); + gpuModel.collisionNarrowVerticalProp = + !gpuModel.collisionSteppedFountain && + !gpuModel.collisionSteppedLowPlatform && + (narrowVerticalName || narrowVerticalShape); + bool genericSolidPropShape = + (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f); + bool curbLikeName = + (lowerName.find("curb") != std::string::npos) || + (lowerName.find("planter") != std::string::npos) || + (lowerName.find("ring") != std::string::npos) || + (lowerName.find("well") != std::string::npos) || + (lowerName.find("base") != std::string::npos); + bool lowPlatformLikeShape = lowWideShape || lowPlatformShape; + gpuModel.collisionSmallSolidProp = + !gpuModel.collisionSteppedFountain && + !gpuModel.collisionSteppedLowPlatform && + !gpuModel.collisionNarrowVerticalProp && + !curbLikeName && + !lowPlatformLikeShape && + (smallSolidPropName || (genericSolidPropShape && !foliageName && !softTree)); gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) && !forceSolidCurb); } @@ -881,8 +925,13 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f)); // Must be within doodad footprint in local XY. - if (localPos.x < localMin.x || localPos.x > localMax.x || - localPos.y < localMin.y || localPos.y > localMax.y) { + // Stepped low platforms get a small pad so walk-up snapping catches edges. + float footprintPad = 0.0f; + if (model.collisionSteppedLowPlatform) { + footprintPad = model.collisionPlanter ? 0.22f : 0.16f; + } + if (localPos.x < localMin.x - footprintPad || localPos.x > localMax.x + footprintPad || + localPos.y < localMin.y - footprintPad || localPos.y > localMax.y + footprintPad) { continue; } @@ -892,7 +941,12 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) glm::vec3 worldTop = glm::vec3(instance.modelMatrix * glm::vec4(localTop, 1.0f)); // Reachability filter: allow a bit more climb for stepped low platforms. - float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.2f : 1.8f) : 1.0f; + float maxStepUp = 1.0f; + if (model.collisionSmallSolidProp) { + maxStepUp = 2.0f; + } else if (model.collisionSteppedLowPlatform) { + maxStepUp = model.collisionPlanter ? 3.0f : 2.4f; + } if (worldTop.z > glZ + maxStepUp) continue; if (!bestFloor || worldTop.z > *bestFloor) { @@ -938,13 +992,17 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3 localFrom = glm::vec3(instance.invModelMatrix * glm::vec4(from, 1.0f)); glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(adjustedPos, 1.0f)); - float localRadius = playerRadius / instance.scale; + float radiusScale = model.collisionNarrowVerticalProp ? 0.45f : 1.0f; + float localRadius = (playerRadius * radiusScale) / instance.scale; glm::vec3 rawMin, rawMax; getTightCollisionBounds(model, rawMin, rawMax); glm::vec3 localMin = rawMin - glm::vec3(localRadius); glm::vec3 localMax = rawMax + glm::vec3(localRadius); float effectiveTop = getEffectiveCollisionTopLocal(model, localPos, rawMin, rawMax) + localRadius; + glm::vec2 localCenter((localMin.x + localMax.x) * 0.5f, (localMin.y + localMax.y) * 0.5f); + float fromR = glm::length(glm::vec2(localFrom.x, localFrom.y) - localCenter); + float toR = glm::length(glm::vec2(localPos.x, localPos.y) - localCenter); // Feet-based vertical overlap test: ignore objects fully above/below us. constexpr float PLAYER_HEIGHT = 2.0f; @@ -952,15 +1010,38 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, continue; } + bool fromInsideXY = + (localFrom.x >= localMin.x && localFrom.x <= localMax.x && + localFrom.y >= localMin.y && localFrom.y <= localMax.y); + bool fromInsideZ = (localFrom.z + PLAYER_HEIGHT >= localMin.z && localFrom.z <= effectiveTop); + bool escapingOverlap = (fromInsideXY && fromInsideZ && (toR > fromR + 1e-4f)); + bool allowEscapeRelax = escapingOverlap && !model.collisionSmallSolidProp; + // Swept hard clamp for taller blockers only. // Low/stepable objects should be climbable and not "shove" the player off. - float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.8f : 2.4f) : 1.20f; + float maxStepUp = 1.20f; + if (model.collisionSmallSolidProp) { + // Keep box/crate-class props hard-solid to prevent phase-through. + maxStepUp = 0.75f; + } else if (model.collisionSteppedLowPlatform) { + maxStepUp = model.collisionPlanter ? 2.8f : 2.4f; + } bool stepableLowObject = (effectiveTop <= localFrom.z + maxStepUp); bool climbingAttempt = (localPos.z > localFrom.z + 0.18f); bool nearTop = (localFrom.z >= effectiveTop - 0.30f); - bool climbingTowardTop = climbingAttempt && (localFrom.z + (model.collisionPlanter ? 0.95f : 0.60f) >= effectiveTop); - bool forceHardLateral = model.collisionSteppedLowPlatform && !model.collisionPlanter && !nearTop && !climbingTowardTop; - if (!stepableLowObject || forceHardLateral) { + float climbAllowance = model.collisionPlanter ? 0.95f : 0.60f; + if (model.collisionSteppedLowPlatform && !model.collisionPlanter) { + // Let low curb/planter blocks be stepable without sticky side shoves. + climbAllowance = 1.00f; + } + if (model.collisionSmallSolidProp) { + climbAllowance = 1.05f; + } + bool climbingTowardTop = climbingAttempt && (localFrom.z + climbAllowance >= effectiveTop); + bool forceHardLateral = + model.collisionSmallSolidProp && + !nearTop && !climbingTowardTop; + if ((!stepableLowObject || forceHardLateral) && !allowEscapeRelax) { float tEnter = 0.0f; glm::vec3 sweepMax = localMax; sweepMax.z = std::min(sweepMax.z, effectiveTop); @@ -986,14 +1067,19 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, float pushFront = localMax.y - localPos.y; float minPush = std::min({pushLeft, pushRight, pushBack, pushFront}); - if (model.collisionSteppedLowPlatform && nearTop && stepableLowObject) { + if (allowEscapeRelax) { + continue; + } + if (model.collisionSteppedLowPlatform && stepableLowObject) { // Already on/near top surface: don't apply lateral push that ejects - // the player from the curb when landing. + // the player from the object when landing. continue; } // Gentle fallback push for overlapping cases. float pushAmount; - if (model.collisionSteppedLowPlatform) { + if (model.collisionNarrowVerticalProp) { + pushAmount = std::clamp(minPush * 0.10f, 0.001f, 0.010f); + } else if (model.collisionSteppedLowPlatform) { if (model.collisionPlanter && stepableLowObject) { pushAmount = std::clamp(minPush * 0.06f, 0.001f, 0.006f); } else { diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index d110f07d..0849974a 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1067,6 +1067,10 @@ 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; + // Use a tiny Z threshold so ramp-side logic still triggers with + // smoothed ground snapping and small per-step vertical deltas. + bool steppingUp = (localTo.z > localFrom.z + 0.005f); + bool steppingDown = (localTo.z < localFrom.z - 0.005f); for (const auto& group : model.groups) { // Quick bounding box check @@ -1100,6 +1104,10 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Get triangle Z range float triMinZ = std::min({v0.z, v1.z, v2.z}); float triMaxZ = std::max({v0.z, v1.z, v2.z}); + float fromDist = glm::dot(localFrom - v0, normal); + float toDist = glm::dot(localTo - v0, normal); + bool towardWallMotion = (std::abs(toDist) + 1e-4f < std::abs(fromDist)); + bool awayFromWallMotion = !towardWallMotion; // Only collide with walls in player's vertical range if (triMaxZ < localFeetZ + 0.3f) continue; @@ -1112,17 +1120,56 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Ignore short near-vertical side strips around ramps/edges. // These commonly act like invisible side guard rails. float triHeight = triMaxZ - triMinZ; + bool likelyRealWall = + (std::abs(normal.z) < 0.20f) && + (triHeight > 2.4f || triMaxZ > localFeetZ + 2.6f); + bool structuralWall = + (triHeight > 1.6f) && + (triMaxZ > localFeetZ + 1.8f); if (std::abs(normal.z) < 0.25f && triHeight < 1.8f && triMaxZ <= localFeetZ + 1.4f) { continue; } + // Motion-aware permissive ramp side strips: + // keeps side entry/exit from behaving like invisible rails. + bool rampSideStrip = false; + if (steppingUp) { + rampSideStrip = + (std::abs(normal.z) > 0.02f && std::abs(normal.z) < 0.45f) && + triMinZ <= localFeetZ + 0.30f && + triHeight < 3.6f && + triMaxZ <= localFeetZ + 2.8f; + } else if (steppingDown) { + rampSideStrip = + (std::abs(normal.z) > 0.02f && std::abs(normal.z) < 0.65f) && + triMinZ <= localFeetZ + 0.45f && + triHeight < 4.5f && + triMaxZ <= localFeetZ + 3.8f; + } + // High on ramps, side triangles can span very tall strips and + // still behave like side rails. If we're stepping down and + // moving away from the wall, don't let them trap movement. + if (!rampSideStrip && + steppingDown && + awayFromWallMotion && + std::abs(normal.z) > 0.02f && std::abs(normal.z) < 0.70f && + triMinZ <= localFeetZ + 0.60f && + triMaxZ <= localFeetZ + 4.5f && + localFeetZ >= triMinZ + 0.80f) { + rampSideStrip = true; + } + if (rampSideStrip && !likelyRealWall && !structuralWall) { + continue; + } // Let players run off ramp sides: ignore lower side-wall strips // that sit around foot height and are not true tall building walls. if (std::abs(normal.z) < 0.45f && + std::abs(normal.z) > 0.05f && triMinZ <= localFeetZ + 0.20f && triHeight < 4.0f && - triMaxZ <= localFeetZ + 4.0f) { + triMaxZ <= localFeetZ + 4.0f && + !likelyRealWall && !structuralWall) { continue; } @@ -1130,10 +1177,22 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, if (triMaxZ <= localFeetZ + stepHeightLimit) continue; // Treat as step-up, not hard wall // Swept test: prevent tunneling when crossing a wall between frames. - float fromDist = glm::dot(localFrom - v0, normal); - float toDist = glm::dot(localTo - v0, normal); + bool shortRampEdgeStrip = + (steppingUp || steppingDown) && + (std::abs(normal.z) > 0.01f && std::abs(normal.z) < (steppingDown ? 0.50f : 0.30f)) && + triMinZ <= localFeetZ + (steppingDown ? 0.45f : 0.35f) && + triHeight < (steppingDown ? 4.2f : 3.0f) && + triMaxZ <= localFeetZ + (steppingDown ? 3.8f : 3.2f); if ((fromDist > PLAYER_RADIUS && toDist < -PLAYER_RADIUS) || (fromDist < -PLAYER_RADIUS && toDist > PLAYER_RADIUS)) { + // For true wall-like faces, always block segment crossing. + // Motion-direction heuristics are only for ramp-side stickiness. + if (!towardWallMotion && !likelyRealWall && !structuralWall) { + continue; + } + if (shortRampEdgeStrip && !likelyRealWall && !structuralWall) { + continue; + } float denom = (fromDist - toDist); if (std::abs(denom) > 1e-6f) { float tHit = fromDist / denom; // Segment param [0,1] @@ -1172,8 +1231,19 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, pushDir2 = glm::normalize(n2); } - // Soft pushback avoids hard side-snaps when skimming walls. - pushDist = std::min(0.06f, pushDist * 0.35f); + // Softer push when stepping up near ramp side edges. + bool rampEdgeLike = (std::abs(normal.z) < 0.45f && triHeight < 4.0f); + if (!towardWallMotion && !likelyRealWall && !structuralWall) continue; + if (shortRampEdgeStrip && + !likelyRealWall && !structuralWall && + std::abs(toDist) >= std::abs(fromDist) - PLAYER_RADIUS * 0.25f) continue; + float pushScale = 0.35f; + float pushCap = 0.06f; + if (rampEdgeLike && (steppingUp || steppingDown)) { + pushScale = steppingDown ? 0.08f : 0.12f; + pushCap = steppingDown ? 0.015f : 0.022f; + } + pushDist = std::min(pushCap, pushDist * pushScale); if (pushDist <= 0.0f) continue; glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f);