Фізика поїздів в Assassin's Creed Syndicate


У цій статті я хочу розповісти про нашому власному симуляторі, створеному для моделювання фізики поїздів в Assassin's Creed Syndicate. Дія гри відбувається в Лондоні 1868 року, в період промислової революції, коли розвиток суспільства залежало від пари і сталі. Для мене було величезним задоволенням попрацювати над унікальною можливістю реалізації світу Лондона вікторіанської епохи. Увага до історичних і реальним деталей привело нас до створення цієї фізичної симуляції.

Введення
Сьогодні писати свої фізичні движки не дуже популярно. Проте бувають ситуації, в яких створення власного фізичного симулятора з нуля надзвичайно корисно. Такі ситуації можуть виникати, коли є особлива необхідність в новій геймплейної функції або частини сімуліруемого ігрового світу. Саме така проблема виникла у нас при розробці системи залізниць і управління поїздами в Лондоні 19-го століття.

Стандартна система з'єднання європейських поїздів наведена на Рис. 1 ліворуч. Така ж система використовувалася в поїздах 19-го століття в Лондоні [1]. Коли ми почали роботу над потягами, то швидко усвідомили, що можна створити цікаві взаємодії і залежності, симулюючи стяжку фізично. Тому замість жорсткого скріплення вагонів ми з'єднали їх рухомим зчіпним пристроєм, що керує рухом усіх вагонів поїзда.

image
Рис. 1. Зліва — деталі гвинтової стяжки (джерело: Вікіпедія [1]). Праворуч — сполучна система в Assassin's Creed Syndicate.

У цьому випадку наша фізична симуляція дає кілька переваг:

  • Звивисті залізничні колії простіше контролювати за допомогою 1D-симулятора. Примусова вставка 3D-фізики для використання обмежувачів контролю за рухом в одновимірному просторі — досить ризиковане рішення. Вона може бути дуже чутлива до виникаючої нестабільності, з-за якої вагони злетять у повітря. Проте нам все одно потрібно було розпізнавати зіткнення вагонів в повному 3D-просторі.
  • Рухливе з'єднання забезпечує більшу свободу в дизайні геймплея. У порівнянні з реальним світом, нам потрібно набагато більшу відстань між вагонами. Це потрібно для того, щоб мати більше простору для виконання різних дій гравця і камери (наприклад, для взбіранія на дах вагона). Крім того, наша стяжка з'єднана набагато менш жорстко, ніж в реальному світі, щоб забезпечити більш вільний відносний рух між вагонами. Це дозволяє нам простіше справлятися з різкими поворотами залізничних шляхів, а розпізнавання зіткнень між вагонами захищає від взаємопроникнення.
  • Завдяки нашій системі ми можемо виконувати расцепку вагонів (з урахуванням сил тертя) і розрахунок зіткнень між відчепленими вагонами і рештою складу (наприклад, при різкій зупинці складу, коли відчеплені вагони продовжують рухатися і в результаті вдаряються про склад).
Ось відео з прикладом роботи нашої фізики:


Ми почнемо з розділу, в якому пояснюється, як контролюються поїзда.

Примітка: щоб спростити пояснення, ми позначимо терміном «тягач» вагон, що знаходиться ближче до локомотива, і терміном «причіп» вагон, розташований ближче до хвоста поїзда.

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

Локомотив::SetDesiredSpeed(float DesiredSpeed, float TimeToReachDesiredSpeed)

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

image

де F — обчислюється сила, m — маса локомотива, image(необхідна швидкість — поточна швидкість), а t = TimeToReachDesiredSpeed (час для досягнення необхідної швидкості).

Після обчислення сили ми передаємо її в WagonPhysicsState як «силу двигуна» для приведення локомотива в рух (докладніше про це в наступному розділі).

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

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

У наступному розділі описано базовий рівень фізичної симуляції.

Крок базової симуляції
Ось структура, використовувана для зберігання фізичної інформації про кожному вагоні (і локомотиві):

struct WagonPhysicsState
{
// Значення змінних при інтегруванні: 
// відстань вздовж шляхів і момент.
RailwayTrack m_Track;
float m_LinearMomentum;

// Швидкість, обчислюється з моменту.
float m_LinearSpeed;

// Поточне значення сил.
float m_EngineForce;
float m_FrictionForce;

// Положення і поворот відносно світу, одержувані безпосередньо від шляхів.
Vector m_WorldPosition;
Кватерніонів m_WorldRotation;

// Постійна при симуляції:
float m_Mass;
}

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

Для кожного вагона в якості кроку базової симуляції ми використовуємо метод Ейлера [2] (dt — це час одного кроку симуляції):

