Animación

De IberOgre

Con "Creación básica de escenas", "Manipulación de nodos" y "Luces, sombras y entorno" aprendimos a poblar el árbol de escena y configurar el ambiente de la forma deseada. No obstante, la mayoría de videojuegos no están completos sin personajes animados. A menos que estemos ante un sistema muy estático (un juego tipo puzzle, quizás), los personajes deberán correr, saltar, atacar y llevar a cabo decenas de acciones que precisen animaciones internas además de externas.

En este artículo revisaremos las herramientas que nos proporciona Ogre para animar nuestras entidades. Por supuesto, veremos ejemplos sencillos para manipular, controlar y combinar animaciones. Por último, se adjunta un ejemplo de mayor envergadura ilustrando todo el contenido del texto.


Contenido



Requisitos mínimos

Este artículo alcanza un nivel intermedio de complejidad y para seguirlo sin experimentar problemas ni confusiones es recomendable que conozcas a fondo los contenidos de artículos anteriores de IberOgre. Especialmente deberías prestar atención a:

  • Gestión de recursos: vamos a utilizar entidades (modelos 3D), esqueletos, texturas y otros recursos. Por tanto, es recomendable que comprendas los mecanismos para la gestión de recursos en Ogre.
  • Creación básica de escenas: en los ejemplos cargaremos nodos sin mayores explicaciones. Por tanto, para comprender lo que ocurre en el sistema sería recomendable acudir al artículo correspondiente.
  • Manipulación de nodos: para las animaciones externas moveremos y rotaremos nodos de la escena. Si quieres conocer más detalles sobre estas operaciones es recomendable que acudas a este texto.


Conceptos básicos sobre animación en Ogre

Animación por fotogramas claves en Blender

El concepto y los mecanismos de la animación no han cambiado demasiado desde la época en la que sólo podíamos dibujar varias figuras en una libreta, pasar las páginas rápidamente y conseguir sensación de movimiento. Simplemente se basa en modificar una entidad de la escena cada fotograma ligeramente. Cuando el juego alcance unos 25 cuadros por segundo como mínimo habremos conseguido dar sensación de movimiento natural y continuo.

Ogre no controla las fuerzas que intervienen en la acción de levantar un brazo, se trata de una ilusión. Al igual que la mayoría de software de animación que existe, Ogre utiliza una serie de controladores que dependen del tiempo. Un controlador almacena información sobre la pose de un modelo (posición, orientación...), dicha información dependerá del tipo de animación que estemos utilizando.

Un fotograma clave podría definirse como el par de un controlador y un instante determinado. El concepto de fotograma clave viene de los tiempos de la animación manual. El maestro animador proporcionaba a sus trabajadores una serie de fotogramas clave en una secuencia para que éstos hicieran el trabajo "sucio" de rellenar los huecos intermedios con interpolaciones. Estos cuadros correspondían a los instantes de máxima importancia o amplitud de movimientos, eran las guías. Nosotros determinamos los fotogramas claves en nuestra herramienta de animación 3D y Ogre es el encargado de crear las poses intermedias en tiempo real.


Tipos de animación en Ogre

Ogre soporta principalmente dos tipos de animación y uno que supone una mezcla de los dos anteriores. Cada técnica no es superior al resto, simplemente será más adecuada para determinadas situaciones. Lo importante es comprender su funcionamiento y ser capaz de distinguir cual utilizar en cada momento.

Animación basada en esqueletos

La mascota de Ogre Simbad bailando a través de skeletal animation

La animación basada en esqueletos, popularmente conocida como skeletal animation, es la que utilizaremos con mayor frecuencia. En este tipo de animación, los vértices de un modelo 3D están ligados a huesos. Estos huesos carecen de representación visual, simplemente son una transformación (posición, escala y rotación).

En cada fotograma clave de una animación basada en esqueleto se guarda la transformación de cada hueso. La posición de cada vértice del modelo se recalcula aplicando las transformaciones de los huesos que influyen sobre dicho vértice. Cada vértice puede verse afectado por más de un hueso de forma que cada hueso ejerce un peso sobre el mismo indicando su capacidad de manipularlo.

Huesos de la protagonista de Sintel, un corto libre creado con Blender

Los huesos asociados a un modelo se organizan de forma jerárquica en un esqueleto, lo cual es conceptualmente cercano a la realidad. Al igual que en un vertebrado, los huesos están conectados entre sí formando una jerarquía. El hueso raíz puede ser cualquiera aunque lo recomendable es que tenga sentido dentro del modelo en el que estemos trabajando. La jerarquía de huesos se utiliza para propagar transformaciones desde un nodo hasta las hojas de su rama. Por supuesto, la jerarquía y los pesos se definen en tu herramienta de modelado y animación 3D y más tarde exportarás tu trabajo.

Si tienes problemas para comprender cómo funciona la animación basada en esqueleto, es recomendable que abras los ficheros .mesh.xml y .skeleton.xml de cualquier modelo. Estas son las versiones en texto plano de la malla y el esqueleto, antes de ser procesables por Ogre. Podrás ver que los vértices de un modelo se definen como tres puntos relativos al centro del mismo. Las caras triangulares se definen como grupos de tres vértices utilizando sus índices. Para el esqueleto enumeramos los huesos y asociamos un grupo de vértices a un hueso con un peso determinado. Para cada animación tenemos un nombre y una serie de fotogramas clave con las transformaciones de cada hueso.

Animación basada en vértices

Ejemplo FacialAnimation de Ogre

En la animación basada en esqueletos tenemos vértices asociados a huesos y éstos huesos se animan a base de fotogramas clave. En cambio, en la animación basada en vértices (vertex animation) los huesos se obvian y son los propios vértices los que se animan. Esta técnica consume muchísimos más recursos que la anterior ya que para cada fotograma clave se guarda información de los vértices del modelo. No obstante, puede ser extremadamente útil y necesaria para animaciones precisas como las expresiones faciales.

Dentro de la animación basada en vértices contamos con dos sub-técnicas:

  • Morph: es la más básica, simple y menos eficiente de las dos. Guarda fotografías de todas y cada una de las posiciones de los vértices animados del modelo. Su ventaja es que es muy barato calcular las posiciones intermedias mediante interpolaciones sencillas. Como inconveniente adicional, no resulta posible mezclar dos animaciones de tipo morph al mismo tiempo, ya que las posiciones de los vértices se guardan de forma absoluta.
  • Pose: es más compleja aunque versátil que las animaciones morph. En este caso se almacena el desplazamiento de cada vértice animado con respecto a la posición de reposo. Esto permite combinar varias animaciones pose al mismo tiempo para producir otras más complejas.

Es posible observar las animaciones basadas en vértices en el ejemplo FacialAnimation distribuido junto a Ogre.

Animaciones combinadas

Simbad corriendo y atacando con sus espadas alk mismo tiempo

Ogre permite al usuario combinar múltiples animaciones al mismo tiempo para producir movimientos más naturales. Como ya hemos mencionado anteriormente, se nos presentan ciertas restricciones. No es posible mezclar animaciones pose y morph o dos de tipo morph. Imaginad que tenemos un personaje con animaciones basadas en esqueleto para correr y golpear con la espada. En la primera sólo mueve las piernas mientras que en la segunda utiliza el tren superior, esto nos permite activar las dos animaciones al mismo tiempo y hacer que el personaje golpee y corra a la vez. Podemos complicar la operación añadiendo una animación facial de tipo pose para que ponga una mueca agresiva.


La clase AnimationState

En el texto ya hemos comentado los diferentes tipos de animaciones que ofrece Ogre aunque a partir de ahora nos centraremos de forma exclusiva en las basadas en esqueleto. La razón es muy simple, son las más básicas, necesarias y sencillas de utilizar.

Los objetos AnimationState nos permiten controlar las animaciones de los objetos Entity. Con una herramienta de modelado y animación 3D como Blender creamos un personaje, colocamos su esqueleto y definimos sus animaciones a través de poses en fotogramas clave. Cada animación se identifica con un nombre, es decir, una pequeña cadena de texto. En la exportación a Ogre los nombres se mantienen y podemos pedirle a un Entity que nos devuelva el objeto AnimationState correspondiente a una animación.

Recuperar animaciones de una entidad

Para obtener una animación de una entidad empleamos el método Entity::getAnimationState como se indica a continuación.

Ogre::AnimationState* Ogre::Entity::getAnimationState(const Ogre::String& name) const;
  • const Ogre::String& name: nombre de la animación a recuperar.

En el caso de que no exista ninguna animación con dicho nombre se producirá una excepción. Lo normal es conocer qué animaciones posee cada modelo pero puede que lo debas comprobar en tiempo de ejecución. A tal efecto puedes utilizar el método Entity::getAllAnimationStates.

Ogre::AnimationStateSet* Ogre::Entity::getAllAnimationStates() const;

Esto nos devuelve un puntero a AnimationStateSet que podemos recorrer (consultar la documentación) o preguntarle directamente si posee alguna animación en concreto con AnimationStateSet::hasAnimationState.

bool Ogre::AnimationState::hasAnimationState(const Ogre::String& name) const;
  • const Ogre::String& name: nombre de la animación a consultar. Se devuelve true si la animación existe y false en caso contrario.

En el siguiente ejemplo partimos de que contamos con Ogre y sus subsistemas inicializados. Nos disponemos a cargar la mascota de Ogre, el marino Simbad y a consultar su lista de animaciones.

// Cargamos la entidad
Ogre::Entity* entity = sceneManager->createEntity("simbad", "simbad.mesh");
 
// Tomamos todas las animaciones
Ogre::AnimationStateSet* animations = entity->getAllAnimations();
 
// Recorremos las animaciones
Ogre::AnimationStateIterator it = animations->getAnimationStateIterator();
 
while(iter.hasMoreElements())
    std::cout << "Animación: " << it->getNext()->getAnimationName() << std::endl;

Si probáis este fragmento dentro de un código que lleve a cabo la inicialización de Ogre y su configuración veréis la siguiente lista:

Dance
DrawSwords
HandsClosed
HandsRelaxed
IdleBase
IdleTop
JumpEnd
JumpLoop
JumpStart
RunBase
RunTop
SliceHorizontal
SliceVertical

Manipulación de propiedades

Los siguientes métodos nos ayudan a consultar y modificar ciertos parámetros de una animación en concreto. Como veremos, Ogre nos permite configurar varias opciones para producir animaciones a nuestro gusto incluso en tiempo de ejecución. Por supuesto, para acceder a la lista completa de métodos de la clase lo mejor es acudir a la referencia oficial.

Para consultar el nombre de una animación una vez tenemos el objeto podemos hacer uso del método AnimationState::getAnimationName.

const Ogre::String& Ogre::AnimationState::getAnimationName() const;

Las animaciones pueden ser cíclicas si se reproducen en un bucle indefinido por lo que su duración podría ser desconocida. No obstante, todas las animaciones cuentan con un ciclo de duración conocida gracias al método AnimationState::getLength que nos lo devuelve en segundos.

Ogre::Real Ogre::AnimationState::getLength() const;

Podemos modificar la duración de una iteración de cualquier animación con el método AnimationState::setLength. Si la nueva duración es menor que la real, la animación se verá recortada.

void Ogre::AnimationState::setLength(Ogre::Real len);
  • Ogre::Real len: nueva duración en segundos de la animación.

Como ya hemos mencionado en más de una ocasión a lo largo de este texto, las animaciones pueden mezclarse unas con otras. Cuando dos animaciones se reproducen al unísono dos vértices podrían verse afectados por los dos estados. La transformación del vértice es el resultado de aplicar las transformaciones de los huesos correspondientes por el peso de cada animación. El peso de una animación es un real que va de 0 (no afecta) a 1 (afecta por completo). Para consultar el peso de una animación empleamos el método AnimationState::getWeight.

Real Ogre::AnimationState::getWeight() const;

Para combinar animaciones y ajustar los pesos podemos emplear el método AnimationState::setWeight. Los valores de los pesos son los que hemos mencionado en el punto anterior.

void Ogre::AnimationState::setWeight(Ogre::Real weight);
  • Ogre::Real weight: nuevo peso de la animación (en el intervalo cerrado entre 0.0 y 1.0).

Reproducción de animaciones

En esta sección recorreremos los métodos que nos permiten reproducir y ajustar la reproducción de nuestras animaciones basadas en esqueletos. Como podréis comprobar, al fin y al cabo reproducir una animación es harto sencillo.

Para saber si una animación se está reproduciendo, podemos emplear el método AnimationState::getEnabled que nos devolverá un bool con true en caso afirmativo y false en caso contrario.

bool Ogre::AnimationState::getEnabled() const;

Para activar o desactivar una animación simplemente recurrimos al método AnimationState::setEnabled. Recordad que si activamos una animación y no desactivamos las anteriores, las animaciones activas se mezclarán en función de sus pesos.

void Ogre::AnimationState::setEnabled(bool enabled);
  • bool enabled: true para activar la animación, false para desactivarla.

Las animaciones pueden contener un ciclo y reproducirse de manera indefinida. Para conocer si una animación tiene el bucle infinito activado haremos uso del método AnimationState::getLoop.

bool Ogre::AnimationState::getLoop() const;

Si nuestra animación "andar" tiene un ciclo y queremos que el personaje ande indefinidamente (hasta que el jugador deje de pulsar el botón de caminar) tendremos que activar el bucle. Para hacerlo emplearemos el método AnimationState::setLoop.

void Ogre::AnimationState::setLoop(bool loop);
  • bool loop: true activa el bucle y false lo desactiva.

Si activamos una animación y comenzamos el bucle de juego veremos que nuestra entidad no se mueve en absoluto (estamos hablando siempre de las animaciones internas). Debemos indicarle a la animación el tiempo que ha transcurrido desde su última actualización para que continúe con el movimiento. Para ello recurrimos al método AnimationState::addTime. Si tras añadir el tiempo, la animación llega a su final el personaje se detendrá a menos que el bucle esté activado para dicha animación.

void Ogre::AnimationState::addTime(Ogre::Real time);
  • Ogre::Real time: tiempo en milisegundos que deseamos que avance la animación. Normalmente corresponde al tiempo desde el último cuadro.

Podemos consultar en cualquier momento el instante (en segundos) en el que se encuentra un ciclo de animación. Esta información nos la proporciona el método AnimationState::getTimePosition.

Ogre::Real Ogre::AnimationState::getTimePosition() const;

Podemos hacer que una animación avance o retroceda como si de un software de reproducción multimedia se tratase. Simplemente llamamos al método AnimationState::setTimePosition con el instante deseado en segundos.

void Ogre::AnimationState::setTimePosition(Ogre::Real timePos);
  • Ogre::Real timePos: posición en la que se colocará la animación a partir de ese momento (en segundos).

Es probable que haya animaciones (como la de atacar) que no se reproduzcan de forma indefinida. Por ejemplo, queremos que cuando nuestro personaje finalice dicha acción comience su animación de reposo (efecto de respiración). Para consultar si una animación ha llegado a su fin utilizaremos el método AnimationState::hasEnded. Es importante tener en cuenta que si el bucle está activado, este método nunca devolverá verdadero.

bool Ogre::AnimationState::hasEnded() const;

En el siguiente fragmento de código también utilizaremos a Simbad. Tomaremos su animación "bailar", la reproduciremos en el bucle de juego y cuando termine mostraremos un mensaje por la salida estándar. Es un pequeño fragmento de código de forma esquemática y no está hecho para utilizarse directamente sino para ilustrar el proceso. Es recomendable que lo implementes de forma independiente. En este caso suponemos que tenemos una clase Juego que controla la ejecución e implementa la interfaz FrameListener.

void Juego::inicializar() {
    // Cargamos la entidad
    Ogre::Entity* simbad = sceneManager->createEntity("simbad", "simbad.mesh");
 
    // Creamos un nodo
    Ogre::SceneNode* simbadNodo = sceneManager->getRootSceneNode()->createChildSceneNode("simbadNodo");
 
    // Adjuntamos la entidad al nodo
    simbadNodo->attachObject(simbad)
 
    // Tomamos la animación bailar
    Ogre::AnimationState* bailar = simbad->getAnimationState("Dance");
 
    // Reproducimos la animación sin bucle
    bailar->setEnabled(true);
}
 
...
 
// Actualizar escena por utilizando un FrameListener
bool Juego::frameStarted(const Ogre::FrameEvent& event) {
    // Actualizamos la animación
    bailar->addTime(event.timeSinceLastFrame);
 
    // Comprobamos si hemos llegado al final
    if (bailar->hasEnded())
        std::cout << "Animación finalizada" << std::endl;
 
    return true;
}

Combinación de animaciones

Llegados a este punto ya eres consciente de que es posible combinar animaciones dejando que Ogre se encargue de transformar los vértices del modelo en función de los pesos de las mismas. En esta pequeña sección vamos a centrarnos en cómo conseguirlo. Lo primero es disponer de un modelo preparado para ser animado por partes. Mezclar dos animaciones que afecten a los mismos vértices podría ser contraproducente y quedar extraño.

Si os fijáis en la lista de animaciones de Simbad, veréis que cuenta con animaciones para su tren superior e inferior por separado para las mismas acciones (Idle, Run, etc). Esto nos permitiría atacar (superior) mientras corremos (inferior). En el siguiente ejemplo implementaremos exactamente la situación que acabamos de ver. De nuevo, es un mero ejemplo explicativo no listo para ser compilado, te animamos a que lo implementes por tí mismo.

void Juego::inicializar() {
    // Cargamos la entidad
    Ogre::Entity* simbad = sceneManager->createEntity("simbad", "simbad.mesh");
 
    // Creamos un nodo
    Ogre::SceneNode* simbadNodo = sceneManager->getRootSceneNode()->createChildSceneNode("simbadNodo");
 
    // Adjuntamos la entidad al nodo
    simbadNodo->attachObject(simbad)
 
    // Tomamos la animación correr inferior y atacar
    Ogre::AnimationState* correrInferior = simbad->getAnimationState("RunBase");
    Ogre::AnimationState* atacar = simbad->getAnimationState("SliceHorizontal");
 
    // Reproducimos las animaciones con bucle
    correrInferior->setEnabled(true);
    correrInferior->setLoop(true);
    atacar->setEnabled(true);
    atacar->setLoop(true);
}
 
...
 
// Actualizar escena por utilizando un FrameListener
bool Juego::frameStarted(const Ogre::FrameEvent& event) {
    // Actualizamos las animaciones
    correrInferior->addTime(event.timeSinceLastFrame);
    atacar->addTime(event.timeSinceLastFrame);
 
    return true;
}


Ejemplo

Diagrama de clases del ejemplo de animación
Ejemplo de animación
Pequeña aplicación que crea una escena en la que podemos controlar a Sinbad, mascota de Ogre.

En este ejemplo resumiremos de forma general los mecanismos de la animación basada en esqueletos. Como hemos venido haciendo en el resto de artículos, comenzamos con una clase AplicacionOgre que se encarga de inicializar el motor y las bibliotecas auxiliares como OIS. La clase EscenaSimple es la encargada de configurar la escena y añadir los elementos que la compondrán. En este caso tenemos a la mascota de Ogre (Sinbad), un plano, cuatro puntos de luz y un SkyDome.

El ejemplo es interactivo, podemos controlar al personaje y activar algunas de sus animaciones.

  • Teclas de dirección: movemos a Sinbad en cuatro direcciones posibles.
  • d: activa o desactiva la animación de bailar ("dance"). Mientras bailamos, no es posible desplazarse.

Al sistema que ya teníamos hasta el momento le hemos añadido un diccionario cuyas claves son cadenas y cuyos elementos son punteros a AnimationState. De esta manera podemos identificar animaciones por su nombre. Contamos con referencias a dos AnimationState, una para el tren superior del personaje y otra para el inferior. Por último, guardamos un vector con su dirección actual para controlar los giros.

En el método actualizarPersonaje consultamos la pulsación de teclas y vamos moviendo y animando a Sinbad en función de las mismas. El código es bastante sencillo y está suficientemente comentado. Es recomendable que lo estudies y hagas cambios. Por ejemplo, podrías hacer que mientras esté bailando puedas correr.

Recuerda que debes preparar los plugins (pues no vienen incluidos en el paquete). Puedes encontrar más información sobre esto en "Creación de un entorno de trabajo multiplataforma". Los plugins necesarios son:

  • RenderSystem_GL
Captura del ejemplo de animaciones en ejecución

Conclusiones

Con los conocimientos que has adquiridos tras seguir este texto con detenimiento eres capaz de incluir animaciones básicas y de nivel medio (mezclando animaciones basadas en esqueletos) en tu proyecto sin ningún problema. Es altamente recomendable que pruebes todos los ejemplos con tu propio código y modifiques opciones para ver los resultado.

Herramientas personales