Home > OS >  Unable to find solution of a ray colliding a list of circles
Unable to find solution of a ray colliding a list of circles

Time:01-13

I am coding a method that calculates the intersection of a line and a circle as a first step to write some kind of ray casting demo. In case an intersection is calculated it gets the shortest distance to the two points of intersection that will be the collision point, then it repeats the process where the new line originates from the collision point.

I was motivated by this video of a laser hitting different circles.

The method receives the angle of the line, the point where it originates, the size of the window, the radius of the circles, the array of centers of the circles and the GraphicsContext object from JavaFX.

The method has a couple of booleans to determine whether a collision has been made or not, and an ArrayList to store the collisions that will be later drawn on a JavaFX Canvas.

Inside a while loop the equation of the line is defined with the form y = m*x b. Then checks which of the circles has a distance between the circle center and the line smaller than the radius of the line, this is calculated with the method explained here: math.stackexchange.com.

In case the distance to the center is smaller than the radius a collision occurs against that circle. As far as I know to find the intersection between a line and a circle you need to solve the equation system: y = m*x b, (x-x1)^2 (y-y1)^2 = r^2, that I solved via substitution. This results in a second degree polinomial equation that has a real solution if: p1*p1 >= 4*p0*p2.

The solution with the shortest distance to the origin point is the one that the line hits first and is the solution to our problem. A new angle is calculated with the center of the circle, the collision point and the origin point. With this a new line is defined and the loop repeats until no collision against the circles is calculated, situation where the collision against the borders of the window is calculated.

At the end a for loop draws all of the lines defined as couples of points inside collisionList.

This is the code, I've tried to comment it as best as I could:

    private void extendPoint(double angle, Point origin, double x, double y, double radius, ArrayList<Point> pointList) {
        
        double newAngle = angle; //Angle that defines the direction of the line
        
        //This is used if the line does not hit a circle
        double angle11 = Math.atan2(origin.getY(), origin.getX());
        double angle_11 = Math.atan2(origin.getY(), -origin.getX());
        double angle_1_1 = angle11   Math.PI;
        double angle1_1 = angle_11   Math.PI;

        boolean noCollision = true; //Will be true if the line does not hit a circle
        boolean repeat = true; //If no collision has been made the while loop stops with this
        Point currentPoint = Point.copy(origin); // (x0, y0)
        Point collision = new Point(-1,-1); //Stores the collision point
        Point newDirection = new Point(-1,-1); //Stores the new direction after a collision, returns(magnitud, angle) of a vector
        ArrayList <Point> collisionList = new ArrayList<>(); //ArrayList of collision points that will be drawn later
        collisionList.add(origin); //The origin point is added as a collision for representation purposes
        
        while(repeat == true) {
            //Line equation that passes through a point with an angle
            //y = a*x - a*x0   y0; -> y = m*x   b;
            double m = Math.tan(-newAngle);
            double a = m;
            double b = -m*currentPoint.getX()   (currentPoint.getY());

            for(int i = 0; i < pointList.size(); i  ) {
                Point gridPoint = pointList.get(i); //(x1, y1)
                
                //From: https://math.stackexchange.com/questions/2552687/distance-between-line-and-point
                //Given a line defined as A*x   B*y   C = 0 
                //x*(y1-y0) y*(x1-x0) (-y0*(x1-x0)-x0*(y1-y0)
                double A = gridPoint.getY()-currentPoint.getY();
                double B = gridPoint.getX()-currentPoint.getX(); 
                double C = -currentPoint.getY()*B   currentPoint.getX()*A;
//              double d_cp_gp = Math.abs(m*gridPoint.getX()-b*(gridPoint.getY()))/(Math.sqrt(m*m   1));
                double d_cp_gp = Math.abs(A   B   C)/Math.sqrt(A*A   B*B);

                if(d_cp_gp < radius) {
                    System.out.println("radio "   d_cp_gp);
                    //The intersection between a line and a circunference:
                    //Circunference: (x-x1)^2   (y-y1)^2 = r^2
                    //Line: y = tan(alpha)*(x-x0) y0 -> y = a*x   b; a = tan(alfa), b = -tan(alfa)*x0   y0
                    //Substituting the line equation in the circunference equation:
                    //x^2*(1 a^2)   x*(-2x1   2*a*b)   2*a*b   x1^2 b^2-r^2 = 0
                    double p2 = 1   a*a;
                    double p1 = -2*gridPoint.getX()   2*a*b;
                    double p0 = gridPoint.getX()*gridPoint.getX()   b*b - radius*radius;
                    double p0_ = 4*p0*p2;
                    System.out.println(p1*p1   " "   p0_);
                    //Check if the second order equation has solutions
                    if(p1*p1 >= p0_) {
                        System.out.println("IT HAS SOLUTION");
                        //Solution
                        double root = Math.sqrt(p1*p1 - p0_);
                        double sol1x = (-p1   root)/(2*p2);
                        double sol2x = (-p1 - root)/(2*p2);
                        double sol1y = a*sol1x - a*currentPoint.getX()   currentPoint.getY();
                        double sol2y = a*sol1x - a*currentPoint.getX()   currentPoint.getY();
                        
                        //The line will intersect twice with the circle, we want the solution
                        //with the shortest distance to currentPoint (x0,y0)
                        double distSol1 = Math.sqrt(Math.pow(currentPoint.getX()- sol1x, 2)   
                                Math.pow(currentPoint.getY() - sol1y, 2));
                        double distSol2 = Math.sqrt(Math.pow(currentPoint.getX()- sol2x, 2)   
                                Math.pow(currentPoint.getY() - sol2y, 2));
                        
                        //The collision point is the point that the line hits first
                        if(distSol1 < distSol2) {
                            collision.setXY(sol1x, sol1y);
                        }
                        else {
                            collision.setXY(sol2x, sol2y);
                        }

                        //newAngle returns a vector with the form (magnitude, angle)
                        newDirection = newAngle(currentPoint, gridPoint, collision, radius);
                        currentPoint = collision;
                        
                        //The new line after the collision is defined here
                        m = Math.tan(-newDirection.getY());
                        a = m;
                        b = -m*collision.getX()   (collision.getY());
                        collisionList.add(collision);
                        System.out.println("A collision has been calculated successfully: "   collision.toString());
                        
                        //If a collision
                        noCollision= false;
                    }
                }

                //If no collisions have been detected at the end of the for loop exit the while loop
                if(i == pointList.size() - 1 && noCollision == true) {
                    repeat = false;
                }
            }
            //If no collision has been calculated with the circles this
            //calculates the collision with the limits of the window
            if(noCollision == true && repeat == false) {

                if(angle<angle11 || angle > angle1_1) {
                    collision.setXY(x, m*x   b);
                }
                else if(angle > angle11 && angle < angle_11){
                    collision.setXY((0 - b)/m, 0);
                }
                else if(angle > angle_11 && angle < angle_1_1) {
                    collision.setXY(0, m*0   b);
                }
                else if(angle> angle_1_1 && angle < angle1_1) {
                    collision.setXY((y - b)/m, y);
                }

                collisionList.add(collision);
            }
            
        }
        
        System.out.println("Number of collisions: "   (int)(collisionList.size() - 1));
    }

