<

Nota del traductor: Esto es una traducción del tutorial para la construcción de un motor gráfico tridimensional escrito por Damiano Vitulli en su sitio www.spacesimulator.net. El documento original en inglés se encuentra en https://www.spacesimulator.net/3d_engine_tutorial.html. Esta traducción fue hecha por Ciro Durán <ciro[at]ldc.usb.ve>, el 23 de octubre de 2004, cualquier corrección con respecto a la traducción de este documento puedes enviarme un correo; otros asuntos como los conceptos aquí descritos y el código utilizado deben ser tratados con el autor original de este documento, Damiano Vitulli <damianovitulli[at]gmail.com>.

Acerca del proceso de sintetización de imágenes tridimensionales, utilizaré el término renderizar, pues a pesar de que es un americanismo, describe de manera concisa el concepto que queremos ilustrar, esto aplica igualmente para otros términos secuestrados del idioma inglés, como motor (engine).

INTRODUCCIÓN

Original Author: Damiano Vitulli

Translation by: Click here

En primer lugar, es necesario explicar el concepto básico... ¿qué es un motor 3d? La respuesta es relativamente simple:

un motor 3D es un colección de estructuras, funciones y algoritmos utilizados para visualizar, después de muchos cálculos y transformaciones, objetos tridimensionales en una pantalla bidimensional.

Las secciones principales de un motor 3d son:

  1. La ubicación de los datos de los objetos en estructuras.
  2. Las transformaciones para posicionar los objetos en el mundo.
  3. Renderizar la escena en la pantalla bidimensional.

Es por esto que estamos aquí... así que manos a la obra!

En esta lección estudiaremos cómo definir las estructuras principales necesarias para dibujar un objeto 3d.

LOS VÉRTICES

Supón que tienes un objeto y quieres mostrarlo en una pantalla 2d. Para poder hacer esto, es necesario obtener información acerca de su estructura. ¿Cómo podemos hacer esto?. Primero, debemos definir algunos puntos clave: los vértices del objeto. Cada vértice se compone de tres coordenadas las cuales llamaremos x, y, z. Cada coordenada debe ser expresada con una variable de tipo FLOAT o DOUBLE, necesario pues siempre necesitamos tener la mejor resolución para renderizar la escena. Esta solución es utilizada en casi todos los motores 3d pero no está libre de errores y, de hecho, pierde precisión cuando el valor numérico es muy alto. Esta particularidad limita el espacio en el cual la simulación puede trabajar. En un simulador espacial, deberíamos ser capaces de movernos a través de un espacio infinito así que nos tendremos que enfrentar con este problema eventualmente. Para definir un vértice en C, utilizaremos una estructura compuesta de tres variables x, y, z.

typedef struct {
    float x, y, z;
} vertex_type;

Es muy importante tener en cuenta que todos los cálculos relacionados con el posicionamiento y rotación del objeto son aplicados a los vértices, ya que ellos son las unidades que hacen la estructura básica. La segunda estructura, en términos de importancia, es el polígono.

LOS POLÍGONOS

Los polígonos son las caras del objeto y están compuestos de N vértices. En la mayoría de los motores 3D los polígonos están compuestos de 3 vértices, por lo tanto aquí aplicaremos esta regla también.

typedef struct {
    vertex_type a,b,c;
} polygon_type;

También declararemos un arreglo de tipo polygon_type que contendrá la información de todos los polígonos que constituyen el objeto.

#define MAX_POLYGONS 2000
polygon_type polygon[MAX_POLYGONS];

Una advertencia. Nuestra definición asigna 3 vértices a cada polígono y estos vértices no son compartidos con los otros polígonos. De hecho, si reflexionamos un poco sobre el problema veremos que cada polígono de un objeto en realidad sí comparte sus lados, y también sus vértices, con otros polígonos. ¡Así que hemos hecho un error! Bueno, en realidad no es un error, pero hemos incrementado considerablemente el número real de vértices en una escena bastante más allá de lo necesario. Ya hemos dicho además que el motor utilizará los vértices para hacer la mayoría de sus cálculos, así que deberíamos encontrar otro método para definir los polígonos. Entonces debemos crear una lista de vértices que mantendrá la información de todos los vértices del objeto entero. Luego, para definir los polígonos, utilizaremos una especie de esquema de referenciación para "apuntar" a los vértices de esa lista. Declaramos a continuación un arreglo de tipo vertex_type que contendrá una cantidad MAX_VERTICES de vértices.

#define MAX_VERTICES 2000
vertex_type vertex[MAX_VERTICES];

La estructura del polígono no contendrá más los vértices, sino 3 números solamente que apuntarán a 3 elementos de la lista de vértices. De esta manera más polígonos pueden apuntar al mismo vértice. Esto optimiza muy bien el diseño del motor.

typedef struct {
    int a,b,c;
} polygon_type;

EL OBJETO

Haremos un poco de limpieza aquí y organizaremos las definiciones previamente hechas en una estructura que llamaremos obj_type.

typedef struct {
    vertex_type vertex[MAX_VERTICES];
    polygon_type polygon[MAX_POLYGONS];
} obj_type, *obj_type_ptr;

Eso es solamente una definición básica. En el futuro, agregaremos más campos que identificarán la posición, rotación y estado del objeto.

En este punto podemos declarar la variable que representará el objeto y llenar la lista de vértices:

obj_type obj;

obj.vertex[0].x=0;  obj.vertex[0].y=0;  obj.vertex[0].z=0; //vertex v0
obj.vertex[1].x=1;  obj.vertex[1].y=0;  obj.vertex[1].z=0; //vertex v1
obj.vertex[2].x=1;  obj.vertex[2].y=0;  obj.vertex[2].z=1; //vertex v2
obj.vertex[3].x=0;  obj.vertex[3].y=0;  obj.vertex[3].z=1; //vertex v3
obj.vertex[4].x=0;  obj.vertex[4].y=1;  obj.vertex[4].z=0; //vertex v4
obj.vertex[5].x=1;  obj.vertex[5].y=1;  obj.vertex[5].z=0; //vertex v5
obj.vertex[6].x=1;  obj.vertex[6].y=1;  obj.vertex[6].z=1; //vertex v6
obj.vertex[7].x=0;  obj.vertex[7].y=1;  obj.vertex[7].z=1; //vertex v7

Ahora el problema consiste en cómo subdividir nuestro cubo en triángulos. La respuesta es simple: cada cara del cubo es un cuadrado compuesto por dos triángulos adyacentes. Así que nuestro cubo estará compuesto de 12 polígonos (triángulos) y 8 vértices.

Tut 3dengine cube.png

Una representación gráfica de la subdivisión hecha al cubo para convertirlo en triángulos.

La lista de polígonos debe ser llenada entonces:

obj.polygon[0].a=0;  obj.polygon[0].b=1;  obj.polygon[0].c=4;  //polygon v0,v1,v4
obj.polygon[1].a=1;  obj.polygon[1].b=5;  obj.polygon[1].c=4;  //polygon v1,v5,v4
obj.polygon[2].a=1;  obj.polygon[2].b=2;  obj.polygon[2].c=5;  //polygon v1,v2,v5
obj.polygon[3].a=2;  obj.polygon[3].b=6;  obj.polygon[3].c=5;  //polygon v2,v6,v5
obj.polygon[4].a=2;  obj.polygon[4].b=3;  obj.polygon[4].c=6;  //polygon v2,v3,v6
obj.polygon[5].a=3;  obj.polygon[5].b=7;  obj.polygon[5].c=6;  //polygon v3,v7,v6
obj.polygon[6].a=3;  obj.polygon[6].b=0;  obj.polygon[6].c=7;  //polygon v3,v0,v7
obj.polygon[7].a=0;  obj.polygon[7].b=4;  obj.polygon[7].c=7;  //polygon v0,v4,v7
obj.polygon[8].a=4;  obj.polygon[8].b=5;  obj.polygon[8].c=7;  //polygon v4,v5,v7
obj.polygon[9].a=5;  obj.polygon[9].b=6;  obj.polygon[9].c=7;  //polygon v5,v6,v7
obj.polygon[10].a=3; obj.polygon[10].b=2; obj.polygon[10].c=0; //polygon v3,v2,v0
obj.polygon[11].a=2; obj.polygon[11].b=1; obj.polygon[11].c=0; //polygon v2,v1,v0

Debes tener en cuenta que para definir los polígonos apropiadamente es necesario usar siempre la misma dirección para todos los polígonos en la escena, ya sea sentido horario (en el mismo de las agujas del reloj), o anti-horario (en contra del movimiento de las agujas del reloj). Como veremos en el siguiente tutorial, la forma como la dirección es utilizada para controlar si el polígono es visible o no. Así que presta mucha atención a la manera como defines los polígonos, o muchos de ellos no serán visibles. Nosotros utilizaremos el método anti-horario (por ejemplo, el primer polígono de nuestro cubo está definido por v0, v1, v4 ó v1, v4, v0 ó v4, v0,v1, y no por v1, v0, v4 ó v0, v4, v1 ó v4, v1, v0). UNA MANERA MÁS ELEGANTE DE DEFINIR NUESTRO OBJETO

En C/C++ podemos definir la estructura obj_type utilizando una manera más elegante:

obj_type cube = 
{
    {
        -10,-10, 10, //vertex v0
         10,-10, 10, //vertex v1
         10,-10,-10, //vertex v2
        -10,-10,-10, //vertex v3
        -10, 10, 10, //vertex v4
         10, 10, 10, //vertex v5
         10, 10,-10, //vertex v6 
        -10, 10,-10  //vertex v7
    },  
    {
        0, 1, 4, //polygon v0,v1,v4
        1, 5, 4, //polygon v1,v5,v4
        1, 2, 5, //polygon v1,v2,v5
        2, 6, 5, //polygon v2,v6,v5
        2, 3, 6, //polygon v2,v3,v6
        3, 7, 6, //polygon v3,v7,v6
        3, 0, 7, //polygon v3,v0,v7
        0, 4, 7, //polygon v0,v4,v7
        4, 5, 7, //polygon v4,v5,v7
        5, 6, 7, //polygon v5,v6,v7
        3, 2, 0, //polygon v3,v2,v0
        2, 1, 0, //polygon v2,v1,v0
    }
};

Puedes ver también que hemos cambiados las coordenadas de los vértices de 0,1 a 10, -10. Esto es intencional y, de hecho, necesitamos tener el centro del objeto en el punto 0, 0 (entenderemos el por qué de esto en el siguiente tutorial). Nuestro objeto es también 20 veces más grande (sólo para manejar las coordenadas más fácilmente).

CONCLUSIONES

Eso es todo para esta lección. En la próxima, comenzaremos a utilizar OpenGL para mostrar el cubo en la pantalla.

SOURCE CODE

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