Un vértice, dos vértices, tres vértices ¡coño un triángulo!

Publicado el Sábado 25 septiembre 2010 en DirectX por Thund

Ha pasado más tiempo del que me gustaría desde que escribí el anterior post sobre la inicialización de Direct3D, pero es que hacía mucha calor para ponerme a redactar… espero me comprendan, ejem. De hecho, el código expuesto aquí lleva escrito desde hace un mes. Pero vamos al lío, que no hay tiempo que perder. En éste post mostraré el tortuoso camino hacia la visualización de elementos geométricos, desde donde lo dejamos.

Hasta ahora teníamos 4 interfaces de objetos correctamente inicializados: ID3D11Device, IDXGISwapChain, ID3D11DeviceContext e ID3D11RenderTarget. Esto era suficiente para poder arrancar la maquinaria de Direct3D y rellenar el área de cliente de la ventana con un color para probarlo; sin embargo, en el paso 4, que era el último, sería lógico incluir algo que necesitaremos en adelante, que es la asociación del render target al device context:

pImmediateContext->OMSetRenderTargets(1, &pRenderTargetView, NULL);

Paso 5: El ojo de buey

Es necesario definir sobre qué porción de nuestra ventana será proyectado el front buffer. Antes se utilizaba la estructura D3DVIEWPORT9 para empaquetar los parámetros del viewport, ahora utilizamos D3D11_VIEWPORT, que ni quita ni pone nada nuevo.

D3D11_VIEWPORT viewport;
viewport.Height   = 600.0f;
viewport.Width    = 800.0f;
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.MaxDepth = 1.0f;
viewport.MinDepth = 0.0f;

pImmediateContext->RSSetViewports(1, &viewport);

Paso 6: El átomo

Como bien sabe el lector, el universo tridimensional que vemos por pantalla está formado en su inmensa mayoría por vértices que, ordenados de cierta manera, definen superficies triangulares que, a su vez, pueden formar mallas. Toda la parafernalia geométrica puede encontrarse fácilmente en Internet y no es el objeto de éste blog por el momento, así que me lo salto. El hecho es que, tal como ocurre con los átomos tradicionales, no todos los vértices son iguales; podemos definir un vértice al cual afecte la luz, que pueda ser envuelto por una textura o que sea representado por un color determinado. O podemos diseñar otro para el cual las anteriores propiedades no tengan sentido.

struct MyVertexType
{
    XMFLOAT4 position;
    XMFLOAT4 color;
};

