Как я собирал физику колёс в Unigine

Доброго дня.
В этот раз речь пойдёт о российском движке Unigine engine, и о том, как в нём собрать простую машинку. Схема не идеальная, так как в некоторых вещах ещё не разобрался, но хотя бы поделюсь опытом.

c2zwgaa8w_tvfzoalwi0twc8tpw.jpeg
Первым делом, чтобы понять, как делается физика колёс, отправляемся в официальную документацию и находим вот этот пример.
Кстати, обратите внимание, что это пример для wheels joint, а лежит он почему-то по адресу /car_suspension_joints/. Ещё нужно следить, чтобы документация не перескакивала на старые версии (то есть что открыта именно DOCS (2.11)) и отметить сверху статьи плашку «C#».

Изучив пример, видим, что написан он по аналогии с ранним C++ решением. То есть всё создаётся через код. Вот вам и визуальное программирование, приехали. Ну, не всё так печально, зато можно посмотреть, как это всё делается в коде. А побегав по ссылкам, получить общее представление об устройстве wheels joint и настройке физики.

В комплекте с самим движком демо с ездящей колёсной техникой нет (потому что пока в приоритете было скорее показать свободную версию движка артистам и научить базовым возможностям). Вернее есть, но это мини-примеры, написанные на устаревшем UnigineScript, где опять же всё создаётся кодом и не настраивается в редакторе. Зато в одном из демо присутствуют статичные модели машинок с LOD'ами.

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

Сборка каркаса

1. Создаём новый проект. Выбираем C# вариант. Открываем проект в редакторе.

rktnh7wpsyzisoq4ty669fphlse.jpeg

-yap5w7rp7hvkwcn5kvplalrl4y.jpeg

qoi9h50sjmgnd8ffvr0j_0adgqg.jpeg

2. Создаём основу машинки.

Для этого щёлкаем правой кнопкой мыши в сцене, выбирая Create > Primitive > Box. Получится объект Cuboid. Слегка приподнимаем его над «полом».

cvzvwl6izztpmcwchcizqgxtnok.jpeg

3. Создаём колёса.

Снова щёлкаем кнопкой мыши и выбираем Create > Primitive > Cylinder. Потребуется 4 штуки.

zmzlqe9puvjmazj1oqxx4vyouj4.jpeg

Чтобы не запутаться с ориентированием можно повернуть камеру так, чтобы сверху в кубике светилась -Y (то есть сейчас мы смотрим на будущую машинку сзади) и расположить два цилиндра позади Cuboid, два других впереди него. Как-то вращать и подгонять их не нужно. Для удобства задним и передним можно назначить разные цвета.

4. Добавляем физику основе.

Выбираем Cuboid и слева, в окне Parameters с вкладки Node переключаемся на Physics. В графе Body выбираем тип Rigid. После этого проматываем вниз и в открывшейся графе Shapes выбираем пункт Box.

n3ifmrljrcf7hdti6jl1d_hkwzy.jpeg

okb7bdarwhc9ecqndtxrv99_yvo.jpeg

Видим появившуюся форму ShapeBox. Зайдём в её настройки и сразу зададим нашей основе рекомендуемую массу (Mass) в 64 килограмма.

sc3bxgfko_smsb0azhe3cxe6ska.jpeg

5. Добавляем физику колёсам.

По очереди выбираем цилиндры и также проставляем им Rigid во вкладке Physics.
Shape им создавать не потребуется, потому что wheel джоинты работают на рейкастах.

6. Привязываем колёса к основе.

Время назначить wheel joints. Чтобы сделать это правильно выбираете Cuboid и во вкладке Physics проматываете вниз, до графы Joints. В списке выбираете Wheel и при нажатии кнопки Add выпадет окошко Select a Body. Там выбираете цилиндр, он подсвечивается белой сеткой в сцене, и жмёте Ok. В списке появится привязка WheelJoint. Вот здесь уже желательно их переименовывать (для этого нужно щёлкнуть по строчке JointWheel и написать новое имя в графе Name).

xlwc55bh8sfeeycxgd_0g69bc4y.jpeg

cytxgutnev7f2dtruvcuxulb_di.jpeg

Таким же образом прикрепляете к Cuboid остальные цилиндры. Чтобы в итоге в его списке джоинтов горело 4 строчки.

k9w7m0xomsussfajdt7pfrma0bo.jpeg
Без переименования здесь было бы 4 строчки WheelJoint

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

7. Настраиваем конфигурацию привязок.

Снова выбираем Cuboid, находим в Physics список его джоинтов и выбираем один из них. Видим, что в сцене внутри Cuboid появился белый уголок с квадратиками.
В настройках джоинта находим графу вектора Anchor 0, и регулируем значения его трёх полей таким образом, чтобы уголок этот сместить по оси X, выведя за пределы Cuboid и немного опустить по оси Z.

