mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 15:50:20 +00:00
Add WoW-style camera system with collision and first-person mode
- Implement orbit camera with smooth zoom and collision detection - Add 50° slope limiting with sliding (prevents mountain climbing) - Add first-person mode that hides player model and weapons - Add floor clearance check to prevent camera clipping through ground - Improve WMO wall collision with proper height range checks - Add two-sided floor collision detection for WMO geometry - Increase M2 render distance slightly for better visibility
This commit is contained in:
parent
3e792af3e5
commit
54dc27c2ec
7 changed files with 307 additions and 64 deletions
|
|
@ -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 <glm/glm.hpp>
|
||||
|
|
@ -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<float> {
|
||||
std::optional<float> 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<float> 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<float> {
|
||||
std::optional<float> 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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue