Aula Macedonia


Curso de Programación Gráfica en OpenGL


Artículo realizado por
Oscar García "Kokopus".





Capitulo 2.
Programación gráfica (parte I).

En al anterior articulo describí cuales son los principales módulos a destacar en un sistema gráfico completo. Asimismo comente el modelo cámara sintética en el cual se fundamenta OpenGL en el momento de proyectar nuestro mundo 3D en la pantalla 2D. Por ultimo repasamos un poco todo lo referido al pipeline gráfico y mencione algunas llamadas básicas a OpenGL.

En este segundo capitulo aprenderemos a interactuar con OpenGL con mas profundidad, concentrándonos en el uso de las primitivas de las que disponemos para ser capaces de empezar a dibujar algo serio. Vamos a ello pues !!!

Uso de funciones gráficas en OpenGL.

Nuestro sistema gráfico es una caja negra. Con esto quiero decir que existen unas especificaciones de entrada y otras de salida, de manera que no nos importa para nada, cuando programamos, como se implementan a nivel interno las operaciones que demandamos. Evidentemente mas adelante, con mas experiencia, empezaremos a preguntarnos que es lo que esta ocurriendo "ahí dentro". Básicamente contamos con entradas tales como llamadas a funciones, uso de dispositivos tales como el teclado o el ratón e incluso mensajes que nos llegan desde el mismísimo sistema operativo ( Programación orientada a eventos ). Lo iremos analizando.

OpenGL es una API ( Applications Programming Interface ), o sea una librería. Llamando sucesivamente a sus funciones conseguiremos el comportamiento adecuado sin importarnos en un primer momento que es lo que esta "API" esta haciendo en realidad. Contamos pues con diversos tipos de funciones:

Todas las funciones que iré mencionando se encuentran catalogadas en una de estas categorías. En su conjunto conforman los dos centenares de funciones que construyen a nuestra API, OpenGL.

La interficie OpenGL.

Todas las llamadas a OpenGL empiezan siempre con las letras "gl", como ya observasteis en el anterior capitulo. Pero no solo contamos con estas funciones. Observemos el siguiente diagrama:

Además de GL ( Graphics Library ), que contiene la mayoría de las llamadas que usaremos, existen otras dos librerías inseparables de la primera.

  • GLU ( Graphics Utility Library ) contiene funciones para objetos comunes a dibujar como esferas o toros ( un toro es algo así como un "donut" ) así como funciones de control de la cámara, entre otras. Estas funciones empiezan con las letras "glu".
  • GLUT ( GL Utility Toolkit ) contiene todas aquellas funciones que permitirán a nuestro programa ser interactivo, es decir, manejable desde el ratón y el teclado. También permite crear objetos complejos al igual que GLU. Estas funciones empiezan con las letras "glut".

Las tres librerías deberán incluirse ( #include ,en C ) en nuestro programa de manera que seamos capaces de aprovechar al máximo todo lo que nos brindan.

Nuestro programa "llamara" a estas funciones, que se encuentran en las librerías, y estas actuaran sobre el frame buffer ( recordad que el frame buffer es la zona de memoria que hace de pantalla virtual "guardando" todo aquello que deberá mostrarse por nuestro monitor ).

A todos los familiarizados con C no creo que les suene demasiado raro todo esto y a los demás, algo nuevo que sabéis no??.

Muy bien o sea que cojo un compilador de C, incluyo las librerías y las uso.....hasta ahí perfecto Oscar pero....de donde saco las librerías???.....Bien bien la verdad es que navegando un poquito son bastante fáciles de encontrar pero yo os aconsejo algunos "sites" interesantes referidos a OpenGL:

Por cierto, una vez encontréis donde "bajaros" las librerías, verificad que son las referidas a vuestro sistema operativo en concreto!!!.

Además de las librerías encontrareis un montón de ejemplos, especificaciones, manuales y noticias !!!.


Primitivas y atributos. Polígonos. Texto. Color.

Entendemos por primitivas aquellas entes mínimas que pueden ser especificadas directamente mediante funciones de OpenGL. Como ya observamos en el anterior articulo la estructura en C será de la forma:

glBegin( TIPO );
    glVertex*(...);
    .......
    .......
    glVertex*(...);
glEnd();

Cada vértice especifica un punto y todos ellos especificaran un objeto de una u otra forma según el TIPO.

Veamos la figura:
 

En la figura se observa como usando diferentes tipos ( GL_POINTS, GL_LINES, etc ... ) conseguimos diferentes objetos en pantalla dados unos vértices creados según el orden que veis en la figura ( primero p0, luego p1, etc ... ) para los 6 rectángulos de la primera fila. Es decir:

glBegin( TIPO );
    glVertex3f( p0x, p0y, p0z );
    glVertex3f( p1x, p1y, p1z );
    glVertex3f( p2x, p2y, p2z );
    .....
    glVertex3f( p7x, p7y, p7z );
glEnd();

Lo que OpenGL dibujara según el caso es lo que se encuentra en color negro en la figura, no el trazado gris que solo lo he puesto como referencia.

En la segunda fila de la figura vemos como crear sucesivos "polígonos de cuatro lados" ( no tienen porque ser cuadrados o rectángulos, eso depende de en que posiciones situemos los vértices ) con un lado en común de dos a dos. Hay mas variantes que podréis encontrar en las especificaciones de OpenGL aunque no suelen usarse mas que para aplicaciones muy concretas.

Los polígonos especificados por una serie de puntos deben ser convexos para su correcto trazado, y coloreado, por parte de OpenGL. Que quiere decir esto?, pues bien se define a un polígono como convexo si cogiendo dos puntos cualesquiera de su interior y trazando la línea que los une, todos los puntos que pertenecen a la línea se encuentran dentro del polígono. En la anterior figura tenéis un ejemplo de esto.

Por tanto asegurada siempre que definís polígonos convexos, de lo contrario los resultados son simplemente impredecibles y pocas veces acertados. Entonces, como dibujo polígonos que no sean convexos?, lo que normalmente se hace es dibujar un polígono complejo, convexo o no, a base de muchos triángulos. Es lo que se denomina "Tesselation". Todo polígono puede descomponerse en triángulos y por tanto a partir de estos somos capaces de dibujar cualquier cosa.  También lo tenéis en la figura explicitado gráficamente.

Usando GLU o GLUT podemos crear con una sola llamada objetos mas complejos como es el caso de una esfera :

  • Usando GLU con void gluSphere( GLUquadricObj *qobj, GLdouble radius, GLint slices, GLint stacks );
  • Usando GLUT con void glutSolidSphere(GLdouble radius, GLint slices, GLint stacks);

En cuanto programemos lo veréis mas claro pero de hecho no dejan de ser funciones en C de las de siempre. Con radius definimos el radio que queremos para la esfera, con slices y stacks la "partimos" en porciones como si de un globo terráqueo se tratara ( paralelos y meridianos ) de manera que a mas particiones, mas calidad tendrá ya que OpenGL la aproximara por mas polígonos. En cuanto a *qobj en la primera función, se trata de definir primero un objeto de tipo quadric con otra función de GLU ( gluNewQuadric ) para despues asociar la esfera al susodicho objeto. No os preocupéis porque no usaremos demasiado esta filosofía. De momento quedaros con la idea que es lo que me interesa.

A alguien se le ocurrirá probablemente pensar que en estas funciones no se define la posición de la esfera en el mundo. Es cierto y por lo tanto previamente a crearla, y por tanto dibujarla, deberemos especificar donde queremos que OpenGL la dibuje. Lo podemos especificar con:

glTranslatef( posX, posY, posZ );, función que nos traslada posX unidades en el eje X, posY en el eje Y ... y si tras esto llamamos a glutSolidSphere, ya tendremos a nuestra esfera dibujada en el punto que deseamos.

Bueno hablemos un poco de los atributos. Una cosa es el tipo de la primitiva a dibujar y otra es como se dibuja, es decir, con que color de borde, con que color de relleno, con que ancho de borde, con que ancho por punto, etc ..... Estos son algunos de los atributos que pueden especificarse. Algo importante, en OpenGL los atributos no se pueden definir explícitamente para cada objeto sino que lo usual es definir unos ciertos atributos generales de manera que todo lo que se dibuje a partir de ese momento seguirá esas especificaciones. Si deseamos cambiar los atributos deberemos hacerlo en otra parte del programa de manera que la forma de dibujar será diferente a partir de esa línea, ok??. Veamos algunas funciones de ejemplo referidas a atributos:
 

  • glClearColor( 0.0, 0.0, 0.0, 0.0 );, esta es algo genérica y se refiere al color con el cual debe de "resetearse" el frame buffer cada vez que redibujemos toda la escena de nuevo. En este caso el "fondo" de nuestra ventana será como el fijado por esta función en el frame buffer, de color negro. El cuarto parámetro de la función se refiere al valor de "alpha" en cuanto al color. Veremos mas adelante que el valor de alpha permite variar el grado de transparencia de un objeto.

  • glColor3f( 1.0, 0.0, 0.0 );, esta ya os "sonara" del articulo anterior. En este caso definimos que todo lo que se dibuje desde este momento será de color rojo. Recordad que el orden de parámetros es Red, Green, Blue ( RGB ).

  • glPointSize( 2.0 );, con esta llamada definimos que cada uno de nuestros puntos deberá tener un grosor de dos pixels en el momento de ser trasladado a pantalla.

  • glNormal3f( 1.0, 0.0, 0.0 );, cuando operemos con luces veremos que para cada cara de un polígono hay que asociar un vector o normal. Esta función define como normal a partir de este momento a un vector definido desde el origen con dirección positiva de las X. El orden de los parámetros es X, Y, Z.

  • glMaterialfv(GL_FRONT, GL_DIFFUSE,   blanco);, por ultimo y también referido al tema de las luces. Cada objeto será de un material diferente de manera que los reflejos que en el se produzcan debidos a las luces de nuestra escena variaran según su rugosidad, transparencia, capacidad de reflexión ... en este caso definimos que todas las caras principales ( FRONT, son las caras "que se ven" del polígono ) de los objetos dibujados a partir de ahora tendrán una componente difusa de color blanco ( asumiendo que el parámetro "blanco" es un vector de reales que define este color ). La componente difusa de un material define que color tiene la luz que este propaga en todas las direcciones cuando sobre el incide un rayo luminoso. En este caso se vería luz blanca emanando del objeto/s dibujados a partir de ahora. Por supuesto antes hay que definir luces en nuestra escena y activarlas. Lo veremos mas adelante en el apartado de iluminación.

En cuanto a los colores. Trabajaremos con la convención RGBA, es decir, grado de rojo, verde, azul y transparencia. Los valores para cada componente se encontraran siempre dentro del intervalo [ 0, 1 ]. Si nos pasamos o nos quedamos cortos numéricamente hablando, OpenGL adoptara como valor 1 o 0 según sea el caso. Es decir, que redondea automáticamente aunque hay que evitarle cálculos innecesarios y prever valores correctos.

En el caso del valor para alpha, 0 significa transparencia total y de aquí podemos usar cualquier valor hasta 1, objeto totalmente opaco. Y esto para que servirá?, pues para poder "ver" a través del objeto lo que hay detraes!!!.

Que decir del texto.....no nos es de mucho interés ahora mismo que lo que estamos deseando es empezar a dibujar polígonos 3D como locos pero comentare un par de cosas. OpenGL permite dos modos de texto: Stroke Text y Raster Text.

En el primer caso cada letra del alfabeto es una nuevo polígono literalmente construido a partir de primitivas de manera que puede tratarse tal y como si de un objeto cualquiera se tratara. Es pesado pues tenemos que dibujar literalmente cada letra a base de primitivas pero interesante en cuanto a posibilidades pues podremos escalar, rotar, trasladar.....en fin todo lo que nos es posible aplicar a un polígono.

En cuanto al segundo caso tenemos un ejemplo de simplicidad y rapidez. Es el típico alfabeto creado a partir de bloques de bits ( BitMaps ), de manera que tendremos una rejilla sobre la que un 1 significara lleno y un 0 vacío. De esta forma :

1 0 0 0 1

1 0 0 0 1

1 1 1 1 1

1 0 0 0 1

1 0 0 0 1

se corresponde con la letra H !!!

Así podremos crearnos fácilmente un alfabeto y posicionar directamente en el frame buffer sendas letras. Para ello es necesario conocer como acceder directamente, a nivel de bit, al frame buffer y esto aun nos tomara algo de tiempo!!!

De momento dejemos de lado el tema del texto y centrémonos pues en los gráficos puros y duros !!!

Visionado de gráficos. Matrices.

Recordáis el diagrama referido a la proyección de nuestro mundo 3D en un plano 2D ???, lo tenéis en el anterior capitulo por si las moscas...

Bien, vamos a trabajar un poco el tema de proyección. Posteriormente iniciaremos el tema de las matrices que actúan sobre nuestra geometría y de esta forma podremos empezar a dibujar.

Para empezar analizaremos la mas sencilla de las proyecciones que OpenGL nos ofrece. Fijaros bien en el diagrama:

Esta es la llamada proyección ortografica. Su funcionamiento es bastante simple puesto que se trata de coger todos los puntos ( x, y, z ) que se encuentren dentro del volumen de visualización ( 1. ) y eliminar su componente Z. Así todos los puntos pasan a ser del tipo ( x, y, 0 ) y por tanto lo que hemos hecho ha sido "proyectarlos", trasladarlos al plano de proyección que podéis ver para Z = 0 en el diagrama ( 2. y 3. ). Literalmente "chafamos" toda la geometría en el plano Z=0 y es esto lo que se traslada a pantalla !!!

Lo veis mas o menos claro???...echadle algo de imaginación y seguro que lo entendéis.

Necesitamos especificar el volumen de visualización puesto que la visión del mundo por parte de la cámara, o sea nuestros ojos, es limitada y por tanto solo una reducida parte de este puede verse a cada momento en pantalla. Todo lo que se encuentre dentro del volumen será lo que veremos. Debido a su simplicidad, esta proyección no se usa demasiado en aplicaciones 3D serias ya que se pierde la noción de tamaño al alejarte/acercarte de los objetos en el mundo. Por que?, pues porque siempre se proyecta todo en Z = 0 este donde este la cámara y por tanto eso es lo que se ve en pantalla, la proyección, que siempre será la misma. Mas adelante consideraremos casos mas realistas como la proyección en perspectiva que nos permitirá conseguir efectos 3D reales como en el caso de Doom, Heretic, Quake o cualquiera de los juegos de este tipo.

En OpenGL usaremos:

void glOrtho( GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far );

o bien

void gluOrtho2D( GLdouble left, GLdouble right, GLdouble bottom, GLdouble top );

Ambas nos permiten especificar las distancias que tenéis comentadas en la figura anterior ( 4. ). En el caso de la segunda función observareis que no podemos especificar las distancias "near" y "far". Esto es porque se asumen de valores -1.0 y 1.0 respectivamente en esta función.

Por tanto estamos "dimensionando" literalmente nuestro volumen de visualización dándole una altura, una anchura y una longitud.

Por cierto que en la figura ( 4. ) he asumido que el plano de proyección "descansa" sobre los ejes X e Y. Esto no tiene porque ser así. De hecho podemos generar cualquier plano de proyección ( anchura y altura ) que se nos ocurra. Eso si, siempre estará situado automáticamente en Z=0.

Como conclusión cabe relacionar claramente esto con el "clipping". Toda la geometría situada dentro del volumen se dibujara, es decir, saldar en pantalla. El resto se eliminara y por tanto no aparecerá en imagen. Esto no quiere decir que por no dibujarse no se haya calculado previamente. Precisamente para discernir que se encuentra dentro/fuera del volumen hay que calcular previamente las posiciones de todos los objetos en el mundo. Este es un gran coste que interesaría minimizar para poder optimizar el rendimiento a tiempo real. Mas adelante comentare como evitar calcular colores, iluminación, etc... de todo aquello que no debe ser dibujado gracias a estructuras de datos llamadas "quadtrees" y "octrees".

Hablemos ahora de matrices. Como visteis en el anterior capitulo, toda la geometría generada pasa a través del pipeline de gráfico sufriendo sucesivas transformaciones hasta su disposición final en pantalla. Estas transformaciones son literalmente matrices que multiplican a nuestros vectores ( vértices ) modificando sus características.

En OpenGL contamos con dos matrices principales que multiplicaran siempre a toda la geometría. Son las llamadas GL_PROJECTION (matriz de proyección) y GL_MODELVIEW (matriz de modelado/visionado).

En el momento en que definimos que tipo de proyección deseamos estamos actualizando los valores de la matriz de proyección. A partir de ese momento todo quedara proyectado tal como hemos especificado pues todo se multiplicara por esta matriz. Por otra parte cada vez que rotemos, traslademos, escalemos o bien cambiemos la posición de la cámara estaremos actuando sobre la matriz de modelado/visionado. Así, toda la geometría generada a partir de ese momento quedara trasladada, rotada, ... según hayamos definido.

Lo primero que debe hacerse en cualquier aplicación OpenGL es inicializar ambas matrices. Esto se consigue asociándoles la matriz identidad. Cualquier vector multiplicado por una matriz identidad resulta en el mismo, es decir no varia. Ese es el valor inicial que nuestras matrices deberán tener. Mas adelante en el programa y según nos interese las iremos modificando. Como ejemplo aquí tenéis la que es la matriz identidad de 3 filas por 3 columnas ( 3 x 3 ):

1 0 0

0 1 0

0 0 1

de manera que cualquier vector, que puede ser un vértice XYZ, multiplicado por ella no varia ...

Vamos a teclear un poco...

Bueno supongo que los que habéis tenido la paciencia de leer hasta este punto os merecéis ya algo de "carnaza", vamos un regalito !!!....bueno, de acuerdo, vamos a ver un programa de ejemplo aunque aun queda muuuuuuucccccchhhhhooooo de que hablar.

Aquí tenéis un listado comentado de un sencillo programa en C estándar ( ANSI ) que usa OpenGL :
 

/* Copyright (c) Oscar Garcia Panyella 1998,
 * todos los derechos reservados.
 * Curso de OpenGL para Macedonia Magazine.
 * Primer ejemplo.*/

/* Incluimos las librerias */
#include <GL/glut.h>

/* Ancho de la ventana de visualizacion */
#define ANCHO 400

/* Alto de la ventana de visualizacion */
#define ALTO 400 

/* Coordenada X del origen de la ventana,
 * esquina superior izquierda */
#define ORIGENX 100 

/* Coordenada Y del origen de la ventana,
 * esquina superior izquierda */ 
#define ORIGENY 100 

/*Parametros iniciales del programa.*/


void inicio(void) 
{
/* Activamos la matriz de proyeccion. */
	glMatrixMode(GL_PROJECTION);

/* "Reseteamos" esta con la matriz identidad. */ 
	glLoadIdentity();

/* Plano de proyeccion igual a la ventana
 * de visualizacion.
 * Volumen de visualizacion
 * desde z=-10 hasta z=10.*/ 

	glOrtho(0, ANCHO, 0, ALTO, -10, 10);

/* Activamos la matriz de modelado/visionado. */
	glMatrixMode(GL_MODELVIEW);

/* La "reseteamos". */
	glLoadIdentity();

/* Nos trasladamos al centro de nuestra
 * ventana donde siempre dibujaremos el
 * poligono.
 * Nos mantenemos en el plano z=5 que se encuentra
 * dentro del volumen de visualizacion.*/

	glTranslatef((GLfloat)ANCHO/2, (GLfloat)ALTO/2, 5.0);

/* Color de fondo para la ventana 
 * de visualizacion, negro. */
	glClearColor(0.0, 0.0, 0.0, 0.0);

 }

/* OpenGL llamara a esta rutina cada vez
 * que tenga que dibujar de nuevo.
 * 
 * Dado que rellenara el poligono de
 * color y cada vertice es de
 * un color diferente, OpenGL rellenara el
 * interior con una interpolacion
 * de los colores de los 4 vertices. 
 * Lo hace automaticamente.
 */ 

	void dibujar(void)
{ 
/* "Limpiamos" el frame buffer con
 * el color de "Clear", 
 * en este caso negro. */ 
	glClear(GL_COLOR_BUFFER_BIT);

/* Queremos que se dibujen las caras
 * frontales de los poligonos
 * y con relleno de color. */
	glPolygonMode(GL_FRONT, GL_FILL); 
	glBegin(GL_POLYGON); 

		/* Color azul para el primer vertice */
		glColor3f(0.0, 0.0, 1.0);
		glVertex3i(-100, -100, 5);

		/* Color verde para el segundo vertice */
		glColor3f(0.0, 1.0, 0.0);
		glVertex3i(-100, 100, 5);

		/* Color rojo para el tercer vertice */
		glColor3f(1.0, 0.0, 0.0);
		glVertex3i(100, 100, 5);

		/* Color amarillo para el cuarto vertice */
		glColor3f(1.0, 1.0, 0.0);
		glVertex3i(100, -100, 5);

	glEnd();
} 

/* Main del programa.*/

int main(int argc, char **argv)
{ 
/* Primera llamada siempre en OpenGL,
 * por si usaremos
 * la linea de comandos */
	glutInit(&argc, argv);

/* Activamos buffer simple y 
 * colores del tipo RGB */ 
	glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);

/* Definimos una ventana de medidas ANCHO x ALTO
 * como ventana de visualizacion */
	glutInitWindowSize (ANCHO, ALTO);

/* Posicionamos la esquina superior izquierda de
 * la ventana en el punto definido */ 
	glutInitWindowPosition (ORIGENX, ORIGENY);

/* Creamos literalmente la ventana y
 * le adjudicamos el nombre que se
 * observara en su barra de titulo */
	glutCreateWindow("Cuadrado Multicolor");

/* Inicializamos el sistema */
	inicio();

/* Hacemos saber a OpenGL que cada vez
 * que sea necesario dibujar de nuevo,
 * por ejemplo la primera vez, o al redimensionar
 * la ventana con el raton o
 * en caso de provocar un "redibujado" por programa,
 * debe llamar a la funcion "dibujar".*/ 

	glutDisplayFunc(dibujar);

/* Aqui espera el programa mientras
 * nada ocurra, es un Loop
 * infinito que se vera turbado por las
 * sucesivas veces que
 * sea necesario redibujar.*/ 
	glutMainLoop();

/* ANSI C requiere que main retorne un entero. */ 
	return 0;
}
Cuadrado Multicolor. Programa C basico. Primer ejemplo del curso.

  
Este programa crea una nueva ventana en nuestro sistema operativo de ventanas. La utilizara como ventana de visualización. En ella dibuja un polígono, en este caso un cuadrado de lado 200, formado por decenas de colores diferentes creados a partir de la interpolación de los colores de los 4 vértices.

Me interesa mucho que los realmente interesados analicéis el programa, lo compiléis, lo ejecutéis y sobretodo que intentéis entender todo lo que hace. Hay diversas cosas que aun no he explicado en este programa pero creo que si le prestáis atención y leéis mis comentarios, no tendréis problema alguno en dominarlas.

Con esto os dejo hasta la próxima edición de la revista en la que continuaremos en este capitulo segundo que como podéis observar, realmente da y para mucho.

¡Divertiros y escribidme si tenéis dudas!.




AULA MACEDONIA
a
MACEDONIA Magazine