Разработка ремейка методами чайника. Часть 5
Когда я написал прошлую статью, то думал что наконец-таки закончил с мучениями в блендере и могу заняться привычным мне кодингом. Но блендер так не думал… Под ночные новогодние кутежи из клуба напротив, я только и занимался тем, что пытался подобрать идеальный набор параметров ткани для парусов всех возможных кораблей.
Но вы же не думаете что физика ткани это единственная проблема с которой пришлось бороться? Ооо, нееет… игры только начинаются! Впрочем, в игре наконец-то появились хорошие адаптивные паруса! Но это в конце, а пока…
Страдания с коллизией в блендере
Как можно было видеть по прошлой презентации процедурного генерирования парусов, им катастрофически не хватало коллизии с мачтой. Вопрос решается крайне просто - накинуть коллизию на мачту, и все счастливы! Да ведь…?
Из-за того, что паруса в состоянии покоя пересекают мачты, если им сходу накидывать коллизию, то ткань начинает путаться, растягиваться, заворачиваться под такими углами, что фпс блендера опускается ниже 1! Но у нас есть позиция из которой можно начать применять коллизию - как раз после того как паруса завершили адаптироваться к симуляции ветра. Так что логика получилась довольно простая:
- Включить симуляцию ветра
- Подождать пока паруса не стабилизируются
- Выключить симуляцию ветра
- Включить коллизию у мачт и корпуса
- …
- Записать результат как Shape Keys
Хотя конечно иногда коллизия ведет себя очень странно, и приходится останавливать запись, модифицировать свойства под конкретный парус на ходу и плясать в бесконечном цикле. Проделки организации, не иначе!
Страдания с импортом сокетов
Изначально плагин сбора корабля размещал локаторы и модели раздельно. При чем внутри локатора могла размещаться модель, которую мы подгружаем из отдельного файла(mast1 например). Но UE нужно четкое распределение сокета и модели за которой он размещен. Поэтому пришлось переписывать иерархию, до и после:
Теперь все сокеты закреплены за определенной моделью, ну и соответственно внутри сокета может находиться другая модель и так далее. Первое время выглядело так, что все отлично работает, но потом я глянул на корневую модель в UE.
Выяснилось что UE не делает проверки на то, что вложенный сокет принадлежит дочерней модели, и набрасывает сокеты в родителя, так, на всякий случай. По итогу пришлось почти полностью отказаться от сохранения иерархии и размещать все модели внутри корня.
Казалось бы победа, наконец-то можно работать! Но… к нам снова постучался UE. Дело в том, что паруса экспортируются как Armature, которая в свою очередь - конвертируется в Skeletal Mesh. И вот ведь неприятность - в этот меш нельзя добавлять сокеты как в примере выше. Разумеется я пошел копать интернеты, и вышел на плагин интеграции UE с блендером, в котором есть специальная кнопка копирования сокетов по одной модели. Неприятно конечно, но лучше чем руками все сокеты прописывать. Однако и тут меня ждало разочарование, потому что плагину обязательно нужно, чтобы сокет был закреплен за костью, а у меня сокеты могут размещаться в статичных позициях паруса без всяких костей. Выбирая между тем, чтобы создавать лишние кости, и просто написать свою реализацию копирования, я выбрал второе. Благо в данном случае реализация крайне проста!
Баги от UE и новое море
И вот настал момент, когда все необходимые модели импортированы и можно приступить к коду. Немного подправив генерацию тросов, я принялся их тестировать на корабле на ходу. И стала очевидна проблема с исчезновением тросов… т.е. они никуда не удаляются, но текстура становится полностью прозрачной если модель перемещается в пространстве. При чем проблема есть и при простой прокрутке камеры, её можно заметить в видео-презентации из третьей части, но это было не настолько критично.
Я вышел в интернет с таким вопросом. Как оказалось, баг возник начиная с версии 5.0 и поправили его буквально недавно в 5.1. Я не буду описывать все “прелести” с которыми я столкнулся в процессе обновления, но да, баг поправился, хотя странное размытие осталось! А еще, море стало еще красивее!
Архитектура
Немного расскажу об архитектуре Blueprint-а кораблей. Все корабли наследуются от BP_Ship, в котором реализована логика генерации тросов, расчет скорости перемещения, управление и прочее. В наследнике размещаются куски корабля, а так-же мачты как отдельные акторы.
Мачты так-же наследуются от абстракции, из логики: имитации точки pivot, для поворота реи в нужной позиции, установка морфов для парусов согласно направления ветра и позиции в анимации, отлов событий от корабля на действия с парусами и собственно их исполнение. А еще создание Dynamic Instance Material, к нему мы еще вернемся.
Таким образом, получилось разделить ответственность между судном и его мачтами. Для расчета скорости достаточно посчитать состояние каждого паруса от 0 до 1, сложить и разделить на их кол-во. Затем точно так-же посчитать состояние для всех мачт вцелом. В будущем к этому отлично впишется модификатор полученных повреждений.
Morph Targets и внешние факторы
Как вы, я надеюсь, помните, для имитации влияния ветра на парус я сгенерировал у парусов отдельный морф WindStrength. Выкручиваешь его в 1, и парус красиво надувается в одну сторону. -1 в другую. Собственно с учетом этой логики, я и написал логику подстраивания паруса под направление ветра. Но в процессе тестирования заметил как парус при отклонении в отрицательное положение, неестественно искривлялся и увеличивался в размерах.
Стало очевидно, что моя идея с отрицательными морфами - очень плохая, и нужно что-то другое. В какой-то момент мне пришла в голову “гениальная” идея - отзеркалить парус сменой координаты скалирования на отрицательное значение. Но тут тоже поджидала проблема, все объекты смещены относительно 0 согласно их размещению на корабле. Соответственно пропуская парус “через зеркало”, менялось и его положение.
В общем, по итогу я не придумал ничего лучше, чем просто на этапе блендера нагенерировать морфов под левое и правое направление ветра, затем в рантайме менять только необходимый. Благо это быстро и просто!
World Position Offset и текстура паруса
В прошлой статье я рассказывал что с помощью этого параметра можно имитировать функциональность WindStrength, но из-за неудобств в использовании решил от этого отказаться. В общем-то ничего не изменилось, но мне хотелось добавить на паруса что-то типа эффекта ряби от ветра, и как раз для такой простой задачи эта функциональность замечательно подходит. Но у нас остается проблема с тем, что парус трясет даже в спущенном состоянии, и мне не хотелось исправлять это путем смены текстуры на вариант без этой анимации в рантайме. Ведь тогда это будет выглядеть рвано и не так впечатляюще. Поэтому я решил снова пересмотреть видео от UE по прототипированию парусников. И да! Там был ответ, и ответ этот - Material Parameter Collection. Задаешь его значение в Blueprint, получаешь значение в текстуре, все счастливы!
Счастливы по крайней мере до того, как не понимают, что данная коллекция применяется ко всем парусам, а не к конкретному. И вот тут мы возвращаемся к тому, зачем нам создание и установка на модели парусов Dynamic Instance Material, вместо того чтобы просто прописать материал в параметрах ассета. Дело в том, что для каждого конкретного инстанса можно прописывать уникальные параметры для материала в рантайме. Но это можно сделать только если хранить ссылку на этот инстанс в памяти. Через простой Get Material от ассета нужного объекта получить не получиться, даже Cast To завершается ошибкой. Вот как это реализовано у меня:
Таким образом, когда производятся операции с парусами, я получаю ссылку на динамический материал из сохраненной переменной и модифицирую параметр-мультипликатор в текстуре. А вот собственно как выглядит логика текстуры:
Итог
На самом деле тут еще много о чем можно было бы рассказать, например о логике распределения парусов на группы, либо о разделении рей на двигаемые и статичные, либо о работе с анимацией, либо о невероятных багах в UE от которых мой пукан подгорает так, что можно собственный SpaceX открывать. Но статья уже затянулась и у меня истекает мой недельный дедлайн между статьями, поэтому я напоследок просто покажу что получилось:
Я думаю паруса получились весьма неплохо для такого чайника как я. В оригинальной игре у четырехугольных парусов есть неприятный резкий переход между состояниями, я же попытался сделать все максимально плавно. Еще предстоит много работы над тем, чтобы реализовать инерцию от поворотов, влияния веса, стабильность судна на воде и так далее, но это в будущем. К следующей статье я планирую заняться логикой пушек, чтобы добавить немного “экшона” в этот, пока еще пустой, мир. Если к тому моменту клятая простуда меня не добьет конечно…
Если вам понравилась статья, то буду благодарен за ваши комментарии, вопросы или советы!