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


external image help.gifCualquier problema mientras trabajes con este tutorial puedes preguntarlo en los Foros de Ayuda.


Introducción


En este tutorial crearemos el comienzo de un Editor de Escena Básico. Durante este proceso, trataremos:

  1. Como usar RaySceneQueries para guardar a la cámara de caer dentro del terreno.
  2. Como usar las interfaces MouseListener y MouseMotionListener.
  3. Usando el ratón para seleccionar las coordenadas x e y sobre el terreno.

Aquí esta el código para el Tutorial Intermedio 2. Deberías ir añadiendo poco a poco código y mirando los resultados.

Prerrequisitos


Este tutorial asume que sabes como crear proyectos en Ogre y compilarlos con acierto. Que tienes conocimientos de los objetos básicos de Ogre (SceneNodes, Entities, etc). Deberías también estar familiarizado con los iteradores básicos de STL, ya que este tutorial los usa. (Ogre también usa mucho de STL, si no estas familiarizado con ello, deberias tomarte tu tiempo para aprenderlos).

Comenzando


Primero, necesitas crear un nuevo proyecto y añadir el siguiente código:

cabecera ITutorial02
#ifndef ITutorial02_h_
#define ITutorial02_h_
 
#include "BaseApplication.h"
 
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
 #include "../res/resource.h"
#endif
 
class ITutorial02 : public BaseApplication
{
 
public:
 ITutorial02(void);
 virtual ~ITutorial02(void);
 
protected:
 virtual void createScene(void);
 virtual void chooseSceneManager(void);
 virtual void createFrameListener(void);
 // frame listener
 virtual bool frameRenderingQueued(const Ogre::FrameEvent &evt);
 // mouse listener
 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);
 
protected:
 Ogre::RaySceneQuery *mRaySceneQuery; // El puntero de consulta del rayo de la escena
 bool mLMouseDown, mRMouseDown; // True si se pulsa cualquier boton del raton
 int mCount; // Numero de robots en pantalla
 Ogre::SceneNode *mCurrentObject; // El objeto creado nuevamente
 CEGUI::Renderer *mGUIRenderer; // Renderizador CEGUI
 float mRotateSpeed;
 
};
 
#endif #ifndef ITutorial02_h_
Implementación ITutorial02
#include <CEGUISystem.h>
#include <CEGUISchemeManager.h>
#include <RendererModules/Ogre/CEGUIOgreRenderer.h>
 
#include "ITutorial02.h"
 
// -------------------------------------------------------------------------------------
ITutorial02::ITutorial02(void)
{
}
// -------------------------------------------------------------------------------------
ITutorial02::~ITutorial02(void)
{
}
// -------------------------------------------------------------------------------------
void ITutorial02::createScene(void)
{}
void ITutorial02::createFrameListener(void)
{
 BaseApplication::createFrameListener();
}
void ITutorial02::chooseSceneManager(void)
{
 // Usa el scene manager de terreno.
 mSceneMgr = mRoot->createSceneManager(ST_EXTERIOR_CLOSE);
}
 
bool ITutorial02::frameRenderingQueued(const Ogre::FrameEvent &evt)
{return BaseApplication::frameRenderingQueued(evt);}
bool ITutorial02::mouseMoved(const OIS::MouseEvent &arg)
{return true;}
bool ITutorial02::mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{return true;}
bool ITutorial02::mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{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
 ITutorial02 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 este código compila antes de continuar. También, necesitarás configurar tu proyecto para reconocer CEGUI ya que no esta empaquetado con Ogre, así que recuerda añadirlo a las propiedades de tu proyecto (en Visual Studio):

  • En C/C++, añade la ruta de los archivos include de CEGUI a los directorios adicionales Include.
  • En las opciones del Enlazador, añade la ruta a los archivos de la librería CEGUI en los Directorios de Librería Adicional y añade "CEGUIOgreRenderer_d.lib CEGUIBase_d.lib" a las dependencias adicionales (elimina el _d de la configuración Release).
  • Añade la ruta al directorio bin de CEGUI a las Variables de Entorno o copy las DLLs de CEGUI a un lugar que tu proyecto las puede encontrar.

Creando la Escena


Ve al método ITutorial02::createScene. El siguiente código debería ser familiar. Si no, consulta la referencia del API de Ogre antes de continuar. Añade esto a createScene:
// Establecer la luz ambiental
 
 mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));
 mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);
 
 // Geometria del mundo
 mSceneMgr->setWorldGeometry("terrain.cfg");
 
 // Establecer el punto de mira de la camara
 mCamera->setPosition(40, 100, 580);
 mCamera->pitch(Ogre::Degree(-30));
 mCamera->yaw(Ogre::Degree(-45));
Ahora que hemos establecido una geometría básica del mundo, necesitamos encender el cursor. Hacemos esto usando llamadas a función de CEGUI. Antes de que puedas hacer eso, sin embargo, necesitamos comenzar con CEGUI. Esto es muy fácil ahora- el método bootstrapSystem() hará todo lo necesario por nosotros. Nota que esto también hace que CEGUI use el sistema de manejo de recursos de Ogre.
// instalacion CEGUI
 mGUIRenderer = &CEGUI::OgreRenderer::bootstrapSystem();
Ahora necesitamos mostrar el cursor. Otra vez, no explicare la mayoria del codigo. Lo revisitaremos en un tutorial posterior.
// Raton
 CEGUI::SchemeManager::getSingleton().create((CEGUI::utf8*)"TaharezLook.scheme");
 CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
Si compilas y ejecutas el código, verás un cursor en el centro de la pantalla, pero no se moverá todavía.

Es necesario que le digas al manejador de recursos de Ogre los recursos de CEGUI. Si te salen excepciones durante la ejecución, intenta añadir lo siguiente al resources.cfg:
[CEGUI]
FileSystem=/usr/share/CEGUI/schemes
FileSystem=/usr/share/CEGUI/fonts
FileSystem=/usr/share/CEGUI/imagesets
FileSystem=/usr/share/CEGUI/layouts
FileSystem=/usr/share/CEGUI/looknfeel
FileSystem=/usr/share/CEGUI/lua_scripts
FileSystem=/usr/share/CEGUI/schemes
FileSystem=/usr/share/CEGUI/xml_schemas

Introduciendo el FrameListener


Esto fue todo lo que tienes hacer para la aplicación. El FrameListener es la porción complicada del código, asi que me detendré a explicarlo.

  • Primero, queremos enlazar el botón del ratón derecho a un modo de vista del mouse. Este modo no es deseable en todo los casos, se usar para dirigir la mirada mediante el ratón, asi que nuestra primera prioridad sera añadir el control de ratón de vuelta al programa (cuando mantengamos el boton del raton derecho presionado). NOTA: el tutorial de framework ya maneja el control de cámara gracias a la clase sdkCameraMan desde OgreBites pero implementaremos uno desde cero.
  • Segundo, queremos hacerlo de forma que la cámara no pase a través del Terreno.
  • Tercero, queremos añadir entities a la escena donde el terreno nos deje hacer click.
  • Finalmente, queremos ser capaces de coger entidades. Esto será mediante el botón izquierdo del ratón y manteniendo pulsado el botón querremos ver la entidad, y moverla al lugar deseado.

Para hacer esto usaremos varias variables protegidas (estas han sido añadidas a la clase):
Ogre::RaySceneQuery *mRaySceneQuery; // El puntero de consulta del rayo de la escena
 bool mLMouseDown, mRMouseDown; // True si cualquier boton del raton esta presionado
 int mCount; // El numero de robots de la pantalla
 Ogre::SceneNode *mCurrentObject; // El objeto creado nuevamente
 CEGUI::Renderer *mGUIRenderer; // El renderizador cegui
 float mRotateSpeed;
La variable mRaySceneQuery guarda una copia de la RaySceneQuery que estamos usando para encontrar las coordenadas sobre el terreno. Las variables mLMouseDown y mRMouseDown que seguiran si tenemos presionados el botón izquierdo del ratón, falso en otro caso). mCount cuenta el número de entidades que tenemos en la pantalla. mCurrentObject guarda un puntero al SceneNode creado más recientemente que hemos creado (usaremos esto para coger la entidad). Finalmente, mGUIRenderer guarda un puntero al Renderizador CEGUI, que usaremos para actualizar CEGUI.

También nota que hay muchas funciones relacionadas con listeners de Ratón que sobreescribimos para proporcionar el control de cámara.

Creando el FrameListener


Ve al metodo createFrameListener, y añade el siguiente código de inicialización después de llamar a BaseApplication::createFrameListener(). Nota que también estamos reduciendo la velocidad de rotación de la cámara ya que el Terreno es pequeño.
// Instala las variables por defecto
 mCount = 0;
 mCurrentObject = NULL;
 mLMouseDown = false;
 mRMouseDown = false;
 mSceneMgr = sceneManager;
 
 // Reduce la velocidad de rotacion
 mRotateSpeed =.1;
Finalmente, necesitamos crear el objeto RaySceneQuery. Esto es realizado mediante una llamada a SceneManager:
// Crear RaySceneQuery
 mRaySceneQuery = mSceneMgr->createRayQuery(Ogre::Ray());
Esto es todo lo que necesitamos para el createFrameListener, pero si creamos un RaySceneQuery, debemos destruirlo más tarde. Ve al destructor ITutorial02 y añade la siguiente línea:
// Creamos la consulta, y tambien somos responsables de borrarla.
 mSceneMgr->destroyQuery(mRaySceneQuery);
Asegurate de compilar tu codigo antes de seguir a la siguiente seccion.

Añadiendo donde apunta el ratón


Tenemos que enlazar el modo de mirada del raton al boton derecho del raton. Para hacer esto, vamos a:

  • Actualizar CEGUI cuando el raton se mueva (asi que el cursor tambien se mueva)
  • Establecer mRMouseButton a true cuando el raton se presione.
  • Establecer mRMouseButton a false cuando el raton se suelte.
  • Cambiar la vista cuando el raton este "cogido"
  • Ocultar el cursor del raton cuando el raton este cogiendo.

Encontrar el metodo ITutorial02::mouseMoved. Anyadiremos el codigo para mover el cursor del raton cada vez que el raton se mueva. Anyade este codigo a la funcion:
// Actualiza CEGUI con el movimiento del raton
CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
Ahora busca el método ITutorial02::mousePressed method. Esta pieza de código oculta el cursor cuando el botón derecho del ratón se presiona, y establece la variable mRMouseDown a true.
// Se presiona el boton izquierdo del raton
 if (id == OIS::MB_Left)
 {
 mLMouseDown = true;
 } // if
 
 // Boton derecho del raton presionado
 else if (id == OIS::MB_Right)
 {
 CEGUI::MouseCursor::getSingleton().hide();
 mRMouseDown = true;
 } else if // else if
Lo siguiente, necesitamos mostrar el cursor del ratón otra vez y cambiar mRMouseDown cuando el botón del ratón derecho se suelte. Encontrar la función mouseReleased, y añadir este código:
// Se suelta el boton izquierdo del raton
 if (id == OIS::MB_Left)
 {
 mLMouseDown = false;
 } // if
 
 // Se suelta el boton derecho del raton
 else if (id == OIS::MB_Right)
 {
 CEGUI::MouseCursor::getSingleton().show();
 mRMouseDown = false;
 } // else if
Ahora que tenemos todos los prerrequisitos de codigo escritos, queremos cambiar la vista cuando el ratón se mueva mientras el botón derecho del raton es presionado. Lo que vamos a hacer es leer la distancia que se ha movido desde la última vez que el método fue llamado. Esto se realiza del mismo modo que hemos rotado la cámara en el tutorial Básico 5. Encontramos la función ITutorial::mouseMoved y añadimos el siguiente código justo antes del retorno:
// Si estamos cogiendo con el boton izquierdo del raton
 if (mLMouseDown)
 {
 } // if
 
 // Si estamos cogiendo con el boton derecho del raton
 else if (mRMouseDown)
 {
 mCamera->yaw(Ogre::Degree(-arg.state.X.rel * mRotateSpeed));
 mCamera->pitch(Ogre::Degree(-arg.state.Y.rel * mRotateSpeed));
 } // else if
Ahora si compilamos y ejecutamos este código seremos capaces de controlar donde apunta la cámara pulsando el botón derecho del ratón.

Detección de Colisión con el Terreno


Ahora vamos a hacer que cuando nos movamos por el terreno, no podamos atravesarlo. Ya que BaseApplication::createFrameListener esta listo para manejar el movimiento de la cámara, no vamos a tocar ese código. En vez de esto, después de que BaseApplication::createFrameListener() mueva la cámara vamos a asegurarnos de que la cámara este 10 unidades por encima del terreno. Si no esta, vamos a colocarlo allí. Por favor sigue este código con atención. Usaremos RaySceneQuery para hacer otras cosas pero por ahora en este tutorial hemos terminado, y no entraremos en más detalle en esta sección.

Ve al método de ITutorial02::frameRenderingQueued y elimina todo el código del método. La primera cosa que vamos a hacer es llamar al método BaseApplication::frameRenderingQueued para realizar todas sus funciones normales.
Si devuelve false, devolveremos false también.
// Procesa el codigo base de frame listener. Ya que vamos a
 // manipular el vector de translacion, necesitamos que esto ocurra primero.
 if (!BaseApplication::frameRenderingQueued(evt))
 return false;
Hacemos esto en lo alto de nuestra función frameRenderingQueued porque la función miembro BaseApplication::frameRenderingQueued maneja la actualización de la ventana TrayManager desde OgreBites (la ventana de FPS y el logotipo de Ogre) y necesitamos realizar el resto de nuestras acciones en esta función después de que esto ocurra. Nuestra meta es encontrar la posición actual de la cámara, y dispara un rayo recto dentro del terreno. Esto se llama con RaySceneQuery, y nos dirá el alto del terreno debajo de nosotros. Después de conseguir la posición actual de la cámara, necesitamos crear un rayo. Un rayo toma un origen (donde el razo comienza), y la dirección. En este caso nuestra dirección será NEGATIVE_UNIT_Y, ya que estamos apuntando el rayo hacia abajo. Una vez que hemos creado el rayo, le diremos al objeto RaySceneQuery que lo use.
// Instalar la consulta de la escena
 Ogre::Vector3 camPos = mCamera->getPosition();
 Ogre::Ray cameraRay(Ogre::Vector3(camPos.x, 5000.0f, camPos.z), Ogre::Vector3::NEGATIVE_UNIT_Y);
 mRaySceneQuery->setRay(cameraRay);
Nota que hemos usado un alto de 5000.0f en vez de la posicion actual de la camara. Si usamos la posicion Y de la camara en vez de esta altura podriamos perder el terreno completamente si la camara esta bajo el terreno. Ahora necesitamos ejecutar la consulta y conseguir los resultados. Los resultados de la consulta viene a estar en la forma de un std::iterator, lo que describiremos brevemente.
// Realizar la consulta de la escena
 Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
 Ogre::RaySceneQueryResult::iterator itr = result.begin();
El resultado de la consulta es básicamente una lista de worldFragments (fragmentos de mundo) (en este caso el Terrain) y una lista de móviles (cubriremos los móviles en un tutorial posterior). Si no estamos familiarizados con los iteradores de STL, sólo sabemos que conseguiremos el primer elemento del iterator, llama al método de comienzo. Si el result.begin() == result.end(), entonces no hay resultados para devolver. En el siguiente tutorial tenemos que tratar con múltiples valores de retorno para las SceneQuerys. Por ahora, sólo haremos un movimiento ondulatorio y nos moveremos a través de él. La siguiente línea de código asegura que la consulta devuelva al menos un resultado ( itr != result.end() ), y que el resultado es el terreno (itr->worldFragment).
// Consigue los resultados, establece el alto de la camara
 if (itr != result.end() && itr->worldFragment)
 {
La estructura worldFragment contiene la posicion en donde el rayo golpea al terreno en la variable singleIntersection (que es un Vector3). Vamos a conseguir el alto del terreno asignado el valor y de este vector a la variable local. Una vez tengamos el alto, vamos a ver si la camara esta debajo del alto, y asi vamos a mover la camara arriba hasta esta altura. Nota que necesitamos mover la camara 10 unidades. Esto asegura que no podemos ver a traves del terreno si estamos demasiado cerca de el.
Ogre::Real terrainHeight = itr->worldFragment->singleIntersection.y;
 if ((terrainHeight + 10.0f) > camPos.y)
 mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
 }
 
 return true;
Finalmente, devolvemos el valor true para continuar renderizando. En este punto deberias compilar y comprobar tu programa.

Selección de Terreno


En esta seccion se creara y anyadiran objetos a la pantalla cada vez que pulses el boton izquierdo del raton. Cada vez que pulses y mantengas pulsado el boton izquierdo del raton, se creara un objeto y "mantendra" el cursor. Puedes moverte alrededor del objeto hasta que sueltes el boton, en ese punto se bloquera en ese lugar. Para hacer esto necesitamos cargar la funcion mousePressed para hacer algo diferente cuando pulsas el boton izquierdo del raton. Busca el siguiente codigo en la funcion ITutorial02::mousePressed. Anyadiremos este codigo desde del if.
// Pulsacion del boton izquierdo del raton
 if (id == OIS::MB_Left)
 {
 mLMouseDown = true;
 } // if
La primera porcion del codigo te resultara muy familiar. Crearemos un Ray para usarlo con el objeto mRaySceneQuery, y los ajustes del Ray. Ogre nos lo proporciona con Camera::getCameraToViewportRay; una funcion amiga que traslada un click de la pantalla (coordenadas x e y) en un Ray que puede ser usado con el objeto RaySceneQuery.
// Boton izquierdo del raton presionado
 if (id == OIS::MB_Left)
 {
 // Instalar la consulta del rayo de la escena, usa la posicion del raton de CEGUI
 CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
 Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
 mRaySceneQuery->setRay(mouseRay);
A continuacion, ejecutamos la consulta y nos aseguramos de que devuelva un resultado.
// Ejecuta la consulta
 Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
 Ogre::RaySceneQueryResult::iterator itr = result.begin( );
 
 // Consigue los resultados, crea un node/entity sobre la posicion
 if (itr != result.end() && itr->worldFragment)
 {
Ahora que tenemos el worldFragment (y por lo tanto la posicion que fue pulsada), vamos a crear el objeto y colocarlo en esa posicion. Nuestra primera dificultad es que cada Entity y SceneNode en Ogre necesita un nombre unico. Para conseguir esto vamos a nombralos asi Entity "Robot1", "Robot2", "Robot3" ... y cada SceneNode "Robot1Node", "Robot2Node", "Robot3Node" ... y asi todos. Primero creamos el nombre (consulta la referencia de C para mas informacion sobre sprintf).
char name[16];
 sprintf( name, "Robot%d", mCount++);
A continuacion, creamos la Entity y el SceneNode. Nota que usamos itr->worldFragment->singleIntersection para nuestra posicion por defecto del Robot. Tambien lo escalamos hasta un tamanyo de 1/10 por causa del pequeno tamanyo del terreno. Asegurate de tomar nota de que estamos asignando este objeto creado nuevamente a la variable miembro mCurrentObject. Usaremos eso en la siguiente seccion.

//Ogre::Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(std::string(name) + "Node", itr->worldFragment->singleIntersection);
mCurrentObject->attachObject(ent);
mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
} // if
 
mLMouseDown = true;
} // if//
Ahora compila y ejecuta el demo. Puedes colocar ahora los Robots sobre la Escena pulsando donde quieras sobre el Terreno. Hemos completado nuestro programa, pero necesitamos implementar el agarre de objetos antes de que terminemos. Anyadiremos el codigo dentro de la clausula if:


//// Si hemos agarrado un objeto con el boton izquierdo del raton.
if (mLMouseDown)
{
} // if//
El siguiente trozo de codigo deberia ser autoexplicativo. Creamos un Ray basado en la posicion actual del raton, entonces ejecutamos un RaySceneQuery y movemos el objeto a la nueva posicion. Nota que no tenemos que comprobar mCurrentObject para ver si es valido o no, porque mLMouseDown no deberia ser true si mCurrentObject no ha sido establecido por mousePressed. //

if (mLMouseDown)
{
CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));
mRaySceneQuery->setRay(mouseRay);
 
Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
Ogre::RaySceneQueryResult::iterator itr = result.begin();
 
if (itr != result.end() && itr->worldFragment)
mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
} // if

Compila y ejecuta el programa. Ya, hemos terminado!

external image Forum_icon_info.gifNota informativa: Tu ( = el origen del Rayo) debes estar sobre el Terrain para que RaySceneQuery reporte la intersección cuando usamos el TerrainSceneManager.

external image Forum_icon_info.gifNota informativa: Si estas usando tu propio framework, asegúrate de que tu consulta de la escena tiene acceso al frame listener, ej: en el método frameStarted(). De otro modo, si lo usas en la función init() no tendrás resultados.

Ejercicios de Estudio


Ejercicios Fáciles


  1. Mantener la cámara mirando hacia el terreno, escogemos 10 unidades encima del Terrain. Esta selección fue arbitraria. ¿Podríamos mejorar este número y acercarnos al Terrain si atravesarlo? Si es asi, hacer esta variable un miembro de clase estática y asignarlo aquí.
  2. Algunas veces queremos atravesar el terreno, especialmente en un SceneEditor (Editor de Escenas). Crea una bandera con detección del cambio de colisión encendido y apagado, y enlaza esto a una tecla en el teclado. Asegúrate de que no haces una consulta SceneQuery en frameStarted si la detección de la colisión esta apagada.

Ejercicios Intermedios


  1. Estamos haciendo la SceneQuery en cada frame, tanto si la cámara se mueve o no. Arregla este problema y no sólo harás una SceneQuery si la cámara se ha movido. (Encuentra el vector traslación en ExampleFrameListener, después de que la función es llamada compruébalo contra Vector::ZERO.)

Ejercicios Avanzados


  1. Nota que hay mucha duplicación de código cada vez que hacemos una llamada a la consulta de la escena. Envuelve todo la funcionalidad relacionada con SceneQuery en una función protegida. Asegúrate de manejar el caso donde el Terrain no esta interseccionando nada.
  2. En este tutorial hemos usado RaySceneQueries para colocar los objetos sobre el Terreno. Podríamos usarlo para muchos otros propositos. Coge el código del Tutorial 1 y completa la Difícil cuestión 1 y la Cuestión Experta 1. Entonces junta ese código con este en el que el Robot anda sobre el terreno en vez de sólo en el espacio vacío.
  3. Añade el código para que cada vez que pulses en un punto de la escena, el robot se mueve a esta posición.

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