Sprites en OpenGL
Vamos a empezar con un sencillo proyecto que puede ser la base de muchas pruebas, vamos a trabajar con OpenGL para dibujar sprites en pantalla. A partir de aquí se puede hacer cualquier cosa que se nos ocurra así que vamos a ver todo lo que intentaremos hacer de la forma mas clara … bueno … de la forma mas clara siempre que tengamos algunos conceptos de OpenGL claros.
Crear un proyecto en OpenGL
Este será el paso mas fácil de todos, abrimos el Xcode y seleccionamos File > New Proyect indicamos que queremos una “OpenGL ES Application” pulsamos el botón ‘choose’ e indicamos un nombre para el proyecto, en nuestro caso SpriteGL.
Esto ya nos creará un proyecto por defecto con un Quad dando vueltas (es curioso pero en OpenGL ES no existe los Quads … asi que espero que sepais lo que es un Triangle_Strip o un Triangle_Fan). Bueno … disfrutar de vuestro proyecto mientras podais porque el siguiente paso será destrozarlo.
Nuestra Clase de ayuda GL
El siguiente paso será crear una nueva clase donde vamos a meter de forma statica todos nuestros metodos de dibujado, en realidad sólo vamos a necesitar 3 metodos, pero será util tenerlo en una clase lista para usar en distintos proyectos, asi que vamos a por ella:
Vamos a File > New File y seleccionamos que queremos un NSObject subclass (este es tipo base del que todo hereda en Objective-C) Pulsamos Next, y le damos nombre a la clase.
Fijaros que en Location estamos creando la clase dentro del directorio /Classes/ no supone ningún problema crearlo en otro sitio pero hay que ser un poco ordenados. De todos modos si os equivocáis y lo creais en otra ubicación siempre podéis arrastrarlo y cambiarlo de directorio.
Total … seguimos y el Xcode nos creará los archivos GL.h y GL.m. Ahora es cuando empezaremos por fin a meter codigo, de momento un poco de Copy&Paste que siempre viene bien:
Vamos a GL.h y añadimos estas lineas
| C | | copy code | | ? |
| 01 | |
| 02 | #import <Foundation/Foundation.h> |
| 03 | #import <QuartzCore/QuartzCore.h> |
| 04 | #import <OpenGLES/EAGL.h> |
| 05 | #import <OpenGLES/ES1/gl.h> |
| 06 | #import <OpenGLES/ES1/glext.h> |
| 07 | #import <OpenGLES/EAGLDrawable.h> |
| 08 | |
| 09 | @interface GL : NSObject { |
| 10 | |
| 11 | } |
| 12 | |
| 13 | + (GLuint)loadTexture:(StringPtr)cadena; |
| 14 | + (void)setSprite:(GLuint)texture frame:(int)frame; |
| 15 | + (void)drawSprite:(int)x1 y1:(int)y1 x2:(int)x2 y2:(int)y2; |
| 16 | |
| 17 | @end |
| 18 | |
| 19 |
Como veis simplemente son un montonazo de imports .. que ahora mismo ni recuerdo para que son necesarios todos los de OpenGL pero que tampoco hacen mal a nadie por estar ahí, así que los dejamos. El QuartzCore es necesario para la carga de imágenes, luego explicaré como agregar el Framework necesario para que funcione.
Vamos a GL.m y añadimos
En primer lugar después de la implementacion metemos esta variable global que vamos a usar luego, será para guardar las coordenadas del sprite que queremos dibujar:
| C | | copy code | | ? |
| 1 | |
| 2 | GLfloat spriteTexcoords[8]; |
y seguimos por implementar los 3 métodos
| C | | copy code | | ? |
| 01 | |
| 02 | |
| 03 | + (GLuint)loadTexture:(StringPtr)cadena |
| 04 | { |
| 05 | CGImageRef spriteImage; |
| 06 | GLuint spriteTexture = 0; |
| 07 | |
| 08 | // Creates a Core Graphics image from an image file |
| 09 | spriteImage = [UIImage imageNamed:cadena].CGImage; |
| 10 | |
| 11 | if(spriteImage) |
| 12 | { |
| 13 | // Get the width and height of the image |
| 14 | size_t width = CGImageGetWidth(spriteImage); |
| 15 | size_t height = CGImageGetHeight(spriteImage); |
| 16 | |
| 17 | // Texture dimensions must be a power of 2. If you write an application that allows users to supply an image, |
| 18 | GLubyte *spriteData = (GLubyte *) malloc(width * height * 4); |
| 19 | CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast); |
| 20 | CGContextDrawImage(spriteContext, CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height), spriteImage); |
| 21 | CGContextRelease(spriteContext); |
| 22 | |
| 23 | glGenTextures(1, &spriteTexture); |
| 24 | glBindTexture(GL_TEXTURE_2D, spriteTexture); |
| 25 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData); |
| 26 | free(spriteData); |
| 27 | } |
| 28 | |
| 29 | return spriteTexture; |
| 30 | } |
| 31 | |
| 32 | + (void)setSprite:(GLuint)texture frame:(int)frame |
| 33 | { |
| 34 | glEnable(GL_TEXTURE_2D); |
| 35 | glBindTexture(GL_TEXTURE_2D, texture); |
| 36 | |
| 37 | GLfloat x1 = (frame % 2) / 2.0f; |
| 38 | GLfloat y1 = (frame / 2) / 2.0f; |
| 39 | GLfloat x2 = x1 + 0.5f; |
| 40 | GLfloat y2 = y1 + 0.5f; |
| 41 | |
| 42 | // preparamos el array con las coordenadas de la textyra |
| 43 | spriteTexcoords[0] = x1; spriteTexcoords[1] = y1; |
| 44 | spriteTexcoords[2] = x2; spriteTexcoords[3] = y1; |
| 45 | spriteTexcoords[4] = x2; spriteTexcoords[5] = y2; |
| 46 | spriteTexcoords[6] = x1; spriteTexcoords[7] = y2; |
| 47 | } |
| 48 | |
| 49 | // dibuja un sprite cuadrado |
| 50 | + (void)drawSprite:(int)x1 y1:(int)y1 x2:(int)x2 y2:(int)y2 |
| 51 | { |
| 52 | GLfloat spriteVertices[] = { |
| 53 | x1, y1, x2, y1, |
| 54 | x2, y2, x1, y2 |
| 55 | }; |
| 56 | |
| 57 | glVertexPointer(2, GL_FLOAT, 0, spriteVertices); |
| 58 | glEnableClientState(GL_VERTEX_ARRAY); |
| 59 | glTexCoordPointer(2, GL_FLOAT, 0, spriteTexcoords); |
| 60 | glEnableClientState(GL_TEXTURE_COORD_ARRAY); |
| 61 | glDrawArrays(GL_TRIANGLE_FAN, 0, 4); |
| 62 | } |
GL.m (3.5 Kb)
GL.h (541 Bytes)
Una breve explicación de nuestros metodos
loadTexture
Pues con ese nombre tan descriptivo esta función sirve para cargar una textura, ala, ahí queda eso.
| C | | copy code | | ? |
| 1 | |
| 2 | // Carga una imagen y nos devuelve el indice |
| 3 | // de la image dentro de OpenGL |
| 4 | GLUint textureid = [GL loadTexture:@"4Sprites.png"]; |
setSprite
Este método se encarga de activar la textura que hemos cargado previamente con loadTexture, para este ejemplo nuestra textura está dividida en 4 sprites, así que añadimos un segundo parametro ‘frame’ para indicarle que sprite queremos dibujar:
Este método dejará cargado en ‘spriteTexcoords’ las coordenadas necesarias para que todos los sprites que se envíen a pantalla lo hagan con ese frame que hemos marcado.
| C | | copy code | | ? |
| 1 | |
| 2 | // Activamos la textura que hemos cargado en el punto anterior |
| 3 | // y le decimos que vamos a dibujar el frame 0 |
| 4 | [GL setSprite:textureid frame:0]; |
drawSprite
Y finalmente mediante este método dibujamos un sprite, solo tiene 4 parámetros que corresponde que la esquina superior izquierda y la esquina inferior izquierda … la típica rutina para dibujar sprites cuadrados.
Sólo debemos tener en cuenta una cosa … y es que yo, por llevar la contraria a todos, siempre ajusto la vista para que el centro de pantalla corresponda con las coordenadas <0,0> (en lugar de poner el inicio en la esquina superior como hacen los chicos buenos), esto quiere decir para dibujar un sprite en el centro de pantalla de 200 pixels tendríamos que poner:
| C | | copy code | | ? |
| 1 | |
| 2 | [GL drawSprite:-100 y1:-100 x2:100 y2:100]; |
Añadir CoreGraphics.Framework
Para que la carga de texturas funcione es necesario añadir este framework al proyecto, para ello vamos a la sección Frameworks de nuestro proyecto, pulsamos con el botón secundario del ratón y seleccionamos Add > Existing Frameworks.
Esto nos abre un navegador para seleccionar el Framework pero es un caos porque por defecto se abre en el directorio que le da la gana y encontrar el framework no será facil. Podeis usar el buscador y poner el nombre del framework que quereis añadir pero en mi caso concreto me salen 8 distintos .. y hay que ir marcandolos uno a uno para ver que ruta tienen y si es el que necesitamos.
Total … para no perdernos en algo tan fácil como podía haber sido un par de clicks os recomiendo que lo busquéis por su ruta completa: Developer/ platforms/ iPhoneOs.platform/ Developer/ SDKs/ iPhoneOS2.2sdk/ System/ Library/ FrameWorks/ CoreGraphics.framework repufffffff
Vamos a dibujar
Ya tenemos nuestra clase GL que es capaz de cargar texturas y dibujar sprites, así que vamos a modificar ya el cuadradito que daba vueltas al principio para dejar nuestro código.
Empezaremos por tener algo que dibujar
Parece obvio pero no intentéis cargar ninguna imagen que no tengáis dentro de los recursos de vuestro proyecto. Empezamos por descargar esta imagen 4Sprites.png y dejarla dentro de la sección resources del proyecto:
Debereis marcar la casilla copy items si quereis que la imagen se guarde dentro del directorio del proyecto, esto sería lo normal:
Abrimos EAGLView.h
En este archivo podemos ver que tenemos una clase llamada EAGLView esta clase es la que tiene los buffers de dibujado, el Timer que controla el dibujado de pantalla, y los métodos necesarios para dibujar. Nosotros de momento solo vamos a añadir una variable, para guardar nuestra textura, y unos métodos nuevos para organizar mejor nuestro código:
Dentro de la interfaz añadimos esta linea:
| C | | copy code | | ? |
| 1 | |
| 2 | GLuint textureid; |
Y debajo de los 3 metodos que tiene definidos (startAnimation, stopAnimation y drawView) vamos a añadir estos 4 metodos nuestros:
| C | | copy code | | ? |
| 1 | |
| 2 | - (void)inicializar; |
| 3 | - (void)setupView; |
| 4 | - (void)clearBuffer; |
| 5 | - (void)swapBuffer; |
Abrimos EAGLView.h
Como hemos dicho esta clase es la que inicializa los buffers de dibujado y la que crea un Timer para dibujar la pantalla. Asi que Lo primero que haremos será buscar el metodo layoutSubviews y lo modificamos para que quede asi, unicamente es añadir las lineas 5 y 6:
| C | | copy code | | ? |
| 1 | |
| 2 | - (void)layoutSubviews { |
| 3 | [EAGLContext setCurrentContext:context]; |
| 4 | [self destroyFramebuffer]; |
| 5 | [self createFramebuffer]; |
| 6 | [self inicializar]; // aqui cargaremos la textura |
| 7 | [self setupView]; // aqui configuramos la camara en 2D |
| 8 | [self drawView]; |
| 9 | } |
Este método se llama cuando se inicializa la aplicación y se crea la Vista, lo que hacia antes era crear el buffer de dibujado y llamaba a drawView por primera vez, el propio método drawView era quien ponía la cámara del OpenGL, ahora hemos separado ese método para no repetir el proceso de la cámara.
Implementando metodos
Aqui van los 4 metodos que vamos a implementar:
| C | | copy code | | ? |
| 01 | |
| 02 | -(void)inicializar |
| 03 | { |
| 04 | // Cargamos la unica textura que vamos a usar |
| 05 | textureid = [GL loadTexture:@"4Sprites.png"]; |
| 06 | |
| 07 | // |
| 08 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| 09 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| 10 | |
| 11 | // |
| 12 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); |
| 13 | glEnable(GL_BLEND); |
| 14 | } |
| 15 | |
| 16 | - (void)setupView |
| 17 | { |
| 18 | // Sets up matrices and transforms for OpenGL ES |
| 19 | glViewport(0, 0, backingWidth, backingHeight); |
| 20 | glMatrixMode(GL_PROJECTION); |
| 21 | glLoadIdentity(); |
| 22 | glOrthof(-160.0f, 160.0f, |
| 23 | 240.0f,-240.0f, |
| 24 | -1.0f, 1.0f); // center <0,0,0> on screen |
| 25 | glMatrixMode(GL_MODELVIEW); |
| 26 | |
| 27 | // Clears the view with black |
| 28 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); |
| 29 | } |
| 30 | |
| 31 | -(void)clearBuffer |
| 32 | { |
| 33 | [EAGLContext setCurrentContext:context]; |
| 34 | glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer); |
| 35 | glClear(GL_COLOR_BUFFER_BIT); |
| 36 | } |
| 37 | |
| 38 | -(void)swapBuffer |
| 39 | { |
| 40 | glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer); |
| 41 | [context presentRenderbuffer:GL_RENDERBUFFER_OES]; |
| 42 | } |
Con todo esto ya podemos ir al metodo ‘drawView’ y, literalmente, borrar todo lo que tenga porque no sirve de nada, ya no. Ahora nuestro metodo drawView se va a comportar de la siguiente manera:
| C | | copy code | | ? |
| 01 | |
| 02 | - (void)drawView |
| 03 | { |
| 04 | [self clearBuffer]; // Limpiamos el buffer de dibujado |
| 05 | glLoadIdentity(); // reseteamos la matrix |
| 06 | |
| 07 | // activamos la textura y dibujamos un srpite en el centro |
| 08 | [GL setSprite:textureid frame:0]; |
| 09 | [GL drawSprite:-100 y1:-100 x2:100 y2:100]; |
| 10 | |
| 11 | [self swapBuffer]; // y volcamos a pantalla |
| 12 | } |
peeeeeeeero, si os atreveis con un draw algo mas completo, podes probar esto:
| C | | copy code | | ? |
| 01 | |
| 02 | - (void)drawView |
| 03 | { |
| 04 | static int angle = 0; |
| 05 | angle+=3; |
| 06 | [self clearBuffer]; |
| 07 | |
| 08 | glLoadIdentity(); |
| 09 | |
| 10 | glPushMatrix(); |
| 11 | [GL setSprite:textureid frame:0]; |
| 12 | glRotatef(angle,0,0,1); |
| 13 | glTranslatef(-80,-80,0); |
| 14 | glRotatef(-angle,0,0,1); |
| 15 | [GL drawSprite:-64 y1:-64 x2:64 y2:64]; |
| 16 | glPopMatrix(); |
| 17 | |
| 18 | glPushMatrix(); |
| 19 | [GL setSprite:textureid frame:1]; |
| 20 | glRotatef(angle,0,0,1); |
| 21 | glTranslatef( 80,-80,0); |
| 22 | glRotatef(-angle,0,0,1); |
| 23 | [GL drawSprite:-64 y1:-64 x2:64 y2:64]; |
| 24 | glPopMatrix(); |
| 25 | |
| 26 | |
| 27 | glPushMatrix(); |
| 28 | [GL setSprite:textureid frame:2]; |
| 29 | glRotatef(angle,0,0,1); |
| 30 | glTranslatef(-80, 80,0); |
| 31 | glRotatef(-angle,0,0,1); |
| 32 | [GL drawSprite:-64 y1:-64 x2:64 y2:64]; |
| 33 | glPopMatrix(); |
| 34 | |
| 35 | glPushMatrix(); |
| 36 | [GL setSprite:textureid frame:3]; |
| 37 | glRotatef(angle,0,0,1); |
| 38 | glTranslatef( 80, 80,0); |
| 39 | glRotatef(-angle,0,0,1); |
| 40 | [GL drawSprite:-64 y1:-64 x2:64 y2:64]; |
| 41 | glPopMatrix(); |
| 42 | |
| 43 | [self swapBuffer]; |
| 44 | } |
EAGLView.m (6.04 Kb)
EAGLView.h (1.24 Kb)
Si todo va bien, os debería salir algo como esto .. los bichitos dando vueltas la mar de entretenidos ellos
Si tenéis algún problema con los archivos aquí os dejo el proyecto entero
GL-Sprite.zip (180.51 Kb)
Y aquí os dejo la actualización navideña con el proyecto preparado para funcionar con la version 3.1.2, en esta versión nos han modificado el template para las aplicaciones OpenGl, así que echar un vistazo al proyecto, el código no cambia nada … solo la organización de clases.
SpriteGL.zip for 3.1.2 (730.16 Kb)
10 Comments
atdcr on March 14th, 2009
Justo estaba buscando este tipo de tutorial pero tengo un problema y es que las texturas no las carga, solo me sale en pantalla 4 cuadros blancos sin las imagenes, no podrias poner un zip con tu solucion para ver que hice mal. Gracias.
neofar on March 16th, 2009
Bueno … algo es algo, si no te aparecen las imágenes solo puede ser por 2 razones …. o no se ha cargado la textura, o no se ha activado al enviar un sprite.
Supongo que la primera causa la tendrás mas que comprobada … si la textura se ha cargado correctamente textureId debería valer 1 (ya que es el indice de la textura dentro del entorno GL) si vale cualquier otra cosa es que no ha podido cargar la textura.
Y por otro lado asegurate que no te has comido las lineas que activan la textura:
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture);
La primera linea activa las texturas, le dice al OpenGL que vamos a usar una textura (en la siguiente linea le decimos que textura vamos a usar)
… de todos modos … venga vale, te subiré el ejemplo
Nitz on March 25th, 2009
Buenas, muchas gracias por el tuto, era justo lo que buscaba.
Pero me ocurre lo mismo que a @atdcr, me aparece un cuadro en blanco (usando la primera implementación de drawView).
El código es clavado al que tienes (copy paste vamos). Me salen dos warnings, diciéndome que no estoy pasando un tipo de variable correcto al hacer:
spriteImage = [UIImage imageNamed:cadena].CGImage;
y
textureid = [GL loadTexture:@"4Sprites.png"];
¿Alguna idea? ¿Puedes subir el zip please?
Saludos
Nitz on March 25th, 2009
Vale, creo que ya he encontrado el error.
Lo que pasa es que no habilitas el GL_BLEND en tu método “setSprite”.
Aquí traigo un apaño:
+ (void)setSprite:(GLuint)texture frame:(int)frame
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, texture);
GLfloat x1 = (frame % 2) / 2.0f;
GLfloat y1 = (frame / 2) / 2.0f;
GLfloat x2 = x1 + 0.5f;
GLfloat y2 = y1 + 0.5f;
// preparamos el array con las coordenadas de la textyra
spriteTexcoords[0] = x1; spriteTexcoords[1] = y1;
spriteTexcoords[2] = x2; spriteTexcoords[3] = y1;
spriteTexcoords[4] = x2; spriteTexcoords[5] = y2;
spriteTexcoords[6] = x1; spriteTexcoords[7] = y2;
}
Saludos y gracias por el tuto
neofar on March 27th, 2009
Cierto, pero no es necesario meterlo en el setSprite, estos 3 parámetros son generales y basta con activarlos una vez, sería mejor añadirlos en nuestro metodo ‘inicializar’
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
Luego ya si interesa el blend lo puedes activar o no, pero la blendFuncion es raro que se tenga que cambiar sobre la marcha
Nitz on March 27th, 2009
Mmm… ¿Seguro? Estoy probando en hacer lo que dices: dejar el setSprite intacto y setear el blend en el init.
Lamentablemente, me aparece la imagen en blanco.
¿Lo habías probado?
neofar on March 28th, 2009
Pues no, lo del blend no lo habia probado pero son funciones de configuracion del motor, no es necesario cambiarlas ni establecerlas al dibujar cada sprite si no es por implementar algun efecto de transparencia.
De todos modos he modificado el tutorial y os he subido los fuentes, mira las lineas nuevas que han aparecido en la función ‘inicializar’ puedes hacer experimentos ahí comentando lineas.
las glTexParameter realmente no afectarán que el sprite se vea o no, son solo configuraciones de los filtros que se aplican al ampliar o reducir las texturas.
Néstor on December 29th, 2009
pues a mi me saca 5 errores, cada uno en EAGLView.h:
error: expected specifier-qualifier-list before ‘GLuint’
error: expected specifier-qualifier-list before ‘GLuint’
error: expected specifier-qualifier-list before ‘GLuint’
error: expected specifier-qualifier-list before ‘GLint’
error: expected specifier-qualifier-list before ‘GLint’
neofar on December 31st, 2009
Este proyecto tiene más de un año, si lo intentas hacer ahora tienes que tener en cuenta que el template que trae el Xcode ha cambiado bastante (para dar soporte a la version de OpenGlEs que incorpora el nuevo 3GS).
De todos modos el error que te sale tampoco es cosa de otro mundo .. probablemente falta algún import con las librerías OGL.
He agregado al post un .zip con el ejemplo funcionando en la ultima versión del Xcode, lo unico que tienes que tener en cuenta ahora es que toda la logica se hace en el ES1Renderer en lugar del EAGLView
Si tienes dudas pregunta, un saludo

n3wrotyk on January 13th, 2009
Se agradece este tipo de tutoriales, la verdad es que la info sobre iphone es abrumadora y tener un programilla para poder encarar el desarrollo siempre viene bien :-p.