【问题标题】:Unable to find solution of a ray colliding a list of circles无法找到射线碰撞圆列表的解决方案
【发布时间】:2023-01-13 19:06:25
【问题描述】:

我正在编写一种计算直线和圆的交点的方法,作为编写某种光线投射演示的第一步。如果计算出交点,它会获得到将成为碰撞点的两个交点的最短距离,然后它会重复新线从碰撞点开始的过程。

我被这个视频激励了of a laser hitting different circles.

该方法从 JavaFX 接收直线的角度、起点、窗口大小、圆的半径、圆心数组和 GraphicsContext 对象。

该方法有几个布尔值来确定是否发生了碰撞,还有一个 ArrayList 来存储稍后将在 JavaFX Canvas 上绘制的碰撞。

在 while 循环中,直线方程定义为 y = m*x + b。然后检查哪些圆的圆心和线之间的距离小于线的半径,这是用这里解释的方法计算的:math.stackexchange.com

如果到中心的距离小于半径,则会对该圆发生碰撞。据我所知,要找到直线和圆之间的交点,您需要求解方程组:y = m*x + b, (x-x1)^2 + (y-y1)^2 = r^2 ,我通过替换解决了。如果 p1*p1 >= 4*p0*p2,则这会产生具有实数解的二阶多项式方程。

到原点距离最短的解是直线最先碰到的解,也是我们问题的解。以圆心、碰撞点和原点计算出一个新的角度。这样就定义了一条新线并重复循环,直到没有计算出与圆圈的碰撞,即计算与窗口边界的碰撞的情况。

最后,for 循环绘制所有定义为 collisionList 中的点对的线。

这是代码,我已尽力对其进行评论:

    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));
    }

我的主要问题是到圆的最短距离似乎没有正确计算,如果其余代码正常工作,这将直接造成困难。

我尝试了不同的方法来找到最短距离,这是我最喜欢的方法,因为我发现它很容易理解,但是实现不正常。我认为这可能是因为 JavaFX 坐标系(x 向右增加,y 向底部增加)但我不确定,此时我有点迷茫。

谢谢你的时间。

编辑: 正如建议的那样,我添加了一些额外的代码以促进可重复性。

Point 和 Vector 类定义如下:

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()));
    }
    
}

以及碰撞后获取新角度的方法:

    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());
    }

应检测碰撞的不同角度的代码示例:

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);


【问题讨论】:

  • 您应该在这里为我们提供更多代码:我建议您创建一个minimal reproducible example,您实际上并不在屏幕上绘图,而只是进行计算。不要忘记添加输入和预期输出。请删除所有注释掉的代码 (!) - 您当然可以保留实际的 cmets。
  • @cyberbrain 我已经用执行代码和返回一些结果所需的一切更新了代码。

标签: java math javafx collision-detection


【解决方案1】:

如果您只想知道直线是否与给定的圆相交,请创建第二条线,它起源于给定圆的中心,方向是初始线旋转 90 度的方向。然后计算两条线的交点。如果交点与圆心之间的距离小于半径,则两者相交。

前一段时间我写了一个小的几何库,我去掉了与你相关的部分,这是我的代码:

线类

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());
    }
}

交集函数

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();
}

测试用例

@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);
}

我没有添加 CircleVector2DPoint2D 类,因为它们很简单。 PointResult 类只是一个列表。

【讨论】:

  • 我需要交点来计算碰撞后的反射角和新线。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-03
  • 2018-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-11
  • 1970-01-01
相关资源
最近更新 更多