.: Capítulo 4: Moviéndonos por nuestro espacio 3D :.

4.1     Coordenadas oculares

4.2     Transformaciones
4.2.1       El modelador
4.2.1.1         Transformaciones del observador
4.2.1.2         Transformaciones del modelo
4.2.2       Transformaciones de la proyección
4.2.3       Transformaciones de la vista

4.3     Matrices
4.3.1       El canal de transformaciones
4.3.2       La matriz del modelador
4.3.2.1         Translación
4.3.2.2         Rotación
4.3.2.3         Escalado
4.3.2.4         La matriz identidad
4.3.3       La matriz de proyección
4.3.3.1         Proyecciones ortográficas
4.3.3.2         Proyecciones perspectivas

4.4     Ejemplo: una escena simple
4.4.1       Código
4.4.2       Análisis del código

4.5     Mirar hacia un punto

 

En este capítulo se verá como mover el punto de vista sobre la escena y los objetos que la componen, en el sistema de coordenadas. Las herramientas que OpenGL aporta para hacer esto están basadas en matrices de transformación que, aplicadas sobre los sistemas de coordenadas con un orden específico, construirán la escena deseada. 

En las dos primeras secciones (coordenadas oculares y transformaciones) se intentará dar una idea de cómo trabaja ogl con el espacio 3D, a alto nivel. En las siguientes, se hablará más del código necesario para conseguir los resultados deseados. Se termina con un ejemplo en el que se aplica todo lo aprendido.

 

4.1   Coordenadas oculares 

Las coordenadas oculares se sitúan en el punto de vista del observador, sin importar las transformaciones que tengan lugar. Por tanto, estas coordenadas representan un sistema virtual de coordenadas fijo usado como marco de referencia común. En la ilustración 4.1 se pueden apreciar dos perspectivas de este sistema de coordenadas. 


Ilustración 4 . 1

 Cuando se dibuja en 3D con ogl, se utiliza el sistema de coordenadas cartesiano. En ausencia de cualquier transformación, el sistema en uso será idéntico al sistema de coordenadas oculares.

 

4.2   Transformaciones

Las transformaciones son las que hacen posible la proyección de coordenadas 3D sobre superficies 2D. También son las encargadas de mover, rotar y escalar objetos. En realidad, estas transformaciones no se aplican a los modelos en sí, si no al sistema de coordenadas, de forma que si se quiere rotar un objeto, no lo se le rota, sino que se rota el eje sobre el que se sitúa.

4.2.1 El modelador

En esta sección se recogen las transformaciones del observador y del modelado puesto que, como se verá en el apartado 4.2.1.3, constituyen, al fin y al cabo, la misma transformación.

4.2.1.1  Transformaciones del observador

La transformación del observador es la primera que se aplica a la escena, y se usa para determinar el punto más ventajoso de la escena. Por defecto, el punto de vista está en el origen (0,0,0) mirando en dirección negativa del eje z. La transformación del observador permite colocar y apuntar la cámara donde y hacia donde se quiera. Todas las transformaciones posteriores tienen lugar basadas en el nuevo sistema de coordenadas modificado.

4.2.1.2  Transformaciones del modelo

Estas transformaciones se usan para situar, rotar y escalar los objetos de la escena. La apariencia final de los objetos depende en gran medida del orden con el que se hayan aplicado las transformaciones. Por ejemplo, en la ilustración 4.2 podemos ver la diferencia entre aplicar primero un rotación y luego una translación, y hacer esto mismo invirtiendo el orden.

 
Ilustración 4 . 2

 En el caso (a), primero se aplica una rotación, rotando así el sistema de coordenadas propio del objeto. Luego, al aplicar la translación sobre el eje x, como éste esta rotado, la figura ya no se desplaza sobre el eje x del sistema de coordenadas oculares, si no sobre el suyo propio. En el segundo caso, (b), primero se hace la translación sobre el eje x, así que la figura se mueve hacia la derecha, también en el sistema de coordenadas oculares, ya que ambos sistemas coinciden. Luego se hace la rotación, pero el objeto gira sobre si mismo, ya que aún esta centrado en su propio sistema de coordenadas.

 

