.: Capítulo 3: Dibujando en 3D :.

3.1     Definición de un lienzo en 3D

3.2     El punto en 3D: el vértice

3.3     Las primitivas
3.3.1       Dibujo de puntos (GL_POINTS)
3.3.2       Dibujo de líneas (GL_LINES)
3.3.3       Dibujo de polígonos
3.3.3.1         Triángulos (GL_TRIANGLES)
3.3.3.2         Cuadrados (GL_QUADS)

3.4     Construcción de objetos sólidos mediante polígonos
3.4.1       Color de relleno
3.4.2       Modelo de sombreado
3.4.3       Eliminación de las caras ocultas

 

El dibujo 3D en OpenGL se basa en la composición de pequeños elementos, con los que se va construyendo la escena deseada. Estos elementos se llaman primitivas. Todas las primitivas de ogl son objetos de una o dos dimensiones, abarcando desde simples puntos y líneas, a polígonos complejos. Las primitivas se componen de vértices, que no son más que puntos 3D. En este capítulo se pretende presentar las herramientas necesarias para dibujar objetos en 3D a partir de estas formas más sencillas. Para ello hay que deshacerse de la mentalidad en 2D de la computación gráfica clásica y definir el nuevo espacio de trabajo, ya en 3D.

 

3.1    Definición de un lienzo en 3D

La Ilustración 3.1 muestra un eje de coordenadas inmerso en un volumen de visualización sencillo, que se utilizará para definir y explicar el espacio en el que se va a trabajar. Este volumen se correspondería con una perspectiva ortonormal, como la que se ha definido en el capitulo anterior haciendo una llamada a glOrtho(). Como se puede observar en la figura, para el punto de vista, el eje de las x sería horizontal y crecería de izquierda a derecha; el eje y, vertical y crece de abajo hacia arriba y, por último, el eje z, que sería el de profundidad, crecería hacia nuestras espaldas, por tanto, en la dirección del punto de vista, cuanto más lejos de la cámara esté el punto, menor será su coordenada z.


Ilustración 3 . 1

 

3.2    El punto en 3D: el vértice 

Los vértices (puntos 3D) son el denominador común en cualquiera de las primitivas de OpenGL. Con ellos se definen puntos, líneas y polígonos. La función que define vértices es glVertex, y puede tomar de dos a cuatro parámetros de cualquier tipo numérico. Por ejemplo, la siguiente línea de código define un vértice en el punto (10, 5, 3). 

glVertex3f(10.0f, 5.0f, 3.0f);

Este punto se muestra en la Ilustración 3.2. Aquí se ha decidido representar las coordenadas como valores en coma flotante, y con tres argumentos, x, y, z.


Ilustración 3 . 2

Ahora hay que aprender a darle sentido a estos vértices, que pueden ser tanto la esquina de un cubo como el extremo de una línea.

3.3   Las primitivas

Una primitiva es simplemente la interpretación de un conjunto de vértices dibujados de una manera específica en pantalla. Hay diez primitivas distintas en ogl, pero en estos apuntes se explicarán solamente las más comunes: puntos (GL_POINTS), líneas (GL_LINES), triángulos (GL_TRIANGLES) y cuadrados (GL_QUADS). Se comentarán también las primitivas GL_LINES_STRIP, GL_TRIANGLE_STRIP y GL_QUAD_STRIP, utilizadas para definir “tiras” de líneas, triángulos y de cuadrados respectivamente.

Para crear primitivas en ogl se utilizan las funciones glBegin y glEnd. La sintaxis de estas funciones sigue el siguiente modelo:

glBegin(<tipo de primitiva>);
   glVertex(...);
   glVertex(...);
   ...
   glVertex(...);
glEnd(); 

Puede observarse que glBegin y glEnd actúan como llaves (“{“ y “}”) de las primitivas, por eso es común añadirle tabulados a las glVertex contenidos en ellas. El parámetro de glBeing <tipo de primitiva> es del tipo glEnum  (definido por OpenGL) y será el flag con el nombre de la primitiva (GL_POINTS, GL_QUADS, etc.).

  

3.3.1 Dibujo de puntos (GL_POINTS) 

Es la más simple de las primitivas de ogl. Para comenzar, veamos el siguiente código: 

glBegin(GL_POINTS);
  glVertex3f(0.0f, 0.0f, 0.0f);
  glVertex3f(10.0f, 10.0f, 10.0f);
glEnd(); 

El parámetro pasado a glBegin es GL_POINTS, con lo cual interpreta los vértices contenidos en el bloque glBeing-glEnd como puntos. Aquí se dibujarán dos puntos, en (0, 0, 0) y en (10, 10, 10). Se pueden listar múltiples primitivas entre llamadas mientras que sean para el mismo tipo de primitiva. El siguiente código dibujara exactamente lo mismo, pero invertirá más tiempo en hacerlo, decelerando la velocidad de nuestra aplicación:

glBegin(GL_POINTS);
  glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();
 
glBegin(GL_POINTS);
  glVertex3f(10.0f, 10.0f, 10.0f);
glEnd();

 

3.3.2 Dibujo de líneas (GL_LINES)

En el dibujado de puntos, la sintaxis era muy cómoda: cada vértice es un punto. En las líneas, los vértices se cuentan por parejas, denotando punto inicial y punto final de la línea. Si se especifica un número impar de vértices, el último de ellos se ignora.

Acuérdate de añadir la librería math.h en un include al principio del código para que compilen las funciones se seno y coseno.

El siguiente código dibuja una serie de líneas radiales:

GLfloat angulo;
int i;
glBegin(GL_LINES);
for (i=0; i<360; i+=3)
{
      angulo = (GLfloat)i*3.14159f/180.0f; // grados a radianes
      glVertex3f(0.0f, 0.0f, 0.0f);
      glVertex3f(cos(angulo), sin(angulo), 0.0f);
}
glEnd();

Este ejemplo dibuja 120 líneas en el mismo plano (ya que en los puntos que las definen z = 0.0f), con el mismo punto inicial (0,0,0) y puntos finales describiendo una circunferencia. El resultado sería el de la Ilustración 3.3.


Ilustración 3 . 3

Si en vez de GL_LINES utilizásemos GL_LINE_STRIP, ogl ya no trataría los vértices en parejas, si no que el primer vértice y el segundo definirían una línea, y el final de ésta definiría otra línea con el siguiente vértice y así sucesivamente, definiendo un segmento continuo. Veamos el siguiente código como ejemplo:

glBegin(GL_LINE_STRIP);
      glVertex3f(0.0f, 0.0f, 0.0f);  // V0
      glVertex3f(2.0f, 1.0f, 0.0f);  // V1
      glVertex3f(2.0f, 2.0f, 0.0f);  // V2
glEnd();
 Este código construiría las líneas como se ve en la Ilustración 3.4.


Ilustración 3 . 4

Mencionar, por último, la primitiva GL_LINE_LOOP que funciona igual que GL_LINE_STRIP pero, además, une el último vértice con el primero, creando siempre una cuerda cerrada.

3.3.3 Dibujo de polígonos

En la creación de objetos sólidos, el uso de puntos y líneas es insuficiente. Se necesitan primitivas que sean superficies cerradas, rellenas de uno o varios colores que, en conjunto, modelen el objeto deseado. En el campo de la representación 3D de los gráficos en computación, se suelen utilizar polígonos (que a menudo son triángulos) para dar forma a objetos “semisólidos” (ya que en realidad son superficies, están huecos por dentro). Ahora se verá la manera de hacer esto mediante las primitivas GL_TRIANGLES y GL_QUADS.

3.3.3.1   Triángulos (GL_TRIANGLES)

El polígono más simple, es el triángulo, con sólo tres lados. En esta primitiva, los vértices van de tres en tres. El siguiente código dibuja dos triángulos, como se muestra en la Ilustración 3.5:

glBegin(GL_TRIANGLES);
      glVertex3f(0.0f, 0.0f, 0.0f); // V0
      glVertex3f(1.0f, 1.0f, 0.0f); // V1
glVertex3f(2.0f, 0.0f, 0.0f); // V2
  glVertex3f(-1.0f, 0.0f, 0.0f); // V3
      glVertex3f(-3.0f, 2.0f, 0.0f); // V4
      glVertex3f(-2.0f, 0.0f, 0.0f); // V5
glEnd();
 


Ilustración 3.5

 

Es muy importante el orden en que se especifican los vértices. En el primer triángulo (el de la derecha), se sigue la política de “sentido horario”, y en el segundo, “sentido antihorario”. Cuando un polígono cualquiera, tiene sentido horario (los vértices avanzan en el mismo sentido que las agujas del reloj), se dice que es positivo; en caso contrario, se dice que es negativo. OpenGL considera que, por defecto, los polígonos que tienen sentido negativo tienen un “encare frontal”. Esto significa que el triangulo de la izquierda nos muestra su cara frontal, y el de la derecha su cara trasera. Más adelante se verá que es sumamente importante mantener una consistencia en el sentido de los triángulos al construir objetos sólidos.

En OpenGL, por defecto, se muestran las caras frontales y traseras de los polígonos. Sin embargo, es habitual querer que únicamente se rendericen las caras frontales y no las traseras. Para habilitar que OpenGL sólo visualice las caras frontales se utiliza la llamada glEnable(GL_CULL_FACE) que, utilizada en el ejemplo anterior, provoca que sólo se visualice el triángulo izquierdo (Puedes ponerla en la sección de inicialización-init del programa).

Si se necesita invertir el comportamiento por defecto de ogl, basta con una llamada a la función glFrontFace(). Ésta acepta como parámetro GL_CW (considera los polígonos positivos con encare frontal) ó GL_CCW (considera los polígonos negativos con encare frontal).

Como con la primitiva de líneas, con triángulos también existe GL_TRIANGLE_STRIP, y funciona como se puede observar en la Ilustración 3.6, que sigue el siguiente pseudo-código:

glBegin(GL_TRIANGLE_STRIP);
      glVertex(v0);
      glVertex(v1);
      ...
glEnd(); 


Ilustración 3 . 6

 

Por último comentar que la manera más eficiente de componer objetos y dibujarlos es mediante triángulos, ya que sólo necesitan tres vértices.

3.3.3.2   Cuadrados (GL_QUADS) 

Esta primitiva funciona exactamente igual que GL_TRIANGLES, pero dibujando cuadrados. También tiene la variación de GL_QUAD_STRIP, para dibujar “tiras” de cuadrados.

 

3.4   Construcción de objetos sólidos mediante polígonos 

Componer un objeto sólido a partir de polígonos implica algo más que ensamblar vértices en un espacio coordenado 3D. Se muestran, a continuación, una serie de puntos a tener en cuenta para construir objetos, aunque dando sólo un breve acercamiento.

 

3.4.1 Color de relleno 

Para elegir el color de los polígonos, basta con hacer una llamada a glColor entre la definición de cada polígono. Por ejemplo, modificando el código que dibujaba dos triángulos: 

glBegin(GL_TRIANGLES);
      glColor3f(1.0f, 0.0f, 0.0f);
      glVertex3f(0.0f,0.0f, 0.0f);
      glVertex3f(2.0f,0.0f, 0.0f);
      glVertex3f(1.0f,1.0f, 0.0f);
 
      glColor3f(0.0f,1.0f, 0.0f);
      glVertex3f(-1.0f,0.0f, 0.0f);
      glVertex3f(-3.0f,2.0f, 0.0f);
      glVertex3f(-2.0f,0.0f, 0.0f);
glEnd(); 

Esta modificación provocará que el primer triángulo se pinte en rojo y el segundo en verde. La función glColor define el color de rellenado actual y lleva como parámetros los valores de las componentes RGB del color deseado y, opcionalmente, un cuarto parámetro con el valor alpha. Estos parámetros son flotantes y se mueven en el rango [0.0,1.0]. Con ello se pueden componer todos los colores del modo de video usado en ese instante.

 

3.4.2 Modelo de sombreado 

Es el método que utiliza OpenGL para rellenar de color los polígonos. Se especifica con la función glShadeModel. Si el parámetro es GL_FLAT, ogl rellenará los polígonos con el color activo en el momento que se definió el último parámetro; si es GL_SMOOTH, ogl rellenará el polígono interpolando los colores activos en la definición de cada vértice.

 Este código es un ejemplo de GL_FLAT: 

glShadeModel(GL_FLAT);
glBegin(GL_TRIANGLES);  
      glColor3f(1.0f, 0.0f, 0.0f);  // activamos el color rojo
glVertex3f(-1.0f, 0.0f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);  // activamos el color verde
      glVertex3f(1.0f, 0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);  // activamos el color azul
      glVertex3f(0.0f, 1.0f, 0.0f);
glEnd();

La salida sería la Ilustración 3.7


Ilustración 3 . 7

 El triángulo se rellena con el color azul, puesto que el modelo de sombreado es GL_FLAT y el color activo en la definición del último vértice es el azul. Sin embargo, este mismo código, cambiando la primera línea:

glShadeModel(GL_SMOOTH);
glBegin(GL_TRIANGLES);  
      glColor3f(1.0f, 0.0f, 0.0f);  // activamos el color rojo
glVertex3f(-1.0f, 0.0f, 0.0f);
glColor3f(0.0f,1.0f, 0.0f);  // activamos el color verde
      glVertex3f(1.0f,0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);  // activamos el color azul
      glVertex3f(0.0f, 1.0f, 0.0f);
glEnd();

produciría una salida similar a la de la Ilustración 3.8, donde se aprecia claramente la interpolación de colores.


Ilustración 3 . 8

 

3.4.3 Eliminación de las caras ocultas

Cuando se tiene un objeto sólido, o quizá varios objetos, algunos de ellos estarán más próximos a nosotros que otros. Si un objeto está por detrás de otro, evidentemente, sólo se debería ver el de delante, que tapa a los de atrás. OpenGL, por defecto, no tiene en cuenta esta situación, de forma que simplemente va pintando en pantalla los puntos, líneas y polígonos siguiendo el orden en el que se especifican en el código. Veamos un ejemplo:

glColor3f(1.0f, 1.0f, 1.0f);      // activamos el color blanco
glBegin(GL_TRIANGLES);
      glVertex3f(-1.0f, -1.0f, -1.0f);
      glVertex3f(1.0f, -1.0f, -1.0f);
      glVertex3f(0.0f, 1.0f, -1.0f);
glEnd();
 
glColor3f(1.0f, 0.0f, 0.0f); // activamos el color rojo
glBegin(GL_POINTS);
      glVertex3f(0.0f, 0.0f, -2.0f);
      glVertex3f(2.0f, 1.0f, -2.0f);
glEnd(); 

Aquí se está pintando un triángulo en el plano z = -1. Luego se pintan dos puntos, el primero en (0,0,-2) y el segundo en (2,1,-2). Ambos están en el plano z = -2, es decir, más lejos de nuestro punto de vista que el triángulo. El primer punto lo debería tapar el triángulo pero, como por defecto OpenGL no comprueba qué es lo que está por delante y por detrás, lo que hace es pintar primero el triángulo y después los puntos, quedando un resultado como el de la Ilustración 3.9. 


Ilustración 3 . 9  

Para solucionar esto, se introduce aquí un de los buffers que ogl pone a nuestra disposición, el “depth buffer” (buffer de profundidad, también conocido como “z-buffer”). En él se almacenan “las zetas” o distancias desde el punto de vista a cada píxel de los objetos de la escena y, a la hora de pintarlos por pantalla, hace una comprobación de que no haya ninguna primitiva que esté por delante tapando a lo que se va a pintar en ese lugar.

Para activar esta característica, llamada “depth test”, hay que hacer una modificación de su variable en la máquina de estados de OpenGL, usando glEnable, de la siguiente forma:

glEnable(GL_DEPTH_TEST);

Puedes poner esta instrucción también en la parte de inicialización-init. Para desactivarlo, se usará la función glDisable con el mismo parámetro.

El uso del “test de profundidad” necesita también que, de la misma forma que se hacía al principio un glClear con la variable GL_COLOR_BUFFER_BIT, para que se borre la pantalla antes de dibujar cada frame, también hay que borrar el depth buffer, por si algún objeto se ha movido (ha cambiado su coordenada z) y ha cambiado la situación de la escena. Añadir para ello la siguiente línea:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Se puede utilizar el operador binario “or” ya que las definiciones de ogl terminadas en “_BIT” son flags, pudiéndose combinar varias en una misma función.

Añadiendo la línea con la instrucción glEnable al principio del código del anterior ejemplo y modificando la instrucción glClear, se obtendrá el efecto de ocultación deseado, como se puede ver en la Ilustración 3.10.


Ilustración 3 . 10

Gracias a la eliminación de las caras ocultas, se gana realismo en la escena y, al tiempo, se ahorra el procesado de todos aquellos polígonos que no se ven, ganando también en velocidad.