Introducción
Este es el primer artículo de una serie sobre OpenGL, un estándar de la industria para gráficos 2D/3D (ver también Qué es OpenGL). Supondremos que el lector está familiarizado con su plataforma de desarrollo en C, y que tiene ciertos conocimientos con la librería GLUT (sinó, puedes seguir la serie de artículos "Programando GLUT" en este magazine). Bajo Linux, recomendamos el uso de la librería Mesa, que es una maravillosa implementación freeware de OpenGL. Actualmente existe incluso soporte hardware para Mesa (ver Tarjetas gráficas 3Dfx).
La presenación de cada nuevo comando de OpenGL vendrá acompañada de un ejemplo que trata de utilizar su funcionalidad, ¡por lo menos lo intentaremos!. Hacia el final de nuestra serie, os mostraremos el código fuente de un juego de simulación escrito completamente en OpenGL.
Antes de empezar, me gustaría mencionar que, como científico, la mayor parte de mi experiencia con OpenGL proviene de utilizarlo como herramienta para escribir simulaciones de sistemas clásicos y reales. Así pues, todos mis ejemplos van sobre eso ;-). Espero que los lectores encuentren estos ejemplos accesibles o, al menos, divertidos. Si te gustaría ver otro tipo de ejemplos, házmelo saber.
OpenGL se asocia a menudo con gráficos 3D, fantásticos efectos especiales, modelos complejos con modelado realístico de luces, etc. Sin embargo, también es una máquina de trazado de gráficos 2D. Esto es importante porque hay muchas cosas que puedes aprender a hacer en 2D antes de aprender las complejidades de las perspectivas 3D, el trazado de modelos, luces, posiciones de cámaras, etc. Un gran número de aplicaciones de ingeniería y ciencias se pueden trazar en 2D. Así pues, aprendamos primero cómo hacer sencillas animaciones en 2D.
Dibujando Puntos
OpenGL tiene únicamente unas pocas primitivas geométricas: puntos, líneas, polígonos. Todas ellas se describen en términos de sus respectivos vértices. Un vértice está caracterizado por 2 o 3 números en como flotante, las coordenadas cartesianas del vértice, (x, y) en 2D y (x, y, z) en 3D. Aunque las coordenadas cartesianas son las más comunes, en gráficos por ordenador también existe el sistema coordenado homogéneo en el que cada punto se describe con 4 números en coma flotante (x, y, z, w). Volveremos a él después de ver algunas nociones elementales de trazado en 3D.
Como en OpenGL todos los objetos geométricos son finalmente descritos como un conjunto ordenado de vértices, existe una familia de rutinas para declarar un vértice en OpenGL, su sintaxis es: void glVertex{234}{sifd}[v](TYPE coords);
Familiarízate con esta notación. Las llaves indican parte del nombre de la rutina, las rutinas pueden tomar 2, 3 o 4 parámetros de tipo short, long, float o double. Opcionalmente, estos parámetros se pueden proporcionar en forma de vector, en este caso deberemos usar la rutinas del tipo v. Aquí hay algunos ejemplos: void glVertex2s(1, 3);
void glVertex2i(23L, 43L);
void glVertex3f(1.0F, 1.0F, 5.87220972F);
float vector[3];
void glVertex3fv(vector);
Para simplificar nos referiremos a estas rutinas como glVertex*.
OpenGL interpreta cualquier secuencia de vértices segú:n su contexto. El contexto se declara mediante el par de rutinas glBegin(GLenum mode) y glEnd(), toda sentencia glVertex* ejecutada entre estas dos se interpreta según el valor de mode, por ejemplo: glBegin(GL_POINTS);
glVertex2f(0.0, 0.0);
glVertex2f(1.0, 0.0);
glVertex2f(0.0, 1.0);
glVertex2f(1.0, 1.0);
glVertex2f(0.5, 0.5);
glEnd();
dibuja 5 puntos en 2D con las coordenadas especificadas. GL_POINTS es una de las etiquetas definidas en el fichero cabecera de OpenGL <GL/gl.h>, existen muchos otros modos disponibles, pero los veremos cuando sea necesario.
Cada punto se dibuja con el color actualmente guardado en la variable de estado de OpenGL asociada con el buffer de color. Para cambiar el color actual, usaremos la familia de rutinas glColor*; hay mucho que decir sobre la selección y manipulación de colores, habrá un artículo solo para esto. De momento podemos utilizar tres números en coma flotante entre 0.0 y 1.0. Es el codificado RGB (rojo-verde-azul) glColor3f(1.0, 1.0, 1.0); /* Blanco */
glColor3f(1.0, 0.0, 0.0); /* Rojo */
glColor3f(1.0, 1.0, 0.0); /* Magenta */
etc...
Descarga:
../../common/January1998/Makefile
../../common/January1998/../../common/January1998/example1.c
../../common/January1998/../../common/January1998/example2.c
Ya tenemos suficiente material para escribir nuestros dos primeros ejemplos de código. El primer ejemplo es un simple programa en OpenGL que dibuja un número de órbitas de una transformación caótica (la transformación estandar). Si el lector no está familiarizado con transformaciones y con la transformación estandar en particular, no importa. Dicho sencillamente, la transformación toma un punto y genera uno nuevo usando una fórmula definida como:
yn+1 = yn + K sin(xn)
xn+1 = xn + yn+1
en el caso de la transformación estandar, representa un modelo de la traza dejada por una partícula cargada que gira alrededor del toro de un acelerador de partículas y cruza una sección plana del acelerador. Estudiar las propiedades de esta y otras transformaciones es importante en física porque nos ayuda a entender la estabilidad de las partículas cargadas confinadas en el ciclotrón. La transformación estandar está muy bien porque, para algunos valores de su parámetro K, muestra claramente una mezcla de movimiento caótico y movimiento orbital. Incluso aquellos que no están interesados en la física pero quieren desarrollar código para gráficos deberán prestar atención a las transformaciones y sus propiedades, muchos de los algortimos para generar texturas, llamas de fuegos, árboles, tierra, etc... se basan en transformaciones fractales.
Este es el código de ../../common/January1998/../../common/January1998/example1.c:
#include <GL/glut.h>
#include <math.h>
const double pi2 = 6.28318530718;
void NonlinearMap(double *x, double *y){
static double K = 1.04295;
*y += K * sin(*x);
*x += *y;
*x = fmod(*x, pi2);
if (*x < 0.0) *x += pi2;
};
void winInit(){
/* Poner sistema de coordenadas */
gluOrtho2D(0.0, pi2, 0.0, pi2);
};
void display(void){
const int NumberSteps = 1000;
const int NumberOrbits = 100;
const double Delta_x = pi2/(NumberOrbits-1);
int step, orbit;
glColor3f(0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
for (orbit = 0; orbit < NumberOrbits; orbit++){
double x, y;
y = 3.1415;
x = Delta_x * orbit;
glBegin(GL_POINTS);
for (step = 0; step < NumberSteps; step++){
NonlinearMap(&x, &y);
glVertex2f(x, y);
};
glEnd();
};
for (orbit = 0; orbit < NumberOrbits; orbit++){
double x, y;
x = 3.1415;
y = Delta_x * orbit;
glBegin(GL_POINTS);
for (step = 0; step < NumberSteps; step++){
NonlinearMap(&x, &y);
glVertex2f(x, y);
};
glEnd();
};
};
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);
glutInitWindowPosition(5,5);
glutInitWindowSize(300,300);
glutCreateWindow("Standard Map");
winInit();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
Léete el artículo Programando GLUT para entender las rutinas glut*, la mayor parte de este código viene de ahí. La ventana gráfica se abre en modo buffer simple y RGB. Entonces una función callback llamada display() dibuja la transformación: primero seleccionamos el color negro para el fondo y glClear(GL_COLOR_BUFFER_BIT) pone el buffer de color al color actual (negro), a continuación, después de seleccionar el color blanco con glColor, ejecutamos unas cuantas veces la función NonlinearMap() y dibujamos los puntos con glVertex* en modo GL_POINTS. Realmente simple.
Fíjate con en la rutina de inicialización de la ventana winInit() hay una única instrucción del toolkit de utilidades de OpenGL, gluOrtho2D(). Esta rutina pone un sistema de coordenadas 2D ortogonal. Los parámetros que recibe son "mínimo x, máximo x, mínimo y, máximo y".
He elgido una ventana en modo simple y un gran número de puntos para que puedas ver la imagen mientras es dibujada. Esto es habitual en modo simple con imágenes grandes y que tarden en calcularse, los dibujos aparecen en pantalla tal y como van siendo generados por las rutinas de OpenGL.
Después de ejecutar ../../common/January1998/example1 deberías ver esta imagen:
Vayamos al segundo programa, ../../common/January1998/../../common/January1998/example2.c:
#include <GL/glut.h>
#include <math.h>
const double pi2 = 6.28318530718;
const double K_max = 3.5;
const double K_min = 0.1;
static double Delta_K = 0.01;
static double K = 0.1;
void NonlinearMap(double *x, double *y){
/* Transformación estandar */
*y += K * sin(*x);
*x += *y;
/* El ángulo x es módulo 2Pi */
*x = fmod(*x, pi2);
if (*x < 0.0) *x += pi2;
};
/* Función callback:
Qué hacer en ausencia de entradas */
void idle(void){
/* Incrementar el parámetro estocástico */
K += Delta_K;
if(K > K_max) K = K_min;
/* Redibujar el display */
glutPostRedisplay();
};
/* Inicialización de la ventana gráfica */
void winInit(void){
gluOrtho2D(0.0, pi2, 0.0, pi2);
};
/* Función callback:
Qué hacer cuando el display se ha de redibujar */
void display(void){
const int NumberSteps = 1000;
const int NumberOrbits = 50;
const double Delta_x = pi2/(NumberOrbits-1);
int step, orbit;
glColor3f(0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
for (orbit = 0; orbit < NumberOrbits; orbit++){
double x, y;
y = 3.1415;
x = Delta_x * orbit;
glBegin(GL_POINTS);
for (step = 0; step < NumberSteps; step++){
NonlinearMap(&x, &y);
glVertex2f(x, y);
};
glEnd();
};
for (orbit = 0; orbit < NumberOrbits; orbit++){
double x, y;
x = 3.1415;
y = Delta_x * orbit;
glBegin(GL_POINTS);
for (step = 0; step < NumberSteps; step++){
NonlinearMap(&x, &y);
glVertex2f(x, y);
};
glEnd();
};
glutSwapBuffers();
};
int main(int argc, char **argv) {
/* Inicializaciones de GLUT */
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(5,5);
glutInitWindowSize(300,300);
/* Abrir ventana*/
glutCreateWindow("Order to Chaos");
/* Inicialización de la ventana */
winInit();
/* Registrar funciones callback */
glutDisplayFunc(display);
glutIdleFunc(idle);
/* Iniciar el proceso de eventos */
glutMainLoop();
return 0;
}
Este programa se basa en ../../common/January1998/../../common/January1998/example1.c, la principal diferencia es que la ventana se abre en modo de doble buffer, y el parámetro K de la transformación es una variable que cambia durante la ejecución del programa. Hay una nueva función callback idle() registrada para el procesador de eventos de GLUT por glutIdleFunc().Esta función tiene un significado especial, es ejecutada por el procesador de eventos en todo momento que no hay entrada del usuario. La función callback idle() es ideal para animaciones de programas. En ../../common/January1998/example2, se utiliza para cambiar ligeramente el valor del parámetro de la transformación. Al final de idle() hay otra función de GLUT útil, glutPostResDisplay() que redibuja la ventana conservando la inicializaciones anteriores, en general es más eficiente que llamar a display() de nuevo.
Otra diferencia a señalar es el uso de glutSwapBuffers() al final de display(). La ventana se inició en modo doble buffer por lo que todas las directivas de trazado se aplican al buffer oculto, el usuario no ve cómo se dibuja la imagen en este caso. Cuando se ha finalizado la imagen completa (frame), entonces se hace visible intercambiando los buffers visible e invisible con glutSwapBuffers(). Sin esta técnica la animación no iría suave.
Estas son algunas imágenes que se ven durante la animación:
IMPORTANTE: LA función callback display() siempre se invoca al menos una vez antes que idle(). Recuerda esto cuando escribas tus animaciones y decidas qué va en display() y qué va en idle().
Descarga:
../../common/January1998/../../common/January1998/example3.c
Dibujando Líneas y Polígonos
Como hemos dicho antes, glBegin(GLenum mode) acepta varios modos, y la secuencia de vértices v0, v1,v2, v3,v4,... vn-1 declarados a continuación se interpreta acordemente. Los valores posibles para mode y su significado son:
- GL_POINTS Dibuja un punto en cada uno de los n vértices.
- GL_LINES Dibuja una serie de líneas no conectadas. Los segmentos se dibujan entre v0 y v1, v2 y v3,...etc. Si n is impar vn-1 se ignora.
- GL_POLYGON Dibuja un polígono usando v0, v1,..,vn-1 como vértices. n debe ser al menos 3 o entonces no se dibuja nada, además el polígono no se puede cortar a sí mismo y debe ser convexo (debido a limitaciones en los algoritmos de hardware).
- GL_TRIANGLES Dibuja una serie de triángulos usando los vértices v0, v1 y v2, luego v3, v4 y v5 etc. Si n no es un multiplo de 3 los puntos sobrantes se ignoran.
- GL_LINE_STRIP Dibuja una línea desde v0 hasta v1, luego otra desde v1 hasta v2 y así sucesivamente. La última va desde vn-2 hasta vn-1, siendo un total de n-1 segmentos de línea. No hay restricciones en los vértices que describen una tira de líneas, las líneas pueden intersecarse arbitrariamente.
- GL_LINE_LOOP Lo mismo que GL_LINE_STRIP excepto que al final se dibuja un segmento de línea desde vn-1 hasta v0, cerrando el lazo.
- GL_QUADS Dibuja una serie de cuadriláteros usando los vértices v0, v1, v2, v3 y v4, v5, v6, v7 y así sucesivamente.
- GL_QUAD_STRIP Dibuja una serie de cuadriláteros usando los vértices v0, v1, v3, v2 y luego v2, v3, v5, v4 y así sucesivamente.
- GL_TRIANGLE_STRIP Dibuja una serie de triángulos usando los vértices en el orden siguiente: v0, v1, v2, luego v2, v1, v3, luego v2, v3, v4, etc. El orden es para asegurar que los triángulos tienen la orientación correcta y la tira se puede usar para formar parte de una superficie.
- GL_TRIANGLE_FAN Similar a GL_TRIANGLE_STRIP excepto que los triángulos son v0, v1, v2, luego v0, v2, v3, luego v0, v3, v4, etc. Todos los triángulos tienen v0 como vértice común.
En nuestro tercer ejemplo, otra animación, hacemos uso de GL_LINES y GL_POLYGON. Compila el programa, mírate el código fuente y observa cómo funciona. Es básicamente muy similar a ../../common/January1998/../../common/January1998/example2.c, ahora la imagen dibujada es un péndulo muy simple. La animación simula el movimiento de un péndulo ideal. Esto es una fotografía de la animación:
Como antes, hay una función callback idle() cuya misión aquí es mantener el reloj funcionando (actualizando la variable time). La función display() dibuja dos objetos, la cuerda del péndulo y su peso (en blanco y rojo respectivamente). El movimiento de las coordenadas del péndulo está implícito en las fórmulas de xcenter y ycenter:
void display(void){
static double radius = 0.05;
const double delta_theta = pi2/20;
double xcenter , ycenter;
double x, y;
double theta = 0.0;
double current_angle = cos(omega * time);
glColor3f(0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
/* Dibujar la cuerda del péndulo */
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_LINES);
glVertex2f(0.0, 0.0);
xcenter = -cord_length * sin(current_angle);
ycenter = -cord_length * cos(current_angle);
glVertex2f(xcenter, ycenter);
glEnd();
/* Dibujar el disco del péndulo */
glColor3f(1.0, 0.0, 0.0);
glBegin(GL_POLYGON);
while (theta <= pi2) {
x = xcenter + radius * sin(theta);
y = ycenter + radius * cos(theta);
glVertex2f(x, y);
theta += delta_theta;
};
glEnd();
glutSwapBuffers();
};
Ejercicios
Aquí te damos algunas sugerencias para que practiques lo que has aprendido hasta ahora:
- En ../../common/January1998/../../common/January1998/example1.c prueba otras transformaciones. Vete a la biblioteca y coge cualquier libro sobre Caos y Fractales, allí encontrarás muchos ejemplos. Experimenta cambiando los parámetros, sistema de coordenadas, aplicando varias transformaciones consecutivamente antes de dibujar los puntos. Diviértete con ello.
- En ../../common/January1998/../../common/January1998/example2.c puedes añadir colores a cada punto. Por ejemplo, una código de color muy interesante sería asignar a cada punto un color según la estabilidad local de la órbita (Physics Review Letters Vol 63, (1989) 1226), cuando la trayectoria va hacia una región caótica, se vuelve más roja, por ejemplo, mientras que islas casi estables son más azules. Si haces este efecto, se verá más clara la naturaleza fractal de la transformación de nuestro ejemplo. Es un poco avanzado para aquellos de vosotros que no hayais hecho nada de ecuaciones diferenciales, pero es interesante aprenderlo si quereis aprender cómo utilizar transformaciones y fractales en vuestros gráficos por ordenador.
- En ../../common/January1998/../../common/January1998/example3.c , prueba a cambiar el tipo de línea usado para dibujar el disco. Usa GL_LINES, GL_TRIANGLES, etc. Mira lo que ocurre. Prueba a optimizar la generación del disco, no es necesario evaluar tantos senos y cosenos para dibujar el mismo disco en cada imagen, puedes guardarlo en una matriz. Usando polígonos, prueba a poner cajas, diamantes, o cualquier cosa al final del péndulo. Dibuja dos péndulos por imagen, moviéndoso independientemente o incluse chocando entre ellos.
Proximamente....
Esto es todo de momento. Hay todavía muchas cosas a discutir sobre polígonos. En el próximo número (Marzo 1998) continuaremos explorando los polígonos, modelado y estudiaremos más detalles sobre algunos de los comandos que ya nos son familiares.
|