miércoles, febrero 22, 2006

Sistema de Partículas II : Explosiones

Esta serie que estoy escribiendo tiene fundamentalmente 2 objetivos. El primero es mostrar una posible manera de comenzar a hacer una sistema de partículas, comenzando por lo básico y luego con la práctica añadir efectos y mejoras al sistema. El otro objetivo es repuntar ideas de mi parte para luego formar el sistema independiente, expansible a medida que sea necesario. Dejando los discursos de lado, en este capítulo voy a mostrar una manera básica de implementar explosiones, y a partir de ella se podrá adaptar una explosión distinta para cada necesidaad. Más adelante veremos otros tipos de implementación para explosiones mas avanzadas.


  • Set up y movimiento de las partículas

Para modelar una explosión lo que hago primero que nada es inicializar cada partícula estableciendo las siguientes características:
  • Punto en común de orígen
  • Dirección
  • Energía

En las explosiones aéreas la dirección de cada partícula es distribuída en forma esférica centrada en el punto de orígen. Este tipo de explosiones podrían modelarse con la distribución de direcciones de cada partícula de manera uniforme. Si cortamos la esfera que forma esta distribución con un plano donde ocurre la explosión, una pared por ejemplo, la dirección e cada partíucula será distribuida en forma semi-esférica dado que cada una es disparada en la dirección normal al plano de impacto. Habría que adaptar esto ya que si la explosión surge en un ángulo las partículas deberían explotar correctamente para dar mas realismo, dado que necesariamente la distribución será esférica o semi-esférica.

Veamos el código de inicialización para cada partícula (set up). Como parámetros de entrada ingresamos las padrtículas, el vector coordenada de impacto y la cantidad total de partículas. Este último campo es para una implementación estática del sistema, podría obviarse con una celda dummy en el caso de implementación dinámica (recomendado). Minimizar la cantidad de operaciones con memoria (new y delete) hace mas eficiente el sistema, en el caso de la implementacion dinámica a medida que se necesitan partículas se siguen los siguientes pasos: Determinar si hay partículas muertas pero con alojo en memoria, en este caso resucitar las necesarias. Sin no hay muyertas,, entonces empezar a crear las que sean necesarias, pero claro, esto tampoco es barato. El alojamiento en memoria tiende a crecer dependiendo de la cantidad de eventos simultáneos, probablmenete sean partículas muertas las que estén alojadas. Mediante la implementacoión estática también logramos eficiencia, pero a un costo bastante mayor de entrada, dado que aunque haya solo 1 partícula viva en el sistema, siempre se tendrá el máximo de partículas alojado en memoria. Para las explosiones por lo general se conoce de entrada la cantidad de partículas máxima del sistema, y todas interactúan en la explosión (inicialmente). Sin embargo no siempre las explosiones toman el mismo tamaño y energía, entonces en necesario acotar conjuntos independientes del total para adaptar la explosión deseada. Es aquí donde entra en utilidad el campo active que indica si la partícula está activa (viva) o si no lo está (muerta). En nuestro caso usaremos arreglos dinámicos, y este campo solo servirá para descartar partículas muertas, no para acotar sistemas, por ahora almenos.

NOTA (sobre los vectores): en las imlementaciones aparecen directamente funciones de la clase vector. No vale la pena detenerese en esta clase, es muy intuitiva y las funciones también lo son, de hecho hablan por si solas.

// constantes del sistema
#define PARTICLES 1000
#define GRAVITY 9.8
#define ENERGY 7
...
// set up del sistema
particle* p= new particle*[PARTICLES];
...


void explosion::setup(particle* &p, vector coord, long tpar)=
{
float len, ie, dx, dy;
particle* paux=p;

for(long i=0; i < tpar; i++)
{
//activamos la partícula
paux->active= true;

//coordenadas del orígen
paux->pos.setx(coord.getx());

paux->pos.sety(coord.gety());

//dirección aleatoria al explotar
//negativa o positiva (a suerte)
paux->dir.setx(((rand()%4)-1.5));
paux->dir.sety(((rand()%4)-1.5));

//setup de energía inicial
ie= (rand() % ENERGY) + 1;

//registramos las coord para el cálculo
//del módulo, esto debe ir en la clase vector
dx= paux->pos.getx();
dy= paux->pos.gety();

//cálculo del módulo
len= sqrt(dx*dx + dy*dy);

//norma
if (len != 0.0)
len= 1.0 / len;

//normalizar para encontrar la dir.
//del vector, la distr. deja de ser uniforme
paux->pos.setx((paux->pos.getx())*len*ie);
paux->pos.sety((paux->pos.gety())*len*ie);

//color inicial
paux->color= 255;

paux++
}
}



