|
|
Artículo realizado por
Si estais aquí es porque os interesa el curso y eso me parece
perfecto...¡vamos alla pues!
Tal como comenté en el artículo de presentaciín, es inevitable
hablar de cómo programar con OpenGL sin, a la par, mencionar determinados
conceptos. Por tanto primero debemos entender qué es un "pipeline" gráfico,
qué elementos lo configuran y cuáles son sus entradas y salidas.
Oscar García "Kokopus".
Capitulo 1.
Conceptos previos.
Sistemas gráficos. Dispositivos y elementos.
Un sistema gráfico típico se compone de los siguientes
elementos físicos:
![]() |
Estas son las características generales de cada uno de los
elementos:
Entradas : todo aquello que nuestro programa ha calculado
y desea dibujar... En definitiva es el "nuevo estado" de nuestro mundo
tras algún evento que lo ha hecho cambiar como por ejemplo que la cámara
se haya movido o alejado de la escena.
Procesador ("CPU") : máximo administrador del sistema,
este maestro de ceremonias se encargara de gestionar la comunicación entre
todos los módulos. Realizara operaciones según se le pida con ayuda de
una/s ALU/s ( Unidades aritméticas ) y consultara la memoria cuando
le sea necesario. En sistemas dedicados, y por tanto especializados en
gráficos, podemos tener diversas CPU's "trabajando" en paralelo para asegurar
un buen rendimiento en tiempo real (Calcular y dibujar a la vez).
Memoria : elemento indispensable y bastante obvio
como el anterior. En nuestro caso nos interesara una cierta franja de memoria,
el "frame buffer".
"Frame Buffer" : zona de memoria destinada a almacenar
todo aquello que debe ser dibujado. Antes de presentar la información por
pantalla, esta se recibe en el frame buffer. Por tanto nuestro programa
OpenGL escribe en esta area de memoria y automáticamente envía su contenido
a la pantalla después.
Look Up Table ("LUT") : esta "tabla" contiene todos
los colores que tenemos disponibles en nuestro sistema. A algunos os parecerá
quizás mas familiar el termino "paleta" para referiros a la LUT. En sistemas
"indexados" cada color tiene un identificador en la tabla y puedes
referirte a el usándolo en tu programa. Así si deseamos dibujar un polígono
de color rojo, modificaremos su atributo de color dándole a este el valor
del identificador que tiene el color rojo en la LUT...leer despacio esta
línea y seguro que lo entendéis...
Conversor D/A : la información contenida en el frame
buffer a nivel de bit es digital y por tanto debe convertirse a su homónimo
analógico para poder ser procesada por un CRT (Tubo de rayos catódicos)
y proyectada en la pantalla. No profundizo mas en este tema pues no es
el objetivo analizar el funcionamiento de un monitor.
Salidas : tras el conversar ya disponemos de información
analógica para ser visualizada en nuestra pantalla.
Antes de continuar aclarar el termino "pixel" (picture
element). Un pixel es la unidad mínima de pantalla y los encontramos dispuestos
en filas en cualquier monitor o televisor de manera que el conglomerado
de pixels con sus colores asociados da lugar a la imagen.
El frame buffer se caracteriza por su resolucion
y por su profundidad.
La resolución viene dada por el producto ancho x alto,
es decir, el numero de filas multiplicado por el numero de columnas análogamente
a las resoluciones que nuestro monitor puede tener (640 x 480, 800 x 600
...).
La profundidad es el numero de bits que utilizamos para
guardar la información de cada pixel. Este numero dependerá de la cantidad
de colores que deseemos mostrar en nuestra aplicación. Típicamente si queremos
"color real" necesitaremos ser capaces de mostrar 16,7 millones
de colores simultáneamente que es la capacidad aproximada de nuestro sistema
visual. En este caso y suponiendo una resolución de 800 x 600 pixels en
pantalla necesitaremos...:
800 pixels/fila x 600 filas x 24 bits/pixel = 1.37 Megabytes de memoria para el frame buffer... |
dado que color real implica 256 posibles valores de rojo,
256 de verde y 256 de azul por pixel y esto implica un byte/pixel para
cada una de estas componentes. De todas formas mas adelante abordaremos
este tema en profundidad cuando hablemos de color....¡no os preocupéis si
ahora mismo no lo veis del todo claro!.
Por ahora debéis quedaros con la idea de que necesitamos
una área especial de memoria para poder dibujar a cada momento nuestra
imagen 3D. Según la resolución que deseemos para nuestra ventana de visionado
y segun la calidad de color esperada, necesitaremos mas o menos memoria...así
es.
Entonces pensareis...y ¿cómo lo hago para "reservar"
esta memoria?...¿tengo que tratar "yo" directamente con ella?.....NO,
precisamente esto será lo que OpenGL hará por si sola sin que nosotros
nos demos ni cuenta aunque mas adelante veremos que también podríamos llegar
a este nivel de detalle.
Nuestra librería o API (Application Programming
Interface) , en este caso OpenGL, contactara directamente con los elementos
de la figura anterior a medida que lo crea necesario. Por tanto esto nos
será totalmente transparente y no deberá preocuparnos.
OpenGL utiliza este modelo semántico para interpretar
una escena que debe ser dibujada. Básicamente se trata de imaginar un objeto
situado en un determinado lugar y filmado por una cámara...es tan sencillo
como esto. Si conseguís tener claro este punto de vista tendréis mucho
ganado os lo aseguro. Uno de los problemas al programar gráficos es no
tener una visión mental clara del mundo 3D y no saber interpretar QUE es
lo que esta pasando. Vamos a echarle un vistazo al siguiente diagrama:
![]() |
Contamos con un "mundo 3D" que estamos observando desde
una determinada posición. Podéis pensar en el observador como vosotros
mismos mirando hacia vuestro mundo virtual o bien como una cámara que lo
esta filmando. Lo llaméis como lo llaméis, ese punto es el centro de
proyección tal y como se observa en la figura. Evidentemente el mundo
es tridimensional pero su proyección en un plano (plano de proyección)
es bidimensional. Este plano es nuestro frame buffer antes de dibujar o
bien la pantalla del monitor después de haberlo hecho.
Por tanto queda claro que pasamos de coordenadas del mundo
en 3D a coordenadas de pantalla en 2D y por lo tanto necesitamos proyectar.
El modelo "camara sintética" se compone pues de
los siguientes elementos:
Necesitamos luces que iluminen nuestro mundo 3D. Para
ello será necesario que especifiquemos sus localizaciones, sus intensidades
y sus colores.
Necesitamos una camara que "filme" nuestro mundo virtual
y lo muestre por pantalla. Esta cámara es nuestro "punto de vista" del
mundo a cada momento. Se caracteriza por su posicion en el mundo,
su orientacion y su apertura o "campo visual".
El campo visual es la "cantidad" de mundo que la cámara alcanza a ver.
Mirad la figura anterior e imaginad que acercamos el plano de proyección
a nuestro mundo 3D. En este caso estaremos disminuyendo el campo visual
dado que será menos "porción" del mundo la que se proyectara ...¡imaginad
un poco y lo veréis!. De hecho cualquiera que haya usado una cámara
para hacer fotografías entenderá con un poquito de esfuerzo lo que estoy
comentando.
Por ultimo necesitamos objetos que formen parte de nuestro mundo y que precisamente serán los "filmados" por nuestra cámara. Se caracterizaran por sus atributos de color, material, grado de transparencia, etc....ya lo analizaremos mas adelante.
Es muy importante que notéis la independencia que todos estos
elementos tienen entre si. La cámara es independiente de los objetos puesto
que estos están en una posición y ella en otra. Manejaremos los objetos
(Geometria) por un lado y la cámara por otro de manera que los primeros
"actuaran" en nuestro mundo y la segunda "filmara" desde una determinada
posición con un determinado ángulo, ¿ok?
Hablemos ahora del "pipeline gráfico", elemento
que marcara la pauta de actuación para OpenGL e incluso para vuestras manos
en el momento de programar.
La geometría que deseáis dibujar será la entrada de vuestro
pipeline gráfico y como salida tendréis una imagen en pantalla. Pero que
ocurre con todos esos puntos, vectores y polígonos desde que entran hasta
que salen?....veámoslo en la figura:
![]() |
El punto de entrada de nuestra geometría es el superior
izquierdo y a partir de ahí solo tenéis que seguir las líneas que conectan
los diferentes módulos. Quizás ahora penséis que esto no es será útil y
que es simplemente teoría. No, de ninguna manera podéis pretender programar
OpenGL si no sabéis QUE y en QUE ORDEN hace las cosas...lo veréis cuando
empecemos a programar...
Objeto geométrico : nuestro mundo se compone de puntos,
líneas, polígonos .... en definitiva "primitivas". Inicialmente
estos objetos tienen unos atributos que nosotros fijamos pero pueden ser
movibles o fijos, deformables o rígidos...y por tanto debemos ser capaces
de trasladarlos, rotarlos, escalarlos ... antes de dibujarlos en pantalla.
Transformacion del modelo : este modulo es el encargado
de trasladar, rotar, escalar e incluso torcer cualquier objeto para que
sea dibujado en pantalla tal y como debe estar dispuesto en el mundo. OpenGL
realiza estas funciones multiplicando a nuestra geometría (vertices)
por varias matrices, cada una de las cuales implementa un proceso (rotar,
trasladar ...). Nosotros usaremos las funciones de OpenGL para "crear"
estas matrices y "operarlas" con nuestra geometría.
Coordenadas del mundo : tras haber transformado nuestros
vértices, ya sabemos las posiciones de todos los objetos en nuestro mundo.
No son relativas a la cámara, recordad que precisamente he incidido en
la independencia cámara/mundo. Son posiciones referidas a un sistema de
coordenadas que definiremos única y exclusivamente para el mundo que estamos
creando.
Transformacion del visionado : ahora es cuando necesitamos
saber "como" se ven esos objetos, ya posicionados correctamente en el mundo,
desde nuestra cámara. Por tanto los "iluminamos" para que sean visibles
y tomamos sus posiciones tal y como se ven desde la cámara.
Coordenadas de cámara : tras aplicar luces ya sabemos
cuales son las coordenadas de todos los objetos respecto de nuestra cámara,
es decir, como los vemos nosotros desde nuestra posición en el mundo.
"Clipping" : el clipping consiste en recortar (ocultar)
todo aquello que "esta" pero "no se ve" desde la cámara. Por ejemplo, imaginad
una habitación 3D como las de Duke Nukem' o Doom. Cuando el personaje mira
en una dirección, solo ve esa pared y su contenido o incluso solo una fracción
de la pared, pero el resto de la habitación "esta allí" aunque no se ve.
A "ese" resto de la habitación se le ha aplicado un "clipping" o recorte,
se ha eliminado por tanto de la información que debe dibujarse en pantalla.
Proyeccion : ya la hemos comentado anteriormente.
Pasamos de coordenadas 3D del mundo a coordenadas 2D de nuestro plano de
proyección.
"D.I.S.C." (Device Independent Screen Coordinates)
: tras proyectar tenemos lo que se llaman "coordenadas de pantalla independientes
de dispositivo". Esto es sencillo aunque suena muy raro. Las coordenadas
que tenemos calculadas en este momento todavía no se han asociado a ningún
tipo de pantalla o monitor. En este punto del pipeline no sabemos si nuestro
monitor es de 15 pulgadas y de resolución 800 x 600 o si es de 19 pulgadas
y de resolución 1024 x 1480. De hecho esto no tenemos porque controlarlo
nosotros, nos será transparente. Se trata de asociar la imagen recortada
2D que se encuentra en el frame buffer con los pixels de la pantalla. Según
la resolución de pantalla sea mayor o menor, un punto de nuestro mundo
ocupara mas o menos pixels.
"Rasterization" : este proceso se conoce también como
"scan conversión". Finalmente asociamos todos nuestros puntos a
pixels en pantalla. Tras esto solo falta iluminar los fósforos de nuestra
pantalla con energía suficiente para que veamos lo que esperamos. Evidentemente
esto es un proceso totalmente físico llevado a cabo en el tubo de rayos
catódicos del monitor. No actuaremos pues sobre el.
Imagen de pantalla : proceso cerrado. Ya tenemos la
imagen de lo que nuestra cámara "ve" ¡en frente de nuestros ojos!.
El pipeline gráfico puede implementarse vía software o hardware.
En máquinas dedicadas, por ejemplo Silicon Graphics, todos los módulos
están construidos en la placa madre de manera que el sistema es muy rápido.
En sistemas mas convencionales como PC o Mac, todo se realiza vía software
y por tanto es mas lento. Evidentemente esta es la razón de que una estación
gráfica potente no sea precisamente barata.
De todas formas lo interesante de OpenGL es que funciona
independientemente de la implementación del pipeline. No tendréis que cambiar
el código si cambiáis de plataforma, ¡funcionara igual!...lo único es
que según el sistema conseguiréis mas o menos velocidad.
Empecemos pues con algunos comandos básicos de OpenGL.
Como ya dije, supongo que tenéis nociones de C y por tanto no debe de seros
complicado de asimilar.
OpenGL tiene sus propios tipos en cuanto a variables
se refiere. Así aunque podemos usar los típicos (int, float, double), nos
acostumbraremos a los definidos por esta librería (GLint, GLfloat, GLdouble).
Como veis son los mismos pero con el prefijo "GL" delante. Se declaran
exactamente igual que cualquier variable en C y tienen casi las mismas
propiedades. Usémoslos porque el funcionamiento general del sistema sera
mas optimo.
Las funciones OpenGL empiezan con el prefijo "gl",
en minúsculas. Veamos un ejemplo. supongamos que queremos crear un vértice,
es decir un punto:
Usaremos...
glVertex3f( 0.0 , 0.0 , 0.0 );
o...
GLfloat vertice[3] = { 0.0, 0.0, 0.0 };
y después...
glVertexfv( vertice );
Ambas funciones crean un vértice situado en el origen
de coordenadas del mundo, es decir, x = y = z = 0.0. En el primer caso
el nombre de la función termina en "3f" (3 floats). Esto significa que
vas a especificar el vértice con 3 valores o variables de tipo real, o
sea float. En cambio en el segundo caso tenemos "fv" (float vector). Estamos
indicando a OpenGL que el vértice lo daremos mediante un array/vector de
floats. Precisamente este es el array que defino justo antes de llamar
a la función.
Trabajamos en 3D y especificamos las coordenadas del vértice
en este orden: X, Y, Z. Si deseamos trabajar en 2D solo tenemos que hacer
una coordenada igual a 0.0, normalmente la Z.
Ya que estamos puestos vamos a definir nuestro primer
polígono, un triángulo. OpenGL tiene varios tipos definidos de manera que
nos facilita la creación de polígonos simples. Vamos allá:
glBegin( GL_TRIANGLES );
glVertex3f (-1.0, 0.0, 0.0);
glVertex3f (1.0, 0.0, 0.0);
glVertex3f (0.0, 1.0, 0.0);
glEnd( );
Este código crea un triángulo situado en el plano XY ya
que observareis que los valores de Z son todos 0.0 para los tres vértices
que lo forman. Sus tres vértices se encuentran en las posiciones ( -1.0,
0.0 ), ( 1.0, 0.0 ) y (0.0, 1.0 ) según la forma ( X, Y ).
Un polígono se encapsula entre las funciones glBegin y
glEnd. El parámetro que recibe la primera sirve para decirle a OpenGL que
tipo de polígono deseamos crear, en nuestro caso un triángulo. GL_TRIANGLES
es una constante ya definida en la librería. Ya veréis como usaremos muchisimas
constantes de este tipo para programar.
Por claridad es conveniente tabular (indentar) el código
entre glBegin y glEnd tal y como veis en el ejemplo. Cualquier programa
OpenGL que examinéis seguirá esta convención si esta bien estructurado.
Vamos a definir alguno de los atributos de nuestro triángulo,
por ejemplo su color. Usamos:
glColor3f (0.5, 0.5, 0.5);
donde los tres valores (floats) que se le pasan a la función
glColor son por orden, la cantidad de rojo (Red) que deseamos, la cantidad
de verde (Green) y la cantidad de azul (Blue). Es el llamado sistema RGB
que muchos conoceréis sobradamente. Aplicando una cantidad de cada color
conseguimos el tono deseado (Teoría aditiva del color). Los valores
de cada color deben estar entre 0.0 (No aplicar ese color) y 1.0 (Aplicar
ese color en su máxima intensidad). Por tanto:
glColor3f (0.0, 0.0, 0.0); se corresponde con el color
NEGRO
mientras que...
glColor3f (1.0, 1.0, 1.0); ¡se corresponde con el BLANCO!
y de esta manera si queremos definir un triángulo blanco
obraremos asi:
glColor3f (1.0, 1.0, 1.0);
glBegin( GL_TRIANGLES );
glVertex3f( -1.0, 0.0, 0.0 );
glVertex3f( 1.0, 0.0, 0.0 );
glVertex3f( 0.0, 1.0, 0.0 );
glEnd( );
de manera que primero especificamos el color y TODO lo
que dibujemos a partir de este momento será de ese color, en este caso
el triángulo.
Al ejecutar cada función glVertex, el vértice en cuestión
"entra" en el pipeline y se traslada a la posición que hemos especificado
para el. Entonces se "mapea" en el frame buffer (representación
en memoria del plano de proyección 2D) pasando posteriormente a la pantalla
de nuestro monitor. Así en este caso tenemos 3 vértices que sucesivamente
entran en el pipeline uno detrás de otro.
Algo muy importante que debéis tener en cuenta. Cuando
defináis un polígono a base de sus vértices deberéis seguir un orden concreto.
Si no lo hacéis, OpenGL no asegura que la representación en pantalla sea
la correcta. La convención es crear los vértices siguiendo el polígono
según el sentido antihorario de las manecillas del reloj. Comprobad
mi ejemplo sobre papel y veréis como esta definido siguiendo este criterio.
OpenGL supone la cara "a dibujar" del polígono como la que se define de
esta manera.
Respecto a las constantes que podemos usar en la función
glBegin tenemos entre otras:
GL_POINTS : para que todos los vértices indicados
entre ambas funciones se dibujen por separado a modo de puntos "libres".
GL_LINES : cada dos vértices definidos, se traza automáticamente
una línea que los une.
GL_POLYGON : se unen todos los vértices formando un
polígono.
GL_QUADS : cada 4 vértices se unen para formar un
cuadrilátero.
GL_TRIANGLES : cada 3 vértices se unen para formar
un triángulo.
GL_TRIANGLE_STRIP : crea un triángulo con los 3 primeros
vértices. entonces sucesivamente crea un nuevo triángulo unido al anterior
usando los dos últimos vértices que se han definido y el actual.....ya
se que es complicado pero esto vale mas la pena verlo en pantalla...¡no
os impacientéis!
.
GL_QUAD_STRIP : igual que el anterior pero con cuadriláteros.
Etc, etc, etc ...
De hecho los tipos mas usados son los tres primeros mientras
que el resto pueden sernos de ayuda en casos determinados. Ya lo iremos
viendo...
Bueno creo que con este material concluyo este primer
capitulo del curso. Espero que os sea de interés y sigáis leyendo en la
próxima edición.
En el próximo capitulo ya seremos capaces de teclear un
programa de iniciación y prueba. Por ahora es importante que todo os quede
muy claro a nivel teórico pues sino mas adelante ¡os daréis con una pared!
También intentare mencionar aquellos web's que os pueden
ser de mas interés en cuanto a este tema así como posible bibliografía
a consultar.
¡Hasta ahora! ;)
|
|