diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 8da908c8..1226ba05 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -52,12 +52,20 @@ public: void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); } void setUseWoWSpeed(bool use) { useWoWSpeed = use; } + // For first-person player hiding + void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { + characterRenderer = cr; + playerInstanceId = playerId; + } + private: Camera* camera; TerrainManager* terrainManager = nullptr; WMORenderer* wmoRenderer = nullptr; M2Renderer* m2Renderer = nullptr; WaterRenderer* waterRenderer = nullptr; + CharacterRenderer* characterRenderer = nullptr; + uint32_t playerInstanceId = 0; // Stored rotation (avoids lossy forward-vector round-trip) float yaw = 180.0f; @@ -74,13 +82,22 @@ private: bool leftMouseDown = false; bool rightMouseDown = false; - // Third-person orbit camera + // Third-person orbit camera (WoW-style) bool thirdPerson = false; - float orbitDistance = 15.0f; - float minOrbitDistance = 3.0f; - float maxOrbitDistance = 50.0f; - float zoomSpeed = 2.0f; + float userTargetDistance = 10.0f; // What the player wants (scroll wheel) + float currentDistance = 10.0f; // Smoothed actual distance + float collisionDistance = 10.0f; // Max allowed by collision + static constexpr float MIN_DISTANCE = 0.5f; // Minimum zoom (first-person threshold) + static constexpr float MAX_DISTANCE = 50.0f; // Maximum zoom out + static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases + static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths + static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height + static constexpr float CAM_SPHERE_RADIUS = 0.2f; // Collision sphere radius + static constexpr float CAM_EPSILON = 0.05f; // Offset from walls + static constexpr float MIN_PITCH = -88.0f; // Look almost straight down + static constexpr float MAX_PITCH = 35.0f; // Limited upward look glm::vec3* followTarget = nullptr; + glm::vec3 smoothedCamPos = glm::vec3(0.0f); // For smooth camera movement // Gravity / grounding float verticalVelocity = 0.0f; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 5551b8b0..72e91937 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -61,6 +61,7 @@ public: void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation); void setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets); + void setInstanceVisible(uint32_t instanceId, bool visible); void removeInstance(uint32_t instanceId); /** Attach a weapon model to a character instance at the given attachment point. */ @@ -95,6 +96,7 @@ private: glm::vec3 position; glm::vec3 rotation; float scale; + bool visible = true; // For first-person camera hiding // Animation state uint32_t currentAnimationId = 0; diff --git a/src/core/application.cpp b/src/core/application.cpp index dc501d33..b9778537 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -939,7 +939,7 @@ void Application::spawnPlayerCharacter() { // Spawn character at camera's ground position glm::vec3 spawnPos = camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f); uint32_t instanceId = charRenderer->createInstance(1, spawnPos, - glm::vec3(0.0f), 2.0f); + glm::vec3(0.0f), 1.0f); // Scale 1.0 = normal WoW character size if (instanceId > 0) { // Set up third-person follow @@ -974,6 +974,11 @@ void Application::spawnPlayerCharacter() { static_cast(spawnPos.z), ")"); playerCharacterSpawned = true; + // Set up camera controller for first-person player hiding + if (renderer->getCameraController()) { + renderer->getCameraController()->setCharacterRenderer(charRenderer, instanceId); + } + // Load equipped weapons (sword + shield) loadEquippedWeapons(); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index fe92b935..ab6ceda0 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -3,6 +3,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/water_renderer.hpp" +#include "rendering/character_renderer.hpp" #include "game/opcodes.hpp" #include "core/logger.hpp" #include @@ -164,9 +165,9 @@ void CameraController::update(float deltaTime) { glm::vec3 oldFeetPos = *followTarget; glm::vec3 adjusted; if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) { + // Only apply horizontal adjustment (don't let wall collision change Z) targetPos.x = adjusted.x; targetPos.y = adjusted.y; - targetPos.z = adjusted.z; } } @@ -179,6 +180,88 @@ void CameraController::update(float deltaTime) { } } + // WoW-style slope limiting (50 degrees, with sliding) + // dot(normal, up) >= 0.64 is walkable, otherwise slide + constexpr float MAX_WALK_SLOPE_DOT = 0.6428f; // cos(50°) + constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation + { + glm::vec3 oldPos = *followTarget; + + // Helper to get ground height at a position + auto getGroundAt = [&](float x, float y) -> std::optional { + std::optional h; + if (terrainManager) { + h = terrainManager->getHeightAt(x, y); + } + if (wmoRenderer) { + auto wh = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f); + if (wh && (!h || *wh > *h)) { + h = wh; + } + } + return h; + }; + + // Get ground height at target position + auto centerH = getGroundAt(targetPos.x, targetPos.y); + if (centerH) { + // 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 && hNegX) { + dzdx = (*hPosX - *hNegX) / (2.0f * SAMPLE_DIST); + } else if (hPosX) { + dzdx = (*hPosX - *centerH) / SAMPLE_DIST; + } else if (hNegX) { + dzdx = (*centerH - *hNegX) / SAMPLE_DIST; + } + + if (hPosY && hNegY) { + dzdy = (*hPosY - *hNegY) / (2.0f * SAMPLE_DIST); + } else if (hPosY) { + dzdy = (*hPosY - *centerH) / SAMPLE_DIST; + } else if (hNegY) { + dzdy = (*centerH - *hNegY) / 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 { std::optional terrainH; @@ -201,15 +284,25 @@ void CameraController::update(float deltaTime) { } if (groundH) { - // Smooth ground height to prevent stumbling on uneven terrain float groundDiff = *groundH - lastGroundZ; - if (std::abs(groundDiff) < 2.0f) { - // Small height difference - smooth it - lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f); - } else { - // Large height difference (stairs, ledges) - snap - lastGroundZ = *groundH; + float currentFeetZ = targetPos.z; + + // Only consider floors that are: + // 1. Below us (we can fall onto them) + // 2. Slightly above us (we can step up onto them, max 1 unit) + // Don't teleport to roofs/floors that are way above us + bool floorIsReachable = (*groundH <= currentFeetZ + 1.0f); + + if (floorIsReachable) { + if (std::abs(groundDiff) < 2.0f) { + // Small height difference - smooth it + lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f); + } else { + // Large height difference - snap (for falling onto ledges) + lastGroundZ = *groundH; + } } + // If floor is way above us (roof), ignore it and keep lastGroundZ if (targetPos.z <= lastGroundZ + 0.1f) { targetPos.z = lastGroundZ; @@ -230,50 +323,106 @@ void CameraController::update(float deltaTime) { // Update follow target position *followTarget = targetPos; - // Compute camera position orbiting behind the character - glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight); + // ===== WoW-style orbit camera ===== + // Pivot point at upper chest/neck + glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT); - // Camera collision detection - raycast from character head to desired camera position - glm::vec3 rayDir = -forward3D; // Direction from character toward camera - float desiredDist = orbitDistance; - float actualDist = desiredDist; - const float cameraOffset = 0.3f; // Small offset to not clip into walls + // Camera direction from yaw/pitch (already computed as forward3D) + glm::vec3 camDir = -forward3D; // Camera looks at pivot, so it's behind - // Raycast against WMO bounding boxes - if (wmoRenderer) { - float wmoHit = wmoRenderer->raycastBoundingBoxes(lookAtPoint, rayDir, desiredDist); - if (wmoHit < actualDist) { - actualDist = std::max(minOrbitDistance, wmoHit - cameraOffset); - } - } + // Smooth zoom toward user target + float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime); + currentDistance += (userTargetDistance - currentDistance) * zoomLerp; - // Raycast against M2 bounding boxes (larger objects only affect camera) - if (m2Renderer) { - float m2Hit = m2Renderer->raycastBoundingBoxes(lookAtPoint, rayDir, desiredDist); - if (m2Hit < actualDist) { - actualDist = std::max(minOrbitDistance, m2Hit - cameraOffset); - } - } + // Desired camera position (before collision) + glm::vec3 desiredCam = pivot + camDir * currentDistance; - glm::vec3 camPos = lookAtPoint + rayDir * actualDist; + // ===== Camera collision (sphere sweep approximation) ===== + // Find max safe distance using raycast + sphere radius + collisionDistance = currentDistance; - // Clamp camera above terrain/WMO floor - { - float minCamZ = camPos.z; + // Helper to get floor height + auto getFloorAt = [&](float x, float y, float z) -> std::optional { + std::optional h; if (terrainManager) { - auto h = terrainManager->getHeightAt(camPos.x, camPos.y); - if (h) minCamZ = *h + 1.0f; // 1 unit above ground + h = terrainManager->getHeightAt(x, y); } if (wmoRenderer) { - auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight); - if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f; + auto wh = wmoRenderer->getFloorHeight(x, y, z + 5.0f); + if (wh && (!h || *wh > *h)) { + h = wh; + } } - if (camPos.z < minCamZ) { - camPos.z = minCamZ; + return h; + }; + + // Raycast against WMO bounding boxes + if (wmoRenderer && collisionDistance > MIN_DISTANCE) { + float wmoHit = wmoRenderer->raycastBoundingBoxes(pivot, camDir, collisionDistance); + if (wmoHit < collisionDistance) { + collisionDistance = std::max(MIN_DISTANCE, wmoHit - CAM_SPHERE_RADIUS - CAM_EPSILON); } } - camera->setPosition(camPos); + // Raycast against M2 bounding boxes + if (m2Renderer && collisionDistance > MIN_DISTANCE) { + float m2Hit = m2Renderer->raycastBoundingBoxes(pivot, camDir, collisionDistance); + if (m2Hit < collisionDistance) { + collisionDistance = std::max(MIN_DISTANCE, m2Hit - CAM_SPHERE_RADIUS - CAM_EPSILON); + } + } + + // Check floor collision along the camera path + // Sample a few points to find where camera would go underground + for (int i = 1; i <= 4; i++) { + float testDist = collisionDistance * (float(i) / 4.0f); + glm::vec3 testPos = pivot + camDir * testDist; + 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; + } + } + + // Use collision distance (don't exceed user target) + float actualDist = std::min(currentDistance, collisionDistance); + + // Compute actual camera position + glm::vec3 actualCam; + if (actualDist < MIN_DISTANCE + 0.1f) { + // First-person: position camera at pivot (player's eyes) + actualCam = pivot + forward3D * 0.1f; // Slightly forward to not clip head + } else { + actualCam = pivot + camDir * actualDist; + } + + // Smooth camera position to avoid jitter + if (glm::length(smoothedCamPos) < 0.01f) { + smoothedCamPos = actualCam; // Initialize + } + float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); + smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; + + // ===== Final floor clearance check ===== + // After smoothing, ensure camera is above the floor at its final position + // This prevents camera clipping through ground in Stormwind and similar areas + constexpr float MIN_FLOOR_CLEARANCE = 0.20f; // Keep camera at least 20cm above floor + auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z + 5.0f); + if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) { + smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE; + } + + camera->setPosition(smoothedCamPos); + + // Hide player model when in first-person (camera too close) + // WoW fades between ~1.0m and ~0.5m, hides fully below 0.5m + // For now, just hide below first-person threshold + if (characterRenderer && playerInstanceId > 0) { + bool shouldHidePlayer = (actualDist < MIN_DISTANCE + 0.1f); // Hide in first-person + characterRenderer->setInstanceVisible(playerInstanceId, !shouldHidePlayer); + } } else { // Free-fly camera mode (original behavior) glm::vec3 newPos = camera->getPosition(); @@ -464,7 +613,8 @@ void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { yaw -= event.xrel * mouseSensitivity; pitch += event.yrel * mouseSensitivity; - pitch = glm::clamp(pitch, -89.0f, 89.0f); + // WoW-style pitch limits: can look almost straight down, limited upward + pitch = glm::clamp(pitch, MIN_PITCH, MAX_PITCH); camera->setRotation(yaw, pitch); } @@ -525,8 +675,9 @@ void CameraController::reset() { } void CameraController::processMouseWheel(float delta) { - orbitDistance -= delta * zoomSpeed; - orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance); + // Adjust user's target distance (collision may limit actual distance) + userTargetDistance -= delta * 2.0f; // 2.0 units per scroll notch + userTargetDistance = glm::clamp(userTargetDistance, MIN_DISTANCE, MAX_DISTANCE); } void CameraController::setFollowTarget(glm::vec3* target) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 6fe8295b..94a14239 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -978,6 +978,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons for (const auto& pair : instances) { const auto& instance = pair.second; + + // Skip invisible instances (e.g., player in first-person mode) + if (!instance.visible) continue; + const auto& gpuModel = models[instance.modelId]; // Set model matrix (use override for weapon instances) @@ -1118,6 +1122,21 @@ void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unorder } } +void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) { + auto it = instances.find(instanceId); + if (it != instances.end()) { + it->second.visible = visible; + + // Also hide/show attached weapons (for first-person mode) + for (const auto& wa : it->second.weaponAttachments) { + auto weapIt = instances.find(wa.weaponInstanceId); + if (weapIt != instances.end()) { + weapIt->second.visible = visible; + } + } + } +} + void CharacterRenderer::removeInstance(uint32_t instanceId) { instances.erase(instanceId); } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index f9b74842..61039c3b 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -364,7 +364,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; // Distance-based culling threshold for M2 models - const float maxRenderDistance = 300.0f; // Reduced for performance + const float maxRenderDistance = 400.0f; // Balance between performance and visibility const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const glm::vec3 camPos = camera.getPosition(); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 3fb1e44f..5cd8a7f0 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -725,6 +725,12 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ glm::vec3 worldOrigin(glX, glY, glZ + 500.0f); glm::vec3 worldDir(0.0f, 0.0f, -1.0f); + // Debug: log when no instances + static int debugCounter = 0; + if (instances.empty() && (debugCounter++ % 300 == 0)) { + core::Logger::getInstance().warning("WMO getFloorHeight: no instances loaded!"); + } + for (const auto& instance : instances) { auto it = loadedModels.find(instance.modelId); if (it == loadedModels.end()) continue; @@ -735,12 +741,17 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f)); glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f))); + int groupsChecked = 0; + int groupsSkipped = 0; + int trianglesHit = 0; + for (const auto& group : model.groups) { - // Quick bounding box check: does the ray intersect this group's AABB? - // Use proper ray-AABB intersection (slab method) which handles rotated rays + // Quick bounding box check if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) { + groupsSkipped++; continue; } + groupsChecked++; // Raycast against triangles const auto& verts = group.collisionVertices; @@ -751,8 +762,15 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ const glm::vec3& v1 = verts[indices[i + 1]]; const glm::vec3& v2 = verts[indices[i + 2]]; + // Try both winding orders (two-sided collision) float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2); + if (t <= 0.0f) { + // Try reverse winding + t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1); + } + if (t > 0.0f) { + trianglesHit++; // Hit point in local space -> world space glm::vec3 hitLocal = localOrigin + localDir * t; glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f)); @@ -766,6 +784,14 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ } } } + + // Debug logging (every ~5 seconds at 60fps) + static int logCounter = 0; + if ((logCounter++ % 300 == 0) && (groupsChecked > 0 || groupsSkipped > 0)) { + core::Logger::getInstance().debug("Floor check: ", groupsChecked, " groups checked, ", + groupsSkipped, " skipped, ", trianglesHit, " hits, best=", + bestFloor ? std::to_string(*bestFloor) : "none"); + } } return bestFloor; @@ -779,8 +805,14 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y)); if (moveDistXY < 0.001f) return false; - // Player collision radius (WoW character is about 0.5 yards wide) - const float PLAYER_RADIUS = 0.5f; + // Player collision parameters + const float PLAYER_RADIUS = 0.6f; // Character collision radius + const float PLAYER_HEIGHT = 2.0f; // Player height for wall checks + + // Debug logging + static int wallDebugCounter = 0; + int groupsChecked = 0; + int wallsHit = 0; for (const auto& instance : instances) { auto it = loadedModels.find(instance.modelId); @@ -790,15 +822,17 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Transform positions into local space using cached inverse glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f)); + float localFeetZ = localTo.z; for (const auto& group : model.groups) { // Quick bounding box check - float margin = PLAYER_RADIUS + 5.0f; + float margin = PLAYER_RADIUS + 2.0f; if (localTo.x < group.boundingBoxMin.x - margin || localTo.x > group.boundingBoxMax.x + margin || localTo.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin || localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) { continue; } + groupsChecked++; const auto& verts = group.collisionVertices; const auto& indices = group.collisionIndices; @@ -817,7 +851,16 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, normal /= normalLen; // Skip mostly-horizontal triangles (floors/ceilings) - if (std::abs(normal.z) > 0.7f) continue; + // Only collide with walls (vertical surfaces) + if (std::abs(normal.z) > 0.5f) 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; // Signed distance from player to triangle plane float planeDist = glm::dot(localTo - v0, normal); @@ -827,27 +870,27 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, // Project point onto plane glm::vec3 projected = localTo - normal * planeDist; - // Check if projected point is inside triangle using same-side test - // Use edge cross products and check they all point same direction as normal + // Check if projected point is inside triangle (or near edge) float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal); float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal); float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal); - // Also check nearby: if projected point is close to a triangle edge - bool insideTriangle = (d0 >= 0.0f && d1 >= 0.0f && d2 >= 0.0f); + // Allow small negative values for edge tolerance + const float edgeTolerance = -0.1f; + bool insideTriangle = (d0 >= edgeTolerance && d1 >= edgeTolerance && d2 >= edgeTolerance); if (insideTriangle) { - // Push player away from wall + wallsHit++; + // Push player away from wall (horizontal only) float pushDist = PLAYER_RADIUS - absPlaneDist; if (pushDist > 0.0f) { - // Push in the direction the player is on (sign of planeDist) float sign = planeDist > 0.0f ? 1.0f : -1.0f; glm::vec3 pushLocal = normal * sign * pushDist; - // Transform push vector back to world space (direction, not point) + // Transform push vector back to world space glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f)); - // Only apply horizontal push (don't push vertically) + // Only horizontal push adjustedPos.x += pushWorld.x; adjustedPos.y += pushWorld.y; blocked = true; @@ -857,6 +900,12 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, } } + // Debug logging every ~5 seconds + if ((wallDebugCounter++ % 300 == 0) && !instances.empty()) { + core::Logger::getInstance().debug("Wall collision: ", instances.size(), " instances, ", + groupsChecked, " groups checked, ", wallsHit, " walls hit, blocked=", blocked); + } + return blocked; }