Основное руководство 1
Анимация, хождение между точками и базовые кватернионы
Введение
В этом уроке мы рассмотрим, как получить объект сущность(Entity), анимировать ее и заставить двигаться между предопределёнными точками. Также мы рассмотрим основы вращения через кватернионы и посмотрим, как заставить сущность смотреть в направлении её движения. Во время прохождения урока, вы должны постепенно добавлять код к вашему проекту и наблюдать результаты после его компиляции.
Предварительные условия
Этот урок предполагает, что вы уже знаете, как настроить проект, использующий
Ogre и заставить его успешно компилироваться. Этот урок также использует такую структуру данных из
STL, как двустороннюю очередь
std::deque. Хотя вам и не требуется никаких предварительных знаний о том, как использовать двустороннюю очередь, вы, по крайней мере, должны знать, что такое шаблоны. Если вы не знакомы с
STL, я бы порекомендовал вам почитать STL Pocket Reference [ISBN 0-596-00556-3] (в русском переводе известна как "STL. Карманный справочник" [ISBN 5-469-00389-2] - прим. пер.). В будущем это сохранит вам много времени.
Вы можете прочитать первую часть "STL Pocket Reference"
здесь (на английском языке - прим. пер.).
Для начала
Во-первых, Вы должны создать новый проект (я назвал его ITutorial01), и добавить следующий код:
#ifndef __ITutorial01_h_
#define __ITutorial01_h_
#include "BaseApplication.h"
#include <deque>
class ITutorial01 : public BaseApplication
{
public:
ITutorial01(void);
virtual ~ITutorial01(void);
protected:
virtual void createScene(void);
virtual void createFrameListener(void);
virtual bool nextLocation(void);
virtual bool frameRenderingQueued(const Ogre::FrameEvent &evt);
Ogre::Real mDistance; // Расстояние, которое осталось пройти объекту
Ogre::Vector3 mDirection; // Направление движения объекта
Ogre::Vector3 mDestination; // Назначение в направлении которого движется объект
Ogre::AnimationState *mAnimationState; // Текущее состояние анимации объекта
Ogre::Entity *mEntity; // Сущность, которую мы анимируем
Ogre::SceneNode *mNode; // Узел, к которому привязана сущность
std::deque<Ogre::Vector3> mWalkList; // Список точек между которыми движется объект
Ogre::Real mWalkSpeed; // Скорость с которой движется объект
};
#endif // #ifndef __ITutorial01_h_
#include "ITutorial01.h"
//-------------------------------------------------------------------------------------
ITutorial01::ITutorial01(void)
{
}
//-------------------------------------------------------------------------------------
ITutorial01::~ITutorial01(void)
{
}
//-------------------------------------------------------------------------------------
void ITutorial01::createScene(void)
{
}
void ITutorial01::createFrameListener(void){
BaseApplication::createFrameListener();
}
bool ITutorial01::nextLocation(void){
return true;}
bool ITutorial01::frameRenderingQueued(const Ogre::FrameEvent &evt){
return BaseApplication::frameRenderingQueued(evt);
}
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char *argv[])
#endif
{
// Создать объект приложение
ITutorial01 app;
try {
app.go();
} catch( Ogre::Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
std::cerr << "An exception has occured: " <<
e.getFullDescription().c_str() << std::endl;
#endif
}
return 0;
}
#ifdef __cplusplus
}
#endif
Перед продолжением убедитесь, что этот код компилируется.
Настройка сцены
Прежде, чем мы начнём, обратите внимание на то, что в заголовочном файле мы уже определили три переменные. Переменная mEntity будет хранить нашу сущность, переменная mNode будет хранить узел сцены, а переменная mWalkList будет содержать все точки, через которые должен пройти наш объект.
Сначала мы установим окружающее освещение на полную мощность для того, чтобы мы могли видеть размещаемые объекты. Перейдите в функцию ITutorial01::createScene() и добавьте в неё следующий код:
// Установить глобальный свет
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0f, 1.0f, 1.0f));
Затем создадим робота, чтобы нам было с кем играть.
// Создать сущность
mEntity = mSceneMgr->createEntity("Robot", "robot.mesh");
// Создать узел
mNode = mSceneMgr->getRootSceneNode()->
createChildSceneNode("RobotNode", Ogre::Vector3(0.0f, 0.0f, 25.0f));
mNode->attachObject(mEntity);
Для этого мы создадим сущность, а затем объект SceneNode, к которому её и привяжем. Это всё базовые вещи, так что я не буду вдаваться в подробности. В следующей части кода мы собираемся сказать роботу, куда он должен переместиться. Для тех из вас, кто ничего не знает о STL, объект deque - это эффективная реализация двусторонней очереди. Мы будем использовать только некоторые из его методов. Методы push_front() и push_back() помещают элементы в начало и конец очереди соответственно. Методы front() и back() возвращают значения, соответственно, с начала и с конца очереди. Методы pop_front() и pop_back() удаляют элементы из начала и из конца очереди. Наконец, метод empty() сообщает, пуста ли очередь или нет. Этот код добавляет два вектора позиций в очередь, к которым позже будет двигаться наш робот:
// Создать список точек прохождения
mWalkList.push_back(Ogre::Vector3(550.0f, 0.0f, 50.0f ));
mWalkList.push_back(Ogre::Vector3(-100.0f, 0.0f, -200.0f));
Далее мы размещаем в сцене некоторые объекты, которые будут показывать, куда должен будет двигаться наш робот. Это позволит нам видеть перемещения робота относительно других объектов на экране. Обратите внимание на отрицательную компоненту Y в их позиции. Это сделано для того, чтобы поместить объекты под тем местом, в которое должен двигаться робот и поэтому он будет стоять сверху них, когда дойдёт до него.
// Создать объекты, для визуального отображения точек прохождения
Ogre::Entity *ent;
Ogre::SceneNode *node;
ent = mSceneMgr->createEntity("Knot1", "knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot1Node", Ogre::Vector3(0.0f, -10.0f, 25.0f));
node->attachObject(ent);
node->setScale(0.1f, 0.1f, 0.1f);
ent = mSceneMgr->createEntity("Knot2", "knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot2Node", Ogre::Vector3(550.0f, -10.0f, 50.0f));
node->attachObject(ent);
node->setScale(0.1f, 0.1f, 0.1f);
ent = mSceneMgr->createEntity("Knot3","knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot3Node",Ogre::Vector3(-100.0f,-10.0f,-200.0f));
node->attachObject(ent);
node->setScale(0.1f, 0.1f, 0.1f);
Наконец, мы помещаем камеру в точку, из которой хорошо видно всё происходящее. Мы просто передвинем камеру в эту наилучшую позицию.
// Установить камеру смотреть на то, что мы наляпали
mCamera->setPosition(90.0f, 280.0f, 535.0f);
mCamera->pitch(Ogre::Degree(-30.0f));
mCamera->yaw(Ogre::Degree(-15.0f));
Теперь скомпилируйте код и запустите приложение.
Анимация
Сейчас мы переходим к настройке базовой анимации. Анимация в Ogre очень проста. Для её настройки вы должны получить объект AnimationState от объекта Entity, установить его параметры и активировать. Это сделает анимацию активной, но вы также должны будете каждый кадр добавлять к ней время, чтобы она работала. Мы всё сделаем за один раз. Сначала перейдите в метод ITutorial01::createFrameListener() и добавьте следующий код после вызова BaseApplication::createFrameListener():
// Задать анимацию бездействия
mAnimationState = mEntity->getAnimationState("Idle");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
Во второй строке мы получаем объект AnimationState из сущности. В третьей строке мы вызываем метод setLoop(true), который зацикливает анимацию. Для некоторых анимаций (например, анимации смерти) мы должны бы были установить этот параметр в false. Четвёртая строка фактически включает анимацию. Но подождите... откуда мы взяли 'Idle'? Как туда проскользнула эта магическая константа? Каждый меш имеет свой собственный набор анимаций, определённых специально для него. Чтобы видеть все анимации для особого mesh, Вы должны загрузить OgreMeshViewer и рассмотреть цикл оттуда.
Теперь, если мы скомпилируем и запустим приложение, мы увидим... что ничего не изменилось. Это потому, что мы должны обновлять состояние анимации каждый кадр. Найдите метод ITutorial01::frameRenderingQueued() и добавьте эту строку кода в начало функции:
mAnimationState->addTime(evt.timeSinceLastFrame);
Теперь скомпилируйте и запустите приложение. Вы должны увидеть, что робот выполняет свою анимацию "ничего-не-деланья", стоя на месте.
Перемещение робота
Теперь мы собираемся выполнить сложную задачу создания прогулки робота от пункта до пункта. Прежде, чем мы начнем, я хотел бы описать переменные, которые мы определили. Для реализации задачи перемещения робота мы будем использовать четыре переменные. Прежде всего, мы должны сохранить направление, в котором перемещается робот, в переменной mDirection. Текущую точку назначения робота в его путешествии сохраним в переменной mDestination. В переменной mDistance мы сохраним расстояние, которое роботу всё ещё нужно пройти до пункта назначения. И, наконец, в переменной mWalkSpeed мы сохраним скорость движения робота.
Первая вещь, которую мы должны сделать, состоит в том, чтобы настроить эти переменные. Мы установим скорость ходьбы в 35 единиц в секунду. Здесь нужно обратить внимание на одну очень важную вещь. Переменную mDirection мы явно устанавливаем в нулевой вектор, потому что позже мы будем использовать её для определения того, двигается ли робот или нет. Добавьте следующий код к методу ITutorial01::createFrameListener():
// Установить стандартное значение переменных
mWalkSpeed = 35.0f;
mDirection = Ogre::Vector3::ZERO;
Теперь, когда это сделано, мы должны привести робота в движение. Чтобы заставить робота двигаться, мы просто сменим ему анимацию. Однако нам нужно начинать перемещение робота только в том случае, если есть позиция, куда он может двигаться. По этой причине мы вызываем функцию ITutorial01::nextLocation. Добавьте этот код в самый верх метода ITutorial01::frameRenderingQueued() прямо перед вызовом метода AnimationState::addTime():
if (mDirection == Ogre::Vector3::ZERO)
{
if (nextLocation())
{
// Задать анимацию хождения
mAnimationState = mEntity->getAnimationState("Walk");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
}
}
Если вы сейчас скомпилируете и запустите приложение, робот будет шагать на месте. Это произойдёт потому, что робот начинает идти в нулевом направлении и наша функция ITutorial01::nextLocation() всегда возвращает true. В последующих шагах мы добавим немного больше интеллекта функции ITutorial01::nextLocation().
Теперь мы собираемся перемещать робота по сцене. Для этого мы должны двигать его на небольшое расстояние каждый кадр. Перейдите в метод ITutorial01::frameRenderingQueued. Мы будем добавлять следующий код между предыдущим оператором if и вызовом метода AnimationState::addTime(). Этот код будет обрабатывать тот случай, когда робот действительно движется, т.е. когда mDirection !== Ogre::Vector3::ZERO.
Причина, по которой mWalkspeed умножается на evt.timeSinceLastFrame состоит в том, что в этом случае скорость ходьбы останется неизменной при различной частоте кадров.
Если бы мы написали Real move = mWalkspeed, робот бы ходил медленнее на слабых машинах и быстрее на мощных.
else
{
Ogre::Real move = mWalkSpeed * evt.timeSinceLastFrame;
mDistance -= move;
Теперь нам нужно проверить, дошли ли мы до конечной точки. Если mDistance меньше нуля, мы должны прыгнуть в точку и продолжить движение к следующей точке. Обратите внимание, что мы устанавливаем mDirection в нулевой вектор. Если метод nextLocation() не изменяет mDirection (что означает, что больше некуда идти), мы не должны больше двигаться.
if (mDistance <= 0.0f)
{
mNode->setPosition(mDestination);
mDirection = Ogre::Vector3::ZERO;
Теперь, когда мы передвинулись в данную точку, мы должны настроить движение к следующей точке. Когда мы узнаем, надо нам двигаться в следующую точку или нет, мы можем установить подходящую анимацию - анимацию ходьбы, если есть следующая точка и анимацию бездействия, если такой точки нет. Если больше нет точек назначения, то это просто вопрос установки анимации бездействия.
// Set animation based on if the robot has another point to walk to.
if (! nextLocation())
{
// Set Idle animation
mAnimationState = mEntity->getAnimationState("Idle");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
}
else
{
// Здесь будет код поворота
}
}
Обратите внимание: нам не нужно устанавливать анимацию ходьбы, если есть дополнительные точки в очереди, к которым нужно пройти. Поскольку робот уже идёт, нет никаких оснований говорить делать ему тоже самое. Однако, если робот должен пойти в другой пункт, мы должны повернуть его таким образом, чтобы он смотрел в эту точку. Пока мы просто оставим комментарий в предложении else; запомните это место, поскольку мы позже к нему вернёмся.
Вышенаписанный код обрабатывает ситуацию, когда мы находимся близко к цели. Теперь же мы должны обработать нормальный случай, когда мы находимся на пути к цели, но ещё не там. Для этого мы переместим робота в направлении его передвижения на величину, определяемую переменной перемещения. Это достигается путём добавления следующего кода:
else
{
mNode->translate(mDirection * move);
} // else
} // if
Мы сделали почти всё. Наш код делает всё, за исключением установки переменных, необходимых для движения. Если мы правильна установим переменные передвижения, наш робот будет двигаться так, как и предполагалось. Найдите функцию ITutorial01::nextLocation(). Эта функция возвращает false, когда мы находимся в точке назначения. Это будет первая строка нашей функции. (Обратите внимание, что вы должны оставить оператор return true в конце функции.)
if (mWalkList.empty())
return false;
Теперь мы должны установить переменные (это всё ещё метод nextLocation()). Сначала мы достаём вектор назначения из очереди. Мы устанавливаем вектор направления путём вычитания текущей позиции SceneNode из вектора назначения. Тут у нас возникает проблема. Помните, что мы умножали mDirection на значение перемещения в методе frameRenderingQueued()? Если мы сделаем это, мы должны быть уверены, что вектор направления является единичным вектором (что означает, что его длина равна единице). Функция normalise() делает его единичным за нас и возвращает старую длину вектора. Это удобно, поскольку нам так же нужно узнать расстояние до пункта назначения.
mDestination = mWalkList.front(); // получает начало списка
mWalkList.pop_front(); // удаляет начало списка
mDirection = mDestination - mNode->getPosition();
mDistance = mDirection.normalise();
Теперь скомпилируйте и запустите приложение. Оно работает! Вроде... Робот теперь ходит по всем точкам, однако он постоянно смотрит в одном направлении - Ogre::Vector3::UNIT_X (его ориентация по умолчанию). Мы должны изменить направление его поворота, когда он движется между точками.
Что нам нужно сделать - это получить направление поворота робота и использовать функцию rotate() для вращения объекта до придания ему правильного положения. Вставьте следующий код туда, где мы оставили комментарий на предыдущем шаге. В первой строке мы получаем направление поворота робота. Во второй строке мы создаём кватернион, представляющий вращение из текущего направления в направление к цели. В третьей строке, собственно, робот и поворачивается.
Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X;
Ogre::Quaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
Мы кратко упоминали кватернионы в Базовом уроке 4, но только сейчас столкнулись с их реальным использованием. Проще говоря, кватернионы представляют вращение в трёхмерном пространстве. Они используются для слежения за позиционированием объектов в пространстве и могут использоваться в Ogre для их вращения. В первой строке мы вызвали метод getOrientation(), который возвращается объект Quaternion, представляющий ориентацию робота в пространстве. Поскольку Ogre не имеет понятия, какая сторона робота является "лицевой", мы должны умножить эту ориентацию на вектор UNIT_X (который задаёт направление "естественной" ориентации робота) для получения направления, в котором робот сейчас смотрит. Мы сохраняем это направление в переменной src. Во второй строке метод getRotationTo() возвращает кватернион, который представляет вращение из направления, куда сейчас смотрит робот, в направление, куда мы хотим его повернуть. В третьей строке мы вращаем узел таким образом, что грани робота приобретают новую ориентацию.
В этом коде, который мы написали, существует одна проблема. В некоторых случаях метод SceneNode::rotate() потерпит неудачу. Если мы попытаемся повернуть робота на 180 градусов, код вращения упадёт с ошибкой деления на ноль. Для исправления этой ошибки, мы проверим, вращаемся ли мы на 180 градусов. Если это так, мы просто применим метод yaw() для поворота робота на 180 градусов вместо применения метода rotate(). Для этого удалите три строки, которые мы только что поместили сюда и замените их на эти:
Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X;
if ((1.0f + src.dotProduct(mDirection)) < 0.0001f)
{
mNode->yaw(Ogre::Degree(180));
}
else
{
Ogre::Quaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
} // else
Здесь всё должно быть понятно, за исключением, быть может, агрумента в операторе if. Если два единичных вектора противоположно направлены (т.е. угол между ними равен 180 градусам), то их скалярное произведение даст в результате -1. Итак, если скалярное произведение двух векторов равно -1, мы вращаем робота методом yaw() на 180 градусов, в противном случае используем метод rotate(). Почему же я добавляю 1.0f и проверяю, что результат меньше 0.0001f? Не забывайте об ошибке округления чисел с плавающей запятой. Вы никогда не должны непосредственно сравнивать два числа с плавающей запятой. Наконец, обратите внимание, что в этом случае скалярное произведение этих двух векторов попадёт в диапазон [-1, 1]. Если же вам это не совершенно очевидно, вы должны знать как минимум базовые вещи из линейной алгебры для программирования компьютерной графики! По крайней мере, вы должны просмотреть Примеры кватернионов и вращения и обратиться к книгам, разжёвывающим базовые операции над векторами и матрицами.
Теперь наш код завершён! Скомпилируйте и запустите приложение и посмотрите, как робот ходит по точкам, которые вы прописали.
Дополнительная информация
Хождение по местности (оно же "изменение оси y")
Код для вращения робота не будет работать должным образом, при изменении оси Y у робота. Вы можете получить неожиданные вращения.
Это может быть исправлено, как в приведенном ниже коде:
Vector3 mDestination = mWalkList.front( ); // mDestination - следующая точка
Vector3 mDirection = mDestination - mNode->getPosition(); // B-A = A->B (смотрите про вектора выше)
Vector3 src = mNode->getOrientation() * Vector3::UNIT_X; // Ориентация от начального положения
src.y = 0; // Игнорировать разницу наклона угла
mDirection.y = 0;
src.normalise();
Real mDistance = mDirection.normalise( );
Quaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
Мой робот в начале не повернут лицом в нужное направление!
Робот поворачивается, лишь когда прибывает в первую точку. Для того, чтобы повернуть робота во всех случаях, лучше переместить код вращения в новую функцию:
//put this line to the header
virtual rotateRobotToDirection(void);
//this to the application itself
void ITutorial01::rotateRobotToDirection(void)
{
Vector3 src = mNode->getOrientation() * Vector3::UNIT_X; // Ориентация от начального положения
src.y = 0; // Игнорировать разницу наклона угла
mDirection.y = 0;
src.normalise();
Real mDistance = mDirection.normalise( );
Quaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
}
Упражнения
Простые задачи
1. Добавьте больше точек на пути у робота. Не забудьте так же добавить дополнительные узлы в эти позиции, чтобы вы смогли отслеживать, куда он может пойти.
2. Роботы, отжившие свой срок, больше не должны ничего делать! Когда робот закончит идти, примените к нему анимацию смерти вместо анимации "ничего-не-делания". Анимация смерти называется 'Die'.
Усложнённые задачи
1. Что-то не так с переменной mWalkSpeed. Вы обратили на это внимание, когда проходили урок? Мы устанавливаем её значение только один раз и больше никогда не меняем его. Поэтому она может быть константной статической переменной класса. Измените её описание, чтобы так и было.
2. Код сейчас очень не оптимален, он определяет, движется ли робот или нет путём сравнения вектора mDirection с нулевым вектором Vector3::ZERO. Значительно лучше было бы определить булевскую переменную mWalking, котороя отслеживала бы, движется робот или нет. Реализуйте эти изменения.
Задачи повышенной трудности
1. Одним из ограничений этого класса является то, что вы не можете добавить точки к пути следования робота после того, как вы создадите объект. Устраните эту проблему, реализовав метод, принимающий в качестве параметра значение типа Vector3 и добавляющий его в очередь mWalkList. (Подсказка: если робот ещё не закончил идти, вы просто должны добавить точку в конец очереди. Если же робот остановился, вы должны заставить его опять идти - вызовите для этого метод nextLocation().)
Наисложнейшие задачи
1. Ещё одно существенное ограничение данного класса состоит в том, что он отслеживает только один объект. Исправьте класс таким образом, чтобы он позволял перемещать и анимировать любое количество объектов независимо друг от друга. (Подсказка: вы должны создать вспомогательный класс, которые содержит все данные, необходимые для анимации одного объекта. Сохраните объекты этого класса в карте STL std::map - таким образом вы сможете позже получить данные по ключу.) Вы получите бонусные баллы, если сделаете это без регистрации дополнительных слушателей кадра.
2. После применения предыдущих изменений, вы можете заметить, что теперь роботы сталкиваются друг с другом. Исправьте это путём создания умной функции поиска пути, либо определяйте момент столкновения роботов и останавливайте их, чтобы они не прошли друг через друга.
Главный переводчик: Mingun
Редактор: proVIDec (UnitArt)
unitart.org совместно с ogre3d.ru
Оригинальный вариант статьи на ogre3d.org