void WagonPhysicsState::BasicSimulationStep(float dt)
{
// Обчислення похідних.
float dPosition = m_LinearSpeed;
float dLinearMomentum = m_EngineForce + m_FrictionForce;

// Оновлення моменту.
m_LinearMomentum += dLinearMomentum*dt;
m_LinearSpeed = m_LinearMomentum / m_Mass;

// Оновлення положення.
float DistanceToTravelDuringThisStep = dPosition*dt;
m_Track.MoveAlongSpline( DistanceToTravelDuringThisStep );

// Отримання нового положення і повороту від шляхів.
m_WorldPosition = m_Track.GetCurrentWorldPosition();
m_WorldRotation = m_Track.AlignToSpline();
}

Для реалізації BasicSimulationStep ми використовуємо три основні рівняння. Ці рівняння показують, що швидкість — це похідна від положення, а сила — похідна моменту (точка над символом означає похідну по часу) [2 — 4]:

image

image

У третьому рівнянні визначається момент P, який є добутком маси і швидкості:

image

У нашій реалізації додаток імпульсу до вагону є просто операцією додавання з поточним моментом:

void WagonPhysicsState::ApplyImpulse(float AmountOfImpulse)
{
m_LinearMomentum += AmountOfImpulse;
m_LinearSpeed = m_LinearMomentum / m_Mass;
}

Як можна побачити, відразу після зміни моменту ми перераховуємо швидкість для більш зручного доступу до цього значення. Це робиться так само, як у [2].

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

Високорівневі кроки симуляції для одного поїзда
Ось псевдокод повного кроку симуляції для одного поїзда:

// Частина А
Оновлення швидкостей пуску поїзда

// Частина Б
Для всіх вагонів поїзда
ApplyDeferredImpulses (Додаток отриманих імпульсів)

// Частина
Для всіх вагонів поїзда
UpdateCouplingChainConstraint (Оновлення обмежувача сполучної стяжки)

// Частина Г
Для всіх вагонів поїзда
UpdateEngineAndFrictionForces (Оновлення сил двигуна і тертя)
SimulationStepWithFindCollision (Крок симуляції та пошук зіткнень)
CollisionResponse (Реакція на зіткнення)

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

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

Кожна частина цього циклу високорівневої симуляції докладно розглянута в наступних розділах. Через особливої важливості частині М ми почнемо з неї і з SimulationStepWithFindCollision.

Симуляція із зіткненнями
Ось код функції SimulationStepWithFindCollision:

WagonPhysicsState SimulationStepWithFindCollision(WagonPhysicsState InitialState, float dt)
{
WagonPhysicsState NewState = InitialState;
NewState.BasicSimulationStep( dt );
bool IsCollision = IsCollisionWithWagonAheadOrBehind( NewState );
if (!IsCollision)
{
return NewState;
}
return FindCollision(InitialState, dt);
}

Спочатку ми виконуємо пробний крок симуляції з повним зміною часу, викликаючи

NewState.BasicSimulationStep( dt );

і перевіряючи, виявляються в новому стані якісь зіткнення:

bool IsCollision = IsCollisionWithWagonAheadOrBehind( NewState );

Якщо цей метод повертає false, то можна використовувати безпосередньо нове обчислене стан. Але якщо виявлено зіткнення, ми виконуємо FindCollision для знаходження більш точного часу і стану фізики прямо перед подією зіткнення. Для виконання цього завдання ми застосовуємо бінарний пошук, схожий на використаний в [2].

Ось цикл для знаходження більш точного часу зіткнення і стану фізики:

WagonPhysicsState FindCollision(WagonPhysicsState CurrentPhysicsState, float TimeToSimulate)
{
WagonPhysicsState Result = CurrentPhysicsState;

float MinTime = 0.0 f;
float MaxTime = TimeToSimulate;
for (int step = 0 ; step<MAX_STEPS ; ++step)
{
float TestedTime = (MinTime + MaxTime) * 0.5 f;

WagonPhysicsState TestedPhysicsState = CurrentPhysicsState;
TestedPhysicsState.BasicSimulationStep(TestedTime);
if (IsCollisionWithWagonAheadOrBehind(TestedPhysicsState))
{
MaxTime = TestedTime;
}
else
{
MinTime = TestedTime;
Result = TestedPhysicsState;
}
}

return Result;
}

Кожна ітерація все більше наближає нас до точного часу зіткнення. Ми також знаємо, що потрібно перевіряти зіткнення тільки з одним вагоном, що знаходяться прямо перед поточним (або за ним, у разі руху назад). Для розрахунку результату метод IsCollisionWithWagonAheadOrBehind використовує перевірку зіткнення між двома орієнтованими коллайдерами (oriented bounding boxes, OBB). Ми перевіряємо зіткнення в повному 3D-просторі з допомогою m_WorldPosition m_WorldRotation WagonPhysicsState.

Реакція на зіткнення
Отримавши стан фізики прямо перед подією зіткнення, ми повинні обчислити відповідний реактивний імпульс j, щоб прикласти його і до тягача (rc), і до причепа (trailer). Ми почнемо з обчислення поточної відносної швидкості між вагонами перед зіткненням:

image

Схоже значення відносної швидкості image, після події зіткнення:

image

де imageimage— швидкості після прикладання імпульсу реакції на зіткнення j. Ці швидкості можна обчислити з допомогою швидкостей до зіткнення і імпульсу j наступним чином (imageimage— це маси вагонів):

image

image

Тепер ми готові визначити коефіцієнт пружного відновлення r:

image

Коефіцієнт пружного відновлення визначає, наскільки «пружна» реакція на зіткнення. Значення r = 0 означає повну втрату енергії, значення r = 1 — відсутність втрати енергії (абсолютну пружність). Підставляючи це рівняння в попередні формули, отримуємо

image

image

Впорядкуємо це рівняння, щоб отримати імпульс j:

image

image

Нарешті, ми можемо обчислити імпульс j:

image

image

У грі ми використовуємо коефіцієнт пружного відновлення r = 0.35.

Прикладаємо імпульс +j до тягача і імпульс j до причепу. Однак для тягача ми використовуємо «відкладені» імпульси. Оскільки ми вже виконали інтегрування для тягача і не хочемо змінювати його поточну швидкість, то відкладаємо імпульс на наступний кадр симуляції. Візуально це не дуже помітно, тому що різниця в один кадр мало видно. Такий «відкладений» імпульс зберігається для вагона і застосовується в частині Б наступного кадру симуляції.

Відео з прикладом зупинки поїзда:


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

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

image

де x — відстань, V — поточна швидкість, а t — час кроку симуляції.

Потім ми обчислюємо формулу:

image

де:

image— відстань, яку пройде тягач на найближчому кроці симуляції.

image— відстань, яку пройде причіп на найближчому кроці симуляції.

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

image

Якщо обмежувач відстані буде розірваний, то значення d буде позитивним. В такому випадку, щоб задовольняти умові обмежувача відстані, нам потрібно докласти такі імпульси, щоб d = 0. Щоб встановити масштаб необхідних імпульсів, використовуємо масу вагона. Потрібно, щоб більш легкий вагон рухався більше, а більш важкий — менше. Призначимо наступні коефіцієнти imageimage

image

image

Зауважте, що image. Нам потрібно, щоб на наступному кроці симуляції причіп пройшов з додатковим відстанню image, а тягач — з відстанню image. Щоб виконати це додатком імпульсу, нам потрібно помножити цю відстань на масу, поділену на час кроку симуляції:

image

image

Якщо використовувати додатковий коефіцієнт C

image

то можна спростити формули імпульсів до

image

image

Можна помітити, що вони мають однакову величину, але різні знаки.

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

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

Ми виконуємо ці обчислення послідовно для кожної сполучної стяжки в поїзді, починаючи з локомотива. Ми завжди прикладаємо імпульси до обох вагонах, сполученим стяжкою. Але в цьому правилі є один виняток: ми ніколи не прикладаємо імпульс до локомотиву. Нам потрібно, щоб локомотив зберігав свою швидкість, тому імпульс прикладається тільки до першого вагону після локомотива. Цей імпульс прикладається тільки до причепу, якому необхідна компенсація для всього необхідного відстані d (у такому разі маємо imageimageimage).

Корекція при різких поворотах
Оскільки симуляція виконується вздовж одновимірної лінії, у нас виникають проблеми з ідеальним відповідністю для сполучної стяжки на гаку, коли вагони рухаються на різких поворотах. У цій ситуації 1D-світ перетинається з 3D-світом гри. Сполучна стяжка розташовується в 3D-світі, але імпульси (компенсуючі для задоволення умови обмежувача відстані) прикладаються тільки в спрощеному 1D-світі. Для корекції визначення остаточного положення стяжки на гаку ми трохи змінюємо MaximumLengthOfCouplingChain в залежності від відносного кута між напрямками тягача і стяжки. Чим більше кут, тим менше максимально можлива довжина стяжки. Спочатку ми обчислюємо скалярний добуток двох нормалізованих векторів:

image

де image— нормалізоване напрямок сполучної стяжки, а image— вектор напрямку вперед тягача. Потім ми використовуємо наступну формулу для остаточного обчислення відстані, яку потрібно відняти з фізичної довжини сполучної стяжки:

float DistanceConvertedFromCosAngle = 2.0 f*clamp( (1.0 f-s)-0.001 f, 0.0 f, 1.0 f );
float DistanceSubtract = clamp( DistanceConvertedFromCosAngle, 0.0 f, 0.9 f );

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

MaximumLengthOfCouplingChain = ChainPhysicalLength - DistanceSubtract;

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

Тепер ми розглянемо особливий випадок — пуск поїзда.

Пуск поїзда
Як я зазначив раніше, ми не дозволяємо імпульсам сполучної стяжки змінювати швидкість локомотива. Однак нам потрібен спосіб симуляції ефектів тягової сили, особливо при пуску поїзда. Коли локомотив запускається, він починає тягнути інші вагони, однак сам локомотив теж повинен сповільнюватися відповідно масі вагонів. Щоб досягти цього, ми змінюємо швидкості, коли поїзд прискорюється з нульової швидкості. Ми починаємо з обчислень, заснованих на законі врівноваження моменту. Цей закон говорить, що «момент системи постійний, якщо на систему не діють зовнішні сили» [3]. Це означає, що в нашому випадку момент imageперед початком буксирування іншого вагона повинен бути дорівнює моменту imageвідразу після того, як сполучна стяжка потягне інший вагон:

image

image

У нашому випадку можна розгорнути це до наступної формули:

image

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

image

то можна спростити формулу наступним чином

image

image— це значення, яке ми шукаємо:

image

З допомогою цієї формули можна просто встановити нову швидкість imageлокомотива і всіх вагонів (від 2 до n), які в даний момент тягнуться сполучної стяжкою.
На Рис. 2 показано схематичне опис моменту, коли локомотив imageі два вагони починають тягнути третій вагон image:

image
Рис. 2. Пуск поїзда.

Ось відео пуску поїзда:


Тертя
Для обчислення сили тертя (змінна m_FrictionForce WagonPhysicsState) використовуються формули і значення, обрані після серії експериментів і найбільш відповідні геймплею. Значення сили тертя постійно, але ми додатково масштабується її згідно поточної швидкості (коли швидкість нижче 4). Ось графік стандартної сили тертя для вагонів у грі:

image
Рис. 3. Стандартна сила тертя для вагонів.

Для від'єднаних вагонів використовуються інші значення:

image
Рис. 4. Сила тертя для від'єднаних вагонів.

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

image

де t — час, що минув після події від'єднання (в секундах).

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

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

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

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

Підсумок
Ми розповіли про нашому фізичному симуляторі поїздів, створеному для Assassin's Creed Syndicate. Робота над цією частиною гри принесла велике задоволення і була дуже складною. В ігровому процесі відкритого світу існує безліч геймплейних можливостей і різних взаємодій. Відкритий світ створює ще більше складнощів забезпечення стабільних і стійких систем. Але після всієї виконаної роботи дуже радісно спостерігати за поїздами, переміщаються в грі та роблять свій внесок в якість ігрового процесу.

Подяки
Я хочу подякувати Джеймса Карнахана (James Carnahan) з Ubisoft Quebec City і Нобуюкі Міура (Nobuyuki Miura) з Ubisoft Singapore за редагування цієї статті та корисні поради.

Також я хочу подякувати моїх колег зі студії Ubisoft Quebec City studio: П'єра Форте (Pierre Fortin), який допоміг мені почати працювати з фізикою поїздів і надихав на розвиток; Дейва Трембле (Dave Tremblay) за технічну підтримку; Джеймса Карнахана за всі наші розмови про фізику; Матьє П'єро (Matthieu Pierrot) за надихаючий підхід; Максима Бегіна (Maxime Begin), який завжди готовий поговорити зі мною про програмування; Вінсана Мартіно (Vincent Martineau) за допомогу. Крім того, я вдячний Мартену Бедару (Martin Bedard), Марку Паренто (Marc Parenteau), Джонатану Джендрону (Jonathan Gendron), Карлу Дюмону (Carl Dumont), Патріку Шарлану (Patrick Charland), Емілю Уддестранду (Emil Uddestrand), Дамьену Бастіану (Damien Bastian), Еріку Мартелю (Eric Martel), Стіву Блези (Steve Blezy), Патріку Легар (Patrick Legare), Гильерму Люпина (Guillaume Lupien), Еріку Жирару (Eric Girard) і всім іншим, працювали над Assassin's Creed Syndicate і створив таку приголомшливу гру!

Довідкові матеріали
[1] «Buffers and chain coupler», https://en.wikipedia.org/wiki/Buffers_and_chain_coupler [Прим. пер.: російськомовна статья на Вікіпедії досить поверхнева. Детальніше про з'єднаннях, використовуваних у складах, можна прочитати тут.]

[2] Andrew Witkin, David Baraff and Michael Kass, «An Introduction to Physically Based Modeling», http://www.cs.cmu.edu/~baraff/pbm/

[3] Fletcher Dunn, Ian Parberry, «3D Math Primer for Graphics and Game Development, Second Edition», CRC Press, Taylor & Francis Group, 2011.

[4] David H. Eberly, “Game Physics. Second Edition", Morgan Kaufmann, Elsevier, 2010.
Джерело: Хабрахабр

0 коментарів

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