// // ShapeCollider.cpp // hifi // // Created by Andrew Meadows on 2014.02.20 // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. // #include #include #include "ShapeCollider.h" namespace ShapeCollider { bool shapeShape(const Shape* shapeA, const Shape* shapeB, CollisionInfo& collision) { // ATM we only have two shape types so we just check every case. // TODO: make a fast lookup for correct method if (shapeA->getType() == Shape::SPHERE_SHAPE) { const SphereShape* sphereA = static_cast(shapeA); if (shapeB->getType() == Shape::SPHERE_SHAPE) { return sphereSphere(sphereA, static_cast(shapeB), collision); } else if (shapeB->getType() == Shape::CAPSULE_SHAPE) { return sphereCapsule(sphereA, static_cast(shapeB), collision); } } else if (shapeA->getType() == Shape::CAPSULE_SHAPE) { const CapsuleShape* capsuleA = static_cast(shapeA); if (shapeB->getType() == Shape::SPHERE_SHAPE) { return capsuleSphere(capsuleA, static_cast(shapeB), collision); } else if (shapeB->getType() == Shape::CAPSULE_SHAPE) { return capsuleCapsule(capsuleA, static_cast(shapeB), collision); } } return false; } bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, CollisionInfo& collision) { glm::vec3 BA = sphereB->getPosition() - sphereA->getPosition(); 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.f, 1.f, 0.f); distance = totalRadius; } else { BA /= distance; } // penetration points from A into B collision._penetration = BA * (totalRadius - distance); // contactPoint is on surface of A collision._contactPoint = sphereA->getPosition() + sphereA->getRadius() * BA; return true; } return false; } bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, CollisionInfo& collision) { // find sphereA's closest approach to axis of capsuleB glm::vec3 BA = capsuleB->getPosition() - sphereA->getPosition(); glm::vec3 capsuleAxis; capsuleB->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(BA, capsuleAxis); float absAxialDistance = fabs(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); if (radialDistance2 > totalRadius * totalRadius) { // 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.f) ? 1.f : -1.f; radialAxis = BA + (sign * capsuleB->getHalfHeight()) * capsuleAxis; radialDistance2 = glm::length2(radialAxis); } if (radialDistance2 > EPSILON * EPSILON) { // 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->getPosition() + sphereA->getRadius() * radialAxis; } 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; } // ... but still defined for the cap case if (axialDistance < 0.f) { // we're hitting the start cap, so we negate the capsuleAxis capsuleAxis *= -1; } // penetration points from A into B float sign = (axialDistance > 0.f) ? -1.f : 1.f; collision._penetration = (sign * (totalRadius + capsuleB->getHalfHeight() - absAxialDistance)) * capsuleAxis; // contactPoint is on surface of sphereA collision._contactPoint = sphereA->getPosition() + (sign * sphereA->getRadius()) * capsuleAxis; } return true; } return false; } bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, CollisionInfo& collision) { // find sphereB's closest approach to axis of capsuleA glm::vec3 AB = capsuleA->getPosition() - sphereB->getPosition(); glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(AB, capsuleAxis); float absAxialDistance = fabs(axialDistance); float totalRadius = sphereB->getRadius() + capsuleA->getRadius(); if (absAxialDistance < totalRadius + capsuleA->getHalfHeight()) { glm::vec3 radialAxis = AB + axialDistance * capsuleAxis; // from sphereB to axis of capsuleA float radialDistance2 = glm::length2(radialAxis); if (radialDistance2 > totalRadius * totalRadius) { // sphere is too far from capsule axis return false; } // closestApproach = point on capsuleA's axis that is closest to sphereB's center glm::vec3 closestApproach = capsuleA->getPosition() + axialDistance * capsuleAxis; if (absAxialDistance > capsuleA->getHalfHeight()) { // sphere hits capsule on a cap // --> recompute radialAxis and closestApproach float sign = (axialDistance > 0.f) ? 1.f : -1.f; closestApproach = capsuleA->getPosition() + (sign * capsuleA->getHalfHeight()) * capsuleAxis; radialAxis = closestApproach - sphereB->getPosition(); radialDistance2 = glm::length2(radialAxis); } if (radialDistance2 > EPSILON * EPSILON) { // normalize the radialAxis float radialDistance = sqrtf(radialDistance2); radialAxis /= radialDistance; // penetration points from A into B collision._penetration = (radialDistance - totalRadius) * radialAxis; // points from A into B // contactPoint is on surface of capsuleA collision._contactPoint = closestApproach - capsuleA->getRadius() * radialAxis; } else { // A is on B's axis, so the penetration is undefined... if (absAxialDistance > capsuleA->getHalfHeight()) { // ...for the cylinder case (for now we pretend the collision doesn't exist) return false; } else { // ... but still defined for the cap case if (axialDistance < 0.f) { // we're hitting the start cap, so we negate the capsuleAxis capsuleAxis *= -1; } float sign = (axialDistance > 0.f) ? 1.f : -1.f; collision._penetration = (sign * (totalRadius + capsuleA->getHalfHeight() - absAxialDistance)) * capsuleAxis; // contactPoint is on surface of sphereA collision._contactPoint = closestApproach + (sign * capsuleA->getRadius()) * capsuleAxis; } } return true; } return false; } bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, CollisionInfo& collision) { glm::vec3 axisA; capsuleA->computeNormalizedAxis(axisA); glm::vec3 axisB; capsuleB->computeNormalizedAxis(axisB); glm::vec3 centerA = capsuleA->getPosition(); glm::vec3 centerB = capsuleB->getPosition(); // 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.f - aDotB * aDotB; float totalRadius = capsuleA->getRadius() + capsuleB->getRadius(); if (denominator > EPSILON) { // distances to points of closest approach float distanceA = glm::dot((centerB - centerA), (axisA - (aDotB) * axisB)) / denominator; float distanceB = glm::dot((centerA - centerB), (axisB - (aDotB) * axisA)) / denominator; // clamp the distances to the ends of the capsule line segments float absDistanceA = fabs(distanceA); if (absDistanceA > capsuleA->getHalfHeight() + capsuleA->getRadius()) { float signA = distanceA < 0.f ? -1.f : 1.f; distanceA = signA * capsuleA->getHalfHeight(); } float absDistanceB = fabs(distanceB); if (absDistanceB > capsuleB->getHalfHeight() + capsuleB->getRadius()) { float signB = distanceB < 0.f ? -1.f : 1.f; distanceB = signB * capsuleB->getHalfHeight(); } // collide like spheres at closest approaches (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) { // 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.f, 1.f, 0.f); 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; return true; } } else { // capsules are approximiately parallel but might still collide glm::vec3 BA = centerB - centerA; float axialDistance = glm::dot(BA, axisB); float maxAxialDistance = totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight(); if (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) { // 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.f, 1.f, 0.f); } 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[4] > points[2]) { points[5] = 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 return true; } } return false; } } // namespace ShapeCollider