9rqcrqrxixc4te5t044g7b6rgvc.jpeg

k29wbzbjz46oihwptmknnhs2vvk.jpeg

После чего сдвигаем уголок по оси Y, в зависимости от того привязка какого колеса сейчас выбрана.

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

Если выбрать все джоинты сразу, то станет видна общая конструкция. Также можно включить отображение joints в хелперах на специальной панельке над сценой: Helpers > Physics > Joints.

hbupzcovchgweojafgbxklzpqag.jpeg
Вид на каркас сзади с включёнными хелперами

8. Смотрим, что получилось.

В редакторе есть кнопка расчёта физики. Если её включить, то запустится симуляция физики (скрипты при этом не действуют) и можно проверить как себя поведёт наш франкенштейн.
Перед запуском убедитесь что в настройках «пола» выставлены флажки enabled для Collision и Physics Intersection. Иначе наше создание может вовсе провалиться под пол целиком или одними колёсами.

qbtmo8bd8kf3-p_w6sh7im8g3p0.jpeg

Будьте внимательны, перед включением симуляции лучше сохраниться, а исправленные во время её действия параметры вернутся к своим значениям после отключения.

Как мы видим, колёса подхватываются в нужные места, но конструкция падает торцами цилиндров на поверхность.

o1fw-1vccn8ik64sqcdbuqy2ghg.jpeg

Можно замораживать на месте сам Cuboid, предварительно включив на вкладке Physics в графе Body галочку Immovable.

9. Настраиваем оси колёс.

Отключаем симуляцию физики. Выбираем Cuboid, заходим в его вкладку Physics и правим у каждого джоинта вектора Axis 00, Axis 10 и Axis 11. Обратите внимание, что в сумме поля каждого вектора принимают значения от 0 до 1 и редактор будет пытаться автоматически подправлять значения, если вы будете сперва ставить 1 в новую ось, не обнулив вектор.

По всей видимости я пока что развернул вектора не совсем правильно, хелперы показывают, что левая и правая части смотрят в одну сторону, но не дают чёткого понимания какая ось где, что затрудняет настройку.
В любом случае такая раскладка более менее работает. Тут я выставил вектора осей следующим образом: Axis 00 (0,0,1), Axis 10 (1,0,0), Axis 11 (0,0,1).

t3aggx5tphzit6yeuvzgwdlvaz0.jpeg

Если запустить симуляцию физики теперь, то конструкция должна упасть на правильно повёрнутые цилиндры, которые в дальнейшем будут крутятся вдоль правильной оси.

10. Масса и поведение колёс, физические итерации.

Скорее всего конструкция после падения на пол теперь частично проваливается.

Для начала зайдём в общие настройки физики. Щёлкаем на верхнюю строку редактора. Windows > Settings и в открывшейся вкладке Settings находим графу Physics (Runtime/World/). Выставляем в поле Iterations хотя бы 5.

hzrz3a_ss_pg09irawvrwz5i5tg.jpeg

Снова заходим в настройки каждого джоинта.
Вверху списка у каждого есть поле Iteration, выставляем 8.
Linear Restitution и Angular Restitution устанавливаем на 0.1.
Linear From меняем -0.15, Linear To на 0.15.
Внизу проставляем им массу 25 кг в поле Wheel Mass.

При запуске симуляции физика «машинка» всё ещё должна частично проваливаться/ заваливаться.

Выставим каждому джоинту Linear Damping в 50. И установим Linear Spring на 10.
Теперь при симуляции конструкция должна падать и чуть подпрыгивать на полу. Если не получается, то поэкспериментируйте со значениями Linear Damping и Linear Spring.
В документации советуют ставить damping 400 и spring 100, но лично у меня при этом «машинка» начинает крутиться как вертолёт, подпрыгивает и улетает.

11. Финальная настройка каркаса.

Попробуйте нажать Play чтобы погулять по сцене от первого лица. Скорее всего «машинка» повисла немного в воздухе, но катится, если её толкнуть в центральный куб.

gcrmph0gmkv8unsmjjihna9glio.jpeg

Теперь можно подрегулировать Anchor 0 у джоинтов, чтобы они оказались примерно равноудалены от центра (хотя не обязательно). После чего запустить симуляцию и прямо во время неё менять Linear Spring, чтобы найти оптимальное значение при котором каждое колесо нормально касается земли.

После всех этих манипуляций у вас должна получиться корректно падающая на пол «машинка», которая слегка катится при толкании в игровом режиме, где вы по умолчанию бегаете на отдельном контроллере с видом от первого лица.

