Nota del traductor: seguí fielmente el texto original, sin embargo muchas palabras y tecnicismos no tienen traducción directa al español, en cualquier parte que se haga referencia a la función de dibujo o de renderización se trata de la función display. El término callback function hace referencia a una función que se llama indirectamente, es decir sin que nosotros especifiquemos explícitamente su llamada, se ha traducido como función indirecta. Transformación de visor se refiere a una Viewport Transformation referente a las transformaciones que afectan la salida que nos da OpenGL como en el caso de la resolución, transformación de vista es una Viewing Transformation, referente a las transformaciones que afectan a la escena desde el punto de vista de la cámara.

INTRODUCCIÓN

Original Author: Damiano Vitulli

Translation by: Click here

En la lección anterior, construimos la primera sección de nuestro proceso de renderizado: cómo adquirir los datos del objeto y almacenarlos en una estructura. Hoy, completaremos los puntos 2 y 3:

  • Las transformaciones para colocar los objetos en el mundo (transformaciones de vista y de modelado)
  • El renderizado de la escena en la pantalla 2d (transformación de proyección, transformación de visor, eliminación de caras ocultas, buffers de color y de profundidad)

Después se introducirán las librerías gráficas de OpenGL y las librerías de utilidades de GLUT. Al final de esta lección seremos capaces de mostrar un objeto sólido girando en nuestra pantalla!

TRANSFORMACIONES PARA COLOCAR LOS OBJETOS EN EL MUNDO (transformaciones de vista y de modelado)

Después de definir la estructura del objeto, debemos aplicar algunas transformaciones a todos los vértices que tenemos antes de que podamos mostrarlo en la pantalla. Queremos crear un mundo, o para ser más exacto, (estando loco por los simuladores espaciales) todo un universo.

La primera transformación es la TRANSFORMACIÓN DE MODELADO. Como nuestra intención es crear naves espaciales que, hasta que se pruebe lo contrario, no son objetos estáticos ;-), tenemos que transformar el sistema de coordenadas local del objeto (que es relativo a la posición central de cada objeto) a un sistema de coordenadas absoluto (que es relativo al centro del universo 3D ;-) ). En otras palabras debemos trasladar el objeto agregando la posición del objeto actual en el universo (la cual puede continuar a cambiar si hay movimiento) a las coordenadas de los vértices locales.

Enseguida está la TRANSFORMACIÓN DE VISTA. Nuestro mayor objetivo, es por supuesto explorar este universo moviéndonos a través de él como queramos, haciendo el monitor una cámara de video con libertad de movimiento. Cómo podemos hacer este tipo de transformación? La respuesta es relativamente simple. De hecho, puedes simplemente considerar que la cámara esté siempre en la posición 0,0 sin rotaciones. Y luego? Simple! Tomamos cualquier transformación que tuviéramos que aplicar a la cámara para obtener un cierto movimiento y aplicamos la transformación opuesta a todos los objetos en lugar de mover la cámara. Supón por ejemplo que quisiéramos mover nuestro punto de vista hacia el objeto +10 puntos en el eje Z y para ver al objeto desde arriba (rotando 40 grados en el eje x). Lo que realmente pasa en casi todos los motores de gráficos es que el objeto ha hecho una traslación de -10 unidades en el eje Z y después una rotación de -40 grados en el eje X. Esto simplifica enormemente el manejo del motor porque la cámara de video siempre estará en el origen.

RENDERIZAR LA ESCENA EN LA PANTALLA 2D (eliminación de caras ocultas, transformaciones de proyección y de visor, buffers de color y de profundidad)

La siguiente operación a realizar es la de ELIMINACIÓN DE CARAS OCULTAS. Esto significa que vamos a excluir los triángulos que no son visibles, son las caras de las partes traseras de los objetos. Podemos ahorrar mucho tiempo de renderizado de esta forma porque la función rederizadora sólo dibujará una parte de todos los triángulos. OpenGL hará esta acción por nosotros.

La siguiente transformación es la TRANSFORMACIÓN DE PROYECCIÓN. En este punto necesitamos 'calcar' una escena 3d en una pantalla 2d. Necesitamos simular el eje Z porque nuestro pobre monitor solo tiene dos ejes X y Y. La forma más fácil de llevar a cabo este tipo de transformación es dividir todas las coordenadas (x,y) de los puntos relativas a su componente Z. El efecto de este procedimiento es comprimir los puntos distantes para que parezca que sigan el punto central x = 0 y y = 0. Esto es exactamente lo que sucede en OpenGL. Muchos de ustedes pueden conocer esto como "proyección de perspectiva".

La última transformación es la TRANSFORMACIÓN DE VISOR. Todos lo que hace es convertir todos los puntos que serán usados a la resolución del visor actual.

Un buffer es una zona de memoria en la que podemos guardar algunos datos. Los buffers de OpenGL son regiones precisamente tan largas como nuestro visor. Por ejemplo, si abrimos una ventana con 640 x 480 de tamaño colocamos un buffer: 640 x 480 = 307200 píxeles. Eso significa, para un modo de color de 16 bits: 307200*16 = 4915200 bits. Eso corresponde a cerca de 614 Kbytes de memoria de video!

OpenGL tiene dos buffers principales: EL BUFFER DE COLOR (que puede ser simple o doble) y el BUFFER DE PROFUNDIDAD.

El BUFFER DE COLOR es lo que vemos en la pantalla y a donde el resultado de todas las operaciones de dibujo van. Después de haber llevado a acabo todos los cálculos geométricos, OpenGL comienza a llenar este buffer píxel por píxel, llenando nuestros triángulos. Si la escena es animada el BUFFER DE COLOR es dibujo y borrado cada fotograma. EL BUFFER DE COLOR es a menudo usado en modo de operación dual, llamado DOBLE BUFFER, que es lo que nosotros usaremos. El modo dual de operación consiste en mostrar un buffer cuando el otro es limpiado y llenado con el siguiente fotograma. Una vez que esta operación es completada los buffers serán volteados. Usando esta técnica, la animación resultante esta prácticamente libre de parpadeos.

Ahora, supón que hay dos triángulos en nuestra escena, uno detrás del otro, los dos visibles. En este caso, el orden en el que los triángulos son dibujos es muy importante. Si dibujamos primero el triángulo más cercano a nuestro punto de vista y después el más distante, los píxeles del más cercano estarán cubiertos por el lejano, creando un efecto desagradable. Una técnica para evitar esto es ordenar todos los triángulos visibles por sus vértices Z. después dibujarlos en orden, empezando por el más lejano al más cercano. Ésta técnica es llamada "EL ALGORITMO DEL DIBUJANTE". No usaremos este método porque OpenGL nos provee con una herramienta más eficiente: el BUFFER DE PROFUNDIDAD. Este buffer tiene las mismas dimensiones que el BUFFER DE COLOR pero en vez de contener los colores de los píxeles contiene la información de cada píxel en el eje Z. Guardar ésta información es muy importante y por una razón muy simple. Cuando vamos a dibujar nuestros triángulos píxel a píxel en la pantalla, primero hacemos una prueba para ver si el píxel a imprimir está más cerca que el píxel ya almacenado en el buffer Z. Si está más cerca entonces actualizamos el buffer de profundidad con el nuevo valor y lo escribimos al BUFFER DE COLOR. Si no está más cerca no consideramos a ese píxel para ser dibujo. Esto produce resultados excelentes!

No necesitamos preocuparnos por escribir estas operaciones "a mano" porque nuestra genial librería GL hará todos los cálculos por nosotros. OpenGL también llevará a cabo todas las operaciones de dibujo de bajo nivel incluyendo dibujar nuestros triángulos y aplicar colores a ellos así como efectos de mapeado e iluminación.

No tenemos que hacer nada? Entonces, qué estamos haciendo aquí? Perdiendo nuestro tiempo? No! Necesitamos proveer a OpenGL con toda la información que necesita para que pueda hacer todos esos cálculos, servir como interfaz con la tarjeta de video y realizar todas las operaciones de bajo nivel usando (si está presente) la aceleración 3d por hardware.

OPENGL, FINALMENTE!

OpenGL es una librería que nos permite interactuar con el hardware de gráficos. Tiene una serie de funciones para dibujar puntos, líneas y polígonos, y lleva a cabo todos los cálculos necesarios para la iluminación, sombreado y transformación de los vértices. Glut por otra parte es una librería de utilidades usada para que OpenGL interactúe con el sistema de ventanas. Nos permite crear una ventana independiente de la plataforma usada (Windows o Linux). También maneja la entrada del teclado.

La estructura de nuestro programa de OpenGL se divide en diferentes secciones:

  1. La función init: usada para cargar OpenGL e iniciar las transformaciones de modelado, de vista y las matrices de proyección. También podemos poner todas las operaciones de iniciación que requiramos en esta función.
  2. La función resize: llamada cada vez que el usuario ejecuta el programa o cambia la resolución de la ventana de salida. Esto es necesario para comunicar el nuevo tamaño del visor a OpenGL.
  3. La función keyboard: llamada cada vez que el usuario presiona una tecla.
  4. La función de renderizado: limpia todos los buffers (de color y de profundidad). Todas las transformaciones de modelado, de vista y de proyección son llevadas a cabo y la escena es dibujada. Finalmente los dos buffers de color son intercambiados.
  5. Ciclo principal: un ciclo infinito que llama todas nuestras funciones cada cuadro.

Una función típica de OpenGL se ve como: glNombreFunción(GL_TIPO argumentos). Para funciones Glut tenemos: glutNombreFunción(argumentos). OpenGL también tiene tipos específicos para ayudar a la portabilidad. Estos tipos empiezan con el prefijo "GL" y son procedidos con "u" (para los valores sin signo) y por el tipo (float, int etc.). Por ejemplo, podemos usar GLfloat o GLuint para definir tipos de variable similares a los tipos "float" y "unsigned int" en C.

CABECERAS

La primera cosa que hacer es incluir todas las cabeceras necesarias: windows.h (para los usuarios de Windows) y glut.h

#include <windows.h>
#include <GL/glut.h>

Al incluir glut.h también hemos incluido indirectamente gl.h y glu.h (las cabeceras de OpenGL). También es muy importante establecer las opciones del enlazador (linker) en el compilador para que incluya las librerías opengl32.lib, glu32.lib and glut32.lib

Ahora, debemos declarar una función que inicializa OpenGL.

FUNCIÓN INIT

void init(void)
{
   glClearColor(0.0, 0.0, 0.2, 0.0);
   glShadeModel(GL_SMOOTH);
   glViewport(0,0,screen_width,screen_height);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   gluPerspective(45.0f,(GLfloat)screen_width/(GLfloat)screen_height,1.0f,1000.0f);
   glEnable(GL_DEPTH_TEST);
   glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
}

Vamos a empezar a analizar el código:

  • void glClearColor( GLfloat rojo, GLfloat verde, GLfloat azul, GLfloat alfa); especifica los valores rojo, verde, azul y alfa usados por glClear para limpiar los buffers de color. Usamos azul oscuro como un color de fondo así que asignamos 0.2 como el componente azul. Los otros colores se declaran a 0.0 y también el valor alfa (explicaré el significado de este componente en otro tutorial). Olvidé decirte que en OpenGL el rango efectivo para los parámetros es 0-1. Así que podemos mezclar todos los componentes para crear cualquier color que queramos. Por ejemplo, para crear un fondo amarillo debemos establecer el componente rojo a 0 y el verde y azul ambos a 1.0.
  • void glShadeModel( GLenum modo ); especifica un valor representando una técnica de sombreado (la forma en la que OpenGL llenará los triángulos). Si usamos GL_FLAT como "modo" entonces cada triángulo será dibujo en modo de sombreado plano y el color será uniforme (el mismo color para cada píxel del triángulo). Si en cambio usamos GL_SMOOTH, OpenGL hará interpolaciones lineares entre los colores de los vértices del triángulo. Esta técnica también es llamada SOMBREADO GOURAUD.
  • void glViewport( GLint x, GLint y, GLsizei anchura, GLsizei altura); establece las dimensiones del visor actual para la transformación de visor.
  • void glMatrixMode( GLenum modo); le dice a OpenGL cual matriz es la matriz actual para operaciones de matriz. Qué es una matriz? Aun no hemos hablado de matrices. Este tema es muy complicado por ahora y le dedicaremos un tutorial completo después. Sólo necesitas saber que una matriz es un objeto usado por OpenGL para hacer todas las transformaciones geométricas por ahora. En "modo" podemos insertar el valor GL_PROJECTION para hacer transformaciones de proyección o GL_MODELVIEW para hacer transformaciones de vista y de modelado. Una vez que especifiquemos la matriz actualmente activa, podemos modificarla como queramos.
  • void glLoadIdentity( void ); reinicia la matriz actualmente activa (GL_PROJECTION en nuestro caso) cargando la matriz de identidad (identity matrix).
  • void gluPerspective(GLdouble fovy, GLdouble aspecto, GLdouble cercano, GLdouble lejano); recuerdas la transformación de proyección? Aquí es donde la ponemos en práctica. OpenGL llena la matriz actualmente activa (GL_PROJECTION) usando los valores especificados en gluPerspective. En "fovy" debemos especificar el ángulo del campo de vista, en "aspecto" el cociente del aspecto, en "cercano" y "lejano" la distancia mínima y máxima para los planos de recorte.
  • void glEnable( GLenum capacidad ); habilita varias capacidades. En esta situación habilitamos el buffer Z (buffer de profundidad).
  • void glPolygonMode( GLenum cara, GLenum modo ); dibuja nuestros polígonos como puntos, líneas o llenos (usando GL_POINT,GL_LINES o GL_FILL como parámetro).

LA FUNCIÓN RESIZE (de reajuste de tamaño)

Esta función es muy similar a "init". Limpia los buffers, redefine nuestro visor, y vuelve a mostrar nuestra escena.

void resize (int width, int height)
{
   screen_width=width; 
   screen_height=height; 
   glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glViewport(0,0,screen_width,screen_height);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   gluPerspective(45.0f,(GLfloat)screen_width/(GLfloat)screen_height,1.0f,1000.0f);
   glutPostRedisplay ();
}

Aquí están las funciones que no hemos cubierto:

  • void glClear( GLbitfield máscara ); limpia los buffers especificados en "máscara". Podemos insertar más de un buffer separándolos con el operador lógico OR "|". En nuestro caso limpiamos los dos buffers de color y el buffer de profundidad.

La llamada a glViewport es necesaria aquí para redefinir el nuevo visor con los nuevos valores almacenados en screen_width y screen_height.

  • void glutPostRedisplay(void); Esta es una función de glut. Llama cualquier rutina que hayamos insertado en la función glutDisplayFunc (llamada en nuestra función principal 'main' ), para volver a renderizar la escena.

LAS FUNCIONES DE TECLADO

Definiremos dos funciones de teclado: una para manejar el carácter ASCII de entrada ("r" y "R" y una carácter en blanco ' ') y otro para manejar las teclas direccionales:

void keyboard (unsigned char key, int x, int y)
{
   switch (key)
   {
      case ' ':
         rotation_x_increment=0;
         rotation_y_increment=0;
         rotation_z_increment=0;
      break;
      case 'r': case 'R':
         if (filling==0)
         {
            glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
            filling=1;
         } 
         else 
         {
            glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
            filling=0;
         }
      break;
   }
}

Usamos tres variables para hacer nuestro objeto girar alrededor del eje deseado: rotation_x_increment, rotation_y_increment y rotation_z_increment. Podemos reiniciar todas estas variables usando la barra espaciadora y pausando el movimiento de nuestro objeto en su posición actual. Podemos también cambiar el modo de dibujo para nuestros polígonos a contorneado o lleno con la tecla "r" o "R".

void keyboard_s (int key, int x, int y)
{
   switch (key)
   {
      case GLUT_KEY_UP:
         rotation_x_increment = rotation_x_increment +0.005;
      break;
      case GLUT_KEY_DOWN:
         rotation_x_increment = rotation_x_increment -0.005;
      break;
      case GLUT_KEY_LEFT:
         rotation_y_increment = rotation_y_increment +0.005;
      break;
      case GLUT_KEY_RIGHT:
         rotation_y_increment = rotation_y_increment -0.005;
      break;
   }
}

Esta función es muy similar a la anterior pero esta maneja las teclas direccionales. Nota las constantes de Glut GLUT_KEY_UP, GLUT_KEY_DOWN, GLUT_KEY_LEFT y GLUT_KEY_RIGHT que identifican sus dirección respectivas e incrementa o disminuye nuestros valores de rotación.

LA FUNCIÓN DISPLAY

Damas y caballeros, la función que han estado esperando: la función de renderizado!

void display(void)
{
   int l_index;
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glTranslatef(0.0,0.0,-50);
   rotation_x = rotation_x + rotation_x_increment;
   rotation_y = rotation_y + rotation_y_increment;
   rotation_z = rotation_z + rotation_z_increment;
   if (rotation_x > 359) rotation_x = 0;
   if (rotation_y > 359) rotation_y = 0;
   if (rotation_z > 359) rotation_z = 0;
   glRotatef(rotation_x,1.0,0.0,0.0);
   glRotatef(rotation_y,0.0,1.0,0.0);
   glRotatef(rotation_z,0.0,0.0,1.0);

La primera parte de ésta función limpia los buffers de color y de profundidad y aplica las transformaciones de vista y de modelado. Establecemos la matriz modelview como la matriz actualmente activa usando glMatrixMode con GL_MODELVIEW. Después, inicializamos esta matriz cada cuadro con una llamada a glLoadIdentity.

  • void glTranslatef( GLfloat x, GLfloat y, GLfloat z ); mueve nuestro objeto en el espacio 3D. Esta función multiplica la matriz de modelo con una matriz de traslado definida usando los parámetros x, y, z. La letra "f" al final del nombre indica que estamos usando valores del tipo float, en lugar de usar la letra "d" que indica parámetros del tipo doble. Usamos glTranslate para mover el objeto 50 puntos hacia adelante en este caso. Recuerdas la cámara de video y la transformación de vista? Bien, podemos considerar esta transformación como una traslación de -50 para nuestra cámara de video. Este movimiento es necesario porque debemos movernos una pequeña distancia lejos del objeto para poder verlo. Una vez que compiles este proyecto puedes tratar de modificar el valor Z sólo para ver cómo afecta la distancia.
  • void glRotatef( GLfloat ángulo, GLfloat x, GLfloat y, GLfloat z ); Esta función multiplica la matriz actual por una matriz de rotación. Es usada para aplicar rotaciones de grados de ángulo alrededor del vector (x,y,z). Las variables rotation_x, rotation_y y rotation_z almacenan los valores de rotación del objeto. Consideramos esta transformación una transformación de modelado porque sólo rotamos el objeto y no el punto de vista. Como puedes ver, las diferencias entre las transformaciones de modelado y de vista no son tan evidentes. Como sea, no necesitamos preocuparnos por eso ahora. Nos enfrentaremos a este tema en el tutorial de cámara libre.
   glBegin(GL_TRIANGLES);
   for (l_index=0;l_index<12;l_index++)
   {
      glColor3f(1.0,0.0,0.0);
      glVertex3f( cube.vertex[ cube.polygon[l_index].a ].x, cube.vertex[ cube.polygon[l_index].a ].y, cube.vertex[ cube.polygon[l_index].a ].z);
      glColor3f(0.0,1.0,0.0);
      glVertex3f( cube.vertex[ cube.polygon[l_index].b ].x, cube.vertex[ cube.polygon[l_index].b ].y, cube.vertex[ cube.polygon[l_index].b ].z);
      glColor3f(0.0,0.0,1.0);
      glVertex3f( cube.vertex[ cube.polygon[l_index].c ].x, cube.vertex[ cube.polygon[l_index].c ].y, cube.vertex[ cube.polygon[l_index].c ].z);
   }
   glEnd();
   glFlush();
   glutSwapBuffers();
}

La segunda parte de la función display usa las funciones glBegin y glEnd. Estos dos comandos marcan los vértices que definen una primitiva gráfica.

  • void glBegin( GLenum modo ); indica el inicio de una lista de datos de vértice que define una primitiva geométrica. En el parámetro "modo" podemos insertar el tipo de primitiva que vamos a dibujar. Hay diez tipos disponibles (GL_TRIANGLES, GL_POYLGON, GL_LINES etc.). Usamos GL_TRIANGLES porque queremos dibujar nuestro cubo usando 12 triángulos. Hacemos esto empezando un ciclo "for" en el que hacemos 3 llamadas a glVertex3f y glColor3f.
  • void glVertex3f( GLfloat x, GLfloat y, GLfloat z ); especifica las coordenadas x, y, z de un vértice. El "3f" indica que hay tres parámetros del tipo float. Sólo necesitas trabajas con valores x y y del tipo doble, el comando correcto es void glVertex2d( GLdouble x, GLdouble y ). Insertamos los datos geométricos de nuestro objeto en cada parámetro.
  • void glColor3f( GLfloat rojo, GLfloat verde, GLfloat azul ); especifica el color actualmente activo. definimos un color diferente (rojo 1,0,0 - verde 0,1,0 - azul 0,0,1) por cada vértice. Nota como el modo de sombreado GL_SMOOTH de OpenGL afecta los colores finales del triángulo. Vemos que los colores son interpolados vértice por vértice así que cambien gradualmente. Eso es algo bueno de ver!
  • void glEnd( void ); termina nuestra definición del objeto.
  • void glFlush( void ); usado para forzar OpenGL a dibujar la escena. Los buffers de color y de profundidad son llenados y la escena es ahora visible. Finalmente!
  • void glutSwapBuffers(void); intercambia el buffer anterior con (donde trabajan todas las funciones de dibujo) con el buffer posterior (lo que vemos en la ventana) cuando se está en modo de doble buffer. Si no estamos en modo de doble buffer esta función no tiene efecto.

LA FUNCIÓN PRINCIPAL (MAIN)

int main(int argc, char **argv)
{

Las siguientes 4 funciones de la librería glut nos permite crear nuestra ventana para la salida gráfica.

   glutInit(&argc, argv);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
   glutInitWindowSize(screen_width,screen_height);
   glutInitWindowPosition(0,0);
   glutCreateWindow("www.spacesimulator.net - 3d engine tutorials: Tutorial 2");
  • void glutInit(&argc, argv); inicializa la librería glut. Necesitamos llamar esta función antes de llamar cualquier otra función de glut
  • void glutInitDisplayMode(unsigned int modo); establece el modo de salida. Las constantes glut GLUT_DOUBLE, GLUT RGB, GLUT DEPTH definen el modo de buffer doble, el modo de color RGB y el buffer de profundidad respectivamente.
  • void glutInitWindowSize(int ancho, int alto); y void glutInitWindowPosition(int x, int y); usados para establecerlas dimensiones y la posición inicial de nuestra ventana de salida.
  • int glutCreateWindow(char *nombre); crea nuestra ventana de salida.

Para definir las llamadas usamos

   glutDisplayFunc(display);
   glutIdleFunc(display);
   glutReshapeFunc (resize);
   glutKeyboardFunc (keyboard);
   glutSpecialFunc (keyboard_s);
   init();
   glutMainLoop();
}
  • void glutDisplayFunc(void (*func) (void)); especifica la función indirecta a llamar cuando la ventana necesita ser mostrada de nuevo, eso es cuando hay una llamada de glutPostRedisplay o cuando hay un error reportado por el sistema de ventanas.
  • void glutIdleFunc(void (*func) (void)); establece la funciónindirecta idle: la función que es llamada cada vez que no hay eventos del sistema . Esto significa que nuestra función idle "display" es llamada continuamente, a menos que eventos de la ventana sean recibidos. Esto mantendrá nuestra animación funcionando.
  • void glutReshapeFunc(void (*func) (void)); establece la función indirecta de cambio de tamaño de la ventana.
  • void glutKeyboardFunc(void (*func) (void)); función indirecta de teclado para caracteres ASCII.
  • void glutSpecialFunc(void (*func) (void)); función indirecta de teclado para caracteres no ASCII.
  • void glutMainLoop(void); empieza el ciclo infinito de glut, con proceso de eventos.

CONCLUSIONES

Y bien? Sí, eso es realmente todo lo que hay! Ya sé, fue un trabajo difícil pero míralo de esta manera! Ahora eres capaz de crear un objeto 3d rotando! Eso significa que hiciste tu primer motor 3d! En la siguiente lección, estudiaremos como hacer el mapeado de texturas.

SOURCE CODE

The Source Code of this lesson can be downloaded from the Tutorials Main Page