Circle line-segment collision detection algorithm?

前端 未结 28 1376
被撕碎了的回忆
被撕碎了的回忆 2020-11-22 06:38

I have a line from A to B and a circle positioned at C with the radius R.

\"Image\"

What is a good alg

28条回答
  •  灰色年华
    2020-11-22 07:04

    Here is a solution written in golang. The method is similar to some other answers posted here, but not quite the same. It is easy to implement, and has been tested. Here are the steps:

    1. Translate coordinates so that the circle is at the origin.
    2. Express the line segment as parametrized functions of t for both the x and y coordinates. If t is 0, the function's values are one end point of the segment, and if t is 1, the function's values are the other end point.
    3. Solve, if possible, the quadratic equation resulting from constraining values of t that produce x, y coordinates with distances from the origin equal to the circle's radius.
    4. Throw out solutions where t is < 0 or > 1 ( <= 0 or >= 1 for an open segment). Those points are not contained in the segment.
    5. Translate back to original coordinates.

    The values for A, B, and C for the quadratic are derived here, where (n-et) and (m-dt) are the equations for the line's x and y coordinates, respectively. r is the radius of the circle.

    (n-et)(n-et) + (m-dt)(m-dt) = rr
    nn - 2etn + etet + mm - 2mdt + dtdt = rr
    (ee+dd)tt - 2(en + dm)t + nn + mm - rr = 0
    

    Therefore A = ee+dd, B = - 2(en + dm), and C = nn + mm - rr.

    Here is the golang code for the function:

    package geom
    
    import (
        "math"
    )
    
    // SegmentCircleIntersection return points of intersection between a circle and
    // a line segment. The Boolean intersects returns true if one or
    // more solutions exist. If only one solution exists, 
    // x1 == x2 and y1 == y2.
    // s1x and s1y are coordinates for one end point of the segment, and
    // s2x and s2y are coordinates for the other end of the segment.
    // cx and cy are the coordinates of the center of the circle and
    // r is the radius of the circle.
    func SegmentCircleIntersection(s1x, s1y, s2x, s2y, cx, cy, r float64) (x1, y1, x2, y2 float64, intersects bool) {
        // (n-et) and (m-dt) are expressions for the x and y coordinates
        // of a parameterized line in coordinates whose origin is the
        // center of the circle.
        // When t = 0, (n-et) == s1x - cx and (m-dt) == s1y - cy
        // When t = 1, (n-et) == s2x - cx and (m-dt) == s2y - cy.
        n := s2x - cx
        m := s2y - cy
    
        e := s2x - s1x
        d := s2y - s1y
    
        // lineFunc checks if the  t parameter is in the segment and if so
        // calculates the line point in the unshifted coordinates (adds back
        // cx and cy.
        lineFunc := func(t float64) (x, y float64, inBounds bool) {
            inBounds = t >= 0 && t <= 1 // Check bounds on closed segment
            // To check bounds for an open segment use t > 0 && t < 1
            if inBounds { // Calc coords for point in segment
                x = n - e*t + cx
                y = m - d*t + cy
            }
            return
        }
    
        // Since we want the points on the line distance r from the origin,
        // (n-et)(n-et) + (m-dt)(m-dt) = rr.
        // Expanding and collecting terms yeilds the following quadratic equation:
        A, B, C := e*e+d*d, -2*(e*n+m*d), n*n+m*m-r*r
    
        D := B*B - 4*A*C // discriminant of quadratic
        if D < 0 {
            return // No solution
        }
        D = math.Sqrt(D)
    
        var p1In, p2In bool
        x1, y1, p1In = lineFunc((-B + D) / (2 * A)) // First root
        if D == 0.0 {
            intersects = p1In
            x2, y2 = x1, y1
            return // Only possible solution, quadratic has one root.
        }
    
        x2, y2, p2In = lineFunc((-B - D) / (2 * A)) // Second root
    
        intersects = p1In || p2In
        if p1In == false { // Only x2, y2 may be valid solutions
            x1, y1 = x2, y2
        } else if p2In == false { // Only x1, y1 are valid solutions
            x2, y2 = x1, y1
        }
        return
    }
    

    I tested it with this function, which confirms that solution points are within the line segment and on the circle. It makes a test segment and sweeps it around the given circle:

    package geom_test
    
    import (
        "testing"
    
        . "**put your package path here**"
    )
    
    func CheckEpsilon(t *testing.T, v, epsilon float64, message string) {
        if v > epsilon || v < -epsilon {
            t.Error(message, v, epsilon)
            t.FailNow()
        }
    }
    
    func TestSegmentCircleIntersection(t *testing.T) {
        epsilon := 1e-10      // Something smallish
        x1, y1 := 5.0, 2.0    // segment end point 1
        x2, y2 := 50.0, 30.0  // segment end point 2
        cx, cy := 100.0, 90.0 // center of circle
        r := 80.0
    
        segx, segy := x2-x1, y2-y1
    
        testCntr, solutionCntr := 0, 0
    
        for i := -100; i < 100; i++ {
            for j := -100; j < 100; j++ {
                testCntr++
                s1x, s2x := x1+float64(i), x2+float64(i)
                s1y, s2y := y1+float64(j), y2+float64(j)
    
                sc1x, sc1y := s1x-cx, s1y-cy
                seg1Inside := sc1x*sc1x+sc1y*sc1y < r*r
                sc2x, sc2y := s2x-cx, s2y-cy
                seg2Inside := sc2x*sc2x+sc2y*sc2y < r*r
    
                p1x, p1y, p2x, p2y, intersects := SegmentCircleIntersection(s1x, s1y, s2x, s2y, cx, cy, r)
    
                if intersects {
                    solutionCntr++
                    //Check if points are on circle
                    c1x, c1y := p1x-cx, p1y-cy
                    deltaLen1 := (c1x*c1x + c1y*c1y) - r*r
                    CheckEpsilon(t, deltaLen1, epsilon, "p1 not on circle")
    
                    c2x, c2y := p2x-cx, p2y-cy
                    deltaLen2 := (c2x*c2x + c2y*c2y) - r*r
                    CheckEpsilon(t, deltaLen2, epsilon, "p2 not on circle")
    
                    // Check if points are on the line through the line segment
                    // "cross product" of vector from a segment point to the point
                    // and the vector for the segment should be near zero
                    vp1x, vp1y := p1x-s1x, p1y-s1y
                    crossProd1 := vp1x*segy - vp1y*segx
                    CheckEpsilon(t, crossProd1, epsilon, "p1 not on line ")
    
                    vp2x, vp2y := p2x-s1x, p2y-s1y
                    crossProd2 := vp2x*segy - vp2y*segx
                    CheckEpsilon(t, crossProd2, epsilon, "p2 not on line ")
    
                    // Check if point is between points s1 and s2 on line
                    // This means the sign of the dot prod of the segment vector
                    // and point to segment end point vectors are opposite for
                    // either end.
                    wp1x, wp1y := p1x-s2x, p1y-s2y
                    dp1v := vp1x*segx + vp1y*segy
                    dp1w := wp1x*segx + wp1y*segy
                    if (dp1v < 0 && dp1w < 0) || (dp1v > 0 && dp1w > 0) {
                        t.Error("point not contained in segment ", dp1v, dp1w)
                        t.FailNow()
                    }
    
                    wp2x, wp2y := p2x-s2x, p2y-s2y
                    dp2v := vp2x*segx + vp2y*segy
                    dp2w := wp2x*segx + wp2y*segy
                    if (dp2v < 0 && dp2w < 0) || (dp2v > 0 && dp2w > 0) {
                        t.Error("point not contained in segment ", dp2v, dp2w)
                        t.FailNow()
                    }
    
                    if s1x == s2x && s2y == s1y { //Only one solution
                        // Test that one end of the segment is withing the radius of the circle
                        // and one is not
                        if seg1Inside && seg2Inside {
                            t.Error("Only one solution but both line segment ends inside")
                            t.FailNow()
                        }
                        if !seg1Inside && !seg2Inside {
                            t.Error("Only one solution but both line segment ends outside")
                            t.FailNow()
                        }
    
                    }
                } else { // No intersection, check if both points outside or inside
                    if (seg1Inside && !seg2Inside) || (!seg1Inside && seg2Inside) {
                        t.Error("No solution but only one point in radius of circle")
                        t.FailNow()
                    }
                }
            }
        }
        t.Log("Tested ", testCntr, " examples and found ", solutionCntr, " solutions.")
    }
    

    Here is the output of the test:

    === RUN   TestSegmentCircleIntersection
    --- PASS: TestSegmentCircleIntersection (0.00s)
        geom_test.go:105: Tested  40000  examples and found  7343  solutions.
    

    Finally, the method is easily extendable to the case of a ray starting at one point, going through the other and extending to infinity, by only testing if t > 0 or t < 1 but not both.

提交回复
热议问题