Y el vector velocidad? La velocidad inicial no se tiene en cuenta en esta implementación, todas se mueven a velocidad constante. Recordar que esto es un primer intento, luego de tener la idea es fácil mejorarlo y agregar lo que venga. Con esta implementación mover las partóuclas es muy sencillo. Esta inicialización posiciona las partículas en el núcleo de la explosión. Ahora debemos expandir las partículas, recordando que no aceleran para este caso. Una explosión con partículas aceleradas y considerando adicinoalmente la aceleración de la gravedad, aunque se forme con pixeles, va de lujo en cualquier cosa :p, pero primero bastaría con formar una explosión común y corriente, para luego empezar a modificar a gusto teniendo el concepto.
Consideraremos una explosión en una habitación, para observar las colisiones. La habitación será un cadrado de 100x100 pixeles cuyo vértice superior izquierdo se ubica en el punto de coordenadas (350,250). También consideraremos la aceleración de la gravedad, y un efecto muy simple de "chispa pixelada" para indicar las partículas próximas a la muerte.

Veamos ahora la implementación para el movimiento.


void explosion::move_particles(particle*p, long tpar)
{
particle* paux= p;

for(long i=0; i < tpar; i++)
{
if (paux->active) {

//movemos la partícula de acuerdo al
//vector dir
paux->pos.setx(paux->pos.getx()+paux->dir.getx());
paux->pos.sety(paux->pos.gety()+paux->dir.gety());

//chequeamos colisiones con piso: muerte
if (paux->pos.gety() > 350)
paux->active= false;
else

//colisiones con techo
if (paux->pos.gety()) < 250)
{
paux->pos.sety(250);
paux->dir.setx(paux->dir.getx()/4);
paux->dir.sety(-(paux->dir.gety())/2);
}
else

//no actúan fuerzas en el eje x -> 0
//gravedad en el eje y -> 9.8
//en update sumo gravedad a la dirección en y
paux.update(0, GRAVITY);

//colisiones con paredes
if (paux->pos.getx() < 350)
{
paux->pos.setx(350);
paux->dir.setx(-(paux->dir.getx())/2);
paux->dir.sety(paux->dir.gety()/4);
}
else

if (paux->pos.getx() > 450) {
paux->pos.setx(450);
paux->pos.setx(-(paux->dir.getx())/2);
paux->dir.sety(paux->dir.gety()/4);
}

//si la partícula está próxima al piso
//hacemos un efecto de chispa cambiando
//el color del pixel

if (paux->pos.gety() >= 348)
paux->color= (rand() % 128) + 128;

}
paux++;
}
}



Esta implementación requiere de sincronismo. SDL_GetTicks() es la solución. Para los que no tienen ni idea de que es lo que hace esta función, devuelve (en milisegundos) el tiempo desde que se ha encendido la máquina. Podemos entonces implementar una función, que dado un punto de inicio retorna el tiempo transcurrido hasta el punto actual (en términos de tiempo):

inline Uint32 lapsedTime(Uint32 &t)
{
return t= SDL_GetTicks()-t;
}

Esto es todo por hoy. Recordar que éstas implementaciones pueden ser optimizadas. Pero al optimizar algunas cosas teóricas sencillas pueden resultar engorrosas, y lo importante es que quede el concepto, la implementación en este caso es lo de menos, cada cual implementa a su manera :).





miércoles, febrero 15, 2006

Sistema de Partículas I

Hace un tiempo atrás tenía implementado un sistema de partículas simple, lo había hecho antes de empezar con un juego de aviones, para modelar explosiones aéreas. A mitad de proyecto volví a cambiar la implementación del sistema agregando explosiones terrestres, pérdidas de combustible desde el mismo avión y otra clase para manejo de vectores. Eso en un principio, hoy mi sistema tiene ya varios efectos diferentes. Cada vez que se agrega un efecto es muy probable que haya que modificar varias clases, sobre todo la misma clase 'partícula'. El objetivo de esta serie 'Sistema de partículas' es comentar como he empezado mi sistema de partículas, desde los efectos iniciales. La idea es repasar mas que nada fragmentos de código (en C++) y algunos puntos teóricos. El código del sistema original no es corto, y está adaptado a mis necesidades en cuanto a lo que haga, asi que los fragmentos de código que use en esta serie pueden verse reducidos para no confundirse.
Humo, explosiones, nubes, chispas, lluvia, sangre son algunos efectos que se benefician de un sistema de partículas.

  • Performance y Requerimientos
Un sistema de partículas avanzado puede requerir de código extensamente largo, por tanto las estructuras deben estar lo mejor diseñadas posible. Un buen comienzo es hacer la menor cantidad de operaciones que impliquen memoria (new y delete) como sea posible. A veces, luego de que la partícula este muerta, es mejor no liberarla de memoria y simplemente darla por muerta desde un campo booleano de la partícula. En este caso, el sistema de partículas liberaría la memoria en el caso de que todas las partículas del sistema esten muertas.
Al crear un sistema de partículas es importante considerar todos los posibles parametros que sabremos que serán afectados en el sistema en algún momento del juego, y crear esa flexibilidad dentro del sistema.

  • Jerarquía del sistema dentro del motor
