Який найшвидший спосіб опрацювати перехрестя 2D обмежувальної коробки?


62

Припустимо, що кожен об'єкт Box має властивості x, y, ширину, висоту і в центрі їх походження, і що ні об'єкти, ні обмежувальні коробки не обертаються.


Це обмежувальні коробки, орієнтовані на осі або об'єкти?
tenpn

3
Коли ви задасте це питання, вам неодмінно потрібно буде перевірити інші види перехрестя в майбутньому;). Тому я пропоную СПИСОК щодо перетину об'єкта / об’єкта. У таблиці наведено перетини між усіма популярними типами об’єктів (коробки, сфери, трикутники, цикліндри, конуси, ...) у статичних та динамічних ситуаціях.
Дейв О.

2
Будь ласка, перефразуйте своє запитання до обмежуючих ректів. З моєї точки зору, поле передбачає 3d-об’єкт.
Дейв О.

Відповіді:


55

(Псевдокод C-ish - адаптуйте мовні оптимізації відповідно)

bool DoBoxesIntersect(Box a, Box b) {
  return (abs(a.x - b.x) * 2 < (a.width + b.width)) &&
         (abs(a.y - b.y) * 2 < (a.height + b.height));
}

Англійською: На кожній осі перевірте, чи є центри коробок достатньо близькими, щоб вони перетиналися. Якщо вони перетинаються на обох осях, то ящики перетинаються. Якщо вони цього не роблять, то ні.

Ви можете змінити <'s на <=, якщо ви хочете вважати дотик до краю як перетинається. Якщо ви хочете певної формули, що стосується лише дотику, ви не можете використовувати == - це підкаже, чи торкаються кути, а не якщо краї торкаються. Ви хочете зробити щось логічно рівнозначне return DoBoxesIntersectOrTouch(a, b) && !DoBoxesIntersect(a, b).

Варто зазначити, що ви можете отримати невелике, але значне збільшення швидкості, зберігаючи половину ширини та напіввисоти на додаток до (або замість) повної ширини та повної висоти. З іншого боку, рідкісне перетину 2-огрантовної коробки є вузьким місцем продуктивності.


9
Це, очевидно, передбачає, що вікна вирівнюються по осі.
tenpn

1
Абс не повинен бути особливо повільним - принаймні повільнішим, ніж умовний, принаймні, і єдиний спосіб зробити це без абс (що мені відомо) включає додаткові умови.
ZorbaTHut

4
Так, це припускає вікна, орієнтовані на осі. Описані структури не мають жодного способу вказівки на обертання, тому я вважав, що це безпечно.
ZorbaTHut

3
Ось декілька корисних порад щодо прискорення оцінювання в Actioncript (в основному ціле число calc): lab.polygonal.de/2007/05/10/bitwise-gems-fast-integer-math Я публікую це, оскільки він також містить швидший заміна на Math.abs (), яка, як правило, загальмовує речі в Actioncript (звичайно кажучи про критичні показники продуктивності).
bummzack

2
Ви пропускаєте, що вони мають походження в центрі, а не в лівому краї. На коробці, яка працює від 0 до 10, насправді буде "x = 5", а у вікні від 8 до 12 буде "x = 10". Ви закінчите з abs(5 - 10) * 2 < (10 + 4)=> 10 < 14. Вам потрібно буде зробити кілька простих налаштувань, щоб змусити його працювати з топлефтом-кутом і розміром.
ZorbaTHut

37

Це працює для двох прямокутників, вирівняних по осі X і Y.
Кожен прямокутник має властивості:
"ліворуч", х координата його лівої сторони,
"верх", y координата його верхньої сторони,
"права", х координата його правої сторони,
"нижня", координата y її нижня сторона,

function IntersectRect(r1:Rectangle, r2:Rectangle):Boolean {
    return !(r2.left > r1.right
        || r2.right < r1.left
        || r2.top > r1.bottom
        || r2.bottom < r1.top);
}

