Tutorial Intermedio 3 Selección con el Ratón (Selección de Objetos 3D) y Máscaras SceneQuery


external image help.gifCualquier problema que encuentres mientras estas trabajando con este tutorial deberías preguntarlo en los Foros de Ayuda.

Introducción

En este tutorial continuaremos el trabajo sobre el tutorial anterior. Cubriremos como seleccionar cualquier objeto sobre la pantalla usando el ratón, y como restringir lo que es seleccionable.

Puedes encontrar el código de este tutorial aquí. Añade código poco a poco en tu proyecto y mira los resultados.

Prerrequisitos


Este tutorial asume que has usado el Tutorial Framework Ogre. También que sabes usar los iteradores STL para moverte a través de múltiples resultados SceneQueries.

Comenzando


Ahora si quieres seguir este tutorial completamente, o solo copiar y pegar, asegrate de que tu archivo .h se parece a este:
#ifndef IntermediateTutorial3_h_
#define IntermediateTutorial3_h_
 
#include "BaseApplication.h"
 
#include <CEGUISystem.h>
#include <CEGUISchemeManager.h>
#include <RendererModules/Ogre/CEGUIOgreRenderer.h>
 
class IntermediateTutorial3 : public BaseApplication
{
public:
 IntermediateTutorial3(void);
 virtual ~IntermediateTutorial3(void);
 
protected:
 virtual void createScene(void);
 
 virtual void chooseSceneManager(void);
 virtual void createFrameListener(void);
 
 virtual bool frameRenderingQueued(const Ogre::FrameEvent& arg);
 
 virtual bool mouseMoved(const OIS::MouseEvent& arg);
 virtual bool mousePressed(const OIS::MouseEvent& arg, OIS::MouseButtonID id);
 virtual bool mouseReleased(const OIS::MouseEvent& arg, OIS::MouseButtonID id);
 
 Ogre::SceneNode *mCurrentObject; // Puntero a nuestro objeto seleccionado
 
Ogre::RaySceneQuery* mRayScnQuery; // Puntero a nuestra consulta de Ray de Escena
 CEGUI::Renderer* mGUIRenderer; // nuestro renderizador CEGUI
 
bool bLMouseDown, bRMouseDown; // true si los botones del raton estan presionados
 int mCount; // numero de objetos creados
 float mRotateSpeed; // velocidad de rotacion de la camara
 
};
 
#endif // #ifndef IntermediateTutorial3_h_
Deberías asegurarte de que IntermediateTutorial3.cpp se parece a esto. Si has visto los tutoriales previos y tu código se parecerá a algo como esto. Si no asegúrate de que lo hace y compílalo y ejecútalo correctamente. La única cosa diferente deberían ser los comentarios y algunas de las variables de inicialización que han sido movidas al constructor.
#include "IntermediateTutorial3.h"
 
// -------------------------------------------------------------------------------------
IntermediateTutorial3::IntermediateTutorial3(void):
mCount(0),
mCurrentObject(0),
bLMouseDown(false),
bRMouseDown(false),
mRotateSpeed(0.1f)
{
}
// -------------------------------------------------------------------------------------
IntermediateTutorial3::~IntermediateTutorial3(void)
{
 mSceneMgr->destroyQuery(mRayScnQuery);
}
// -------------------------------------------------------------------------------------
void IntermediateTutorial3::createScene(void)
{
 // Instala la Escena
 mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5f, 0.5f, 0.5f));
 mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);
 
 // Geometria del mundo
 mSceneMgr->setWorldGeometry("terrain.cfg");
 
 // Instala la camara
 mCamera->setPosition(40, 100, 580);
 mCamera->pitch(Ogre::Degree(-30));
 mCamera->yaw(Ogre::Degree(-45));
 
 // Instala CEGUI
 mGUIRenderer = &CEGUI::OgreRenderer::bootstrapSystem();
 
 // Muestra el cursor CEGUI
 CEGUI::SchemeManager::getSingleton().create((CEGUI::utf8*)"TaharezLook.scheme");
 CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
}
 
void IntermediateTutorial3::chooseSceneManager(void)
{
 // crea un scene manager para manejar las scenas exteriores
 mSceneMgr = mRoot->createSceneManager(Ogre::ST_EXTERIOR_CLOSE);
}
 
void IntermediateTutorial3::createFrameListener(void)
{
 // todavia queremos crear el frame listener desde la aplicacion base.
 BaseApplication::createFrameListener();
 
 // pero tambien queremos establecer nuestra raySceneQuery despues de que todo ha sido inicializado
 mRayScnQuery = mSceneMgr->createRayQuery(Ogre::Ray());
}
 
bool IntermediateTutorial3::frameRenderingQueued(const Ogre::FrameEvent& arg)
{
 // Queremos ejecutar todo en la llamada previa frameRenderingQueued
 // pero tambien queremos hacer algo despues, asi que comencemos con esto
 if(!BaseApplication::frameRenderingQueued(arg))
 {
 return false;
 }
/*
 Este gran pedazo basicamente envia un rayo recto desde la posicion de la camara. Entonces comprueba para ver si esta debajo de la geometria del mundo y si lo esta movemos la camara arriba.
 */
 Ogre::Vector3 camPos = mCamera->getPosition();
 Ogre::Ray cameraRay(Ogre::Vector3(camPos.x, 5000.0f, camPos.z), Ogre::Vector3::NEGATIVE_UNIT_Y);
 
 mRayScnQuery->setRay(cameraRay);
 
 Ogre::RaySceneQueryResult& result = mRayScnQuery->execute();
 Ogre::RaySceneQueryResult::iterator iter = result.begin();
 
 if(iter != result.end() && iter->worldFragment)
 {
 Ogre::Real terrainHeight = iter->worldFragment->singleIntersection.y;
 
 if((terrainHeight + 10.0f) > camPos.y)
 {
 mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
 }
 }
 return true;
}
 
bool IntermediateTutorial3::mouseMoved(const OIS::MouseEvent& arg)
{
 // Actualiza CEGUI con los movimientos del raton
 CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
 
 // Si el boton izquierdo del raton esta presionado
 if(bLMouseDown)
 {
 // buscar la posicion actual del raton
 CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
 
 // crea un rayo desde la camara en la posicion del raton
 Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
 mRayScnQuery->setRay(mouseRay);
 
 Ogre::RaySceneQueryResult& result = mRayScnQuery->execute();
 Ogre::RaySceneQueryResult::iterator iter = result.begin();
 
 // comprueba para ver si el raton esta apuntando al mundo y pone nuestro objeto en esa posicion
 if(iter != result.end() && iter->worldFragment)
 {
 
 mCurrentObject->setPosition(iter->worldFragment->singleIntersection);
 }
 }
 else if(bRMouseDown) // si el boton derecho del raton esta presionado, rotamos la camara con el raton
 {
 mCamera->yaw(Ogre::Degree(-arg.state.X.rel * mRotateSpeed));
 mCamera->pitch(Ogre::Degree(-arg.state.Y.rel * mRotateSpeed));
 }
 
 return true;
}
 
bool IntermediateTutorial3::mousePressed(const OIS::MouseEvent& arg, OIS::MouseButtonID id)
{
 if(id == OIS::MB_Left)
 {
 // busca la posicion actual del raton
 CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
 
 // entonces envia un rayo desde la camara a la posicion del raton
 Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
 mRayScnQuery->setRay(mouseRay);
 /*
 Este pedazo busca los resultados del rayo. Si el raton esta apuntando a la geometria del mundo ponemos un robot en esa posicion
 */
 Ogre::RaySceneQueryResult& result = mRayScnQuery->execute();
 Ogre::RaySceneQueryResult::iterator iter = result.begin();
 if(iter != result.end() && iter->worldFragment)
 {
 char name[16];
 sprintf(name, "Robot%d", mCount++);
 
 Ogre::Entity* ent = mSceneMgr->createEntity(name, "robot.mesh");
 
 mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(std::string(name) + "Node", iter->worldFragment->singleIntersection);
 mCurrentObject->attachObject(ent);
 
 mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
 }
 bLMouseDown = true;
 }
 else if(id == OIS::MB_Right) // si el boton derecho del raton esta presionado, ocultamos el cursor del raton para el modo de vista
 {
 CEGUI::MouseCursor::getSingleton().hide();
 bRMouseDown = true;
 }
 
 return true;
}
 
bool IntermediateTutorial3::mouseReleased(const OIS::MouseEvent& arg, OIS::MouseButtonID id)
{
 if(id == OIS::MB_Left)
 {
 bLMouseDown = false;
 }
 else if(id == OIS::MB_Right) // cuando el boton derecho del raton este suelto hacemos reaparecer el cursor
 {
 CEGUI::MouseCursor::getSingleton().show();
 bRMouseDown = false;
 }
 return true;
}
 
#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 un objeto de aplicacion
 IntermediateTutorial3 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 y ejecutar este código antes de continuar. Debería funcionar de la misma forma que en el último tutorial.

Mostrando el Objeto que ha sido seleccionado


En este tutorial lo haremos, así que podrás "seleccionar" y mover objetos para después poder colocarlos. Nos gustaría tener un modo para que el usuario sepa que objeto esta manipulando. En un juego, probablemente nos gustaría crear un modo especial de resaltar el objeto, pero para nuestro tutorial, puedes usar el método showBoundingBox para crear una caja alrededor de los objetos.

Nuestra idea básica es desactivar la caja externa del viejo objeto cuando el ratón es pulsado, entonces activar la caja límite tan pronto como tengamos el nuevo objeto. Para hacer esto, añadimos el siguiente código al comienzo de la función mousePressed:
 // muestra que el objeto actual ha sido deseleccionado eliminando la caja limite
if(mCurrentObject)
{
 mCurrentObject->showBoundingBox(false);
}

Entonces añade el siguiente código al final de la función mousePressed:
// ahora mostramos la caja limite para que el usuario pueda ver que esta este objeto seleccionado.
if(mCurrentObject)
{
 mCurrentObject->showBoundingBox(true);
}
Ahora mCurrentObject esta siempre resaltado en la pantalla.

Añadiendo Ninjas


Ahora queremos modificar el código no sólo para soportar robots, también queremos colocarlos y mover los ninjas. Tendremos un "Modo Robot" y un "Modo Ninja" que determinará que objeto colocaremos en la pantalla. Hagamos que la tecla espacio sirva para cambiar entre modos.

Primero, instalamos el IntermediateTutorial para estar en Modo Robot desde el comienzo. Necesitamos añadir una variable que guarde el estado del objeto (esto es, si estamos colocando robots o ninjas). Ve a la sección protected de IntermediateTutorial y añade esta variable:
bool bRobotMode; // Estado actual
Ahora añade este código en el constructor de IntermediateTutorial3:
 // Establece el estado por defecto
bRobotMode = true;
Esto nos pone en el Modo Robot! . Ahora necesitamos crear una malla Robot o Ninja basándose en la variable bRobotMode. Localiza este código en mousePressed:
char name[16];
sprintf(name, "Robot%d", mCount++);
 
Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
La sustitución debería ser directa. Dependiendo del estado bRobotMode ponemos un Robot o un Ninja, y le nombramos apropiadamente:
Entity *ent;
char name[16];
 
if (bRobotMode)
{
 sprintf(name, "Robot%d", mCount++);
 ent = mSceneMgr->createEntity(name, "robot.mesh");
} // if
else
{
 sprintf(name, "Ninja%d", mCount++);
 ent = mSceneMgr->createEntity(name, "ninja.mesh");
} // else
Ahora, ya hemos hecho todo. La única cosa que falta es que se enlace la barra espaciadora con el cambio de estados. Ve a IntermediateTutorial3.h y añade un override a la función keyPressed como este:
virtual bool keyPressed(const OIS::KeyEvent& arg);
Ahora en IntermediateTutorial3.cpp llenamos la función. Necesitamos añadir la funcionalidad para la barra espaciadora, pero también queremos mantener la funcionalidad de la BaseApplication. Asi que, ¿cómo hacemos esto? Fácil, en unas pocas líneas:
bool IntermediateTutorial3::keyPressed(const OIS::KeyEvent& arg)
{
 // comprueba y mira si la barra espaciadora fue pulsada, esto cambiará la malla que fue lanzada.
 if(arg.key == OIS::KC_SPACE)
 {
 bRobotMode = !bRobotMode;
 }
 
 // entonces devolvemos la funcion keyPressed de la app base asi que conseguiremos toda la funcionalidad
 // y devolvemos el valor en una linea
 return BaseApplication::keyPressed(arg);
}
Como puedes ver, comprobamos la barra espaciadora y cambiamos de modo. También llamamos a la función keyPressed de BaseApplication dentro de la sentencia de retorno, de ese modo comprobamos todo lo programado antes, además conseguimos devolver un valor.

Compila y ejecuta la demo. Ahora puedes escoger que objeto colocarás usando la barra espaciadora.

Selección de Objetos


Ahora vamos a meternos de lleno en el tutorial: usando RaySceneQueries para seleccionar objetos sobre la pantalla. Antes de que comencemos haciendo cambios al código primero explicaré una RaySceneQueryResultEntry con más detalle. (Por favor sigue el enlace y mira la estructura brevemente).

El RaySceneQueryResult devuelve un iterator de estructuras RaySceneQueryResultEntry. Esta estructura contiene tres variables. La variable distancia te dice como de lejos esta el objeto del rayo. Una de las otras dos variables será no nula. La variable movable contendrá un MovableObject si el Ray intersecciona uno. El worldFragment contendrá un objeto WorldFragment si golpea en un fragmento de mundo (como el terreno).

Los MovableObjects son básicamente cualquier objeto que te gustaría acoplar a un SceneNode (tales como Entidades, Luces, etc). Mirar el árbol de herencia de esta página para encontrar que tipo de objetos deber ser devueltos. La mayoría de las aplicaciones normales de RaySceneQueries involucran la selección y manipulación del MovableObject sobre el que has pulsado, o los SceneNodes que has acoplado. Para conseguir el nombre del MovableObject, llama al método getName. Para conseguir el SceneNode(o Node) del Objeto al que esta acoplado, llama a getParentSceneNode (o getParentNode). La variable movable en un RaySceneQueryResultEntry será igual a NULL si el resultado no es un MovableObject.

El WorldFragment es diferente. Cuando el el miembro worldFragment de un RaySceneQueryResult esta establecido, significa que el resultado es parte de la geometría del mundo creada por el SceneManager. El tipo del fragmento de mundo que es devuelto esta basado en el SceneManager. El modo en que este está implementado es la estructura WorldFragment que contiene la variable fragmentType que especifica el tipo de fragmento de mundo que contiene. Basándose en la variable fragmentType, una de las otras variables se establecerá (singleIntersection, planes, geometry, o renderOp). Generalmente, RaySceneQueries sólo devuelven WorldFragments WFT_SINGLE_INTERSECTION. La variable singleIntersection es simplemente un Vector3 que dice su posición de intersección. Otros tipos de fragmentos de mundo estan fuera del alcance de este tutorial.

Ahora veamos un ejemplo. Queremos imprimir la lista de resultados después de una RaySceneQuery. El siguiente código debería hacer esto. (Asume que el objeto fout es del tipo ofstream, y que él ha sido creado usando el método open.)
// No anyadir este código al programa, sólo leer:
 
RaySceneQueryResult &result = mRayScnQuery->execute();
RaySceneQueryResult::iterator itr;
 
// iterar a traves de los resultados
for ( itr = result.begin( ); itr != result.end(); itr++ )
{
 // Este es el resultado de un WorldFragment?
 if ( itr->worldFragment )
 {
 Vector3 location = itr->worldFragment->singleIntersection;
 fout << "WorldFragment: (" << location.x << ", " << location.y << ", " << location.z << ")" << endl;
 } // if
 
 // Este es el resultado de un MovableObject?
 else if ( itr->movable )
 {
 fout << "MovableObject: " << itr->movable->getName() << endl;
 } // else if
} // for
Esto debería imprimir los nombres de todos los MovableObjects que el rayo intersecciona, y debería imprimir la posición de donde es interseccionada la geometría del mundo (si lo hizo). Nota que esto puede a veces actuar de modos extraños. Por ejemplo, si estas usando el TerrainSceneManager, el origen del rayo que disparas debe estar sobre el Terrain o la query que intersecciona no lo registrará. Diferentes scene managers implementan RaySceneQueries de diferentes formas. Asegúrate de experimentar con eso cuando lo uses con un nuevo SceneManager.

Ahora, si miramos atrás a nuestro código de RaySceneQuery, algo debería hacerte saltar: no estamos moviéndonos por todos los resultados! De hecho sólo estamos mirando el primer resultado, que (en el caso del TerrainSceneManager) es la geometría del mundo. Esto es malo, ya que no podemos asegurarnos que el TerrainSceneManager siempre devolverá la geometría del mundo primero. Necesitamos movernos a través de los resultados para asegurarnos de que hemos encontrado lo que estabamos buscando. Otra cosa que podemos hacer es señalar y coger objetos que ya esten colocados. si pulsas sobre un objeto que ya ha sido colocado, el programa lo ignora y coloca un robot detrás de él. Arreglémoslo ahora.

Vamos a la función mousePressed en el código. Primero queremos asegurarnos que cuando hacemos click, conseguimos la primera cosa que da el Rayo. Para hacer esto, necesitamos establecer la búsqueda por profundidad del RaySceneQuery. Busca este código en la función mousePressed:
 // Instalamos la consulta del rayo de escena, usando la posicion CEGUI del raton
CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
mRayScnQuery->setRay(mouseRay);
 
// Ejecutamos la consulta
RaySceneQueryResult &result = mRayScnQuery->execute();
RaySceneQueryResult::iterator itr = result.begin();
Y lo cambiamos a esto:
 // Instala la consulta del rayo de la escena
CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
mRayScnQuery->setRay(mouseRay);
mRayScnQuery->setSortByDistance(true);
 
// Ejecuta la consulta
RaySceneQueryResult &result = mRayScnQuery->execute();
RaySceneQueryResult::iterator iter;
Ahora que hemos devuelvo el los resultados en orden, necesitamos actualizar el código de los resultados de la consulta. Vamos a reescribir esta sección, asi que eliminemos este código:
 // Conseguir los resultados, crear un node/entity en la posición
if (iter != result.end() && iter->worldFragment)
{
 Entity *ent;
 char name[16];
 
 if (bRobotMode)
 {
 sprintf(name, "Robot%d", mCount++);
 ent = mSceneMgr->createEntity(name, "robot.mesh");
 } // if
 else
 {
 sprintf(name, "Ninja%d", mCount++);
 ent = mSceneMgr->createEntity(name, "ninja.mesh");
 } // else
 
 mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", iter->worldFragment->singleIntersection);
 mCurrentObject->attachObject(ent);
 mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
} // if
Queremos hacerlo asi para que podamos seleccionar que objetos estan colocados en la pantalla. Nuestra estrategia tiene dos partes. Primero, si el usuario pulsa sobre el objeto, entonces haz el mCurrentObject igual a su SceneNode padre. Si el usuario no hace click en en objeto (Si el pulsa sobre el terreno), coloca un nuevo Robot allí como hicimos antes. El primer cambio es que usaremos un bucle en vez de un sentencia if:
 Conseguir resultados, crea un node/entity sobre la posicion
for ( iter; iter != result.end(); iter++ )
{
Lo primero que tenemos que comprobar es si la primera intersección es un MovableObject, si hemos asignado el mCurrentObject a su SceneNode padre. Hay un inconveniente. El TerrainSceneManager crea MovableObjects para el terrain por sí mismo, así podríamos interseccionar una de las losetas. Para arreglar esto, compruebo el nombre de los objetos para asegurarme que no utilizar un nombre de loseta del terrain; un ejemplo de nombre de loseta podría ser "tile00,1". Finalmente, date cuenta de la sentencia de break. Sólo necesitamos actuar en el primer objeto, tan pronto como podamos encontrar uno válido necesitamos sacarlo del bucle.
if (iter->movable && iter->movable->getName().substr(0, 5) != "tile[")
{
 mCurrentObject = iter->movable->getParentSceneNode();
 break;
} // if
Siguiente, comprobar para ver si la intersección devolvió un WorldFragment.
else if (iter->worldFragment)
{
 Entity *ent;
 char name[16];
Ahora crearemos una entity Robot o una entity Ninja basándonos en el mRobotState. Después de crear la entity crearemos el SceneNode y saldremos del bucle.
if (bRobotMode)
 {
 sprintf(name, "Robot%d", mCount++);
 ent = mSceneMgr->createEntity(name, "robot.mesh");
 } // if
 else
 {
 sprintf(name, "Ninja%d", mCount++);
 ent = mSceneMgr->createEntity(name, "ninja.mesh");
 } else
 
 mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", iter->worldFragment->singleIntersection);
 mCurrentObject->attachObject(ent);
 mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
 break;
 } // else if
} // for
Creelo o no, eso es todo lo que hay que hacer! Compila y juega con el código. Ahora creamos el tipo correcto de objeto qucnod pulsamos sobre el terrain, y cuando hacemos click en el objeto veremos la caja de límites (no la cojas ahora, esto vendrá en el siguiente paso). Una pregunta válida es, ya que sólo queremos la primera intersección, y ya que hemos buscado por profundidad, porque usamos sólo una sentencia if? La razón principal es que se nos estropearía la idea si el primer objeto devuelto es uno de estos azulejos. Tenemos que movernos en el bucle hasta que hayamos encontrado otro cosa que no sea un azulejo o hallamos llegado al final de la lista.

Ahora que estamos sobre el tema, necesitamos también actualizar el código del RaySceneQuery en otras posiciones. En ambas funciones frameRenderingQueued y mouseMoved sólo necesitamos encontrar el Terrain. No hay ninguna razón para ordenar los resultados porque el terreno estará al final de la lista de búsqueda (así eliminamos la búsqueda). Todavía queremos movernos en bucle a través de los resultados, sólo en el caso de cuando el TerrainManager es cambiado en un tiempo futuro y no devuelve el terrain primero. Primero, busca esta sección de código en frameRenderingQueued:
 // Realiza la consulta de la escena
Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();
Ogre::RaySceneQueryResult::iterator itr = result.begin();
 
 // Consigue los resultados, y establece el alto de la camara
if (iter != result.end() && iter->worldFragment)
{
 Ogre::Real terrainHeight = iter->worldFragment->singleIntersection.y;
 if ((terrainHeight + 10.0f) > camPos.y)
 {
 mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
 }
}
Entonces reemplazalo con esto:
 // Realiza la consulta de la escena
mRayScnQuery->setSortByDistance(false);
Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();
Ogre::RaySceneQueryResult::iterator iter = result.begin();
 
 // Consigue los resultados, y establece la altura de la camara
for (iter; iter != result.end(); iter++)
{
 if (iter->worldFragment)
 {
 Ogre::Real terrainHeight = iter->worldFragment->singleIntersection.y;
 if ((terrainHeight + 10.0f) > camPos.y)
 {
 mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
 }
 break;
 } // if
} // for
Esto debería ser autoexplicativo. Añadimos una línea para apagar la búsqueda, entonces convertimos la sentencia if en un bucle for, y salimos con break tan rápido como hallamos encontrado la posición que estamos buscando. Haremos la misma cosa con la función mouseMoved. Buscar esta sección de código en el mouseMoved:
mRayScnQuery->setRay(mouseRay);
 
 Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();
 Ogre::RaySceneQueryResult::iterator iter = result.begin();
 
 if (iter != result.end() && iter->worldFragment)
 {
 mCurrentObject->setPosition(iter->worldFragment->singleIntersection);
 }
Y sustitúyela con este código
mRayScnQuery->setRay(mouseRay);
mRayScnQuery->setSortByDistance(false);
 
Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();
Ogre::RaySceneQueryResult::iterator iter = result.begin();
 
for (iter; iter != result.end(); iter++)
{
 if (iter->worldFragment)
 {
 mCurrentObject->setPosition(iter->worldFragment->singleIntersection);
 break;
 } // if
}
Compila y comprueba el código. Debería presentar una notable diferencia desde la última vez que ejecutamos, pero ahora vamos a hacer esto de la manera correcta, y las futuras actualizaciones del TerrainSceneManager no romperan nuestro código.

Máscaras de Consulta

Nota que no hay manera de saber en que modo estamos cuando seleccionamos cada objeto. Nuestro RaySceneQuery devolverá Robots o Ninjas, que estan en el frente. Esto no tiene que ser así. Todos los MovableObjects te permiten establecer una máscara de valor, y las SceneQueries te permiten filtrar tus resultados basándose en esta máscara. Todas las máscaras se hacen usando la operación binaria AND, así que si no estas familiarizado con esto, deberías ponerte a ello antes de continuar.

La primera cosa que vamos a hacer es crear la máscara de valores. Vamos al comienzo de la clase IntermediateTutorial3 y añadimos esto después de la sentencia public:
enum QueryFlags
{
 NINJA_MASK = 1<<0,
 ROBOT_MASK = 1<<1
};
Esto crea un enum con dos valores, que son en binario 0001 y 0010. Ahora, cada vez que creamos una entity Robot, llamamos a su función "setMask" para establecer las banderas de búsqueda para que sean ROBOT_MASK. Cada vez que creamos una entity Ninja llamamos a su función "setMask" y usamos NINJA_MASK en su lugar. Ahora, cuando estamos en el modo Ninja, haremos que el RaySceneQuery sólo considere objetos con la bandera NINJA_MASK, y cuando estemos en el modo Robot haremos que sólo considere ROBOT_MASK.

Busca esta sección de mousePressed:
if (bRobotMode)
{
 sprintf(name, "Robot%d", mCount++);
 ent = mSceneMgr->createEntity(name, "robot.mesh");
} // if
else
{
 sprintf(name, "Ninja%d", mCount++);
 ent = mSceneMgr->createEntity(name, "ninja.mesh");
} // else

Añadiremos dos líneas para establecer la máscara de ambos:
if (bRobotMode)
{
 sprintf(name, "Robot%d", mCount++);
 ent = mSceneMgr->createEntity(name, "robot.mesh");
 ent->setQueryFlags(ROBOT_MASK);
} // if
else
{
 sprintf(name, "Ninja%d", mCount++);
 ent = mSceneMgr->createEntity(name, "ninja.mesh");
 ent->setQueryFlags(NINJA_MASK);
} // else
Todavía tenemos que hacerlo así que cuando estemos en un modo, podemos sólo pulsar y coger objetos de ese tipo. Necesitamos establecer las banderas de consulta para que el tipo de objeto correcto pueda ser seleccionado. Cumplimos con esto estableciendo la máscara de consulta en RaySceneQuery para que sea la ROBOT_MASK en el modo Robot, y establecerla en NINJA_MASK para el modo Ninja. En la función onLeftPressed, busca este código:
mRayScnQuery->setSortByDistance(true);
Añadir esta línea de código después:
mRayScnQuery->setQueryMask(bRobotMode ? ROBOT_MASK : NINJA_MASK);
Compila y ejecuta el tutorial. Ahora seleccionamos sólo los objetos que estamos buscando. Todos los rayos que pasan a traves de los objetos pasan a traves de ellos y golpean sobre el objeto correcto. Ahora hemos terminado nuestro trabajo con este código. La siguiente sección no sera modificarlo.

Máscaras de tipo Consulta


Hay una o más cosas que considerar cuando estamos usando las consultas de escena. Suponte que estas añadiendo un billboardset o un sistema de partículas a la escena anterior, y quieres moverte por él. Encontrarás que la consulta nunca devuelve el billboardset sobre el que pulsastes. Esto es porque el SceneQuery tiene otra máscara, el QueryTypeMask, que te limita a la hora de seleccionar, sólo al tipo especificado por la bandera. Por defecto cuando haces una consulta, devuelve sólo objetos de un tipo de entity.

En tu código, si quieres que tu consulta devuelva BillboardSets o sístemas de partículas, tendrás que hacer esto antes de ejecutar tu consulta:
mRayScnQuery->setQueryTypeMask(SceneManager::FX_TYPE_MASK);
Ahora la consulta sólo devolverá BillboardSets o ParticleSystems como resultados.
Hay 6 tipos de QueryTypeMask definidos en la clase SceneManager como miembros estáticos:
WORLD_GEOMETRY_TYPE_MASK // Devuelve la geometria del mundo
ENTITY_TYPE_MASK // Devuelve las entities
FX_TYPE_MASK // Devuelve los billboardsets y los sistemas de particulas
STATICGEOMETRY_TYPE_MASK // Devuelve la geometria estatica
LIGHT_TYPE_MASK // Devuelve las luces.
USER_TYPE_MASK_LIMIT // Limite de la mascara de tipo User.
La QueryTypeMask por defecto cuando la propiedad no esta establecida manualmente a ENTITY_TYPE_MASK.

Más sobre Máscaras


Nuestro ejemplo de máscara es muy simple, así que gustaría ir a ejemplos más complicados.

Ajustando una máscara MovableObject

Cada vez que queremos crear una nueva máscara, la representación binaria debe contener sólo una de estas. Eso es, estas son máscaras válidas:

 00000001
 00000010
 00000100
 00001000
 00010000
 00100000
 01000000
 10000000
Y podemos crear fácilmente estos valores tomando 1 y desplazando los bits por el valor de posición. Esto es:
00000001 = 1<<0
00000010 = 1<<1
00000100 = 1<<2
00001000 = 1<<3
00010000 = 1<<4
00100000 = 1<<5
01000000 = 1<<6
10000000 = 1<<7
Todo igual hasta 1<<32. Esto nos da 32 máscaras distintas que podemos usar para MovableObjects.

Consultando máscaras múltiples


Podemos consultar múltiples máscaras usando el operador OR. Veamos que tenemos tres grupos diferentes de objetos en un juego:
enum QueryFlags
{
FRIENDLY_CHARACTERS = 1<<0,
ENEMY_CHARACTERS = 1<<1,
STATIONARY_OBJECTS = 1<<2
};
Ahora, si queremos consultar sólo por personajes amigos haríamos:
mRayScnQuery->setQueryMask(FRIENDLY_CHARACTERS);
Si queremos que la consulta devuelva ambos personajes enemigos y objetos estacionarios, necesitamos usar:
mRayScnQuery->setQueryMask(ENEMY_CHARACTERS | STATIONARY_OBJECTS);
Si usas muchos de estos tipos de consultas, querras definir esto en el enum:
OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS
y entonces usar simplemente OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS para consultar.

Consultando por todo pero en una Mascara


Tambien puedes consultar por todo:
mRayScnQuery->setQueryMask(~FRIENDLY_CHARACTERS);
Lo que devolvera todo los otros personajes. Tambien puedes hacer esto para multiples mascaras:
mRayScnQuery->setQueryMask(~(FRIENDLY_CHARACTERS | STATIONARY_OBJECTS));
lo que deberia devolver otras personajes amigos y objetos estacionarios.

Seleccionando todos los Objetos o No Objetos


Puedes hacer algo muy interesante con máscaras. La cosa que tienes que recordar es, que si has establecido la máscara de consulta QM para un SceneQuery, coincidiran todos los MovableObjects que tiene la máscara OM si QM y OM contiene al menos uno. Además, el ajuste de la máscara de consulta para un SceneQuery a 0 hará que no devuelva MovableObjects. El ajuste de la máscara de consulta a ~0 (0xFFFFF....) hará devolver todos los MovableObjects que no hagan la máscara 0.

Usar una máscara de 0 puede ser muy útil en algunas situaciones. Por ejemplo, el TerrainSceneManager no usa QueryMasks cuando devuelve un worldFragment. Haciendo esto:
mRayScnQuery->setQueryMask(0);
conseguirás sólo el worldFragment en tu RaySceneQueries para ese SceneManager. Esto puede ser muy útil si tienes muchos objetos en la pantalla y no quieres perder tiempo recorriendo todos ellos si sólo necesitas mirar por la intersección del Terreno.

Ejercicios


Ejercicios Fáciles


  1. El TerrainSceneManager crea losetas con una máscara por defecto de ~0 (todos las consultas seleccionan). Arreglamos este problema testando para ver si el nombre de el objeto movil es igual a "tile00,2". Incluso pienso que no esta implementado todavía, el TerrainSceneManager soporta multiples páginas, y si hubiera más cosas que sólo "tile00,2" esto podría causar que nuestro código se rompiera.
  2. En vez de hacer la comprobación en el bucle, arregla el problema apropiadamente ajustando todos los objetos loseta creados por el TerrainSceneManager para tener una máscara única. (El TerrainSceneManager crea un SceneNode llamado "Terrain" que contiene todas estas losetas. Recórrelas y establece el acoplado de las máscaras de objeto a algo de tu elección.)

Ejercicios Intermedios


  1. Nuestro programa viene con dos cosas, Robots y Ninjas. Si vamos a implementar un scene editor, podríamos querer colocar cualquier número de tipos de objeto diferentes. Generaliza este código para permitir la colocación de cualquier tipo de objeto de una lista predefinida. Crea una superposición con la lista de los objetos que quieras que el editor tenga (tales como Ninjas, Robots, Knots, Ships, etc), y tener las SceneQueries para que sólo se seleccionen ese tipo de objetos.
  2. Ya que estamos usando múltiples tipos de objetos, usa el Patrón por Defecto para crear apropiadamente los SceneNodes y las Entities.

Ejercicios Avanzados


  1. Generaliza los ejercicios previos para leer en todas las mallas que Ogre conoce (ej: todo lo que fue comprobado en el directorio Media), y da la habilidad de colocarlos. Nota que no deberías tener un límite a cuantos tipos de objeto Ogre puede colocar. Ya que sólo tenemos 32 máscaras únicas de consulta para usar, necesitarás encontrar un método de cambiar rápidamente todas las banderas de consulta para los objetos en la pantalla.
  2. Debes haber notado que cuando pulsas en un objeto, el objeto se eleva desde el fondo de la caja límite. Para ver esto, pulsa en lo alto de cualquier personaje y muévelo. Se transportará instantaneamente donde sea. Modifica el programa para arreglar este problema.

Ejercicio para el Estudio


  1. Añadir un modo para seleccionar múltiples objetos en el programa tales como que cuando presiones la tecla Ctrl y pulses en múltiples objetos se resalten. Cuando muevas estos objetos, moverlos todos como un grupo.
  2. Muchos programas de edición de escena te permiten agrupar los objetos así que se moverán siempre juntos. Implementa esto en el programa.

Siguiente, Tutorial Intermedio 4 Selección de Volumen y Manual Básico de Objetos