Вибухи в Box2D

У цій статті ми розглянемо кілька видів вибухів у фізичному движку Box2D.
Симуляція вибуху зводиться до знаходження тіл, які знаходяться в радіусі дії вибухової хвилі та застосування сили до них, щоб відкинути їх від центру вибуху.

Ми розглянемо три види вибухів різної складності:
  • Знаходження тіл в радіусі вибуху
  • Raycast — знаходження тіл в радіусі променів
  • Частинки — поширення багатьох маленьких тіл від епіцентру вибуху

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



Застосування імпульсу

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

Все описане вище можна виразити наступним кодом:
void applyBlastImpulse(b2Body* body, b2Vec2 blastCenter, b2Vec2 applyPoint, float blastPower)
{
b2Vec2 blastDir = applyPoint - blastCenter;
float distance = blastDir.Normalize();
// Ігнорування тіл, які знаходяться в центрі вибуху
if ( distance == 0 )
return;

float invDistance = 1 / distance;
float impulseMag = blastPower * invDistance * invDistance;
body->ApplyLinearImpulse( impulseMag * blastDir, applyPoint );
}


Знаходження тіл в радіусі вибуху

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

MyQueryCallback queryCallback;
b2AABB aabb;
aabb.lowerBound = center - b2Vec2( blastRadius, blastRadius );
aabb.upperBound = center + b2Vec2( blastRadius, blastRadius );
m_world->QueryAABB( &queryCallback, aabb );

// Подивитися всі знайдені тіла і вибрати тільки ті, у яких центр мас входить в радіус вибуху
for (int i = 0; i < queryCallback.foundBodies.size(); i++)
{
b2Body* body = queryCallback.foundBodies[i];
b2Vec2 bodyCom = body->GetWorldCenter();

//ignore bodies outside the range blast
if ( (bodyCom - center).Length() >= m_blastRadius )
continue;

applyBlastImpulse(body, center, bodyCom, blastPower );
}


Давайте подивимося на результат від такої реалізації вибуху. На картинці вказані тіла, які отримають імпульс після вибуху:


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

Об'єкти по обидва боки від центру вибуху мають однакову масу, але тіла праворуч отримають імпульс у 4 рази більше, ніж тіло зліва.

Raycast метод

Ми можемо прибрати всі проблеми, знайдені в першому методі, використовуючи промені для пошуку тіл, з якими буде взаємодіяти вибухова хвиля.

for (int i = 0; i < numRays; i++)
{
float angle = (i / (float)numRays) * 360 * DEGTORAD;
b2Vec2 rayDir( sinf(angle), cosf(angle) );
b2Vec2 rayEnd = center + blastRadius * rayDir;

RayCastClosestCallback callback;
m_world->RayCast(&callback, center, rayEnd);
if ( callback.m_body ) 
applyBlastImpulse(callback.body, center, callback.point, (m_blastPower / (float)numRays));
}


Зверніть увагу, ми ділимо силу вибуху на кількість променів. Зроблено це для того, щоб було легше підібрати кількість променів без зміни загальної сили імпульсу вибуху. Давайте подивимося на вибух з використанням 32 променів:

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

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

Метод частинок

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

for (int i = 0; i < numRays; i++)
{
float angle = (i / (float)numRays) * 360 * DEGTORAD;
b2Vec2 rayDir( sinf(angle), cosf(angle) );

b2BodyDef bd;
bd.type = b2_dynamicBody;
bd.fixedRotation = true; // Обертання необов'язкова
bd.bullet = true;
bd.linearDamping = 10;
bd.gravityScale = 0; // Игнорирвать гравітацію
bd.position = center; // Початкова точка в центрі вибуху
bd.linearVelocity = blastPower * rayDir;
b2Body* body = m_world->CreateBody( &bd );

b2CircleShape circleShape;
circleShape.m_radius = 0.05; // Дуже маленький радіус для тіла

b2FixtureDef fd;
fd.shape = &circleShape;
fd.density = 60 / (float)numRays;
fd.friction = 0; // Тертя необов'язково
fd.restitution = 0.99f; // Відображення від тіл
fd.filter.groupIndex = -1; // Частки не повинні стикатися один з одним
body->CreateFixture( &fd );
}

Так багато коду тільки тому що ми створюємо тіло і додаємо йому фікстури.


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

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


Ще один побічний ефект: сила вибуху не застосовується одночасно на всі тіла. Поширення частинок займає деякий час.


Автор оригіналу (англ) — мій хороший знайомий iforce2d, автор фізичної редактора R.U.B.E. для Box2D.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.