Зауважте, що це розроблено для системи координат, у якій вісь + y спрямована вниз, а вісь + x спрямована вправо (тобто типові координати екрана / пікселя). Щоб пристосувати це до типової декартової системи, в якій + y спрямована вгору, порівняння вздовж вертикальних осей буде перевернуто, наприклад:

return !(r2.left > r1.right
    || r2.right < r1.left
    || r2.top < r1.bottom
    || r2.bottom > r1.top);

Ідея полягає в тому , щоб охопити всі можливі умови , при яких прямокутники будуть НЕ перетинаються, а потім звести на немає відповіді , щоб побачити , якщо вони будуть перекриватися. Незалежно від напрямку осей, легко помітити, що два прямокутники не будуть перетинатися, якщо:

  • лівий край r2 далі правий, ніж правий край r1

     ________     ________
    |        |   |        |
    |   r1   |   |   r2   |
    |        |   |        |
    |________|   |________|
    
  • або правий край r2 далі лівий, ніж лівий край r1

     ________     ________
    |        |   |        |
    |   r2   |   |   r1   |
    |        |   |        |
    |________|   |________|
    
  • або верхній край r2 знаходиться нижче нижнього краю r1

     ________ 
    |        |
    |   r1   |
    |        |
    |________|
     ________ 
    |        |
    |   r2   |
    |        |
    |________|
    
  • або нижній край r2 вище верхнього краю r1

     ________ 
    |        |
    |   r2   |
    |        |
    |________|
     ________ 
    |        |
    |   r1   |
    |        |
    |________|
    

Оригінальну функцію - та альтернативний опис того, чому це працює - можна знайти тут: http://tekpool.wordpress.com/2006/10/11/rectangle-intersection-determine-if-two-given-rectangles-intersect- один-інший чи ні /


1
Дивно інтуїтивно і ще раз показує, що якщо знайти відповідь занадто важко, намагання знайти відповідь на протилежне питання може допомогти вам. Дякую!
Lodewijk

1
Ви повинні згадати, що вісь y спрямована вниз (як на зображенні). В іншому випадку нерівності r2.top> r1.bottom та r2.bottom <r1.top потрібно повернути.
користувач1443778

@ user1443778 хороший улов! Я пішов вперед і також легко пояснив логіку цього алгоритму координатно-агностично.
Ponkadoodle

11

Якщо ви хочете встановити об'єктивні обмежувальні поля, спробуйте цей підручник з теореми осі відокремлення метанетом: http://www.metanetsoftware.com/technique/tutorialA.html

SAT - це не найшвидше рішення, але відносно просте. Ви намагаєтеся знайти одну лінію (або площину, якщо 3D), яка відокремить ваші об'єкти. Якщо ця лінія існує, гарантовано мати паралель до краю однієї з ваших скриньок, тож ви повторіть тестування всіх країв, щоб побачити, чи вона розділяє поля.

Це також працює для вирівнюваних по осі коробок, обмежуючись лише віссю x / y.


Я не думав про ротацію, але дякую, що це цікаве посилання.
Iain

5

Вище DoBoxesIntersect є хорошим попарним рішенням. Однак якщо у вас багато коробок, у вас все ще виникає проблема O (N ^ 2), і вам може здатися, що вам потрібно зробити щось над цим, як те, на що посилається Kaj. (У літературі з 3D-виявлення зіткнень це відоме як з широкофазним, так і з вузькофазним алгоритмом. Ми зробимо щось дійсно швидко, щоб знайти всі можливі пари перекриттів, а потім щось дорожче, щоб побачити, чи можливо наше пари - фактичні пари.)

Широкофазний алгоритм, який я використовував раніше, - це "зачистка та обрізка"; для 2D ви б підтримували два відсортовані списки початку та кінця кожного поля. Поки рух коробки не буде >> масштабом поля від кадру до кадру, порядок цих списків не зміниться сильно, і тому ви можете використовувати сортування бульбашок чи вставок для його підтримки. У книзі "Візуалізація в режимі реального часу" є приємний перелік оптимізацій, які ви можете зробити, але вона зводиться до часу O (N + K) у широкій фазі, для N коробок, K з яких перекриваються, і з відмінним реальним світом продуктивність, якщо ви можете дозволити N ^ 2 булеви, щоб відслідковувати, які пари коробок перетинаються від кадру до кадру. Тоді у вас загальний час O (N + K ^ 2), який становить << O (N ^ 2), якщо у вас багато коробок, але лише кілька перекриттів.


5

Тут багато математики для дуже простої проблеми, припустимо, що у нас є 4 бали, визначені для прямої, верхньої, лівої, нижньої, правої ...

У випадку визначення того, чи стикаються 2 прямої стрілки, нам потрібно лише подивитися, що всі можливі крайності, які запобігали б зіткненням, якщо жодне з них не дотримано, то ДВІ ПІДТВЕРДЖУЮТЬСЯ стикатися, якщо ви хочете включити граничні зіткнення, просто замініть> та < з відповідними> = і = <.

struct aRect{
  float top;
  float left;
  float bottom;
  float right;
};

bool rectCollision(rect a, rect b)
{
  return ! ( b.left > a.right || b.right < a.left || b.top < a.bottom || b.bottom > a.top);
}

Я, чесно кажучи, не впевнений, чому це не найголовніша відповідь. Це просто, правильно і ефективно.
3Dave

3

Альтернативна версія відповіді ZorbaTHut:

bool DoBoxesIntersect(Box a, Box b) {
     return (abs(a.x - b.x) < (a.width + b.width) / 2) &&
     (abs(a.y - b.y) < (a.height + b.height) / 2);
}

Насправді це арифметика справно працює. Ви можете виконувати будь-яку арифметичну операцію в будь-яку сторону <, і це не змінює її (множення на мінус означає, що вам доведеться змінити менше, хоча). У цьому прикладі поля не повинні стикатися. Якщо центр поля A знаходиться на 1, він простягається від -4 до 6. Поле b центрується на 10, а проміжки 7,5 - 12,5, там немає зіткнень ... Тепер метод, який розмістив Wallacoloo, не тільки правильний, але швидше працюватимуть на мовах, які реалізують коротке замикання, оскільки більшість перевірок все одно повернеться помилковими, коротке замикання може вирізатись після
Deleter

Так, я зрозумів це, коли прокинувся сьогодні вранці. Кріс бамбусив мене своїм <> мішанням.
Iain

1
Дві проблеми: по-перше, ділення, як правило, значно повільніше, ніж множення. По-друге, якщо задіяні значення є цілими числами, це може спричинити за собою деякі проблеми усікання цілих чисел (ax = 0, bx = 9, a.width = 9, b.width = 10: abs (0-9) <(9 + 10) / 2, 9 <19/2, 9 <9, функція повертається помилково, незважаючи на те, що поля остаточно перетинаються.)
ZorbaTHut

2

Залежно від проблеми, яку ви намагаєтеся вирішити, вам може бути краще відслідковувати об'єкт під час переміщення, тобто зберігати список відсортованих х початкових та кінцевих позицій та одного для початкових та кінцевих позицій. Якщо вам доведеться зробити багато перевірок на перекриття, і тому вам потрібно оптимізувати, ви можете використовувати це на вашу користь, оскільки ви можете негайно шукати, хто закінчується, закривається зліва, кожен, хто закінчується, знаходиться зліва від цього, може бути обрізаний негайно. Те саме стосується верху, знизу і справа.
Бухгалтерський облік коштує часу, звичайно, тому він більше підходить для ситуації, коли мало об’єктів, що рухаються, але багато чеків перекриття.
Іншим варіантом є просторове хешуваннявання, коли ви збираєте об'єкти на основі приблизної позиції (розмір може містити їх у декількох відрах), але знову ж таки, лише якщо об’єктів багато, причому порівняно мало їх переміщується за ітерацією через вартість бухгалтерського обліку.
В основному все, що уникає (n * n) / 2 (якщо ви перевіряєте об'єкт a проти b, очевидно, вам не доведеться перевіряти b проти), допомагає більше, ніж оптимізувати чеки в обмежувальних полях. Якщо чекі з обмежувальною коробкою є вузьким місцем, я б серйозно порадив вивчити альтернативні варіанти вирішення проблеми.


2

Відстань між центрами не є такою ж, як відстань між кутами (коли, наприклад, один ящик всередині іншого), тож ЗАГАЛЬНО це рішення є правильним (я думаю).

відстань між центрами (для, скажімо, х): abs(x1+1/2*w1 - x2+1/2*w2)або1/2 * abs(2*(x1-x2)+(w1-w2)

Мінімальна відстань - це 1/2 w1 + 1/2 w2 or 1/2 (w1+w2). половинки скасовують так ..

return 
ABS(2*(x1 - x2) + (w1-w2) ) < (w1+w2)) &&
ABS(2*(y1 - y2) + (h1-h2) ) < (h1+h2));

1
Що там із заявою "return"?
doppelgreener

1

Ось моя реалізація в Java, передбачаючи архітектуру з двома доповненнями . Якщо ви не підтримуєте двійки, замість цього використовуйте стандартний виклик функції Math.abs :

boolean intersects(IntAxisAlignedBox left, IntAxisAlignedBox right) {
    return
        (
            lineDeltaFactor(left.min.x, left.max.x, right.min.x, right.max.x) |
            lineDeltaFactor(left.min.y, left.max.y, right.min.y, right.max.y) |
            lineDeltaFactor(left.min.z, left.max.z, right.min.z, right.max.z)
        ) == 0;
}

int lineDeltaFactor(int leftMin, int leftMax, int rightMin, int rightMax) {
    final int
            leftWidth = leftMax - leftMin,
            rightWidth = rightMax - rightMin,

            leftMid = leftMin + ((leftMax - leftMin) >> 1),
            rightMid = rightMin + ((rightMax - rightMin) >> 1);

    return (abs(leftMid - rightMid) << 1) / (leftWidth + rightWidth + 1);
}

int abs(int value) {
    final int mask = value >> (Integer.SIZE - 1);

    value ^= mask;
    value += mask & 1;
    return value;
}

Якщо припустити, що напівпристойний компілятор / LLVM вбудований, розширює ці функції, щоб уникнути дорогого жонглювання стека та перегляду v-таблиці. Це не вдасться для вхідних значень, близьких до 32-бітових крайнощів (тобто Integer.MAX_VALUEі Integer.MIN_VALUE).


0

Найшвидший спосіб - об'єднати всі 4 значення в одному векторному регістрі.

Зберігайте поля у векторному з наступними значеннями [ min.x, min.y, -max.x, -max.y ]. Якщо ви зберігаєте такі коробки, тест перехрестя містить лише 3 інструкції щодо процесора:

_mm_shuffle_ps змінити порядок другого поля, гортаючи хв та максимум половини.

_mm_xor_psз магічним числом _mm_set1_ps(-0.0f)перевернути знаки всіх 4 значень у другому полі.

_mm_cmple_ps для порівняння всіх 4 значень один з одним, порівнюючи наступні два регістри:

[ a.min.x, a.min.y, -a.max.x, -a.max.y ] < [ b.max.x, b.max.y, -b.min.x, -b.min.y ]

Нарешті, якщо потрібно, _mm_movemask_psдля отримання результату з векторної одиниці в скалярний регістр. Значення 0 означає поля, що перетинаються. Або якщо у вас більше 2-х коробок, це не потрібно, залиште значення у векторних регістрах та використовуйте побітові операції для об'єднання результатів з декількох полів.

Ви не вказали ні мову, ні платформу, але підтримка SIMD, як ця, або дуже схожа, є на всіх платформах та мовах. У мобільному телефоні ARM має NEON SIMD з дуже подібними речами. .NET має Vector128 у просторі імен System.Runtime.Intrinsics тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.