Gràfics per computador
(Curs 04-05)
Departament de Sistemas Informàtics i Computació
Escola Politècnica Superior d’Alcoi
Enginyeria Tècnica en Informàtica de Gestió
PRÀCTICA 2
Jordi Linares i Pellicer
jlinares@[Link]
Pág. 1 de 44
1 Descripción y objetivos ............................................................................................ 3
2 Introducción.............................................................................................................. 3
2.1 OpenGL. Un estándar ....................................................................................... 3
2.2 Características básicas de OpenGL .................................................................. 3
2.3 GLUT ............................................................................................................... 4
3 Nuestra primera aplicación OpenGL........................................................................ 4
3.1 Ficheros asociados............................................................................................ 5
3.2 Esqueleto de la aplicación ................................................................................ 5
3.3 Breve explicación de la aplicación ................................................................. 10
4 Profundizando en OpenGL..................................................................................... 13
4.1 Sintaxis y tipos de datos ................................................................................. 13
4.2 Dibujando elementos geométricos ................................................................. 14
4.2.1 Borrar la ventana .................................................................................... 14
4.2.2 Especificar un color ................................................................................ 15
4.2.3 Volcar el último paquete de dibujo ........................................................ 15
4.2.4 Descripción de puntos, líneas y polígonos ............................................. 15
4.2.5 Cómo especificar los vértices de un objeto ............................................ 16
4.2.6 Dibujo de primitivas geométricas........................................................... 16
4.2.7 Restricciones al usar glBegin() y glEnd()............................................... 17
4.2.8 Dibujo de puntos, líneas y polígonos ..................................................... 17
5 Interacción con GLUT............................................................................................ 19
5.1 Interacción. Control de la ejecución ............................................................... 19
5.2 Iniciando ventanas y bucle principal de visualización ................................... 19
5.3 Eventos de ventana, teclado y ratón ............................................................... 21
5.3.1 Eventos de ventana ................................................................................. 21
5.3.2 Eventos de teclado .................................................................................. 22
5.3.3 Eventos de ratón ..................................................................................... 22
5.4 Gestión de menús ........................................................................................... 23
6 Desarrollo de la práctica ......................................................................................... 24
6.1 Almacenamiento de la información................................................................ 25
6.2 Aplicación básica............................................................................................ 28
6.3 Tareas iniciales a resolver............................................................................... 37
6.4 Matrices de transformación en dos dimensiones............................................ 38
6.5 Últimas tareas a resolver................................................................................. 42
Pág. 2 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
1 Descripción y objetivos
El primer objetivo de esta primera práctica consiste en presentar la librería OpenGL y su
librería de apoyo GLUT, describiendo sus más básicas características y aprendiendo
cómo debemos generar con VC++ 5.0 una aplicación OpenGL.
El núcleo principal de la práctica se centrará en el desarrollo de una sencilla herramienta
de dibujo 2D basada en el uso de OpenGL y GLUT.
2 Introducción
2.1 OpenGL. Un estándar
OpenGL es, sin lugar a dudas, la API que prevalece en la industria para desarrollar
aplicaciones gráficas. Se le puede considerar el sucesor a la formidable IRIS GL-library
de Silicon Graphics, que hizo tan popular las estaciones de trabajo SGI como
plataforma predilecta para desarrollo científico, de ingeniería y de efectos especiales.
Varios fabricantes de software y hardware tomaron parte en la especificación de
OpenGL y permanecen detrás suyo. Gracias a esto podemos decir que las aplicaciones
OpenGL pueden ser fácilmente portadas a virtualmente cualquier plataforma del
mercado, desde Windows, Linux, pasando por estaciones UNIX de alto nivel y
mainframes. La Architectural Review Board (Junta de Revisión de Arquitecturas)
gobierna sobre OpenGL, sus especificaciones, acepta o rechaza cambios y propone tests
de conformidad.
OpenGL es, en su diseño, independiente de plataformas y sistemas operativos. Es
perceptiva a la red, de manera que es posible separar nuestra aplicación OpenGL en un
servidor y un cliente.
2.2 Características básicas de OpenGL
OpenGL es un software de interfase al hardware gráfico. Esta interfase (o API) contiene
aproximadamente unas 120 funciones. Estas funciones se usan para definir objetos o
modelos y para producir aplicaciones gráficas 3D interactivas.
OpenGL está diseñado para funcionar eficientemente incluso si el ordenador que
visualiza los gráficos no es el ordenador que ejecuta la aplicación (cliente-servidor).
Incluso cliente y servidor pueden ser de plataformas distintas. De hecho, OpenGL está
diseñado para ser independiente de hardware y plataforma (lo podemos encontrar en
Unix, Linux, Mac, Windows…)
Para asegurar esta independencia, no encontraremos en OpenGL funciones para la
gestión de ventanas o interacción con el usuario. Por ejemplo, en Windows, podremos
crear un contexto de dispositivo capaz de ejecutar funciones OpenGL. Una alternativa a
utilizar código dependiente de plataforma, consiste en usar GLUT. Describiremos
GLUT en el siguiente apartado.
OpenGL tampoco proporciona funciones de alto nivel para la descripción de modelos
tridimensionales; debemos construir nuestros modelos a partir de un pequeño conjunto
de primitivas geométricas: puntos, líneas y polígonos. Existen librerías, como
OpenInventor, OSG, OpenSG etc. que constituyen una capa adicional sobre OpenGL, y
Pág. 3 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
que proporcionan funciones de alto nivel para tareas de modelado y configuración de la
escena como una jerarquía.
Algunas de las cosas que con OpenGL podemos llevar a cabo son:
• Visualizar nuestros modelos en alámbrico (wireframe)
• Visualizar nuestros modelos como objetos sólidos de un único color
• Visualizar nuestros modelos sombreados en función de las fuentes de luz de la
escena
• Utilizar técnicas de mapeado de texturas, con las que aumentar significativamente el
realismo de la visualización
• Modelar efectos atmosféricos (como la niebla)
• Modelar efectos como profundidad de campo
• Etc.
2.3 GLUT
Para hacer OpenGL verdaderamente portable e independiente de la plataforma fue
necesario sacrificar todos los comandos que interactuaban con el sistema de ventanas,
como por ejemplo: abrir una ventana, cerrar una ventana, escalar una ventana, dar forma
a una ventana, leer la posición del cursor; y también con los dispositivos de entrada
como la lectura del teclado etc. Se deja por tanto al desarrollador de OpenGL conocer su
propia plataforma y tomar medidas para manejar ventanas en la plataforma nativa.
Gracias a Mark J. Kilgard de SGI hay una biblioteca complementaria a OpenGL que
evita este problema. Mark esciribió la GLUT-library, un conjunto de herramientas y
utilidades que sustituyen a la antigua biblioteca AUX, desarrollada para dar soporte a la
guía oficial de OpenGL (libro fundamental para un correcto aprendizaje de OpenGL).
La biblioteca GLUT está disponible libremente y se puede encontrar código fuente para
ella así como versiones binarias para prácticamente cualquier sistema operativo.
La biblioteca GLUT ofrece un paradigma común para el sistema de ventanas y
dispositivos periféricos. Así pues, cuando una aplicación OpenGL quiere abrir una
ventana para una animación gráfica, puede usar el conjunto de comandos GLUT y éste
toma el control del sistema de ventanas subyacente. De algún modo GLUT oculta al
desarrollador los detalles sucios de los sistemas de ventanas específicos (X11,
Windows, Motif, etc.) y le deja concentrarse en la tarea principal, el código OpenGL.
Por supuesto su uso no es obligatorio. Siempre podemos recurrir al SDK de, en nuestro
caso, Windows para la gestión de ventanas y la E/S con el usuario.
Otra ventaja de usar GLUT es que hace el código independiente de la plataforma. Una
aplicación desarrollada con OpenGL+GLUT es fácilmente portable a cualquier
plataforma; incluso sin tener muchas veces que cambiar una línea del código fuente.
3 Nuestra primera aplicación OpenGL
Vamos a abordar nuestra primera aplicación en OpenGL y cómo desarrollarla con
VC++.
Pág. 4 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
3.1 Ficheros asociados
Para poder crear y compilar una aplicación OpenGL, debemos localizar los diferentes
ficheros (librerías y cabeceras) que precisan VC++ y su entorno. Estos ficheros
dependen también de la versión del Visual Studio empleado. Los ficheros aquí descritos
son los necesarios para la versión 5.0. Para versiones posteriores, como la 6.0 y Visual
Studio .Net, los ficheros son distintos y se suministrarán también para los alumnos
interesados.
Nuestras aplicaciones van a hacer uso de, por supuesto OpenGL, y de GLUT. Además
utilizaremos también otra librería, la librería de utilidades o GLU (proporcionada como
parte de OpenGL, y que nos facilita la creación de matrices de proyección, visualización
de superficies etc.).
Los ficheros de OpenGL y GLU los encontraremos, generalmente y si Bill Gates no
decide lo contrario, tras la instalación de VC++. La librería GLUT y sus ficheros
correspondientes serán proporcionados junto a la práctica.
Ficheros implicados:
Cabecera Librería DLL
gl.h [Link] [Link]
glu.h [Link] [Link]
glut.h [Link] [Link]
[Link] [Link]
Los ficheros de cabecera, generalmente, los encontraremos en el directorio
..\DevStudio\VC\include\gl
Los ficheros .lib, generalmente, los encontraremos en el directorio
..\DevStudio\VC\lib
Los .dll, y en función del Windows que estemos utilizando, deben ubicarse en
C:\Windows\System ó C:\Windows\System32 (o en cualquier caso, en un
directorio de búsqueda por defecto en la localización de DLL’s en su carga por
Windows).
Excepto la librería GLUT (con sus ficheros asociados) el resto debería estar en las
ubicaciones anteriormente detalladas.
Por tanto, nuestro primer paso consistirá en copiar glut.h al directorio
..\DevStudio\VC\include\gl, los ficheros [Link] y [Link] al
directorio ..\DevStudio\VC\lib y finalmente los ficheros [Link] y
[Link] al directorio C:\Windows\System ó C:\Windows\System32.
3.2 Esqueleto de la aplicación
Una vez tenemos la certeza de que los ficheros necesarios para el desarrollo de
aplicaciones OpenGL con GLUT están en sus ubicaciones adecuadas, a continuación
crearemos el esqueleto de la aplicación OpenGL.
Como vamos a utilizar GLUT para la gestión de ventanas y eventos, no será necesario
utilizar las MFC e incluso nada de C++. Por ello, desarrollaremos nuestra aplicación
como del tipo consola, es decir, la más sencilla.
Pág. 5 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
En el menú File, seleccionamos New e indicamos que deseamos una aplicación Win32
del tipo consola.
Con ello obtenemos un proyecto de aplicación que no tiene incluso ningún fichero
asociado.
A continuación vamos a asegurarnos que, al compilar nuestra aplicación, el entorno va a
encontrar la información necesaria para crear una aplicación OpenGL, es decir, asegurar
el acceso a las librerías oportunas.
Esto lo podemos llevar a cabo en el menú Project y seleccionando Settings.
Pág. 6 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Seleccionamos la solapa Link, y añadimos en Object/library modules, al final de la
lista y separados por un espacio, nuestros cuatro ficheros [Link] y que permitirán el enlace
de nuestra aplicación con las DLL’s.
A continuación, vamos a añadir un fichero a nuestro proyecto. Este fichero tendrá todo
el código de nuestra aplicación. Lo llamaremos “practica [Link]”.
1
No siempre va a ser necesario enlazar las cuatro librerías. Obviamente como mínimo la de OpenGL,
pero la GLU y la GLUT sólo en el caso en el que se vayan a utilizar. Por metodología, en la práctica
vamos a enlazarlas todas (y así se reflejará también en los include).
Pág. 7 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Esto lo hacemos también con File -> New, y en este caso hemos seleccionado C++
Source File y hemos seleccionado su nombre como “practica 2”. La opción Add
to Project debe estar activada.
Finalmente ya podemos teclear nuestra primera aplicación, compilarla y ejecutarla.
Pág. 8 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Este mismo proceso es el que debemos repetir cada vez que vayamos a generar una
nueva aplicación OpenGL.
A continuación, el código de nuestro primer programa OpenGL, que deberemos
introducir en el fichero “practica [Link]”.
// Primero incluimos las cabeceras necesarias
// para crear una aplicación OpenGL.
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glut.h>
void display(void)
{
/* borra la pantalla */
glClear(GL_COLOR_BUFFER_BIT);
/* dibuja un polígono blanco (rectángulo) con sus esquinas en
* (0.25, 0.25) y (0.75, 0.750)
*/
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_POLYGON);
glVertex2f(0.25, 0.25);
glVertex2f(0.75, 0.25);
glVertex2f(0.75, 0.75);
glVertex2f(0.25, 0.75);
glEnd();
/* Vacia el buffer de dibujo */
glFlush ();
}
void inicializa (void)
{
/* selecciona el color de borrado */
glClearColor (0.0, 0.0, 0.0, 0.0);
/* inicializa los valores de la vista */
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 1.0, 0.0, 1.0);
}
void main(int argc, char** argv)
{
// Inicializa la librería auxiliar GLUT
glutInit(&argc, argv);
// Inicializa el modo de visualización
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
// Indica el tamaño de la ventana (ancho,alto)
glutInitWindowSize(250, 250);
// Indica la posición inicial (xmin,ymin)
glutInitWindowPosition(100, 100);
Pág. 9 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
// Abre la ventana con el título indicado
glutCreateWindow("Primer programa con OpenGL");
// Inicializar valores
inicializa();
// Indica cual es la función de dibujo
glutDisplayFunc(display);
// Comienza el bucle de dibujo y proceso de eventos.
glutMainLoop();
3.3 Breve explicación de la aplicación
En primer lugar incluimos las cabeceras de las librerías que vamos a utilizar. Incluimos
la cabecera a las funciones del API de Windows, que, al menos de forma indirecta, van
a utilizarse2.
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glut.h>
La función main es el punto de entrada de nuestra aplicación (de cualquier aplicación
en C, en realidad). En ella, lo primero que hacemos es inicializar la librería GLUT, que
como ya hemos comentado, tiene como misión la gestión de ventanas y eventos.
// Inicializa la librería auxiliar GLUT
glutInit(&argc, argv);
Como podemos apreciar, las funciones GLUT tienen todas prefijo glut.
A continuación definimos el tipo de visualización que deseamos. Aquí es donde
podemos indicar el modo de color, si queremos o no doble buffer etc. En nuestra
aplicación inicial queremos un buffer simple y color RGB.
// Inicializa el modo de visualización
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
Ahora podemos crear una ventana capaz de visualizar los comandos OpenGL. Podemos
para ello, definir su tamaño y posición inicial, y finalmente crearla.
// Indica el tamaño de la ventana (ancho,alto)
glutInitWindowSize(250, 250);
// Indica la posición inicial (xmin,ymin)
glutInitWindowPosition(100, 100);
// Abre la ventana con el título indicado
glutCreateWindow("Primer programa con OpenGL");
GLUT nos permite la creación de varias ventanas y también de las denominadas sub-
ventanas. Las ventanas se crean con glutCreateWindow y actúan como ventanas
con su propio marco e independientes. Las sub-ventanas se crean con
2
La cabecera windows.h no será necesaria si se lleva este código a otras plataformas que, en su caso,
pueden exigir de otros enlaces.
Pág. 10 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
glutCreateSubWindow y son siempre hijas de ventanas. Veremos esto más
adelante.
La función inicializa se encarga de dos cosas. En primer lugar, seleccionar el color
de fondo (o el que se va a utilizar en la limpieza o borrado de la pantalla).
/* selecciona el color de borrado */
glClearColor (0.0, 0.0, 0.0, 0.0);
En segundo lugar, dentro de la función inicializa, establecemos el tipo y
características de la proyección que OpenGL va a utilizar para la visualización de
nuestro modelo. Esta primera aplicación visualiza un cuadrado sobre la pantalla.
Nuestro modelo es 2D, por tanto no precisamos del uso, en principio, de ninguna
proyección para nuestro modelo.
No obstante en OpenGL siempre hay proyección. Todo lo que modelamos en OpenGL
es de tres dimensiones. Incluso en esta sencilla aplicación, aunque el cuadrado lo
modelemos como un polígono de vértices en dos dimensiones, internamente estos
vértices son de tres dimensiones (con z=0).
Por tanto, y para visualizar información de modelos 2D, lo más cómodo es definir una
proyección paralela ortográfica. Esto mismo lo podemos llevar a cabo con la función
OpenGL glOrtho o, la más sencilla en nuestro caso, la función de la librería de
utilidades gluOrtho2D.
Como se ha visto en la parte teórica de la asignatura, las transformaciones geométricas
son de suma importancia en gráficos por computador, y su implementación como
matrices una metodología que simplifica enormemente el proceso de composición de
transformaciones. Esto ha provocado que OpenGL disponga de muchas funciones para
la creación y gestión de matrices de transformación.
Aunque lo veremos con más detalle posteriormente, todo elemento a ser visualizado es
transformado en primer lugar por una matriz modelo / vista (con objeto de definir la
posición de los objetos frente a la cámara) y en segundo lugar por una matriz de
proyección. Estas dos matrices son internas en OpenGL y pueden modificarse tras su
selección con glMatrixMode y la posterior llamada a funciones como
gluOrtho2D.
/* inicializa los valores de la vista */
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 1.0, 0.0, 1.0);
Concretamente, en la primera línea, hemos seleccionado la matriz de proyección con
objeto de modificarla. Con glLoadIdentity() hacemos que la matriz seleccionada,
la de proyección, pase a ser la matriz unidad (elemento neutro en el producto de
matrices).
Finalmente con gluOrtho2D, hacemos que la matriz seleccionada, la de proyección,
se convierta en una matriz de proyección paralela ortográfica, donde se define un
volumen de vista de z = -1.0 hasta z = 1.0, y con esquinas en x=0.0, y =0.0 hasta x=1.0,
y=1.0. Todo objeto que se trace en el interior de ese volumen se visualizará, en
consecuencia, sobre x=0.0, y=0.0 hasta x=1.0, y=1.0.
Pág. 11 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
A efectos prácticos, es como si hubiésemos definido una ventana en CMR como
(0.0, 0.0)-(1.0, 1.0) y como marco toda el área de la ventana Windows. Por tanto,
para dibujar en 2D con OpenGL basta usar glutOrtho2D como en el ejemplo y
lanzar nuestras primitivas de dibujo teniendo en cuenta que nuestras coordenadas
de mundo real están entre (0.0,0.0) y (1.0, 1.0). El (0.0, 0.0) corresponde a la
esquina inferior izquierda.
Gran parte de nuestro trabajo con GLUT consistirá en informar qué función va a
controlar qué evento. Es decir, GLUT utiliza un mecanismo de asignación de funciones
del tipo CALLBACK (las llamaremos rutinas de servicio), definidas por el usuario, y
que darán respuesta a los eventos que el sistema operativo mande a la aplicación (como
redibujado de la ventana, cambio de tamaño de la ventana, entrada desde el teclado o el
ratón etc.)
// Indica cual es la función de dibujo
glutDisplayFunc(display);
// Comienza el bucle de dibujo y proceso de eventos.
glutMainLoop();
Con glutDisplayFunc indicamos qué función va a hacerse cargo del redibujado de
la ventana activa3. La función display, cuyo nombre y contenido es cosa totalmente
nuestra, es la función que va a invocarse siempre que se requiera de un redibujado de la
ventana (en pocas palabras, cuando se envía un WM_PAINT a la ventana). Esta
necesidad de redibujado se da al inicio, o cuando una ventana que estaba encima se
cambia de posición dejando al descubierto un hueco a redibujar etc.
Por último, dentro del main, se invoca a la función GLUT glutMainLoop() que
básicamente activa las ventanas con sus funciones CALLBACK correspondientes.
Respecto a la función display, su cometido es el de dibujar nuestro modelo. Para
ello, OpenGL dispone de un conjunto de primitivas muy básico.
/* borra la pantalla */
glClear(GL_COLOR_BUFFER_BIT);
/* dibuja un polígono blanco (rectángulo) con sus esquinas en
* (0.25, 0.25) y (0.75, 0.750)
*/
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_POLYGON);
glVertex2f(0.25, 0.25);
glVertex2f(0.75, 0.25);
glVertex2f(0.75, 0.75);
glVertex2f(0.25, 0.75);
glEnd();
/* Vacia el buffer de dibujo */
glFlush();
3
Muchas de las funciones de GLUT se ejecutan sobre los elementos activos o actuales. En el caso de
ventanas, la ventana activa o actual es la última creada o podemos definir como actual cualquier ventana
con la instrucción glutSetWindow, que precisa como argumento el identificador de ventana. El
identificador de ventana lo podemos obtener almacenando el parámetro que devuelven las funciones de
creación de ventanas glutCreateWindow y glutCreateSubWindow; ambas devuelven el
identificador de la ventana creada.
Pág. 12 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Borramos en primer lugar el buffer de color (hay otros tipos de buffers en OpenGL) al
color de fondo especificado en la función inicializa.
Seleccionamos el color que vamos a utilizar para dibujar con glColor3f4. Indicamos
de esta forma un color RGB, donde el rango de cada canal va de 0.0 (mínima
intensidad) a 1.0 (máxima intensidad).
El dibujo en OpenGL se lleva a cabo entre las funciones glBegin y glEnd. En el
argumento de glBegin indicaremos la primitiva a dibujar.
En el interior de este par de funciones, definiremos la primitiva geométrica
especificando sus vértices significativos y sus propiedades (como color, coordenadas,
coordenadas de textura, normales etc.).
En nuestro caso, el dibujo es un polígono (un cuadrado) y con glVertex indicamos
sus cuatro vértices. El ‘2’ indica dos argumentos y la ‘f’ que son argumentos del tipo
float (reales). Muchas funciones de OpenGL admiten diferentes sufijos en función
del número de argumentos que se le pasa y su tipo.
Aunque definamos sólo las coordenadas x e y de los vértices, éstos son en realidad 3D.
No indicar la z implica z=0.0. En nuestro caso esto nos es muy útil, ya que hemos
definido un volumen de vista de z = -1.0 hasta z = 1.0 con la función gluOrtho2D.
4 Profundizando en OpenGL
4.1 Sintaxis y tipos de datos
OpenGL utiliza un conjunto de sufijos en los comandos que permiten indicar el tipo de
datos con el que trabaja, el número de parámetros de la llamada a la función, etc.
En la siguiente tabla se muestra el significado de los sufijos utilizados para especificar
tipos de datos:
4
OpenGL fuciona como una máquina de estados. Es decir, seleccionamos un modo o estado y éste
permanece hasta que se cambie. Un ejemplo es la función glColor, una vez seleccionado el color de
dibujo, éste permanece seleccionado (estado actual) hasta que se cambie.
Pág. 13 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Sufijo Tipo de datos Tipo de datos en C Definición en OpenGL
b Entero de 8 bits signed char GLbyte
s Entero de 16 bits short GLshort
i Entero de 32 bits long GLint, GLsizei
f Coma flotante de 32 bits float GLfloat, GLclampf
d Coma flotante de 64 bits double GLdouble, GLclampd
ub Entero sin signo de 8 bits unsigned char GLubyte, GLboolean
us Entero sin signo de 16 bits unsigned short GLushort
ui Entero sin signo de 32 bits unsigned long GLuint, GLenum,
GLbitfield
En nuestro primer programa, vemos como las funciones gl, terminan con un sufijo
compuesto por el número de argumentos y el tipo de los mismos.
Algunos comandos permiten el uso del sufijo v para indicar que el parámetro de la
llamada a la función es un puntero a un vector. Por ejemplo:
glColor3f(1.0, 0.0, 0.0);
float color_array[] = {1.0, 0.0, 0.0}; glColor3fv(color_array);
Es conveniente, por tanto, ceñirse al uso de los tipos OpenGL. Aún no siendo éstos
obligatorios (podríamos usar los de C), nos garantizaran la portabilidad a otras
plataformas.
4.2 Dibujando elementos geométricos
Vamos a ver a continuación algunas de las operaciones más básicas en OpenGL.
4.2.1 Borrar la ventana
Antes de iniciar un nuevo dibujo es necesario borrar la pantalla utilizando un color de
fondo.
Por otra parte, en muchas máquinas, el hardware gráfico consta de múltiples buffers de
memoria además del que contiene los colores de los píxeles que aparecen en la pantalla.
Es conveniente disponer de un comando que permita seleccionar qué buffer ha de
borrarse, o que combinación de ellos.
Por ejemplo, estas líneas borran la pantalla dejándola de color negro:
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
La primera línea especifica el color de borrado5. La segunda hace que se borre la
ventana.
5
Especificamos 4 valores correspondientes a RGBA. Los valores RGB ya los conocemos, el valor de A
es el del canal de transparencias (alfa channel).
Pág. 14 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Esta es la sintaxis de los comandos anteriores:
void glClearColor(GLclampf red, GLclampf green, GLclampf blue,
GLclampf alpha);
void glClear(GLbitfield mask);
4.2.2 Especificar un color
En OpenGL, la descripción de la forma de un objeto es independiente de la descripción
de su color. En general, el programador especifica un color como color actual de dibujo,
y después dibuja el objeto. Mientras no se especifique otro color, todos los objetos que
se dibujen utilizarán el actual. Por ejemplo, el siguiente comando fija el color de dibujo
a rojo:
glColor3f(1.0, 0.0, 0.0);
La sintaxis es la siguiente:
void glColor3f(GLclampf red, GLclampf green, GLclampf blue);
4.2.3 Volcar el último paquete de dibujo
Trabajando en red, no se envía un paquete por cada comando que se ejecute,
normalmente se envían grupos de comandos. Para que en nuestra pantalla aparezca por
completo todo lo que se ha mandado a dibujar, OpenGL incluye el comando
glFlush(), que fuerza al cliente a enviar el paquete aunque no esté lleno. Si
glFlush() no es suficiente porque además se necesita recibir notificación de que se
ha terminado de dibujar, entonces hay que utilizar glFinish(). En nuestro caso, y a
efecto prácticos, utilizaremos glFlush().
Esta es la sintaxis de los comandos anteriores:
void glFlush(void);
void glFinish(void);
4.2.4 Descripción de puntos, líneas y polígonos
Vamos a ver como describir primitivas geométricas en OpenGL.
Punto: un punto queda definido por sus coordenadas en tres dimensiones. Cuando un
punto se utiliza para definir líneas o polígonos, recibe el nombre de vértice.
Líneas: llamadas segmentos, pues no son de longitud infinita. Una línea se define como
la porción de recta comprendida entre dos vértices.
Polígonos: área cerrada formada por segmentos, teniendo en cuenta las siguientes
limitaciones:
1. Los segmentos que forman el polígono no pueden cortarse entre sí.
2. El polígono resultante debe ser convexo.
3. No pueden describirse agujeros en el interior de un polígono.
Sin embargo, no hay restricción en cuanto al número de lados.
Rectángulos: Para dibujar un rectángulo es preferible utilizar el comando glRect(),
mucho más eficiente que el comando general para dibujar polígonos.
Pág. 15 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
La sintaxis de este comando es:
void glRect{sifd}(TYPExl, TYPEyl, TYPEx2, TYPEy2);
void glRect{sifd}v(TYPE*vl, TYPE*v2);
Curvas: se dibujan por aproximación utilizando segmentos pequeños. De igual forma,
las superficies curvas se aproximan mediante polígonos pequeños (habitualmente
triángulos o cuadriláteros)
4.2.5 Cómo especificar los vértices de un objeto
Todos los objetos de OpenGL se describen como un conjunto ordenado de vértices.
Para indicar cada uno de ellos se utiliza el comando glVertex(), cuya sintaxis es la
siguiente:
void glVertex{234}{sfid}[v] (TYPEcoords);
Para indicar el principio y el final del conjunto de vértices utilizaremos los comandos
glBegin() y glEnd(), tal y como veremos a continuación.
4.2.6 Dibujo de primitivas geométricas
Una vez conocida la forma de especificar vértices, veremos como crear un conjunto de
puntos, una línea o un polígono a partir de dichos vértices. Veamos un ejemplo para un
polígono:
glBegin(GL POLYGON);
glVertex2f(0.0, 0.0);
glVertex2f(0.0, 3.0);
glVertex2f(3.0, 3.0);
glVertex2f(4.0, 1.5);
glVertex2f(3.0, 0.0);
glEnd() ;
En la siguiente tabla, se muestran los posibles valores que pueden pasarse como
parámetro del comando glBegin() y el correspondiente tipo de primitiva que se
generará como resultado:
Pág. 16 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
4.2.7 Restricciones al usar glBegin() y glEnd()
Entre un comando glBegin() y su glEnd() correspondiente, además de
coordenadas de los vértices que definen el objeto, puede indicarse información adicional
como el color del vértice, su normal asociada, o su relación con las coordenadas de
textura. Los comandos utilizados para proporcionar esta información adicional son los
siguientes:
glVertex*() coordenadas
glColor*() color
glIndex*() índice de color
glNormal*() vector normal
glEvalCoord*() genera coordenadas
glCallList*() ejecuta las display list
glTexCoord*() coordenadas de textura
glEdgeFlag*() control dibujo de aristas
glMaterial*() material
No se puede incluir ningún otro tipo de comando entre glBegin() y glEnd(). Sin
embargo, si se puede incluir código de programación. La restricción sólo afecta a
comandos OpenGL.
4.2.8 Dibujo de puntos, líneas y polígonos
Por defecto, un punto se dibuja como un píxel, una línea se dibuja de forma sólida y de
un píxel de ancho, y los polígonos se dibujan rellenos. A continuación veremos como
cambiar estas características por defecto.
Pág. 17 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Sobre los Puntos
Para controlar el tamaño de un punto se utiliza el comando glPointSize(), con un
único argumento que indica el ancho en píxeles. Su sintaxis es la siguiente:
void glPointSize(GLfloat size);
Sobre las Líneas
Se puede especificar tanto el ancho de la línea como el patrón de dibujo (puntos,
guiones o combinaciones de ambos). Veremos cómo cambiar el ancho.
Respecto al ancho
Se utiliza el comando glLineWidth() con un único parámetro similar al analizado
para los puntos. Su sintaxis es la siguiente:
void glLineWidth(GLfloat size);
Sobre los Polígonos
Por defecto los polígonos se dibujan con borde y rellenos del color de dibujo, pero no
tiene porque ser así. Vamos a ver las posibilidades.
Un polígono tiene dos caras y puede dibujarse de forma distinta dependiendo de la cara
que veamos. Por defecto ambas caras se dibujan de la misma forma. Para cambiar este
comportamiento por defecto, así como para dibujar únicamente la frontera o los vértices
del polígono, utilizaremos el comando glPolygonMode(), cuya sintaxis es:
void glPolygonMode(GLenum face, GLenum mode);
El parámetro face puede tener los siguientes valores: GL_FRONT, GL_BACK o
GL_FRONT_AND_BACK, indicando a cual de las dos caras del polígono afectará la
orden; el parámetro mode puede tomar los valores GL_POINT, GL_LINE o GL_FILL
para dibujar los vértices, la frontera o el interior respectivamente.
Pág. 18 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
5 Interacción con GLUT
5.1 Interacción. Control de la ejecución
La interacción entre la aplicación y el usuario es otra de las tareas importantes en el
desarrollo de aplicaciones gráficas. Operaciones como hacer clic con el ratón sobre un
botón, o señalar un píxel para determinar las coordenadas de inicio de una línea son
ejemplos de este tipo de tareas.
GLUT da respuesta a los diferentes eventos (redibujado, cambio del tamaño de la
ventana, pulsación de los botones del ratón o su movimiento, pulsaciones de teclas …)
permitiéndonos especificar rutinas de servicio que den respuesta a estos eventos.
5.2 Iniciando ventanas y bucle principal de visualización
La tarea interactiva más simple que implementa GLUT es permitir que se controle la
ejecución de la aplicación gráfica mediante un bucle principal de visualización, que
mantiene las ventanas en pantalla hasta que el usuario las cierre.
El bucle principal de visualización espera a que se produzcan eventos de usuario (mover
o cambiar el tamaño de una ventana, pulsar un botón del ratón o pulsar una tecla), y
cada vez que se produce un evento, lo atiende.
Cómo ya hemos visto en el ejemplo anterior, la creación de una ventana en GLUT es un
proceso muy simple.
En primer lugar iniciamos GLUT con la llamada glutInit a la que pasamos los
argumentos que recibimos en main:
glutInit(&argc, argv);
El siguiente paso tiene como objetivo definir las características de los buffers que
vamos a utilizar. Aquí podríamos definir algunas de las características que OpenGL nos
permite, como doble-buffer, z-buffer, transparencias etc. Debido al uso más avanzado
de estas características, no conformaremos con la siguiente llamada:
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
que nos define un buffer simple con valores RGB de color.
A continuación definimos posición inicial de la ventana que vamos a crear y su tamaño:
glutInitWindowSize(250, 250);
glutInitWindowPosition(100, 100);
glutCreateWindow("Nombre que queramos darle a la ventana");
Como se comentó anteriormente, GLUT permite la creación de varias ventanas
asociadas a la aplicación. La utilización de varias ventanas facilita el desarrollo de
interfases. Por ejemplo, es habitual definir como ventanas diferenciadas los botones de
la aplicación, cajas de herramientas, ventanas flotantes con información adicional, la
propia área de dibujo de la aplicación etc. Dividiendo los elementos a visualizar y con
los que se quiere interactuar en ventanas separadas, facilita no sólo la interacción del
usuario con la aplicación sino también la implementación de la interfase.
Veamos con algo más de detalle las funciones de creación de ventanas y sub-ventanas.
int glutCreateWindow(char * nombre);
Pág. 19 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
La función glutCreateWindow crea un ventana de alto nivel o ventana de tipo
principal (con marco y posibilidad de cambio de tamaño por el usuario). La función
devuelve un identificador de ventana que nos permite hacer referencia a la misma.
Tras la ejecución de esta función, la ventana creada queda como ventana activa, lo que
implica que, hasta que no haya cambio al respecto, las funciones GLUT que afectan a la
ventana actual, lo harán sobre esta última ventana creada.
El nombre será el que aparecerá sobre el marco de la ventana.
int glutCreateSubWindow(int win, int x, int y, int ancho, int alto);
La función glutCreateSubWindow crea una sub-ventana. Las ventanas pueden
contener sub-ventanas. El uso de estas sub-ventanas permite que determinadas áreas de
la ventana padre puedan estar gestionadas por ventanas hijas y, por tanto, ellas
gestionen los eventos asociados a estas regiones. Un ejemplo de esto es la creación de
botones. Los botones, utilizados para ser pulsados y activar con ello una operación
concreta, pueden ser definidos como sub-ventanas, de forma que detecten si han sido
pulsados y actúen en consecuencia.
En una aplicación 3D, es habitual mostrar al usuario distintas vistas de un objeto
(planta, perfil, alzado y perspectiva, por ejemplo). Una forma de implementar este tipo
de interfase consiste en crear una ventana principal y definir en su interior 4 ventanas
que, cubriendo distintas áreas de la ventana principal, se encarguen de visualizar el
mismo objeto pero bajo proyecciones o puntos de vista distintos.
Aplicaremos la creación de sub-ventanas en la práctica para la creación y gestión de
botones de herramientas.
La función glutCreateSubWindow recibe como argumentos el identificador de la
ventana madre win, su posición sobre la área de la ventana madre x, y, y finalmente su
tamaño ancho y alto.
Tanto ventanas de nivel alto como sub-ventanas pueden tener asociadas funciones
CALLBACK (rutinas de servicio). Estas funciones tienen como objetivo definir qué
hacer ante un evento, por ejemplo, si el usuario pulsa el botón izquierdo sobre la
ventana. Cuando se definen las fuciones CALLBACK, éstas se aplicar sobre la ventana
activa. Para asegurarse que la ventana activa es la que nos interesa, podemos hacer uso
de la función glutSetWindow.
void glutSetWindow(int win);
La función glutSetWindow pone como ventana activa a la ventana cuyo
identificador es win. A partir de ese instante, y si no se invoca de nuevo a
glutSetWindow o se crea una ventana o sub-ventana, las funciones GLUT
relacionadas con ventanas se aplicarán sobre esta ventana.
Hay un evento de obligada gestión a través de la asignación a su rutina de servicio
correspondiente. Se trata del evento de redibujado de la ventana.
El redibujado de ventana se precisa cada vez que hay que dibujar su interior, como al
inicio, cuando apartamos una ventana que la estaba cubriendo etc.
Por tanto, definir qué función va a hacerse cargo de indicar qué se debe dibujar es un
paso obligatorio. Indicamos la rutina de servicio para el redibujado con:
glutDisplayFunc(display);
Pág. 20 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Donde display (nombre que podemos obviamente elegir libremente) debe ser una
función que definamos en nuestro código y que contenga las primitivas de dibujo
necesarias para la visualización de nuestro modelo.
Cada ventana puede tener su propia rutina de servicio para el redibujado o compartirlo
si hacen lo mismo. Para asegurarse que estamos definiendo la rutina de servicio a una
ventana en concreto, o lo hacemos tras su creación o usamos la función
glutSetWindow con el identificador de la ventana antes de asignar la rutina de
servicio.
Por último lanzamos el bucle principal de visualización con:
glutMainLoop();
que inicia el bucle de visualización y gestión de los eventos definidos.
5.3 Eventos de ventana, teclado y ratón
El bucle principal de visualización implementado por la rutina glutMainLoop,
analizado en el apartado anterior, mantiene a GLUT en un ciclo de espera de eventos.
Cuando se produce un evento para el que se ha programado una rutina de servicio,
GLUT ejecuta la rutina asociada.
Los posibles eventos que puede atender GLUT se dividen en tres grupos:
• Eventos de ventana. Se generan al crear una ventana de la aplicación, cambiar su
tamaño o su posición en la pantalla. Los eventos de ventana se atienden mediante
glutReshapeFunc, glutDisplayFunc etc.
• Eventos de teclado. Se generan al pulsar alguna de las teclas. Los eventos de teclado
se atienden mediante la orden glutKeyboardFunc y glutSpecialFunc.
• Eventos de ratón. Se generan al pulsar o liberar los botones del ratón, y se atienden
mediante la orden glutMouseFunc.
Si en una rutina de servicio conviene que la imagen vuelva a dibujarse o actualizarse, se
puede llamar a la función glutPostRedisplay(), especialmente útil si al pulsar
una tecla, por ejemplo, la visualización debe cambiar.
5.3.1 Eventos de ventana
Ya hemos utilizado glutDisplayFunc. La orden glutReshapeFunc de la
librería GLUT especifica la rutina de servicio a ejecutar siempre que la ventana de la
aplicación cambie de tamaño o de posición. El formato de la orden es el siguiente:
void glutReshapeFunc(void (*function)(int width, int height));
El argumento function es un puntero a la función que actúa como rutina de servicio.
Esta función espera dos parámetros, la nueva anchura y altura de la ventana. La función
recibe el tamaño en pixels de la ventana tras su cambio de tamaño. De esta forma, en
esta función, debemos llevar a cabo todas las acciones que podamos precisar tras un
cambio de tamaño de esta ventana. Por ejemplo, si estamos visualizando un modelo 2D
con una transformación anisotrópica, el cambio de tamaño de la ventana puede hacer
necesario el cálculo de nuevos factores de escala que aplicar sobre nuestro modelo antes
de redibujarlo sobre la ventana.
Pág. 21 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
En aplicaciones 3D, típicamente esta rutina de servicio establecerá la proyección a
utilizar y el marco sobre la ventana para que se ajusten a las nuevas dimensiones
manteniendo la proporcionalidad.
5.3.2 Eventos de teclado
La orden glutKeyboardFunc de la librería GLUT especifica la rutina de servicio a
ejecutar cada vez que se pulsa una de las teclas incluidas en el conjunto de teclas
manejables. El formato de la orden es el siguiente:
void glutKeyboardFunc(void(*function)(unsigned char key,
int x, int y));
El argumento function es un puntero a la función que actúa como rutina de servicio,
definida para recibir como parámetros la tecla (unsigned char) y dos valores x e y.
El argumento key es el valor ASCII de la tecla pulsada. Los valores x e y indican la
posición del ratón sobre la ventana en el momento en el que se pulsó la tecla.
Si queremos dar respuesta a la pulsación de teclas más específicas, debemos usar
glutSpecialFunc, con este formato:
void glutSpecialFunc(void (*func)(int key, int x, int y));
En este caso key recibe un código de tecla, como por ejemplo, las teclas de cursor
definidas por las constantes: GLUT_KEY_LEFT, GLUT_KEY_RIGHT,
GLUT_KEY_UP, GLUT_KEY_DOWN.
Por ejemplo:
void tecla(int key, int x, int)
{
switch(key) {
case GLUT_KEY_UP:
…
y dentro del main:
…
glutSpecialFunc(tecla);
…
5.3.3 Eventos de ratón
La orden glutMouseFunc de GLUT especifica la rutina de servicio a ejecutar cada
vez que se pulse o se libere uno de los botones del ratón. El formato de la orden es el
siguiente:
void glutMouseFunc(void (*function)(int button, int state,
int x, int y));
Suponiendo que trabajamos con un ratón de tres botones, los valores que puede tomar
button son: GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON y
GLUT_RIGHT_BUTTON. El argumento state indica si la rutina se ejecutará al pulsar
o al liberar el botón mediante las constantes GLUT_UP y GLUT_DOWN.
Los valores de x e y son las coordenadas sobre la ventana en la que se encuentra el
ratón en el momento del evento.
Pág. 22 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
5.4 Gestión de menús
GLUT es una herramienta con enormes ventajas pero también con muchas limitaciones.
Una de las más limitaciones más destacadas viene de la mano de los menús.
Los menús que podemos activar y usar en GLUT son únicamente del tipo emergente, de
los que encontramos por ejemplo en Windows al pulsar el botón derecho en muchas
aplicaciones.
La creación de menús viene de la mano de la función glutCreateMenu.
int glutCreateMenu(void (*menu)(int valor));
Cuando utilizamos glutCreateMenu, tenemos que dar como parámetro una función
que recibirá un entero como parámetro. Este entero, valor, indicará a la función qué
opción de menú ha sido elegida por el usuario.
La función glutCreateMenu nos devuelve un identificador de menú. Al igual que
con las ventanas, con los menús también tenemos un menú activo o actual que será o
bien el último creado o el activado con glutSetMenu:
void glutSetMenu(int menu);
Para añadir opciones o entradas al menú, invocamos a la función
glutAddMenuEntry:
void glutAddMenuEntry(char * nombre, int valor);
donde nombre es el texto que aparecerá asociado a la acción y valor será el entero
que recibirá la rutina de servicio.
Los menús en GLUT admiten sub-menús. Para ello hemos de crear el sub-menú con
glutCreateMenu y añadirlo a un menú principal con glutAddSubMenu:
void glutAddSubMenu(char * nombre, int menu);
donde nombre es el texto que aparecerá como entrada en el menú padre y que si el
usuario selecciona llevará consigo el despliegue del sub-menú y menu es el
identificador del menu padre.
Veamos un ejemplo:
#define opcion1 1
#define opcion2 2
#define opcionSub1 3
#define opcionSub2 4
#define opcion3 5
void gestionMenus(int valor)
{
switch(valor) {
case opcion1: ...
break;
case opcion2: ...
break;
case opcionSub1: ...
break;
...
}
}
Pág. 23 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
main() {
...
int menu, submenu;
submenu = glutCreateMenu(gestionMenus);
glutAddMenuEntry(“Sub-Opción 1”, opcionSub1);
glutAddMenuEntry(“Sub-Opcion 2”, opcionSub2);
menu = glutCreateMenu(gestionMenus);
glutAddMenuEntry(“Opción 1”, opcion1);
glutAddMenuEntry(“Opción 2”, opcion2);
glutAddSubMenu(“Sub Opciones”, submenu);
glutAddMenuEntry(“Opción 3”, opcion3);
...
}
Los menús deben finalmente asociarse a una ventana o sub-ventana, indicando cuándo
deben aparecer.
void glutAttachMenu(int boton);
Esta función indica que el menú actual o activo será vinculado a la ventana actual o
activa al pulsar el botón del ratón caracterizado por boton. Los valores de boton
pueden ser GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON y
GLUT_RIGHT_BUTTON.
6 Desarrollo de la práctica
OpenGL combinado con GLUT nos permite el desarrollo de poderosas aplicaciones
gráficas. Aunque enfocado a los gráficos 3D, con OpenGL podemos abordar
perfectamente cualquier desarrollo 2D.
El objetivo de esta práctica será el desarrollar una sencilla herramienta de dibujo 2D.
Con esta pequeña aplicación podremos realizar sencillos dibujos utilizando figuras
geométricas básicas y aprenderemos a gestionar convenientemente los eventos lanzados
a nuestra aplicación para proporcionar una interfase interactiva al usuario.
Cuando se aborda el desarrollo de una herramienta de dibujo 2D, la primera decisión a
tomar es el formato de almacenamiento del dibujo y su naturaleza, es decir, si el dibujo
será gestionado mediante un mapa de bits o de forma vectorial.
Ambos enfoques, tal y como se describe en la parte teórica de la asignatura, tienen sus
ventajas e inconvenientes. Para la presente práctica, utilizaremos una aproximación
vectorial.
La aproximación vectorial nos obliga a almacenar de forma dinámica las diferentes
primitivas que el usuario trace, de forma que nos guardemos las características
geométricas que las definen.
La interfase con el usuario se va a gestionar mediante la creación de una ventana
principal (del tipo alto nivel) y una colección de sub-ventanas (hijas de la principal) que
se encargarán de implementar botones de las diferentes herramientas de dibujo de la
aplicación. La tarea de dibujo (y por tanto la gestión de los eventos correspondientes) la
llevará a cabo la ventana principal.
Sin más preámbulos, se describe a continuación el código de la aplicación que
utilizaremos como base en el desarrollo de la práctica.
Pág. 24 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
6.1 Almacenamiento de la información
Hemos elegido que nuestro dibujo se almacene en formato vectorial, con lo que
debemos definir una estructura de datos que permita almacenar una colección de
primitivas geométricas, y más concretamente, una colección de atributos que sinteticen
estas primitivas geométricas.
La estructura de datos que elegiremos en este caso es el TAD lista, con la clásica
implementación enlazada mediante memoria dinámica. De esta forma, el límite de
primitivas geométricas del dibujo vendrá acotado únicamente por la memoria.
Este TAD lista lo implementaremos en dos ficheros, en lista.h los tipos y prototipos
de las funciones y en [Link] la implementación de las funciones.
Veamos en primer lugar lista.h.
// Jordi Linares - DSIC - EPSA
// jlinares@[Link]
//
// lista.h
/* Lista de elementos geométricos */
typedef enum { EG_PUNTO,
EG_LINEA } TipoElementoGeometrico;
typedef struct {
TipoElementoGeometrico tipo;
double x1, y1, x2, y2;
} ElementoGeometrico;
typedef struct _lnodo {
ElementoGeometrico e;
struct _lnodo *sig; /* Puntero al siguiente nodo que contiene un
elemento. */
} lnodo;
typedef lnodo *posicion; /* Cada posicion se referencia con un
puntero. */
typedef struct { /* Definimos el tipo lista con un
puntero */
posicion primero, ultimo; /* al primero y ultimo nodos. */
} lista;
/* Funciones del TAD lista */
lista *crearl();
lista *insertar(lista *l, ElementoGeometrico e, posicion p);
ElementoGeometrico recuperar(lista *l, posicion p);
lista *borrar(lista *l, posicion p);
int vacial(lista *l);
posicion fin(lista *l);
Pág. 25 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
posicion principio(lista *l);
posicion siguiente(lista *l, posicion p);
En primer lugar definimos TipoElementoGeometrico como un enumerado6 que
permite almacenar dos tipos de figuras: puntos y líneas. Nuestra herramienta inicial de
dibujo sólo será capaz de dibujar estos dos tipos de primitivas.
A continuación se define ElementoGeometrico como un registro con los siguientes
campos:
• tipo del tipo TipoElementoGeometrico
• x1, y1, x2, y2 del tipo double7
Cada nodo de la lista almacenará una primitiva dibujada por el usuario, en nuestro caso
un ElementoGeometrico. El tipo especificará qué elemento ha dibujado el usuario
y los cuatro reales dan cabida a dos coordenadas 2D. Si dibujamos un punto, sólo se
aprovecharán realmente los dos primeros, y si se dibuja una línea se aprovecharán los
cuatro (como punto inicial y final de la línea).
El resto de las definiciones son las clásicas en una lista enlazada mediante memoria
dinámica. Las funciones del TAD también son las habituales y así se han declarado.
La implementación de la lista en el fichero [Link] también es la habitual. A
continuación podemos ver el código de la misma.
// Jordi Linares - DSIC - EPSA
// jlinares@[Link]
/*********************************************************/
/* Implementación del TAD lista */
/*********************************************************/
#include <stdlib.h>
#include <malloc.h>
#include "lista.h"
/* Crear lista */
lista *crearl() {
lista *l;
l = (lista *) malloc(sizeof(lista)); /* Creamos una lista. */
l->primero = (lnodo *) malloc(sizeof(lnodo));
6
Las variables que se definan del tipo TipoElementoGeometrico sólo podrán tener como valor las
constantes que tenemos entre las llaves en el enum.
7
El tipo double es un float de doble precisión. En gráficos es ya habitual recurrir a este tipo de reales
para asegurar una mayor precisión.
Pág. 26 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
/* Creamos el centinela */
l->primero->sig = NULL;
l->ultimo = l->primero;
return(l); /* Devolvemos un puntero a la lista creada. */
}
/* Insertar un elemento */
lista *insertar(lista *l, ElementoGeometrico e, posicion p) {
posicion q;
q = p->sig; /* Dejamos q apuntando al nodo que se desplaza. */
p->sig = (lnodo *) malloc(sizeof(lnodo));
/* Creamos un nodo. */
p->sig->e = e; /* Guardamos el elemento. */
p->sig->sig = q;
/* El sucesor del nuevo nodo esta apuntado por q. */
/* Si el nodo insertado ha pasaso a ser el ultimo, */
if (p == l->ultimo)
l->ultimo = p->sig; /* actualizamos ultimo. */
return(l); /* Devolvemos un puntero a la lista modificada. */
}
/* Borrar un elemento */
lista *borrar(lista *l, posicion p) {
posicion q;
if (p->sig == l->ultimo)
/* Si el nodo que borramos es el ultimo, */
l->ultimo = p; /* actualizamos ultimo. */
q = p->sig; /* Dejamos q apuntando al nodo a borrar. */
p->sig = p->sig->sig; /* p->sig apuntara a su sucesor. */
free(q); /* Liberamos la memoria ocupada por el nodo a borrar.
*/
return(l); /* Devolvemos un puntero a la lista modificada. */
}
/* Recuperar un elemento */
ElementoGeometrico recuperar(lista *l, posicion p) {
/* Devolvemos el elemento que hay en la posicion p. */
return(p->sig->e);
}
/* Comprobar si la lista está vacía */
int vacial(lista *l) {
/* Devolvemos 0 (falso) si la lista */
/* no esta vacia, y 1 (cierto) en caso contrario. */
return(l->primero->sig == NULL);
}
/* Última posición de la lista */
posicion fin(lista *l) {
Pág. 27 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
return(l->ultimo);
}
/* Primera posición de la lista */
posicion principio(lista *l) {
return(l->primero);
}
/* Siguiente elemento a p */
posicion siguiente(lista *l, posicion p) {
return(p->sig);
}
6.2 Aplicación básica
Veamos ahora, paso a paso, la aplicación de dibujo básica y que será la plantilla para la
resolución de la práctica. Todo el código de la aplicación básica está concentrado en el
fichero [Link].
// Primero incluimos las cabeceras necesarias
// para crear una aplicación OpenGL.
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glut.h>
#include <math.h>
#include <stdio.h>
#include "lista.h"
En la lista de los include, introducimos también el correspondiente a la lista.
// Definiciones globales
#define PI 3.1415926
#define BORDE 100
int alto, ancho; // Área de dibujo
#define numHerramientas 2
typedef enum { HERRAMIENTA_PUNTOS,
HERRAMIENTA_LINEAS } TiposHerramientas;
int * IdBotonHerramientas;
int ventanaPrincipal;
TiposHerramientas herramientaSeleccionada = HERRAMIENTA_PUNTOS;
En las definiciones globales se ha definido BORDE como una constante que determina el
tamaño de los botones de la aplicación. Vamos a crear un botón (y por tanto una sub-
ventana) por cada herramienta de dibujo que el usuario puede seleccionar. Los botones
los colocaremos a partir de la esquina superior izquierda hacia abajo.
Las variables alto y ancho almacenarán en cada momento el ancho y alto de la
ventana de dibujo. El ancho incluye el espacio ocupado por los botones. Su valor será
Pág. 28 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
actualizado cada vez que cambiemos el tamaño de la ventana; esto se consigue mediante
la definición de la correspodiente rutina de servicio con el uso de
glutReshapeFunc.
La constante numHerramientas marca el número de herramientas de dibujo que
tiene la aplicación. Inicialmente está a 2, pero tendremos que cambiar esto cuando
añadamos nuevas herramientas a la aplicación. El tipo enumerado
TiposHerramientas nos delimita los valores que la variable global
herramientaSeleccionada puede tener. Sólo tendremos una única herramienta
seleccionada en un instante determinado, y la herramienta en cuestión vendrá
especificada en herramientaSeleccionada.
El puntero IdBotonHerramientas será en realidad un vector de enteros que
almacenará los identificadores de las sub-ventanas de cada uno de los botones.
Almacenar estos identificadores es especialmente útil para no perder acceso a los
botones en ningún momento y poder, por ejemplo, ordenar su redibujado (ante la
selección de una nueva herramienta).
Por último, en ventanaPrincipal, almacenaremos el identificador de la ventana
principal. Será útil para poder acceder a ella y asegurarnos que es, por ejemplo, la
ventana activa o actual.
lista * listaElemGeometricos;
La variable global listaElemGeometricos almacenará un puntero a la lista de
elementos geométricos que el usuario ha ido trazando en su interacción con la
aplicación.
double ultimo_x = -1.0, ultimo_y = -1.0;
Las variables ultimo_x y ultimo_y son esenciales en la implementación de la
interfase. En herramientas de dibujo que implican más de un clic del usuario para su
definición, por ejemplo el dibujo de una línea, es necesario que la rutina de servicio
correspondiente sepa si el usuario está pulsando el primer punto de la línea o el segundo
(la selección del segundo punto de la línea implica su dibujo en pantalla y su
almacenamiento en la lista de elementos geométricos). Si estas variables están a -1,
implicará que el punto que hemos pulsado es el primero. Si no están a -1, implicará que
el nuevo punto pulsado es el segundo punto y que estas variables almacenan el primer
punto seleccionado (y con ello puedo registrar la línea introducida).
// --------------------------------------------------------
// Funciones generales
// --------------------------------------------------------
void inicializa (void)
{
/* selecciona el color de borrado */
glClearColor (0.0, 0.0, 0.0, 0.0);
Pág. 29 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
/* inicializa los valores de la vista */
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 1.0, 0.0, 1.0);
}
La función inicializa será invocada tras la creación de cada una de las ventanas
(principal y botones). Tanto los botones como la ventana principal van a visualizar
dibujos 2D, con lo que se establece una proyección paralela ortográfica y que, en
definitiva, nos permite dibujar sobre unas coordenadas de mundo real de (0.0, 0.0) a
(1.0, 1.0) con el (0.0, 0.0) en la esquina inferior izquierda.
void NuevaHerramienta(TiposHerramientas herramienta)
{
// Hacemos el cambio de herramienta
herramientaSeleccionada = herramienta;
// Para herramientas de varios pasos,
// invalidamos el último punto clicado
// sobre la área de dibujo.
ultimo_x = ultimo_y = -1.0;
// Redibujamos botones para contemplar el cambio
int i;
for (i=0; i < numHerramientas; i++) {
glutSetWindow(IdBotonHerramientas[i]);
glutPostRedisplay();
}
glutSetWindow(ventanaPrincipal);
}
La función NuevaHerramienta será la encargada de efectuar un cambio de
herramienta. Cuando el usuario pulse el botón izquierdo sobre un botón, la herramienta
que simboliza quedará activada. Esta activación se llevará a cabo llamando a esta
función con un argumento: la herramienta seleccionada.
NuevaHerramienta asigna el nuevo valor a la variable global
herramientaSeleccionada. A continuación asigna -1 a las variables ultimo_x
y ultimo_y para reiniciar cualquier secuencia en herramientas de dibujo de varios
pasos.
Finalmente, ordena el redibujado de todos los botones. Para que el usuario conozca qué
herramienta se encuentra seleccionada en cada instante, se pintará el botón de la
herramienta seleccionada en rojo. Con la función glutSetWindow estableceremos
como ventana actual la sub-ventana de cada botón y con glutPostRedisplay
indicaremos a GLUT que debe redibujar la ventana.
Pág. 30 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
// --------------------------------------------------------
// Funciones CALLBACK de la ventana principal
// --------------------------------------------------------
void displayPrincipal(void)
{
/* Variables locales */
posicion p = principio(listaElemGeometricos);
ElementoGeometrico elem;
/* borra la pantalla */
glClear (GL_COLOR_BUFFER_BIT);
glColor3f (1.0, 1.0, 1.0);
while (p != fin(listaElemGeometricos)) {
elem = recuperar(listaElemGeometricos, p);
switch ([Link]) {
case EG_PUNTO:
glBegin(GL_POINTS);
glVertex2f(elem.x1, elem.y1);
glEnd();
break;
case EG_LINEA:
glBegin(GL_LINE_STRIP);
glVertex2f(elem.x1, elem.y1);
glVertex2f(elem.x2, elem.y2);
glEnd();
break;
}
p = siguiente(listaElemGeometricos, p);
}
/* Vacia el buffer de dibujo */
glFlush ();
}
La función displayPrincipal se convertirá en la rutina de servicio de la ventana
principal ante su redibujado. Por tanto, su principal cometido es el de redibujar las
primitivas geométricas que el usuario haya trazado hasta un instante determinado. Este
cometido se lleva a cado recorriendo toda la lista desde el principio, tomando cada nodo
e interpretando cada tipo de primitiva en su correspondiente secuencia de dibujo.
En el caso de la aplicación básica, estamos ante puntos sueltos o una cadena de líneas, y
por tanto se hace uso de GL_POINTS o GL_LINE_STRIP.
Pág. 31 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
void reshapePrincipal(int w, int h)
{
// Anisotrópica
ancho = w-BORDE;
alto = h;
glViewport(BORDE, 0, w-BORDE, h);
}
La función reshapePrincipal se convertirá en la rutina de servicio de la ventana
principal ante su cambio de tamaño, recibiendo como parámetros el nuevo ancho (w) y
alto (h) de la ventana. El área realmente aprovechable de la ventana principal para su
dibujo será la total menos la empleada por los botones (BORDE).
La instrucción glViewPort(BORDE, 0, w-BORDE, h) define el marco sobre el
que se van a mapear las coordenadas del mundo real. Los dos primeros valores definen
las coordenadas x,y de inicio; los dos restantes el ancho y alto del marco. Esto implica
que cuando dibujemos entre las coordenadas (0.0,0.0)-(1.0,1.0) este cuadrado inicial se
reescalará a la totalidad de la ventana, con excepción de la parte izquierda de los
botones. De esta forma estamos definiendo una transformación anisotrópica, donde
nuestro dibujo no va a mantener generalmente sus proporciones ante cambios de tamaño
de la ventana.
void mousePrincipal(int boton, int estado, int x, int y)
{
ElementoGeometrico elem;
// Rectificamos las coordenadas x, y
x -= BORDE;
y = alto - y;
// ¿Qué herramienta está seleccionada? y
// ¿Qué botón pulsamos o soltamos?
if (boton == GLUT_LEFT_BUTTON && estado == GLUT_DOWN) {
switch (herramientaSeleccionada) {
case HERRAMIENTA_PUNTOS:
[Link] = EG_PUNTO;
elem.x1 = double(x) / ancho;
elem.y1 = double(y) / alto;
listaElemGeometricos =
insertar(listaElemGeometricos, elem, fin(listaElemGeometricos));
glutPostRedisplay();
break;
case HERRAMIENTA_LINEAS:
// ¿ El último punto era válido ?
if (ultimo_x > -1.0 && ultimo_y > -1.0) {
[Link] = EG_LINEA;
elem.x1 = ultimo_x;
elem.y1 = ultimo_y;
elem.x2 = double(x) / ancho;
elem.y2 = double(y) / alto;
listaElemGeometricos =
insertar(listaElemGeometricos, elem, fin(listaElemGeometricos));
Pág. 32 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
glutPostRedisplay();
}
break;
}
ultimo_x = double(x) / ancho;
ultimo_y = double(y) / alto;
La función mousePrincipal se convertirá en la rutina de servicio de la ventana
principal cuando el usuario pulse o suelte un botón del ratón sobre ella, recibiendo como
parámetros el boton, si se ha pulsado o soltado en estado, y las coordenadas x,y en
las que se encuentra situado el ratón en ese momento.
La tarea fundamental de esta función consiste en introducir nuevas primitivas a la lista
de elementos geométricos y visualizar estos cambios en la ventana.
Las coordenadas x,y del ratón se encuentran en píxeles. El primer paso es convertirlas
a coordenadas 0.0 – 1.0. En primer lugar se resta BORDE a x para considerar el espacio
ocupado por los botones (y que no cuenta como área de dibujo). El origen de las
coordenadas x,y recibidas está en la esquina superior izquierda, mientras que en CMR
(por la utilización de glOrtho2D) está en la esquina inferior izquierda, por lo que
tenemos que invertir la dirección de las y. Para pasar finalmente al rango 0.0 – 1.0, las
coordenadas x,y serán divididas por ancho y alto respectivamente.
Una vez comprobamos que hemos pulsado el ratón izquierdo, y en función de
herramientaSeleccionada, rellenamos la estructura ElementoGeometrico
correspondiente y lo insertamos al final de la lista de elementos geométricos. En el caso
de las líneas, esto lo llevamos a cabo si ya hemos introducido algún punto previo,
mediante la consulta a ultimo_x y ultimo_y (que siempre son actualizados al final
al último punto introducido).
Para asegurarnos que la nueva primitiva va a ser dibujada en pantalla, provocamos un
redibujado de la ventana principal con glutPostRedisplay.
// --------------------------------------------------------
// Funciones CALLBACK de los botones
// --------------------------------------------------------
void displayPuntos(void)
{
/* borra la pantalla */
glClear (GL_COLOR_BUFFER_BIT);
if (herramientaSeleccionada == HERRAMIENTA_PUNTOS)
glColor3f (1.0, 0.0, 0.0);
else
glColor3f (0.0, 0.0, 0.0);
Pág. 33 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
// Fondo
glBegin(GL_POLYGON);
glVertex2f(0.0, 0.0);
glVertex2f(1.0, 0.0);
glVertex2f(1.0, 1.0);
glVertex2f(0.0, 1.0);
glEnd();
glColor3f (1.0, 1.0, 1.0);
// Dibujo icono de la herramienta
glBegin(GL_POINTS);
glVertex2f(0.1, 0.4);
glVertex2f(0.5, 0.1);
glVertex2f(0.3, 0.8);
glVertex2f(0.7, 0.5);
glVertex2f(0.6, 0.8);
glEnd();
/* Vacia el buffer de dibujo */
glFlush ();
}
void mousePuntos(int boton, int estado, int x, int y)
{
if (boton == GLUT_LEFT_BUTTON && estado == GLUT_DOWN)
NuevaHerramienta(HERRAMIENTA_PUNTOS);
}
void displayLineas(void)
{
/* borra la pantalla */
glClear (GL_COLOR_BUFFER_BIT);
if (herramientaSeleccionada == HERRAMIENTA_LINEAS)
glColor3f (1.0, 0.0, 0.0);
else
glColor3f (0.0, 0.0, 0.0);
// Fondo
glBegin(GL_POLYGON);
glVertex2f(0.0, 0.0);
glVertex2f(1.0, 0.0);
glVertex2f(1.0, 1.0);
glVertex2f(0.0, 1.0);
glEnd();
glColor3f (1.0, 1.0, 1.0);
// Dibujo icono de la herramienta
glBegin(GL_LINE_STRIP);
glVertex2f(0.1, 0.4);
Pág. 34 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
glVertex2f(0.5, 0.1);
glVertex2f(0.3, 0.8);
glVertex2f(0.7, 0.5);
glVertex2f(0.6, 0.8);
glEnd();
/* Vacia el buffer de dibujo */
glFlush ();
}
void mouseLineas(int boton, int estado, int x, int y)
{
if (boton == GLUT_LEFT_BUTTON && estado == GLUT_DOWN)
NuevaHerramienta(HERRAMIENTA_LINEAS);
}
Las rutinas de servicio displayLineas y displayPuntos son muy sencillas.
Únicamente se encargar de dibujar el propio botón. Para ello disponemos de toda la área
(0.0,0.0)-(1.0, 1.0) y que se mapeará sobre todo el tamaño del botón. Merece la pena
recordar que los botones son sub-ventanas y, por tanto, precisan por parte del
desarrollador de la definición de una rutina de servicio que va a ser llamada ante la
necesidad de redibujar su interior, bien sea por acciones del usuario o por la llamada
explícita a glutPostRedisplay.
Con objeto de identificar la herramienta seleccionada, el botón se dibujará de fondo rojo
si está seleccionado o negro si no.
Las rutinas de servicio mouseLineas y mousePuntos se encargan de detectar si el
usuario pulsa el botón izquierdo sobre el botón herramienta en cuestión. No nos interesa
dónde exactamente, es decir, ignoramos la x,y. En caso de haber pulsado el botón
izquierdo activamos la herramienta correspondiente mediante la llamada a
NuevaHerramienta con el argumento adecuado.
// --------------------------------------------------------
// Main
// --------------------------------------------------------
void main(int argc, char** argv)
{
// Creamos el vector para almacenar los identificadores
// de las ventanas de herramientas o botones.
IdBotonHerramientas = (int *) malloc(numHerramientas *
sizeof(int));
// Iniciamos la lista de elementos geométricos
listaElemGeometricos = crearl();
// Inicializa la librería auxiliar GLUT
glutInit(&argc, argv);
// Inicializa el modo de visualización
Pág. 35 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
// ----------- VENTANA PRINCIPAL ---------------
// Indica el tamaño de la ventana (ancho,alto)
glutInitWindowSize(500, 500);
// Indica la posición inicial (xmin,ymin)
glutInitWindowPosition(0, 0);
// Abre la ventana con el título indicado
ventanaPrincipal = glutCreateWindow("Herramienta de dibujo");
// Inicializar valores para la ventana actual
inicializa();
// Indica función de dibujo para la ventana actual
glutDisplayFunc(displayPrincipal);
// Indica función de cambio de tamaño de la ventana actual
glutReshapeFunc(reshapePrincipal);
// Respuesta a la pulsación del ratón
glutMouseFunc(mousePrincipal);
// ----------- BOTÓN 1: PUNTOS ---------------
// Crea sub-ventana
IdBotonHerramientas[0] = glutCreateSubWindow(ventanaPrincipal,
0, 0, BORDE-1, BORDE-1);
// Inicializar valores para la ventana actual
inicializa();
// Indica cual es la función de dibujo para la ventana actual
glutDisplayFunc(displayPuntos);
// Respuesta a la pulsación del ratón
glutMouseFunc(mousePuntos);
// ----------- BOTÓN 2: LÍNEAS ---------------
// Crea sub-ventana
IdBotonHerramientas[1] = glutCreateSubWindow(ventanaPrincipal,
0, BORDE, BORDE-1, BORDE-1);
// Inicializar valores para la ventana actual
inicializa();
// Indica cual es la función de dibujo para la ventana actual
glutDisplayFunc(displayLineas);
// Respuesta a la pulsación del ratón
glutMouseFunc(mouseLineas);
// Comienza el bucle de dibujo y proceso de eventos.
glutMainLoop();
Pág. 36 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
// Eliminamos la lista
while (!vacial(listaElemGeometricos))
listaElemGeometricos = borrar(listaElemGeometricos,
principio(listaElemGeometricos));
free(listaElemGeometricos);
// Eliminamos vector de botones
free(IdBotonHerramientas);
Finalmente, en el main, iniciamos las estructuras de datos, iniciamos GLUT, creamos
cada ventana (principal y botones) con sus respectivas rutinas de servicio, llamamos al
bucle del proceso de eventos (del que se sale cuando cerramos la ventana principal) y
liberamos las estructuras reservadas.
Podemos ver en ejecución esta pequeña aplicación en la siguiente imagen:
6.3 Tareas iniciales a resolver
Es hora de poner en práctica los conocimientos adquiridos. Usando como plantilla el
programa de dibujo anterior, se pide al alumno ampliar el programa anterior para
resolver las siguientes tareas:
1. Añadir dos nuevas herramientas de dibujo de entre las cuatro que se sugieren a
continuación: círculos, rectángulos, elipses y dibujo a mano alzada.
Pág. 37 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Para el trazado de círculos se sugiere hacer lo siguiente. En primer lugar el usuario
selecciona el centro (mediante un click) y a continuación el radio (otro click). Usar para
ello un esquema similar al de las líneas mediante el uso de las variables ultimo_x y
ultimo_y. Para almacenar el círculo en la lista de elementos geométricos, habrá que
crearle un nuevo tipo en la lista, y utilizar x1 y y1 para el centro y, por ejemplo, x2
para almacenar el radio.
El siguiente código recuerda cómo podemos dibujar un círculo de centro cx, cy y
radio r:
#define RANG_CIRCULO 100
glBegin(GL_LINE_STRIP);
for (i = 0; i < RANG_CIRCULO + 1; i++) { // +1 para cerrar
glVertex2f( cx + r * cos(2.0 * PI * i / RANG_CIRCULO),
cy + r * sin(2.0 * PI * i / RANG_CIRCULO) );
}
glEnd();
En el caso de los rectángulos la tarea es más simple: el primer click para la esquina
superior izquierda y el segundo para la esquina inferior derecha. Su trazo es más
sencillo.
La elipse puede implementarse de forma análoga al rectángulo. En este caso hay que
quedarse con los mismos dos puntos, con los que podemos definir un código similar al
del círculo pero con dos radios, fácilmente deducibles del rectángulo que circunscribe a
la elipse.
El trazo a mano alzada implica dibujar mientras mantenemos pulsado el ratón izquierdo.
Invitamos para ello al alumno que consulte la documentación oficial de GLUT y que
averigue cómo llevar a cabo esta operación.
2. Añadir un menú emergente al pulsar el botón derecho sobre la ventana de dibujo que
permita seleccionar varios colores (por ejemplo, blanco, rojo, verde, azul) con los
que dibujar. Será necesario ampliar ElementoGeométrico para que incluya su
color.
3. Permitir cambiar a voluntad entre un mapeado anisotrópico u otro isotrópico (la
proporcionalidad del dibujo no debe cambiar aunque cambie el tamaño y
proporciones de la ventana). Usar para ello el menú emergente.
4. Mediante la pulsación de la tecla ‘ESC’ (cuyo código es el 27), permitir abortar el
trazo de las operaciones de dibujo de más de un paso una vez iniciadas. Por ejemplo,
si estamos dibujando líneas, la pulsación de la tecla ‘ESC’ nos permitiría iniciar una
nueva secuencia de líneas no conectadas con las anteriores.
6.4 Matrices de transformación en dos dimensiones
Para añadir nuevas funcionalidades a nuestra herramienta de dibujo, vamos a introducir
la posibilidad de aplicar matrices de transformación a nuestro modelo.
Las transformaciones geométricas nos sirven, básicamente, para cumplir dos objetivos.
En primer lugar, nos permiten acometer la transformación que sufre nuestro modelo (en
coordenadas de mundo real) para su visualización sobre el dispositivo de salida
(coordenadas de dispositivo). En segundo lugar, las transformaciones geométricas son
un mecanismo imprescindible para modificar e interactuar con nuestro modelo.
Pág. 38 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Describiremos a continuación una implementación de estas transformaciones
geométricas 2D y que deberá utilizarse para la segunda parte de la práctica.
6.4.1 Transformaciones geométricas. Implementación en lenguaje C
De las muchas transformaciones que podemos aplicar en el plano 2D, nos centraremos
en las transformaciones afín, y más concretamente en la traslación, escalado y giro (o
rotación).
Escalado y giro son fácilmente definibles mediante matrices, no así la traslación. Para
que las tres puedan ser expresadas matricialmente se recurre a las coordenadas
homogéneas, y de esta forma quedan definidas las tres como matrices 3x3.
La definición de las transformaciones en formato matricial facilita enormemente la
composición (o aplicación en cadena) de varias transformaciones. Por muy compleja
que sea la transformación, siempre que sea afín, la podremos sintetizar con una única
matriz 3x3.
Cuando se plantea el uso de transformaciones 2D en el desarrollo de una aplicación, no
siempre resultará imprescindible la implementación de las transformaciones como
matrices, es decir, mediante la definición del tipo matriz y funciones como el producto
de matrices etc. Si nuestra aplicación va a realizar unas transformaciones bien definidas
e incluso fijas, podemos resolver analíticamente, y a priori, las matrices a aplicar (y si
son compuestas obteniendo la matriz final resolviendo el producto) e implementar estas
transformaciones resolviendo cómo calcular el punto de salida a partir de el punto de
entrada (implementando directamente los productos sobre el punto de entrada que nos
dan el punto de salida).
En esta práctica sí vamos a implementar el tipo matriz y las funciones necesarias para su
manipulación, y resolver las transformaciones geométricas generando y multiplicando
matrices. De esta forma nos familiarizaremos con los elementos necesarios para poder
abordar desarrollos más complejos, en los que resulta fundamental su resolución
mediante el tipo matriz y sus funciones de manipulación.
Como veremos en la próxima práctica, en OpenGL las matrices de transformación son
una parte importante de su API y, por tanto, evitan en la mayoría de los casos la
necesidad de su implementación.
6.4.2 El tipo vector (punto)
La primera estructura que abordaremos es la de vector, que utilizaremos para definir
también los puntos que definen el modelo a visualizar.
Utilizaremos la misma notación y proceder que en la parte teórica de la asignatura:
vectores columna y premultiplicación de las matrices por los puntos.
El tipo vector lo definiremos de la siguiente forma:
typedef struct {
double x, y, w;
} VECTOR;
El uso de las coordenadas homogéneas nos obliga a la definición de los puntos 2D con
tres componentes (x, y, w).
Pág. 39 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
6.4.3 El tipo matriz
El tipo matriz, que dará soporte a las transformaciones geométricas, se definirá como
sigue:
typedef double MATRIZ[3][3];
6.4.4 Producto de matrices (composición de transformaciones)
Tratándose de matrices 3x3, no sería estrictamente necesario implementar el producto
de las matrices con los tradicionales 3 bucles anidados. Sería más óptimo simplemente
codificar los productos de los coeficientes de la matriz a calcular asignándoles las
expresiones correspondientes.
No obstante, y para una mayor claridad, implementamos la función Componer con el
clásico algoritmo del producto de dos matrices:
void Componer(MATRIZ izq, MATRIZ der, MATRIZ comp)
{
int i, j, k;
MATRIZ aux; // Utilizando una matriz auxiliar podremos
// usar la matriz donde almacenaremos el
// resultado también como argumento.
for (i=0; i<3; i++)
for (j=0; j<3; j++) {
aux[i][j]=0.0;
for (k=0; k<3; k++)
aux[i][j]=aux[i][j]+izq[i][k]*der[k][j];
}
for (i=0; i<3; i++)
for (j=0; j<3; j++)
comp[i][j]=aux[i][j];
}
6.4.5 Producto matriz y vector
Una vez hemos calculado la matriz de transformación, ésta deberá ser aplicada al
conjunto de puntos a transformar. Recurrimos por tanto al producto de una matriz por
un vector columna.
ve es el vector de entrada (por transformar) y vs el de salida (transformado).
void Transformar(MATRIZ m, VECTOR ve, VECTOR * vs)
{
vs->x=m[0][0]*ve.x+m[0][1]*ve.y+m[0][2]*ve.w;
vs->y=m[1][0]*ve.x+m[1][1]*ve.y+m[1][2]*ve.w;
vs->w=1.0;
}
Destacar que vs debe ser pasado por referencia, y de ahí que utilicemos el * en la
declaración del formato y -> para acceder a sus campos. Esto es debido a que hemos
declarado VECTOR como un struct, y éste puede ser pasado por valor o por
referencia. Distinto es el caso de MATRIZ, que al haber sido declarada como un array o
vector, siempre es pasada por referencia ya que en realidad es un puntero.
Pág. 40 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
6.4.6 La matriz identidad
Inicializar una matriz puede ser útil en el caso de que ésta vaya acumulando sucesivos
productos de otras matrices. La forma de asegurar que una matriz está inicializada de
forma que actúe como elemento neutro del producto de matrices es mediante la
asignación de sus coeficientes a los de la matriz identidad.
void Identidad(MATRIZ m)
{
m[0][0]=1.0;m[0][1]=0.0;m[0][2]=0.0;
m[1][0]=0.0;m[1][1]=1.0;m[1][2]=0.0;
m[2][0]=0.0;m[2][1]=0.0;m[2][2]=1.0;
}
6.4.7 Definición de las matrices de transformación 2D
Expondremos a continuación un conjunto de 3 funciones cuyo objetivo es inicializar la
matriz que reciben como argumento a una de las 3 matrices de transformación
geométricas.
Traslación
void Traslacion(MATRIZ m, double tx, double ty)
{
m[0][0]=1.0;m[0][1]=0.0;m[0][2]=tx;
m[1][0]=0.0;m[1][1]=1.0;m[1][2]=ty;
m[2][0]=0.0;m[2][1]=0.0;m[2][2]=1.0;
}
Escalado
void Escalado(MATRIZ m, double sx, double sy)
{
m[0][0]=sx; m[0][1]=0.0;m[0][2]=0.0;
m[1][0]=0.0;m[1][1]=sy; m[1][2]=0.0;
m[2][0]=0.0;m[2][1]=0.0;m[2][2]=1.0;
}
Giro
void Giro(MATRIZ m, double grados)
{
double radianes = (PI*grados)/180.0;
double coseno = cos(radianes) ;
double seno = sin(radianes) ;
m[0][0]=coseno;m[0][1]=-seno; m[0][2]=0.0;
m[1][0]=seno; m[1][1]=coseno;m[1][2]=0.0;
m[2][0]=0.0; m[2][1]=0.0; m[2][2]=1.0;
}
6.4.8 Un ejemplo
Veamos cómo podríamos rotar (45 grados) y trasladar (-5, 4) un punto:
VECTOR puntoInicial = {10.0, 10.0, 1.0}; // Punto inicial el (10,10)
VECTOR puntoTransformado;
MATRIZ mTrans; // Matriz de transformación
Pág. 41 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
MATRIZ mTraslacion, mGiro;
………
// Definimos las transformaciones a aplicar.
Traslacion(mTraslacion, -5.0, 4.0);
Giro(mGiro, 45.0);
Componer(mGiro, mTraslacion, mTrans);
Transformar(mTrans, puntoInicial, &puntoTransformado);
6.5 Últimas tareas a resolver
Cómo último punto de la práctica, se pide al alumno la implementación de dos nuevas
herramientas: escalar el modelo al doble de su tamaño y escalar el modelo a la mitad de
su tamaño.
Para ello habrá que integrar las matrices de transformación 2D a nuestro desarrollo
mediante la inclusión de los ficheros matriz.h y [Link].
Las dos nuevas herramientas se pueden incluir como botones o como opciones nuevas
en el menú emergente.
Los dos escalados deben efectuarse respecto al centro del marco y no respecto al punto
(0,0), que es lo que ocurre por defecto utilizando únicamente la matriz de escalado. Para
ello habrá que componer una nueva matriz de transformación para permitir el escalado
respecto al punto 0.5, 0.5. En la parte teórica de la asignatura queda resuelto este
procedimiento.
Para aplicar los escalados, se aconseja añadir nuevas funciones al TAD lista, de forma
que se pueda aplicar a cada uno de los puntos de la lista de elementos geométricos. Los
puntos de los elementos geométricos deben convertirse al tipo VECTOR (sin olvidar que
en este caso hay que iniciar la componente w a 1.0), modificarse mediante la función
Transformar y actualizar su resultado en el nodo correspondiente de la lista.
En las siguientes imágenes vemos el efecto de escalar por dos y dividido entre dos el
dibujo de partida:
Pág. 42 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Pág. 43 de 44
Gràfics per Computador – DSIC – EPSA – Pràctica 3
Pág. 44 de 44