El sistema de partículas debe ser del mismo grado del motor, es decir, poder actuar sobre los mismos objetos. Por ejemplo disparar un misil y a su vez agregar el efecto de humo en la tobera del mismo, ó que la sangre corra por el cuerpo del personaje mientras este también corre. En estos casos, cuando el objeto se mueve la posición del sistema de partículas que está unido al objeto en cuestión debe ser actualizado correctamente. Lo mismo pasa al considerar la aceleración de la gravedad sobre cada objeto.

  • Multi-Rendering
Algunos sistemas de partículas deberán renderizar sus partículas de distinta manera. Un ejemplo, un chorro de sangre puede estar volando por el aire pero al tocar una pared deja de volar para mancharla, y dejar salpicones. Por tanto podemos diferenciar 2 sistemas a renderizar, "chorro de sangre" y "sangre chorreada" para pisos y paredes. También dependiendo de la situación las partículas pueden ser sólo pixeles, en el caso de las 2D un sprite, ó un modelo 3D. En este caso, una de las cosas a tener en cuenta es que el sistema de partículas incrementa enormemente el número de polígonos visibles por frame.

Existen muchos requerimientos, pero para empezar no es necesario saberselos todos. En esta serie siempre se hará referencia a un sistema de partículas en 2 dimensiones, dado que llevarla a 3 es simplemente agregar una componente, para el caso de los algoritmos (renderizar cada partícula es muy distinto, claro está). Como dije antes el código está escrito en C++ y usaré el motor SDL para la tarea simple de visualizar las partículas. La elección del motor depende únicamente del efecto visual que se desee para las partículas, en esta serie el cometido es el sistema de partículas en sí y no la parte de visualización de las mismas, por este motivo el motor a usar da lo mismo. También voy a considerar las partículas como pixeles, para facilitar el código.

Las Partículas

Una partícula es, en general, un solo punto en el espacio. A este punto se le asigna atributos o características. Las características más comunes de una partícula son:
  • posición
  • energía
  • velocidad
  • dirección
  • tiempo de vida
A medida que avanza la animación de la partícula las características son requeridas para ello, a la vez que se modifican y se actualizan. Tenemos entonces la clase partícula:


class particle {
public:
bool active;
int color;
vector pos;
vector dir;
vector oldpos;
vector vel;
float energy;

reload();
update(float& friction, float& gravity);
rendering();
}


active:
La partícula puede estar cargada en memoria, aún cuando muerta. Este campo indica cada caso.

color:
Define el color como un RGB (para los pixeles). Puede usarse para almacenar el índice de un arreglo de bitmaps creado previamente para colorear cada partícula.

energy:
Como la misma etiqueta, registra la energía actual de la partícula. Evita el tener 2 campos como 'lifetime' y 'age', a medida que la partícula actúa en el sistema la energía se va agotando o incrementando; cuando energy<=o la partícula ha muerto. Pero si esto es así, entonces para qué está la etiqueta 'active' ??? Bueno, por algo la puse ;) . Este campo será reemplzado por una constante en las explosiones simples (sin considerar aceleración).

oldpos: Es el vector posición anterior de la partícula, antes de haber sido modificado. No será usado en las explosiones (almenos en las mas comunes). Este campo es muy usado por ejemplo para modelar chispas y en algunos casos, llamaradas de fuego. El resto de los campos, hablan por sí solos.

Ahora veamos las funciones:

reload( ): Optimiza el rendimiento del sistema, evita las operaciones new y delete. Resucita una partícula muerta sin pedir memoria.

update( ): Actualiza la física de la partícula, sus valores pueden ser modificados en base a los parámetros que recibe (gravedad y fricción).

rendering( ): Es el encargado de renderizar el comportamiento de la partícula. La clase vector debe incluir, aparte de lo básico como vector de 2 componentes, una función para normalizar el vector, que será usada en los algoritmos de explosión para normalizar el vector dirección, como se verá mas adelante.

Esto es todo por el primer número, suficiente para empezar con las explosiones en el siguiente capítulo. Si al avanzar veo que quedó algo colgado, actualizaré el capítulo correspondiente para agregarlo, asi que hasta no terminar la serie los capítulos pueden ser modificados.

viernes, febrero 03, 2006

Cerrado por Examenes

Hace varios días no he tenido tiempo suficiente para nada, todo sería mas productivo durmiendo menos. Prácticamente se me han corrido todos los tiempos, me levanto a las 12 a.m. mínimo, almuerzo alrededor de las 3 p.m. y no tengo cena prácticamente, ya que a las 7 p.m. vuelvo a comer algo. Me estoy llendo a dormir en promedio a las 4 de la mañana. En términos "horarios" un desastre...pero en la obscuridad de mi cuarto no tengo noción de la hora, que sean las 2 a.m. o las 2 p.m. da lo mismo, me voy a dormir cuando tenga sueño y me despierto cuando abro los ojos jeje...A partir del lunes este blog no va a ser el mismo, no me gusta para nada el look actual, demasiado 'standard' y se nota que las entradas son solo 'por poner algo'. Pero vamos, esto no es de nerd ni de vago, simplemente no hay tiempo libre.

Etiquetas: