- Resumen
- Introducción
- Cargando texturas en memoria
- Pasando las texturas a opengl
- Parámetros de las texturas
- Renderizando con texturas
- Colores, luces y texturas
- Algoritmos
- Conclusiones
- Bibliografía
En este trabajo veremos un aspecto básico si queremos que una escena contenga un mínimo de realismo: La texturización. Por texturización entendemos en el proceso de asignar una imagen (bitmap) a un polígono, de manera que en lugar de ver este de un color plano, o un gradiente de colores, veremos la imagen proyectada en él.
Hasta ahora las primitivas se han dibujado en OpenGL con un solo color o
interpolando varios colores entre los vértices de una primitiva. OpenGL dispone de funciones específicas para el mapeado de texturas (pegar imágenes en la superficie de las primitivas dibujadas), añadiendo realismo a la escena. En este trabajo se explica el proceso de mapeado de texturas a través de un sencillo ejemplo, para posteriormente emplear las texturas en la aplicación.
Las texturas en OpenGL pueden ser 1D, 2D o 3D. Las 1D tienen anchura pero no altura; las 2D son imágenes que tienen una anchura y una altura de más de 1 píxel, y son generalmente cargadas a partir de un archivo .bmp (aunque puede ser en principio cualquier formato). En este trabajo no se hablará de las texturas 3D (volumen). Un aspecto muy importante a tener en cuenta es que en OpenGL las dimensiones de las imágenes deben ser potencia de 2.
Este trabajo se limitara a estudiar la texturización 2D, y proponer implementaciones para un mejor cargado y rendereado de texturas.
DESARROLLO
1.1. Cargando texturas en memoria
El proceso de cargar la textura en memoria, no es propio de OpenGL, lo tendremos que hacer nosotros mismos. No obstante, hay que tener en cuenta unas ciertas limitaciones que la librería nos impone. Primeramente, las dimensiones de todas las texturas que carguemos tienen que ser potencias de 2, como por ejemplo 64×64, 128×64, etc.
También hemos de tener en cuenta que si estamos dibujando en RGB, sin color indexado, o bien cargamos texturas en formato RGB o las convertimos a RGB. Es decir, si cargamos una imagen GIF, que tiene color indexado, correrá de nuestra cuenta pasarla a RGB. Sea cuál sea el método que escojamos, al final tendremos un puntero a un segmento de memoria que contiene la imagen:
unsigned char *textura;
Es importante también guardar las propiedades de la textura, en concreto sus dimensiones de ancho y alto, así como su profundidad en bits. Si estamos trabajando en RGB, la profundidad será 24bits.
1.2. Pasando las texturas a OpenGL
Ahora ya tenemos la textura en la memoria RAM. No obstante, OpenGL no puede trabajar directamente con esta memoria, ha de usar su propia memoria para guardar las texturas. El se encargará de guardarlas en su espacio de memoria RAM o, directamente, pasarlas a la tarjeta aceleradora. Una vez le pasemos la textura a OpenGL, éste nos devolverá un identificador que tendremos de guardar. Cada textura tendrá un identificador propio, que tendremos que usar después cuando dibujemos. Veamos el proceso de obtención de este identificador. Creemos una variable para almacenarlo:
GLuint idTextura;
A continuación llamaremos a la función glGenTextures(…), a la cual le pasamos el numero de texturas que queremos generar, y un array de identificadores donde los queremos almacenar. En este caso, solo queremos una textura, y por lo tanto no hace falta pasarle un array, sino un puntero a una variable de tipo GLuint.
glGenTextures(1, &idTextura);
Con esto OpenGL mirará cuantas texturas tiene ya almacenadas, y en función de esto pondrá en idTextura el valor del identificador. Seguidamente, usaremos la función glBindTexture(…) para asignar el valor de idTextura, a una textura de destino. Es como si activáramos la textura asignada a idTextura, y todas las propiedades que modifiquemos a partir de entonces, serán modificaciones de esa textura solamente, y no de las demás, del mismo modo que activamos una luz y definimos sus propiedades.
glBindTexture(GL_TEXTURE_2D, idTextura);
Ahora falta el paso más importante, que es pasarle la textura a OpenGL. Para ello haremos uso de la función glTexImage2D(…)
GlTexImage2D(GL_TEXTURE_2D, 0, 3, anchoTextura, altoTextura, 0,
GL_RGB, GL_UNSIGNED_BYTE, textura);
Pero veamos todos los parámetros, viendo los parámetros de esta función uno
por uno:
void glTexImage2D(
GLenum tipoTextura,
GLint nivelMipMap,
GLint formatoInterno,
GLsizei ancho,
GLsizei alto,
GLint borde,
GLenum formato,
GLenum tipo,
const GLvoid *pixels
);
· tipoTextura: El tipo de textura que estamos tratando. Tiene que ser
GL_TEXTURE_2D.
· NivelMipMap: El nivel de MipMapping que deseemos. De momento
pondremos ‘0’, más adelante veremos que significa.
· FormatoInterno: El numero de componentes en la textura. Es decir, si
estamos trabajando en formato RGB, el numero de componentes será 3.
· Ancho, alto: El ancho y alto de la textura. Han de ser potencias de 2.
· Borde: La anchura del borde. Puede ser 0.
· Formato: El formato en que esta almacenada la textura en memoria. Nosotros
usaremos GL_RGB.
· Tipo: El tipo de variables en los que tenemos almacenada la textura. Si la
hemos almacenado en un unsigned char, usaremos GL_UNSIGNED_BYTE.
· Pixels: El puntero a la región de memoria donde esté almacenada la imagen.
Una observación. Teníamos en memoria RAM una textura cargada desde un archivo. Esta textura, se la hemos pasado a OpenGL, que se la ha guardado en su propia memoria. Por tanto, ahora tenemos dos copias en memoria de la misma textura, solo que una ya no es necesaria, la nuestra. Por tanto, es recomendable eliminar nuestras texturas de memoria una vez se las hemos pasado a OpenGL.
free(textura);
1.3. Parámetros de las texturas
A cada textura le podemos asignar unas ciertas características. El primero de ellos es el filtro de visualización. Una textura es un bitmap, formado por un conjunto de píxels, dispuestos de manera regular. Si la textura es de 64 x 64 píxel, i la mostramos completa en una ventana de resolución 1024×768, OpenGL escalará estos pixels, de manera que cada píxel de la textura (de ahora en adelante téxel) ocupará 16×12 píxeles en la pantalla.
1024 pixels ancho / 64 texels ancho = 16;
768 pixels alto / 64 texels alto = 12;
Eso quiere decir que lo que veremos serán "cuadrados" de 16×12, representando cada uno un texel de la textura. Visualmente queda muy poco realista ver una textura ‘pixelizada’, de manera que le aplicamos filtros para evitarlo. El más común es el ‘filtro lineal’, que se basa en hacer una interpolación en cada píxel en pantalla que dibuja. A pesar de que pueda parecer costoso a nivel computacional, esto se hace por hardware y no afecta en absoluto al rendimiento.
Veamos como lo podemos implementar:
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Con esto estamos parametrizando dos filtros. Uno para cuando la textura se representa más grande de lo que es en realidad (el ejemplo que hemos comentado) y otro para cuando la textura es mas pequeña: GL_TEXTURE_MAG_FILTER y GL_TEXTURE_MIN_FILTER, respectivamente. En los dos le decimos que haga un filtro lineal. Tambien podriamos decirle que no aplicara ningún filtro (GL_NEAREST).
Generalmente al texturizar una escena, el modelador aplica una textura pequeña que se repite a lo largo de una superficie, de manera que, sacrificando realismo, usamos menos
memoria. Podemos indicar a OpenGL que prepare la textura para ser dibujada a manera
de ‘tile’ o para ser dibujada solo una vez. Por ejemplo, un cartel en una pared. En la pared habría una textura pequeña de ladrillo, que se repetiría n veces a lo largo de toda la superficie de la pared. En cambio, el cartel solo se dibuja una sola vez. La textura de ladrillo tendríamos que preparar para que se repitiese, y la del cartel no. Realmente lo que hace OpenGL realmente es, al dibujar la textura y aplicar el filtro lineal, es, en los bordes, interpolar con los texels del borde opuesto, para dar un aspecto mas realista.
Si queremos activar esta opción, haríamos:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
Si no nos interesa este efecto:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
Por GL_TEXTURE_WRAP_S y GL_TEXTURE_WRAP_T nos referimos al
filtro para las filas y las columnas, respectivamente.
1.4. Renderizando con texturas
Ahora que ya tenemos las texturas cargadas y ajustadas a nuestro gusto, veamos ahora cómo podemos dibujar polígonos con texturas aplicadas. Supongamos que queremos dibujar un simple cuadrado, con la textura que hemos cargado anteriormente.
Si lo dibujamos sin textura seria:
glBegin (GL_QUADS);
glVertex3i (-100, -100, 5);
glVertex3i (-100, 100, 5);
glVertex3i (100, 100, 5);
glVertex3i (100, -100, 5);
glEnd ();
Veamos ahora como lo hariamos aplicando una textura:
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, idTextura);
glBegin (GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3i (-100, -100, 5);
glTexCoord2f(1.0f, 0.0f);
glVertex3i (-100, 100, 5);
glTexCoord2f(1.0f, 1.0f);
glVertex3i (100, 100, 5);
glTexCoord2f(0.0f, 1.0f);
glVertex3i (100, -100, 5);
glEnd ();
glDisable(GL_TEXTURE_2D);
Analicemos el código paso por paso:
glEnable(GL_TEXTURE_2D);
La función glEnable de OpenGL nos permite activar y desactivar propiedades que se aplican a la hora de renderizar. En este caso, le estamos diciendo que active la texturización
glBindTexture(GL_TEXTURE_2D, idTextura);
Como ya habiamos visto antes, la función glBindTexture se encarga de activar la textura que deseemos, referenciada por el identificador que habiamos guardado previamente en idTextura.
glBegin (GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3i (-100, -100, 5);
Aquí vemos algo que no estaba antes, nos referimos a glTexCoord2f(). Esta funcion nos introduce un concepto nuevo: las coordenadas de textura, y nos referiremos a ellas generalmente como ‘s’ y ‘t’, siendo ‘s’ el eje horizontal y ‘t’ el vertical. Se usan para referirnos a una posición de la textura. Generalmente se mueven en el intervalo [0,1].
La coordenada (0, 0) se refiere a la esquina inferior izquierda, y la (1, 1) a la superior derecha. Por lo tanto, si nos fijamos en el código, a cada vértice le asignamos una coordenada de textura, siempre en orden contrario a las agujas del reloj, sino la textura se vería al revés.
Hemos comentado que las coordenadas de textura se mueven en el intérvalo [0,1], pero OpenGL permite otros valores. Podríamos considerar que la textura se repite infinitamente en todas las direcciones, de manera que la coordenada (1, 0) seria el mismo texel que (2, 0), (3, 0), etc. No obstante, si dibujamos un cuadrado y le asignamos a un extremo (2, 2) en lugar de (1, 1), dibujara la textura repetida 2 veces en cada dirección, en total, 4 veces. Podemos jugar y asignar valores de (-1, -1) a (1, 1),etc.
Una vez hayamos dibujado la geometría que queramos, es importante desactivar la texturización, si no lo hacemos, si mas adelante queremos dibujar algún objeto sin texturizar, OpenGL intentará hacerlo, aunque no le pasemos coordenadas de textura, produciendo efectos extraños. Así pues:
glDisable(GL_TEXTURE_2D);
1.5. Colores, luces y texturas
Hemos estado hablando de texturizar una superficie, pero, que pasa con el color? Realmente podemos seguir usándolos, es mas, OpenGL texturizará y coloreará a la vez. Podemos activar el color rojo, y todas las texturas que se dibujen a partir de entonces, saldrán tintadas de ese mismo color.
Del mismo modo con las luces. No hay ningún problema en combinar colores, luces y texturas a la vez. Si no queremos iluminación alguna, basta con no activar las iluminación, y si no queremos que las texturas salgan tintadas de algún color, basta con definir el color activo como el blanco.
#include <GL/glut.h>
#include "bitmap.h"
BITMAPINFO *TexInfo; /* Texture bitmap information */
GLubyte *TexBits; /* Texture bitmap pixel bits */
void display(void) {
glClearColor (1.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
TexBits = LoadDIBitmap("escudo.bmp", &TexInfo);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TexInfo-> bmiHeader.biWidth, TexInfo-> bmiHeader.biHeight, 0, GL_BGR_EXT,GL_UNSIGNED_BYTE, TexBits);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glColor3f(1.0, 1.0, 1.0);
//se activa el mapeado detexturas
glEnable(GL_TEXTURE_2D);
glBegin(GL_POLYGON);
glTexCoord2f(0.0, 0.0);
glVertex2f(-1.0, -1.0);
glTexCoord2f(1.0, 0.0);
glVertex2f(1.0, -1.0);
glTexCoord2f(1.0, 1.0);
glVertex2f(1.0, 1.0);
glTexCoord2f(0.0, 1.0);
glVertex2f(-1.0, 1.0);
glEnd();
glDisable(GL_TEXTURE_2D);
glutSwapBuffers() ;
}
void reshape(int width, int height) {
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (GLfloat)height / (GLfloat)width, 1.0, 128.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0.0, 1.0, 3.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("tecnunLogo");
glutReshapeFunc(reshape);
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
Cargar textura
bool CTextura::CargarBMP(char *szNombreFichero)
{
FILE *hFichero;
AUX_RGBImageRec *Imagen;
bool bResultado=false;
if (szNombreFichero)
// Comprobamos que el nombre de fichero sea correcto
{
hFichero=fopen(szNombreFichero,"r");
// Comprobamos si el fichero existe (Si podemos abrirlo)
if (hFichero) // ¿Existe el fichero?
{
fclose(hFichero); // Cerramos el handle
Crear(szNombreFichero);
Imagen=auxDIBImageLoad(szNombreFichero);
// Cargamos el BMP
m_nAncho=Imagen->sizeX;
m_nAlto=Imagen->sizeY;
glTexImage2D(GL_TEXTURE_2D, 0, 3, m_nAncho, m_nAlto, 0, GL_RGB, GL_UNSIGNED_BYTE, Imagen->data);
bResultado=true;
}
}
delete Imagen->data;
delete Imagen;
return bResultado;
}
El mapeado de texturas puede resumirse en cuatro pasos:
1. Se carga una imagen y se define como textura.
2. Se indica cómo va a aplicarse la textura a cada píxel de la superficie.
3. Se activa el mapeado de texturas.
4. Se dibuja la escena, indicando la correspondencia entre las coordenadas
de la textura y los vértices de la superficie.
Normalmente en una escena suelen haber muchas texturas diferentes. Si dibujamos toda la geometría desordenadamente, es posible que cada pocos triángulos tengamos que cambiar de textura. El proceso de cambio de textura tiene un cierto coste, por eso es recomendable organizar nuestra geometría, haciendo grupos de superficies que tengan la misma textura, de manera que hagamos los mínimos cambios posibles. Asimismo, podemos hacer lo mismo con los materiales en el caso que hagamos uso de ellos.
1. [IPG04]
Introducción a la Programación Gráfica con OpenGL
(Oscar García y Alex Guevara) La Salle 2004
2.[Map01]
Tutorial Mapeado de textura
(Fernando Jose Serrano) 25/04/01
3. [Ham99]
Hammersley, T., "Texture Shading", GameDev.Net Website at http://www.gamedev.net, October, 1999.
Autor:
Yuri A. Rodrgíuez Cruz