Готовый перевод Ogre3D Intermediate Tutorials / Промежуточные туториалы Ogre3D: Intermediate Tutorial 1: Animation, Walking Between Points, and Basic Quaternions
ВВЕДЕНИЕ
В этом уроке мы рассмотрим, как получить объект Entity (сущность), анимировать его и заставить двигаться между предопределёнными точками.
Также мы рассмотрим основы вращения через кватернионы и посмотрим, как заставить сущность смотреть в направлении её движения.
Поскольку вы проходите урок, вы должны постепенно добавлять код к вашему проекту и наблюдать результаты после его построения.
Предварительные условия
Этот урок предполагает, что вы уже знаете, как настроить проект, использующий Ogre и заставить его успешно компилироваться.
Этот урок также использует такую структуру данных из STL, как двустороннюю очередь std::deque.
Хотя вам и не требуется никаких предварительных знаний о том, как использовать двустороннюю очередь, вы, по крайней мере, должны знать, что такое шаблоны.
Если вы не знакомы с STL, я бы порекомендовал вам почитать STL Pocket Reference [ISBN 0-596-00556-3] (в русском переводе известна как "STL. Карманный справочник" [ISBN 5-469-00389-2] - прим. пер.).
В будущем это сохранит вам много времени.
Вы можете прочитать первую часть "STL Pocket Reference" здесь (на английском языке - прим. пер.).
С чего начать
Во-первых, Вы должны создать новый проект (я назвал его ITutorial01), и добавить следующий код:
Перед продолжением убедитесь, что этот код компилируется.
Настройка сцены
Прежде, чем мы начнём, обратите внимание на то, что в заголовочном файле мы уже определили три переменные.
Переменная mEntity будет хранить нашу сущность, переменная mNode будет хранить узел сцены, а переменная mWalkList будет содержать все точки, через которые должен пройти наш объект.
Перейдите в функцию ITutorial01::createScene() и добавьте в неё следующий код.
Сначала мы установим окружающее освещение на полную мощность для того, чтобы мы могли видеть видеть размещаемые объекты.
Затем создадим робота, чтобы нам было с кем играть.
Для этого мы создадим сущность, а затем объект SceneNode, к которому её и прицепим.
Это всё базовые вещи, так что я не буду вдаваться в подробности.
В следующей части кода мы собираемся сказать роботу, куда он должен переместиться.
Для тех из вас, кто ничего не знает об STL, объект deque - это эффективная реализация двусторонней очереди.
Мы будем использовать только некоторые из его методов.
Методы push_front() и push_back() помещают элементы в начало и конец очереди соответственно.
Методы front() и back() возвращают значения, соответственно, с начала и с конца очереди.
Методы pop_front() и pop_back() удаляют элементы из начала и из конца очереди.
Наконец, метод empty() сообщает, пуста ли очередь или нет.
Этот код добавляет два вектора позиций в очередь, к которым позже будет двигаться наш робот.
Далее мы размещаем в сцене некоторые объекты, которые будут показывать, куда должен будет двигаться наш робот.
Это позволит нам видеть перемещения робота относительно других объектов на экране.
Обратите внимание на отрицательную компоненту Y в их позиции.
Это сделано для того, чтобы поместить объекты под тем местом, в которое должен двигаться робот и поэтому он будет стоять сверху них, когда дойдёт до него.
Наконец, мы помещаем камеру в точку, из которой хорошо видно всё происходящее. Мы просто передвигаем камеру в эту наилучшую позицию.
Теперь скомпилируйте и запустите код.
АНИМАЦИЯ
Сейчас мы переходим к установке базовой анимации.
Анимация в Ogre очень проста. Для её настройки вы должны получить объект AnimationState от объекта Entity, установить его параметры и активировать.
Это сделает анимацию активной, но вы также должны будете добавлять время к ней каждый кадр, чтобы она работала.
Мы всё сделаем за один раз. Сначала перейдите в метод ITutorial01::createFrameListener() и добавьте следующий код после вызова BaseApplication::createFrameListener():
Во второй строке мы получаем объект AnimationState из сущности.
В третьей строке мы вызываем метод setLoop(true), который зацикливает анимацию.
Для некоторых анимаций (например, анимации смерти) мы должны бы были установить этот параметр в false.
Четвёртая строка фактически включает анимацию. Но подождите... откуда мы взяли 'Idle'?
Как туда проскользнула эта магическая константа? Каждая сетка имеет свой собственный набор анимаций, определённых специально для неё.
Чтобы увидеть все анимации конкретной сетки, с которой вы работаете, вы должны скачать OgreMeshViewer и посмотреть сетку через него.
Теперь, если мы скомпилируем и запустим приложение, мы увидим... что ничего не изменилось.
Это потому, что мы должны обновлять состояние анимации временем каждого кадра.
Найдите метод ITutorial01::frameRenderingQueued() и добавьте эту строку кода в начало функции:
Теперь постройте и запустите приложение. Вы должны видеть, что робот выполняет его бесполезную анимацию, стоя на месте.
ПЕРЕМЕЩЕНИЕ РОБОТА
Теперь мы собираемся выполнить сложную задачу создания прогулки робота от пункта до пункта.
Прежде, чем мы начнем, я хотел бы описать переменные, которые мы определили.
Для реализации задачи перемещения робота мы будем использовать четыре переменные.
Прежде всего, мы должны сохранить направление, в котором перемещается робот, в переменной mDirection.
Мы сохраним текущую точку назначения робота в его путешествии в переменной mDestination.
В переменной mDistance мы сохраним расстояние, которое роботу всё ещё нужно пройти до пункта назначения.
И, наконец, в переменной mWalkSpeed мы сохраним скорость движения робота.
Перво-наперво нам нужно установить начальные значения этим переменным.
Мы установим скорость ходьбы в 35 единиц в секунду. Здесь нужно обратить внимание на одну очень важную вещь.
Переменную mDirection мы явно устанавливаем в нулевой вектор, потому что позже мы будем использовать её для определения того, двигается ли робот или нет.
Добавьте следующий код к методу ITutorial01::createFrameListener():
Теперь, когда это сделано, мы должны привести робота в движение.
Чтобы заставить робота двигаться, мы просто говорим ему изменять анимации.
Однако нам нужно начинать перемещение робота только в том случае, если есть позиция, куда он может двигаться.
По этой причине мы вызываем функцию ITutorial01::nextLocation.
Добавьте этот код в самый верх метода ITutorial01::frameRenderingQueued() прямо перед вызовом метода AnimationState::addTime():
Если вы сейчас скомпилируете и запустите код, робот будет шагать на месте.
Это произойдёт потому, что робот начинает идти в нулевом направлении и наша функция ITutorial01::nextLocation() всегда возвращает true.
В следующих шагах мы добавим немного больше интеллекта функции ITutorial01::nextLocation().
Теперь нам нужно действительно переместить робота в сцене. Для этого мы должны перемещать его на небольшое расстояние каждый кадр.
Перейдите в метод ITutorial01::frameRenderingQueued.
Мы будем добавлять следующий код между предыдущим оператором if и вызовом метода AnimationState::addTime().
Этот код будет обрабатывать тот случай, когда робот действительно движется, т.е. когда mDirection != Ogre::Vector3::ZERO.
Причина, по которой mWalkspeed умножается на evt.timeSinceLastFrame состоит в том, что в этом случае скорость ходьбы останется неизменной при различной частоте кадров.
Если бы мы написали Real move = mWalkspeed, робот бы ходил медленнее на слабых машинах и быстрее на мощных.
Теперь нам нужно проверить, дошли ли мы до конечной точки.
Если mDistance меньше нуля, мы должны перепрыгнуть в целевую точку и продолжить движение к следующей точке.
Обратите внимание, что мы устанавливаем mDirection в нулевой вектор.
Если метод nextLocation() не изменяет mDirection (что означает, что больше некуда идти), мы не должны больше двигаться.
Теперь, когда мы передвинулись в эту точку, мы должны настроить движение к следующей точке.
Когда мы узнаем, надо нам двигаться в следующую точку или нет, мы можем установить подходящую анимацию - анимацию ходьбы, если есть следующая точка и анимацию бездействия, если такой точки нет.
Это просто вопрос установки анимации бездействия, если больше нет точек назначения.
Обратите внимание: нам не нужно устанавливать анимацию ходьбы, если есть дополнительные точки в очереди, к которым нужно пройти.
Поскольку робот уже идёт, нет никаких оснований говорить делать ему тоже самое.
Однако, если робот должен пойти в другой пункт, мы должны повернуть его таким образом, чтобы он смотрел в эту точку.
Пока мы просто оставим комментарий в предложении else оператора if; запомните это место, поскольку мы позже к нему вернёмся.
Вышенаписанный код обрабатывает ситуацию, когда мы находимся близко к цели. Теперь же мы должны обработать нормальный случай, когда мы находимся на пути к цели, но ещё не там.
Для этого мы переместим робота в направлении его передвижения на величину, определяемую переменной перемещения.
Это достигается путём добавления следующего кода:
Мы сделали почти всё. Наш код делает всё, за исключением установки переменных, необходимых для движения.
Если мы правильна установим переменные передвижения, наш робот будет двигаться так, как и предполагалось.
Найдите функцию ITutorial01::nextLocation(). Эта функция возвращает false, когда мы находимся в точке назначения.
Это будет первая строка нашей функции. (Обратите внимание, что вы должны оставить оператор return true в конце функции.)
Теперь мы должны установить переменные (это всё ещё метод nextLocation()).
Сначала мы достаём вектор назначения из очереди.
Мы устанавливаем вектор направления путём вычитания текущей позиции SceneNode из вектора назначения.
Тут у нас возникает проблема. Помните, что мы умножали mDirection на значение перемещения в методе frameRenderingQueued()?
Если мы сделаем это, мы должны быть уверены, что вектор направления является единичным вектором (что означает, что его длина равна единице).
Функция normalise() делает его единичным за нас и возвращает старую длину вектора.
Это удобно, поскольку нам так же нужно узнать расстояние до пункта назначения.
Теперь скомпилируйте и запустите приложение. Оно работает! Вроде.
Робот теперь ходит по всем точкам, однако он постоянно смотрит в одном направлении - Ogre::Vector3::UNIT_X (его ориентация по умолчанию).
Мы должны изменить направление его взгляда, когда он движется между точками.
Что нам нужно сделать - это получить направление взгляда робота и использовать функцию rotate() для вращения объекта до придания ему правильного положения.
Вставьте следующий код туда, где мы оставили комментарий на предыдущем шаге.
В первой строке мы получаем направление взгляда робота.
Во второй строке мы создаём кватернион, представляющий вращение из текущего направления в целевое направление.
В третьей строке, собственно, робот и поворачивается.
Мы кратко упоминали кватернионы в Базовом уроке 4, но только сейчас столкнулись с их реальным использованием.
Проще говоря, кватернионы представляют вращение в трёхмерном пространстве.
Они используются для слежения за позиционированием объектов в пространстве и могут использоваться в Ogre для их вращения.
В первой строке мы вызвали метод getOrientation(), который возвращается объект Quaternion, представляющий ориентацию робота в пространстве.
Поскольку Ogre не имеет понятия, какая сторона робота является "лицевой", мы должны умножить эту ориентацию на вектор UNIT_X (который задаёт направление "естественной" ориентации робота) для получения направления, в котором робот сейчас смотрит.
Мы сохраняем это направление в переменной src.
Во второй строке метод getRotationTo() возвращает кватернион, который представляет вращение из направления, куда сейчас смотрит робот, в направление, куда мы хотим его повернуть. В третьей строке мы вращаем узел таким образом, что грани робота приобретают новую ориентацию.
В этом коде, который мы написали, существует одна проблема.
В некоторых случаях метод SceneNode::rotate() потерпит неудачу.
Если мы попытаемся повернуть робота на 180 градусов, код вращения упадёт с ошибкой деления на ноль.
Для исправления этой ошибки, мы проверим, вращаемся ли мы на 180 градусов.
Если это так, мы просто применим метод yaw() для поворота робота на 180 градусов вместо применения метода rotate().
Для этого удалите три строки, которые мы только что поместили сюда и замените их на эти:
Здесь всё должно быть понятно, за исключением, быть может, агрумента в операторе if.
Если два единичных вектора противоположно направлены (т.е. угол между ними равен 180 градусам), то их скалярное произведение даст в результате -1.
Итак, если скалярное произведение двух векторов равно -1, мы вращаем робота методом yaw() на 180 градусов, в противном случае используем метод rotate().
Почему же я добавляю 1.0f и проверяю, что результат меньше 0.0001f? Не забывайте об ошибке округления чисел с плавающей запятой.
Вы никогда не должны непосредственно сравнивать два числа с плавающей запятой.
Наконец, обратите внимание, что в этом случае скалярное произведение этих двух векторов попадёт в диапазон [-1, 1].
Если же вам это не совершенно очевидно, вы должны знать как минимум базовые вещи из линейной алгебры для программирования компьютерной графики!
По крайней мере, вы должны просмотреть Примеры кватернионов и вращения и обратиться к книгам, разжёвывающим базовые операции над векторами и матрицами.
Теперь наш код завершён! Скомпилируйте и запустите приложение и посмотрите, как робот ходит по точкам, которые вы прописали.
Упражнения
Простые задачи
1. Добавьте больше точек к пути робота.
Не забудьте так же добавить дополнительные узлы в эти позиции, чтобы вы смогли отслеживать, куда он может пойти.
2. Роботы, отжившие свой срок, больше не должны ничего делать!
Когда робот закончит идти, примените к нему анимацию смерти вместо анимации "ничего-не-делания". Анимация смерти называется 'Die'.
Усложнённые задачи
1. Что-то не так с переменной mWalkSpeed.
Вы обратили на это внимание, когда проходили урок?
Мы устанавливаем её значение только один раз и больше никогда не меняем его. Поэтому она может быть константной статической переменной класса.
Измените её описание, чтобы так и было.
2. Код сейчас очень не оптимален, он определяет, движется ли робот или нет путём сравнения вектора mDirection с нулевым вектором Vector3::ZERO.
Значительно лучше было бы определить булевскую переменную mWalking, котороя отслеживала бы, движется робот или нет.
Реализуйте эти изменения.
Задачи повышенной трудности
1. Одним из ограничений этого класса является то, что вы не можете добавить точки к пути следования робота после того, как вы создадите объект.
Устраните эту проблему, реализовав метод, принимающий в качестве параметра значение типа Vector3 и добавляющий его в очередь mWalkList.
(Подсказка: если робот ещё не закончил идти, вы просто должны добавить точку в конец очереди.
Если же робот остановился, вы должны заставить его опять идти - вызовите для этого метод nextLocation().)
Задачи для гуру
1. Ещё одно существенное ограничение данного класса состоит в том, что он отслеживает только один объект.
Исправьте класс таким образом, чтобы он позволял перемещать и анимировать любое количество объектов независимо друг от друга.
(Подсказка: вы должны создать вспомогательный класс, которые содержит все данные, необходимые для анимации одного объекта.
Сохраните объекты этого класса в карте STL std::map - таким образом вы сможете позже получить данные по ключу.)
Вы получите бонусные баллы, если сделаете это без регистрации дополнительных слушателей кадра.
2. После применения предыдущих изменений, вы можете заметить, что теперь роботы сталкиваются друг с другом.
Исправьте это путём создания умной функции поиска пути, либо определяйте момент столкновения роботов и останавливайте их, чтобы они не прошли друг через друга.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/20063/65473
Переводчики: Mingun, ghostintheshell, proVIDec
Настройки
Готово:
