mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 00:03:50 +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
|
|
@ -52,12 +52,20 @@ public:
|
||||||
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
||||||
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
||||||
|
|
||||||
|
// For first-person player hiding
|
||||||
|
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||||
|
characterRenderer = cr;
|
||||||
|
playerInstanceId = playerId;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Camera* camera;
|
Camera* camera;
|
||||||
TerrainManager* terrainManager = nullptr;
|
TerrainManager* terrainManager = nullptr;
|
||||||
WMORenderer* wmoRenderer = nullptr;
|
WMORenderer* wmoRenderer = nullptr;
|
||||||
M2Renderer* m2Renderer = nullptr;
|
M2Renderer* m2Renderer = nullptr;
|
||||||
WaterRenderer* waterRenderer = nullptr;
|
WaterRenderer* waterRenderer = nullptr;
|
||||||
|
CharacterRenderer* characterRenderer = nullptr;
|
||||||
|
uint32_t playerInstanceId = 0;
|
||||||
|
|
||||||
// Stored rotation (avoids lossy forward-vector round-trip)
|
// Stored rotation (avoids lossy forward-vector round-trip)
|
||||||
float yaw = 180.0f;
|
float yaw = 180.0f;
|
||||||
|
|
@ -74,13 +82,22 @@ private:
|
||||||
bool leftMouseDown = false;
|
bool leftMouseDown = false;
|
||||||
bool rightMouseDown = false;
|
bool rightMouseDown = false;
|
||||||
|
|
||||||
// Third-person orbit camera
|
// Third-person orbit camera (WoW-style)
|
||||||
bool thirdPerson = false;
|
bool thirdPerson = false;
|
||||||
float orbitDistance = 15.0f;
|
float userTargetDistance = 10.0f; // What the player wants (scroll wheel)
|
||||||
float minOrbitDistance = 3.0f;
|
float currentDistance = 10.0f; // Smoothed actual distance
|
||||||
float maxOrbitDistance = 50.0f;
|
float collisionDistance = 10.0f; // Max allowed by collision
|
||||||
float zoomSpeed = 2.0f;
|
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* followTarget = nullptr;
|
||||||
|
glm::vec3 smoothedCamPos = glm::vec3(0.0f); // For smooth camera movement
|
||||||
|
|
||||||
// Gravity / grounding
|
// Gravity / grounding
|
||||||
float verticalVelocity = 0.0f;
|
float verticalVelocity = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ public:
|
||||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||||
void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation);
|
void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation);
|
||||||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||||
|
void setInstanceVisible(uint32_t instanceId, bool visible);
|
||||||
void removeInstance(uint32_t instanceId);
|
void removeInstance(uint32_t instanceId);
|
||||||
|
|
||||||
/** Attach a weapon model to a character instance at the given attachment point. */
|
/** Attach a weapon model to a character instance at the given attachment point. */
|
||||||
|
|
@ -95,6 +96,7 @@ private:
|
||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
glm::vec3 rotation;
|
glm::vec3 rotation;
|
||||||
float scale;
|
float scale;
|
||||||
|
bool visible = true; // For first-person camera hiding
|
||||||
|
|
||||||
// Animation state
|
// Animation state
|
||||||
uint32_t currentAnimationId = 0;
|
uint32_t currentAnimationId = 0;
|
||||||
|
|
|
||||||
|
|
@ -939,7 +939,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
// Spawn character at camera's ground position
|
// Spawn character at camera's ground position
|
||||||
glm::vec3 spawnPos = camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f);
|
glm::vec3 spawnPos = camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f);
|
||||||
uint32_t instanceId = charRenderer->createInstance(1, spawnPos,
|
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) {
|
if (instanceId > 0) {
|
||||||
// Set up third-person follow
|
// Set up third-person follow
|
||||||
|
|
@ -974,6 +974,11 @@ void Application::spawnPlayerCharacter() {
|
||||||
static_cast<int>(spawnPos.z), ")");
|
static_cast<int>(spawnPos.z), ")");
|
||||||
playerCharacterSpawned = true;
|
playerCharacterSpawned = true;
|
||||||
|
|
||||||
|
// Set up camera controller for first-person player hiding
|
||||||
|
if (renderer->getCameraController()) {
|
||||||
|
renderer->getCameraController()->setCharacterRenderer(charRenderer, instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
// Load equipped weapons (sword + shield)
|
// Load equipped weapons (sword + shield)
|
||||||
loadEquippedWeapons();
|
loadEquippedWeapons();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "rendering/wmo_renderer.hpp"
|
#include "rendering/wmo_renderer.hpp"
|
||||||
#include "rendering/m2_renderer.hpp"
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/water_renderer.hpp"
|
#include "rendering/water_renderer.hpp"
|
||||||
|
#include "rendering/character_renderer.hpp"
|
||||||
#include "game/opcodes.hpp"
|
#include "game/opcodes.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
@ -164,9 +165,9 @@ void CameraController::update(float deltaTime) {
|
||||||
glm::vec3 oldFeetPos = *followTarget;
|
glm::vec3 oldFeetPos = *followTarget;
|
||||||
glm::vec3 adjusted;
|
glm::vec3 adjusted;
|
||||||
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
|
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
|
||||||
|
// Only apply horizontal adjustment (don't let wall collision change Z)
|
||||||
targetPos.x = adjusted.x;
|
targetPos.x = adjusted.x;
|
||||||
targetPos.y = adjusted.y;
|
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
|
// Ground the character to terrain or WMO floor
|
||||||
{
|
{
|
||||||
std::optional<float> terrainH;
|
std::optional<float> terrainH;
|
||||||
|
|
@ -201,15 +284,25 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groundH) {
|
if (groundH) {
|
||||||
// Smooth ground height to prevent stumbling on uneven terrain
|
|
||||||
float groundDiff = *groundH - lastGroundZ;
|
float groundDiff = *groundH - lastGroundZ;
|
||||||
if (std::abs(groundDiff) < 2.0f) {
|
float currentFeetZ = targetPos.z;
|
||||||
// Small height difference - smooth it
|
|
||||||
lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f);
|
// Only consider floors that are:
|
||||||
} else {
|
// 1. Below us (we can fall onto them)
|
||||||
// Large height difference (stairs, ledges) - snap
|
// 2. Slightly above us (we can step up onto them, max 1 unit)
|
||||||
lastGroundZ = *groundH;
|
// 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) {
|
if (targetPos.z <= lastGroundZ + 0.1f) {
|
||||||
targetPos.z = lastGroundZ;
|
targetPos.z = lastGroundZ;
|
||||||
|
|
@ -230,50 +323,106 @@ void CameraController::update(float deltaTime) {
|
||||||
// Update follow target position
|
// Update follow target position
|
||||||
*followTarget = targetPos;
|
*followTarget = targetPos;
|
||||||
|
|
||||||
// Compute camera position orbiting behind the character
|
// ===== WoW-style orbit camera =====
|
||||||
glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight);
|
// 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
|
// Camera direction from yaw/pitch (already computed as forward3D)
|
||||||
glm::vec3 rayDir = -forward3D; // Direction from character toward camera
|
glm::vec3 camDir = -forward3D; // Camera looks at pivot, so it's behind
|
||||||
float desiredDist = orbitDistance;
|
|
||||||
float actualDist = desiredDist;
|
|
||||||
const float cameraOffset = 0.3f; // Small offset to not clip into walls
|
|
||||||
|
|
||||||
// Raycast against WMO bounding boxes
|
// Smooth zoom toward user target
|
||||||
if (wmoRenderer) {
|
float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime);
|
||||||
float wmoHit = wmoRenderer->raycastBoundingBoxes(lookAtPoint, rayDir, desiredDist);
|
currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
|
||||||
if (wmoHit < actualDist) {
|
|
||||||
actualDist = std::max(minOrbitDistance, wmoHit - cameraOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raycast against M2 bounding boxes (larger objects only affect camera)
|
// Desired camera position (before collision)
|
||||||
if (m2Renderer) {
|
glm::vec3 desiredCam = pivot + camDir * currentDistance;
|
||||||
float m2Hit = m2Renderer->raycastBoundingBoxes(lookAtPoint, rayDir, desiredDist);
|
|
||||||
if (m2Hit < actualDist) {
|
|
||||||
actualDist = std::max(minOrbitDistance, m2Hit - cameraOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// Helper to get floor height
|
||||||
{
|
auto getFloorAt = [&](float x, float y, float z) -> std::optional<float> {
|
||||||
float minCamZ = camPos.z;
|
std::optional<float> h;
|
||||||
if (terrainManager) {
|
if (terrainManager) {
|
||||||
auto h = terrainManager->getHeightAt(camPos.x, camPos.y);
|
h = terrainManager->getHeightAt(x, y);
|
||||||
if (h) minCamZ = *h + 1.0f; // 1 unit above ground
|
|
||||||
}
|
}
|
||||||
if (wmoRenderer) {
|
if (wmoRenderer) {
|
||||||
auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight);
|
auto wh = wmoRenderer->getFloorHeight(x, y, z + 5.0f);
|
||||||
if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f;
|
if (wh && (!h || *wh > *h)) {
|
||||||
|
h = wh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (camPos.z < minCamZ) {
|
return h;
|
||||||
camPos.z = minCamZ;
|
};
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
// Free-fly camera mode (original behavior)
|
// Free-fly camera mode (original behavior)
|
||||||
glm::vec3 newPos = camera->getPosition();
|
glm::vec3 newPos = camera->getPosition();
|
||||||
|
|
@ -464,7 +613,8 @@ void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||||
yaw -= event.xrel * mouseSensitivity;
|
yaw -= event.xrel * mouseSensitivity;
|
||||||
pitch += event.yrel * 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);
|
camera->setRotation(yaw, pitch);
|
||||||
}
|
}
|
||||||
|
|
@ -525,8 +675,9 @@ void CameraController::reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CameraController::processMouseWheel(float delta) {
|
void CameraController::processMouseWheel(float delta) {
|
||||||
orbitDistance -= delta * zoomSpeed;
|
// Adjust user's target distance (collision may limit actual distance)
|
||||||
orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance);
|
userTargetDistance -= delta * 2.0f; // 2.0 units per scroll notch
|
||||||
|
userTargetDistance = glm::clamp(userTargetDistance, MIN_DISTANCE, MAX_DISTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CameraController::setFollowTarget(glm::vec3* target) {
|
void CameraController::setFollowTarget(glm::vec3* target) {
|
||||||
|
|
|
||||||
|
|
@ -978,6 +978,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
|
|
||||||
for (const auto& pair : instances) {
|
for (const auto& pair : instances) {
|
||||||
const auto& instance = pair.second;
|
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];
|
const auto& gpuModel = models[instance.modelId];
|
||||||
|
|
||||||
// Set model matrix (use override for weapon instances)
|
// 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) {
|
void CharacterRenderer::removeInstance(uint32_t instanceId) {
|
||||||
instances.erase(instanceId);
|
instances.erase(instanceId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
lastDrawCallCount = 0;
|
lastDrawCallCount = 0;
|
||||||
|
|
||||||
// Distance-based culling threshold for M2 models
|
// 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 float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||||
const glm::vec3 camPos = camera.getPosition();
|
const glm::vec3 camPos = camera.getPosition();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -725,6 +725,12 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
||||||
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
||||||
glm::vec3 worldDir(0.0f, 0.0f, -1.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) {
|
for (const auto& instance : instances) {
|
||||||
auto it = loadedModels.find(instance.modelId);
|
auto it = loadedModels.find(instance.modelId);
|
||||||
if (it == loadedModels.end()) continue;
|
if (it == loadedModels.end()) continue;
|
||||||
|
|
@ -735,12 +741,17 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
||||||
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
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)));
|
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) {
|
for (const auto& group : model.groups) {
|
||||||
// Quick bounding box check: does the ray intersect this group's AABB?
|
// Quick bounding box check
|
||||||
// Use proper ray-AABB intersection (slab method) which handles rotated rays
|
|
||||||
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
||||||
|
groupsSkipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
groupsChecked++;
|
||||||
|
|
||||||
// Raycast against triangles
|
// Raycast against triangles
|
||||||
const auto& verts = group.collisionVertices;
|
const auto& verts = group.collisionVertices;
|
||||||
|
|
@ -751,8 +762,15 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
||||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||||
|
|
||||||
|
// Try both winding orders (two-sided collision)
|
||||||
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
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) {
|
if (t > 0.0f) {
|
||||||
|
trianglesHit++;
|
||||||
// Hit point in local space -> world space
|
// Hit point in local space -> world space
|
||||||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||||
|
|
@ -766,6 +784,14 @@ std::optional<float> 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;
|
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));
|
float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y));
|
||||||
if (moveDistXY < 0.001f) return false;
|
if (moveDistXY < 0.001f) return false;
|
||||||
|
|
||||||
// Player collision radius (WoW character is about 0.5 yards wide)
|
// Player collision parameters
|
||||||
const float PLAYER_RADIUS = 0.5f;
|
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) {
|
for (const auto& instance : instances) {
|
||||||
auto it = loadedModels.find(instance.modelId);
|
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
|
// Transform positions into local space using cached inverse
|
||||||
glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f));
|
glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f));
|
||||||
|
float localFeetZ = localTo.z;
|
||||||
|
|
||||||
for (const auto& group : model.groups) {
|
for (const auto& group : model.groups) {
|
||||||
// Quick bounding box check
|
// 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 ||
|
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.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin ||
|
||||||
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
|
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
groupsChecked++;
|
||||||
|
|
||||||
const auto& verts = group.collisionVertices;
|
const auto& verts = group.collisionVertices;
|
||||||
const auto& indices = group.collisionIndices;
|
const auto& indices = group.collisionIndices;
|
||||||
|
|
@ -817,7 +851,16 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
||||||
normal /= normalLen;
|
normal /= normalLen;
|
||||||
|
|
||||||
// Skip mostly-horizontal triangles (floors/ceilings)
|
// 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
|
// Signed distance from player to triangle plane
|
||||||
float planeDist = glm::dot(localTo - v0, normal);
|
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
|
// Project point onto plane
|
||||||
glm::vec3 projected = localTo - normal * planeDist;
|
glm::vec3 projected = localTo - normal * planeDist;
|
||||||
|
|
||||||
// Check if projected point is inside triangle using same-side test
|
// Check if projected point is inside triangle (or near edge)
|
||||||
// Use edge cross products and check they all point same direction as normal
|
|
||||||
float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal);
|
float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal);
|
||||||
float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal);
|
float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal);
|
||||||
float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal);
|
float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal);
|
||||||
|
|
||||||
// Also check nearby: if projected point is close to a triangle edge
|
// Allow small negative values for edge tolerance
|
||||||
bool insideTriangle = (d0 >= 0.0f && d1 >= 0.0f && d2 >= 0.0f);
|
const float edgeTolerance = -0.1f;
|
||||||
|
bool insideTriangle = (d0 >= edgeTolerance && d1 >= edgeTolerance && d2 >= edgeTolerance);
|
||||||
|
|
||||||
if (insideTriangle) {
|
if (insideTriangle) {
|
||||||
// Push player away from wall
|
wallsHit++;
|
||||||
|
// Push player away from wall (horizontal only)
|
||||||
float pushDist = PLAYER_RADIUS - absPlaneDist;
|
float pushDist = PLAYER_RADIUS - absPlaneDist;
|
||||||
if (pushDist > 0.0f) {
|
if (pushDist > 0.0f) {
|
||||||
// Push in the direction the player is on (sign of planeDist)
|
|
||||||
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
|
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
|
||||||
glm::vec3 pushLocal = normal * sign * pushDist;
|
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));
|
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.x += pushWorld.x;
|
||||||
adjustedPos.y += pushWorld.y;
|
adjustedPos.y += pushWorld.y;
|
||||||
blocked = true;
|
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;
|
return blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue