Tutorial Intermedio 1 Animación, Caminando entre Puntos, y Quaterniones Básicos


external image help.gifCuaquier problema que encuentres mientras estas trabajando con este tutorial deberías preguntarlo en la Ayuda del Foro.

Introducción


En este tutorial cubriremos como coger una Entity, y animarla, y hacer que ande entre unos puntos predefinidos. También cubre conceptos básicos sobre la rotación de Quaterniones mostrando como mantener la cara de la Entity en la direccion del movimiento. Según vayas realizando el demo deberías ir añadiendo lentamente código a tu proyecto y mirar los resultados.

Puedes ver el estado final de este tutorial aquí.

Prerrequisitos


Este tutorial asume que sabes como crear un proyecto con Ogre y compilarlo correctamente. Este tutorial también hace uso de la estructura STL deque. No necesitas saber como usar deque, sólo debes saber sobre las plantillas (templates). Si no estas familiarizado con STL, recomiendo que compres STL Pocket Reference [ISBN 0-596-00556-3]. Que te ahorrará mucho tiempo en el futuro.

Puedes leer esta primera parte en "STL Pocket Reference" aquí.

Comenzando


Primero, necesitas crear un nuevo proyecto (Yo lo he llamado ITutorial01) y añade el siguiente código:

cabecera 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; // Distancia del objeto por la izquierda
 Ogre::Vector3 mDirection; // Direccion en la que el objeto se esta moviendo
 Ogre::Vector3 mDestination; // El destino en al que el objeto se esta moviendo
 Ogre::AnimationState *mAnimationState; // El estado actual de la animacion del objeto
 Ogre::Entity *mEntity; // La Entity que estamos animando
 Ogre::SceneNode *mNode; // El SceneNode al que la Entity esta acoplado
 std::deque<Ogre::Vector3> mWalkList; // La lista de los puntos por los que estamos caminando
 
 Ogre::Real mWalkSpeed; // La velocidad a la que el objeto se esta moviendo
 
};
 
#endif // #ifndef ITutorial01_h_

ITutorial01 implementation
#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
 {
 // Crea objeto de aplicacion
 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

Asegúrate de que puedes compilar este código antes de continuar.

Construyendo la Scene


Antes de que comencemos, nota que has definido 3 variables en el archivo de cabecera. mEntity que guardará la entidad que hemos creado, mNode que guardará el nodo que hemos creado, y mWalkList que contendrá todos los puntos por los que deseamos que el objeto ande.

Ve a la función ITutorial01::createScene y añade el siguiente código. Primero establecemos la luz ambiental al máximo para que podamos ver los objetos que ponemos en la pantalla.
// Establecemos la iluminacion por defecto.
 mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0f, 1.0f, 1.0f));
A continuación creamos un Robot en la pantalla para que así podamos jugar con él. Para hacer esto creamos la entity para el Robot, entonces creamos un SceneNode para él.
 // Crear la entity
 mEntity = mSceneMgr->createEntity("Robot", "robot.mesh");
 
 // Create the scene node
 mNode = mSceneMgr->getRootSceneNode()->
 createChildSceneNode("RobotNode", Ogre::Vector3(0.0f, 0.0f, 25.0f));
 mNode->attachObject(mEntity);
Esto debería ser todo básico, así que no entraré en detalles. En el siguiente trozo de código, le diremos al robot donde tiene que moverse. Para quienes no sepan sobre STL, el objeto deque es una implementación eficiente de una cola doblementemente enlazada. Y solamente usaremos unos pocos de sus métodos. Los métodos push_front y push_back ponen objetos al comienzo y final de la deque respectivamente. Los métodos front y back devuelven los valores de comienzo y final de la deque respectivamente. Los métodos pop_front y pop_back eliminan los objetos del comienzo y del final de la cola respectivamente. Finalmente, el método empty devuelve si la deque esta vacía. Este código añade dos vectores a la deque, lo que hara que más tarde el robot se mueva.
// Crea la lista para andar
mWalkList.push_back(Ogre::Vector3(550.0f, 0.0f, 50.0f ));
mWalkList.push_back(Ogre::Vector3(-100.0f, 0.0f, -200.0f));
Lo siguiente, queremos colocar algunos objetos en la escena para mostrar donde se supone que el robot se esta moviendo. Esto nos permitira ver el movimiento del robot con respecto a otros objetos de la pantalla. Nota la componente negativa de Y a su posición. Esto pone los objetos debajo de donde el robot se esta moviendo, y hará que se levanten cuando consigan el punto correcto.
// Creamos objetos para ver el movimiento
 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);
Finalmente, queremos establecer la cámara para tener un buen punto de vista desde ella. Moveremos la cámara para conseguir una mejor posición.
 // Establece la camara para que apunte hacia nuestro trabajo
 mCamera->setPosition(90.0f, 280.0f, 535.0f);
 mCamera->pitch(Ogre::Degree(-30.0f));
 mCamera->yaw(Ogre::Degree(-15.0f));
Ahora compila y ejecuta el código.

Animación


Ahora vamos con la animación. La animación en Ogre es muy sencilla. Para hacer esto, necesitas conseguir un AnimationState desde un objeto Entity, establecer sus opciones, y activarlo. Esto hará que la animación se active, pero necesitas añadir el tiempo después de cada frame para que la animación funcione. Nos tomará un paso cada vez. Primero, ve a
ITutorial01::createFrameListener y añade el siguiente código, después de llamar a BaseApplication::createFrameListener:
// Establecer animacion vacia (idle)
 mAnimationState = mEntity->getAnimationState("Idle");
 mAnimationState->setLoop(true);
 mAnimationState->setEnabled(true);
La segunda línea consigue el AnimationState de la entity. En la tercera línea llamamos a setLoop( true ), lo que hace un bucle de animación una y otra vez. Para algunas animaciones (como la animación de la muerte), querríamos establecer esto a falso en su lugar. La cuarta línea activa la Animación. Pero espera... de donde conseguimos el idle? como apareció esta constante mágica? Cada malla tiene su propio conjunto de Animaciones definidas. Para ver todas estas animaciones en las que estas trabajando, necesitas descargar el OgreMeshViewer y mirar la malla desde allí.
Ahora, si compilamos y ejecutamos el demo veremos... nada a cambiado. Esto es porque necesitamos actualizar el estado de la animación con un tiempo para cada frame. Busca el método ITutorial01::frameRenderingQueued, y añade esta línea de código al comienzo de la función:
mAnimationState->addTime(evt.timeSinceLastFrame);
Ahora compila y ejecuta la aplicación. Deberías ver un robot realizando esta animación vacía permaneciendo en el lugar.

Moviendo el Robot


Ahora vamos a realizar la tarea para hacer que el robot ande de un punto a otro. Antes de que comencemos me gustaría describir las variables que hemos definido. Vamos a usar 4 variables para completar la tarea de mover el robot. Lo primero de todo, vamos a guardar la dirección del robot en la que se esta moviendo mDirection. Guardaremos el destino actual del Robot en su viaje a mDestination. Guardaremos la distancia que le resta al robot por viajar en mDistance. Finalmente, guardaremos la velocidad del movimiento en mWalkSpeed.

La primera cosa que necesitamos hacer es establecer estas variables. Estableceremos la velocidad para andar en 35 unidades por segundo. Hay una cosa que debemos notar. Estamos ajustando explícitamente mDirection a ser el vector ZERO porque más tarde usaremos esto para determinar si el Robot se mueve o no. Añade el siguiente código a ITutorial01::createFrameListener:
 // Establece los valores por defecto de las variables
 mWalkSpeed = 35.0f;
mDirection = Ogre::Vector3::ZERO;
Ahora que esto esta hecho, necesitamos establecer el robot en movimiento. Para hacer que el robot se mueva, simplemente tenemos que decirle como cambia la animación. Sin embargo, sólo queremos que comience el movimiento del robot si hay otra posición a la que moverse. Por esta razon llamamos a la función ITutorial01::nextLocation. Anyade este código a lo alto del método ITutorial01::frameRenderingQueued justo antes de la llamada a AnimationState::addTime:
if (mDirection == Ogre::Vector3::ZERO)
 {
 if (nextLocation())
 {
 // Estableciendo la animacion de caminar
 mAnimationState = mEntity->getAnimationState("Walk");
 mAnimationState->setLoop(true);
 mAnimationState->setEnabled(true);
 }
 }
Si compilas y ejecutas el código ahora, el robot andará en el sitio. Esto es así porque el robot comienza con una dirección de ZERO y nuestra función ITutorial01::nextLocation siempre devuelve true. En los últimos pasos añadiremos un poco más de inteligencia a la función ITutorial01::nextLocation.

Ahora vamos a mover el robot en la escena. Para hacer esto necesitamos moverle un poquito en cada frame. Ve al método ITutorial01::frameRenderingQueued. Añadiremos el siguiente código justo después de nuestra declaración de if anterior y sólo antes de la llamada a AnimationState::addTime call. Este código manejará en este caso cuando el robot esta movimiéndose; mDirection != Ogre::Vector3::ZERO.
La razón por la que mWalkspeed esta multiplicado por evt.timeSinceLastFrame, es para mantener el paso constante, sin variaciones en el número de frames.
Si sólo has escrito Real move = mWalkspeed, el robot debería andar lento en un ordenador lento, y rápido en uno rápido.

external image Rates.jpg
else
 {
 Ogre::Real move = mWalkSpeed * evt.timeSinceLastFrame;
 mDistance -= move;
Ahora, necesitamos comprobar y mirar si hemos sobrepasado la posición objetivo. Esto es, si mDistance es ahora menor que zero, tenemos que saltar el punto y establecer el movimiento al siguiente punto. Nota que estamos ajustando mDirection al vector ZERO. Si el método nextLocation no cambia mDirection (ej: va hacia la izquierda) entonces no podemos movernos.
if (mDistance <= 0.0f)
 {
 mNode->setPosition(mDestination);
 mDirection = Ogre::Vector3::ZERO;
Ahora que hemos movido el punto, necesitamos establecer el movimiento al siguiente punto. Una vez que sabemos si necesitamos movernos a otro punto o no, podemos establecer la animación apropiada; caminando si hay otro punto al que ir o esperando si no hay más puntos de destino. Esta es una manera simple de ajustar la animación en vacío si no hay más posiciones.
// Establece la animacion en base a si el robot tiene otro punto al que ir.
if (! nextLocation())
{
 // Establecer animacion espera (Idle)
 mAnimationState = mEntity->getAnimationState("Idle");
 mAnimationState->setLoop(true);
 mAnimationState->setEnabled(true);
}
else
{
 // El codigo de rotacion ira aqui despues
}
}
Nota que no tenemos la necesidad establecer la animación de caminar otra vez si hay más puntos en la cola para andar. Ya que el robot esta listo para andar en cualquier caso. Sin embargo, si el robot necesita ir a otro punto, entonces necesita que se le rote la cara a ese punto. Por ahora dejamos una marca de posición comentada en la claúsula else; recuerda que es el punto por el que regresaremos más tarde.

Esto toma en cuenta cuando estan muy cerca de alcanzar la posición objetivo. Ahora necesitamos manejar el caso normal, cuando sólo estamos en el camino de la posición pero todavía no hemos llegado. Para hacer esto trasladamos el robot en la dirección en la que estamos viajando, y lo movemos una cantidad especificada por la variable move. Esto se consigue añadiendo el siguiente código:
else
 {
 mNode->translate(mDirection * move);
 } // else
 } // if
Hemos hecho casi todo. Ahora nuestro código hace todo excepto establecer variables requeridas para el movimiento. Si podemos establecer correctamente las variables de movimiento nuestro Robot se moverá como se supone. Busca la funcion ITutorial01::nextLocation.
Esta función devuelve false cuando recorremos los puntos a los que ir. Esta sera la primera línea de nuestra función. (Nota que deberías devolver true al final de la función).
if (mWalkList.empty())
 return false;
Ahora tenemos que establecer las variables (todavía en el método nextLocation). Primero meteremos el vector dirección de deque. Estableceremos el vector dirección substrayendo la posición actual de SceneNode del destino. Tenemos un problema pienso. Recuerda como multiplicamos mDirection por la cantidad de movimiento en frameRenderingQueued? Si haces esto, necesitaremos que el vector de dirección sea unitario (esto es, que su longitud sea uno). La función normalise hace esto por nosotros, y devuelve la vieja longitud del vector. Fácil entonces, ya que también necesitamos establecer la distancia al destino.
mDestination = mWalkList.front(); // consigue el frente de la cola
 mWalkList.pop_front(); // elimina el frente de deque
 
  mDirection = mDestination - mNode->getPosition();
  mDistance = mDirection.normalise();
Ahora compila y ejecuta el código. Funciona! Suerte. El robot ahora camina por todos los puntos, pero siempre apunta en la dirección Ogre::Vector3::UNIT_X (su valor por defecto). Necesitamos cambiar la dirección a la que apunta cuando se esta moviendo a través de los puntos.

Lo que tenemos que hacer es conseguir la dirección a la que el Robot apunta, y usar la función rotate para rotar el objeto a la posición correcta. Inserta el siguiente código donde dejamos la marca comentada en el paso anterior. La primera línea consigue la dirección a la que el Robot esta apuntando. La segunda línea crea un Quaternion de rotación desde la dirección actual a la dirección de destino. La tercera línea rota el Robot.
Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X;
 Ogre::Quaternion quat = src.getRotationTo(mDirection);
 mNode->rotate(quat);
Hemos mencionado brevemente los Quaterniones en el Tutorial Básico 4, pero este es el primer uso real al que los hemos aplicado. Básicamente hablando, los Quaterniones son representaciones de rotaciones en el espacio tridimensional. Se usan para seguir la pista a como los objetos se posicionan en el espacio, y deben ser usados para rotar los objetos en Ogre. En la primera línea llamamos al método getOrientation, que devuelve un Quaternion que representa el modo en el que el Robot esta orientado en el espacio. Ya que Ogre no sabe que lado del Robot es el "frente" del robot, debemos multiplicar esta orientación por el vector UNIT_X (que es la dirección a la que el robot "naturalmente" apunta) para obtener la dirección del robot a la que esta apuntado. Guardamos esta dirección en la variable src. En la segunda línea, el método getRotationTo nos da un Quaternion que representa la rotación desde la dirección a la que el Robot esta apuntando a la dirección que queremos que apunte. En la tercera línea, rotamos el nodo para que asi apunte a la nueva orientación.

Hay sólo un problema con el código que hemos creado. Hay un caso especial donde SceneNode::rotate fallará. Si estamos intentando girar el robot 180 grados, el código de rotate nos dará un error de división por cero. Para arreglar esto, comprobaremos si estamos realizando una rotacion de 180 grados. Si es así, simplemente damos la vuelta al robot por 180 grados en vez de usar rotate. Para hacer esto, borra las tres líneas que pusimos y reemplázalas con esto:
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
Todo esto debería ser lo suficiente explicativo excepto por lo que esta envuelto en el if. Si dos vectores unidad son opuestos (esto es, el ángulo entre ellos es 180 grados), entonces su producto será -1. Así, si hacemos el producto de los dos vectores juntos y el resultado es -1.0f, entonces necesitaremos yaw por 180 grados, sino usaremos rotate. Por que añado 1.0f y compruebo si es menor que 0.0001f? No te olvides del error de redondeo de punto flotante. Nunca deberías comparar directamente dos numeros en punto flotante. Finalmente, nota que en este caso el producto de estos dos vectores caerá en el rango -1,1. En este caso deberás saber algo de álgebra linear básica para hacer programación gráfica! Al menos deberías revisar Quaterniones y Rotaciones y consultar un libro sobre operaciones con matrices y vectores.

Nuestro código esta completo! Compila y ejecuta la demo para ver como el Robot anda por los puntos que le dimos.

Ejercicios para el Estudio


Preguntas Fáciles


  1. Añade más puntos a la ruta del robot. Asegúrate de que también añades más nodos debajo de su posición para que puedas decirle donde ir.
  2. Los robots que ya no tienen utilidad deberían dejar de existir! .Cuando el robot ha terminado de andar, tienes que realizar una animación (su muerte) en vez de esperar.

Preguntas Intermedias


  1. Hay algún fallo con mWalkSpeed. Te distes cuenta de esto a lo largo del tutorial? Sólo establecimos un valor, y nunca cambia. Esto debería ser una variable constante de la clase estática. Cambia la variable.
  2. El código hace algo muy raro, y sigue la pista de si el Robot esta andando mirando al vector mDirection y comparandolo con el Vector3::ZERO. Podría haber sido mejor si en vez de tener una variable boleana llamada mWalking que guarde la pista de si el robot se esta moviendo?. Implementa este cambio.

Preguntas Difíciles


  1. Una de las limitaciones de esta clase es que no puedes añadir puntos a la ruta andada por el robot después de que hayas creado el objeto. Arregla este problema implementando un nuevo método que tome un Vector3 y lo añada a la cola mWalkList deque. (Si el robot no ha terminado de andar sólo necesitarás añadir el punto al final de deque. Si el robot ha terminado, necesitarás hacerle que comience a andar de nuevo, y llama a nextLocation para que comience a andar otra vez.)

Preguntas Expertas


  1. Otra limitación de esta clase es que sólo sigue un objeto. Reimplementa esta clase para que pueda mover y animar cualquier número de objetos independientemente de cualquier otro. (deberías crear otra clase que contenga todo lo que necesita saber para animar un objeto completamente. Guárdalo en un objeto de mapa STL para que puedas devolver los datos más tarde con un tecla.) Puedes conseguir puntos de bonificación si puedes hacer esto sin registrar ningun frame listener adicional.
  2. Después de hacer el cambio anterior, habrás notado que los Robots pueden ahora colisionar entre ellos. Arregla esto creando una función para encontrar las rutas, o detectando cuando los robots colisionan y pararlos antes de que se atraviesen.

Siguiente, Tutorial Intermedio 2 RaySceneQueries y Uso del Ratón Básico