Resumen:
En este Blog se explica en una función de C++ como dibujar una esfera en el espacio 3D con funciones de OpenGL.
Las librerías de OpenGL incluyen el dibujo de ciertos objetos como Cilindros, discos, y por supuesto esferas.
Para dibujar una esfera solo es necesario declarar un puntero de tipo GLUquadric
GLUquadric *quadObj;
Inicializar el objeto y habilitarle su textura en la función donde se inicializan los parámetros de OpenGl
quadObj = gluNewQuadric();
gluQuadricTexture(quadObj, GL_TRUE);
Y en nuestra función de dibujo simplemente llamamos a la siguiente función:
gluSphere (quadObj, 0.5, 40, 40);
Donde el segundo argumento es el radio, el tercero y cuarto son las divisiones longitudinales y latitudinales respectivamente.
En este Blog explicaremos como hacer los cálculos para dibujar tu propia esfera a partir de primitivas triangulares, como tapizarlas y aplicarles luz:
**Se incluye un código fuente ejemplo en Microsoft Visual C++ Express 2010 para descargar**
Pre-requisitos:
En este blog no se explica la inicialización de OpenGL ni el manejos de la matrices de proyección ni visualización, sin embargo estos detalles están disponibles para consulta en el código fuente.
La ecuación de una esfera en el espacio cartesiano:
Cada punto en espacio de la superficie de una esfera puede ser definido usando las coordenadas esféricas (r, ϕ, θ) mediante las siguientes ecuaciones paramétricas:
donde r es el radio (Constante), y (θ, ϕ) son los parámetros angulares de la esfera (Variables).
El Angulo θ (Teta) representa el barrido Longitudinal mientras que ϕ (Phi) representa el barrido Latitudinal.
Sin embargo al momento de dibujar en OpenGL los ejes cambian como se muestra a continuación:
Para dibujar todos los puntos de una esfera se tiene que barrer los parámetros angulares θ (teta) y ϕ (Phi) dentro de sus rangos.
Por supuesto que para que la esfera sea una superficie sólida, el barrido debe ser continuo. En nuestro programa realizaremos este barrido mediante bucles anidados para lo cual es necesario definir un incremento o Step lo suficientemente pequeño a fin de obtener la mayor cantidad de puntos de la superficie.
Bucles Anidados:
Los bucles anidados son dos sentencias for() que nos permiten recorrer toda la superficie de la esfera con incrementos angulares fijos múltiplos enteros de Phi, en el caso de este ejemplo utilizaremos el valor(PI=π) PI/40, y tendrá la siguiente codificación:
increRad=PI/40;
…
for(teta=0;teta<PI;teta+=increRad){
for(phi=0;phi<2*PI;phi+=increRad){
Vertice1z=(radio)*((float) sin(teta))*((float)cos(phi));
Vertice1x=(radio)*((float) sin(teta))*((float)sin(phi));
Vertice1y=(radio)*((float) cos(teta));
}
}
Esto quiere decir que el bucle for() para el ángulo Teta tendrá un total de 40 ciclos:
Mientras que Phi tendrá un total de 80 ciclos:
Si multiplicamos el total ciclos de cada bucle, obtendremos el total de ciclos de los bucles anidados :
Esto quiere decir que con un incremento angular (step) de PI/40 tenemos 3200 ciclos.
Uso de Triángulos
En cada uno de esto 3200 ciclos procederemos a dibujar un segmento plano conformado por dos triángulos.
En cada uno de los ciclos tenemos un valor para los ángulos Phi y Teta, pero para dibujar un segmento plano necesitamos cuatro vértices, estos los obtenemos al movernos en los ángulos próximos tanto en Phi como en Teta de la siguiente forma:
1. vértice 1 , ángulos = Phi , Teta
2. vértice 2 , ángulos = Phi+increRad , Teta
3. vértice 3 , ángulos = Phi+increRad , Teta +increRad
4. vértice 4 , ángulos = Phi, Teta+increRad
El movimiento de los vértices se ve de la siguiente forma:
Los triángulos en OpenGL se dibujan invocando la función glBegin con el Parámetro GL_TRIANGLES para que cada tres vértices se dibuje un triangulo.
Los vértices en el espacio de coordenadas x y z son definidos mediante la función glVertex3f(x,y,z)
Los vértices cuatro vértices se calculan de la siguiente forma:
//VERTICE 1
Vertice1z=(radio)*((float) sin(teta))*((float)cos(phi));
Vertice1x=(radio)*((float) sin(teta))*((float)sin(phi));
Vertice1y=(radio)*((float) cos(teta));
//VERTICE 2
Vertice2z=(radio)*((float) sin(teta+increRad))*((float)cos(phi));
Vertice2x=(radio)*((float) sin(teta+increRad))*((float)sin(phi));
Vertice2y=(radio)*((float) cos(teta+increRad));
//VERTICE 3
Vertice3z=(radio)*((float) sin(teta+increRad))*((float)cos(phi+increRad));
Vertice3x=(radio)*((float) sin(teta+increRad))*((float)sin(phi+increRad));
Vertice3y=(radio)*((float) cos(teta+increRad));
//VERTICE 4
Vertice4z=(radio)*((float) sin(teta))*((float)cos(phi+increRad));
Vertice4x=(radio)*((float) sin(teta))*((float)sin(phi+increRad));
Vertice4y=(radio)*((float) cos(teta));
Y los Triángulos que forma en segmento plano se dibujan de la siguiente forma:
glBegin(GL_TRIANGLES);
//TRIANGULO 1
glVertex3f(Vertice1x, Vertice1y,Vertice1z);
glVertex3f(Vertice2x, Vertice2y,Vertice2z);
glVertex3f(Vertice3x, Vertice3y,Vertice3z);
//TRIANGULO2
glVertex3f(Vertice1x, Vertice1y,Vertice1z);
glVertex3f(Vertice3x, Vertice3y,Vertice3z);
glVertex3f(Vertice4x, Vertice4y,Vertice4z);
glEnd();
Es importante destacar que como el incremento o step angular es igual para el recorrido de Phi y Teta, pero Phi tiene el doble de ciclos que Teta, entonces los segmentos planos formados por los triángulos serán rectangulares y no cuadrados. Esta diferencia la corregiremos al momento de mapear la textura en el segmento plano.
Vector normal:
Para activar la iluminación, es necesario definir un vector normal para cada uno de los segmentos cuadrados de la esfera.
Como los triángulos contiguos representan el mismo segmento plano de la superficie de la esfera, tanto el triangulo 1 como el triangulo 2 llevan el mismo vector normal.
Un vector normal es un vector perpendicular a la superficie plana y es necesario declararlo para que OPEN GL pueda calcular el efecto de la iluminación sobre la superficie.
Como todos los puntos en la superficie pueden ser considerados vectores radiales cuyo origen es el centro de la esfera,
Podemos definir para cada segmento plano de la esfera un vector normal tomando los valores x, y, z del vertice1 y normalizándolos (simplemente no los multiplicamos por el valor constante del radio):
En código por cada ciclo sería el siguiente:
glNormal3f((float)1.5*sin(teta)*sin(phi),(float)1.5*cos(teta),(float)1.5*sin(teta)*cos(phi));
Se utilizó en este ejemplo 1.5 en vez de 1 porque así se ve más brillante la esfera:
Textura:
La textura permite tapizar la superficie de esfera con cualquier imagen.
Para obtener este efecto es necesario mapear cada triangulo y segmento plano con una porción de la imagen.
Estas porciones se definen con coordenadas bidimensionales las cuales llamaremos (porcentajex,porcentajey) y representan los puntos de la imagen.
Para cubrir toda la imagen recorreremos estos puntos de forma independiente a los contadores phi y teta utilizados como contadores en los bucles anidados.
El porcentajey representa el movimiento vertical en la imagen y se incrementará en el bucle exterior que es el que barre el ángulo Teta.
Debe tener en cuenta que mientras que el Angulo Teta se incrementa, vamos bajando la esfera de norte a sur. En cambio mientras que porcentajey se incrementa vamos subiendo la imagen de Sur a norte.
Por tanto debemos hacer negativo el incremento de porcentajey (incrementoy) y positivo el incremento de Teta (IncreRad).
El porcentajex se incrementará (incrementox) en el bucle interior que es el que barre el ángulo phi. Ambos crecen en el mismo sentido: de izquierda a derecha (contrario a giro de las manecillas del reloj visto desde arriba).
Como indicamos anteriormente phi tiene el doble de ciclos que Teta, por tanto incrementoy tiene que ser el doble de incrementox
El código fuente quedaría de la siguiente forma:
increRad=PI/40;
porcentajex=0;
porcentajey=1;
incrementox=(float)(1/(80));
incrementoy=(float)(-1/(40));
…
for(teta=0;teta<PI;teta+=increRad){
for(phi=0;phi<2*PI;phi+=increRad){
glBegin(GL_TRIANGLES);
//TRIANGULO 1
glTexCoord2f(porcentajex, porcentajey);
glTexCoord2f(porcentajex, porcentajey+incrementoy);
glTexCoord2f(porcentajex+incrementox, porcentajey+incrementoy);
//TRIANGULO2
glTexCoord2f(porcentajex, porcentajey);
glTexCoord2f(porcentajex+incrementox, porcentajey+incrementoy);
glTexCoord2f(porcentajex+incrementox, porcentajey);
glEnd();
porcentajex+=incrementox;
}
porcentajey+=incrementoy;
porcentajex=0;
}
A continuación presentamos el código completo de la función que dibuja la esfera.
Notaran una sección del código que incrementa el tope de los bucles para Teta y para Phi.
El objetivo de este código es dibujar la esfera de forma progresiva para que el usuario pueda ver el sentido en el que se va dibujando la superficie.
Esta función de dibujo esta contenida en un ciclo infinito dentro de la función WinMain, por tanto será invocada infinitas veces hasta que se concluya que se cierre el programa.
GLvoid MiEsfera(void)
{
int i=0;
int j=0;
float radio=0.5;
float teta;
float phi;
float porcentajex=0;
float porcentajey=1;
float incrementox=(float)(1/((float)2*ResolucionEsfera)); //1/20
float incrementoy=(float)(-1/((float)ResolucionEsfera)); //1/10
float increRad=(float)(PI/ResolucionEsfera);
float Vertice1x,Vertice1y, Vertice1z=0;
float Vertice2x,Vertice2y, Vertice2z=0;
float Vertice3x,Vertice3y, Vertice3z=0;
float Vertice4x,Vertice4y, Vertice4z=0;
//-----------\/---------Construcción de la esfera poco a poco---------------
if(FinalPhi<2*PI)FinalPhi+=increRad/50;/*FinalPhi=2*PI;*/
if(FinalTeta<PI)FinalTeta+=increRad/100;/*FinalTeta=(float)PI;*/
//-----------/\---------Construcción de la esfera poco a poco---------------
for(teta=0;teta<FinalTeta;teta+=increRad){
for(phi=0;phi<FinalPhi;phi+=increRad){
//VERTICE 1
Vertice1z=(radio)*((float) sin(teta))*((float)cos(phi));
Vertice1x=(radio)*((float) sin(teta))*((float)sin(phi));
Vertice1y=(radio)*((float) cos(teta));
//VERTICE 2
Vertice2z=(radio)*((float) sin(teta+increRad))*((float)cos(phi));
Vertice2x=(radio)*((float) sin(teta+increRad))*((float)sin(phi));
Vertice2y=(radio)*((float) cos(teta+increRad));
//VERTICE 3
Vertice3z=(radio)*((float) sin(teta+increRad))*((float)cos(phi+increRad));
Vertice3x=(radio)*((float) sin(teta+increRad))*((float)sin(phi+increRad));
Vertice3y=(radio)*((float) cos(teta+increRad));
//VERTICE 4
Vertice4z=(radio)*((float) sin(teta))*((float)cos(phi+increRad));
Vertice4x=(radio)*((float) sin(teta))*((float)sin(phi+increRad));
Vertice4y=(radio)*((float) cos(teta));
glNormal3f((float)1.5*sin(teta)*sin(phi),(float)1.5*cos(teta),(float)1.5*sin(teta)*cos(phi));
glBegin(GL_TRIANGLES);
//TRIANGULO 1
glTexCoord2f(porcentajex, porcentajey);glVertex3f(Vertice1x, Vertice1y,Vertice1z);
glTexCoord2f(porcentajex,porcentajey+incrementoy);glVertex3f(Vertice2x, Vertice2y,Vertice2z);
glTexCoord2f(porcentajex+incrementox,porcentajey+incrementoy);glVertex3f(Vertice3x,Vertice3y,Vertice3z);
//TRIANGULO
glTexCoord2f(porcentajex, porcentajey);glVertex3f(Vertice1x, Vertice1y,Vertice1z);
glTexCoord2f(porcentajex+incrementox,porcentajey+incrementoy);glVertex3f(Vertice3x, Vertice3y,Vertice3z);
glTexCoord2f(porcentajex+incrementox,porcentajey);glVertex3f(Vertice4x, Vertice4y,Vertice4z);
glEnd();
porcentajex+=incrementox;
}
porcentajey+=incrementoy;
porcentajex=0;
}
}