4.2.2 Transformaciones de la proyección

 La transformación de proyección se aplica a la orientación final del modelador. Esta proyección define el volumen de visualización y establece los planos de trabajo. A efectos prácticos, esta translación especifica cómo se traslada una escena finalizada a la imagen final de la pantalla.

 Los dos tipos de proyección más utilizados son la ortográfica y la perspectiva, que veremos más adelante.

 

4.2.3 Transformaciones de la vista

 En el momento en que se ha terminado todo el proceso de transformaciones, solo queda un último paso: proyectar lo que hemos dibujado en 3D al 2D de la pantalla, en la ventana en la que estamos trabajando. Esta es la denominada transformación de la vista.

 

4.3   Matrices

 Las matemáticas que hay tras estas transformaciones se simplifican gracias a las matrices. Cada una de las transformaciones de las que se acaba de hablar puede conseguirse multiplicando una matriz que contenga los vértices por una matriz que describa la transformación. Por tanto todas las transformaciones ejecutables con ogl pueden describirse como la multiplicación de dos o más matrices.

 

4.3.1 El canal de transformaciones

 Para poder llevar a cabo todas las transformaciones de las que se acaba de hablar, deben modificarse dos matrices: la matriz del Modelador y la matriz de Proyección. OpenGL proporciona muchas funciones de alto nivel que hacen muy sencillo la construcción de matrices para transformaciones. Éstas se aplican sobre la matriz que este activa en ese instante. Para activar una de las dos matrices utilizamos la función glMatrixMode. Hay dos parámetros posibles:

glMatrixMode(GL_PROJECTION);

activa la matriz de proyección, y

glMatrixMode(GL_MODELVIEW);

activa la del modelador. Es necesario especificar con que matriz se trabaja, para poder aplicar las transformaciones necesarias en función de lo que se desee hacer.

El camino que va desde los datos “en bruto” de los vértices hasta las coordenadas en pantalla sigue el siguiente camino: Primero, el vértice se convierte en una matriz 1x4 en la que los tres primeros valores son las coordenadas x,y,z. El cuarto número (llamado parámetro w) es un factor de escala, que no se va a usar de momento, usando el valor 1.0. Entonces se multiplica el vértice por la matriz del modelador, para obtener las coordenadas oculares. Éstas se multiplican por la matriz de proyección para conseguir las coordenadas de trabajo. Con esto se eliminan todos los datos que estén fuera del volumen de proyección. Estas coordenadas de trabajo se dividen por el parámetro w del vértice, para hacer el escalado relativo del que se habló antes. Finalmente, las coordenadas resultantes se mapean en un plano 2D mediante la transformación de la vista.

 

4.3.2 La matriz del modelador

La matriz del modelador es una matriz 4x4 que representa el sistema de coordenadas transformado que se está usando para colocar y orientar los objetos. Si se multiplica la matriz del vértice (de tamaño 1x4) por ésta se obtiene otra matriz 1x4 con los vértices transformados sobre ese sistema de coordenadas.

OpenGL proporciona funciones de alto nivel para conseguir matrices de translación, rotación y escalado, y además la multiplican por la matriz activa en ese instante, de manera que no hay que preocuparse por ello en absoluto.

4.3.2.1  Translación

Imaginemos que se quiere dibujar un cubo con la función de la librería GLUT glutSolidCube, que lleva como parámetro el lado del cubo. Si se escribe el siguiente código

glutSolidCube(5);

se obtiene un cubo centrado en el origen (0,0,0) y con el lado de la arista 5. Ahora se quiere mover 10 unidades hacia la derecha (es decir, 10 unidades en el sentido positivo del eje de las x). Para ello se tendría que construir una matriz de transformación y multiplicarla por la matriz del modelador. Ogl nos ofrece la función glTranslate, que crea la matriz de transformación y la multiplica por la matriz que esté activa en ese instante (en este caso debería ser la del modelador, GL_MODELVIEW). Entonces el código quedaría de la siguiente manera:

glTranslatef(10.0f, 0.0f, 0.0f);
glutSolidCube(5);
La “f” añadida a la función indica que se usarán flotantes. Los parámetros de glTranslate son las unidades a desplazar en el eje x, y y z, respectivamente. Pueden ser valores negativos, para trasladar en el sentido contrario.

4.3.2.2  Rotación

Para rotar, tenemos también una función de alto nivel que construye la matriz de transformación y la multiplica por la matriz activa, glRotate. Lleva como parámetros el ángulo a rotar (en grados, sentido horario), y después x, y y z del vector sobre el cual se quiere rotar el objeto. Una rotación simple, sobre el eje y, de 10º sería 

glRotatef(10, 0.0f, 1.0f, 0.0f);

4.3.2.3  Escalado

Una transformación de escala incrementa el tamaño de nuestro objeto expandiendo todos los vértices a lo largo de los tres ejes por los factores especificados. La función glScale lleva como parámetros la escala en x, y y z, respectivamente. El valor 1.0f es la referencia de la escala, de tal forma que la siguiente línea:

glScalef(1.0f, 1.0f, 1.0f);

no modificaría el objeto en absoluto. Un valor de 2.0f sería el doble, y 0.5f sería la mitad. Por ejemplo, para ensanchar un objeto a lo largo de su eje z, de tal forma que quedase cuatro veces más “alargado” en este eje, sería:

glScalef(1.0f, 1.0f, 4.0f);

4.3.2.4  La matriz identidad

El “problema” del uso de estas funciones surge cuando se tiene más de un objeto en la escena. Estas funciones tienen efectos acumulativos. Es decir, si se quiere preparar una escena como la de la ilustración 4.3,


Ilustración 4 .3

con una esfera (de radio 3) centrada en (0,10,0) y otra centrada en (10,0,0), y se escribe el siguiente código (que es incorrecto):

glTranslatef(0.0f, 10.0f, 0.0f);
glutSolidSphere(3.0f, 8, 8);
glTranslate(10.0f, 0.0f, 0.0f);
glutSolidsphere(3.0f, 8, 8);

En este código, se dibuja primero una esfera en (0,10,0) como se quiere. Pero después, se está multiplicando la matriz del modelador (que ya estaba transformada para dibujar la primera esfera) por otra matriz de transformación que desplaza 10 unidades hacia la derecha. Por ello la segunda matriz se dibujaría, como se puede ver en la ilustración 4.4, en (10,10,0), y no en (10,0,0), como se pretendía. 


Ilustración 4 .4

Para solventar este problema se debe reiniciar la matriz del modelador a un estado conocido, en este caso, centrada en el origen de nuestro sistema de coordenadas oculares. Para ello se carga en la matriz del modelador la matriz identidad (una matriz 4x4 llena de ceros excepto en la diagonal, que contiene unos). Esto se consigue gracias a la función glLoadIdentity, que no lleva parámetros. Simplemente carga la matriz identidad en la matriz activa en ese instante. El código correcto para el ejemplo anterior quedaría de la siguiente manera:

glTranslatef(0.0f, 10.0f, 0.0f);
glutSolidSphere(3.0f, 8, 8);
glLoadIdentity();
glTranslate(10.0f, 0.0f, 0.0f);
glutSolidsphere(3.0f, 8, 8);  

 

4.3.3 La matriz de proyección 

La matriz de proyección especifica el tamaño y la forma del volumen de visualización. El volumen de visualización es aquel cuyo contenido es el que se representa en pantalla. Está delimitado por una serie de planos de trabajo. De estos planos, los más importantes son los planos de corte, que son los que nos acotan el volumen de visualización por delante y por detrás. En el plano más cercano a la cámara (znear) es donde se proyecta la escena para luego pasarla a la pantalla. Todo lo que esté más adelante del plano de corte más alejado de la cámara (zfar) no se representa.

Veamos los distintos volúmenes de visualización de las dos proyecciones más usadas: ortográficas y perspectivas.

 

4.3.3.1  Proyecciones ortográficas

Una proyección ortográfica es cuadrada en todas sus caras. Esto produce una proyección paralela, útil para aplicaciones de tipo CAD o dibujos arquitectónicos, o también para tomar medidas, ya que las dimensiones de lo que representan no se ven alteradas por la proyección.

Una aproximación menos técnica pero mas comprensible de esta proyección es imaginar que se tiene un objeto fabricado con un material deformable, y se aplasta literalmente como una pared. Se obtendría el mismo objeto, pero plano, liso. Pues eso es lo que se vería por pantalla.

Para definir la matriz de proyección ortográfica y multiplicarla por la matriz activa (que debería ser en ese momento la de proyección, GL_PROJECTION), se utiliza la función glOrtho, que se define de la siguiente forma:

glOrtho(limiteIzquierdo, limiteDerecho, limiteAbajo, limiteArriba, znear, zfar)

siendo todos flotantes. Los valores de znear y zfar no son las posiciones de esos planos en el espacio 3D. Representan la distancia desde el centro de proyección, con valor positivo hacia delante y negativo hacia atrás. Con esto simplemente se acota lo que será el volumen de visualización (un cubo).

Por ejemplo, la ilustración 4.5 es un render de un coche con proyección ortográfica, visto desde delante.


Ilustración 4 .5

 El código utilizado para esta proyección ha sido

glOrtho(-0.5f, 0.5f, -0.5f, 0.5f, 0.01f, 20.0f);

En la siguiente sección, se podrá apreciar la diferencia usando una proyección perspectiva.

 

4.3.3.2  Proyecciones perspectivas

Una proyección en perspectiva reduce y estirar los objetos más alejados del observador. Es importante saber que las medidas de la proyección de un objeto no tienen por qué coincidir con las del objeto real, ya que han sido deformadas.

El volumen de visualización creado por una perspectiva se llama frustum. Un frustum es una sección piramidal, vista desde la parte afilada hasta la base (ilustración 4.6).


Ilustración 4 .6

Se puede definir esta proyección utilizando la función glFrustum. Pero existe otra función de la librería GLU llamada gluPerspective que hace el proceso más sencillo. Se define de la siguiente forma:

Void gluPerspective(angulo, aspecto, znear, zfar);

Los parámetros de gluPerspective son flotantes y definen las características mostradas en la ilustración 4.7, el ángulo para el campo de visión en sentido vertical, el aspecto que es la relación entre la altura (h) y la anchura (w) y las distancias znear y zfar de los planos que acotan el fustrum al observador. Los valores de znear y zfar no son las posiciones de esos planos en el espacio 3D, representan la distancia desde el centro de proyección, con valor positivo hacia delante y negativo hacia atrás.


Ilustración 4 .7

La ilustración 4.8 muestra la escena del coche de la sección anterior, esta vez con una proyección en perspectiva:


Ilustración 4 .8

El código utilizado para definir la proyección ha sido:

gluPerspective(45.0f,(GLfloat)(width/height),0.01f,100.0f);

Se usan 45º de ángulo, la relación entre el ancho y alto de la pantalla (width y height son el ancho y alto actual de la ventana) y las distancias a los planos de corte znear y zfar son 0.01 y 100 respectivamente.

 

4.4   Ejemplo: una escena simple

4.4.1 Código

El siguiente código es un programa que usa OpenGL mediante la librería GLUT. Primero se lista el código completo y luego se comenta línea por línea. Con él se aplicará lo aprendido en el anterior capítulo y el presente, además de nuevas funciones de la librería GLUT.

La escena consiste en un cubo de colores girando sobre si mismo y una esfera blanca de alambre (modo más conocido como “wired” que consiste en no dibujar las caras del objeto, sino solamente las líneas que unen sus vértices, dando la sensación de ser una figura de alambre) girando alrededor del cubo. Se pueden utilizar las teclas ‘o’ y ‘p’ para cambiar el tipo de proyección, ortográfica y perspectiva, respectivamente. Con la tecla ‘esc’ se abandona el programa.

#include <GL/glut.h>
 
GLfloat anguloCuboX = 0.0f;
GLfloat anguloCuboY = 0.0f;
GLfloat anguloEsfera = 0.0f;
 
