[XNA] Sistemas de partículas en 2D

Otro “must-do” de la programación gráfica es implementar nuestro propio sistema de partículas. Un sistema de partículas, a pesar de lo sofisticado de su nombre, no es más que un montón de “objetos”, a los que llamaremos partículas, cada uno de ellos con un comportamiento autónomo, pero con cierta relación con el de las demás partículas del mismo sistema. Esto se acostumbra a utilizar para generar efectos parecidos a los que se producen en la naturaleza, como humo, fuego, explosiones… la idea básica aplica tanto para 2D como 3D, sólo cambiarían los tipos de vectores y la forma de pintar, pero la lógica sería la misma o muy parecida.

Como no se si os habré aclarado o liado más con mi particular definición de lo que es un sistema de partículas… os pongo un enlace a la definición técnica de la wikipedia

Así pues, sin más dilación, en este ejemplo os explicaré como implementar un sencillo sistema de partículas para generar un efecto parecido al humo, básicamente el código hace lo que podéis ver en este vídeo:

[View:http://www.youtube.com/watch?v=W4_SX4DNf5c:550:0]

Cómo podéis ver el efecto es muy sencillo, pero trabajándolo se pueden conseguir cosas muy interesantes. Veamos el código de una vez…

Toda la lógica se almacena en una clase que he llamado ManagerParticulas. Con esto conseguimos tener un objeto reutilizable para aplicar las partículas de ese tipo en el momento que queramos. Contiene un montón de constantes, la textura (que es la misma para todas las partículas) y la posición inicial de la que parten todas las partículas “en principio”. Y lo pongo entre comillas porqué al inicializar cada partícula es muy importante que apliquemos un factor de aleatoriedad a cada propiedad de la misma… Todas esas constantes definen valores máximos y mínimos relativos a las partículas.

managerParticulas

La clase Particula es la que nos permite almacenar la información de los cientos de partículas que podemos manejar a la vez.

particula

Vamos a ver algunos de los métodos más interesantes… El constructor cargará la textura y inicializa el array de partículas, y establece la posición inicial.

   1: public ManagerParticulas(ContentManager contentManager, int numeroParticulas, Vector2 posicionInicial) 

   2: {

   3:     this.posicionInicial = posicionInicial;

   4:  

   5:     // Inicializar textura (es la misma para todas las partículas)

   6:     this.textura = contentManager.Load<Texture2D>("humo");

   7:  

   8:     // Inicializar array de partículas, aplicando aleatoriedad en los parámetros para que parezca más "natural"

   9:     particulas = new Particula[numeroParticulas];

  10:     

  11: }

El método Update, que se llamará desde fuera del Manager de Partículas, llama a los métodos encargados de mover a las partículas y de parametrizar a las no inicializadas.

   1: public void Update(GameTime gameTime) 

   2: {

   3:     // Animar partículas "vivas"

   4:     this.AnimarParticulas(gameTime);

   5:  

   6:     // Reparametrizar partículas "muertas"

   7:     this.ParametrizarParticulas(this.posicionInicial, gameTime);

   8: }

La animación como he comentado mueve las partículas y además establece un fade-in fade-out para que las partículas no aparezcan y desaparezcan de golpe, creando un efecto más natural. Esto lo hacemos estableciendo la propiedad alfa de cada instancia de la clase Particula.

   1: private void AnimarParticulas(GameTime gameTime) 

   2: {

   3:     for (int i = 0; i < particulas.Length; i++) 

   4:     {

   5:         if (particulas[i] != null) 

   6:         {

   7:             // Movemos las partículas

   8:             particulas[i].edad += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

   9:             particulas[i].Mover();

  10:  

  11:             // Aplicamos fadeout

  12:             if (particulas[i].edad >= particulas[i].edadMuerte * TIEMPO_INICIO_DECREMENTO_FADEOUT)

  13:             {

  14:                 if (particulas[i].tiempoDesdeUltimoFadeout >= INTERVALO_FADEOUT)

  15:                 {

  16:                     particulas[i].tiempoDesdeUltimoFadeout = 0.0f;

  17:                     particulas[i].alfa -= DECREMENTO_FADEOUT;

  18:                 }

  19:             }

  20:             else

  21:             {

  22:                 if(particulas[i].alfa<MAX_ALFA)

  23:                     particulas[i].alfa += INCREMENTO_FADEOUT;

  24:             }

  25:  

  26:             particulas[i].tiempoDesdeUltimoFadeout += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

  27:         }                

  28:     }

  29: }

Este método es el que tiene más “chicha”, es el que parametriza las partículas, las crea en el intervalo establecido y les aplica un factor de “aleatoriedad” a sus propiedades, para que todas las partículas sean “parecidas pero no iguales” (sinó menudo humo más raro no?).

Comentar que hay varios bucles for que se podrían poner en una sola función, y ahorraríamos tiempo de procesamiento (lo cual se traduce en que podríamos meter más parículas en el sistema sin afectar al rendimiento), pero aquí lo he separado para que el código sea más limpio y fácil de entender.

   1: private void ParametrizarParticulas(Vector2 posicionInicial, GameTime gameTime)

   2: {

   3:     for (int i = 0; i < particulas.Length; i++)

   4:     {

   5:         if (tiempoCreacionUltimaParticula >= INTERVALO_CREACION_PARTICULAS)

   6:         {

   7:             // Instanciamos una nueva partícula si todavía no lo está

   8:             if (particulas[i] == null)

   9:             {

  10:                 particulas[i] = new Particula();

  11:             }

  12:  

  13:             // Sólo reparametrizamos las partículas "muertas"

  14:             if (particulas[i].edad >= particulas[i].edadMuerte)

  15:             {

  16:                 // Reinicializamos la edad

  17:                 particulas[i].edad = 0.0f;

  18:  

  19:                 // Reinicializamos alfa

  20:                 particulas[i].alfa = 0.0f;

  21:                 particulas[i].tiempoDesdeUltimoFadeout = 0.0f;

  22:  

  23:                 // Situamos la posición inicial

  24:                 particulas[i].posicion = posicionInicial + new Vector2((float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_POSICIONINICIAL), (float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_POSICIONINICIAL));

  25:  

  26:                 // Establecemos la aceleración

  27:                 particulas[i].aceleracion.X = (float)Aleatorio.ObtenerAleatorio(MAX_ACELERACION_X) ;

  28:                 particulas[i].aceleracion.Y = -Aleatorio.ObtenerAleatorio(MAX_ACELERACION_Y) - MIN_ACELERACIONY;

  29:                 particulas[i].aceleracion.Normalize();

  30:  

  31:                 if (Aleatorio.ObtenerAleatorio(10) <= 5)

  32:                     particulas[i].aceleracion.X *= -1;

  33:  

  34:                 // Establecemos la edad de la "muerte"

  35:                 particulas[i].edadMuerte = EDAD_MUERTE_INICIAL;

  36:                 particulas[i].edadMuerte += Aleatorio.ObtenerAleatorio(MAX_DESVIACION_MUERTE);

  37:  

  38:                 // Establecemos la desviación en la escala (queremos unas partículas más pequeñas que otras)

  39:                 particulas[i].escala = (float)1.0f / (float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_ESCALA);

  40:  

  41:                 tiempoCreacionUltimaParticula = 0.0f;

  42:             }

  43:         }

  44:         else 

  45:         {

  46:             tiempoCreacionUltimaParticula += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

  47:         }

  48:     }

  49: }

El Draw ya no tiene ningún secreto… excepto el detalle de que estamos aplicando un alpha a la textura, que eso sí es interesante:

   1: public void Draw(GameTime gameTime, SpriteBatch spriteBatch)

   2: {

   3:     for (int i = 0; i < particulas.Length; i++)

   4:     {

   5:         if(particulas[i]!=null)

   6:             spriteBatch.Draw(textura,

   7:                 particulas[i].posicion, null,

   8:                 new Color(255, 255, 255, (byte)MathHelper.Clamp(particulas[i].alfa, 0, 255)), 0.0f, Vector2.Zero, particulas[i].escala,

   9:                 SpriteEffects.None, 0);

  10:     }

  11: }

El resultado del código es el del vídeo que os he mostrado antes, podéis además descargaros el código para probarlo. Algo que os recomiendo es jugar un poco con los valores de las constantes, ampliar el número de partículas hasta 10.000 (lo aguanta perfectamente jejeje) y aumentad el tiempo de vida de las partículas, y observaréis efectos curiosos.

Hasta la próxima!

[XNA] Animaciones básicas en 3D

Las animaciones en 3D implican muchas operaciones de matrices… pero por suerte XNA nos facilita el trabajo hasta tal punto que prácticamente el más negado en matemáticas puede trabajar con ellas… pero aunque sea fácil cualquier programador de XNA que se precie debe hacer un esfuerzo por comprenderlas si quiere saber lo que hace xD. En fin, el de las animaciones es un tema muy ámplio, e iré explicando algunas partes en pequeños episodios. En esta introducción veremos las operaciones más básicas.

Animar objetos en su forma más básica consistirá en multiplicar matrices, y algo muy importante es el orden en el que realicemos estas multiplicaciones. Si no realicamos las multiplicaciones en el orden correcto, nuestro modelo 3D sin duda se moverá de forma cuanto menos «rara» o inesperada. Este problema se soluciona recordando el orden que hay que aplicar, que siempre va a ser el mismo, partiendo de la matriz identidad:

  1. Escalar el modelo
  2. Rotar el modelo sobre su propio eje
  3. Rotar el modelo sobre un punto externo
  4. Trasladar el modelo

Y como he comentado, XNA nos ayuda enormemente en estas posibles operaciones básicas de matrices.

Esto se podría traducir en los siguientes ejemplos:

   1: // Matriz identidad

   2: Matrix.Identity;

   3:  

   4: // Crear escalas

   5: Matrix.CreateScale(1.0f);

   6: Matrix.CreateScale(Vector3.Zero);

   7:  

   8: // Rotar ejes

   9: Matrix.CreateRotationX(0.5f);

  10: Matrix.CreateRotationY(0.5f);

  11: Matrix.CreateRotationZ(0.5f);

  12:  

  13: // Trasladar

  14: Matrix.CreateTranslation(Vector3.Zero);

Ahora vamos a ver un par de ejemplos prácticos muuuy sencillos.

Ejemplo 1: Animando un ventilador

Para animar este ventilador, lo primero que he hecho es descargar el modelo 3D de Internet. En la web hay cientos de páginas que permiten descargar modelos 3D libres de derecho… con una simple búsqueda en Bing encontraremos 41 millones de resultados 🙂 Eso sí, a continuación he tenido que realizar algunas pequeñas modificaciones en 3DSMAX.

Si ampliáis la imágen veréis que he “fusionado” las aspas, con el eje central y los soportes que unen las aspas con los ejes, para animarlos todos juntos, y les he puesto un nombre: “aspas”, al que después accederé desde XNA para animar sólo esa parte del modelo 3D.

captura_3dmax

Ese acostumbra a ser un problema para más de un programador… que no acostumbramos a conocer 3D Max, por suerte siempre hay buenos amigos en los foros que nos ayudaran en estos detalles… en el caso de 3D MAX hay que convertir cada pala a malla editable (con el botón derecho sobre el aspa: Convert To->Editable Mesh), y posteriormente, en las propiedades de la malla, usaremos la función “Attach” para ir fusionando las palas. Finalmente ponemos un nombre que usaremos desde XNA al conjunto de aspas y el eje.

El código no es que tenga mucha dificultad… tampoco entraré mucho en detalle porque os dejo el link para descargarlo.

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

   2: {

   3:     modelo.CopyAbsoluteBoneTransformsTo(transformaciones);

   4:  

   5:     foreach(ModelMesh mesh in modelo.Meshes)

   6:     {

   7:         Vector3 rotacionTmp = Vector3.Zero;

   8:  

   9:         if (mesh.Name == "aspas")

  10:         {

  11:             rotacion.Y += 0.01f;

  12:  

  13:             if (rotacion.Y > 360)

  14:                 rotacion.Y = 0;

  15:  

  16:             rotacionTmp = rotacion;

  17:         }

  18:  

  19:         Matrix.CreateTranslation(Vector3.Zero);

  20:         Matrix.CreateRotationX(0.5f);

  21:         Matrix.CreateRotationY(0.5f);

  22:         Matrix.CreateRotationZ(0.5f);

  23:  

  24:         foreach (BasicEffect efecto in mesh.Effects) 

  25:         {

  26:             efecto.EnableDefaultLighting();

  27:  

  28:             efecto.World = transformaciones[mesh.ParentBone.Index] * 

  29:                 Matrix.CreateFromYawPitchRoll(

  30:                 rotacionTmp.Y,

  31:                 rotacionTmp.X,

  32:                 rotacionTmp.Z) *

  33:                 Matrix.CreateScale(escala) *

  34:                 Matrix.CreateTranslation(posicion);

  35:  

  36:             efecto.View = vista;

  37:             efecto.Projection = proyeccion;

  38:         }

  39:         mesh.Draw();

  40:     }

  41: }

 La parte interesante es la que compobamos si el nombre de la malla es “aspas”, en cuyo caso lo rotamos en su eje Y.

Ejemplo 2: Animando la tierra y la luna

Este vamos a complicarlo un poco, pero tampoco mucho eh 🙂  Lo que haremos va a ser poner en evidencia el orden de la multiplicación de las matrices.

En este caso he creado una simple esfera con el 3D Max, y le he asignado primero la textura de la Tierra, y he exportado a FBX, y después lo mismo con la Luna. Estas texturas también pueden encontrarse rápidamente en Internet.

A la clase que he llamado “ModeloExtendido”, que es la que se encarga de almacenar la información del modelo y pintarlo, le he añadido las siguientes matrices (la matriz World pasa a ser privada):

   1: public Matrix rotacion;

   2: public Matrix traslacion;

   3: public Matrix orbitacion;

   4: public Matrix escala;

 

Ahora, desde fuera de la clase podemos modificar cualquiera de estas matrices en el orden que queramos, sólo en el Draw tendremos que tener en cuenta el orden de multiplicación de las mismas:

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

   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:             efecto.World = transformaciones[mesh.ParentBone.Index] * ObtenerWorld();

  12:             efecto.View = vista;

  13:             efecto.Projection = proyeccion;

  14:         }

  15:  

  16:         mesh.Draw();

  17:     }

  18: }

  19:  

  20: private Matrix ObtenerWorld() 

  21: {

  22:     return world * escala * rotacion * orbitacion * traslacion;

  23: }

 

Con su salsa y demás, el resultado es el vídeo que habéis visto. El código está disponible para descargar.