diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index a71fe7cf..0946d4a7 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -126,9 +126,9 @@ private: static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_JUMP_VELOCITY = 7.96f; - // Default spawn position (in front of Stormwind gate) - glm::vec3 defaultPosition = glm::vec3(-8900.0f, -170.0f, 150.0f); - float defaultYaw = 0.0f; // Look north toward Stormwind gate + // Default spawn position (on the road outside Stormwind) + glm::vec3 defaultPosition = glm::vec3(-8830.0f, -150.0f, 82.0f); + float defaultYaw = 180.0f; // Look south toward Stormwind gate float defaultPitch = -5.0f; }; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 66f36ebf..a7f9acab 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -144,6 +144,15 @@ public: bool checkCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos, float playerRadius = 0.5f) const; + /** + * Raycast against M2 bounding boxes for camera collision + * @param origin Ray origin (e.g., character head position) + * @param direction Ray direction (normalized) + * @param maxDistance Maximum ray distance to check + * @return Distance to first intersection, or maxDistance if no hit + */ + float raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const; + // Stats uint32_t getModelCount() const { return static_cast(models.size()); } uint32_t getInstanceCount() const { return static_cast(instances.size()); } diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 24984f94..c48bf4ce 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -240,8 +240,8 @@ private: // Streaming parameters bool streamingEnabled = true; - int loadRadius = 4; // Load tiles within this radius (9x9 grid, ~2133 units) - int unloadRadius = 6; // Unload tiles beyond this radius (~3200 units, past far clip) + int loadRadius = 6; // Load tiles within this radius (13x13 grid, ~3200 units) + int unloadRadius = 8; // Unload tiles beyond this radius (~4266 units) float updateInterval = 0.1f; // Check streaming every 0.1 seconds float timeSinceLastUpdate = 0.0f; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 00883195..52d2f983 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -149,6 +149,15 @@ public: */ bool isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId = nullptr) const; + /** + * Raycast against WMO bounding boxes for camera collision + * @param origin Ray origin (e.g., character head position) + * @param direction Ray direction (normalized) + * @param maxDistance Maximum ray distance to check + * @return Distance to first intersection, or maxDistance if no hit + */ + float raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const; + private: /** * WMO group GPU resources diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 51e4891a..fe92b935 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -201,9 +201,18 @@ void CameraController::update(float deltaTime) { } if (groundH) { - lastGroundZ = *groundH; - if (targetPos.z <= *groundH) { - targetPos.z = *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; + } + + if (targetPos.z <= lastGroundZ + 0.1f) { + targetPos.z = lastGroundZ; verticalVelocity = 0.0f; grounded = true; swimming = false; // Touching ground = wading, not swimming @@ -223,7 +232,30 @@ void CameraController::update(float deltaTime) { // Compute camera position orbiting behind the character glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight); - glm::vec3 camPos = lookAtPoint - forward3D * orbitDistance; + + // 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 + + // Raycast against WMO bounding boxes + if (wmoRenderer) { + float wmoHit = wmoRenderer->raycastBoundingBoxes(lookAtPoint, rayDir, desiredDist); + if (wmoHit < actualDist) { + actualDist = std::max(minOrbitDistance, wmoHit - cameraOffset); + } + } + + // 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); + } + } + + glm::vec3 camPos = lookAtPoint + rayDir * actualDist; // Clamp camera above terrain/WMO floor { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 7b5eaa5c..b619ef3e 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 = 500.0f; // Don't render small doodads beyond this + const float maxRenderDistance = 1000.0f; // Don't render small doodads beyond this const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const glm::vec3 camPos = camera.getPosition(); @@ -392,7 +392,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: shader->setUniform("uModel", instance.modelMatrix); shader->setUniform("uTime", instance.animTime); - shader->setUniform("uAnimScale", 1.0f); // Enable animation for all M2s + shader->setUniform("uAnimScale", 0.0f); // Disabled - proper M2 animation needs bone/particle systems glBindVertexArray(model.vao); @@ -587,5 +587,46 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, return collided; } +float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const { + float closestHit = maxDistance; + + for (const auto& instance : instances) { + auto it = models.find(instance.modelId); + if (it == models.end()) continue; + + const M2ModelGPU& model = it->second; + + // Transform model bounds to world space (approximate with scaled AABB) + glm::vec3 worldMin = instance.position + model.boundMin * instance.scale; + glm::vec3 worldMax = instance.position + model.boundMax * instance.scale; + + // Ensure min/max are correct + glm::vec3 actualMin = glm::min(worldMin, worldMax); + glm::vec3 actualMax = glm::max(worldMin, worldMax); + + // Ray-AABB intersection (slab method) + glm::vec3 invDir = 1.0f / direction; + glm::vec3 tMin = (actualMin - origin) * invDir; + glm::vec3 tMax = (actualMax - origin) * invDir; + + // Handle negative direction components + glm::vec3 t1 = glm::min(tMin, tMax); + glm::vec3 t2 = glm::max(tMin, tMax); + + float tNear = std::max({t1.x, t1.y, t1.z}); + float tFar = std::min({t2.x, t2.y, t2.z}); + + // Check if ray intersects the box + if (tNear <= tFar && tFar > 0.0f) { + float hitDist = tNear > 0.0f ? tNear : tFar; + if (hitDist > 0.0f && hitDist < closestHit) { + closestHit = hitDist; + } + } + } + + return closestHit; +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 60d7de4c..4f63122d 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -354,7 +354,7 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: // Render all instances with instance-level culling const glm::vec3 camPos = camera.getPosition(); - const float maxRenderDistance = 1500.0f; // Don't render WMOs beyond this distance + const float maxRenderDistance = 3000.0f; // Don't render WMOs beyond this distance const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; for (const auto& instance : instances) { @@ -883,5 +883,44 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode return false; } +float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const { + float closestHit = maxDistance; + + for (const auto& instance : instances) { + auto it = loadedModels.find(instance.modelId); + if (it == loadedModels.end()) continue; + + const ModelData& model = it->second; + + // Transform ray into local space + glm::mat4 invModel = glm::inverse(instance.modelMatrix); + glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(origin, 1.0f)); + glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(direction, 0.0f))); + + for (const auto& group : model.groups) { + // Ray-AABB intersection (slab method) + glm::vec3 tMin = (group.boundingBoxMin - localOrigin) / localDir; + glm::vec3 tMax = (group.boundingBoxMax - localOrigin) / localDir; + + // Handle negative direction components + glm::vec3 t1 = glm::min(tMin, tMax); + glm::vec3 t2 = glm::max(tMin, tMax); + + float tNear = std::max({t1.x, t1.y, t1.z}); + float tFar = std::min({t2.x, t2.y, t2.z}); + + // Check if ray intersects the box + if (tNear <= tFar && tFar > 0.0f) { + float hitDist = tNear > 0.0f ? tNear : tFar; + if (hitDist > 0.0f && hitDist < closestHit) { + closestHit = hitDist; + } + } + } + } + + return closestHit; +} + } // namespace rendering } // namespace wowee