Я создал прототип ракетной атаки! Для этого понадобилась хитрая математика, о которой будет рассказано в этой статье.

Мы поговорим о кубических кривых Безье, шуме Перлина и rotation minimizing frames.

Пусть Итиро Итано гордится.

В статье будет довольно мало кода, потому что основной упор мы сделаем на геометрию. Многих людей пугает математика, но помните, что необязательно понимать всё, чтобы ею пользоваться.

Итеративное и аналитическое движение


В общем случае существует два стиля написания кода движения. Итеративный код обновляет позицию объекта инкрементно кадр за кадром, выполняя процесс, который профессора колледжей называют интегрированием. Популярным примером такого стиля является метод Эйлера, при котором мы вычисляем вектор скорости объекта и на протяжении шага времени «подталкиваем» позицию в этом направлении:

void Update( float DeltaTime ) {
	Vector3 Velocity = CalculateVelocity();
	Vector3 Position = GetPosition();
	SetPosition( Position + DeltaTime * Velocity );
}

Дельта (Delta) — это просто математическое обозначение «изменения», например, «изменение со временем этого Update()»

Это естественный способ создания управления персонажем, при котором ввод игрока в каждом кадре меняется, или в сложных физических симуляциях, где нет известного аналитического решения для реального времени.

Если же вы знаете всё движение заранее, можно использовать аналитический код, в котором весь путь вычисляется по начальным условиям (математики называют это параметрической кривой), после чего сэмплируется текущее время. Хорошим примером этого является известная кубическая кривая Безье:

Vector3 CalcBezierPos( Vector3 P0, Vector3 P1, Vector3 P2, Vector3 P3, float t ) {
	float t_ = 1 - t;
	return
		(t_ * t_ * t_)    * P1 + 
		(3 * t_ * t_ * t) * P2 +
		(3 * t_ * t * t)  * P3 + 
		(t * t * t)       * P4 ;
}


Если вы когда-нибудь работали с векторным редактором, то узнаете её. Кривые Безье — это кубические многочлены, то есть это простейшие контуры с четырьмя степенями свободы: конечными точками P0 и P3 и «контрольными точками» P1 и P2, влияющими на ориентацию и кривизну.

Входное значение t называется входным параметром, это коэффициент в интервале 0-1. То есть, например, t=0.333 — это примерно треть пути. Чтобы переместить точку, мы просто берём время, прошедшее с начала движения, и делим на общую длительность движения.

float StartTime;
float Duration;

void Update() {
	float CurrentTime = Time.time;
	float Elapsed = CurrentTime - StartTime;
	if( Elapsed >= Duration )
		SetPosition( P3 ); // мы в конце
	else
		SetPosition( CalcBezierPos( P0, P1, P2, P3, Elapsed / Duration ) );
}

Кроме позиции мы также можем использовать параметры кривой Безье для вычисления производной в t, то есть скорости изменения. Этот вектор полезен, потому что он касателен к кривой, то есть указывает в направлении движения. Чтобы преобразовать его в скорость, нужно разделить его на общую длительность.

