Летающий по экрану самолётик и его винт, который крутится спереди, – это, конечно анимация, но мы понимаем, что внутренне сами объекты остаются неизменными, меняются только положение и ориентация объектов Место, к которым они привязаны. В этом уроке мы рассмотрим один из видов такой анимации, в которой меняется форма меша, а именно самый распространённый её вид – скелетную анимацию. Мы будем анимировать персонаж (я надеюсь, модель у меня получилась хоть немного похожей на человека).
Для этого типа анимации в пару к объекту типа Меш добавляется объект типа Скелет, в котором сохранена информация о первоначальном расположении костей и об их расположении в каждом кадре каждой из доступных анимаций. Этой информации достаточно, чтобы менять форму меша, т. е. пересчитывать координаты его вершин.
Наверное, я должен тут добавить, что анимации для этого и следующего урока я скачал с сайта https://sites.google.com/a/cgspeed.com/cgspeed/motion-capture/daz-friendly-release, где находится база анимаций в формате .BVH в довольно большом количестве. Они были созданы методом motion capture в университете Carnegie Mellon и выложены для свободного использования.
Итак, начнём с пустой сцены, на которую добавим «пол» – простой квадратный меш, на который потом «поставим» модель нашего персонажа. Создайте новый модуль scena.d, и заполните его следуюшими строками:
module scena; import derelict.sdl2.sdl; import std.math, std.conv, std.functional, std.string; import ddd, ddd.mesto, ddd.lamp, ddd.vector, ddd.quaternion, ddd.ddd_object, ddd.mesh; import ddd.zavisimost.sobytija_SDL2; import my_utils.journal; СтандартныйОбработчикСобытий обработчик_событий; void сцена() { менеджер.визуализатор.задать_цвет_фона([0.9, 0.85, 0.8, 1]); // Бежевый цвет менеджер.место_камеры.задать_позицию(0,1,5); // Камера "висит" на высоте 1 метра обработчик_событий = new СтандартныйОбработчикСобытий(); менеджер.задать_обработчик_событий(обработчик_событий); auto лампа0 = new Лампа("свет0", 0); лампа0.задать_диффузный_свет([1,1,1,1]); // Белый цвет auto место_лампы0 = менеджер.начальное_место.создать_ребёнка("Место лампы0", (Вектор3(-2,2,6)), Кватернион.НОЛЬ); место_лампы0.присоединить_объект(лампа0); auto объект_пол = менеджер.получить_объект("пол"); auto место_пола = менеджер.начальное_место.создать_ребёнка("Место пола", Вектор3(0, 0, 0), Кватернион.НОЛЬ); место_пола.присоединить_объект(объект_пол); } }
В этой части ничего не выходит за рамки уже рассмотренных нами тем из предыдущих уроков, поэтому я не стал подробно на них останавливаться.
Перед объявлением функции сцена() объявим несколько глобальных переменных и одну константу, которые нам потом потребуются:
const int КАДРОВ_В_ПРЫЖКЕ = 60;
Место место_персонажа;
Меш меш_персонажа;
float номер_кадра = 1;
bool прыжок = false;
Вернёмся в конец функции сцена() и добавим туда меш и место нашего персонажа. Это девушка-ассасин с кодовым именем «Кленовый лист»:
меш_персонажа = менеджер.получить_меш("Кленовый лист"); место_персонажа = менеджер.начальное_место.создать_ребёнка("Место объекта1", Вектор3(0,0,1.5), Кватернион.НОЛЬ); if (!(меш_персонажа is null)) { auto объект1 = new Объект("объект1", меш_персонажа); место_персонажа.присоединить_объект(объект1); объект1.получить_материал.задать_диффузную_текстуру(менеджер.получить_текстуру("Кленовый лист")); }
Если сейчас скомпилировать и запустить нашу программу, то мы увидим нашу ассасиншу в начальной Т-позе (именно в такой позе обычно создают персонажей в 3D-редакторах):
Т-поза не является «естественной» для человека, пора добавить скелет и «поставить» персонаж в более «естественную» позу (здесь для этого используется анимация с именем «Стояние2»). Добавьте эту строку перед блоком if:
auto скелет = менеджер.получить_скелет("Кленовый лист");
А в конце блока if добавьте эти строки:
if (!(скелет is null)) { меш_персонажа.присвоить_скелет(скелет); меш_персонажа.установить_состояние_анимации("Стояние2", 1); }
Анимация «Стояние2» – самая простая из всех возможных: в ней есть информация всего об одном кадре (с номером 1).
Здесь надо прояснить ещё кое-что. Сами анимации находятся в объекте Скелет, но метод установить_состояние_анимации мы вызываем у объекта Меш. Это важный момент: один и тот же скелет можно назначить нескольким разным мешам, и каждому из них может быть назначена своя собственная поза через одну из доступных анимаций объекта Скелет.
Теперь наша программа должна выдавать такое изображение, девушка стоит в более «естественной» позе:
Конечно, любой может сказать, что пока никакими анимациями в этой программе всё ещё даже не пахнет, ведь на экране ничего не двигается, не шевелится. Так что займёмся, наконец, движением. Нашей целью будет такое поведение программы: по нажатию на клавишу «Пробел» девушка должна подпрыгнуть (для этого у скелета есть анимации «Прыжок1» и «Прыжок2», в данном случае мы воспользуемся анимацией «Прыжок2»). Так же, как в предыдущем уроке, добавьте функцию обработчик отжатия клавиши «Пробел»:
bool обработка_отжатия_клавиши_пробел(SDL_Event e) { if (!прыжок) { прыжок = true; номер_кадра = 1; } return false; }
Здесь переменная прыжок — это флаг, указывающий находится ли персонаж в данный момент в состоянии прыжка.
В конце функции сцена() зарегистрируйте наш обработчик:
обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_SPACE, toDelegate(&обработка_отжатия_клавиши_пробел));
Наконец-то переходим непосредственно к анимации.
В конец модуля scena.d добавьте функцию обновления сцены:
void обновление_сцены(float длительность_кадра) { if (прыжок) { номер_кадра += длительность_кадра * 25; меш_персонажа.установить_состояние_анимации("Прыжок2", номер_кадра); if (номер_кадра > КАДРОВ_В_ПРЫЖКЕ) прыжок = false; } else меш_персонажа.установить_состояние_анимации("Стояние2", 1); }
Здесь мы, в зависимости от того, находится ли в данный момент персонаж в состоянии прыжка, устанавливаем состояние одной из анимаций: «Прыжок2» или «Стояние2». Для прыжка мы дополнительно вычисляем номер кадра, который нужно будет установить. Номер кадра не обязательно должен быть целым числом, DDD самостоятельно выполняет линейную интерполяцию между состояниями предыдущего и последующего кадров. Всего в анимации «Прыжок2» 60 кадров (с 1-го по 60-й), так что после 60-го кадра мы заканчиваем прыжок и отключаем флаг прыжок.
В модуль urok.d добавьте (или раскомментируйте, если она у вас оставалась с урока 12) строку, регистрирующую функцию обновления сцены:
менеджер.задать_функцию_обновления_сцены(&обновление_сцены);
Теперь можно запустить нашу программу и поглядеть, как прыгает наша ассасинша:
К сожалению, переходы между стоянием и прыжком очень заметны (модель сильно дёргается), особенно в конце прыжка. Тут уж извините, аниматор из меня вообще никакой. Даже на то, чтобы воспользоваться уже готовыми анимациями, я потратил несколько недель вот с таким очень слабым результатом. Для полноценного мультика или игры понадобится провести работы по анимированию примерно в миллион раз больше и качественнее, а для двух уроков я посчитал такой результат достаточным, сил на дальнейшие улучшения больше не оставалось...
Если вы хотите узнать, анимации с какими именами вообще присутствуют в объекте Скелет, то можно вызвать его метод получить_имена_анимаций(). Добавьте эту строку после строки if (!(скелет is null)) {:
в_журнал(имя_модуля, УровниЖурнала.Информация, format("Все анимации скелета: %s", скелет.получить_имена_анимаций()));
Теперь, после перекомпиляции и выполнения программы в файле журнал.log можно найти примерно такую строку:
0.31716|место |Информация | Все анимации скелета: ["Прыжок1", "Тест020", "Тест0", "Ходьба2", "Ходьба1", "Медленная_ходьба1", "Прыжок2", "Тест010", "Стояние2", "Тест045", "Стояние1"]