gztxmpwohsuj6xw3pcwosvommj4.jpeg

Написание управляющего скрипта

Заготовка «машинки» у нас есть, теперь напишем ей управляющий код на C#.
Для этого щелкаем правой кнопкой в окошке под сценой и выбираем Create > Create C# Component. Вписываем в появившееся окошко название скрипта.

7mwttcgrd3_7ugr6w-9frtohwqo.jpeg

o3hax_u0jcam5wcbhrqwk4uwpiq.jpeg

Щёлкаем по появившемуся файлу скрипта двойным кликом и запускается среда разработки.

_iak6tyajkyxwcwsharicq44s7o.jpeg

Выделяете строки, залитые синим на картинке, удаляете их и вместо этого добавляете следующий код:

фрагмент скрипта
	[ShowInEditor][Parameter(Tooltip = "Left Wheel")]
	private Node targetWheelL = null;
	[ShowInEditor][Parameter(Tooltip = "Right Wheel")]
	private Node targetWheelR = null;

	[ShowInEditor][Parameter(Tooltip = "Left Wheel F")]
	private Node targetFWL = null;
	[ShowInEditor][Parameter(Tooltip = "Right Wheel F")]
	private Node targetFWR = null;

	[ShowInEditor][Parameter(Tooltip = "theCar")]
	private Node targetCar = null;

    private JointWheel my_jointL;
    private JointWheel my_jointR;
    private JointWheel my_jointFL;
    private JointWheel my_jointFR;

	Controls controls = null;
	float angle = 0.0f;
	float velocity = 0.0f;
	float torque = 0.0f;

	private float ifps;

	private void Init()
	{
	my_jointL = targetWheelL.ObjectBodyRigid.GetJoint(0) as JointWheel;
	my_jointR = targetWheelR.ObjectBodyRigid.GetJoint(0) as JointWheel;
	my_jointFL = targetFWL.ObjectBodyRigid.GetJoint(0) as JointWheel;
	my_jointFR = targetFWR.ObjectBodyRigid.GetJoint(0) as JointWheel;

	PlayerPersecutor player = new PlayerPersecutor();
	controls = player.Controls;
	}
	
	private void Update()
	{
			ifps = Game.IFps;

			if ((controls.GetState(Controls.STATE_FORWARD) == 1) || (controls.GetState(Controls.STATE_TURN_UP) == 1))
			{
				velocity = MathLib.Max(velocity, 0.0f);
				velocity += ifps * 50.0f;
				torque = 5.0f;
			}
			else if ((controls.GetState(Controls.STATE_BACKWARD) == 1) || (controls.GetState(Controls.STATE_TURN_DOWN) == 1))
			{
				velocity = MathLib.Min(velocity, 0.0f);
				velocity -= ifps * 50.0f;
				torque = 5.0f;
			}
			else
			{
				velocity *= MathLib.Exp(-ifps);
			}
			velocity = MathLib.Clamp(velocity, -90.0f, 90.0f);

			if ((controls.GetState(Controls.STATE_MOVE_LEFT) == 1) || (controls.GetState(Controls.STATE_TURN_LEFT) == 1))
				angle += ifps * 100.0f;
			else if ((controls.GetState(Controls.STATE_MOVE_RIGHT) == 1) || (controls.GetState(Controls.STATE_TURN_RIGHT) == 1))
				angle -= ifps * 100.0f;
			else
			{
				if (MathLib.Abs(angle) < 0.25f) angle = 0.0f;
				else angle -= MathLib.Sign(angle) * ifps * 45.0f;
			}
	
			angle = MathLib.Clamp(angle, -10.0f, 10.0f);

			float base_a = 3.3f;
			float width = 3.0f;
			float angle_0 = angle;
			float angle_1 = angle;
			if (MathLib.Abs(angle) > MathLib.EPSILON)
			{
				float radius = base_a / MathLib.Tan(angle * MathLib.DEG2RAD);
				angle_0 = MathLib.Atan(base_a / (radius + width / 2.0f)) * MathLib.RAD2DEG;
				angle_1 = MathLib.Atan(base_a / (radius - width / 2.0f)) * MathLib.RAD2DEG;
			}

			my_jointFL.Axis10 = MathLib.RotateZ(angle_0).GetColumn3(0);
			my_jointFR.Axis10 = MathLib.RotateZ(angle_1).GetColumn3(0);

			if (controls.GetState(Controls.STATE_USE) == 1)
			{
				velocity = 0.0f;
					my_jointL.AngularDamping = 20000.0f;
					my_jointR.AngularDamping = 20000.0f;
					my_jointFL.AngularDamping = 20000.0f;
					my_jointFR.AngularDamping = 20000.0f;
			}
			else
			{
					my_jointL.AngularDamping = 0.0f;
					my_jointR.AngularDamping = 0.0f;
					my_jointFL.AngularDamping = 0.0f;
					my_jointFR.AngularDamping = 0.0f;
			}

			if (Input.IsKeyDown(Input.KEY.Q))
			{
				targetCar.ObjectBodyRigid.AddLinearImpulse(vec3.UP*1000f);
			}
			if (Input.IsKeyDown(Input.KEY.E))
			{
				targetCar.ObjectBodyRigid.AddLinearImpulse(vec3.DOWN*1000f);
			}

	}

		private void UpdatePhysics()
		{
			my_jointL.AngularVelocity = velocity;
			my_jointR.AngularVelocity = velocity;
			
			my_jointL.AngularTorque = torque;
			my_jointR.AngularTorque = torque;
		}

////// здесь будет код с комментариями /////

После этого сохраняете файл скрипта и возвращаетесь в окно редактора. Unigine проверит программу на ошибки и если всё нормально, то справа внизу должен показаться зелёный прямоугольник с уведомлением Project build successful.

Теперь выбираем Cuboid, добавляем ему поле компонента и перетягиваем туда наш скрипт. Появляется несколько полей, которые надо заполнить. В поле Target Wheel L аккуратно перетягиваем левое заднее колесо (то есть обозначающий его Cylinder), Target Whell R — правое заднее колесо, Target FWL — переднее левое, Target FWR — переднее правое, и в поле Target Car — сам Cuboid.

Запускаем Play — теперь машинка начала двигаться. Правда одновременно с персонажем, от лица которого вы сейчас смотрите на мир.
Управление у неё следующее: W — катится вперёд, S — назад, A и D — поворот передних колёс влево и вправо соответственно. На Q подаётся импульс вверх, машинка как бы подпрыгивает. На E, наоборот, импульс подаётся вниз.

////////// позже напишу как перевесить камеру на саму машинку //////////

Невангеры в движке Unigine

Научившись собирать приблизительную физику колёс я сделал тестовую сцену с моделькой био-мехоса из своего проекта Невангеры. Вот как это выглядит на данный момент:

Основные проблемы на текущий момент — некоторая нестабильность конструкции. Что связано опять же с тем, что я пока не досконально разобрался во всех аспектах и нюансах настройки. Иногда машинка обо что-то запинается и начинает люто вращаться. Уменьшение Linear Damping вроде не особо помогает, подкручивание прочих параметров тоже, хотя что-то я ещё не крутил или настраивал, но не комплексно.
Другой баг — бывают ситуации с тотальным застреванием в полу, когда передние колёса торчат вверх, а задние под ландшафтом. Или пробивание насквозь самого ландшафта на высокой скорости, с последующим улетанием под него. Всё это, видимо, правится настройкой допусков и итераций, но пока не нашёл оптимальных значений.
В целом, на достаточно ровной поверхности движение уже более адекватное. Не совсем ясно насколько хорошо Wheel Joints ведут себя на неровной поверхности в идеале, как я понял в видеодемках движка для машинок использовались устаревшие (вроде бы), но более физически «честные» Suspension Joints (кстати на них собирать машинку я и не пробовал, возможно, там всё несколько проще). Учитывают ли Wheel Joints параметры трения ландшафта, или их интересует только то, включен ли на на нём флажок физики.

Общие впечатления от движка

Что касается работы с редактором, мне больше всего происходящее напомнило процесс работы с CryEngine. Хотя местами удобнее. Но это кому как, в том же CryEngine можно было моделировать прямо в редакторе. Мне это не актуально, потому как для подобного использую Blender, где у тебя на порядки больше контроля над моделью, а кому-то может быть важно.

На нижеследующей картинке слева виден список материалов. Меши красятся через группу mesh based материалов, от которых можно клонировать свои варианты материала. Покрасить террейн текстурой уже сложнее, зато по полной используются маски — можно смешивать текстуры, карты высот и так далее, если разобраться в устройстве. Для той же воды есть своя отдельная группа материалов, добавляется она буквально в один клик и довольно хорошо выглядит.

Справа иерархия объектов сцены и окно свойств. Немного неудобно манипулировать числовыми значениями на лету — нужно попасть в иконку с маленькими стрелочками. Некоторые ползунки поначалу вводят в заблуждение, потому как показывают не весь возможный диапазон параметра, а, допустим, лишь промежуток от 0 до 1. Скорость полёта камеры регулируется в поле с бегущим человечком сверху, по умолчанию она как-то медленно перемещается, судя по ощущениям. Наверное так более удобно, когда вы работаете с моделями в реальных масштабах.

s01ov1bzrei4oabsjfjequkpzvo.jpeg

© Habrahabr.ru