D3D11_INPUT_ELEMENT_DESC arElements[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

Lo único que hacemos aquí es declarar nuestro tipo de vértice, como hacíamos con DirectX 9. No, no es así de simple, hay un montón de cosas que explicar. Empezaré por la primera estructura, en la que aparece un nuevo "tipo básico" de DirectX llamado XMFLOAT4. Se trata de una de las muchas nuevas estructuras matemáticas añadidas a la API mediante la librería XNAMath. ¿Y de dónde sale XNA si lo que vamos a usar es DirectX11 a pelo? Pues se trata  de un intento por unificar parte de las APIs de ambas plataformas, aumentando la portabilidad entre PC y XBox; aunque aún no han marcado como deprecated los antiguos tipos, creo que es preferible ir migrando desde ya en lugar de hacer adaptaciones posteriores. Para añadir la librería a nuestro código hay que incluir el archivo "xnamath.h". En éste caso, XMFLOAT4 vendría a sustituir a D3DXVECTOR4.

La siguiente estructura es también muy similar a lo que estábamos acostumbrados, usamos el tipo D3D11_INPUT_ELEMENT_DESC en lugar de D3DVERTEXELEMENT9. Nótese cómo Microsoft ha optado por un nombrado más legible. El contenido de este nuevo tipo ha cambiado bastante, y para verlo mejor, observemos las diferencias:

typedef struct D3DVERTEXELEMENT9 {
  WORD Stream;
  WORD Offset;
  BYTE Type;
  BYTE Method;
  BYTE Usage;
  BYTE UsageIndex;
} D3DVERTEXELEMENT9, *LPD3DVERTEXELEMENT9;
typedef struct D3D11_INPUT_ELEMENT_DESC {
  LPCSTR                     SemanticName;
  UINT                       SemanticIndex;
  DXGI_FORMAT                Format;
  UINT                       InputSlot;
  UINT                       AlignedByteOffset;
  D3D11_INPUT_CLASSIFICATION InputSlotClass;
  UINT                       InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;

Antes especificábamos el índice del stream que iba a contener vértices con tal elemento, el desfase de bytes respecto al inicio de la estructura, el tipo del elemento (vector de 3 floats, un entero, etc.), la interpretación del tesselator, la semántica del elemento en los shaders (elegido de un enumerado) y el índice del elemento para esa semántica. Ahora nos olvidamos de los streams y nos centramos únicamente en lo que respecta al formato de entrada a los shaders: determinamos la semántica del elemento en los shaders mediante una cadena, y su índice, por si hay más de un elemento con la misma; a continuación pasamos el formato del elemento, tal y como queremos que sea interpretado por los shaders (dense cuenta de que es el mismo enumerado de la API de DXGI que usamos anteriormente). El hueco de entrada (input slot) es un índice que determina por qué "puerto" del Input Assembler entrará la información de los vértices, de manera que, si ponemos los datos de posición (vectores de 3 componentes, por ejemplo) en un buffer de vértices y los datos de color (un entero) en otro buffer, podamos introducirlos por distintos slots y obtener como entrada al vertex shader una estructura con ambos datos ensamblados; ya veremos un ejemplo de esto en el futuro. Lo siguiente es especificar cuántos bytes de desfase hay entre el elemento actual, según su formato, respecto al precedente; lo normal aquí es usar la constante D3D11_APPEND_ALIGNED_ELEMENT que, como puede deducirse, pone el elemento justo después del anterior. Quisiera apuntar que el valor de este dato se calcula respecto al input slot al que pertenece el elemento, pudiendo tener 2 elementos con desfase cero si son los primeros de 2 slots distintos. Los 2 datos restantes está relacionados con el concepto de instanciación, disponible desde DirectX 10, al que dedicaré un post en su momento.

Alguien puede pensar que la declaración del tipo de vértice está incompleta, ¿dónde está la interfaz IDirect3DVertexDeclaration9? Ha dejado de existir, ahora lo que se crean son una especie de objetos intermediarios (layouts) que relacionan el array de la declaración con la firma de un shader determinado, como veremos más adelante. A partir de DirectX 10 ya no tenemos la fixed function pipeline, así que no es posible especificar el formato de un vértice mediante constantes D3DFVF, hay que seguir forzosamente ésta metodología. Otra cosa que falta a la declaración es el uso de la macro D3DDECL_END() como último elemento, que ya no es necesaria.

Paso 7: El cristal con que miramos

Ahora empieza la parte terrorífica de la historia, es hora de hablar de los… ¡shaders! Pero que nadie se achante, en realidad son como los dobermanns, acojonan al principio pero cuando los conoces pueden ser tus mejores amigos (vaya comparación de mierda :D ). El primer bicho con el que trataremos es la interfaz ID3DBlob, que encapsula un trozo de memoria de tamaño determinado, sin estructura definida. El uso que le daremos en esta ocasión será como recipiente de un shader compilado y como salida de los mensajes de error de la compilación. No voy a utilizar el Effects Framework todavía sino las funciones globales de que nos provee DirectX 11. Por tanto, para compilar un shader escrito en HLSL en un archivo de texto, llamaré a D3DXCompileShaderFromFile… ah no, que ahora se llama D3DX11CompileFromFile. Las diferencias entre una y otra función son ínfimas, quizá lo más destacable sea la posibilidad de realizar la compilación en un thread distinto mediante el uso de la interfaz ID3DX11ThreadPump, que no explicaré aquí, pero que según tengo entendido consume más de lo que ahorra. Para tener acceso a tal función hay que incluir la cabecera "D3DX11async.h".

UINT shaderFlags = D3D10_SHADER_DEBUG | D3D10_SHADER_SKIP_OPTIMIZATION | D3D10_SHADER_ENABLE_STRICTNESS;

ID3DBlob* pBlobVS = NULL;
ID3DBlob* pErrorBlobVS = NULL;
D3DX11CompileFromFile( L"MyEffect.fx", NULL, NULL, "vs_main", "vs_4_0", shaderFlags, NULL, NULL, &pBlobVS, &pErrorBlobVS, NULL );

ID3DBlob* pBlobPS = NULL;
ID3DBlob* pErrorBlobPS = NULL;
D3DX11CompileFromFile( L"MyEffect.fx", NULL, NULL, "ps_main", "ps_4_0", shaderFlags, NULL, NULL, &pBlobPS, &pErrorBlobPS, NULL );

Al igual que con las versiones anteriores, especificamos la ruta (relativa o absoluta) del archivo de texto, las macros del shader (que aquí no usamos), los trozos de shader a incluir (que tampoco necesitamos), el nombre de la función principal del shader y la versión del Shader Model para la cual se compilará. A continuación, se especifican los flags de compilación, valores constantes heredados de DirectX 10 (con prefijo D3D10_SHADER_) que se pueden combinar para modificar el comportamiento del compilador de shaders; aquí hemos usado, por ejemplo, D3D10_SHADER_DEBUG para generar información de depuración que sirva a un depurador, como el de PIX, para mostrar el código HLSL directamente, en lugar del código ensamblador; D3D10_SHADER_SKIP_OPTIMIZATION evita que el compilador realice optimizaciones, lo cual provocaría que el código HLSL visto en el depurador no concordara con el generado; finalmente, el flag D3D10_SHADER_ENABLE_STRICTNESS nos obligará a utilizar la sintaxis de la versión 4.0 y posteriores del Shader Model. Lo siguiente que se nos pide es una combinación de flags para efectos (no ponemos nada), un puntero a la interfaz del thread antes mencionada y variables de salida, de las que sólo cabe destacar la última, pHResult, que debe apuntar a una zona válida de memoria para almacenar el resultado de la operación llevada a cabo en el otro thread, siempre y cuando se haya especificado la susodicha interfaz. Otro NULL al bote.

Para poder insertar nuestros shaders en la pipeline, primero tenemos que encapsularlos en sendas interfaces ID3D11VertexShader y ID3D11PixelShader (antes llamadas IDirect3DVertexShader9 y IDirect3DPixelShader9, respectivamente). La única información que hay que pasar al dispositivo para crear los objetos es el punto de inicio del buffer que contiene el resultado de la compilación y su longitud. A continuación se asocian al contexto encargado de dibujar nuestro polígono.

ID3D11VertexShader* pVS = NULL;
pDevice->CreateVertexShader(pBlobVS->GetBufferPointer(), pBlobVS->GetBufferSize(), NULL, &pVS);
pImmediateContext->VSSetShader(pVS, NULL, 0);

ID3D11PixelShader* pPS = NULL;
pDevice->CreatePixelShader(pBlobPS->GetBufferPointer(), pBlobPS->GetBufferSize(), NULL, &pPS);
pImmediateContext->PSSetShader(pPS, NULL, 0);

Ya tenemos nuestros shaders cargados en memoria, compilados y preparados para ser ejecutados por el mecanismo de DirectX para representar nuestra geometría con el aspecto que más nos guste. Alto, ¿cómo sabe DirectX qué tipo de datos va a enviar al Input Assembler para que los shaders los procesen? ¿Cómo deben ser transformados aquéllos para que coincidan con los esperados?

Paso 8: Las gallinas que entran por las que van saliendo, o no

Como mencioné anteriormente, en DirectX 11 se usan unos intermediarios que actúan como adaptadores entre los shaders, el Input Assembler (por cierto, aunque ya lo expliraré, el Input Assembler es una de las fases (stage) de la pipeline, concretamente la de entrada) y los datos de entrada, que llegan en forma de paquetes de vértices. Estos adaptadores van encapsulados en interfaces ID3D11InputLayout y están sujetos a la firma de un shader y al formato de los datos de entrada, información que se transfiere al Input Assembler.

ID3D11InputLayout* pMyInputLayout = NULL;
pDevice->CreateInputLayout(arElements, sizeof(arElements) / sizeof(arElements[0]), pBlobVS->GetBufferPointer(), pBlobVS->GetBufferSize(), &pMyInputLayout);
pImmediateContext->IASetInputLayout(pMyInputLayout);

Tal y como puede verse claramente, se utilizan la declaración del tipo de datos de nuestro vértice y el resultado de la compilación del vertex shader (especificando sus puntos de inicio y su tamaño en memoria). Acto seguido se asocia el nuevo layout con el Input Assembler por medio del método IASetInputLayout (es obvio de dónde le viene el prefijo, veremos que han usado tal convención de nombres para todos los métodos relacionados directamente con ciertas fases de la pipeline).

Paso 9: Un pino, dos pinos, tres pinos… ¡coño un pinar!

Sólo resta meter datos en la tubería para que sean procesados por la maquinaria que acabamos de ajustar. Primero creamos una lista de 3 vértices cuyas coordenadas en el espacio proyectado sobre el viewport forman un triángulo (no vamos a aplicar transformaciones aún). Tal información irá empaquetada en un buffer cuya estructura debemos describir usando el tipo D3D11_BUFFER_DESC.

MyVertexType vertices[3];
vertices[0].position = XMFLOAT4(0.0f, 0.5f, 0.0f, 1.0f);
vertices[1].position = XMFLOAT4(0.5f, -0.5f, 0.0f, 1.0f);
vertices[2].position = XMFLOAT4(-0.5f, -0.5f, 0.0f, 1.0f);
vertices[0].color = vertices[1].color = vertices[2].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);

D3D11_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.ByteWidth = sizeof( vertices );
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;

Antes de continuar, quisiera remarcar que el buffer que estamos definiendo es tratado por DirectX 11 como un recurso, al igual que las texturas (sus interfaces heredan de ID3D11Resource), por lo que la manera de crearlos y parametrizarlos es muy similar, basta observar cómo se repiten determinados campos en las estructuras D3D11_BUFFER_DESC, D3D11_TEXTURE1D_DESC, D3D11_TEXTURE2D_DESC y D3D11_TEXTURE3D_DESC. En DirectX 9 ocurría igual, aunque ahora se ha simplificado o unificado las opciones. Un ejemplo de esto era el parámetro de tipo D3DPOOL en el que especificábamos dónde alojar el recurso (memoria de sistema, de la tarjeta gráfica o administrado por DirectX), o la cantidad de tipos de uso, que ahora se ve reducida de 16 opciones a 4. También hay que mencionar la pérdida de las interfaces IDirect3DVertexBuffer9 e IDirect3DIndexBuffer9, cuyas responsabilidades son ahora cubiertas por ID3D11Buffer, según el flag de enlazado (bind flag).

Prosigamos. Rellenar la estructura es sencillo, como puede verse. Primero especificamos el uso (acceso de lectura / escritura por parte de la CPU y la GPU); luego el tamaño de nuestra lista de vértices; seguidamente el flag de enlazado, que en este caso indica que nuestro buffer actuará como un "vertex buffer"; no necesitamos acceder al recurso desde la CPU en esta ocasión, por lo que pasamos un cero, igual que para los flags especiales.

D3D11_SUBRESOURCE_DATA initialData;
initialData.pSysMem = vertices;
initialData.SysMemPitch = 0;
initialData.SysMemSlicePitch = 0;

ID3D11Buffer* pBuffer = NULL;
pDevice->CreateBuffer( &bufferDesc, &initialData, &pBuffer );

Para crear el buffer tenemos que "empaquetar" los datos en oootra esctructura (nótese mi jartura, con jota), D3D11_SUBRESOURCE_DATA, a la que pasamos un puntero a nuestra lista de vértices. Los otros 2 campos no son necesarios dado que no estamos tratando con una textura. De este modo, tenemos por un lado los datos y por otro su forma. Los usamos en el método CreateBuffer y hala, ya tenemos un dichoso buffer.

Como colofón, pasamos nuestro recién armado vertex buffer como entrada del Input Assembler:

UINT stride = sizeof( MyVertexType );
UINT offset = 0;
pImmediateContext->IASetVertexBuffers( 0, 1, &pBuffer, &stride, &offset );
pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

El método IASetVertexBuffers reemplaza al antiguo SetStreamSource, con el añadido de que se le pueden pasar varios vertex buffers en una sola llamada. La última sentencia especifica qué tipo de primitivas forman los vértices suministrados, es decir, si se trata de una lista de líneas (cogiendo los vértices de 2 en 2), o si es una tira (que es distinto de uns lista) de triángulos, etc. Aquí se envía una lista en la que hay un único triángulo.

El resultado de todo este trabajo lo podemos ver si llamamos al método Draw del immediate context. Existen varias versiones de este método según el proceso que queramos seguir a la hora de dibujar. Por ejemplo, si estuviéramos usando índices llamaríamos a DrawIndexed, y en caso de usar instanciación invocaríamos a DrawInstanced. Hablaré de todos ellos en los próximos posts. Como es obvio, tales métodos son evoluciones de sus casi homónimos de la interfaz del dispositivo de DirectX 9.

pImmediateContext->Draw( 3, 0 );

Con esto expresamos que queremos dibujar 3 vértices, empezando por el número cero. Y éste es el resultado:

Sumario de Portabilidad

DirectX 9 DirectX 11 Comentario
D3DVIEWPORT9 D3D11_VIEWPORT  
D3DXFLOAT4 XMFLOAT4  
D3DVERTEXELEMENT9 D3D11_INPUT_ELEMENT_DESC  
IDirect3DVertexDeclaration9   No hay análogo.
Constantes D3DFVF   No hay análogo.
D3DDECL_END()   No hay análogo.
D3DXCompileShaderFromFile D3DX11CompileFromFile  
IDirect3DVertexBuffer9 ID3D11Buffer Bind Flag = D3D11_BIND_VERTEX_BUFFER
IDirect3DIndexBuffer9 ID3D11Buffer Bind Flag = D3D11_BIND_INDEX_BUFFER
IDirect3DDevice9::CreateVertexBuffer ID3D11Device::CreateBuffer  
IDirect3DDevice9::CreateIndexBuffer ID3D11Device::CreateBuffer  
IDirect3DVertexShader9 ID3D11VertexShader  
IDirect3DPixelShader9 ID3D11PixelShader  
IDirect3DDevice9::SetStreamSource ID3D11DeviceContext::IASetVertexBuffers  
IDirect3DDevice9::DrawPrimitive ID3D11DeviceContext::Draw  

 

Descargas

Podéis descargar el código de ejemplo desde aquí:

EndlessGameTriangle.zip (23 Kb)
 

Etiquetas: , ,

4 Respuestas a 'Un vértice, dos vértices, tres vértices ¡coño un triángulo!'

Suscríbete a los comentarios con RSS o TrackBack a 'Un vértice, dos vértices, tres vértices ¡coño un triángulo!'.

  1. Alejandro dijo:

    Muy buen tutorial,es el mejor q he visto en español hasta ahora sobre el tema gracias.

    on septiembre 16th, 2011 at 22:23

  2. Thund dijo:

    Gracias, me alegro de que te sea útil :)

    on septiembre 16th, 2011 at 22:45

  3. Jordi dijo:

    ¿Cuando será el siguiente capítulo? Qué diferéncia entre leerlo del help del SDK de DirectX a como se explica en esta Web.

    on octubre 8th, 2011 at 16:26

  4. Thund dijo:

    El siguiente capítulo desconozco cuándo será, ando corto de tiempo por desgracia… Diferencias: está en español, se me pueden hacer preguntas de lo que no se entienda, son ejemplos diferentes con explicaciones distintas, expongo comparaciones con la API antigua… Gracias por el interés :)

    on octubre 8th, 2011 at 18:05

Publica un comentario