GLint ancho=400;
GLint alto=400;
 
int hazPerspectiva = 0;
 
void reshape(int width, int height)
{
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
   
    if(hazPerspectiva)
     gluPerspective(60.0f, (GLfloat)width/(GLfloat)height, 1.0f, 20.0f);
    else       glOrtho(-4,4, -4, 4, 1, 10);

    glMatrixMode(GL_MODELVIEW);
 
    ancho = width;
    alto = height;
}
 
void drawCube(void)
{
    glColor3f(1.0f, 0.0f, 0.0f);
    glBegin(GL_QUADS);  //cara frontal
    glVertex3f(-1.0f, -1.0f,  1.0f);
    glVertex3f( 1.0f, -1.0f,  1.0f);
    glVertex3f( 1.0f,  1.0f,  1.0f);
    glVertex3f(-1.0f,  1.0f,  1.0f);     glEnd();

    glColor3f(0.0f, 1.0f, 0.0f);     glBegin(GL_QUADS);  //cara trasera
    glVertex3f( 1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glVertex3f( 1.0f,  1.0f, -1.0f);     glEnd();

    glColor3f(0.0f, 0.0f, 1.0f);
    glBegin(GL_QUADS);  //cara lateral izq
    glVertex3f(-1.0f,-1.0f, -1.0f);
    glVertex3f(-1.0f,-1.0f,  1.0f);
    glVertex3f(-1.0f, 1.0f,  1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glEnd();

    glColor3f(1.0f, 1.0f, 0.0f);
    glBegin(GL_QUADS);  //cara lateral dcha
    glVertex3f(1.0f, -1.0f,  1.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);
    glVertex3f(1.0f,  1.0f, -1.0f);
    glVertex3f(1.0f,  1.0f,  1.0f);
    glEnd(); 
    glColor3f(0.0f,      1.0f, 1.0f);
    glBegin(GL_QUADS);  //cara arriba
    glVertex3f(-1.0f, 1.0f,  1.0f);
    glVertex3f( 1.0f, 1.0f,  1.0f);
    glVertex3f( 1.0f, 1.0f, -1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glEnd();
 
    glColor3f(1.0f, 0.0f, 1.0f);
    glBegin(GL_QUADS);  //cara abajo
    glVertex3f( 1.0f,-1.0f, -1.0f);
    glVertex3f( 1.0f,-1.0f,  1.0f);
    glVertex3f(-1.0f,-1.0f,  1.0f);
    glVertex3f(-1.0f,-1.0f, -1.0f);
    glEnd();
}

void display()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    glLoadIdentity();
 
    glTranslatef(0.0f, 0.0f, -5.0f);
 
    glRotatef(anguloCuboX, 1.0f, 0.0f, 0.0f);
    glRotatef(anguloCuboY, 0.0f, 1.0f, 0.0f);
 
    drawCube();
 
    glLoadIdentity();
 
    glTranslatef(0.0f, 0.0f, -5.0f);
    glRotatef(anguloEsfera, 0.0f, 1.0f, 0.0f);
    glTranslatef(3.0f, 0.0f, 0.0f);
 
    glColor3f(1.0f, 1.0f, 1.0f);
    glutWireSphere(0.5f, 8, 8);
 
    glFlush();
    glutSwapBuffers();
 
    anguloCuboX+=0.1f;
    anguloCuboY+=0.1f;
    anguloEsfera+=0.2f;
}

void init()
{
    glClearColor(0,0,0,0);
    glEnable(GL_DEPTH_TEST);
    ancho = 400;
    alto = 400;
}
  void idle()
{
    display();
}
 
void keyboard(unsigned char key, int x, int y)
{
    switch(key)
    {
    case 'p':
    case 'P':
      hazPerspectiva=1;
      reshape(ancho,alto);
      break;
 
    case 'o':
    case 'O':
      hazPerspectiva=0;
      reshape(ancho,alto);
      break;
 
    case 27:   // escape
      exit(0);
      break;
    }
}
 
int main(int argc, char **argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(ancho, alto);
    glutCreateWindow("Cubo 1");
    init();
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutIdleFunc(idle);
    glutKeyboardFunc(keyboard);
    glutMainLoop();
    return 0;
}
 

4.4.2 Análisis del código

Se pasa ahora a comentar el código. Se reproduce el código entero, pero se irán haciendo pausas en las funciones que no se hayan explicado en el capítulo 2.

Se empieza por la función main():

int main(int argc, char **argv)

     {

         glutInit(&argc, argv);

         glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);

 En esta ocasión, se utiliza GLUT_DOUBLE en vez de GLUT_SIMPLE. Esto hace posible la utilización de la técnica de “double buffer”, con la que se utilizan dos buffers, uno para pintar y otro para visualizar por pantalla, en vez de uno. Con esto se consigue una mayor fluidez en las escenas animadas.

    glutInitWindowPosition(100, 100);
    glutInitWindowSize(ancho, alto);
    glutCreateWindow("Cubo 1");
    init();
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutIdleFunc(idle);

Aquí se añade una función callback nueva, la de idle. Esta función es llamada cuando el programa no tiene nada que hacer (está en idle, ocioso), es decir, no se está realizando ninguna tarea o no está procesando ningún evento de entrada. Normalmente se ejecuta la misma función que la de dibujado, como en este ejemplo (la función idle() que hemos definido simplemente llama a la función display()).

    glutKeyboardFunc(keyboard);

Otro callback nuevo, usado para capturar y manejar el teclado cuando la ventana está activa. La definición de esta función ha de ser de la forma:

    void keyboard(unsigned char tecla, int x, int y)

donde “tecla” es el código ASCII de la tecla pulsada en cada momento y “x” e “y” las coordenadas del ratón en ese instante.

    glutMainLoop();
    return 0;
} 

Ahora se verá la función drawCube, que se utilizará para crear un cubo. El cubo está centrado en el origen y el lado es igual a 2. 

void drawCube(void)
{
    glColor3f(1.0f, 0.0f, 0.0f);
    glBegin(GL_QUADS);  //cara frontal (C0)
    glVertex3f(-1.0f, -1.0f,  1.0f);
    glVertex3f( 1.0f, -1.0f,  1.0f);
    glVertex3f( 1.0f, 1.0f,  1.0f);
    glVertex3f(-1.0f, 1.0f,  1.0f);
    glEnd();

Con estas líneas se crea un polígono cuadrado, ensamblando cuatro vértices. Además se utiliza previamente glColor para asignarle el color rojo. Es importante observar que se utiliza el sentido antihorario, para que la normal del polígono vaya hacia fuera (recordemos que, por defecto, el sentido antihorario es el que define la normal hacia fuera). Este cuadrado conformará la cara del cubo frontal, la más cercana al observador.

    glColor3f(0.0f, 1.0f, 0.0f);
    glBegin(GL_QUADS);  //cara trasera (C1)
    glVertex3f( 1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f,  1.0f, -1.0f);
    glVertex3f( 1.0f,  1.0f, -1.0f);
    glEnd();

Se define aquí la cara trasera, la más alejada al observador, de color verde.

    glColor3f(0.0f, 0.0f, 1.0f);
    glBegin(GL_QUADS);  //cara lateral izq (C2)
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f,  1.0f);
    glVertex3f(-1.0f,  1.0f,  1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glEnd();

Cara lateral izquierda, de color azul.

    glColor3f(1.0f, 1.0f, 0.0f);
    glBegin(GL_QUADS);  //cara lateral dcha (C3)     glVertex3f( 1.0f, -1.0f,  1.0f);
    glVertex3f( 1.0f, -1.0f, -1.0f);
    glVertex3f( 1.0f,  1.0f, -1.0f);
    glVertex3f( 1.0f,  1.0f,  1.0f);     glEnd();

Cara lateral derecha, de color amarillo, por ser mezcla de rojo y verde.

    glColor3f(0.0f, 1.0f, 1.0f);
    glBegin(GL_QUADS);  //cara arriba (C4)
    glVertex3f(-1.0f,  1.0f,  1.0f);
    glVertex3f( 1.0f,  1.0f,  1.0f);
    glVertex3f( 1.0f,  1.0f, -1.0f);
    glVertex3f(-1.0f,  1.0f, -1.0f);
    glEnd(); 

Ésta será la cara de arriba, de color azul claro

    glColor3f(1.0f, 0.0f, 1.0f);
    glBegin(GL_QUADS);  //cara abajo (C5)
    glVertex3f( 1.0f, -1.0f, -1.0f);
    glVertex3f( 1.0f, -1.0f,  1.0f);
    glVertex3f(-1.0f, -1.0f,  1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glEnd(); 
}

 Y, finalmente, la cara de abajo, de color violeta. 

El cubo que se ha definido se puede ver de una forma más grafica en la ilustración 4.9 Sobre los colores se hablará en el próximo capítulo.

 

Ilustración 4 .9

Vamos ahora con el contenido de los callbacks:

Primero el init(), que como se ha dicho, no es un callback, simplemente activa los estados iniciales de ogl que se necesiten. En este caso, se activa un nuevo estado:

    glEnable(GL_DEPTH_TEST);

para que haga el test de profundidad, utilizando el z-buffer. Además se asigna el ancho y alto de la ventana.

El callback reshape, llamado cuando la ventana se redimensiona:

void reshape(int width, int height)

     {

    glViewport(0, 0, width, height);

    glMatrixMode(GL_PROJECTION);

Hace que la matriz activa sea la de proyección, puesto que es aquí donde se define el tipo de proyección a usar.

    glLoadIdentity();    

Se carga en la matriz de proyección la matriz identidad, para resetearla y poder trabajar sobre ella.

    if(hazPerspectiva)
      gluPerspective(60.0f,(GLfloat)width/(GLfloat)height, 1.0f, 20.0f);
    else
      glOrtho(-4, 4, -4, 4, 1, 10);

La variable “hazPerspectiva”, definida como un entero, hace las funciones de un booleano. Si su valor es cero, hace una proyección ortonormal. Para ello se usa la función glOrtho, definiendo los límites de los planos de trabajo. Si la variable está a uno, se hace una perspectiva con gluPerspective.

    glMatrixMode(GL_MODELVIEW);

Aquí se reactiva la matriz del modelador, que es con la que se trabaja habitualmente.

    ancho = width;
    alto = height;
}

Por último, se actualizan las variables “ancho” y “alto”, con los valores actuales de la ventana.

Veamos ahora el callback del teclado:

void keyboard(unsigned char key, int x, int      y)
{
    switch(key)
    {
    case 'p':
    case 'P':
      hazPerspectiva=1;
      reshape(ancho,alto);
      break; 

Si se pulsa la tecla p (en minúscula o mayúscula) se activa la variable “hazPerspectiva”. Para que se efectúe el cambio, se llama a la función reshape, de una manera un tanto “artificial”, ya que el propósito de esta función es ajustar los parámetros de la proyección ante un cambio de la dimensión de la ventana. En este caso, la ventana no se ha redimensionado, y se llama manualmente a la función con los valores actuales, para realizar el cambio de proyección.

    case 'o':
    case 'O':
      hazPerspectiva=0;
      reshape(ancho,alto);
      break; 

Si la tecla pulsada es la ‘o’, se usa, igual que antes, la proyección ortográfica. 

    case 27:        // escape
      	exit(0);
        break;
    }
}

 En caso de que la tecla sea “ESC” (su código ASCII es el 27), se sale del programa.  

 El último callback a analizar es el que dibuja la escena: 

void display()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

Aquí se limpia el frame buffer, que es el buffer donde se dibuja, y el z-buffer, utilizado para el test de profundidad. 

    glLoadIdentity();

Se resetea la matriz del modelador. 

    glTranslatef(0.0f, 0.0f, -5.0f);

    glRotatef(anguloCuboX, 1.0f, 0.0f, 0.0f);
    glRotatef(anguloCuboY, 0.0f, 1.0f, 0.0f);

Con estas tres líneas se carga en la matriz del modelador una transformación de la siguiente forma: primero se traslada lo que va a dibujar (que será el cubo) cinco unidades hacia atrás, para ponerlo delante de nosotros, dentro del volumen de vista. Luego se gira sobre el eje x y el eje y el número de grados que marquen las variables “anguloCuboX” y “anguloCuboY” respectivamente. Éstas variables se van incrementando en cada frame, como se verá pocas líneas mas abajo.

    drawCube();

Se llama a la función para dibujar un cubo (implementada por nosotros). Al hacerlo, se ejecutarán todas las funciones que crean las caras, centrando el cubo en el origen. Pero, inmediatamente, se modifican las posiciones de los vértices por la matriz de transformación cargada en el modelador, dejando así el cubo donde interesa.

    glLoadIdentity();

Se resetea la matriz del modelador, ya que el cubo ya está situado en su posición y se necesita una nueva matriz de transformación para ubicar la esfera.

    glTranslatef(0.0f, 0.0f, -5.0f);
    glRotatef(anguloEsfera, 0.0f, 1.0f, 0.0f);
    glTranslatef(3.0f, 0.0f, 0.0f);

Con estas nuevas tres líneas, se crea la matriz de transformación para la esfera. Primero se traslada 5 unidades hacia atrás, de forma que queda centrada en el mismo sitio que el cubo. Ahora se rota el sistema de coordenadas tantos grados como contenga la variable “anguloEsfera”, sobre el eje y. Ahora que se tiene rotado el sistema de coordenadas de la esfera, solo hay que desplazar la esfera en el eje x, de forma que según se incrementa “anguloEsfera”, ésta vaya describiendo una circunferencia, de radio el número de unidades que la se desplaza (en este caso 3). 

    glColor3f(1.0f, 1.0f, 1.0f);
    glutWireSphere(0.5f, 8, 8); 

Se activa el color para la esfera, blanco en este caso y se dibuja. Para ello, la librería GLUT proporciona una serie de funciones de alto nivel con objetos comunes, como cubos, cilindros, esferas, etc. Además GLUT permite dibujarlas como “Solid” o como “Wire”, es decir, como un objeto sólido, conformado por polígonos, o como un objeto “de alambre”. Los parámetros de glutWireSphere (y también los de glutSolidSphere) son el radio, el número de líneas de longitud y el número de líneas de latitud.

Para dibujar el cubo se puede utilizar también la función glutSolidCube, que tiene como parámetro la longitud del lado, pero se ha preferido crear una función que lo haga, con fines didácticos.

    glFlush();

Se hace que todos los comandos de ogl que estén en espera se ejecuten.

    glutSwapBuffers();

Al utilizar la técnica de doble buffer, se tiene que llamar siempre a esta función al final del pintado de cada frame, para que vuelque de un buffer a otro el frame correspondiente.

    anguloCuboX+=0.1f;
    anguloCuboY+=0.1f;
    anguloEsfera+=0.2f;
} 

Por último, se incrementan todos los ángulos que se están usando. 

En la ilustración 4.10 se ve uno de los frame de la salida del programa.

 


Ilustración4 .10

4.5 Mirar hacia un punto

La función en la librería de utilidades denominada gluLookAt() sitúa el punto de vista respecto al punto de atención, la sintaxis de la función es la siguiente:


void gluLookAt(
    Gldouble P_vistaX,
    Gldouble P_vistaY,
    Gldouble P_vistaZ,
    Gldouble P_atencionX,
    Gldouble P_atencionY,
    Gldouble P_atencionZ,
    Gldouble arribaX,
    Gldouble arribaY,
    Gldouble arribaZ)

 P_vista y P_atención son las coordenadas tridimensionales del punto de vista (donde está el observador o la cámara) y del punto de atención (al objeto donde apunta o está mirando la cámara) respectivamente y ambos definen la línea de visión. Los puntos Arriba y P_vista definen el vector que indica donde esta la parte superior del plano de vista (indica donde está el cielo). La función realiza internamente las transformaciones de modelado necesarias para situar la cámara en la posición adecuada, por ello es importante llamarla antes de construir la escena porque, de lo contrario, habría objetos que no se verían afectados.