My main problem is that the shortest distance to a circle doesn't seem to be calculated properly, which directly difficults if the rest of the code works properly.

I've tried different methods to find the shortest distance and this is the one that I liked the most as I find it easy to understand, however the implementation doesn't work properly. I've thought that this could be because of JavaFX coordinate system (x increases to the right and y to the bottom) but I'm not sure, I'm a bit lost at this point.

Thanks for your time.

Edit: As suggested I am adding some extra code to facilitate reproducibility.

The Point and Vector classes are defined as follows:

public class Point {
    private double x;
    private double y;
    
    public Point(double x, double y) {
        this.x = x;
        this.y = y;}
    public double getX() {
        return x;}
    public double getY() {
        return y;}
    public void setX(double x) {
        this.x = x;}
    public void setY(double y) {
        this.y = y;}
    public void setXY(double x, double y) {
        this.x = x;
        this.y = y;}
    
    @Override
    public String toString() {
        return("("   this.x   ","   this.y   ")");
    }
    public static Point copy(Point a) {
        return new Point(a.getX(), a.getY());
    }
}
public class Vector {
    private double vx; 
    private double vy; 
    private double ptoApX; 
    private double ptoApY; 
    private double angle;
    private double modulo;
    
    
    public Vector(double vx, double vy) {
        this.vx = vx;
        this.vy = vy;
        this.ptoApX = 0;
        this.ptoApY = 0;
        this.angle = angle(vx,vy);
        this.modulo = modulo(vx,vy);
    }
    //Getters
    public double getVx() {
        return this.vx;
    }
    public double getVy() {
        return this.vy;
    }
    public double getPtoApX() {
        return this.ptoApX;
    }
    public double getPtoApY() {
        return this.ptoApY;
    }
    public double getAngle() {
        return this.angle;
    }
    public double getModulo() {
        return this.modulo;
    }
    
