From 26f4d664abdca3b5f27a867bf393133787b5693a Mon Sep 17 00:00:00 2001 From: ICOM725 Date: Fri, 18 Sep 2026 10:05:07 +0800 Subject: [PATCH] Math: fix sphere-cone intersections near the apex. Use the negative radius-scaled sine offset for the dividing plane, matching the documented algorithm and sphereConeView(). The previous sign rejected spheres that intersected the cone side near its apex without containing the apex itself. Add Float and Double coverage for near-apex intersections and misses, including precomputed angles and translated and rotated cones. --- src/Magnum/Math/Intersection.h | 2 +- src/Magnum/Math/Test/IntersectionTest.cpp | 63 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Magnum/Math/Intersection.h b/src/Magnum/Math/Intersection.h index 0a3fd0166..77f4afa94 100644 --- a/src/Magnum/Math/Intersection.h +++ b/src/Magnum/Math/Intersection.h @@ -621,7 +621,7 @@ template bool sphereCone( const Vector3 diff = sphereCenter - coneOrigin; /* Point - cone test */ - if(Math::dot(diff - sphereRadius*sinAngle*coneNormal, coneNormal) > T(0)) { + if(Math::dot(diff + sphereRadius*sinAngle*coneNormal, coneNormal) > T(0)) { const Vector3 c = sinAngle*diff + coneNormal*sphereRadius; const T lenA = Math::dot(c, coneNormal); diff --git a/src/Magnum/Math/Test/IntersectionTest.cpp b/src/Magnum/Math/Test/IntersectionTest.cpp index 6b83a0c30..fb4eecced 100644 --- a/src/Magnum/Math/Test/IntersectionTest.cpp +++ b/src/Magnum/Math/Test/IntersectionTest.cpp @@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE. */ +#include #include #include @@ -53,6 +54,7 @@ struct IntersectionTest: TestSuite::Tester { void pointCone(); void pointDoubleCone(); void sphereCone(); + template void sphereConeNearApex(); void sphereConeView(); void sphereConeViewNotRigid(); void rangeCone(); @@ -68,6 +70,25 @@ using Magnum::Constants; using Magnum::Range3D; using Magnum::Rad; +constexpr struct { + const char* name; + Float angle; + Vector3 center; + Float radius; + bool intersects; +} SphereConeNearApexData[]{ + {"wide cone, beside apex", 120.0f, {1.5f, 0.0f, 0.0f}, 1.0f, true}, + {"wide cone, behind apex", 120.0f, {1.1f, 0.0f, -0.2f}, 1.0f, true}, + {"wide cone, in front of apex", 120.0f, {1.5f, 0.0f, 0.2f}, 1.0f, true}, + {"narrow cone, beside apex", 60.0f, {1.1f, 0.0f, 0.0f}, 1.0f, true}, + {"outside side", 120.0f, {2.5f, 0.0f, 0.0f}, 1.0f, false}, + {"outside behind apex", 120.0f, {0.0f, 0.0f, -1.5f}, 1.0f, false}, + {"contains apex", 120.0f, {0.0f, 0.0f, -0.5f}, 1.0f, true}, + {"touches apex", 120.0f, {0.0f, 0.0f, -1.0f}, 1.0f, true}, + {"inside cone", 120.0f, {0.0f, 0.0f, 3.0f}, 1.0f, true}, + {"zero radius outside", 120.0f, {1.0f, 0.0f, 0.0f}, 0.0f, false} +}; + IntersectionTest::IntersectionTest() { addTests({&IntersectionTest::pointCircle, &IntersectionTest::pointSphere, @@ -88,6 +109,10 @@ IntersectionTest::IntersectionTest() { &IntersectionTest::sphereConeViewNotRigid, &IntersectionTest::rangeCone, &IntersectionTest::aabbCone}); + + addInstancedTests({&IntersectionTest::sphereConeNearApex, + &IntersectionTest::sphereConeNearApex}, + Containers::arraySize(SphereConeNearApexData)); } void IntersectionTest::pointCircle() { @@ -407,6 +432,44 @@ void IntersectionTest::sphereCone() { CORRADE_VERIFY(!Intersection::sphereCone(center - 4.0f*surface, 0.5f, center, normal, angle)); } +template void IntersectionTest::sphereConeNearApex() { + auto&& data = SphereConeNearApexData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + setTestCaseTemplateName(TypeTraits::name()); + + const Math::Vector3 sphereCenter{data.center}; + const T radius = T(data.radius); + const Math::Rad angle{Math::Deg{T(data.angle)}}; + const T sinAngle = Math::sin(angle/T(2)); + const T tanAngle = Math::tan(angle/T(2)); + const Math::Vector3 normal = Math::Vector3::zAxis(); + + /* A sphere beside the apex can intersect the cone's side without + containing the apex. For example, at (1.5, 0, 0) the distance to the + side of a 120-degree cone is 1.5*cos(60 degrees) = 0.75. */ + CORRADE_COMPARE(Intersection::sphereCone(sphereCenter, radius, + Math::Vector3{}, normal, angle), data.intersects); + CORRADE_COMPARE(Intersection::sphereCone(sphereCenter, radius, + Math::Vector3{}, normal, sinAngle, T(1) + tanAngle*tanAngle), data.intersects); + + /* The view-based overload should agree, also after rotating and + translating the cone and the sphere together. */ + const Math::Vector3 origin{T(1), T(-2), T(3)}; + const Math::Vector3 rotatedNormal = Math::Vector3::yAxis(); + const Math::Vector3 transformedCenter = origin + + Math::Vector3{sphereCenter.x(), sphereCenter.z(), -sphereCenter.y()}; + const Math::Matrix4 view = Math::Matrix4::lookAt(origin, + origin + rotatedNormal, Math::Vector3::zAxis()).invertedRigid(); + CORRADE_COMPARE(Intersection::sphereCone(transformedCenter, radius, + origin, rotatedNormal, angle), data.intersects); + CORRADE_COMPARE(Intersection::sphereCone(transformedCenter, radius, + origin, rotatedNormal, sinAngle, T(1) + tanAngle*tanAngle), data.intersects); + CORRADE_COMPARE(Intersection::sphereConeView(transformedCenter, radius, + view, angle), data.intersects); + CORRADE_COMPARE(Intersection::sphereConeView(transformedCenter, radius, + view, sinAngle, tanAngle), data.intersects); +} + void IntersectionTest::sphereConeView() { const Vector3 center{1.0f, -2.0f, 1.3f}; const Vector3 normal{Vector3{0.5f, 1.0f, 2.0f}.normalized()};