Введение
Уже не раз на habr затрагивалась данная тема: раз, два.
Я лишь хочу поделиться своей реализацией. Думаю, это кому-то пригодится.
Предупреждаю, данный метод не учитывает некоторые пограничные случаи и вхождение отрезков друг в друга.
Подготовка
Весь последующий код будет приведён на языке Java
Для начала создадим вспомогательный класс Dot. В нём всего две переменные x и y:
static class Dot {
float x,y;
Dot(float x, float y){
this.x =x;
this.y = y;
}
@Override
public String toString() {
return "x:"+x+" y:"+y;
}
}
И класс Vector2. В нём всего две функции: crs (cross) - векторное произведение, scl (scale) - масштабирование вектора.
public static class Vector2 {
float x,y;
public Vector2(Dot d1, Dot d2) {
this.x = d2.x-d1.x;
this.y = d2.y-d1.y;
}
/** Calculates the 2D cross product between this and the given vector.
* @param v2 the other vector
* @return the cross product (Z vector) */
public static float crs(Vector2 v1, Vector2 v2) {
return v1.x * v2.y - v1.y * v2.x;
}
/** Multiplies this vector by a scalar */
public Vector2 scl (float scalar) {
x *= scalar;
y *= scalar;
return this;
}
}
Задача 1: определить, касаются ли отрезки.
Решение:
Из векторного произведения мы можем узнать, обхо́дим мы вектор по часовой или против часовой. Это поможет понять где находятся точки. Соединим точки A и D, A и C.
Перемножим вектора main и v2, main и v1. Если полученные произведения имеют разные знаки, значит точки C и D находятся по разные стороны относительно отрезка AB. Назовём такой метод fromDifferentSides:
public static boolean fromDifferentSides(Vector2 main, Vector2 v1, Vector2 v2){
float product1 =crs(main ,v1), product2 = crs(main ,v2);
return (product1>=0&&product2<=0 || product1<=0&&product2>=0);
}
Таким же образом необходимо проверить точки A и B относительно отрезка CD. Соединим это в один метод и получится следующее:
public static boolean linesIntersect(Dot a, Dot b, Dot c, Dot d) {
Vector2 main = new Vector2(a,b);
Vector2 v1 = new Vector2(a,c);
Vector2 v2 = new Vector2(a,d);
if (fromDifferentSides(main,v1,v2)) {
main = new Vector2(c,d);
v1 =new Vector2(c,a);
v2 =new Vector2(c,b);
return fromDifferentSides(main,v1,v2);
}
return false;
}
Задача 2: определить точку касания.
Решение:
Как и в предыдущей задаче определяем, касаются ли отрезки. Если касаются, начинаем определять эту точку. Через подобие найдём длину DO. Коэффициент подобия (k) равен отношению уже известных нам векторных произведений:
//коэффициент подобия
double k = Math.abs(product2 / product1);
if (Float.isInfinite((float) k)) return c;
В третей строчке проверяем пограничный случай. Теперь узнаем чему равно DO. Для этого решим систему уравнений:
DO = CO*k
Подставляем во второе уравнение:
CO*k+CO = CD
CO(k+1)= CD
CO = CD/(k+1)
В итоге:
DO = (CD/(k+1))*k
Теперь создаём вектор CD и уменьшаем его до длинны DO. Но поскольку мы будем его умножать, нужно взять обратное от (k+1)*k:
//вектор DC
Vector2 dc = new Vector2(d, c);
//уменьшаем вектор
dc.scl((float) (1 / (k + 1)*k));
Теперь осталось добавить вектор к точке D:
//добавляем вектор к точке
return new Dot(d.x + dc.x, d.y + dc.y);
Вот и всё! Мы получили заветную точку.
Предупреждаю, данный метод не учитывает некоторые пограничные случаи и вхождение отрезков друг в друга.
Функцию нахождения точки я назвал pointOfIntersection. Привожу полный код на Java:
public class Main {
public static void main(String[] args) {
System.out.println(pointOfIntersection(new Dot(0, 0),new Dot(5, 0), new Dot(2, 3),new Dot(2, 0) ));
}
public static Dot pointOfIntersection(Dot a, Dot b, Dot c, Dot d) {
Vector2 main = new Vector2(c,d);
Vector2 v1= new Vector2(c,a);
Vector2 v2 = new Vector2(c,b);
if (fromDifferentSides(main,v1,v2)) {
main = new Vector2(a,b);
v1 = new Vector2(a,c);
v2 = new Vector2(a,d);
double product1 = crs(main ,v1), product2 = crs(main ,v2);
if (product1>=0&&product2<=0 || product1<=0&&product2>=0) {
//коэффициент подобия
double k = Math.abs(product2 / product1);
if (Float.isInfinite((float) k)) return c;
//вектор DC
Vector2 dc = new Vector2(d, c);
//уменьшаем вектор
dc.scl((float) (1 / (k + 1)*k));
//добавляем вектор к точке
return new Dot(d.x + dc.x, d.y + dc.y);
}
}
return null;
}
public static boolean linesIntersect(Dot a, Dot b, Dot c, Dot d) {
Vector2 main = new Vector2(a,b);
Vector2 v1 = new Vector2(a,c);
Vector2 v2 = new Vector2(a,d);
if (fromDifferentSides(main,v1,v2)) {
main = new Vector2(c,d);
v1 =new Vector2(c,a);
v2 =new Vector2(c,b);
return fromDifferentSides(main,v1,v2);
}
return false;
}
static class Dot {
float x,y;
Dot(float x, float y){
this.x =x;
this.y = y;
}
@Override
public String toString() {
return "x:"+x+" y:"+y;
}
}
public static class Vector2 {
float x,y;
public Vector2(Dot d1, Dot d2) {
this.x = d2.x-d1.x;
this.y = d2.y-d1.y;
}
/** Calculates the 2D cross product between this and the given vector.
* @param v2 the other vector
* @return the cross product (Z vector) */
public static float crs(Vector2 v1, Vector2 v2) {
return v1.x * v2.y - v1.y * v2.x;
}
/** Multiplies this vector by a scalar */
public Vector2 scl (float scalar) {
x *= scalar;
y *= scalar;
return this;
}
public static boolean fromDifferentSides(Vector2 main, Vector2 v1, Vector2 v2){
float product1 =crs(main ,v1), product2 = crs(main ,v2);
return (product1>=0&&product2<=0 || product1<=0&&product2>=0);
}
//функция округления
public static double round(double value, int places) {
if (places < 0) throw new IllegalArgumentException();
BigDecimal bd = new BigDecimal(Double.toString(value));
bd = bd.setScale(places, RoundingMode.HALF_UP);
return bd.doubleValue();
}
}
}
Надеюсь, вам понравилось. Удачи!
Комментарии (8)
KaneUA
19.09.2021 04:05+3Предупреждаю, данный метод не учитывает некоторые пограничные случаи
Какие?
и вхождение отрезков друг в друга.
Почему?
И почему часть комментариев на русском, а часть на английском?
technic93
19.09.2021 05:05+2Задача о нахождении отрезков не такая простая как кажется на первый взгляд, легко пропустить какой-то крайний случай, так что советую покрыть ваше решение тестами. К оформлению решения есть ряд замечаний. Код оформлен небрежно, в одном месте есть пробел перед
=
в другом нет, пробелы иногда перед запятой а не после (!) - надо бы использовать какой-то линтер. Названия методовcrs
иscl
слишком короткие. Вместо названия fromDifferentSides по смыслу больше подходит atDifferentSides. Последнюю простыню кода можно убрать под спойлер. По окончанию статьи так и не понятно какой мне метод брать, ваш или какой-то из двух других по ссылкам, или в комментариях по ссылкам? :) В любом случае на фоне засилия гугло-переводов на хабре, всегда приятно видеть когда человек пытается разобраться сам.Tiriet
19.09.2021 09:12+14Кмк, на фоне качества выбранного алгоритма недостатки оформления теряются, как муха на фоне слона.
Ведь есть же гораздо более простой и эффективный метод: точка пересечения одновременно удовлетворяет уравнениям двух прямых
Line1: C + alfa1*(D-C)
Line2: A + alfa2*(B-A)
alfa1*(D.x-C.x) - alfa2*(B.x-A.x) = A.x-C.x
alfa1*(D.y-C.y) - alfa2*(B.y-A.y) = A.y-C.y
И все, что требуется для нахождения пересечения двух отрезков- это убедиться, что эта система имеет решение и хотя-бы одно из этих решений удовлетворяет условия alfa1, alfa2 in [0.0 .. 1.0 ]
Решение единственное, если (D.x-C.x)*(B.y-A.y)-(D.y-C.y)*(B.x-A.x) <> 0. надо проверить, что alfa1,alfa2 удовлетворяют требованиям 0.0<=a<=1.0.
Отрезки коллинеарные, если Det = (D.x-C.x)*(B.y-A.y)-(D.y-C.y)*(B.x-A.x) = 0, надо проверить, что один отрезок лежит целиком левее или целиком правее другого. если да- то пересечения нет, если нет- то отрезки перекрываются и пересечений много.
Такое решение реализуется в виде простой функции, не требует создания кучи объектов, не грузит гарбажколлектор, и укладывается в 20 строк кода. А тут - куски решения раскиданы по пяти методам, каждый из которых делает непойми что и непойми зачем, а на выходе еще и "пограничные случаи". (Пограничные случаи- это когда Abs(Det) < eps=1,0e-12*(L2_norm(A-B)+L2_norm(C-D)) - но пересечение отрезков на float-числах- при малых eps- это несколько лотерея или тема отдельной большой статьи).
Tiriet
19.09.2021 09:25https://habr.com/ru/post/523440/#comment_22182564
чуть меньше года назад размещено на хабре.
GeMir
19.09.2021 09:41Раз уж скриншоты из GeoGebra, а она open source, не стоит ли для начала заглянуть «под капот» оной и посмотреть на имеющееся решение?
DistortNeo
19.09.2021 14:53+2Думаю, это пригодится разработчикам игр.
Исходя из того, что
Предупреждаю, данный метод не учитывает некоторые пограничные случаи и вхождение отрезков друг в друга.
Крайне сомневаюсь в том что это будет иметь практическую ценность.
Ведь именно корректная обработка пограничных случаев и представляет основной интерес.
RigelGL
19.09.2021 18:04Приятно видеть попытки разобраться в линейной алгебре на плоскости, это поможет не только в разработке игр, но и при создании разметки изображений.
А теперь расскажу о простом для понимания, но несколько дорогом для cpu (задействуется извлечение корня) алгоритме о пересечении отрезков. Поехали.
Первым делом зададим точки A, B, C, D на двумерной плоскости, А и В задают первый отрезок, C и D - второй.
Основная идея - привести систему к вырожденному случаю, когда точка А' имеет координаты (0, 0), а точка В' лежит на оси ОХ.
Первым делом вычтем из точек В, С и D точку А, так мы достигнем первого условия. Теперь нужно повернуть точки С и D вокруг точки А (точку В поворачивать не нужно, её координаты будут (length(A, B), 0)). Для этого воспользуемся матрицей поворота, для неё нужны синус и косинус угла линии AB, благо их легко вычислить, зная длину AB и расстояние по оси ОХ и ОY между точками А и В.
После умножения точек С и D на матрицу поворота мы получим С' и D' в локальных координатах отрезка АВ.
Задача о пересечении становится тривиальной:
Точки С' и D' с разных сторон оси ОХ, или одна из точек близка к отрезку А'В' на некоторую эпсилон.
Теперь когда точки С' и D' с разных сторон оси ОХ мы можем найти точку пересечения С'D' и ОХ:
Найдем расстояние по ОХ между С' и D' (cdLy)
Найдём параметр alpha, поделив координату Y точки С' на расстояние по OY точек С' и D'.
Найдём координату X точки М' (пересечение), помножив cdLy на alpha. Координата X должна оказаться больше или равна 0 и меньше или равна длине АВ, в противном случае отрезки не пересекаются.
Вернём точку М' в мировое пространство, помножив её на инверсную матрицу поворота (в данном случае нужно всего лишь поменять знаки). По факту здесь можно умножить кординату X точки М' на синус и косинус угла линии АВ.
После этого к точке М нужно прибавить координаты точки А.
Весьма долгий путь, зато общий. Так можно найти расстояние от отрезка до точки (а заодно самую ближайшую точку на отрезке к заданной точке), то же можно провернуть с лучом или линией, приведя их к форме отрезка и убрав проверки на ось ОY и/или длину АВ.
Подобный подход можно использовать для пересечения отрезка и треугольника в трёхмерном пространстве. В таком случае пусть треугольник будет АВС, а отрезок DE. Аналогично нужно из точек B, C, D и Е отнять точку А и помножить на матрицу треугольника АВС (тангент - сторона АВ, нормаль - векторное произведение АВ и АС, битангент - тангент х нормаль, получится матрица TBN. Все вектора T, B, N нужно нормализовать). После этого задача опять тривиальна: точка А' лежит в начале координат, точка В' на оси ОХ, а точка С' выше оси ОХ. Найти точку пересечения в этом случае тривиально, описывать не стану.
mvv
На фоне новостей о Google, Apple и "беспринципном негодяе" Дурове - блестящая статья!