Tutorial Básico 4 Manejadores de Frame (FrameListeners) y Entrada sin búfer


external image dl1773

Introducción al tutorial


En este tutorial nos introduciremos en uno de las construcciones más útiles de Ogre: el FrameListener. Al final del tutorial entenderás los FrameListeners, y como usarlos para hacer cosas que necesitan actualizarse en cada frame, y como usar el sistema OIS de entrada sin búfer.

external image help.gifLos problemas que encuentres mientras trabajes en este tutorial deberías consultarlos en los Foros de Ayuda.

Prerrequisitos


  • Este tutorial asume que tienes conocimientos sobre programación en C++ y eres capaz de compilar e instalar una aplicación de Ogre.
  • Este tutorial también asume que has creado un proyecto usando el Tutorial Framework Ogre Wiki, manualmente, usando CMake o el AppWizard de Ogre - mirar Creando una aplicación para más instrucciones.
  • Este tutorial se compila sobre la versión anterior de los tutoriales básicos, asi que se asume que has trabajado con ellos.

Cuando mires el tutorial deberías ir lentamente añadiendo código a tu proyecto y mirando los resultados. Puedes ver el código fuente para ver el estado final del tutorial aquí. Si tienes problemas con el código, deberías comparar tu fuente del proyecto al resultado final.

Comenzando


Este es el código con el que comenzaremos:

Cabecera BasicTutorial4
class BasicTutorial4 : public BaseApplication
{
public:
 BasicTutorial4(void);
 virtual ~BasicTutorial4(void);
protected:
 virtual void createScene(void);
 virtual bool frameRenderingQueued(const Ogre::FrameEvent& evt);
private:
 bool processUnbufferedInput(const Ogre::FrameEvent& evt);
};
Implementación BasicTutorial4
void BasicTutorial4::createScene(void)
{
}
//-------------------------------------------------------------------------------------
bool BasicTutorial4::processUnbufferedInput(const Ogre::FrameEvent& evt)
{
 return true;
}
//-------------------------------------------------------------------------------------
bool BasicTutorial4::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
 bool ret = BaseApplication::frameRenderingQueued(evt);
 return ret;
}
//-------------------------------------------------------------------------------------
Estamos sobreescribiendo la función frameRenderingQueued, y definiendo una función privada llamada processUnbufferedInput.

El resto del tutorial lo pasaremos haciendo algo interesante.

FrameListeners


Introducción


En Ogre, podemos registrar una clase para recibir notificaciones antes de que un frame sea renderizado en la pantalla.
La clase se llama FrameListener.

Esta interface FrameListener declara tres funciones que pueden ser usadas para recibir eventos de frame:
virtual bool frameStarted(const FrameEvent& evt);
virtual bool frameRenderingQueued(const FrameEvent& evt);
virtual bool frameEnded(const FrameEvent& evt);
  • frameStarted : Llamado justo antes del renderizado de un frame.
  • frameRenderingQueued : Llamado antes de que todos los objetivos de renderizado hayan terminado sus comandos de renderizado, pero antes de que la ventana de renderizado pida que se intercambien sus búferes.
  • frameEnded : Llamado justo después de que un frame haya sido renderizado.

Esto se repite hasta que cualquier de los FrameListeners devuelvan false desde frameStarted, frameRenderingQueued o frameEnded. Los valores de retorno de estas funciones básicamente significan "mantente renderizando".
Si devuelves false desde alguna, el programa saldrá.

El objeto FrameEvent contiene dos variables, pero solo timeSinceLastFrame es útil en un FrameListener. Esta variable guarda la pista de cuanto tiempo ha transcurrido desde la última activación de frameStarted o frameEnded. Nota que en el método frameStarted, FrameEvent::timeSinceLastFrame contendrá cuanto tiempo un método frameEnded fue disparado.

Un concepto importante sobre los FrameListeners de Ogre es que el orden en que son llamados, esto es manejado por Ogre. No puedes determinar que FrameListener será llamado primero, segundo, tercero... y demás. Si necesitas asegurar que FrameListeners son llamados en un cierto orden, entonces deberías registrar sólo un FrameListener y tendrás que llamar a todos los objetos en el orden apropiado.

Así que, ¿cuál de los tres métodos FrameListener debería escogerse?

Depende de lo que necesitemos, pero si sólo quieres actualizar tus datos una vez por frame, ponlo en el evento frameRenderingQueued, porque se llama solo antes de que la GPU este ocupada en intercambiar el búfer de renderizado.
Asi que querrás mantener tu CPU ocupada mientras la GPU funciona.

Cita de documentos del API:
Cita:
La inutilidad de este evento viene del hecho de que los comandos de renderizado son encolados por la GPU para ser procesados.
Y estos pueden tomar un cierto tiempo hasta terminarse, asi que la CPU puede estar haciendo cosas útiles.
Una vez la petición para "intercambiar búferes" ocurre, el hilo llamante se bloquerá hasta que la GPU esté lista, lo cual puede hacer perder ciclos de CPU.
Por lo tanto, es buena idea usar esta llamada para realizar procesado por frame. Porque los comandos de renderizado de frame ya han sido tratados,
cualquier cambio que hagas solo tendrá efecto en el siguiente frame, pero en la mayoría de los casos esto no se notará.
Haz Esto
external image dl1717
Usa la función frameRenderingQueued de FrameListener para actualizar un frame.

Registrando un FrameListener


Nuestra clase BasicTutorial4 ya es un FrameListener - sorpresa. :-)
Deriva de BaseApplication con herencia de un FrameListener:
class BaseApplication : public Ogre::FrameListener
BaseApplication implementa la función frameRenderingQueued, y sobreescribimos esa función en la clase BasicTutorial3 en el tutorial anterior.
Actualmente, también sobreescribimos la función createFrameListener.

Pero ahora explicaremos que hacen estas funciones.

Para que llegue a ser un FrameListener funcional, necesitas registrarlo con Ogre::Root.
Necesitas hacer esto porque Ogre::Root tiene que saber que framelisteners se llaman cuando un evento frame ocurre.

Para añadir o eliminar un FrameListener, podemos usar dos funciones:
Ogre::Root::addFrameListener y Ogre::Root::removeFrameListener.

El método addFrameListener añade un FrameListener, y el método removeFrameListener elimina un FrameListener (esto es, el FrameListener no recibirá actualizaciones).
Nota que los métodos add|removeFrameListener solo toman un puntero a FrameListener (esto es, Los FrameListeners no tienen nombres que puedas usar para eliminarlos).

BaseApplication usa el siguiente código en createFrameListener para registrarse con Ogre::Root como FrameListener:
mRoot->addFrameListener(this);
Después de haber hecho esto, es capaz de recibir eventos de frame desde el Ogre::Root, lo que significa las funciones FrameListener frameStarted, frameRenderingQueued y frameEnded.

Asi que, ¿Cómo funciona?


Toquemos un poco en Ogre::Root::renderOneFrame:
bool Root::renderOneFrame(void)
{
 if(!_fireFrameStarted())
 return false;
 
 if (!_updateAllRenderTargets())
 return false;
 
 return _fireFrameEnded();
}
Aqui puedes ver que Ogre::Root, cuando se renderiza un frame, lanza un evento FrameStarted antes de actualizar todos los objetivos de renderizado.
Y entonces lanza el evento FrameEvent cuando se hace la actualización.

Para ver donde Ogre::Root lanza el evento FrameRenderingQueued, echaremos un vistazo a Ogre::Root::_updateAllRenderTargets:
bool Root::_updateAllRenderTargets(void)
{
 // actualiza todos los objetivos pero no intercambia los buferes
 mActiveRenderer->_updateAllRenderTargets(false);
 // da a una aplicacion cliente la oportunidad de usar el tiempo de la cola de la GPU
 bool ret = _fireFrameRenderingQueued();
 // bloque para el intercambio final
 mActiveRenderer->_swapAllRenderTargetBuffers(mActiveRenderer->getWaitForVerticalBlank());
 // mas codigo a continuacion ...

Aquí puedes ver que ello lanza el evento FrameRenderingQueued después de actualizar los objetivos de renderizado, pero antes de intercambiar los búferes de objetivo de renderizado.

Eso es todo lo que necesitas saber - por ahora - para comenzar a trabajar con FrameListeners.

Asegúrate de que puedes compilar la aplicación antes de continuar.

Creando la Escena


Introducción


Antes de que decidas bucear dentro del código, deja que te de un resumen de lo que haremos para que comprendas donde vas, y cuando crearemos y añadiremos objetos a la escena.

Colocaremos un objeto (un ninja) en la escena, y una luz puntual.
Si pulsas el botón izquierdo del raton, la luz se apagará o encenderá.
Puedes mover el nodo al que el ninja está acoplado usando las teclas IKJL: I para moverte hacia delante, K para moverte hacia atras, J para moverte a la derecha, y derecha-shift + J rota a la derecha, L para moverse a la izquierda, L + right-shift rota a la derecha. Las teclas U y O mueven el nodo de escena del ninja arriba y abajo.

El Código


Busca el método BasicTutorial4::createScene. La primera cosa que necesitamos hacer es establecer la luz ambiental de la escena para que sea muy baja. Queremos que los objetos de la escena todavía sean visibles cuando la luz este apagada, pero también queremos que el cambio de luz de encendido a apagado se note:
mSceneMgr->setAmbientLight(Ogre::ColourValue(0.25, 0.25, 0.25));
Ahora, añade una entidad Ninja a la escena en el origen:
Ogre::Entity* ninjaEntity = mSceneMgr->createEntity("Ninja", "ninja.mesh");
Ogre::SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode");
node->attachObject(ninjaEntity);
Ahora creamos un punto de luz blanco y lo colocamos en la Escena, a una cierta distancia (relativa) desde el Ninja:

Ogre::Light* pointLight = mSceneMgr->createLight("pointLight");
pointLight->setType(Ogre::Light::LT_POINT);
pointLight->setPosition(Ogre::Vector3(250, 150, 250));
pointLight->setDiffuseColour(Ogre::ColourValue::White);
pointLight->setSpecularColour(Ogre::ColourValue::White);
Esto es para la función createScene. Sobre la función frameRenderingQueued...

El FrameListener


Necesitamos poner algo de código en nuestra función framelistener frameRenderingQueued:

bool BasicTutorial4::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
bool ret = BaseApplication::frameRenderingQueued(evt);
 
if(!processUnbufferedInput(evt)) return false;
 
return ret;
}

Ello llama a BaseApplication::frameRenderingQueued y nuestra (todavía se escribirá) función processUnbufferedInput y entonces volverá del bucle.
Para continuar renderizando el método frameStarted debe devolver un valor boleano positivo.
Si cualquiera devuelve falso, Ogre romperá el bucle de renderizado y la aplicación saldrá.

Pon algo en la función "processUnbufferedInput"

Procesando la Entrada


Variables

Hemos definido unas pocas variables en la clase TutorialFrameListener. Vayamos a verlas antes de seguir más adelante:
bool BasicTutorial4::processUnbufferedInput(const Ogre::FrameEvent& evt)
{
static bool mMouseDown = false; // Si un boton del mouse se suelta
static Ogre::Real mToggle = 0.0; // El tiempo que falta hasta el siguiente cambio
static Ogre::Real mRotate = 0.13; // La constante de rotacion
static Ogre::Real mMove = 250; // El movimiento constante
mRotate y mMove son nuestras constantes de rotación y movimiento.
Si quieres que el movimiento o rotacion sea más rápido o más lento, ajusta estas variables para que sean mayores o menores.

Las otras dos variables (mToggle y mMouseDown) controlan nuestra salida. Usaremos la entrada de ratón y teclado sin búfer en este tutorial. (la entrada con búfer será el tema de nuestro siguiente tutorial).
Esto significa que los métodos que serán llamados durante nuestro frame listener al estado de la consulta del teclado y el ratón.
Entramos en un problema interesante cuando intentamos usar el teclado para cambiar el estado de algunos objetos en la pantalla.
Si vemos que una tecla ha sido presionada, podemos actuar sobre la información, pero que ocurre en el siguiente frame?
Podemos ver que la misma tecla ha sido presionada y hace la misma acción otra vez?
En algunos casos (como en el movimiento con las flechas de dirección) esto es lo que debería ocurrir. Sin embargo, digamos que queremos que la tecla "T" cambie entre que la luz este apagada o encendida.
El primer frame en que la tecla T se presiona, la luz cambia, al siguiente frame la tecla T todavía esta bajada, si que se cambiará de nuevo... y otra vez, y otra hasta que la tecla se suelte.
Tenemos que seguir la pista al estado de las teclas entre frames para evitar este problema. Presentamos dos métodos separados para resolver esto.

La variable mMouseDown almacena si se ha o no presionado el ratón en el frame anterior (así si mMouse se presionó y es true, no realizaremos la misma acción otra vez hasta que el ratón sea liberado).
La variable mToggle especifica el tiempo hasta que se nos permite realizar otra acción. Esto es, cuando un botón es presionado, mToggle se establece a un tiempo determinado donde ninguna otra acción ocurrirá.

Las variables son variables locales estáticas, principalmente por conveniencia.
Elas podrían ser miembros de la clase de datos primeramente de la clase BasicTutorial4, pero ellos son sólo usados por esa función, tiene más sentido tenerlos allí.

El sistema de entrada abierto (OIS) proporciona tres clases primarias para proporcionar la entrada: Keyboard, Mouse y Joystick. En estos tutoriales sólo trataremos el uso de los objetos Keyboard y Mouse.
Si estás interesado en usar un joystick (o gamepad) con Ogre, deberías mirar dentro de la clase Joystick.

Antes de seguir, echemos un vistazo a BaseApplication::frameRenderingQueued.
El estado actual para cada frame del teclado y del ratón debe ser capturado, llamando al método de captura de los objetos Mouse y Keyboard.
Esto ocurre en estas dos líneas en BaseApplication::frameRenderingQueued:
mMouse->capture();
mKeyboard->capture();
No añadir esto a nuestra función! :-)

La primera cosa que vamos a hacer es que al hacer click en el botón izquierdo del ratón la luz se encienda o se apage.
Podemos hacer eso si buscamos si el botón ha sido presionado llamando al método getMouseButton de InputReader con el botón por el que estamos preguntando. Normalmente 0 si es el botón izquierdo del raton, 1 si es el botón derecho, y 2 si es el botón central del ratón. En algunos sistemas el botón 1 es el del medio y 2 es el botón derecho. Intenta esta configuración si los botones del ratón no funcionan como esperabas.
Añade esto a nuestra función BasicTutorial4::processUnbufferedInput:
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
La variable currMouse sera true si el botón del mouse está presionado.
Ahora cambiaremos la luz dependiendo de si currMouse es true, y si el ratón no fue presionado en el frame anterior (porque sólo queremos cambiar la luz una vez cada vez que el ratón es presionado).
También nota que el método setVisible de la clase Light determina si el objeto emite o no luz en estos momentos:
if (currMouse && ! mMouseDown)
{
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}
Ahora necesitamos establecer la variable mMouseDown para que sea igual que lo que contiene currMouse.
El siguiente frame esto nos dirá si el botón fue presionado o liberado anteriormente.
mMouseDown = currMouse;
Compila y Ejecuta la aplicación.

Ahora el click izquierdo cambia la luz de encendido a apagado!
Porque podemos llamar al método frameRenderingQueued de BaseApplication, todavia podemos usar las teclas WASD para mover la cámara alrededor.

Este método de guardar el estado anterior de los botones del ratón funciona bien, ya que sabemos que hemos actuado sobre el estado del ratón.
La vuelta del dibujado usa esto para cada tecla que esta enlazada a una acción, necesitamos una variable para esto.
Un modo de que consigamos esto es mantener la pista de la última vez que un botón fue presionado, y sólo permitirá acciones que ocurran después de que transcurra una cierta cantidad de tiempo. Controlaremos esto en el estado de la variable mToggle.
Si mToggle es mayor que 0, entonces no necesitamos hacer ninguna acción, si mToggle es menor que 0, entonces realizamos acciones.
Usaremos este método para las siguientes dos teclas enlazadas.

La primera cosa que queremos hacer es decrementar la variable mToggle por el tiempo que ha transcurrido desde el último frame:
mToggle -= evt.timeSinceLastFrame;
Ahora que hemos actualizado mToggle, podemos actuar sobre él.

mToggle actua con un retraso de 0.5 segundos antes de que se puedan realizar cambios adicionales.
En la práctica, este retraso es más largo que lo necesario, pero ilustra este hecho. Añadimos un modo adicional de cambiar la luz de encendido a apagado:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
mToggle = 0.5;
Ogre::Light* light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}
Compila y ejecuta el tutorial. Podemos ahora encender y apagar la luz presionando "1".

La siguiente cosa que necesitamos hacer es trasladar el nodo que guarda el ninja cuando el usuario pulsa una de las teclas IJKL.
A diferencia del código anterior, no necesitamos mantener una referencia de la última vez que movimos la cámara, ya que para cada frame la tecla es presionada y queremos traducirla otra vez.
Esto hace nuestro código relativamente fácil.
Primero crearemos un Vector3 que guarde donde queremos traducirla:
Ogre::Vector3 transVector = Ogre::Vector3::ZERO;
Ahora, cuando la tecla I se presiona, queremos movernos rectos (lo cual es el eje negativo z, recuerda z negativo recto dentro de la pantalla):
if (mKeyboard->isKeyDown(OIS::KC_I)) Recto
{
transVector.z -= mMove;
}
Tenemos que hacer lo mismo que para la tecla K, pero nos movemos en el eje positivo z en su lugar:
if (mKeyboard->isKeyDown(OIS::KC_K)) Hacia Atras
{
transVector.z += mMove;
}
Para movimiento de izquierda y derecha, vamos en la dirección x positiva o negativa, o rotar a la izquierda o derecha cuando pulsamos derecha-shift:
if (mKeyboard->isKeyDown(OIS::KC_J)) // Izquierda - rotacion yaw o hundir
{
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// rotacion yaw a la izquierda
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(mRotate * 5));
}
else
{
transVector.x -= mMove; // Hundir a la izquierda
}
}
 
if (mKeyboard->isKeyDown(OIS::KC_L)) // Derecha - rotacion yaw o hundir
{
if(mKeyboard->isKeyDown( OIS::KC_LSHIFT ))
{
// rotacion yaw a la derecha
mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(-mRotate * 5));
}
else
{
transVector.x += mMove; // Hundir a la derecha
}
}

Finalmente, tambien queremos dar un modo de movernos arriba y abajo en el eje y, usando las teclas U y O:
if (mKeyboard->isKeyDown(OIS::KC_U)) // Arriba
{
transVector.y += mMove;
}
if (mKeyboard->isKeyDown(OIS::KC_O)) // Abajo
{
transVector.y -= mMove;
}
Ahora, nuestra variable transVector tiene la traslación que deseamos aplicar al SceneNode de la Cámara. El primer inconveniente que encontramos cuando hacemos esto es que si rotas el SceneNode, entonces nuestras coordenadas x, y, z serán erróneas cuando se trasladen. Para arreglar esto, necesitamos aplicar todas las rotaciones que hemos hecho a la SceneNode a nuestro nodo de traslación. Esto es tan sencillo como suena.

Para representar rotaciones, Ogre no usa matrices de transformación como algunos motores gráficos. En vez de eso usa Quaterniones para todas las operaciones de rotación. La matemática detrás de los Quaterniones implica álgebra lineal de cuatro dimensiones, lo que es muy difícil de entender. afortunadamente, no tienes que comprender la matemática que subyace para comprender como usarlos. Es muy simple, usar los Quaterniones para rotar un vector, todo lo que tienes que hacer es multiplicar los dos juntos. en este caso, queremos aplicar todas las rotaciones hechas al SceneNode al vector de traslación. Podemos conseguir un Quaternion que represente estas rotaciones llamando a SceneNode::getOrientation(), entonces podemos aplicarlos al nodo de traslación usando una multiplicación.

El segundo problema que tenemos que mirar es que tenemos que tenemos que escalar la cantidad que hemos trasladado por la cantidad de tiempo desde el último frame. De otro modo, Cuanto de rápido te mueves depende del ritmo de frames de la aplicación. Definitivamente no es lo que queremos. Esta es la función que llamaremos cuando necesitemos trasladar nuestra nodo de cámara sin encontrarnos con estos problemas:
mSceneMgr->getSceneNode("NinjaNode")->translate(transVector * evt.timeSinceLastFrame, Ogre::Node::TS_LOCAL);
Ahora hemos introducido algo nuevo.

Cuando traslades un nodo, o lo rotes sobre cualquiera de sus ejes, puedes especificar que espacio de transformacion quieres usar para mover el objeto.
Normalmente cuando rotas un objeto, no tienes que establecer este parámetro. Es por defecto TS_PARENT, lo que significa que el objeto es movido en el espacio de transformacion del nodo padre. En este caso, el nodo padre es el nodo raíz de la escena.
Cuando presionamos el botón I (para movernos adelante), restamos de la direccion Z, lo que significa que nos movemos hacia atrás en el eje Z negativo. Si no hubieramos especificado TS_LOCAL en esta línea anterior, podríamos mover el ninja recto cuando presionaramos I, necesitamos que vaya en la dirección a la que el nodo actual está apuntando.
Por tanto, usamos el espacio de transformación "local".

Hay otro modo de hacer esto (pero es menos directo). Podríamos coger la orientación de un nodo, un quaternion, y multiplicarlo por el vector dirección para conseguir el mismo resultado. Esto podría ser perfectamente válido:

No añadas esto al programa
mSceneMgr->getSceneNode("NinjaNode")->translate(mSceneMgr->getSceneNode("NinjaNode")->getOrientation() * transVector * evt.timeSinceLastFrame, Ogre::Node::TS_WORLD);
Esto también traslada el nodo ninja en el espacio local. En este caso, no hay razón real para hacerlo.
Ogre define tres espacios de trasformación: TS_LOCAL, TS_PARENT, y TS_WORLD.
Hay un caso donde necesitas hacer una traslación o rotación en otro espacio vectorial de estos tres.
Si este es el caso, debería ser similar a la anterior línea de código.
Toma un quaternion que represente el espacio vectorial (o una orientación cualquiera del objeto con la que quieres que coincida), multiplícalo por el vector de traslación para conseguir el vector correcto de traslación, y muévelo en el espacio TS_WORLD. Esto no es muy usado, y no nos referiremos a ello en otros tutoriales.

Compila el programa y pruébalo.

Este tutorial no es una auténtica guía sobre rotaciones y Quaterniones (es bastante material como para llenar un tutorial entero por si mismo). En el siguiente tutorial, usaremos la entrada de ratón con búfer en vez de comprobar las teclas que han sido pulsadas en cada frame.

Siguiente Tutorial Básico 5 Entrada con Búfer