mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Tune collision feel and align M2 movement/camera behavior
This commit is contained in:
parent
b372c499a4
commit
d03ff5ee4c
4 changed files with 308 additions and 96 deletions
|
|
@ -7,6 +7,7 @@
|
|||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
|
|
@ -144,6 +145,14 @@ public:
|
|||
bool checkCollision(const glm::vec3& from, const glm::vec3& to,
|
||||
glm::vec3& adjustedPos, float playerRadius = 0.5f) const;
|
||||
|
||||
/**
|
||||
* Approximate top surface height for standing/jumping on doodads.
|
||||
* @param glX World X
|
||||
* @param glY World Y
|
||||
* @param glZ Query/reference Z (used to ignore unreachable tops)
|
||||
*/
|
||||
std::optional<float> getFloorHeight(float glX, float glY, float glZ) const;
|
||||
|
||||
/**
|
||||
* Raycast against M2 bounding boxes for camera collision
|
||||
* @param origin Ray origin (e.g., character head position)
|
||||
|
|
|
|||
|
|
@ -165,6 +165,41 @@ void CameraController::update(float deltaTime) {
|
|||
if (thirdPerson && followTarget) {
|
||||
// Move the follow target (character position) instead of the camera
|
||||
glm::vec3 targetPos = *followTarget;
|
||||
auto clampMoveAgainstWMO = [&](const glm::vec3& fromFeet, glm::vec3& toFeet) {
|
||||
if (!wmoRenderer) return;
|
||||
glm::vec3 move = toFeet - fromFeet;
|
||||
move.z = 0.0f;
|
||||
float moveDist = glm::length(glm::vec2(move));
|
||||
if (moveDist < 0.001f) return;
|
||||
|
||||
glm::vec3 dir = glm::normalize(move);
|
||||
glm::vec3 perp(-dir.y, dir.x, 0.0f);
|
||||
constexpr float BODY_RADIUS = 0.38f;
|
||||
constexpr float SAFETY = 0.08f;
|
||||
constexpr float HEIGHTS[3] = {0.4f, 1.0f, 1.6f};
|
||||
const glm::vec3 probes[3] = {
|
||||
fromFeet,
|
||||
fromFeet + perp * BODY_RADIUS,
|
||||
fromFeet - perp * BODY_RADIUS
|
||||
};
|
||||
|
||||
float bestHit = moveDist + 1.0f;
|
||||
for (const glm::vec3& probeBase : probes) {
|
||||
for (float h : HEIGHTS) {
|
||||
glm::vec3 origin = probeBase + glm::vec3(0.0f, 0.0f, h);
|
||||
float hit = wmoRenderer->raycastBoundingBoxes(origin, dir, moveDist + SAFETY);
|
||||
if (hit < bestHit) {
|
||||
bestHit = hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestHit < moveDist + SAFETY) {
|
||||
float allowed = std::max(0.0f, bestHit - SAFETY);
|
||||
toFeet.x = fromFeet.x + dir.x * allowed;
|
||||
toFeet.y = fromFeet.y + dir.y * allowed;
|
||||
}
|
||||
};
|
||||
|
||||
// Check for water at current position
|
||||
std::optional<float> waterH;
|
||||
|
|
@ -234,6 +269,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
for (int i = 0; i < sweepSteps; i++) {
|
||||
glm::vec3 candidate = stepPos + stepDelta;
|
||||
clampMoveAgainstWMO(stepPos, candidate);
|
||||
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 adjusted;
|
||||
|
|
@ -269,13 +305,21 @@ void CameraController::update(float deltaTime) {
|
|||
auto getGroundAt = [&](float x, float y) -> std::optional<float> {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||
}
|
||||
return selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
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)) {
|
||||
base = m2H;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
// Get ground height at target position
|
||||
|
|
@ -340,17 +384,38 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Ground the character to terrain or WMO floor
|
||||
{
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
auto sampleGround = [&](float x, float y) -> std::optional<float> {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + eyeHeight);
|
||||
}
|
||||
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)) {
|
||||
base = m2H;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
// Sample center + small footprint to avoid slipping through narrow floor pieces.
|
||||
std::optional<float> groundH;
|
||||
constexpr float FOOTPRINT = 0.28f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{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);
|
||||
if (h && (!groundH || *h > *groundH)) {
|
||||
groundH = h;
|
||||
}
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
|
||||
}
|
||||
|
||||
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
|
||||
if (groundH) {
|
||||
float groundDiff = *groundH - lastGroundZ;
|
||||
|
|
@ -417,13 +482,7 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// Intentionally ignore M2 doodads for camera collision to match WoW feel.
|
||||
|
||||
// Check floor collision along the camera path
|
||||
// Sample a few points to find where camera would go underground
|
||||
|
|
@ -479,6 +538,41 @@ void CameraController::update(float deltaTime) {
|
|||
} else {
|
||||
// Free-fly camera mode (original behavior)
|
||||
glm::vec3 newPos = camera->getPosition();
|
||||
auto clampMoveAgainstWMO = [&](const glm::vec3& fromFeet, glm::vec3& toFeet) {
|
||||
if (!wmoRenderer) return;
|
||||
glm::vec3 move = toFeet - fromFeet;
|
||||
move.z = 0.0f;
|
||||
float moveDist = glm::length(glm::vec2(move));
|
||||
if (moveDist < 0.001f) return;
|
||||
|
||||
glm::vec3 dir = glm::normalize(move);
|
||||
glm::vec3 perp(-dir.y, dir.x, 0.0f);
|
||||
constexpr float BODY_RADIUS = 0.38f;
|
||||
constexpr float SAFETY = 0.08f;
|
||||
constexpr float HEIGHTS[3] = {0.4f, 1.0f, 1.6f};
|
||||
const glm::vec3 probes[3] = {
|
||||
fromFeet,
|
||||
fromFeet + perp * BODY_RADIUS,
|
||||
fromFeet - perp * BODY_RADIUS
|
||||
};
|
||||
|
||||
float bestHit = moveDist + 1.0f;
|
||||
for (const glm::vec3& probeBase : probes) {
|
||||
for (float h : HEIGHTS) {
|
||||
glm::vec3 origin = probeBase + glm::vec3(0.0f, 0.0f, h);
|
||||
float hit = wmoRenderer->raycastBoundingBoxes(origin, dir, moveDist + SAFETY);
|
||||
if (hit < bestHit) {
|
||||
bestHit = hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestHit < moveDist + SAFETY) {
|
||||
float allowed = std::max(0.0f, bestHit - SAFETY);
|
||||
toFeet.x = fromFeet.x + dir.x * allowed;
|
||||
toFeet.y = fromFeet.y + dir.y * allowed;
|
||||
}
|
||||
};
|
||||
float feetZ = newPos.z - eyeHeight;
|
||||
|
||||
// Check for water at feet position
|
||||
|
|
@ -546,6 +640,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
for (int i = 0; i < sweepSteps; i++) {
|
||||
glm::vec3 candidate = stepPos + stepDelta;
|
||||
clampMoveAgainstWMO(stepPos, candidate);
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
candidate = adjusted;
|
||||
|
|
@ -558,17 +653,37 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Ground to terrain or WMO floor
|
||||
{
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
auto sampleGround = [&](float x, float y) -> std::optional<float> {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, newPos.z);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2H = m2Renderer->getFloorHeight(x, y, newPos.z - eyeHeight);
|
||||
}
|
||||
auto base = selectReachableFloor(terrainH, wmoH, newPos.z - eyeHeight, 1.0f);
|
||||
if (m2H && *m2H <= (newPos.z - eyeHeight) + 1.0f && (!base || *m2H > *base)) {
|
||||
base = m2H;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(newPos.x, newPos.y);
|
||||
std::optional<float> groundH;
|
||||
constexpr float FOOTPRINT = 0.28f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT}
|
||||
};
|
||||
for (const auto& o : offsets) {
|
||||
auto h = sampleGround(newPos.x + o.x, newPos.y + o.y);
|
||||
if (h && (!groundH || *h > *groundH)) {
|
||||
groundH = h;
|
||||
}
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
|
||||
}
|
||||
|
||||
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, newPos.z - eyeHeight, 1.0f);
|
||||
|
||||
if (groundH) {
|
||||
lastGroundZ = *groundH;
|
||||
|
|
|
|||
|
|
@ -9,10 +9,29 @@
|
|||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
namespace {
|
||||
|
||||
void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::vec3& outMax) {
|
||||
glm::vec3 center = (model.boundMin + model.boundMax) * 0.5f;
|
||||
glm::vec3 half = (model.boundMax - model.boundMin) * 0.5f;
|
||||
|
||||
// Tighten footprint to reduce overly large object blockers.
|
||||
half.x *= 0.60f;
|
||||
half.y *= 0.60f;
|
||||
half.z *= 0.90f;
|
||||
|
||||
outMin = center - half;
|
||||
outMax = center + half;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void M2Instance::updateModelMatrix() {
|
||||
modelMatrix = glm::mat4(1.0f);
|
||||
modelMatrix = glm::translate(modelMatrix, position);
|
||||
|
|
@ -175,8 +194,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
|
||||
M2ModelGPU gpuModel;
|
||||
gpuModel.name = model.name;
|
||||
gpuModel.boundMin = model.boundMin;
|
||||
gpuModel.boundMax = model.boundMax;
|
||||
// Use tight bounds from actual vertices for collision/camera occlusion.
|
||||
// Header bounds in some M2s are overly conservative.
|
||||
glm::vec3 tightMin( std::numeric_limits<float>::max());
|
||||
glm::vec3 tightMax(-std::numeric_limits<float>::max());
|
||||
for (const auto& v : model.vertices) {
|
||||
tightMin = glm::min(tightMin, v.position);
|
||||
tightMax = glm::max(tightMax, v.position);
|
||||
}
|
||||
gpuModel.boundMin = tightMin;
|
||||
gpuModel.boundMax = tightMax;
|
||||
gpuModel.boundRadius = model.boundRadius;
|
||||
gpuModel.indexCount = static_cast<uint32_t>(model.indices.size());
|
||||
gpuModel.vertexCount = static_cast<uint32_t>(model.vertices.size());
|
||||
|
|
@ -532,56 +559,102 @@ uint32_t M2Renderer::getTotalTriangleCount() const {
|
|||
return total;
|
||||
}
|
||||
|
||||
std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ) const {
|
||||
std::optional<float> bestFloor;
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
auto it = models.find(instance.modelId);
|
||||
if (it == models.end()) continue;
|
||||
if (instance.scale <= 0.001f) continue;
|
||||
|
||||
const M2ModelGPU& model = it->second;
|
||||
glm::vec3 localMin, localMax;
|
||||
getTightCollisionBounds(model, localMin, localMax);
|
||||
|
||||
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
|
||||
glm::vec3 localPos = glm::vec3(invModel * 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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct "top" point at queried XY in local space, then transform back.
|
||||
glm::vec3 localTop(localPos.x, localPos.y, localMax.z);
|
||||
glm::vec3 worldTop = glm::vec3(instance.modelMatrix * glm::vec4(localTop, 1.0f));
|
||||
|
||||
// Reachability filter: only consider floors slightly above current feet.
|
||||
if (worldTop.z > glZ + 1.0f) continue;
|
||||
|
||||
if (!bestFloor || worldTop.z > *bestFloor) {
|
||||
bestFloor = worldTop.z;
|
||||
}
|
||||
}
|
||||
|
||||
return bestFloor;
|
||||
}
|
||||
|
||||
bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
||||
glm::vec3& adjustedPos, float playerRadius) const {
|
||||
(void)from;
|
||||
adjustedPos = to;
|
||||
bool collided = false;
|
||||
|
||||
// Check against all M2 instances using their bounding boxes
|
||||
// Check against all M2 instances in local space (rotation-aware).
|
||||
for (const auto& instance : instances) {
|
||||
auto it = models.find(instance.modelId);
|
||||
if (it == models.end()) continue;
|
||||
|
||||
const M2ModelGPU& model = it->second;
|
||||
if (instance.scale <= 0.001f) continue;
|
||||
|
||||
// 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;
|
||||
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
|
||||
glm::vec3 localPos = glm::vec3(invModel * glm::vec4(adjustedPos, 1.0f));
|
||||
float localRadius = playerRadius / instance.scale;
|
||||
|
||||
// Ensure min/max are correct
|
||||
glm::vec3 actualMin = glm::min(worldMin, worldMax);
|
||||
glm::vec3 actualMax = glm::max(worldMin, worldMax);
|
||||
glm::vec3 localMin, localMax;
|
||||
getTightCollisionBounds(model, localMin, localMax);
|
||||
localMin -= glm::vec3(localRadius);
|
||||
localMax += glm::vec3(localRadius);
|
||||
|
||||
// Expand bounds by player radius
|
||||
actualMin -= glm::vec3(playerRadius);
|
||||
actualMax += glm::vec3(playerRadius);
|
||||
|
||||
// Check if player position is inside expanded bounds (XY only for walking)
|
||||
if (adjustedPos.x >= actualMin.x && adjustedPos.x <= actualMax.x &&
|
||||
adjustedPos.y >= actualMin.y && adjustedPos.y <= actualMax.y &&
|
||||
adjustedPos.z >= actualMin.z && adjustedPos.z <= actualMax.z) {
|
||||
|
||||
// Push player out of the object
|
||||
// Find the shortest push direction (XY only)
|
||||
float pushLeft = adjustedPos.x - actualMin.x;
|
||||
float pushRight = actualMax.x - adjustedPos.x;
|
||||
float pushBack = adjustedPos.y - actualMin.y;
|
||||
float pushFront = actualMax.y - adjustedPos.y;
|
||||
|
||||
float minPush = std::min({pushLeft, pushRight, pushBack, pushFront});
|
||||
|
||||
if (minPush == pushLeft) {
|
||||
adjustedPos.x = actualMin.x - 0.01f;
|
||||
} else if (minPush == pushRight) {
|
||||
adjustedPos.x = actualMax.x + 0.01f;
|
||||
} else if (minPush == pushBack) {
|
||||
adjustedPos.y = actualMin.y - 0.01f;
|
||||
} else {
|
||||
adjustedPos.y = actualMax.y + 0.01f;
|
||||
}
|
||||
|
||||
collided = true;
|
||||
// Feet-based vertical overlap test: ignore objects fully above/below us.
|
||||
constexpr float PLAYER_HEIGHT = 2.0f;
|
||||
if (localPos.z + PLAYER_HEIGHT < localMin.z || localPos.z > localMax.z) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localPos.x < localMin.x || localPos.x > localMax.x ||
|
||||
localPos.y < localMin.y || localPos.y > localMax.y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float pushLeft = localPos.x - localMin.x;
|
||||
float pushRight = localMax.x - localPos.x;
|
||||
float pushBack = localPos.y - localMin.y;
|
||||
float pushFront = localMax.y - localPos.y;
|
||||
|
||||
float minPush = std::min({pushLeft, pushRight, pushBack, pushFront});
|
||||
// Soft pushback to avoid large snapping when grazing doodads.
|
||||
float pushAmount = std::min(0.05f, std::max(0.0f, minPush * 0.30f));
|
||||
if (pushAmount <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
glm::vec3 localPush(0.0f);
|
||||
if (minPush == pushLeft) {
|
||||
localPush.x = -pushAmount;
|
||||
} else if (minPush == pushRight) {
|
||||
localPush.x = pushAmount;
|
||||
} else if (minPush == pushBack) {
|
||||
localPush.y = -pushAmount;
|
||||
} else {
|
||||
localPush.y = pushAmount;
|
||||
}
|
||||
|
||||
glm::vec3 worldPush = glm::vec3(instance.modelMatrix * glm::vec4(localPush, 0.0f));
|
||||
adjustedPos.x += worldPush.x;
|
||||
adjustedPos.y += worldPush.y;
|
||||
collided = true;
|
||||
}
|
||||
|
||||
return collided;
|
||||
|
|
@ -595,33 +668,36 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3&
|
|||
if (it == models.end()) continue;
|
||||
|
||||
const M2ModelGPU& model = it->second;
|
||||
glm::vec3 localMin, localMax;
|
||||
getTightCollisionBounds(model, localMin, localMax);
|
||||
// Skip tiny doodads for camera occlusion; they cause jitter and false hits.
|
||||
glm::vec3 extents = (localMax - localMin) * instance.scale;
|
||||
if (glm::length(extents) < 0.75f) continue;
|
||||
|
||||
// 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;
|
||||
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)));
|
||||
if (!std::isfinite(localDir.x) || !std::isfinite(localDir.y) || !std::isfinite(localDir.z)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Local-space AABB slab intersection.
|
||||
glm::vec3 invDir = 1.0f / localDir;
|
||||
glm::vec3 tMin = (localMin - localOrigin) * invDir;
|
||||
glm::vec3 tMax = (localMax - localOrigin) * invDir;
|
||||
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});
|
||||
if (tNear > tFar || tFar <= 0.0f) continue;
|
||||
|
||||
// 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;
|
||||
}
|
||||
float tHit = tNear > 0.0f ? tNear : tFar;
|
||||
glm::vec3 localHit = localOrigin + localDir * tHit;
|
||||
glm::vec3 worldHit = glm::vec3(instance.modelMatrix * glm::vec4(localHit, 1.0f));
|
||||
float worldDist = glm::length(worldHit - origin);
|
||||
if (worldDist > 0.0f && worldDist < closestHit) {
|
||||
closestHit = worldDist;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -884,6 +884,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
// Push player away from wall (horizontal only)
|
||||
float pushDist = PLAYER_RADIUS - absPlaneDist;
|
||||
if (pushDist > 0.0f) {
|
||||
// Soft pushback avoids hard side-snaps when skimming walls.
|
||||
pushDist = std::min(0.06f, pushDist * 0.30f);
|
||||
if (pushDist <= 0.0f) continue;
|
||||
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
|
||||
glm::vec3 pushLocal = normal * sign * pushDist;
|
||||
|
||||
|
|
@ -944,22 +947,31 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * 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;
|
||||
// Broad-phase cull with local AABB first.
|
||||
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle negative direction components
|
||||
glm::vec3 t1 = glm::min(tMin, tMax);
|
||||
glm::vec3 t2 = glm::max(tMin, tMax);
|
||||
// Narrow-phase: triangle raycast for accurate camera collision.
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const glm::vec3& v0 = verts[indices[i]];
|
||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||
|
||||
float tNear = std::max({t1.x, t1.y, t1.z});
|
||||
float tFar = std::min({t2.x, t2.y, t2.z});
|
||||
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
||||
if (t <= 0.0f) {
|
||||
// Two-sided collision.
|
||||
t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
||||
}
|
||||
if (t <= 0.0f) continue;
|
||||
|
||||
// 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;
|
||||
glm::vec3 localHit = localOrigin + localDir * t;
|
||||
glm::vec3 worldHit = glm::vec3(instance.modelMatrix * glm::vec4(localHit, 1.0f));
|
||||
float worldDist = glm::length(worldHit - origin);
|
||||
if (worldDist > 0.0f && worldDist < closestHit && worldDist <= maxDistance) {
|
||||
closestHit = worldDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue