← к содержанию

Урок 16. Скелетная анимация

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

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

Наверное, я должен тут добавить, что анимации для этого и следующего урока я скачал с сайта https://sites.google.com/a/cgspeed.com/cgspeed/motion-capture/daz-friendly-release, где находится база анимаций в формате .BVH в довольно большом количестве. Они были созданы методом motion capture в университете Carnegie Mellon и выложены для свободного использования.

  1. Итак, начнём с пустой сцены, на которую добавим «пол» – простой квадратный меш, на который потом «поставим» модель нашего персонажа. Создайте новый модуль 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),
                                            Кватернион.НОЛЬ);
    место_пола.присоединить_объект(объект_пол);
    }
}

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

  1. Перед объявлением функции сцена() объявим несколько глобальных переменных и одну константу, которые нам потом потребуются:

const int КАДРОВ_В_ПРЫЖКЕ = 60;

Место место_персонажа;
Меш меш_персонажа;
float номер_кадра = 1;
bool прыжок = false;
  1. Вернёмся в конец функции сцена() и добавим туда меш и место нашего персонажа. Это девушка-ассасин с кодовым именем «Кленовый лист»:

    меш_персонажа = менеджер.получить_меш("Кленовый лист"); 
    место_персонажа = менеджер.начальное_место.создать_ребёнка("Место объекта1", 

                                                   Вектор3(0,0,1.5),
                                                   Кватернион.НОЛЬ);
    if (!(меш_персонажа is null)) {
        auto объект1 = new Объект("объект1", меш_персонажа);
        место_персонажа.присоединить_объект(объект1);
        объект1.получить_материал.задать_диффузную_текстуру(менеджер.получить_текстуру("Кленовый лист"));
    }

    Если сейчас скомпилировать и запустить нашу программу, то мы увидим нашу ассасиншу в начальной Т-позе (именно в такой позе обычно создают персонажей в 3D-редакторах):



  1. Т-поза не является «естественной» для человека, пора добавить скелет и «поставить» персонаж в более «естественную» позу (здесь для этого используется анимация с именем «Стояние2»). Добавьте эту строку перед блоком if:

	auto скелет = менеджер.получить_скелет("Кленовый лист");

А в конце блока if добавьте эти строки:

		if (!(скелет is null)) {
			меш_персонажа.присвоить_скелет(скелет);
			меш_персонажа.установить_состояние_анимации("Стояние2", 1);
		}

Анимация «Стояние2» – самая простая из всех возможных: в ней есть информация всего об одном кадре (с номером 1).

Здесь надо прояснить ещё кое-что. Сами анимации находятся в объекте Скелет, но метод установить_состояние_анимации мы вызываем у объекта Меш. Это важный момент: один и тот же скелет можно назначить нескольким разным мешам, и каждому из них может быть назначена своя собственная поза через одну из доступных анимаций объекта Скелет.

Теперь наша программа должна выдавать такое изображение, девушка стоит в более «естественной» позе:




  1. Конечно, любой может сказать, что пока никакими анимациями в этой программе всё ещё даже не пахнет, ведь на экране ничего не двигается, не шевелится. Так что займёмся, наконец, движением. Нашей целью будет такое поведение программы: по нажатию на клавишу «Пробел» девушка должна подпрыгнуть (для этого у скелета есть анимации «Прыжок1» и «Прыжок2», в данном случае мы воспользуемся анимацией «Прыжок2»). Так же, как в предыдущем уроке, добавьте функцию обработчик отжатия клавиши «Пробел»:

bool обработка_отжатия_клавиши_пробел(SDL_Event e) {
	if (!прыжок) {
		прыжок = true;
		номер_кадра = 1;
	}
    return false;
}

Здесь переменная прыжок — это флаг, указывающий находится ли персонаж в данный момент в состоянии прыжка.

В конце функции сцена() зарегистрируйте наш обработчик:

    обработчик_событий.зарегистрировать_функцию2(SDL_KEYUP, SDLK_SPACE, toDelegate(&обработка_отжатия_клавиши_пробел));
  1. Наконец-то переходим непосредственно к анимации.

В конец модуля scena.d добавьте функцию обновления сцены:

void обновление_сцены(float длительность_кадра) {
	if (прыжок) {
		номер_кадра += длительность_кадра * 25;
		меш_персонажа.установить_состояние_анимации("Прыжок2", номер_кадра);
		if (номер_кадра > КАДРОВ_В_ПРЫЖКЕ)
			прыжок = false;
	}
	else
		меш_персонажа.установить_состояние_анимации("Стояние2", 1);
}

Здесь мы, в зависимости от того, находится ли в данный момент персонаж в состоянии прыжка, устанавливаем состояние одной из анимаций: «Прыжок2» или «Стояние2». Для прыжка мы дополнительно вычисляем номер кадра, который нужно будет установить. Номер кадра не обязательно должен быть целым числом, DDD самостоятельно выполняет линейную интерполяцию между состояниями предыдущего и последующего кадров. Всего в анимации «Прыжок2» 60 кадров (с 1-го по 60-й), так что после 60-го кадра мы заканчиваем прыжок и отключаем флаг прыжок.

  1. В модуль urok.d добавьте (или раскомментируйте, если она у вас оставалась с урока 12) строку, регистрирующую функцию обновления сцены:

менеджер.задать_функцию_обновления_сцены(&обновление_сцены);

Теперь можно запустить нашу программу и поглядеть, как прыгает наша ассасинша:


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

  1. Если вы хотите узнать, анимации с какими именами вообще присутствуют в объекте Скелет, то можно вызвать его метод получить_имена_анимаций(). Добавьте эту строку после строки if (!(скелет is null)) {:

    в_журнал(имя_модуля, УровниЖурнала.Информация, format("Все анимации скелета: %s", скелет.получить_имена_анимаций()));

Теперь, после перекомпиляции и выполнения программы в файле журнал.log можно найти примерно такую строку:

   0.31716|место          |Информация  | Все анимации скелета: ["Прыжок1", "Тест020", "Тест0", "Ходьба2", "Ходьба1", "Медленная_ходьба1", "Прыжок2", "Тест010", "Стояние2", "Тест045", "Стояние1"]