    //Setters
    public void setVx(double vx) {
        this.vx = vx;
    }
    public void setVy(double vy) {
        this.vy = vy;
    }
    public void setPtoApX(double ptoApX) {
        this.ptoApX = ptoApX;
    }
    public void setPtoApY(double ptoApY) {
        this.ptoApY = ptoApY;
    }
    public void setAngle(double angle) {
        this.angle = angle;
    }
    public void setModulo(double modulo) {
        this.modulo = modulo;
    }
    
    //To String
    @Override
    public String toString() {
        return "(" this.getVx() "," this.getVy() ")";
    }
    
    
    public static double dotProduct(Vector a, Vector b) {
        return a.getVx()*b.getVx()   a.getVy()*b.getVy();
    }
    public static Vector escalarProduct(Vector v, double n) {
        return new Vector(n*v.getVx(), n*v.getVy());
    }
    public static Vector vectorWith2Points(Point a, Point b) {
        Point p = Point.resta(a,b);
        return new Vector(p.getX(),p.getY());
    }
    public static Vector vectorPointAngle(Point a, double angle, double modulo) {
        double angleRadians = Math.toRadians(angle);
        Point b = new Point(Math.cos(angleRadians)*modulo, Math.sin(angleRadians)*modulo);
        return vectorWith2Points(a,b);
    }
    public static double modulo(double vx, double vy) {
        return Math.sqrt(vx*vx   vy*vy);
    }
    public static double angle(double vx, double vy) {
        return Math.atan2(vy, vx);
    }
    public static Vector normalize(Vector v) {
        return new Vector(v.getVx()/v.getModulo(),v.getVy()/v.getModulo());
    }
    public static double angle2vectors(Vector u, Vector v) {
        double argument = dotProduct(u,v)/(u.getModulo()*v.getModulo());
        return Math.acos(argument);
    }
    public static Point polar2cart(double r, double angle) {
        return new Point(r*Math.cos(angle), r*Math.sin(angle));
    }
    public static Point cart2polar(Point p) {
        return new Point(modulo(p.getX(), p.getY()), angle(p.getX(), p.getY()));
    }
    
}

And the method to obtain the new angle after a collision:

    private Point newAngle(Point origin, Point center, Point c, double radius) {
        //Normal vector
        Vector n = Vector.vectorWith2Points(c, center);
        Vector nNorm = Vector.normalize(n);

        //Incident vector
        Vector d = Vector.vectorWith2Points(c, origin);
        //Tangent vector
        Vector tg = new Vector(-nNorm.getVy(), nNorm.getVx());
        
        //Reflected vector
        double product = Vector.dotProduct(d,tg);
        Vector r = new Vector(d.getVx()-2*product*tg.getVx(),
                                    d.getVy() - 2*product*tg.getVy());
        return new Point(r.getModulo(), r.getAngle());
    }

An example of the code of different angles where a collision should be detected:

double x = 600;
double y = 400;
double radius = 10;
ArrayList<Point> pointList = new ArrayList<>();
pointList.add(new Point(40,40));
pointList.add(new Point(500,100));
pointList.add(new Point(40,330));
pointList.add(new Point(450,300));

//This should return a solution
extendPoint(0.4363323129985824, origin, x, y, radius, pointList);
extendPoint(2.6179938779914944, origin, x, y, radius, pointList);


//this returns a solution when it should not
extendPoint(1.5707963267948966, origin, x, y, radius, pointList);
extendPoint(-1.5707963267948966, origin, x, y, radius, pointList);


CodePudding user response:

I think you should create a class that represents an intersection by a ray.

class Intersection{
    double distance;
    Point loc;
    double normal;
}

That way, distance is along the ray and normal is the normal of the object intersected.

Then I would have a method for finding the intersetion of a circle and a point.

List<Intersection> lineAndCircle( Point org, double angle, Point center, double radius){...}

You seem to have a similar method but you're doing more work in it.

Then you also want to check the edge of the screen.

Intersection lineAndBoundary( Point org, double angle){ ... }

You have a very similar method, but you seem to be doing a lot more work in the method. . This way you are testing separate methods. Then your algorithm works as.

1 go through circles and find intersections.
2 get the intersection with the boundary.
3 find the closest intersection ( the smallest distance greater than 0 )

Doing it this way makes it a bit more extensible. First our ray is re-used a lot. Lets make a class.

class Ray{
    Point origin;
    double angle;
}

Then we collide a ray with multiple objects.

interface Interceptable{
    List<Intersection> intercepts(Ray r);
}

Then we can use different classes.

class Circle implements Interceptable{
    Point pos;
    double radius;
    @Override
    List<Intersection> collides(Ray r){
        ...
    }
}

Now you can right collides and testable.

Circle a = new Circle( new Point( 40, 40 ), 5 );

List<Intersection> yes = a.collides( new Ray( new Point(0, 0), 3.14/4 ) );
List<Intersection> no = a.collides( new Ray( new Point(0, 0), 0) ) );

Then you can narrow your example down to. "How do I write a collide method?" or "Why doesn't my collide method work for this ray/circle pair? I expect it to hit at two points, but it misses." etc.

CodePudding user response:

If you only want to know if the line intersects if a given circle, create a second line which originates at the center of the given circle and the direction is the direction of your initial line rotated by 90 degrees. Then compute the intersection of the two lines. If then the distance between the intersection point and the center of the circle is smaller then the radius, both intersect.

A while ago I wrote a small Geometry lib, I striped out the sections which are relevant for you, here is my code:

Line class

public class Line {
    final Vector2D positionVector;
    final Vector2D directionVector;

    public Line(final Vector2D positionVector, final Vector2D directionVector) {
        this.positionVector = positionVector;
        this.directionVector = directionVector;
    }

    public OptionalDouble computeIntersection(final Line line) {
        final double numerator = line.getPositionVector().subtract(this.positionVector).cross(this.directionVector);
        final double denominator = this.directionVector.cross(line.directionVector);

        if (Math.abs(numerator) < 1e-10 && Math.abs(denominator) < 1e-10) {
            // collinear
            return OptionalDouble.of(Double.POSITIVE_INFINITY);
        } else if (Math.abs(denominator) < 1e-10) {
            // parallel
            return OptionalDouble.empty(); // Lines are parallel.
        }

        final double t = line.getPositionVector().subtract(this.positionVector).cross(line.directionVector) / denominator;
        return OptionalDouble.of(t);
    }

    public Vector2D getPositionVector() {
        return positionVector;
    }

    public Vector2D getDirectionVector() {
        return directionVector;
    }

    public Point2D getClosestPointOnLine(final Point2D point) {
        final Line line = new Line(new Vector2D(point.getX(), point.getY()), this.directionVector.turn90DegreeClockwise());
        final OptionalDouble intersection = this.computeIntersection(line);
        final Vector2D result = this.positionVector.add(this.directionVector.lerp(intersection.getAsDouble()));
        return new Point2D(result.getX(), result.getY());
    }
}

intersection function

public static PointResult intersection(final Line l1, final Circle c1) {
    final Point2D intersection = l1.getClosestPointOnLine(c1.getCenter());
    final double dist = intersection.distance(c1.getCenter());
    if (Math.abs(dist - c1.getRadius()) < 1e-10) {
        final List<Point2D> result = new LinkedList<>();
        result.add(intersection);
        return new PointResult(Collections.unmodifiableList(result));
    } else if (dist < c1.getRadius()) {
        // we have two points
        final double adjacentLeg = Math.sqrt(c1.getRadius() * c1.getRadius() - dist * dist);
        final Point2D pt1 = intersection.pointAt(l1.getDirectionVector().angle(), adjacentLeg);
        final Point2D pt2 = intersection.pointAt(l1.getDirectionVector().angle()   Math.PI, adjacentLeg);

        final List<Point2D> result = new LinkedList<>();
        result.add(pt1);
        result.add(pt2);
        return new PointResult(Collections.unmodifiableList(result));
    }
    return new PointResult();
}

TestCase

@Test
void testIntersectionLineCircleTwoPoints() {
    final Point2D ptCircleCenter = new Point2D(2.0, 5.0);
    final Point2D ptLineCircleIntersection = new Point2D(5.0, 2.0);

    final Point2D pt1 = new Point2D(3.0, 0.0);
    final Point2D pt2 = new Point2D(7.0, 4.0);

    final double a = Math.sqrt((2.0 * 2.0)   (2.0 * 2.0));
    final double b = ptCircleCenter.diff(ptLineCircleIntersection).norm();
    final double radius = Math.sqrt((a * a)   (b * b));

    final Line l1 = new Line(pt1, pt2);
    final Circle circle = new Circle(ptCircleCenter, radius);

    PointResult intersection = GeometryOperation.intersection(l1, circle);

    assertTrue(intersection.getPoints().isPresent());
    assertEquals(2, intersection.getPoints().get().size());

    assertEquals(7.0, intersection.getPoints().get().get(0).getX(), 1e-10);
    assertEquals(4.0, intersection.getPoints().get().get(0).getY(), 1e-10);

    assertEquals(3.0, intersection.getPoints().get().get(1).getX(), 1e-10);
    assertEquals(0.0, intersection.getPoints().get().get(1).getY(), 1e-10);
}

I did not add the Circle, Vector2D and Point2D class because they are trivial. And the class PointResult is just a list.

  • Related