Iniciación
Este documento es una pequeña introducción al mundo de la programación grafica mediante una tecnología desarrollada por parte de Microsoft, llamada Direct3D. No se profundiza cada situación de las múltiples casuísticas que se pueden dar ni tampoco se realiza un código donde se peca de over-engineering. Es un primer contacto, código simple y fácil de leer. Para ello es necesario tener conocimientos de Visual Studio y de Visual C++. Con un nivel básico/intermedio es suficiente.
Direct3D es una colección de APIs que forman parte del paquete DirectX, siendo sus principales funciones el renderizado de gráficos 3D utilizando el hardware de la GPU (una tarjeta gráfica, por ejemplo). Prácticamente el software se encarga de mandar los recursos y los comandos para dibujar a la GPU, siendo esta la que los ejecuta en su hardware. También se puede utilizar para el computing (cálculos paralelos), pero no vamos a entrar en esa parte.
De todas las versiones se ha decidido utilizar la versión 11 del Direct3D (actualmente está por la 12). Aunque sea una tecnología presentada en 2008, se ha ido actualizando durante los años, hasta la 11.4 lanzada en 2016.
La versión 11 es suficientemente potente para poder realizar un renderizado 3D complejo y a muy buen rendimiento. Es muy recomendable para proyectos que no necesitan estrujar literalmente el hardware como hace la versión 12. También es la última que utiliza los estados. Es decir, primero se sestean los parámetros en la GPU y luego se lanza el comando para dibujar. Esto impide el multithreading al tener que esperar que se lance el comando antes de cambiar los estados para el siguiente comando desde la CPU a la GPU. La versión 12 ya no utiliza los estados, sino que los prepara anteriormente y la CPU los puede lanzar a la GPU desde varios threads (si se desea) y es ella la que se encarga de ejecutarlos según se han ido introduciendo en los pools.
Como información adicional, la versión 12 esta a la altura de Vulkan (Khronos Group, los mismos del OpenGL) y del Metal (macOS) las tres teniendo su base en una tecnología desarrollada por AMD llamada Mantle. Todas estas, casi rozan el masoquismo.
Una imagen vale más que mil palabras sobre las diferencias entre las dos versiones:
Como se ha dicho anteriormente, la GPU necesita recursos que procesar. Estos recursos suelen ser texturas, buffers y constantes. Normalmente los necesitamos todos, pero para esta introducción vamos a utilizar solo los buffers y las constantes.
Cuando se trata de dibujar geometría la GPU entiende únicamente de puntos, líneas y triángulos. En gran medida se utilizarán los triángulos, así que será nuestra geometría base a utilizar. Los triángulos como bien sabemos se componen por tres puntos. A cada punto se le llama vértice. Y cada vértice almacena información, como mínimo su posición en el espacio 3D. Dicho de otro modo, sus coordenadas XYZ. Mas adelante veremos que se les puede añadir más información.
En la imagen anterior se puede ver los triángulos que componen un modelo. OJO, cada triangulo tiene sus propios vértices, aunque se vea que un vértice es compartido por varios triángulos, en realidad es uno por cada triangulo teniendo la misma posición XYZ.
Aquí podemos tomar dos caminos. El primero es el de almacenar todos los vértices en un único buffer y procesarlos. El segundo es de almacenar solo los vértices únicos en el buffer de vértices y almacenar en un segundo buffer, llamado el buffer de índices, la construcción de nuestra geometría.
Un ejemplo simplificado sería la de un cuadrado, para la GPU son dos triángulos rectángulos pegados por la hipotenusa.
Aquí tenemos los dos vértices del punto 0 y los dos vértices del punto 2 que son iguales entre sí.
Entonces, sí solo guardan la información XYZ y cada lado mide 1 siendo el centro el punto origen (0.0f,0.0f). Componiendo los triangulo en este orden 012 y 023 tendríamos:
Primer camino:
vertex buffer:
{-0.5f, 0.5f, 0.0f}, {0.5f, 0.5f, 0.0f}, {0.5f, -0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f}, {0.5f, -0.5f, 0.0f}, {-0.5f, -0.5f, 0.0f}
Segundo camino:
vertex buffer:
{-0.5f, 0.5f, 0.0f}, {0.5f, 0.5f, 0.0f}, {0.5f, -0.5f, 0.0f}, {-0.5f, -0.5f, 0.0f}
index buffer
{0,1,2} {0,2,3}
Como se puede ver, el segundo camino es más económico (y rápido), sobre todo si lo aplicamos a geometría compleja con millones de triangulos. Vamos a tomar el segundo camino ya que es mejor acostumbrarse a él.
Antes de mandar a dibujar un modelo, se tendrá que indicar a la GPU la composición de un vertice, para que ella sepa que significa cada float de nuestro vertex buffer. En la demo se vera como se realiza este paso.
Una vez que ya hemos visto la parte básica de los recursos, vamos a ver como viaja la geometría por el pipeline del Direct3D hasta convertirse en pixeles en la imagen que se presentara en pantalla.
El pipeline tiene partes fijas y partes programables. En las partes programables se nos permite alterar el resultado mediante pequeños programas llamados shaders. Estos se programan en un lenguaje llamado HLSL (High Level Shading Language).
Los cuadrados son partes fijas y los redondos son programables. De los programables el único obligatorio es el Vertex Shader, todos los demás son opcionales. Vamos a explicar únicamente la parte que nos interesa para esta introducción:
Input Assembler | Recibe la información de los vértices almacenados en los buffers para crear la geometría. Es aquí donde necesita la definición donde indicamos la composición de cada vértice. |
Vertex Shader | Se ejecuta una vez por cada vértice del objeto. Permite manipular su composición, sea la posición o demás información que lleve hacia los siguientes pasos. |
Rasterizer | Transforma la geometría en pixeles. |
Píxel Shader | Se ejecuta una vez por cada píxel de la geometría rasterizada. Nos permite manipularlo, cambiando su textura y/o posición. |
Output Merger | Combina todos los pixeles en una única imagen. |
¿Y cómo se dibujan nuestros pixeles? Para llegar allí, tenemos que procesar la geometría. Cada objeto tiene sus propias transformaciones como posición, tamaño y rotación. Estas transformaciones las almacenamos en una única matriz 4×4. ¿Porque matrices? Pues porque las podemos multiplicar entre sí, o contra vectores y obtener un nuevo resultado. Básicamente realizar cálculos y obtener una transformación ordenada de los objetos.
Pero si cada objeto tiene sus propias transformaciones, ¿cómo se puede saber la transformación de cada uno con respeto al otro en nuestro mundo tridimensional? Esto se hace mediante dos matrices comunes para todos los objetos. Para “poner” todos los objetos en el mismo mundo y vista tenemos que multiplicar cada matriz de cada objeto con estas dos.
La primera es la matriz de proyección. Esta matriz es el espacio donde se simula nuestro mundo 3D. Le aplica a cada vértice una perspectiva. Direct3D la calcula por nosotros pasándole unos parámetros muy simples como el tamaño, la ratio y los limites (el plano de cerca y lejos).
La segunda matriz es la de la vista. Esta es la matriz de nuestra cámara, de nuestro ojo, mediante el cual podemos “ver” en nuestro mundo 3D. Tiene las mismas transformaciones que un objeto. Direct3D dispone de funciones que nos ayudara a posicionar la cámara, hacia donde mirar y el vector que define la dirección hacia arriba.
¿Y dónde se dibujan nuestros pixeles? Nuestros pixeles se dibujan en una imagen final llamada back buffer, que será presentada en pantalla. Esta imagen la crea el Direct3D cuando se inicia especificándole el tamaño. Se recomienda utilizar un doble buffer ya que mientras se dibuja en uno el otro es presentado en pantalla. Al terminar de dibujar uno, hace el cambio para presentar el recién terminado y dibujar en el ya presentado.
Con esto ya estamos listos para empezar con nuestra demo. Vamos a dibujar un cubo que da vueltas en una ventana de Windows. Todos lo que hemos aprendido hasta ahora lo vamos a ver son más detalle en la construcción del proyecto. Los pasos para seguir serán:
- construir una ventana que no pueda cambiar de tamaño
- inicializar el Direct3D buscando algún dispositivo compatible
- cargar nuestros elementos en memoria
- y dibujar la escena.
Antes de todo necesitamos tener instalado el Visual Studio con cualquier opción que incluya el Visual C++ seleccionada. Casi ni se necesita tutorial para esto.
Y vamos a empezar. Hay muchas formas de crear el proyecto, la mía personal es crear un proyecto vacío y añadir el archivo main.cpp.
También hay que cambiar el tipo de proyecto de consola a windows. Click derecho sobre el proyecto -> “Propiedades”. Nos vamos a “Vinculador” -> “Sistema” y cambiamos el “Subsistema”:
Una vez hecho esto ya estamos listos para empezar a codificar.
Basándonos en los pasos a seguir para la construcción, la estructura del proyecto es muy simple:
- un objeto donde inicializamos el Direct3D, inicializamos los recursos a utilizar y dibujar la escena.
- el punto de partida de nuestro programa donde creamos y presentamos una ventana Windows.
Esta será utilizada para presentar los que el Drect3D dibuje.
Compilamos, arrancamos y este debería ser el resultado (el cubo da vueltas ?):
Todo el código fuente esa en el GitHub de Atmira:
https://github.com/atmiraio/Direct3D
Hay comentarios explicativos por cada bloque de código indicando que se esta haciendo en cada momento.
Muchas gracias por el interés ?