Як я можу оптимізувати двигун зіткнення, коли порядок є значним, а зіткнення є умовними на основі об'єктної групи?


14

Якщо у вас це питання вперше, пропоную прочитати попередньо оновлену частину нижче, а потім цю частину. Ось синтез проблеми, однак:

В основному, у мене є механізм виявлення та вирішення зіткнень із сітковою системою просторового розподілу, де мають значення порядок зіткнення та групи зіткнень. Одне тіло за часом повинне рухатись, потім виявляти зіткнення, потім вирішувати зіткнення. Якщо я переміщу всі тіла одразу, а потім генерую можливі пари зіткнення, це, очевидно, швидше, але роздільна здатність порушується, оскільки порядок зіткнення не дотримується. Якщо я рухаю по одному тілу за раз, я змушений змушувати органи перевіряти зіткнення, і це стає проблемою ^ 2. Покладіть групи в суміш, і ви можете собі уявити, чому це дуже повільно стає дуже швидко з великою кількістю тіл.


Оновлення: над цим я дуже багато працював, але не зміг нічого оптимізувати.

Я успішно реалізував описану Віллю картину і змінив групи на бітсети, але це дуже незначна швидкість.

Я також виявив велику проблему: мій двигун залежить від порядку зіткнення.

Я спробував реалізувати унікальну генерацію пари зіткнень , яка напевно пришвидшила все на багато, але порушила порядок зіткнення .

Дозволь пояснити:

  • у моєму оригінальному дизайні (не генеруючи пар) це відбувається:

    1. єдине тіло рухається
    2. після переміщення він оновлює клітини і отримує тіла, проти яких стикається
    3. якщо це перекриває тіло, яке йому потрібно вирішити, вирішити зіткнення

    це означає, що якщо тіло рухається і вдаряється об стіну (або будь-яке інше тіло), тільки тіло, яке перемістилося, вирішить його зіткнення, а інше тіло буде не зачеплене.

    Такої поведінки я прагну .

    Я розумію, що це не є загальним для фізичних двигунів, але це має багато переваг для ігор в стилі ретро .

  • у звичайній конструкції сітки (генерації унікальних пар) це відбувається:

    1. всі тіла рухаються
    2. після переміщення всіх тіл оновіть усі клітини
    3. генерують унікальні пари зіткнень
    4. для кожної пари обробляти виявлення та дозвіл зіткнень

    в цьому випадку одночасне переміщення могло б призвести до перекриття двох тіл, і вони вирішаться одночасно - це ефективно змушує тіла "штовхати одне одного навколо" і порушує стійкість до зіткнення з кількома тілами

    Така поведінка є загальною для фізичних двигунів, але в моєму випадку вона неприйнятна .

Я також виявив ще одне питання, яке є головним (навіть якщо це, швидше за все, не відбудеться в реальній ситуації):

  • розглянути органи групи A, B і W
  • Збіг і вирішує проти W і A
  • B стикається і вирішується проти W і B
  • А нічого не робить проти Б
  • Б нічого не робить проти А

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

Для 100 тіл, що займають одну клітинку, це 100 ^ 100 ітерацій! Це відбувається тому, що унікальні пари не генеруються - але я не можу генерувати унікальні пари , інакше я отримав би поведінку, якої не бажаю.

Чи є спосіб оптимізувати цей тип двигуна зіткнення?

Це рекомендації, яких необхідно дотримуватися:

  • Порядок зіткнення надзвичайно важливий!

    • Органи повинні рухатись по черзі , потім перевіряти на предмет зіткнення по черзі та вирішувати їх після руху по черзі .
  • Органи повинні мати 3 групові біти

    • Групи : групи, до яких належить організм
    • GroupsToCheck : групи, в яких організм повинен виявити зіткнення
    • GroupsNoResolve : групи, яким організм не повинен вирішувати зіткнення проти
    • Можуть бути ситуації, коли я хочу лише виявити зіткнення, але не вирішити його



Попереднє оновлення:

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


