[XNA] Cámaras 3D y cámaras en 3a persona

¿Recordáis el juego Tomb Rider? Si habéis jugado, os acordaréis seguro de que como jugadores movíamos a Lara Croft, viéndola desde una prespectiva «trasera», es decir, que la cámara se movía siempre por detrás de ella. Esto es lo que se llama en desarrollo de videojuegos una cámara en 3a persona, o cámara persecutória (de «chase cam» en inglés, que sin duda queda mejor dicho así :-P). Dando por supuesto que habéis desarrollado una cámara para un juego 3D de XNA antes, añadir esta funcionalidad a un juego es sencillo, de todos modos vamos a repasar los conceptos de las cámaras 3D en XNA. 

Si un juego 2D es muy parecido a los clásicos dibujos animados, en que unos dibujos se superponían sobre otros, sobre una película transparente, para generar una composición final, un juego 3D es más bien parecido al rodaje de una película, mediante un objeto cámara.  La información de la cámara se almacena en las matrices de vista y proyección. La vista contiene información acerca de la posición de la cámara, la proyección define básicamete lo que la cámara puede ver. Más tarde el contenido de esta proyección es lo que la targeta gráfica transformará en una imagen 2D de la pantalla (sí. qué desengaño verdad? todavía no existen las pantallas 3D «reales», y están lejos de existir ahora mismo…). Ahh las matrices… cómo os iban en el instituto? las vísteis en la universidad? os gustaban, eh pillines? pues bueno, aquí lo bueno es que XNA nos facilita enormemente el trabajo con las matrices!!

Crear la vista

Para crear la matriz vista tenemos un método estático llamado Matrix.CreateLookAt, fácil verdad? Los parámetros que debemos pasarle son:

  1. Posición de la cámara, en un Vector 3D.
  2. Objetivo de la cámara (o dónde la cámara está «mirando»), en un Vector 3D.
  3. Vector que indica dónde está la posición «arriba» (sí, en un mundo 3D el concepto de arriba y abajo hay que definirlos, estilo barrio sésamo :-P), en un Vector 3D.

Esto en el código se traduce de la siguiente forma:

   1: private void IniciarVista() 

   2: {

   3:     posicionCamara = new Vector3(0.0f, 900.0f, 600.0f);

   4:     mirar = new Vector3(0.0f, 10.0f, 0.0f);

   5:     arriba = Vector3.Up;

   6:  

   7:     Vista = Matrix.CreateLookAt(posicionCamara, mirar, arriba);

   8: }

Donde como he comentado, posicionCamara, mirar y arriba son variables de la clase Camara de tipo Vector3, y Vista es una matriz.

Crear la proyección

Crear la proyección de una cámara no es mucho más difícil… existe otro método estático en Matrix que se llama Matrix.CreatePrespectiveFieldOfView. Este acepta los siguientes parámetros:

  1. Ángulo de la cámara en radianes, normalmente se pone π/4 (Pi/4).
  2. Proporción, o “aspect ratio” (otra vez el inglés es más claro que el castellano…).
  3. Plano cercano: distancia a partir de la cual la cámara puede “ver”.
  4. Plano lejano: distancia hasta la cual la cámara puede “ver).

Todos estos parámetros son de tipo float. La matriz proyección sirve a XNA para definir el frustum de la cámara, que es el espacio visible por la cámara. Esta imagen vale más que mil palabras…:

¿Porqué ponemos un plano cercano y uno lejano? ¿no sería mejor que se viera todo? Quizá sí… pero nos arriesgaríamos a tener serios problemas de rendimiento…

Esto en código se traduce en lo siguiente:

   1: private void IniciarProyeccion()

   2: {

   3:     float nearClip = 10.0f;

   4:     float farClip = 10000.0f;            

   5:     float aspectRatio;

   6:     

   7:     aspectRatio = (float)Configuracion.graficos.Viewport.Width / 

   8:         (float)Configuracion.graficos.Viewport.Height;

   9:     

  10:     Proyeccion = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, nearClip, farClip);

  11: }

En casi cualquier cámara 3D, el código de inicialización va a ser muy parecido a estos dos métodos que os he descrito, IniciarVista() e IniciarProyeccion(). Con eso ya podríamos renderizar una escena 3D como esta:

escena

Cámara en 3a persona

Vale, tenemos una fantástica cámara 3D… ¿ahora cómo hacemos para que vaya detrás de nuestro personaje del juego? Ya os lo debéis imaginar… simplemente hay que hacer que la posición de la cámara, el view, esté por detrás del jugador. Esto lo haremos con el método siguiente, que se llamará constantemente, a medida que vayamos moviendo al jugador:

   1: public Matrix UpDateCam(Matrix worldJugador)

   2: {

   3:     float distanciaCamara = 100.0f;

   4:  

   5:     posicionCamara = (worldJugador.Translation - (worldJugador.Backward * distanciaCamara)) 

   6:         + new Vector3(0.0f, 200.0f, 0.0f);

   7:  

   8:     Vista = Matrix.CreateLookAt(posicionCamara, worldJugador.Translation, arriba);

   9:  

  10:     return Vista;

  11: }

Básicamente lo que estamos haciendo es calculando la posición de la cámara a partir de la matriz que representa la posición del jugador en el mundo. Usamos el método Translation de la instancia de la clase que representa la posición del jugador, y otra propiedad muy útil de la matriz: Back, que representa el vector que queda detrás de la posición del jugador, a partir de su matriz mundo. A ello, le sumamos un nuevo Vector3, que nos sirve para subir la cámara, para obtener una mejor prespectiva del juego.

¡Y esto es todo amigos! Así es como quedaría la cámara aplicada al robot modelado por Jordi Giménez que vengo usando desde hace algunos tutoriales:

 

El código completo de la cámara

El código completo de la cámara sería este:

   1: using Microsoft.Xna.Framework;

   2: using Colisiones3D;

   3:  

   4: namespace PruebaColisiones3D

   5: {

   6:     public class Camara

   7:     {

   8:         public Matrix Vista;

   9:         public Matrix Proyeccion;

  10:  

  11:         Vector3 posicionCamara;

  12:         Vector3 mirar;

  13:         Vector3 arriba;

  14:  

  15:         public Camara()

  16:         {

  17:             this.IniciarVista();

  18:             this.IniciarProyeccion();

  19:         }

  20:  

  21:         private void IniciarVista() 

  22:         {

  23:             posicionCamara = new Vector3(0.0f, 900.0f, 600.0f);

  24:             mirar = new Vector3(0.0f, 10.0f, 0.0f);

  25:             arriba = Vector3.Up;

  26:  

  27:             Vista = Matrix.CreateLookAt(posicionCamara, mirar, arriba);

  28:         }

  29:     

  30:         private void IniciarProyeccion()

  31:         {

  32:             float nearClip = 10.0f;

  33:             float farClip = 10000.0f;            

  34:             float aspectRatio;

  35:             

  36:             aspectRatio = (float)Configuracion.graficos.Viewport.Width / 

  37:                 (float)Configuracion.graficos.Viewport.Height;

  38:             

  39:             Proyeccion = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, nearClip, farClip);

  40:         }

  41:  

  42:         public Matrix UpDateCam(Matrix worldJugador)

  43:         {

  44:             float distanciaCamara = 100.0f;

  45:  

  46:             posicionCamara = (worldJugador.Translation - (worldJugador.Backward * distanciaCamara)) 

  47:                 + new Vector3(0.0f, 200.0f, 0.0f);

  48:  

  49:             Vista = Matrix.CreateLookAt(posicionCamara, worldJugador.Translation, arriba);

  50:  

  51:             return Vista;

  52:         }

  53:  

  54:     }

  55: }

[XNA] Detección de colisiones en un mundo 3D (Episode III)

Continuando con el ejemplo del episodio anterior, por fin aplicaremos detección de colisiones al nivel 3D que hemos generado para «el videojuego». Tenemos un XML en el que se definen los objetos BoundingBox y BoundingSphere que se usarán en el nivel para la detección de colisiones contra el mismo, tenemos también un mundo que se carga a partir de este XML… y tenemos además un robot que se mueve con animaciones propias por este mundo tridimensional. ¿Qué nos falta ahora? Es bien sencillo… detectar las colisiones entre el robot y el mundo! Es decir, hacer que el robot no pueda “traspasar las paredes” como lo hacía en el vídeo anterior. Lo que vamos a conseguir es lo siguiente:

Superados los pasos anteriores, añadir esta funcionalidad no va a ser demasiado difícil…  lo primero que haré es asegurarme de que se rendericen los boundingbox y boundingsphere cuando lo necesite. En un videojuebo “en producción” obviamente estos objetos no se renderizan, pero durante el desarrollo puede ser muy útil, con objeto de debugación. Y además, en nuestro caso tenemos que definir la posición de los boundings en un XML sin un editor de niveles visual… así que nos será muy útil poder ver donde queda cada bounding, así será más fácil editar dicho XML. Así pues el código sería el siguiente:

   1: public void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion) 

   2: {

   3:     // Pintar modelos

   4:     foreach (ObjetoInerte modelo in nivel.modelosMundo)

   5:     {

   6:         modelo.Draw(gameTime, vista, proyeccion);

   7:     }

   8:  

   9:     if (Configuracion.Debug)

  10:     {

  11:         // Pintar cajas

  12:         BoundingBoxRender renderCaja = new BoundingBoxRender();

  13:  

  14:         foreach (BoundingBox caja in nivel.boundingBoxes)

  15:         {

  16:             renderCaja.Render(caja, Configuracion.graficos, vista, proyeccion, Color.Orange);

  17:         }

  18:  

  19:         // Pintar esferas

  20:         BoundingSphereRender renderEsfera = new BoundingSphereRender();

  21:  

  22:         foreach (BoundingSphere esfera in nivel.boundingSpheres)

  23:         {

  24:             renderEsfera.Render(new BoundingSphere(esfera.Center, esfera.Radius), Configuracion.graficos, vista, proyeccion, Color.Orange);

  25:         }

  26:     }            

  27: }

 

¿Algo extraño? Supongo que habréis observado la existencia de una instancia de una clase que se llama BoundingBoxRender y otra BoundingSphereRender. Esta es una clase que permite renderizar BoundingBox y BoundingSpheres respectivamente (con ese nombre nádie lo diría eh? :-P). Estas son clases sencillas pero extermadamente útiles, que supongo que bajé algun día de Ziggyware (snif, snif, esa web “ha muerto…”). Pues bien, esta pequeña clase nos ayudará a debugar nuestros juegos.

Ahora que tenemos los boundings cargados y pintándose… hay que comprobar si el robot colisiona contra ellos. Para ello añadiré algunas líneas al método que ya existía en el ejemplo anterior: LeerInput.

   1: Vector3 posicionInicial;

   2:  

   3: posicionInicial = base.transformacionRaiz.ObtenerMundo().Translation;

Básicamente aquí guardo la posición del robot antes de aplicarle todas las transformaciones. El método Matrix.Translation permite obtener la posición de la misma en un vector 3D.

Aplicamos las transformaciones pertinentes a las matrices, tal y como hacíamos en el ejemplo anterior, todavía dentro de LeerInput, y comprobaremos si tras las mismas existe colisión entre el robot y el mundo. Si eso ocurre, devolvemos al robot a su posición inicial antes de leer el input de teclado. ¡El resultado es que el robot ya no puede atravesar las paredes!

   1: if (nivel.HayColision(this.esfera))

   2: {

   3:     base.transformacionRaiz.Traslacion.Translation = posicionInicial;

   4:     this.esfera.Center = Posicion + desplazamiento;

   5: }

El método “HayColision” no hace magia… simplemente recorre los boundings del nivel para ver si hay colisión contra el boundingsphere que tiene el propio robot.

   1: public bool HayColision(BoundingSphere bounding) 

   2: {

   3:     foreach (BoundingSphere boundingSphereCheck in boundingSpheres)

   4:     { 

   5:         if(boundingSphereCheck.Intersects(bounding))

   6:             return true;

   7:     }

   8:     foreach(BoundingBox boundingBoxCheck in boundingBoxes)

   9:     {

  10:         if (boundingBoxCheck.Intersects(bounding))

  11:             return true;

  12:     }

  13:  

  14:     return false;

  15: }

Importante: En un juego “real” tendríamos que limitar la comprobación de colisiones de alguna manera. Existen múltiples formas, y en uno de mis artículos hablaba de una de ellas, así que para no repetirme os dejo con el enlace.

Como siempre… os dejo aquí el código del ejemplo calentito para descargar.

[XNA] Detección de colisiones en un mundo 3D – (Episode II)

En el episodio anterior comencé a introducir el tema de la detección de colisiones en juegos 3D, aplicándo el concepto a un posible nivel de un videojuego. En este episodio seguiré desarrollado este «nivel» del videojuego, en concreto diseñaré una pantalla muy simple, pero lo más importante: intentaré simplificar el trabajo que supondría añadir más niveles a un juego y gestionar su información. Así pues esta parte del tutorial podría considerarse prácticamente más afín a las estructuras de datos que no a la detección de colisiones (tema al que volveremos en el episodio III).

Lo primero va a ser disponer de 1 o N modelos que conformen nuestro mundo. En este momento pondré un modelo que se compondrá de distintos objetos, no obstante, lo ideal es que existan N objetos formando nuestro «mundo», y que renderizemos sólo aquellos que son visibles por el jugador (los que están en el frustum de la cámara). Pero por ahora no nos compliquemos la vida… un mundo con un solo modelo. En mi caso lo he hecho en MAX, pero hay un montón de aplicaciones de modelado 3D gratuitas, posteriormente habrá que exportar el modelo a FBX.

Como podéis comprobar ni siquiera aplico texturas al modelo… en este caso se trata de una simple prueba de concepto que iremos perfeccionando.

 

 

Al final el resultado será este (otra vez estoy utilizando el robot modelado por Jordi Gimenez):

Así pues vayamos a ver qué aproximación he utilizado para implementar un gestor de niveles… he creado una clase ManagerNiveles, que gestiona y contiene un “almacén de datos” al que le he llamado “Nivel”.

Nivel podría contener cualquier nivel de nuestro videojuego, mientras la definición del mismo exista en el fichero XML correspondiente (nivelN.xml), y es que basándome en el artículo anterior, ManagerNiveles carga toda la información desde un fichero externo para hacer más fácil de gestionar N niveles en un videojuego. Este XML contiene información de los boundingbox y boundingspheres que se utilizarán en la siguiente parte del tutorial.

clases

Esta clase ManagerNiveles es muy interesante porque encapsula la información del nivel y el método necesario para pintarlo, así como la referencia a todos sus modelos y sus objetos bounding. Así pues, en la clase Game, tendremos un código muy simple, comenzando obviamente por una instancia a la clase:

   1: ManagerNiveles managerNivel;

Después sólo tendremos que hacer un new de la instancia y inicializar el nivel que queramos, en nuestro caso el 1:

   1: // Inicializo el nivel

   2: managerNivel.IniciarNivel(1);

Y para renderizar el nivel, el método Draw de la clase game hará simplemente lo siguiente:

   1: // Pintamos el nivel

   2: managerNivel.Draw(gameTime, camara.Vista, camara.Proyeccion);

Sencillo, verdad? Ahora veamos el método IniciarNivel de la clase ManagerNiveles.

   1: // Deserializamos los datos desde el XML y establecemos los mismos en la clase colisionesMundo

   2: nivel = new Nivel();

   3:  

   4: XmlSerializer ser = new XmlSerializer(typeof(Nivel));

   5:  

   6: TextReader lector = new StreamReader(

   7:     string.Format(@"{0}ContentNivelesNivel{1}.xml", Environment.CurrentDirectory, numeroNivel));

   8: nivel = (Nivel)ser.Deserialize(lector);

   9: lector.Close();

  10:  

  11: // Inicializamos los objetos del mundo

  12: foreach (DatosModelo objeto in nivel.objetosMundo)

  13: {

  14:     nivel.modelosMundo.Add(new ObjetoInerte(objeto.nombreModelo, content));

  15:     nivel.modelosMundo[nivel.modelosMundo.Count - 1].transformacionRaiz.Escala *= Matrix.CreateScale(objeto.Escala);

  16: }

Ese lo que hace es deserializar la información del XML en un objeto de tipo Nivel. Posteriormente carga en memoria todos los modelos que contiene el nivel.

El dibujado, al que hemos llamado antes desde la clase game, simplemente recorre los modelos y los pinta uno a uno (aquí es donde tendremos que utilizar más tarde un frustum culling para pintar sólo los modelos que se encuentran dentro del frustum de la cámara):

   1: public void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion) 

   2: {

   3:     foreach (ObjetoInerte modelo in nivel.modelosMundo)

   4:     {

   5:         modelo.Draw(gameTime, vista, proyeccion);

   6:     }

   7: }

 Y nada más por hoy! En el próximo episodio veremos como el robot tiene detección de colisiones, que le impide atravesar las paredes, como podéis observar que ahora oucrre en el vídeo.

[XNA] Detección de colisiones en un mundo 3D – (Episode I)

Con este inicio una serie de artículos telacionados con la detección de colisiones en videojuegos 3D. Esta servirá más tarde para mover objetos por este mundo 3D sin que se salgan de los límites, o se atraviesen los unos a los otros por ejemplo.

Así pues la detección de colisiones consiste básicamente en comprobar si nuestros objetos interseccionan en el espacio, pues bien, la detección de colisiones en un espacio tridimensional, se basa en XNA en los objetos BoundingBox y BoundingSphere, que no son más que cuerpos geométricos que no se renderizan pero que utilizaremos para realizar el cálculo de las colisiones. Tened en cuenta que si hiciéramos la comprobación de colisiones polígono a polígono necesitaríamos una CPU que todavía hoy no se ha fabricado para poder hacer los cálculos de forma rápida….

boundings

Como una imagen vale más que mil palabras, aquí tenemos un par de ejemplos de boundingboxes, como véis, no son más que prismas que envielven unos modelos 3D. En un juego obviamente estos prismas no se renderizan, pero puede ser útil hacerlo para debugar nuestro juego. Un boundingShpere sería exactamente lo mismo, pero obviamente tendríamos una esfera en lugar de un prisma.

Recordar aquí que siempre va a ser mucho más óptio utilizar BoundingSpheres que no BoundingBoxes. ¿Porqué? porque las esferas son fáciles de transformar… y es muy fácil de comprobar si interseccionan con otras esferas. Al fin y al cabo, matemáticamente estamos hablando de un centro en un vector 3D y un radio… así pues las operaciones con esferas son muy rápidas. Claro está que las cajas van a ser necesarias a veces… pero es recomendable usarlas solo para objetos del escenario, como es nuestro caso en estos momentos.

Tanto la clase BoundingBox como la BoundingSphere tienen como método más importante el “Intersects”. Como no quiero quitarle el pan al MSDN, la explicación detallada de estas clases y sus métodos podéis consultarla allí. Lo que si haré es destacar que el intersects nos permitirá comrpobar si el objeto bounding intersecciona con múltiples objetos muy interesantes:

  • otros boundingbox
  • otros boundingsphere
  • rayos
  • frustrums
  • planos

Básicamente aquí lo utilizaré para detectar colisiones con otros objetos de tipo bounding, pero tened presentes las posibilidades. En artículos anteriores he hablado tanto de frustrums como de rayos, podéis consultarlos si tenéis ganas 🙂

En este caso concretamente desarrollaré una clase que permitirá aplicar BoundingBoxes y BoundingSpheres de forma masiva sobre un “mundo 3D”. El hecho es que si tenemos un mundo, o nivel del juego, sobre el cual queremos hacer detección de colisiones, esto se traduce en que tenemos que crear un montón de BoundingBox y BoundingSpheres. ¿Cómo los creamos y mantenemos? ¿Cómo los instanciamos en el código? A ver, lo ideal, sería tener un editor visual de niveles, y hacerlo gráficamente, y finalmente serializar el “mundo” en un XML, para cargarlo después desde el juego, deserializando ese XML.

Pues bien… como no voy a currarme aquí y ahora un editor de niveles… espero que os conformaréis con la serialización y deserialización de / a XML. Esto es algo sencillísimo para cualquier programador de .Net, pero a ser posible hay que hacerlo con un poco de gracia. Lo que he planteado es lo siguiente:

diagrama_boundings

El caso es que tendremos una clase que he llamado DatosBounding que será deserializada desde un fichero XML, y con ella una lisa de objetos de tipo DatosBoundingBox y DatosBoundingSphere. Esta información posteriormente la podremos asignar a la clase que contenga la información del mundo o escenario 3D.

Para serializar la clase a un XML tendríamos el siguiente código:

   1: DatosBounding listaBoundings = new DatosBounding();

   2:  

   3: listaBoundings.listaBoundingSpheres.Add(new DatosBoundingSphere(new Vector3(1.0f,2.5f,3.2322f), 5.4f));

   4: listaBoundings.listaBoundingSpheres.Add(new DatosBoundingSphere(new Vector3(4.0f, 4.5f, 2.2322f), 22.4f));

   5: listaBoundings.listaBoundingBoxes.Add(new DatosBoundingBox(new Vector3(1,2,3), new Vector3(5,6,7))); 

   6:         

   7: XmlSerializer ser = new XmlSerializer(typeof(DatosBounding));

   8: TextWriter escritor = new StreamWriter(@"C:test.xml");

   9: ser.Serialize(escritor, listaBoundings);

  10:  

  11: escritor.Close();

Lo cual, una vez ejecutado, generará el siguiente fichero XML:

<?xml version=»1.0″ encoding=»utf-8″?>
<DatosBounding xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance» xmlns:xsd=»http://www.w3.org/2001/XMLSchema»>
  <listaBoundingSpheres>
    <DatosBoundingSphere>
      <Radio>0</Radio>
      <Posicion>
        <X>1</X>
        <Y>2.5</Y>
        <Z>3.2322</Z>
      </Posicion>
    </DatosBoundingSphere>
    <DatosBoundingSphere>
      <Radio>0</Radio>
      <Posicion>
        <X>4</X>
        <Y>4.5</Y>
        <Z>2.2322</Z>
      </Posicion>
    </DatosBoundingSphere>
  </listaBoundingSpheres>
  <listaBoundingBoxes>
    <DatosBoundingBox>
      <PosicionMin>
        <X>1</X>
        <Y>2</Y>
        <Z>3</Z>
      </PosicionMin>
      <PosicionMax>
        <X>5</X>
        <Y>6</Y>
        <Z>7</Z>
      </PosicionMax>
    </DatosBoundingBox>
  </listaBoundingBoxes>
</DatosBounding>

Posteriormente, desde nuestro juego, necesitaremos cargar esta información (supuestamente en un juego el XML sería muchísimo mayor, la cual cosa compensaría la serialización y deserialización). El proceso de carga de un XML se llama deserialización, y el código sería este:

   1: DatosBounding listaBoundings2 = new DatosBounding();

   2: 

   3: TextReader lector = new StreamReader(@"C:test.xml");

   4: listaBoundings2 = (DatosBounding)ser.Deserialize(lector);

   5: lector.Close();

Observad que leo/almaceno los datos en la raíz de la C:, chicos, no hagáis esto en casa 🙂 Lo importante aquí es que después de llamar al método Deserialize ya tendríamos una lista de Boundings (listaBoundings2) cargada con los datos del XML!

En el siguiente episodio… más! Hasta entonces, si queréis podéis apalearme por escribir tanto 😛

[XNA] Animando un robot en 3D

En este artículo continuaré hablando de las animaciones en 3D, basadas en transformaciones de matrices. En esta ocasión animaré un modelo más complejo (y también más guapo, creado por el artista 3D Jordi Gimenez Ruiz -gracias Jordi!!), concretamente un robot. El robot en sí no es todavía un T1000, sus movimientos se limitan a la cabeza, brazos y ruedas con las que se desplaza.

Estos tipos de animaciones son básicamente traslaciones y rotaciones de matrices en las que se basa la posición del modelo en el espacio tridimensional. Lo más importante en estos casos, para empezar, es recordar que estas operaciones son multiplicaciones de matrices, y que el órden en que apliquemos la multiplicación variará el resultado de la animación. Para ello, os paso el enlace a un artículo anterior donde introduzco el tema de las animaciones y explico cuál es el orden en que se deben aplicar siempre las multiplicaciones sobre las matrices, leyendo primero ese artículo se entenderá mejor este tutorial.

Siguiente paso… si queremos animar un Modelo, necesitaremos acceder a sus distintos Mesh, para ello, desde la herramienta de diseño 3D que utilicemos tendremos que poner nombre a los distintos objetos que queramos animar. Lo mejor será usar nombres que recordemos fácilmente, u hacernos un esquema, como este:

esquema 

Después, desde el código podremos aplicar transformaciones a cada uno de estos objetos del modelo de forma individual. Como aplicar las transformaciones a distintos mesh de un modelo puede llegar a complicar el código, hay que pensar bien como hacerlo. En este caso os ofrezco una implementación que creo mejorable, pero que es muy muy sencilla, y sobretodo reutilizable en otros modelos.

esquema

Si os fijáis tenemos una clase abstracta “ModeloExtendido”, que se encarga de pintar un modelo y aplicarle todas las transformaciones que le apliquemos mediante el objeto Transformaciones. El propio ModeloExtendido tendrá una instancia de estas transformaciones, que aplicarán al global del modelo (por ejemplo, si queremos hacer que el robot avance, tendremos que transformarlo “todo”, no las ruedas, la cabeza o cualquier otra parte de él, pero obviamente queremos que la cabeza y demás se muevan con el resto del cuerpo… Veamos en detalle la clase Transformaciones, que como vemos es prácticamente un contenedor de datos:

   1: public class Transformaciones

   2: {

   3:     public Matrix Rotacion { get; set; }

   4:     public Matrix Traslacion { get; set; }

   5:     public Matrix Orbitacion { get; set; }

   6:     public Matrix Escala { get; set; }

   7:  

   8:     public string NombreMesh { get; set; }

   9:  

  10:     /// <summary>

  11:     /// Inicializa las matrices

  12:     /// </summary>

  13:     public Transformaciones(string Mesh) 

  14:     {

  15:         this.NombreMesh = Mesh;

  16:         Rotacion = Matrix.Identity;

  17:         Traslacion = Matrix.Identity;

  18:         Escala = Matrix.Identity;

  19:         Orbitacion = Matrix.Identity;

  20:     }

  21:  

  22:     /// <summary>

  23:     /// Genera la matriz mundo

  24:     /// </summary>

  25:     /// <returns></returns>

  26:     public Matrix ObtenerMundo() 

  27:     {

  28:         return Escala * Rotacion * Orbitacion * Traslacion;

  29:     }

  30: }

Así pues las clases que hereden de ModeloExtendido podrán contener una colección de N transformaciones, en este caso, el objeto RobotPatines tiene las siguientes instancias, que representan cada uno de los mesh que hemos visto en el esquema anterior:

   1: private Transformaciones patines_derecha_delante = new Transformaciones("patins_dreta_davant");

   2: private Transformaciones patines_derecha_detras = new Transformaciones("patins_dreta_darrera");

   3: private Transformaciones patines_izquierda_delante = new Transformaciones("patins_esquerra_davant");

   4: private Transformaciones patines_izquierda_detras = new Transformaciones("patins_esquerra_darrera");

   5: private Transformaciones cabeza = new Transformaciones("cap");

   6: private Transformaciones brazo_derecho = new Transformaciones("brac_dret");

   7: private Transformaciones brazo_izquierdo = new Transformaciones("brac_esquerra");

En el método Update() de RobotPatines hay que leer del teclado y calcular las transformaciones matriciales necesarias. En XNA esto es sencillo! Veamos:

   1: private void LeerInput() 

   2: {

   3:     KeyboardState kbState = Keyboard.GetState();

   4:  

   5:     if (kbState.IsKeyDown(Keys.Up))

   6:     {

   7:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

   8:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

   9:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  10:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  11:  

  12:         base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Backward * VELOCIDAD_DESPLAZAMIENTO);

  13:     }

  14:     if (kbState.IsKeyDown(Keys.Down))

  15:     {

  16:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  17:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  18:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  19:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  20:  

  21:         base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Forward * VELOCIDAD_DESPLAZAMIENTO);

  22:     }

  23:     if (kbState.IsKeyDown(Keys.Left))

  24:     {

  25:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  26:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  27:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  28:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  29:  

  30:         base.transformacionRaiz.Rotacion *= Matrix.CreateRotationY(VELOCIDAD_GIRO);

  31:     }

  32:     if (kbState.IsKeyDown(Keys.Right))

  33:     {

  34:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  35:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  36:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  37:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  38:  

  39:         base.transformacionRaiz.Rotacion *= Matrix.CreateRotationY(-VELOCIDAD_GIRO);

  40:     }

  41:     if (kbState.IsKeyDown(Keys.O))

  42:     {

  43:         this.cabeza.Rotacion *= Matrix.CreateRotationY(-VELOCIDAD_CABEZA);

  44:     }

  45:     if (kbState.IsKeyDown(Keys.P))

  46:     {

  47:         this.cabeza.Rotacion *= Matrix.CreateRotationY(VELOCIDAD_CABEZA);

  48:     }

  49:     if (kbState.IsKeyDown(Keys.U))

  50:     {

  51:         this.brazo_derecho.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_CABEZA);

  52:     }

  53:     if (kbState.IsKeyDown(Keys.I))

  54:     {

  55:         this.brazo_derecho.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_CABEZA);

  56:     }

  57:     if (kbState.IsKeyDown(Keys.J))

  58:     {

  59:         this.brazo_izquierdo.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_CABEZA);

  60:     }

  61:     if (kbState.IsKeyDown(Keys.K))

  62:     {

  63:         this.brazo_izquierdo.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_CABEZA);

  64:     }

  65: }

Fácil verdad? La única operación que tiene un poco más de “chicha” es la traslación de la instancia de transformaciones transformacionRaiz, que afecta al modelo entero, en concreto a la transformación de Traslación. Esto lo que hará será mover el robot hacia el frente teniendo en cuenta su giro:

   1: base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Forward * VELOCIDAD_DESPLAZAMIENTO);

Aquí tenemos que dar gracias a los programadores del framework de XNA porque realmente nos están facilitando mucho la vida con el tema de las matrices…

Vale, ahora viene lo más “complicado”, aplicar todas estas transformaciones al modelo… pues no es tan difícil no creáis. He creado una sobrecarga al Draw de la clase base, al cual le envío todas las transformaciones de esta forma (esto es todavía RobotPatines):

   1: public override void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion)

   2: {

   3:     List<Transformaciones> transformaciones = new List<Transformaciones>();

   4:  

   5:     transformaciones.Add(patines_derecha_delante);

   6:     transformaciones.Add(patines_derecha_detras);

   7:     transformaciones.Add(patines_izquierda_delante);

   8:     transformaciones.Add(patines_derecha_delante);

   9:     transformaciones.Add(patines_izquierda_detras);

  10:     transformaciones.Add(cabeza);

  11:     transformaciones.Add(brazo_derecho);

  12:     transformaciones.Add(brazo_izquierdo);

  13:  

  14:     base.Draw(gameTime, vista, proyeccion, transformaciones);

  15: }

Y así es como se dibujaría el modelo en la clase base, en ModeloExtendido:

   1: public virtual void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion, List<Transformaciones> transformacionesHijas) 

   2: {

   3:     modelo.CopyAbsoluteBoneTransformsTo(transformaciones);

   4:  

   5:     foreach(ModelMesh mesh in modelo.Meshes)

   6:     {

   7:         foreach (BasicEffect efecto in mesh.Effects) 

   8:         {

   9:             efecto.EnableDefaultLighting();

  10:  

  11:             if (transformacionesHijas != null && transformacionesHijas.Find(n => n.NombreMesh != string.Empty && n.NombreMesh == mesh.Name) != null)

  12:             {

  13:                 efecto.World = transformacionesHijas.Find(n => n.NombreMesh == mesh.Name).ObtenerMundo() * transformaciones[mesh.ParentBone.Index] * transformacionRaiz.ObtenerMundo();

  14:             }

  15:             else

  16:                 efecto.World = transformaciones[mesh.ParentBone.Index] * transformacionRaiz.ObtenerMundo();

  17:  

  18:             efecto.View = vista;

  19:             efecto.Projection = proyeccion;

  20:         }

  21:  

  22:         mesh.Draw();

  23:     }

  24: }


Observad que utilizo una expresión lambda para aplicar las transformaciones enviadas por parámetro a los mesh que correspondan.

Este es el resultado:

Os dejo el código por si queréis juguetear con él. Enjoy!