Revamped collision system

This commit is contained in:
Jan Racek
2026-03-27 16:39:10 +01:00
parent 090402f71a
commit 480323f5b9
11 changed files with 278 additions and 252 deletions

View File

@@ -3,17 +3,14 @@
#include <psyqo/fixed-point.hh>
// Helper type alias for brevity
using FP = psyqo::FixedPoint<12>;
namespace psxsplash {
// Static member initialization
psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16);
psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16);
psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4); // (32 / 8) = 4
psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4);
// AABB expand implementation
void AABB::expand(const psyqo::Vec3& delta) {
psyqo::FixedPoint<12> zero;
if (delta.x > zero) max.x = max.x + delta.x;
@@ -35,7 +32,6 @@ void SpatialGrid::clear() {
}
void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const {
// Clamp position to world bounds
auto px = pos.x;
auto py = pos.y;
auto pz = pos.z;
@@ -47,13 +43,10 @@ void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz)
if (pz < WORLD_MIN) pz = WORLD_MIN;
if (pz > WORLD_MAX) pz = WORLD_MAX;
// Convert to grid coordinates (0 to GRID_SIZE-1)
// Using integer division after scaling
gx = ((px - WORLD_MIN) / CELL_SIZE).integer();
gy = ((py - WORLD_MIN) / CELL_SIZE).integer();
gz = ((pz - WORLD_MIN) / CELL_SIZE).integer();
// Clamp to valid range
if (gx < 0) gx = 0;
if (gx >= GRID_SIZE) gx = GRID_SIZE - 1;
if (gy < 0) gy = 0;
@@ -69,14 +62,12 @@ int SpatialGrid::getCellIndex(const psyqo::Vec3& pos) const {
}
void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
// Get grid range for this AABB
int minGx, minGy, minGz;
int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz);
worldToGrid(bounds.max, maxGx, maxGy, maxGz);
// Insert into all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) {
@@ -86,8 +77,6 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
if (cell.count < MAX_OBJECTS_PER_CELL) {
cell.objectIndices[cell.count++] = objectIndex;
}
// If cell is full, object won't be in this cell (may miss collisions)
// This is a tradeoff for memory/performance
}
}
}
@@ -96,18 +85,15 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const {
int resultCount = 0;
// Get grid range for query AABB
int minGx, minGy, minGz;
int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz);
worldToGrid(bounds.max, maxGx, maxGy, maxGz);
// Track which objects we've already added (two 32-bit masks for objects 0-63)
uint32_t addedMaskLow = 0; // Objects 0-31
uint32_t addedMaskHigh = 0; // Objects 32-63
uint32_t addedMaskLow = 0;
uint32_t addedMaskHigh = 0;
// Query all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) {
@@ -117,7 +103,6 @@ int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults)
for (int i = 0; i < cell.count; i++) {
uint16_t objIndex = cell.objectIndices[i];
// Skip if already added (using bitmask for objects 0-63)
if (objIndex < 32) {
uint32_t bit = 1U << objIndex;
if (addedMaskLow & bit) continue;
@@ -149,6 +134,7 @@ void CollisionSystem::init() {
void CollisionSystem::reset() {
m_colliderCount = 0;
m_triggerBoxCount = 0;
m_resultCount = 0;
m_triggerPairCount = 0;
m_grid.clear();
@@ -166,6 +152,14 @@ void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& loc
data.gameObjectIndex = gameObjectIndex;
}
void CollisionSystem::registerTriggerBox(const AABB& bounds, int16_t luaFileIndex) {
if (m_triggerBoxCount >= MAX_TRIGGER_BOXES) return;
TriggerBoxData& tb = m_triggerBoxes[m_triggerBoxCount++];
tb.bounds = bounds;
tb.luaFileIndex = luaFileIndex;
}
void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
const psyqo::Matrix33& rotation) {
for (int i = 0; i < m_colliderCount; i++) {
@@ -177,90 +171,114 @@ void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3
}
}
int CollisionSystem::detectCollisions() {
int CollisionSystem::detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack) {
m_resultCount = 0;
const FP zero(0);
pushBack = psyqo::Vec3{zero, zero, zero};
// Clear and rebuild spatial grid
// Rebuild spatial grid with all colliders
m_grid.clear();
for (int i = 0; i < m_colliderCount; i++) {
m_grid.insert(i, m_colliders[i].bounds);
}
// Check each collider against potential colliders from grid
for (int i = 0; i < m_colliderCount; i++) {
const CollisionData& colliderA = m_colliders[i];
// Skip if no collision type
if (colliderA.type == CollisionType::None) continue;
// Query spatial grid for nearby objects
uint16_t nearby[32];
int nearbyCount = m_grid.queryAABB(colliderA.bounds, nearby, 32);
for (int j = 0; j < nearbyCount; j++) {
int otherIndex = nearby[j];
// Skip self
if (otherIndex == i) continue;
// Skip if already processed (only process pairs once)
if (otherIndex < i) continue;
const CollisionData& colliderB = m_colliders[otherIndex];
// Skip if no collision type
if (colliderB.type == CollisionType::None) continue;
// Check layer masks
if ((colliderA.layerMask & colliderB.layerMask) == 0) continue;
// Narrowphase AABB test
psyqo::Vec3 normal;
psyqo::FixedPoint<12> penetration;
if (testAABB(colliderA.bounds, colliderB.bounds, normal, penetration)) {
// Collision detected
if (m_resultCount < MAX_COLLISION_RESULTS) {
CollisionResult& result = m_results[m_resultCount++];
result.objectA = colliderA.gameObjectIndex;
result.objectB = colliderB.gameObjectIndex;
result.normal = normal;
result.penetration = penetration;
}
// Handle triggers
if (colliderA.type == CollisionType::Trigger) {
updateTriggerState(i, otherIndex, true);
}
if (colliderB.type == CollisionType::Trigger) {
updateTriggerState(otherIndex, i, true);
}
}
}
}
// Test player AABB against all colliders for push-back
uint16_t nearby[32];
int nearbyCount = m_grid.queryAABB(playerAABB, nearby, 32);
// Update trigger pairs that are no longer overlapping
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
pair.framesSinceContact++;
for (int j = 0; j < nearbyCount; j++) {
int idx = nearby[j];
const CollisionData& collider = m_colliders[idx];
if (collider.type == CollisionType::None) continue;
// If no contact for several frames, trigger exit
if (pair.framesSinceContact > 2 && pair.state != 2) {
pair.state = 2; // Exiting
psyqo::Vec3 normal;
psyqo::FixedPoint<12> penetration;
if (testAABB(playerAABB, collider.bounds, normal, penetration)) {
// Accumulate push-back along the separation normal
pushBack.x = pushBack.x + normal.x * penetration;
pushBack.y = pushBack.y + normal.y * penetration;
pushBack.z = pushBack.z + normal.z * penetration;
if (m_resultCount < MAX_COLLISION_RESULTS) {
CollisionResult& result = m_results[m_resultCount++];
result.objectA = 0xFFFF; // player
result.objectB = collider.gameObjectIndex;
result.normal = normal;
result.penetration = penetration;
}
}
}
return m_resultCount;
}
void CollisionSystem::detectTriggers(const AABB& playerAABB, SceneManager& scene) {
int writeIndex = 0;
// Mark all existing pairs as potentially stale
for (int i = 0; i < m_triggerPairCount; i++) {
m_triggerPairs[i].framesSinceContact++;
}
// Test player against each trigger box
for (int ti = 0; ti < m_triggerBoxCount; ti++) {
const TriggerBoxData& tb = m_triggerBoxes[ti];
if (!playerAABB.intersects(tb.bounds)) continue;
// Find existing pair
bool found = false;
for (int pi = 0; pi < m_triggerPairCount; pi++) {
if (m_triggerPairs[pi].triggerIndex == ti) {
m_triggerPairs[pi].framesSinceContact = 0;
if (m_triggerPairs[pi].state == 0) {
m_triggerPairs[pi].state = 1; // enter -> active
}
found = true;
break;
}
}
// New pair: enter
if (!found && m_triggerPairCount < MAX_TRIGGER_PAIRS) {
TriggerPair& pair = m_triggerPairs[m_triggerPairCount++];
pair.triggerIndex = ti;
pair.padding = 0;
pair.framesSinceContact = 0;
pair.state = 0;
pair.padding2 = 0;
}
}
// Process pairs: fire events and clean up exited pairs
writeIndex = 0;
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
int16_t luaIdx = m_triggerBoxes[pair.triggerIndex].luaFileIndex;
if (pair.state == 0) {
// Enter
scene.fireTriggerEnter(luaIdx, pair.triggerIndex);
pair.state = 1;
m_triggerPairs[writeIndex++] = pair;
} else if (pair.framesSinceContact > 2) {
// Exit
scene.fireTriggerExit(luaIdx, pair.triggerIndex);
} else {
// Still inside, keep alive
m_triggerPairs[writeIndex++] = pair;
}
}
m_triggerPairCount = writeIndex;
}
bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const {
// Check for overlap on all axes
if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
// Calculate penetration on each axis
auto overlapX1 = a.max.x - b.min.x;
auto overlapX2 = b.max.x - a.min.x;
auto overlapY1 = a.max.y - b.min.y;
@@ -268,17 +286,14 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
auto overlapZ1 = a.max.z - b.min.z;
auto overlapZ2 = b.max.z - a.min.z;
// Find minimum overlap axis
auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2;
auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2;
auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2;
// Constants for normals
const FP zero(0);
const FP one(1);
const FP negOne(-1);
// Determine separation axis (axis with least penetration)
if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) {
penetration = minOverlapX;
normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero};
@@ -293,31 +308,6 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
return true;
}
void CollisionSystem::updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping) {
// Look for existing pair
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
if (pair.triggerIndex == triggerIndex && pair.otherIndex == otherIndex) {
if (isOverlapping) {
pair.framesSinceContact = 0;
if (pair.state == 0) {
pair.state = 1; // Now staying
}
}
return;
}
}
// New pair - add it
if (isOverlapping && m_triggerPairCount < MAX_TRIGGERS) {
TriggerPair& pair = m_triggerPairs[m_triggerPairCount++];
pair.triggerIndex = triggerIndex;
pair.otherIndex = otherIndex;
pair.framesSinceContact = 0;
pair.state = 0; // New (enter event)
}
}
bool CollisionSystem::areColliding(uint16_t indexA, uint16_t indexB) const {
for (int i = 0; i < m_resultCount; i++) {
if ((m_results[i].objectA == indexA && m_results[i].objectB == indexB) ||
@@ -332,32 +322,26 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire
psyqo::FixedPoint<12> maxDistance,
psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal,
uint16_t& hitObjectIndex) const {
// Simple brute-force raycast against all colliders
// TODO: Use spatial grid for optimization
auto closestT = maxDistance;
bool hit = false;
// Fixed-point constants
const FP zero(0);
const FP one(1);
const FP negOne(-1);
const FP largeVal(1000);
const FP negLargeVal(-1000);
FP epsilon;
epsilon.value = 4; // ~0.001 in 20.12 fixed point
epsilon.value = 4;
for (int i = 0; i < m_colliderCount; i++) {
const CollisionData& collider = m_colliders[i];
if (collider.type == CollisionType::None) continue;
// Ray-AABB intersection test (slab method)
const AABB& box = collider.bounds;
auto tMin = negLargeVal;
auto tMax = largeVal;
// X slab
if (direction.x != zero) {
auto invD = one / direction.x;
auto t1 = (box.min.x - origin.x) * invD;
@@ -369,7 +353,6 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire
continue;
}
// Y slab
if (direction.y != zero) {
auto invD = one / direction.y;
auto t1 = (box.min.y - origin.y) * invD;
@@ -381,7 +364,6 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire
continue;
}
// Z slab
if (direction.z != zero) {
auto invD = one / direction.z;
auto t1 = (box.min.z - origin.z) * invD;
@@ -402,14 +384,12 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire
hitObjectIndex = collider.gameObjectIndex;
hit = true;
// Calculate hit point
hitPoint = psyqo::Vec3{
origin.x + direction.x * t,
origin.y + direction.y * t,
origin.z + direction.z * t
};
// Calculate normal (which face was hit)
if ((hitPoint.x - box.min.x).abs() < epsilon) hitNormal = psyqo::Vec3{negOne, zero, zero};
else if ((hitPoint.x - box.max.x).abs() < epsilon) hitNormal = psyqo::Vec3{one, zero, zero};
else if ((hitPoint.y - box.min.y).abs() < epsilon) hitNormal = psyqo::Vec3{zero, negOne, zero};
@@ -422,37 +402,4 @@ bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& dire
return hit;
}
void CollisionSystem::processTriggerEvents(SceneManager& scene) {
// Process trigger pairs and fire Lua events
int writeIndex = 0;
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
// Get game object indices
uint16_t triggerObjIdx = m_colliders[pair.triggerIndex].gameObjectIndex;
uint16_t otherObjIdx = m_colliders[pair.otherIndex].gameObjectIndex;
switch (pair.state) {
case 0: // Enter
scene.fireTriggerEnter(triggerObjIdx, otherObjIdx);
pair.state = 1; // Move to staying
m_triggerPairs[writeIndex++] = pair;
break;
case 1: // Staying
scene.fireTriggerStay(triggerObjIdx, otherObjIdx);
m_triggerPairs[writeIndex++] = pair;
break;
case 2: // Exit
scene.fireTriggerExit(triggerObjIdx, otherObjIdx);
// Don't copy - remove from list
break;
}
}
m_triggerPairCount = writeIndex;
}
} // namespace psxsplash