// // ShapeCollider.cpp // libraries/shared/src // // Created by Andrew Meadows on 02/20/2014. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include "AACubeShape.h" #include "CapsuleShape.h" #include "GeometryUtil.h" #include "ListShape.h" #include "PlaneShape.h" #include "ShapeCollider.h" #include "SphereShape.h" #include "StreamUtils.h" // NOTE: // // * Large ListShape's are inefficient keep the lists short. // * Collisions between lists of lists work in theory but are not recommended. const quint8 NUM_SHAPE_TYPES = UNKNOWN_SHAPE; const quint8 NUM_DISPATCH_CELLS = NUM_SHAPE_TYPES * NUM_SHAPE_TYPES; Shape::Type getDispatchKey(Shape::Type typeA, Shape::Type typeB) { return typeA + NUM_SHAPE_TYPES * typeB; } // dummy dispatch for any non-implemented pairings bool notImplemented(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return false; } // NOTE: hardcode the number of dispatchTable entries (NUM_SHAPE_TYPES ^2) bool (*dispatchTable[NUM_DISPATCH_CELLS])(const Shape*, const Shape*, CollisionList&); namespace ShapeCollider { // NOTE: the dispatch table must be initialized before the ShapeCollider is used. void initDispatchTable() { for (Shape::Type i = 0; i < NUM_DISPATCH_CELLS; ++i) { dispatchTable[i] = ¬Implemented; } dispatchTable[getDispatchKey(SPHERE_SHAPE, SPHERE_SHAPE)] = &sphereVsSphere; dispatchTable[getDispatchKey(SPHERE_SHAPE, CAPSULE_SHAPE)] = &sphereVsCapsule; dispatchTable[getDispatchKey(SPHERE_SHAPE, PLANE_SHAPE)] = &sphereVsPlane; dispatchTable[getDispatchKey(SPHERE_SHAPE, AACUBE_SHAPE)] = &sphereVsAACube; dispatchTable[getDispatchKey(SPHERE_SHAPE, LIST_SHAPE)] = &shapeVsList; dispatchTable[getDispatchKey(CAPSULE_SHAPE, SPHERE_SHAPE)] = &capsuleVsSphere; dispatchTable[getDispatchKey(CAPSULE_SHAPE, CAPSULE_SHAPE)] = &capsuleVsCapsule; dispatchTable[getDispatchKey(CAPSULE_SHAPE, PLANE_SHAPE)] = &capsuleVsPlane; dispatchTable[getDispatchKey(CAPSULE_SHAPE, AACUBE_SHAPE)] = &capsuleVsAACube; dispatchTable[getDispatchKey(CAPSULE_SHAPE, LIST_SHAPE)] = &shapeVsList; dispatchTable[getDispatchKey(PLANE_SHAPE, SPHERE_SHAPE)] = &planeVsSphere; dispatchTable[getDispatchKey(PLANE_SHAPE, CAPSULE_SHAPE)] = &planeVsCapsule; dispatchTable[getDispatchKey(PLANE_SHAPE, PLANE_SHAPE)] = &planeVsPlane; dispatchTable[getDispatchKey(PLANE_SHAPE, AACUBE_SHAPE)] = ¬Implemented; dispatchTable[getDispatchKey(PLANE_SHAPE, LIST_SHAPE)] = &shapeVsList; dispatchTable[getDispatchKey(AACUBE_SHAPE, SPHERE_SHAPE)] = &aaCubeVsSphere; dispatchTable[getDispatchKey(AACUBE_SHAPE, CAPSULE_SHAPE)] = &aaCubeVsCapsule; dispatchTable[getDispatchKey(AACUBE_SHAPE, PLANE_SHAPE)] = ¬Implemented; dispatchTable[getDispatchKey(AACUBE_SHAPE, AACUBE_SHAPE)] = &aaCubeVsAACube; dispatchTable[getDispatchKey(AACUBE_SHAPE, LIST_SHAPE)] = &shapeVsList; dispatchTable[getDispatchKey(LIST_SHAPE, SPHERE_SHAPE)] = &listVsShape; dispatchTable[getDispatchKey(LIST_SHAPE, CAPSULE_SHAPE)] = &listVsShape; dispatchTable[getDispatchKey(LIST_SHAPE, PLANE_SHAPE)] = &listVsShape; dispatchTable[getDispatchKey(LIST_SHAPE, AACUBE_SHAPE)] = &listVsShape; dispatchTable[getDispatchKey(LIST_SHAPE, LIST_SHAPE)] = &listVsList; // all of the UNKNOWN_SHAPE pairings are notImplemented } bool collideShapes(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return (*dispatchTable[shapeA->getType() + NUM_SHAPE_TYPES * shapeB->getType()])(shapeA, shapeB, collisions); } static CollisionList tempCollisions(32); bool collideShapeWithShapes(const Shape* shapeA, const QVector& shapes, int startIndex, CollisionList& collisions) { bool collided = false; if (shapeA) { int numShapes = shapes.size(); for (int i = startIndex; i < numShapes; ++i) { const Shape* shapeB = shapes.at(i); if (!shapeB) { continue; } if (collideShapes(shapeA, shapeB, collisions)) { collided = true; if (collisions.isFull()) { break; } } } } return collided; } bool collideShapesWithShapes(const QVector& shapesA, const QVector& shapesB, CollisionList& collisions) { bool collided = false; int numShapesA = shapesA.size(); for (int i = 0; i < numShapesA; ++i) { Shape* shapeA = shapesA.at(i); if (!shapeA) { continue; } if (collideShapeWithShapes(shapeA, shapesB, 0, collisions)) { collided = true; if (collisions.isFull()) { break; } } } return collided; } bool collideShapeWithAACubeLegacy(const Shape* shapeA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { Shape::Type typeA = shapeA->getType(); if (typeA == SPHERE_SHAPE) { return sphereVsAACubeLegacy(static_cast(shapeA), cubeCenter, cubeSide, collisions); } else if (typeA == CAPSULE_SHAPE) { return capsuleVsAACubeLegacy(static_cast(shapeA), cubeCenter, cubeSide, collisions); } else if (typeA == LIST_SHAPE) { const ListShape* listA = static_cast(shapeA); bool touching = false; for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { const Shape* subShape = listA->getSubShape(i); int subType = subShape->getType(); if (subType == SPHERE_SHAPE) { touching = sphereVsAACubeLegacy(static_cast(subShape), cubeCenter, cubeSide, collisions) || touching; } else if (subType == CAPSULE_SHAPE) { touching = capsuleVsAACubeLegacy(static_cast(subShape), cubeCenter, cubeSide, collisions) || touching; } } return touching; } return false; } bool sphereVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const SphereShape* sphereA = static_cast(shapeA); const SphereShape* sphereB = static_cast(shapeB); glm::vec3 BA = sphereB->getTranslation() - sphereA->getTranslation(); float distanceSquared = glm::dot(BA, BA); float totalRadius = sphereA->getRadius() + sphereB->getRadius(); if (distanceSquared < totalRadius * totalRadius) { // normalize BA float distance = sqrtf(distanceSquared); if (distance < EPSILON) { // the spheres are on top of each other, so we pick an arbitrary penetration direction BA = glm::vec3(0.0f, 1.0f, 0.0f); distance = totalRadius; } else { BA /= distance; } // penetration points from A into B CollisionInfo* collision = collisions.getNewCollision(); if (collision) { collision->_penetration = BA * (totalRadius - distance); // contactPoint is on surface of A collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * BA; collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } } return false; } bool sphereVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const SphereShape* sphereA = static_cast(shapeA); const CapsuleShape* capsuleB = static_cast(shapeB); // find sphereA's closest approach to axis of capsuleB glm::vec3 BA = capsuleB->getTranslation() - sphereA->getTranslation(); glm::vec3 capsuleAxis; capsuleB->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(BA, capsuleAxis); float absAxialDistance = fabsf(axialDistance); float totalRadius = sphereA->getRadius() + capsuleB->getRadius(); if (absAxialDistance < totalRadius + capsuleB->getHalfHeight()) { glm::vec3 radialAxis = BA + axialDistance * capsuleAxis; // points from A to axis of B float radialDistance2 = glm::length2(radialAxis); float totalRadius2 = totalRadius * totalRadius; if (radialDistance2 > totalRadius2) { // sphere is too far from capsule axis return false; } if (absAxialDistance > capsuleB->getHalfHeight()) { // sphere hits capsule on a cap --> recompute radialAxis to point from spherA to cap center float sign = (axialDistance > 0.0f) ? 1.0f : -1.0f; radialAxis = BA + (sign * capsuleB->getHalfHeight()) * capsuleAxis; radialDistance2 = glm::length2(radialAxis); if (radialDistance2 > totalRadius2) { return false; } } if (radialDistance2 > EPSILON * EPSILON) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { // collisions list is full return false; } // normalize the radialAxis float radialDistance = sqrtf(radialDistance2); radialAxis /= radialDistance; // penetration points from A into B collision->_penetration = (totalRadius - radialDistance) * radialAxis; // points from A into B // contactPoint is on surface of sphereA collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * radialAxis; collision->_shapeA = shapeA; collision->_shapeB = shapeB; } else { // A is on B's axis, so the penetration is undefined... if (absAxialDistance > capsuleB->getHalfHeight()) { // ...for the cylinder case (for now we pretend the collision doesn't exist) return false; } CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { // collisions list is full return false; } // ... but still defined for the cap case if (axialDistance < 0.0f) { // we're hitting the start cap, so we negate the capsuleAxis capsuleAxis *= -1; } // penetration points from A into B float sign = (axialDistance > 0.0f) ? -1.0f : 1.0f; collision->_penetration = (sign * (totalRadius + capsuleB->getHalfHeight() - absAxialDistance)) * capsuleAxis; // contactPoint is on surface of sphereA collision->_contactPoint = sphereA->getTranslation() + (sign * sphereA->getRadius()) * capsuleAxis; collision->_shapeA = shapeA; collision->_shapeB = shapeB; } return true; } return false; } bool sphereVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const SphereShape* sphereA = static_cast(shapeA); const PlaneShape* planeB = static_cast(shapeB); glm::vec3 penetration; if (findSpherePlanePenetration(sphereA->getTranslation(), sphereA->getRadius(), planeB->getCoefficients(), penetration)) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return false; // collision list is full } collision->_penetration = penetration; collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * glm::normalize(penetration); collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } return false; } bool capsuleVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return sphereVsCapsule(shapeB, shapeA, collisions); } /// \param lineP point on line /// \param lineDir normalized direction of line /// \param cylinderP point on cylinder axis /// \param cylinderDir normalized direction of cylinder axis /// \param cylinderRadius radius of cylinder /// \param hitLow[out] distance from point on line to first intersection with cylinder /// \param hitHigh[out] distance from point on line to second intersection with cylinder /// \return true if line hits cylinder bool lineCylinder(const glm::vec3& lineP, const glm::vec3& lineDir, const glm::vec3& cylinderP, const glm::vec3& cylinderDir, float cylinderRadius, float& hitLow, float& hitHigh) { // first handle parallel case float uDotV = glm::dot(lineDir, cylinderDir); if (fabsf(1.0f - fabsf(uDotV)) < EPSILON) { // line and cylinder are parallel if (glm::distance2(lineP, cylinderP) <= cylinderRadius * cylinderRadius) { // line is inside cylinder, which we consider a hit hitLow = 0.0f; hitHigh = 0.0f; return true; } return false; } // Given a line with point 'p' and normalized direction 'u' and // a cylinder with axial point 's', radius 'r', and normalized direction 'v' // the intersection of the two is on the line at distance 't' from 'p'. // // Determining the values of t reduces to solving a quadratic equation: At^2 + Bt + C = 0 // // where: // // P = p-s // w = u-(u.v)v // Q = P-(P.v)v // // A = w^2 // B = 2(w.Q) // C = Q^2 - r^2 glm::vec3 P = lineP - cylinderP; glm::vec3 w = lineDir - uDotV * cylinderDir; glm::vec3 Q = P - glm::dot(P, cylinderDir) * cylinderDir; // we save a few multiplies by storing 2*A rather than just A float A2 = 2.0f * glm::dot(w, w); float B = 2.0f * glm::dot(w, Q); // since C is only ever used once (in the determinant) we compute it inline float determinant = B * B - 2.0f * A2 * (glm::dot(Q, Q) - cylinderRadius * cylinderRadius); if (determinant < 0.0f) { return false; } hitLow = (-B - sqrtf(determinant)) / A2; hitHigh = -(hitLow + 2.0f * B / A2); if (hitLow > hitHigh) { // re-arrange so hitLow is always the smaller value float temp = hitHigh; hitHigh = hitLow; hitLow = temp; } return true; } bool capsuleVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const CapsuleShape* capsuleA = static_cast(shapeA); const CapsuleShape* capsuleB = static_cast(shapeB); glm::vec3 axisA; capsuleA->computeNormalizedAxis(axisA); glm::vec3 axisB; capsuleB->computeNormalizedAxis(axisB); glm::vec3 centerA = capsuleA->getTranslation(); glm::vec3 centerB = capsuleB->getTranslation(); // NOTE: The formula for closest approach between two lines is: // d = [(B - A) . (a - (a.b)b)] / (1 - (a.b)^2) float aDotB = glm::dot(axisA, axisB); float denominator = 1.0f - aDotB * aDotB; float totalRadius = capsuleA->getRadius() + capsuleB->getRadius(); if (denominator > EPSILON) { // perform line-cylinder intesection test between axis of cylinderA and cylinderB with exanded radius float hitLow = 0.0f; float hitHigh = 0.0f; if (!lineCylinder(centerA, axisA, centerB, axisB, totalRadius, hitLow, hitHigh)) { return false; } float halfHeightA = capsuleA->getHalfHeight(); if (hitLow > halfHeightA || hitHigh < -halfHeightA) { // the intersections are off the ends of capsuleA return false; } // compute nearest approach on axisA of axisB float distanceA = glm::dot((centerB - centerA), (axisA - (aDotB) * axisB)) / denominator; // clamp to intersection zone if (distanceA > hitLow) { if (distanceA > hitHigh) { distanceA = hitHigh; } } else { distanceA = hitLow; } // clamp to capsule segment distanceA = glm::clamp(distanceA, -halfHeightA, halfHeightA); // find the closest point on capsuleB to sphere on capsuleA float distanceB = glm::dot(centerA + distanceA * axisA - centerB, axisB); float halfHeightB = capsuleB->getHalfHeight(); if (fabsf(distanceB) > halfHeightB) { // we must clamp distanceB... distanceB = glm::clamp(distanceB, -halfHeightB, halfHeightB); // ...and therefore must recompute distanceA distanceA = glm::clamp(glm::dot(centerB + distanceB * axisB - centerA, axisA), -halfHeightA, halfHeightA); } // collide like two spheres (do most of the math relative to B) glm::vec3 BA = (centerB + distanceB * axisB) - (centerA + distanceA * axisA); float distanceSquared = glm::dot(BA, BA); if (distanceSquared < totalRadius * totalRadius) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { // collisions list is full return false; } // normalize BA float distance = sqrtf(distanceSquared); if (distance < EPSILON) { // the contact spheres are on top of each other, so we need to pick a penetration direction... // try vector between the capsule centers... BA = centerB - centerA; distanceSquared = glm::length2(BA); if (distanceSquared > EPSILON * EPSILON) { distance = sqrtf(distanceSquared); BA /= distance; } else { // the capsule centers are on top of each other! // give up on a valid penetration direction and just use the yAxis BA = glm::vec3(0.0f, 1.0f, 0.0f); distance = glm::max(capsuleB->getRadius(), capsuleA->getRadius()); } } else { BA /= distance; } // penetration points from A into B collision->_penetration = BA * (totalRadius - distance); // contactPoint is on surface of A collision->_contactPoint = centerA + distanceA * axisA + capsuleA->getRadius() * BA; collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } } else { // capsules are approximiately parallel but might still collide glm::vec3 BA = centerB - centerA; float axialDistance = glm::dot(BA, axisB); if (fabsf(axialDistance) > totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight()) { return false; } BA = BA - axialDistance * axisB; // BA now points from centerA to axisB (perp to axis) float distanceSquared = glm::length2(BA); if (distanceSquared < totalRadius * totalRadius) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { // collisions list is full return false; } // We have all the info we need to compute the penetration vector... // normalize BA float distance = sqrtf(distanceSquared); if (distance < EPSILON) { // the spheres are on top of each other, so we pick an arbitrary penetration direction BA = glm::vec3(0.0f, 1.0f, 0.0f); } else { BA /= distance; } // penetration points from A into B collision->_penetration = BA * (totalRadius - distance); // However we need some more world-frame info to compute the contactPoint, // which is on the surface of capsuleA... // // Find the overlapping secion of the capsules --> they collide as if there were // two spheres at the midpoint of this overlapping section. // So we project all endpoints to axisB, find the interior pair, // and put A's proxy sphere on axisA at the midpoint of this section. // sort the projections as much as possible during calculation float points[5]; points[0] = -capsuleB->getHalfHeight(); points[1] = axialDistance - capsuleA->getHalfHeight(); points[2] = axialDistance + capsuleA->getHalfHeight(); points[3] = capsuleB->getHalfHeight(); // Since there are only three comparisons to do we unroll the sort algorithm... // and use a fifth slot as temp during swap. if (points[1] > points[2]) { points[4] = points[1]; points[1] = points[2]; points[2] = points[4]; } if (points[2] > points[3]) { points[4] = points[2]; points[2] = points[3]; points[3] = points[4]; } if (points[0] > points[1]) { points[4] = points[0]; points[0] = points[1]; points[1] = points[4]; } // average the internal pair, and then do the math from centerB collision->_contactPoint = centerB + (0.5f * (points[1] + points[2])) * axisB + (capsuleA->getRadius() - distance) * BA; collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } } return false; } bool capsuleVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const CapsuleShape* capsuleA = static_cast(shapeA); const PlaneShape* planeB = static_cast(shapeB); glm::vec3 start, end, penetration; capsuleA->getStartPoint(start); capsuleA->getEndPoint(end); glm::vec4 plane = planeB->getCoefficients(); if (findCapsulePlanePenetration(start, end, capsuleA->getRadius(), plane, penetration)) { CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return false; // collision list is full } collision->_penetration = penetration; glm::vec3 deepestEnd = (glm::dot(start, glm::vec3(plane)) < glm::dot(end, glm::vec3(plane))) ? start : end; collision->_contactPoint = deepestEnd + capsuleA->getRadius() * glm::normalize(penetration); collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } return false; } bool planeVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return sphereVsPlane(shapeB, shapeA, collisions); } bool planeVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return capsuleVsPlane(shapeB, shapeA, collisions); } bool planeVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { // technically, planes always collide unless they're parallel and not coincident; however, that's // not going to give us any useful information return false; } // helper function bool sphereVsAACubeLegacy(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { // sphere is A // cube is B // BA = B - A = from center of A to center of B float halfCubeSide = 0.5f * cubeSide; glm::vec3 BA = cubeCenter - sphereCenter; float distance = glm::length(BA); if (distance > EPSILON) { float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); if (maxBA > halfCubeSide + sphereRadius) { // sphere misses cube entirely return false; } CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return false; } if (maxBA > halfCubeSide) { // sphere hits cube but its center is outside cube // compute contact anti-pole on cube (in cube frame) glm::vec3 cubeContact = glm::abs(BA); if (cubeContact.x > halfCubeSide) { cubeContact.x = halfCubeSide; } if (cubeContact.y > halfCubeSide) { cubeContact.y = halfCubeSide; } if (cubeContact.z > halfCubeSide) { cubeContact.z = halfCubeSide; } glm::vec3 signs = glm::sign(BA); cubeContact.x *= signs.x; cubeContact.y *= signs.y; cubeContact.z *= signs.z; // compute penetration direction glm::vec3 direction = BA - cubeContact; float lengthDirection = glm::length(direction); if (lengthDirection < EPSILON) { // sphereCenter is touching cube surface, so we can't use the difference between those two // points to compute the penetration direction. Instead we use the unitary components of // cubeContact. direction = cubeContact / halfCubeSide; glm::modf(BA, direction); lengthDirection = glm::length(direction); } else if (lengthDirection > sphereRadius) { collisions.deleteLastCollision(); return false; } direction /= lengthDirection; // compute collision details collision->_contactPoint = sphereCenter + sphereRadius * direction; collision->_penetration = sphereRadius * direction - (BA - cubeContact); } else { // sphere center is inside cube // --> push out nearest face glm::vec3 direction; BA /= maxBA; glm::modf(BA, direction); float lengthDirection = glm::length(direction); direction /= lengthDirection; // compute collision details collision->_floatData = cubeSide; collision->_vecData = cubeCenter; collision->_penetration = (halfCubeSide * lengthDirection + sphereRadius - maxBA * glm::dot(BA, direction)) * direction; collision->_contactPoint = sphereCenter + sphereRadius * direction; } collision->_floatData = cubeSide; collision->_vecData = cubeCenter; collision->_shapeA = NULL; collision->_shapeB = NULL; return true; } else if (sphereRadius + halfCubeSide > distance) { // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) CollisionInfo* collision = collisions.getNewCollision(); if (collision) { // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) collision->_penetration = (sphereRadius + halfCubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); // contactPoint is on surface of A collision->_contactPoint = sphereCenter + collision->_penetration; collision->_floatData = cubeSide; collision->_vecData = cubeCenter; collision->_shapeA = NULL; collision->_shapeB = NULL; return true; } } return false; } // helper function CollisionInfo* sphereVsAACubeHelper(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { // sphere is A // cube is B // BA = B - A = from center of A to center of B float halfCubeSide = 0.5f * cubeSide; glm::vec3 BA = cubeCenter - sphereCenter; float distance = glm::length(BA); if (distance > EPSILON) { float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); if (maxBA > halfCubeSide + sphereRadius) { // sphere misses cube entirely return NULL; } CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return NULL; } if (maxBA > halfCubeSide) { // sphere hits cube but its center is outside cube // compute contact anti-pole on cube (in cube frame) glm::vec3 cubeContact = glm::abs(BA); if (cubeContact.x > halfCubeSide) { cubeContact.x = halfCubeSide; } if (cubeContact.y > halfCubeSide) { cubeContact.y = halfCubeSide; } if (cubeContact.z > halfCubeSide) { cubeContact.z = halfCubeSide; } glm::vec3 signs = glm::sign(BA); cubeContact.x *= signs.x; cubeContact.y *= signs.y; cubeContact.z *= signs.z; // compute penetration direction glm::vec3 direction = BA - cubeContact; float lengthDirection = glm::length(direction); if (lengthDirection < EPSILON) { // sphereCenter is touching cube surface, so we can't use the difference between those two // points to compute the penetration direction. Instead we use the unitary components of // cubeContact. glm::modf(cubeContact / halfCubeSide, direction); lengthDirection = glm::length(direction); } else if (lengthDirection > sphereRadius) { collisions.deleteLastCollision(); return NULL; } direction /= lengthDirection; // compute collision details collision->_contactPoint = sphereCenter + sphereRadius * direction; collision->_penetration = sphereRadius * direction - (BA - cubeContact); } else { // sphere center is inside cube // --> push out nearest face glm::vec3 direction; BA /= maxBA; glm::modf(BA, direction); float lengthDirection = glm::length(direction); direction /= lengthDirection; // compute collision details collision->_floatData = cubeSide; collision->_vecData = cubeCenter; collision->_penetration = (halfCubeSide * lengthDirection + sphereRadius - maxBA * glm::dot(BA, direction)) * direction; collision->_contactPoint = sphereCenter + sphereRadius * direction; } collision->_shapeA = NULL; collision->_shapeB = NULL; return collision; } else if (sphereRadius + halfCubeSide > distance) { // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) CollisionInfo* collision = collisions.getNewCollision(); if (collision) { // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) collision->_penetration = (sphereRadius + halfCubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); // contactPoint is on surface of A collision->_contactPoint = sphereCenter + collision->_penetration; collision->_shapeA = NULL; collision->_shapeB = NULL; return collision; } } return NULL; } bool sphereVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { // BA = B - A = from center of A to center of B const SphereShape* sphereA = static_cast(shapeA); const AACubeShape* cubeB = static_cast(shapeB); CollisionInfo* collision = sphereVsAACubeHelper( sphereA->getTranslation(), sphereA->getRadius(), cubeB->getTranslation(), cubeB->getScale(), collisions); if (collision) { collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } return false; } glm::vec3 cubeNormals[3] = { glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f) }; // the wallIndices is a sequence of pairs that represent the OTHER directions for each cube face, // hence the first pair is (1, 2) because the OTHER faces for xFace are (yFace, zFace) = (1, 2) int wallIndices[] = { 1, 2, 0, 2, 0, 1 }; bool capsuleVsAACubeFace(const CapsuleShape* capsuleA, const AACubeShape* cubeB, int faceIndex, const glm::vec3& faceNormal, CollisionList& collisions) { // we only fall in here when the capsuleAxis is nearly parallel to the face of a cube glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); glm::vec3 cubeCenter = cubeB->getTranslation(); // Revisualize the capsule as a line segment between two points. We'd like to collide the // capsule as two spheres located at the endpoints or where the line segment hits the boundary // of the face. // We raytrace forward into the four planes that neigbhor the face to find the boundary // points of the line segment. glm::vec3 capsuleStart; capsuleA->getStartPoint(capsuleStart); // translate into cube-relative frame capsuleStart -= cubeCenter; float capsuleLength = 2.0f * capsuleA->getHalfHeight(); float halfCubeSide = 0.5f * cubeB->getScale(); float capsuleRadius = capsuleA->getRadius(); // preload distances with values that work for when the capsuleAxis runs parallel to neighbor face float distances[] = {FLT_MAX, -FLT_MAX, FLT_MAX, -FLT_MAX, 0.0f}; // Loop over the directions that are NOT parallel to face (there are two of them). // For each direction we'll raytrace against the positive and negative planes to find where // the axis passes through. for (int i = 0; i < 2; ++i) { int wallIndex = wallIndices[2 * faceIndex + i]; glm::vec3 wallNormal = cubeNormals[wallIndex]; // each direction has two walls: positive and negative float axisDotWall = glm::dot(capsuleAxis, wallNormal); if (fabsf(axisDotWall) > EPSILON) { // formula for distance to intersection between line (P,p) and plane (V,n) is: (V-P)*n / p*n distances[2 * i] = (halfCubeSide - glm::dot(capsuleStart, wallNormal)) / axisDotWall; distances[2 * i + 1] = -(halfCubeSide + glm::dot(capsuleStart, wallNormal)) / axisDotWall; } } // sort the distances from large to small int j = 3; while (j > 0) { for (int i = 0; i <= j; ++i) { if (distances[i] < distances[i+1]) { // swap (using distances[4] as temp space) distances[4] = distances[i]; distances[i] = distances[i+1]; distances[i+1] = distances[4]; } } --j; } // the capsule overlaps when the max of the mins is less than the min of the maxes distances[0] = glm::min(capsuleLength, distances[1]); // maxDistance distances[1] = glm::max(0.0f, distances[2]); // minDistance bool hit = false; if (distances[1] < distances[0]) { // if we collide at all it will be at two points for (int i = 0; i < 2; ++i) { glm::vec3 sphereCenter = cubeCenter + capsuleStart + distances[i] * capsuleAxis; // collide like a sphere at point0 with capsuleRadius CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, cubeCenter, 2.0f * halfCubeSide, collisions); if (collision) { // we hit! so store back pointers to the shapes collision->_shapeA = capsuleA; collision->_shapeB = cubeB; hit = true; } } return hit; } else if (distances[1] < capsuleLength + capsuleRadius ) { // we might collide at the end cap glm::vec3 sphereCenter = cubeCenter + capsuleStart + capsuleLength * capsuleAxis; // collide like a sphere at point0 with capsuleRadius CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, cubeCenter, 2.0f * halfCubeSide, collisions); if (collision) { // we hit! so store back pointers to the shapes collision->_shapeA = capsuleA; collision->_shapeB = cubeB; hit = true; } } else if (distances[0] > -capsuleLength) { // we might collide at the start cap glm::vec3 sphereCenter = cubeCenter + capsuleStart; // collide like a sphere at point0 with capsuleRadius CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, cubeCenter, 2.0f * halfCubeSide, collisions); if (collision) { // we hit! so store back pointers to the shapes collision->_shapeA = capsuleA; collision->_shapeB = cubeB; hit = true; } } return hit; } bool capsuleVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { const CapsuleShape* capsuleA = static_cast(shapeA); const AACubeShape* cubeB = static_cast(shapeB); // find nearest approach of capsule's line segment to cube's center glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); float halfHeight = capsuleA->getHalfHeight(); glm::vec3 cubeCenter = cubeB->getTranslation(); glm::vec3 capsuleCenter = capsuleA->getTranslation(); glm::vec3 BA = cubeCenter - capsuleCenter; float axialOffset = glm::dot(capsuleAxis, BA); if (fabsf(axialOffset) > halfHeight) { axialOffset = (axialOffset < 0.0f) ? -halfHeight : halfHeight; } glm::vec3 nearestApproach = capsuleCenter + axialOffset * capsuleAxis; // transform nearestApproach into cube-relative frame nearestApproach -= cubeCenter; // determine the face of nearest approach glm::vec3 signs = glm::sign(nearestApproach); int faceIndex = 0; glm::vec3 faceNormal(signs.x, 0.0f, 0.0f); float maxApproach = fabsf(nearestApproach.x); if (maxApproach < fabsf(nearestApproach.y)) { maxApproach = fabsf(nearestApproach.y); faceIndex = 1; faceNormal = glm::vec3(0.0f, signs.y, 0.0f); } if (maxApproach < fabsf(nearestApproach.z)) { maxApproach = fabsf(nearestApproach.z); faceIndex = 2; faceNormal = glm::vec3(0.0f, 0.0f, signs.z); } if (fabs(glm::dot(faceNormal, capsuleAxis)) < EPSILON) { if (glm::dot(nearestApproach, faceNormal) > cubeB->getScale() + capsuleA->getRadius()) { return false; } // we expect this case to be rare but complicated enough that we split it out // into its own helper function return capsuleVsAACubeFace(capsuleA, cubeB, faceIndex, faceNormal, collisions); } // Revisualize the capsule as a startPoint and an axis that points toward the cube face. // We raytrace forward into the four planes that neigbhor the face to find the furthest // point along the capsule's axis that might hit face. glm::vec3 capsuleStart; if (glm::dot(capsuleAxis, faceNormal) < 0.0f) { capsuleA->getStartPoint(capsuleStart); } else { // NOTE: we want dot(capsuleAxis, faceNormal) to be negative which simplifies some // logic below, so we pretend the end is the start thereby reversing its axis. capsuleA->getEndPoint(capsuleStart); capsuleAxis *= -1.0f; } // translate into cube-relative frame capsuleStart -= cubeCenter; float capsuleLength = 2.0f * halfHeight; float halfCubeSide = 0.5f * cubeB->getScale(); float capsuleRadius = capsuleA->getRadius(); // Loop over the directions that are NOT parallel to face (there are two of them). // For each direction we'll raytrace along capsuleAxis to find where the axis passes // through the furthest face and then we'll clamp to remain on the capsule's line segment float shortestDistance = capsuleLength; for (int i = 0; i < 2; ++i) { int wallIndex = wallIndices[2 * faceIndex + i]; // each direction has two walls: positive and negative for (float wallSign = -1.0f; wallSign < 2.0f; wallSign += 2.0f) { glm::vec3 wallNormal = wallSign * cubeNormals[wallIndex]; float axisDotWall = glm::dot(capsuleAxis, wallNormal); if (axisDotWall > EPSILON) { // formula for distance to intersection between line (P,p) and plane (V,n) is: (V-P)*n / p*n float newDistance = (halfCubeSide - glm::dot(capsuleStart, wallNormal)) / axisDotWall; if (newDistance < 0.0f) { // The wall is behind the capsule, but there is still a possibility that it collides // against the edge so we recast against the diagonal plane beteween the two faces. // NOTE: it is impossible for the startPoint to be in front of the diagonal plane, // therefore we know we'll get a valid distance. glm::vec3 thirdNormal = glm::cross(faceNormal, wallNormal); glm::vec3 diagonalNormal = glm::normalize(glm::cross(glm::cross(capsuleAxis, thirdNormal), thirdNormal)); newDistance = glm::dot(halfCubeSide * (faceNormal + wallNormal) - capsuleStart, diagonalNormal) / glm::dot(capsuleAxis, diagonalNormal); } else if (newDistance < capsuleLength) { // The wall truncates the capsule axis, but we must check the case where the capsule // collides with an edge/corner rather than the face. The good news is that this gives us // an opportunity to check for an early exit case. float heightOfImpact = glm::dot(capsuleStart + newDistance * capsuleAxis, faceNormal); if (heightOfImpact > halfCubeSide + SQUARE_ROOT_OF_2 * capsuleRadius) { // it is impossible for the capsule to hit the face return false; } else { // recast against the diagonal plane between the two faces glm::vec3 thirdNormal = glm::cross(faceNormal, wallNormal); glm::vec3 diagonalNormal = glm::normalize(glm::cross(glm::cross(capsuleAxis, thirdNormal), thirdNormal)); newDistance = glm::dot(halfCubeSide * (faceNormal + wallNormal) - capsuleStart, diagonalNormal) / glm::dot(capsuleAxis, diagonalNormal); } } if (newDistance < shortestDistance) { shortestDistance = newDistance; } // there can only be one hit per direction break; } } } // chose the point that produces the deepest penetration against face // and translate back into real frame glm::vec3 sphereCenter = cubeCenter + capsuleStart + shortestDistance * capsuleAxis; // collide like a sphere at point0 with capsuleRadius CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, cubeCenter, 2.0f * halfCubeSide, collisions); if (collision) { // we hit! so store back pointers to the shapes collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } return false; } bool aaCubeVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return sphereVsAACube(shapeB, shapeA, collisions); } bool aaCubeVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { return capsuleVsAACube(shapeB, shapeA, collisions); } // helper function CollisionInfo* aaCubeVsAACubeHelper(const glm::vec3& cubeCenterA, float cubeSideA, const glm::vec3& cubeCenterB, float cubeSideB, CollisionList& collisions) { // cube is A // cube is B // BA = B - A = from center of A to center of B float halfCubeSideA = 0.5f * cubeSideA; float halfCubeSideB = 0.5f * cubeSideB; glm::vec3 BA = cubeCenterB - cubeCenterA; float distance = glm::length(BA); if (distance > EPSILON) { float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); if (maxBA > halfCubeSideB + halfCubeSideA) { // cube misses cube entirely return NULL; } CollisionInfo* collision = collisions.getNewCollision(); if (!collision) { return NULL; // no more room for collisions } if (maxBA > halfCubeSideB) { // cube hits cube but its center is outside cube // compute contact anti-pole on cube (in cube frame) glm::vec3 cubeContact = glm::abs(BA); if (cubeContact.x > halfCubeSideB) { cubeContact.x = halfCubeSideB; } if (cubeContact.y > halfCubeSideB) { cubeContact.y = halfCubeSideB; } if (cubeContact.z > halfCubeSideB) { cubeContact.z = halfCubeSideB; } glm::vec3 signs = glm::sign(BA); cubeContact.x *= signs.x; cubeContact.y *= signs.y; cubeContact.z *= signs.z; // compute penetration direction glm::vec3 direction = BA - cubeContact; float lengthDirection = glm::length(direction); if (lengthDirection < EPSILON) { // cubeCenterA is touching cube B surface, so we can't use the difference between those two // points to compute the penetration direction. Instead we use the unitary components of // cubeContact. glm::modf(cubeContact / halfCubeSideB, direction); lengthDirection = glm::length(direction); } else if (lengthDirection > halfCubeSideA) { collisions.deleteLastCollision(); return NULL; } direction /= lengthDirection; // compute collision details collision->_contactPoint = cubeCenterA + halfCubeSideA * direction; collision->_penetration = halfCubeSideA * direction - (BA - cubeContact); } else { // cube center is inside cube // --> push out nearest face glm::vec3 direction; BA /= maxBA; glm::modf(BA, direction); float lengthDirection = glm::length(direction); direction /= lengthDirection; // compute collision details collision->_floatData = cubeSideB; collision->_vecData = cubeCenterB; collision->_penetration = (halfCubeSideB * lengthDirection + halfCubeSideA - maxBA * glm::dot(BA, direction)) * direction; collision->_contactPoint = cubeCenterA + halfCubeSideA * direction; } collision->_shapeA = NULL; collision->_shapeB = NULL; return collision; } else if (halfCubeSideA + halfCubeSideB > distance) { // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) CollisionInfo* collision = collisions.getNewCollision(); if (collision) { // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) collision->_penetration = (halfCubeSideA + halfCubeSideB) * glm::vec3(0.0f, -1.0f, 0.0f); // contactPoint is on surface of A collision->_contactPoint = cubeCenterA + collision->_penetration; collision->_shapeA = NULL; collision->_shapeB = NULL; return collision; } } return NULL; } bool aaCubeVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { // BA = B - A = from center of A to center of B const AACubeShape* cubeA = static_cast(shapeA); const AACubeShape* cubeB = static_cast(shapeB); CollisionInfo* collision = aaCubeVsAACubeHelper( cubeA->getTranslation(), cubeA->getScale(), cubeB->getTranslation(), cubeB->getScale(), collisions); if (collision) { collision->_shapeA = shapeA; collision->_shapeB = shapeB; return true; } return false; } bool shapeVsList(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { bool touching = false; const ListShape* listB = static_cast(shapeB); for (int i = 0; i < listB->size() && !collisions.isFull(); ++i) { const Shape* subShape = listB->getSubShape(i); touching = collideShapes(shapeA, subShape, collisions) || touching; } return touching; } bool listVsShape(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { bool touching = false; const ListShape* listA = static_cast(shapeA); for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { const Shape* subShape = listA->getSubShape(i); touching = collideShapes(subShape, shapeB, collisions) || touching; } return touching; } bool listVsList(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { bool touching = false; const ListShape* listA = static_cast(shapeA); const ListShape* listB = static_cast(shapeB); for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { const Shape* subShape = listA->getSubShape(i); for (int j = 0; j < listB->size() && !collisions.isFull(); ++j) { touching = collideShapes(subShape, listB->getSubShape(j), collisions) || touching; } } return touching; } // helper function /* KEEP THIS CODE -- this is how to collide the cube with stark face normals (no rounding). * We might want to use this code later for sealing boundaries between adjacent voxels. bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { glm::vec3 BA = cubeCenter - sphereCenter; float distance = glm::length(BA); if (distance > EPSILON) { BA /= distance; // BA is now normalized // compute the nearest point on sphere glm::vec3 surfaceA = sphereCenter + sphereRadius * BA; // compute the nearest point on cube float maxBA = glm::max(glm::max(fabsf(BA.x), fabsf(BA.y)), fabsf(BA.z)); glm::vec3 surfaceB = cubeCenter - (0.5f * cubeSide / maxBA) * BA; // collision happens when "vector to surfaceA from surfaceB" dots with BA to produce a positive value glm::vec3 surfaceAB = surfaceA - surfaceB; if (glm::dot(surfaceAB, BA) > 0.0f) { CollisionInfo* collision = collisions.getNewCollision(); if (collision) { // penetration is parallel to box side direction BA /= maxBA; glm::vec3 direction; glm::modf(BA, direction); direction = glm::normalize(direction); // penetration is the projection of surfaceAB on direction collision->_penetration = glm::dot(surfaceAB, direction) * direction; // contactPoint is on surface of A collision->_contactPoint = sphereCenter + sphereRadius * direction; collision->_shapeA = NULL; collision->_shapeB = NULL; return true; } } } else if (sphereRadius + 0.5f * cubeSide > distance) { // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) CollisionInfo* collision = collisions.getNewCollision(); if (collision) { // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) collision->_penetration = (sphereRadius + 0.5f * cubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); // contactPoint is on surface of A collision->_contactPoint = sphereCenter + collision->_penetration; collision->_shapeA = NULL; collision->_shapeB = NULL; return true; } } return false; } */ bool sphereVsAACubeLegacy(const SphereShape* sphereA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { return sphereVsAACubeLegacy(sphereA->getTranslation(), sphereA->getRadius(), cubeCenter, cubeSide, collisions); } bool capsuleVsAACubeLegacy(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { // find nerest approach of capsule line segment to cube glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); float offset = glm::dot(cubeCenter - capsuleA->getTranslation(), capsuleAxis); float halfHeight = capsuleA->getHalfHeight(); if (offset > halfHeight) { offset = halfHeight; } else if (offset < -halfHeight) { offset = -halfHeight; } glm::vec3 nearestApproach = capsuleA->getTranslation() + offset * capsuleAxis; // collide nearest approach like a sphere at that point return sphereVsAACubeLegacy(nearestApproach, capsuleA->getRadius(), cubeCenter, cubeSide, collisions); } bool findRayIntersection(const QVector& shapes, RayIntersectionInfo& intersection) { int numShapes = shapes.size(); bool hit = false; for (int i = 0; i < numShapes; ++i) { Shape* shape = shapes.at(i); if (shape) { if (shape->findRayIntersection(intersection)) { hit = true; } } } return hit; } } // namespace ShapeCollider