Particles
Hoy, tras un descansito, vamos a seguir dibujando cosas en OpenGL, algo bastante genérico que vamos a necesitar para casi cualquier proyecto: un sistema de partículas.
En el de hoy tampoco nos vamos a calentar mucho la cabeza, es mejor ir por partes y ver en un principio lo fácil que va a resultar tener muchas partículas y moverlas sin dificultad. En la siguiente entra ya os prometo hacer un sistema usable.
Crear un Proyecto OpenGL
Como siempre vamos a empezar desde 0 y es creando un proyecto en OpenGL Es, abrimos el XCode vamos a File > New Proyect y seleccionamos un proyecto “OpenGL ES Application“, pulsamos el botón choose y le damos el nombre que nos interese:
Nuestra base de Sprite
Para empezar partimos de nuestro sample Sprites en OGL para tener las rutinas básicas para dibujar Sprites en 2D por pantalla. De todos modos al final del tuto se incluyen tanto el .m como el .h, aunque igualmente deberíais echarle un vistazo porque aquí no explico como agregar el framework necesario ni como adjuntar la imagen que usaremos.
Particles and Dots
La lógica al crear cualquier sistema de partículas será siempre el de tener una clase principal que hará de contenedor de las partículas, sobre está clase será donde crearemos las partículas, desde donde se cargarán las texturas y desde donde se dibujarán. Esta clase tendrá un array con todas las partículas. Nunca trabajéis con partículas dinámicas, se supone que cuando se usan partículas para simular cualquier efecto o simplemente para añadir vistosidad a una aplicación vamos a estar continuamente creando y destruyéndolas por lo que no es nada optimo tirar de peticiones de memoria para esos casos, plantearos siempre un número máximo de partículas y crear un array de tamaño fijo.
Cada particula tendrá siempre una variable booleana que indica si la partícula está en uso o no (‘live’ suele ser un nombre bastante descriptivo). Cuando el sistema necesita una partícula recorre el array buscando una partícula muerta y nos la da … si no encuentra ninguna no deberia pasar nada, simplemente no se crea la partícula y listo.
De momento empezaremos viendo nuestra clase Particles:
Particles: h and m
| C | | copy code | | ? |
| 01 | |
| 02 | #import "Dot.h" |
| 03 | #define MAXDOT 64 |
| 04 | |
| 05 | @interface Particles : NSObject { |
| 06 | |
| 07 | GLuint texture; |
| 08 | Dot *dot[MAXDOT]; |
| 09 | } |
| 10 | |
| 11 | -(void)inicializar; |
| 12 | -(void)setTouch:(GLfloat)x y:(GLfloat)y; |
| 13 | -(void)draw; |
| 14 |
Esto es nuestra cabecera del sistema de partículas, vemos que nos hemos puesto un máximo de 64 partículas, tenemos una declaración para la textura, y el array de la clase Dot (cada una de las partículas).
Y la implementación es de lo mas sencillo que vais a encontrar. Como veis esta clase solo inicializa los items del array, le da el touch a cada punto y los dibuja.
| C | | copy code | | ? |
| 01 | |
| 02 | #define DEGREES_TO_RADIANS(__ANGLE__) ((__ANGLE__) / 180.0 * M_PI) |
| 03 | #define RANDOM_SEED() srandom(time(NULL)) |
| 04 | #define RANDOM_INT(__MIN__, __MAX__) ((__MIN__) + random() % ((__MAX__+1) - (__MIN__))) |
| 05 | |
| 06 | @implementation Particles |
| 07 | |
| 08 | -(void)inicializar |
| 09 | { |
| 10 | // cargamos la textura |
| 11 | texture = [GL loadTexture:@"Particles-1.png"]; |
| 12 | |
| 13 | // inicializamos el random |
| 14 | RANDOM_SEED(); |
| 15 | |
| 16 | // y creamos todos los dots |
| 17 | for(int i=0;i<MAXDOT;i++) |
| 18 | { |
| 19 | dot[i] = [Dot alloc]; |
| 20 | [dot[i] createNew:true]; |
| 21 | } |
| 22 | |
| 23 | } |
| 24 | |
| 25 | -(void)setTouch:(GLfloat)x y:(GLfloat)y |
| 26 | { |
| 27 | // le damos el touch a todas particulas |
| 28 | for(int i=0;i<MAXDOT;i++) |
| 29 | [dot[i] setTouch:x y:y]; |
| 30 | |
| 31 | } |
| 32 | |
| 33 | -(void) draw |
| 34 | { |
| 35 | glLoadIdentity(); |
| 36 | |
| 37 | // activamos la textura |
| 38 | [GL setTexture:texture]; |
| 39 | |
| 40 | // y a dibujar todo |
| 41 | for(int i=0;i<MAXDOT;i++) |
| 42 | { |
| 43 | [dot[i] steep]; |
| 44 | [dot[i] render]; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | @end |
Lo del Touch era una sorpresa, jeje, vamos a dibujar los puntos en pantalla pero tiene algo mas de gracia si hacemos que interactuen al tocar la pantalla. Pues bien … nuestro sistema de partículas (como hemos dicho al principio) solo hará de contenedor, como podeis ver al hacer un touch no evaluamos nada, simplemente le damos ese touch a todas las partículas y que sean ellas las que decidan si hacen algo.
Siempre que podáis delegar sobre clases inferiores mucho mejor para todos, imaginar que en un futuro vamos a tener particulas que se ven afectada por el touch, y otras que no, mediante esta filosofía no tendríamos ningún problema, porque será la partícula quien decida lo que hace. Si en nuestra clase ‘Particles’ hubiéramos tomado alguna decisión ya estaríamos limitados a que todas las partículas se comportasen igual.
Particles.m (892 Bytes)
Particles.h (419 Bytes)
Particles-1.png (31.54 Kb)
Dots: h and m
| C | | copy code | | ? |
| 01 | |
| 02 | |
| 03 | @interface Dot : NSObject { |
| 04 | |
| 05 | GLfloat posx, posy; // Posicion |
| 06 | GLfloat dx, dy; // desplazamiento XY |
| 07 | GLfloat radio; // radio |
| 08 | GLfloat vy; // velocidad en Y |
| 09 | GLfloat frame; // frame de la textura |
| 10 | GLfloat colorR, colorG, colorB, colorA; |
| 11 | } |
| 12 | |
| 13 | -(void)setPosition:(GLfloat)x y:(GLfloat)y; |
| 14 | -(void)setRadio:(GLfloat)value; |
| 15 | -(void)setVy:(GLfloat)value; |
| 16 | -(void)setFrame:(GLfloat)value; |
| 17 | -(void)setColor:(int)r g:(int)g b:(int)b alpha:(int)alpha; |
| 18 | |
| 19 | -(void)setTouch:(GLfloat)x y:(GLfloat)y; |
| 20 | -(void)createNew:(bool)inScreen; |
| 21 | |
| 22 | -(void)steep; |
| 23 | -(void)render; |
| 24 | |
| 25 | @end |
| 26 |
Para cada particula podemos ver las propiedades que tiene y como casi todos sus métodos son para establecer esas propiedades (a pesar de que en este ejemplo no los vamos usar desde el contenedor).
Así que vamos a ir a la parte de la implementación que requiera algo más de explicación:
| C | | copy code | | ? |
| 01 | |
| 02 | -(void)createNew:(bool)inScreen |
| 03 | { |
| 04 | // primero un random para las coordenadas XY (siempre creamos la particula en pantalla) |
| 05 | int x = RANDOM_INT(0, 320) - 160; |
| 06 | int y = RANDOM_INT(0, 600) - 300; |
| 07 | |
| 08 | // y si no la queriamos dentro, la bajamos 600 pixeles pabajo |
| 09 | if(!inScreen) y += 600; |
| 10 | |
| 11 | // random para el radio |
| 12 | int r = RANDOM_INT(4, 16); |
| 13 | |
| 14 | // la velocidad va en funcion del radio tb, asi los puntos |
| 15 | // mas grandes se moverán mas lentamente |
| 16 | float velocidad = 2.0f - r * 0.1f; |
| 17 | int fr = RANDOM_INT(0, 4) ; |
| 18 | |
| 19 | [self setPosition:x y:y]; |
| 20 | [self setRadio:r * (fr+2) * 2]; |
| 21 | [self setVy:velocidad]; |
| 22 | [self setFrame:fr]; |
| 23 | [self setColor:RANDOM_INT(0, 0xFF) |
| 24 | g:RANDOM_INT(0, 0xFF) |
| 25 | b:RANDOM_INT(0, 0xFF) |
| 26 | alpha:RANDOM_INT(0, 0x80)]; |
| 27 | |
| 28 | dx = 0; |
| 29 | dy = 0; |
| 30 | } |
| 31 |
Esto podría ser nuestro constructor ya que inicializa la partícula en pantalla, nos interesa que sea un método aparte y no el constructor porque lo vamos a llamar cada vez que queramos resetear una partícula. Como veis pilla casi todo de forma aleatoria, lo único que deberíamos resaltar es que mediante una booleana que le damos como parámetro le indicamos si queremos que la partícula nazca por la pantalla o fuera de ella. Así por ejemplo todas las partículas se crearán por primera vez dentro de la pantalla, pero luego al resetearlas las pondremos fuera de pantalla.
Touching particles
| C | | copy code | | ? |
| 01 | |
| 02 | |
| 03 | Este metodo se llama para cada partícula cuando se toca la pantalla, lo que hace es medir la distancia entre el punto donde tocamos y la partícula (no tiene en cuenta el radio .. aunque seria muy facil de implementar) y si se encuentra a una distancia menor de 40 pixels desplaza la partícula (ponemos en dx y dy el factor de separación que hay en el ejex y en el ejey respectivamente, así luego haremos que la partícula se desplace en función de la dirección inversa a donde se ha tocado) |
| 04 | |
| 05 | -(void)setTouch:(GLfloat)x y:(GLfloat)y |
| 06 | { |
| 07 | GLfloat sepx = posx-x; |
| 08 | GLfloat sepy = posy-y; |
| 09 | |
| 10 | GLfloat sep = sepx * sepx + sepy * sepy; |
| 11 | |
| 12 | if(sep < 2000) // esto parece mucho pero son unos 45 pixels |
| 13 | { |
| 14 | dx += sepx ; |
| 15 | dy += sepy ; |
| 16 | } |
| 17 | } |
| 18 |
Un detalle importante a la hora de medir distancias entre 2 puntos: todos sabemos (o deberiamos) que la distancia entre 2 puntos es la raíz cuadrada del cuadrado de sus componentes sqrt(x*x+y*y), hasta ahi todo es fácil, pero se puede ganar algo de tiempo si obviamos el sqrt, y en lugar de esperar un resultado de 45 pixels si hiciéramos la raíz, trabajar con 45*45, es lo mismo decir sqrt(x^2+x^2)<45 que x^2+y^2<45*45
Drawing
| C | | copy code | | ? |
| 01 | |
| 02 | -(void)steep |
| 03 | { |
| 04 | posx += dx; |
| 05 | posy += dy; |
| 06 | |
| 07 | dx*=0.7f; |
| 08 | dy*=0.7f; |
| 09 | |
| 10 | posy -= vy; |
| 11 | |
| 12 | // cuando la particula se sale por arriba creamos otra, dandole un false para |
| 13 | // que se cree fuera de pantalla, por abajo |
| 14 | if(posy + radio < - 240) [self createNew:false]; |
| 15 | } |
| 16 | |
| 17 | En primer lugar mirar como la posición <strong>x </strong>e <strong>y </strong>se desplazan en funcion de <strong>dx </strong>y <strong>dy </strong>que se establecian con el touch, y luego esas variables dx y dy se van multiplicando con 0.7f para que paso a paso se vayan desacelerando. Luego la posicion y se va decrementando en funcion de vy, osea .. se mueven todas parriba. Y finalmente comprobamos cuando las partículas quedan fuera de pantalla para crearlas de nuevo. |
| 18 | |
| 19 | -(void)render |
| 20 | { |
| 21 | // ponemos el color de esta particula |
| 22 | [GL setColor:colorR g:colorG b:colorB alpha:colorA]; |
| 23 | |
| 24 | // ponemos el frame de la textura |
| 25 | [GL setFrame:frame]; |
| 26 | |
| 27 | // y dibujamos el sprite |
| 28 | [GL drawSprite:posx-radio y1:posy-radio x2:posx+radio y2:posy+radio]; |
| 29 | |
| 30 | } |
Dot.m (2.15 Kb)
Dot.h (791 Bytes)
Lo unico que debemos indicar en nuestro método ‘render’ es que hemos separado los métodos setTexture y setFrame, ya que todas las partículas usan la misma textura y no es nada lógico activar la textura con cada partícula …. una buena (muy muy muy buena) forma de optimizar esto es dibujar las partículas agrupadas por el frame que usan, así podríamos activar el frame 0 y dibujar solo las partículas que usen ese frame, y así sucesivamente.
GL.m (4.93 Kb)
GL.h (649 Bytes)
EAGLView
Empezamos por añadir estas variables en EAGLView.h
| C | | copy code | | ? |
| 1 | |
| 2 | Particles *particles; |
| 3 | bool touching; |
| 4 | GLfloat touchx, touchy; |
| 5 |
Y seguimos con el .m, lo primero que vamos a hacer es meter los métodos para manejar el touch de la pantalla, esto es una versión reducida de lo que ya hicimos en nuetro Touch-sample:
| C | | copy code | | ? |
| 01 | |
| 02 | #pragma mark - |
| 03 | #pragma mark === Touch handling === |
| 04 | #pragma mark |
| 05 | |
| 06 | // Handles the start of a touch |
| 07 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event |
| 08 | { |
| 09 | UITouch *touch = [[event allTouches] anyObject]; |
| 10 | CGPoint location = [touch locationInView:touch.view]; |
| 11 | |
| 12 | touching = true; |
| 13 | touchx = location.x - 160; |
| 14 | touchy = location.y - 240; |
| 15 | } |
| 16 | |
| 17 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event |
| 18 | { |
| 19 | UITouch *touch = [[event allTouches] anyObject]; |
| 20 | CGPoint location = [touch locationInView:touch.view]; |
| 21 | |
| 22 | touching = true; |
| 23 | touchx = location.x - 160; |
| 24 | touchy = location.y - 240; |
| 25 | } |
| 26 | |
| 27 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event |
| 28 | { |
| 29 | touching = false; |
| 30 | } |
| 31 | |
| 32 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event |
| 33 | { |
| 34 | touching = false; |
| 35 | } |
| 36 |
Y a continuación viene la inicialización que se encarga de crear nuestra clase de partículas:
| C | | copy code | | ? |
| 01 | |
| 02 | -(void)inicializar |
| 03 | { |
| 04 | particles = [Particles alloc]; |
| 05 | [particles inicializar]; |
| 06 | |
| 07 | touching = false; |
| 08 | |
| 09 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| 10 | |
| 11 | glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); |
| 12 | glEnable(GL_BLEND); |
| 13 | |
| 14 | } |
Y finalmente nuestro método Draw solo haría esto:
| C | | copy code | | ? |
| 01 | |
| 02 | - (void)drawView |
| 03 | { |
| 04 | [self clearBuffer]; |
| 05 | |
| 06 | if(touching) |
| 07 | [particles setTouch:touchx y:touchy]; |
| 08 | |
| 09 | [particles draw]; |
| 10 | [self swapBuffer]; |
| 11 | } |
EAGLView.m (5.81 Kb)
EAGLView.h (1.22 Kb)


Recent Comments