Я створюю загальноприйнятий двигун виявлення / реагування на зіткнення C ++ 2D з акцентом на гнучкість та швидкість.

Ось дуже основна схема його архітектури:

Основна архітектура двигуна

В основному, основним класом є World, який володіє (управляє пам'яттю) a ResolverBase*, a SpatialBase*і a vector<Body*>.

SpatialBase це чистий віртуальний клас, який займається широкофазним виявленням зіткнень.

ResolverBase це чистий віртуальний клас, який займається вирішенням колізій.

Органи спілкуються World::SpatialBase*з SpatialInfoоб'єктами, якими володіють самі органи.


Тут є один просторовий клас:, Grid : SpatialBaseякий є базовою фіксованою 2D сіткою. Він має власний інфо - клас, GridInfo : SpatialInfo.

Ось як виглядає його архітектура:

Архітектура двигуна з просторовою сіткою

GridКлас володіє 2D масив Cell*. CellКлас містить колекцію (не належить) Body*: а , vector<Body*>який містить всі тіла , які знаходяться в клітці.

GridInfo об'єкти також містять невласні покажчики на клітини, в яких знаходиться організм.


Як я вже говорив раніше, двигун заснований на групах.

  • Body::getGroups()повертає a std::bitsetз усіх груп, до складу яких входить тіло.
  • Body::getGroupsToCheck()повертає a std::bitsetз усіх груп, з якими тіло має перевірити зіткнення.

Органи можуть займати більше однієї клітини. GridInfo завжди зберігає невласні покажчики на зайняті клітини.


Після переміщення одного тіла відбувається виявлення зіткнення. Я припускаю, що всі тіла є осями, що обмежують вісь.

Як працює широкофазне виявлення зіткнень:

Частина 1: оновлення просторової інформації

Для кожного Body body:

    • Обчислюються клітини верхньої лівої зайнятості та найменші праворуч зайняті клітини.
    • Якщо вони відрізняються від попередніх комірок, body.gridInfo.cellsочищається і заповнюється всіма клітинами, якими займає тіло (2D для циклу від верхньої лівої клітини до крайньої нижньої правої клітини).
  1. body тепер гарантовано дізнається, які клітини він займає.

Частина 2: фактичні перевірки на зіткнення

Для кожного Body body:

  • body.gridInfo.handleCollisions називається:

void GridInfo::handleCollisions(float mFrameTime)
{
    static int paint{-1};
    ++paint;

    for(const auto& c : cells)
        for(const auto& b : c->getBodies())
        {
            if(b->paint == paint) continue;
            base.handleCollision(mFrameTime, b);
            b->paint = paint;
        }
}

void Body::handleCollision(float mFrameTime, Body* mBody)
    {
        if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;

        auto intersection(getMinIntersection(shape, mBody->getShape()));

        onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
        mBody->onDetection({*this, mFrameTime, userData, -intersection});

        if(!resolve || mustIgnoreResolution(*mBody)) return;
        bodiesToResolve.push_back(mBody);
    }

  • Потім зіткнення вирішується для кожного органа в bodiesToResolve.

  • Це воно.


Отже, я досить довго намагаюся оптимізувати це широкофазове виявлення зіткнень. Кожен раз, коли я спробую щось інше, ніж поточна архітектура / налаштування, щось не йде так, як планувалося, або я роблю припущення про моделювання, які згодом виявляються помилковими.

Моє запитання: як я можу оптимізувати широкофазний двигун зіткнення ?

Чи є якась магічна оптимізація C ++, яку можна застосувати тут?

Чи можна переробити архітектуру, щоб забезпечити більшу продуктивність?


Вихід з виклику для останньої версії: http://txtup.co/rLJgz


Профілюйте та визначте вузькі місця. Дайте нам знати, де вони знаходяться, тоді нам є над чим працювати.
Майк Семдер

@MaikSemder: Я це зробив і написав у дописі. Це єдиний фрагмент коду, який є вузьким місцем. Вибачте, якщо це довго і детально, але це частина питання, тому що я впевнений, що це вузьке місце можна вирішити лише змінивши щось в конструкції двигуна.
Вітторіо Ромео

Вибачте, важко було знайти. Ви можете дати нам якісь цифри? Час функції та кількість об'єктів, оброблених у цій функції?
Майк Семдер

@MaikSemder: тестований на Callgrind, на двійковому файлі, складеному з Clang 3.4 SVN -O3: 10000 динамічних тіл - функція getBodiesToCheck()викликалася 5462334 рази і займала 35,1% всього часу профілювання (Інструкція для читання часу доступу)
Вітторіо Ромео

2
@Quonux: не вчинено правопорушень. Я просто люблю "винаходити колесо". Я міг би взяти Bullet або Box2D і зробити гру з цими бібліотеками, але це не справді моя мета. Я відчуваю себе набагато виконанішим і дізнаюся набагато більше, створюючи речі з нуля і намагаюся подолати перешкоди, які з’являються, - навіть якщо це означає, що ви засмучуєтесь і просите про допомогу. Крім моєї віри, що кодування з нуля неоціненна для навчальних цілей, я також вважаю, що це дуже весело та із великим задоволенням витрачати свій вільний час.
Вітторіо Ромео

Відповіді:


14

getBodiesToCheck()

З getBodiesToCheck()функцією може виникнути дві проблеми ; спочатку:

if(!contains(bodiesToCheck, b)) bodiesToCheck.push_back(b);

Ця частина є O (n 2 ) чи не так?

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

loop_count++;
if(!loop_count) { // if loop_count can wrap,
    // you just need to iterate all bodies to reset it here
}
bodiesToCheck.clear();
for(const auto& q : queries)
    for(const auto& b : *q)
        if(b->paint != loop_count) {
            bodiesToCheck.push_back(b);
            b->paint = loop_count;
        }
return bodiesToCheck;

Ви перенаправляєте покажчик у фазі збирання, але ви все одно будете відкидати його на етапі тестування, тому, якщо у вас достатньо L1, це не буде великим завданням. Ви можете покращити продуктивність, додавши до компілятора також підказки попереднього __builtin_prefetchвибору, наприклад , хоча це простіше з класичними for(int i=q->length; i-->0; )циклами і подібними.

Це просте налаштування, але моя друга думка полягає в тому, що може бути більш швидкий спосіб організувати це:

Однак ви можете перейти до використання растрових зображень , уникаючи всього bodiesToCheckвектора. Ось такий підхід:

Ви вже використовуєте цілі клавіші для тіл, але потім шукаєте їх у картах та речах та зберігаєте їх списки. Ви можете перейти до розподільника слотів, який в основному є лише масивом або вектором. Наприклад:

class TBodyImpl {
   public:
       virtual ~TBodyImpl() {}
       virtual void onHit(int other) {}
       virtual ....
       const int slot;
   protected:
      TBodyImpl(int slot): slot(slot_) {}
};

struct TBodyBase {
    enum ... type;
    ...
    rect_t rect;
    TQuadTreeNode *quadTreeNode; // see below
    TBodyImpl* imp; // often null
};

std::vector<TBodyBase> bodies; // not pointers to them

Це означає, що всі речі, необхідні для фактичного зіткнення, знаходяться в лінійній кеш-пам’яті, і ви виходите лише до біта, що відповідає конкретній реалізації, і приєднуєте його до одного з цих слотів, якщо є необхідність.

Для відстеження виділень у цьому векторі тіл ви можете використовувати масив цілих чисел як растрову карту та використовувати подвійне подвійне чи __builtin_ffsт. Д. Це надзвичайно ефективно для переміщення до слотів, які зараз зайняті, або пошуку незайнятого слота в масиві. Ви можете навіть іноді компактний масив, якщо він зростає нерозумно великим, а потім партії позначаються видаленими, переміщуючи ті, хто знаходиться в кінці, щоб заповнити прогалини.

перевірити кожне зіткнення лише один раз

Якщо ви перевірили , якщо через вдаряється б , вам не потрібно , щоб перевірити , якщо б вдаряється теж.

З використанням цілих ідентифікаторів випливає, що ви уникаєте цих перевірок простим оператором if. Якщо ідентифікатор потенційного зіткнення менший або рівний поточному ідентифікатору, який перевіряється, його можна пропустити! Таким чином, ви будь-коли перевірятимете кожне можливе парування один раз; що перевищить половину кількості перевірок на зіткнення.

unsigned * bitmap;
int bitmap_len;
...

for(int i=0; i<bitmap_len; i++) {
  unsigned mask = bitmap[i];
  while(mask) {
      const int j = __builtin_ffs(mask);
      const int slot = i*sizeof(unsigned)*8+j;
      for(int neighbour: get_neighbours(slot))
          if(neighbour > slot)
              check_for_collision(slot,neighbour);
      mask >>= j;
  }

поважати порядок зіткнень

Замість того, щоб оцінювати зіткнення, як тільки пара знайдена, обчисліть відстань для удару та зберігайте це у бінарній купі . Ці купи - це те, як ти зазвичай робиш черги з пріоритетом у пошуку шляхів, тому дуже корисний корисний код.

Позначте кожен вузол порядковим номером, щоб ви могли сказати:

  • A 10 потрапляє B 12 в 6
  • A 10 потрапляє в C 12 на 3

Очевидно, що після того, як ви зібрали всі зіткнення, ви почнете вискакувати їх із черги пріоритетів, найшвидшого. Отже, перший ви отримуєте A 10 звернень C 12 на 3. Ви збільшуєте порядковий номер кожного об'єкта ( 10 біт), оцінюєте зіткнення та обчислюєте їх нові шляхи та зберігаєте їх нові зіткнення в тій самій черзі. Нове зіткнення A 11 потрапляє B 12 у 7. Черга тепер має:

  • A 10 потрапляє B 12 в 6
  • А 11 потрапляє в B 12 о 7

Тоді ви вискочити з черги пріоритетів і Св 10 хітів B 12 в 6. Але ви бачите , що 10 є застарілим ; A наразі в 11. Тож ви можете відмовитися від цього зіткнення.

Важливо не турбуватися, намагаючись видалити всі несвіжі зіткнення з дерева; зняти з купи дорого. Просто відкиньте їх, коли ви попхнете їх.

сітка

Слід розглянути можливість використання квадрату. Це дуже проста структура даних для впровадження. Часто ви бачите реалізації, які зберігають точки, але я вважаю за краще зберігати ректи та зберігати елемент у вузлі, який його містить. Це означає, що для перевірки зіткнень вам потрібно лише переглядати всі тіла, і, для кожного, перевірити їх проти тих тіл у тому ж самому вузлі квадратичного дерева (використовуючи фокус сортування, викладений вище) та всіх тих, хто знаходиться у батьківських чотирьох вузлах дерева. Квадратне дерево - це сам список можливих зіткнень.

Ось простий квадратик:

struct Object {
    Rect bounds;
    Point pos;
    Object * prev, * next;
    QuadTreeNode * parent;
};

struct QuadTreeNode {
    Rect bounds;
    Point centre;
    Object * staticObjects;
    Object * movableObjects;
    QuadTreeNode * parent; // null if root
    QuadTreeNode * children[4]; // null for unallocated children
};

Ми зберігаємо рухомі об’єкти окремо, тому що нам не потрібно перевіряти, чи статичні об’єкти зіткнуться з чим.

Ми моделюємо всі об'єкти як вікна, що обмежуються по осі (AABB), і розміщуємо їх у найменшому QuadTreeNode, який їх містить. Коли у QuadTreeNode багато дітей, ви можете поділити їх далі (якщо ці об’єкти добре розподіляються між дітьми).

Кожен ігровий галочок, ви повинні повторно вписатись у квадри і обчислити рух - і зіткнення - кожного рухомого предмета. Це повинно бути перевірено на зіткнення з:

  • кожен статичний об’єкт у своєму вузлі
  • кожен рухомий об’єкт у своєму вузлі, який знаходиться перед ним (або після нього; просто виберіть напрямок) у списку movableObjects
  • кожен рухомий і статичний об'єкт у всіх батьківських вузлах

Це призведе до всіх можливих зіткнень, не упорядкованих. Потім ви робите рухи. Ви повинні встановити пріоритетність цих рухів на відстані та "хто рухається першим" (що є вашою особливою вимогою) та виконати їх у такому порядку. Використовуйте для цього купу.

Ви можете оптимізувати цей шаблон квадрата; вам не потрібно насправді зберігати межі та точки в центрі; це повністю можна отримати, коли ви ходите по дереву. Вам не потрібно перевіряти, чи є модель в межах, лише перевірте, на якій стороні вона знаходиться в центрі (тест "осі розділення").

Для моделювання таких швидкоплинних речей, як снаряди, а не переміщення їх на кожному кроці чи окремого списку «куль», який ви завжди перевіряєте, просто покладіть їх у квадратик з прямою їх польотом на деяку кількість ігрових кроків. Це означає, що вони рухаються в квадраті набагато рідше, але ви не перевіряєте кулі в далекі стіни, тому це гарний компроміс.

Великі статичні об'єкти слід розділити на складові частини; великий куб повинен мати, наприклад, кожне обличчя, окремо зберігається.


"Живопис" звучить добре, я спробую якнайшвидше повідомити про результати. Я не розумію другої частини вашої відповіді - я спробую прочитати щось про попереднє отримання.
Вітторіо Ромео

Я б не рекомендував QuadTree, це складніше, ніж робити сітку, і якщо її не зробити належним чином, вона не працюватиме точно і буде створювати / видаляти вузли занадто часто.
ClickerMonkey

Про вашу купу: чи дотримується порядок руху ? Розглянемо тіло А і тіло Б . A рухається праворуч у напрямку B, а B рухається праворуч у напрямку А. Тепер - коли вони стикаються одночасно, той, хто перемістився перший, повинен вирішити перший , а другий - не вплинути.
Вітторіо Ромео

@VittorioRomeo, якщо A рухається до B, а B рухається в напрямку A в тій же галочці і з однаковою швидкістю, вони зустрічаються посередині? Або A, рухаючись першим, зустрічає B, де починається B?
Буде


3

Б'юсь об заклад, у вас просто пропущено тону кеш-пам'яті під час ітерації над тілами. Ви об'єднуєте всі тіла разом, використовуючи якусь схему дизайну, орієнтовану на дані? За допомогою широкої фази N ^ 2 я можу імітувати сотні та сотні , записуючи за допомогою фрапсів, тіла без будь-яких частотних частот падають у нижчі регіони (менше 60), і це все без спеціального розподільника. Уявіть собі, що можна зробити при правильному використанні кешу.

Розгадка тут:

const std::vector<Body *>

Це відразу піднімає величезний червоний прапор. Ви виділяєте ці органи з новими необмеженими дзвінками? Чи використовується користувацький розподільник? Найголовніше, щоб у вас були всі тіла в величезному масиві, в якому ви рухаєтеся лінійно . Якщо лінійне переміщення по пам’яті не те, що ви вважаєте, ви можете скористатися нав'язливим використанням списку.

Крім того, ви, здається, використовуєте std :: map. Чи знаєте ви, як розподіляється пам'ять у std :: map? У вас буде складність O (lg (N)) для кожного запиту на карті, і це, ймовірно, може бути збільшено до O (1) за допомогою хеш-таблиці. Крім цього, пам'ять, виділена std :: map, також жахливо зруйнує кеш.

Моє рішення - використовувати нав'язливу хеш-таблицю замість std :: map. Хороший приклад як нав'язливих списків, так і нав'язливих хеш-таблиць - в базі Патріка Вайата в рамках його спільного проекту: https://github.com/webcoyote/coho

Отже, коротше кажучи, вам, мабуть, потрібно буде створити для себе певні спеціальні інструменти, а саме розподільник та інтрузивні контейнери. Це найкраще, що я можу зробити, не аналізуючи код для себе.


"Ви виділяєте ці органи з новими необмеженими дзвінками?" Я не явно дзвоню, newколи штовхають тіла на getBodiesToCheckвектор - ти маєш на увазі, що це відбувається всередині? Чи є спосіб запобігти цьому, зберігаючи колекції тіл динамічного розміру?
Вітторіо Ромео

std::mapне є вузьким місцем - я також пам’ятаю, що намагався dense_hash_setі не отримував жодного виступу.
Вітторіо Ромео

@Vittorio, тоді яка частина getBodiesToCheck є вузьким місцем? Нам потрібна інформація, щоб допомогти.
Майк Семдер

@MaikSemder: Профілер не заглиблюється в саму функцію. Вся функція - це вузьке місце, оскільки воно називається один раз на кадр на тіло. 10000 тіл = 10000 getBodiesToCheckвикликів на кадр. Я підозрюю, що постійне чищення / натискання у векторі є вузьким місцем самої функції. containsМетод також є частиною уповільнення, але так як bodiesToCheckне більше ніж 8-10 тел в ньому, має бути , що повільно
Вітторіо Romeo

@Vittorio було б добре, якщо ви введете цю інформацію в питання, це зміна гри;) Особливо я маю на увазі ту частину, яка getBodiesToCheck називається для всіх тіл, тож 10000 разів кожен кадр. Цікаво, ви сказали, що вони були у групах, тож навіщо вводити їх у bodyToCheck-масив, якщо у вас вже є інформація про групу. Ви можете детальніше розглянути цю частину, мені здається, дуже хороший кандидат з оптимізації.
Майк Семдер

1

Зменшіть кількість тіл, щоб перевірити кожен кадр:

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

Скористайтеся квадратом. Дивіться мою детальну відповідь тут

Видаліть усі виділення зі свого фізичного коду. Ви можете використовувати для цього профілер. Але я аналізував розподіл пам'яті лише в C #, тому не можу допомогти з C ++.

Удачі!


0

Я бачу двох проблемних кандидатів у вашій вузькій функції:

По-перше, це "містить" частину - це, мабуть, головна причина вузького місця. Це повторення через вже знайдені тіла для кожного тіла. Можливо, вам слід скористатися якоюсь хеш-таблицею / хеш-мапом замість вектора. Тоді вставлення має бути швидшим (із пошуком дублікатів). Але я не знаю конкретних цифр - я не маю уявлення, скільки тіл тут переглянуто.

Друга проблема може бути векторною: clear та push_back. Очищення може або не може викликати перерозподіл. Але ви, можливо, захочете цього уникнути. Рішенням може бути масив прапорів. Але, можливо, у вас є багато об’єктів, тому пам’яті малоефективними є список усіх об’єктів для кожного об’єкта. Якийсь інший підхід може бути приємним, але я не знаю, який підхід: /


Про першу проблему: я спробував використовувати щільний_гаш_сет замість вектора + містить, і це було повільніше. Я спробував заповнити вектор, а потім видалив всі дублікати, і це було повільніше.
Вітторіо Ромео

0

Примітка: Я нічого не знаю про C ++, лише Java, але ви повинні мати можливість з'ясувати код. Фізика - це універсальна мова правда? Я також усвідомлюю, що це посада за рік, але я просто хотів поділитися цим з усіма.

У мене є схема спостерігача, яка в основному після переміщення сутності повертає об'єкт, з яким вона зіткнулася, включаючи об'єкт NULL. Простіше кажучи:

( Я переробляю minecraft )

public Block collided(){
   return World.getBlock(getLocation());
}

Тому скажіть, що ти блукаєш у своєму світі. щоразу, коли вам дзвонить move(1), то дзвоніть collided(). якщо ви отримаєте потрібний блок, можливо, частинки летять, і ви можете рухатись вліво вправо і назад, але не вперед.

Використовуючи це більше, ніж просто Minecraft, як приклад:

public Object collided(){
   return threeDarray[3][2][3];
}

Просто майте масив, щоб вказати координати, які, буквально, як це робить Java, використовують покажчики.

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

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

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

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