44
Pág. 1 de 44 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 [email protected]

Tutorial Glu

Embed Size (px)

Citation preview

Page 1: Tutorial Glu

Pág. 1 de 44

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 [email protected]

Page 2: Tutorial Glu

Pág. 2 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

Page 3: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 3 de 44

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

Page 4: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 4 de 44

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++.

Page 5: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 5 de 44

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 opengl32.lib opengl32.dll

glu.h glu32.lib glu32.dll

glut.h glut.lib

glut32.lib

glut.dll

glut32.dll

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 glut.lib y glut32.lib al directorio ..\DevStudio\VC\lib y finalmente los ficheros glut.dll y glut32.dll 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.

Page 6: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 6 de 44

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.

Page 7: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 7 de 44

Seleccionamos la solapa Link, y añadimos en Object/library modules, al final de la lista y separados por un espacio, nuestros cuatro ficheros 1.lib 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 2.cpp”.

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).

Page 8: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 8 de 44

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.

Page 9: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 9 de 44

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 2.cpp”. // 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);

Page 10: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 10 de 44

// 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.

Page 11: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 11 de 44

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.

Page 12: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 12 de 44

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.

Page 13: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 13 de 44

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.

Page 14: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 14 de 44

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).

Page 15: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 15 de 44

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.

Page 16: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 16 de 44

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:

Page 17: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 17 de 44

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.

Page 18: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 18 de 44

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.

Page 19: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 19 de 44

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);

Page 20: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 20 de 44

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);

Page 21: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 21 de 44

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.

Page 22: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 22 de 44

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.

Page 23: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 23 de 44

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; ... } }

Page 24: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 24 de 44

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.

Page 25: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 25 de 44

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 lista.cpp la implementación de las funciones.

Veamos en primer lugar lista.h.

// Jordi Linares - DSIC - EPSA // [email protected] // // 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);

Page 26: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 26 de 44

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 lista.cpp también es la habitual. A continuación podemos ver el código de la misma.

// Jordi Linares - DSIC - EPSA // [email protected] /*********************************************************/ /* 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.

Page 27: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 27 de 44

/* 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) {

Page 28: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 28 de 44

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 dibuix.cpp. // 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á

Page 29: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 29 de 44

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);

Page 30: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 30 de 44

/* 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.

Page 31: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 31 de 44

// -------------------------------------------------------- // 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 (elem.tipo) { 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.

Page 32: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 32 de 44

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: elem.tipo = 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) { elem.tipo = 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));

Page 33: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 33 de 44

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);

Page 34: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 34 de 44

// 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);

Page 35: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 35 de 44

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

Page 36: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 36 de 44

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();

Page 37: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 37 de 44

// 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.

Page 38: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 38 de 44

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.

Page 39: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 39 de 44

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).

Page 40: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 40 de 44

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.

Page 41: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 41 de 44

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

Page 42: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 42 de 44

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 matriz.cpp.

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:

Page 43: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 43 de 44

Page 44: Tutorial Glu

Gràfics per Computador – DSIC – EPSA – Pràctica 3

Pág. 44 de 44