Vector3 CalcBezierDeriv( Vector3 P0, Vector3 P1, Vector3 P2, Vector3 P3, float t ) {
	float t_ = 1 - t;
	return  (
		( 3 * t_ * t_ ) * ( P1 - P0 ) + 
		( 6 * t_ * t ) * ( P2 - P1 ) + 
		( 3 * t * t ) * ( P3 - P2 ) ;
}

float Velocity = CalcBezierDeriv( P0, P1, P2, P3, Elapsed / Duration ) / Duration;


Speed = Meters Per Second = ( Meters Per T ) / ( Seconds Per T ) = Deriv / Duration

Симулируем самонаводящиеся ракеты


Так как я знаю, где начинается путь самонаводящейся ракеты (пусковая установка), и где он заканчивается (нарисованная цель), я решил использовать в качестве основы для пути ракеты кривую Безье.


P1 помещена перед истребителем, а P2 проецируется из поверхности цели.
Использование аналитического решения упрощает работу, потому что нам не нужно вычислять сложную «симуляцию», попадающую в нужное место, и есть возможность точно настроить время между выстрелом и попаданием, что более интуитивно понятно, чем физические величины второго порядка.

Вполне приемлемый эффект, однако довольно скучный. Можно его улучшить.

Добавляем шум


Создав фундамент, можно приступить к украшательствам. Ракетные атаки в аниме движутся по хаотичным путям, увеличивая динамику. Мы можем имитировать это, добавив шум.

Художники по спецэффектам обычно используют шум Перлина — псевдослучайные колебания, они хаотичны, но плавны. Код шума слишком длинный, чтобы публиковать его здесь, но примеры легко найти онлайн.


Поищите симплекс-шум (название популярного оптимизированного варианта).

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


Как превратить функцию шума в смещение, искажённое по 3D-кривой? Мы вычислим два независимых значения шума и используем их в качестве компонент X и Y вектора смещения, преобразованного по кадрам поворота, привязанного к производной кривой Безье.

Vector3 LocalOffset;
float NoiseFreq = 2f; // коэффициент частоты вихляния
float NoiseAmp = 8;   // коэффициент величины вихляния
float Envelope = 1 - (1 - 2 * t) * (1 - 2 * t);
LocalOffset.x = NoiseAmp * Envelope * Noise( NoiseSeedX, NoiseFreq * Elapsed );
LocalOffset.y = NoiseAmp * Envelope * Noise( NoiseSeedY, NoiseFreq * Elapsed );
LocalOffset.z = 0;
Quaternion Frame = Quaternion.LookRotation( CalcBezierDeriv( P0, P1, P2, P3, t ) );
SetPosition( CalcBezierPos( P0, P1, P2, P3, t ) + Frame * LocalOffset );


Ракета вихляет по красной и зелёной стрелкам

Это почти сработало, однако иногда возникали глитчи. Вычисленные таким образом закреплённые по вертикали кадры поворота привязаны к производной, но часто вращаются, особенно когда путь вертикальный. Вместо этого нам нужны так называемые Rotation Minimizing Frame, не имеющие мгновенного вращения и обладающие лишь минимальными покачиваниями между направлениями.


(A) Закреплённые по вертикали кадры (B) Rotation Minimizing Frames

Методика вычисления minimizing frames в общем случае математически сложна, но, к счастью, в 2006 году была опубликована статья, в которой приведён удивительно простой «подталкивания» кадра без вращения под названием Double Reflection Method. Нам необязательно разбираться с выводом, достаточно знать, что он малозатратен и работает.



Quaternion Frame; // Инициализируем со значением Quaternion.LookDirection( P1 - P0 );

void UpdateFrame( float t ) {
	// "нормаль" и "касательная" начала
	var n0 = Frame * Vector3.up;
	var t0 = Frame * Vector3.forward;

	// "касательная" цели
	var t1 = CalcBezierDeriv( P0, P1, P2, P3, t ).normalized;

	// первое отражение
	var v1 = CalcBezierPos( P0, P1, P2, P3, t ) - GetPosition(); 
	var c1 = v1.sqrMagnitude;
	var n0_l = n0 - (2 / c1) * Vector3.Dot(v1, n0) * v1;
	var t0_l = t0 - (2 / c1) * Vector3.Dot(v1, t0) * v1;

	// второе отражение
	var v2 = t1 - t0_l;
	var c2 = v2.sqrMagnitude;
	var n1 = n0_l - (2 / c2) * Vector3.Dot(v2, n0_l) * v2;

	// создаём поворот, используя в качестве оси "вверх" нормаль цели
	Frame = Quaternion.LookRotation( t1, n1 );
}


Получилось!

Комментарии (11)


  1. Rio
    00.00.0000 00:00
    +9

    Интересная тема, люблю такой эффект. Я когда-то подобное в своей игрухе делал (в 2D, правда) примерно так: шум Перлина не использовал, а просто между точками старта и цели генерил N случайных точек, а потом двигал ракеты через них, используя сплайн Катмулла-Рома. Насколько помню, это вычислительно легче оказалось, чем Безье считать.


  1. RalphMirebs
    00.00.0000 00:00
    +3

    Писал на юнити что-то подобное. У ракеты была постоянная скорость движения к цели V и скорость поворота (наведения) Vr, без "хаотики" при выборе траектории. Выбор этих двух величин влиял на поведение в полёте. Например, можно было создать быструю, но "тугодумную" ракету, которая просто не успевала отслеживать цель при пуске с определённой дистанции / в неком направлении и летела несколько эллипсов, прежде чем попасть. Или вообще не могла попасть, ложась на круговую орбиту.


  1. DjPhoeniX
    00.00.0000 00:00

    А то же самое в движущиеся мишени можно придумать?


    1. Dboss
      00.00.0000 00:00

      Можно просчитывать кривые с шумом для каждого кадра. Тогда смещение конечной точки будет учитываться для движения ракеты. И появится возможность для цели выполнить маневр уклонения :)


    1. Osnovjansky
      00.00.0000 00:00

      Попробовал вспомнить то, что рассказывали в рамках предмета "Теория оптимального управления" на харьковском мехмате, году эдак 95ом - 96ом.

      Для наведения ракет на маневрирующую цель может использоваться метод "половинного спрямления траектории"

      Модель такая: есть цель, для которой известны скорость, направление движения в данный момент, и известно, что ускорение бокового маневра ограничено некоторым значением.

      Тоже самое известно и для ракеты, которую наводим.

      Для цели вычисляем примерную точку где она окажется в момент подлета ракеты, если цель не будет маневрировать. Ракету перенацеливаем в точку посередине между текущим положением и спрогнозированным. И так цикл за циклом.

      Скорее всего немного наврал, так как по-памяти.

      Дальше - отсебятина.

      Можно попробовать модифицировать алгоритм наведения, добавив ракете боковой маневр случайной длительности в случайном направлении, случайным ускорением в диапазоне, скажем, до половины максимального.

      Имеет смысл, если цель может отстреливаться от ракет.

      В реальности алгоритм должен быть заметно сложнее, чтобы обеспечить одновременный подлет всех ракет залпа.


  1. Vasiliy_S
    00.00.0000 00:00
    +4

    Наверняка периодически возникают пересечения траекторий разных ракет. Добавьте взаимное уничтожение ракет при столкновении. Интересно посмотреть


    1. numark
      00.00.0000 00:00
      +1

      Это ж перевод. Пишите автору оригинального текста :)
      https://blog.littlepolygon.com/posts/missile/


  1. Vsevo10d
    00.00.0000 00:00
    +9

    Я предчувствую минусы за тематику, но не могу удержаться, поскольку для меня маневрирование боевых ракет - это определенная техническая эстетика, выраженное в движении решение технических задач.

    Например, "танцующий" запуск МБР с поезда БЖРК - ракета после выхода из установки специальным двигателем отклоняется от вертикальной оси, чтобы не повредить насыпь и состав работой маршевого двигателя: Шакальное видео 1; момент пуска из док. фильма 2; замедленный момент из блогерского ролика 3, полное видео пуска 4 (все лучше без звука).

    Похожий по смыслу запуск ракеты "Бастион" со срабатыванием и отстрелом корректирующих двигателей в носовой части: Видео с разных ракурсов (чистый звук без блевотного закадра).

    Полет ракеты "земля-воздух" HQ-17: Видео (как и положено шортсу, со всратой музыкой).

    Вращающийся полет противотанковых ракет: Корнет, TOW.


    1. vadimr
      00.00.0000 00:00

      Но настоящие ракеты летают совершенно по другим моделям, чем описано в статье – обычно по принципу терминального управления. И ещё может быть важно, с какой стороны к цели подлетать.


  1. Perforethor
    00.00.0000 00:00

    Класс! :)

    Добавлю в закладки)