Compartir a través de


Trabajo con sombreadores y recursos de sombreador

Es el momento de aprender a trabajar con sombreadores y recursos de sombreador en el desarrollo de su juego de Microsoft DirectX para Windows 8. Hemos visto cómo configurar el dispositivo gráfico y los recursos, y quizás incluso ha empezado a modificar su canalización. Ahora echemos un vistazo a los sombreadores de píxeles y vértices.

Si no está al tanto de los lenguajes de sombreado, conviene hacer un breve repaso. Los sombreadores son programas pequeños y de bajo nivel que se compilan y ejecutan en fases específicas de la canalización de gráficos. Su especialidad son operaciones matemáticas de punto flotante muy rápidas. Los programas de sombreador más comunes son:

  • Sombreador de vértices: se ejecuta para cada vértice de una escena. Este sombreador funciona en elementos de búfer de vértices proporcionados por la aplicación que realiza la llamada y, al mínimo, da como resultado un vector de posición de cuatro componentes que se rasterizará en una posición de píxel.
  • Sombreador de píxeles: se ejecuta para cada píxel de un destino de representación. Este sombreador recibe coordenadas rasterizadas de las fases anteriores del sombreador (en las canalizaciones más sencillas, sería el sombreador de vértices) y devuelve un color (u otro valor de cuatro componentes) para esa posición de píxel, que luego se escribe en un destino de representación.

En este ejemplo se incluyen sombreadores de vértices y píxeles muy básicos que solo dibujan geometría y sombreadores más complejos que agregan cálculos básicos de iluminación.

Los programas de sombreador se escriben en Lenguaje de sombreador de alto nivel (HLSL) de Microsoft. La sintaxis de HLSL se parece mucho a C, pero sin punteros. Los programas de sombreador deben ser muy compactos y eficientes. Si el sombreador se compila en demasiadas instrucciones, no se puede ejecutar y se devuelve un error. (Tenga en cuenta que el número exacto de instrucciones permitidas forma parte del nivel de característica de Direct3D).

En Direct3D, los sombreadores no se compilan en tiempo de ejecución; se compilan cuando lo hace el resto del programa. Al compilar la aplicación con Microsoft Visual Studio 2013, los archivos HLSL se compilan en archivos CSO (.cso) que la aplicación debe cargar y colocar en la memoria de GPU antes de dibujar. Asegúrese de incluir estos archivos de CSO con la aplicación al empaquetarlo; son activos como mallas y texturas.

Descripción de la semántica de HLSL

Es importante dedicar un momento en analizar la semántica de HLSL antes de continuar, ya que a menudo son un punto de confusión para los nuevos desarrolladores de Direct3D. La semántica de HLSL son cadenas que identifican un valor pasado entre la aplicación y un programa de sombreador. Aunque pueden ser cualquiera de una variedad de cadenas posibles, el procedimiento recomendado es usar una cadena como POSITION o COLOR que indique el uso. Estas semánticas se asignan al construir un búfer de constantes o un diseño de entrada. También puede anexar un número entre 0 y 7 a la semántica para que use registros independientes para valores similares. Por ejemplo: COLOR0, COLOR1, COLOR2...

La semántica que tiene el prefijo "SV\_" es la semántica de valor del sistema que el programa sombreador escribe. El mismo juego (que se ejecuta en la CPU) no puede modificarla. Normalmente, esta semántica contiene valores que son entradas o salidas de otra fase del sombreador en la canalización de gráficos o que la GPU genera por completo.

Además, la semántica SV_ tiene comportamientos diferentes cuando se usan para especificar la entrada o salida de una fase del sombreador. Por ejemplo, SV_POSITION (salida) contiene los datos de vértices transformados durante la fase del sombreador de vértices y SV_POSITION (entrada) contiene los valores de posición de píxeles interpolados por la GPU durante la fase de rasterización.

Estas son algunas semánticas comunes de HLSL:

  • POSITION(n) para los datos del búfer de vértices. SV_POSITION proporciona una posición de píxel al sombreador de píxeles y no se puede escribir en el juego.
  • NORMAL(n) para los datos normales proporcionados por el búfer de vértices.
  • TEXCOORD(n) para los datos de coordenadas UV de textura proporcionados a un sombreador.
  • COLOR(n) para los datos de color RGBA proporcionados a un sombreador. Tenga en cuenta que se trata de forma idéntica para coordinar los datos, incluida la interpolación del valor durante la rasterización; la semántica simplemente le ayuda a identificar que es datos de color.
  • SV_Target[n] para escribir desde un sombreador de píxeles en una textura de destino u otro búfer de píxeles.

Veremos algunos ejemplos de semántica de HLSL a medida que revisamos el ejemplo.

Lectura de los búferes de constantes

Cualquier sombreador puede leer desde un búfer de constantes si ese búfer está asociado a su fase como un recurso. En este ejemplo, solo se asigna un búfer de constantes al sombreador de vértices.

El búfer de constantes se declara en dos lugares: en el código de C++ y en los archivos HLSL correspondientes que tendrán acceso a él.

Aquí se muestra cómo se declara la estructura del búfer de constantes en el código de C++.

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

Al declarar la estructura del búfer de constantes en el código de C++, asegúrese de que todos los datos se alinean correctamente a lo largo de los límites de 16 bytes. La manera más fácil de hacerlo es usar tipos DirectXMath, como XMFLOAT4 o XMFLOAT4X4, como se muestra en el código de ejemplo. También puede protegerse frente a búferes desalineados declarando una aserción estática:

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

Esta línea de código provocará un error en tiempo de compilación si ConstantBufferStruct no está alineado a 16 bytes. Para más información sobre la alineación y el empaquetado del búfer de constantes, consulte Reglas de empaquetado para variables constantes.

Ahora, aquí se muestra cómo se declara el búfer de constantes en el sombreador de vértices de HLSL.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

Todos los búferes(constantes, texturas, muestreador u otros) deben tener definido un registro para que la GPU pueda acceder a ellos. Cada fase del sombreador permite hasta 15 búferes de constantes y cada búfer puede contener hasta 4096 variables constantes. La sintaxis de declaración de uso de registro es la siguiente:

  • b*#*: registro para un búfer de constantes (cbuffer).
  • t*#*: registro de un búfer de textura (tbuffer).
  • s*#*: registro de un muestreador. (Un muestreador define el comportamiento de búsqueda de elementos de textura en el recurso de textura).

Por ejemplo, el HLSL para un sombreador de píxeles podría tomar una textura y un muestreador como entrada con una declaración como esta.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

Es necesario asignar búferes de constantes a los registros; al configurar la canalización, se adjunta un búfer de constantes a la misma ranura a la que se le asignó en el archivo HLSL. Por ejemplo, en el tema anterior, la llamada a VSSetConstantBuffers indica "0" para el primer parámetro. Esto indica a Direct3D que adjunte el recurso de búfer de constantes para registrar 0, que coincide con la asignación del búfer para registrar(b0) en el archivo HLSL.

Lectura de los búferes de vértices

El búfer de vértices proporciona los datos de triángulo de los objetos de escena al sombreador de vértices. Al igual que con el búfer de constantes, la estructura del búfer de vértices se declara en el código de C++, con reglas de empaquetado similares.

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

No hay ningún formato estándar para los datos de vértices en Direct3D 11. En su lugar, definimos nuestro propio diseño de datos de vértices mediante un descriptor; los campos de datos se definen mediante una matriz de estructuras D3D11_INPUT_ELEMENT_DESC. Aquí se muestra un diseño de entrada simple que describe el mismo formato de vértice que la estructura anterior:

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

Si agrega datos al formato de vértice al modificar el código de ejemplo, asegúrese de actualizar también el diseño de entrada o del sombreador no podrá interpretarlo. Puede modificar el diseño del vértice de la siguiente manera:

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

En ese caso, modificaría la definición de diseño de entrada como se indica a continuación.

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayoutExtended
    );

Cada una de las definiciones de elemento de diseño de entrada tiene como prefijo una cadena, como "POSITION" o "NORMAL", que es la semántica que hemos analizado anteriormente en este tema. Es como un identificador que ayuda a la GPU a identificar ese elemento al procesar el vértice. Elija nombres comunes y significativos para los elementos de vértice.

Al igual que con el búfer de constantes, el sombreador de vértices tiene una definición de búfer correspondiente para los elementos de vértices entrantes. (Por eso proporcionamos una referencia al recurso del sombreador de vértices al crear el diseño de entrada: Direct3D valida el diseño de datos por vértice con la estructura de entrada del sombreador). Observe cómo la semántica coincide entre la definición de diseño de entrada y esta declaración de búfer HLSL. Sin embargo, COLOR tiene un "0" anexado. No es necesario agregar el 0 si solo tiene un elemento COLOR declarado en el diseño, pero es recomendable anexarlo en caso de que elija agregar más elementos de color en el futuro.

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

Paso de los datos entre sombreadores

Los sombreadores toman tipos de entrada y devuelven tipos de salida de sus funciones principales tras la ejecución. Para el sombreador de vértices definido en la sección anterior, el tipo de entrada era la estructura VS_INPUT y definimos un diseño de entrada coincidente y una estructura de C++. Una matriz de esta estructura se usa para crear un búfer de vértices en el método CreateCube.

El sombreador de vértices devuelve una estructura de PS_INPUT, que debe contener mínimamente la posición final del vértice de cuatro componentes (float4). Este valor de posición debe tener la semántica del valor del sistema, SV_POSITION, declarada para ello, por lo que la GPU tiene los datos que necesita para realizar el siguiente paso de dibujo. Observe que no hay una correspondencia 1:1 entre la salida del sombreador de vértices y la entrada del sombreador de píxeles; el sombreador de vértices devuelve una estructura para cada vértice que se proporciona, pero el sombreador de píxeles se ejecuta una vez para cada píxel. Esto se debe a que los datos por vértice pasan primero por la fase de rasterización. Esta fase decide qué píxeles "cubren" la geometría que está dibujando, calcula los datos interpolados por vértice para cada píxel y, a continuación, llama al sombreador de píxeles una vez para cada uno de esos píxeles. La interpolación es el comportamiento predeterminado al rasterizar valores de salida, y es esencial en particular para el procesamiento correcto de datos vectoriales de salida (vectores de luz, normales por vértice y tangentes, entre otros).

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

Revisión del sombreador de vértices

El sombreador de vértices de ejemplo es muy sencillo: tomar un vértice (posición y color), transformar la posición de las coordenadas del modelo en coordenadas proyectadas desde la perspectiva y devolverla (junto con el color) al rasterizador. Observe que el valor de color se interpola directamente junto con los datos de posición, proporcionando un valor diferente para cada píxel aunque el sombreador de vértices no haya realizado ningún cálculo en el valor de color.

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

Un sombreador de vértices más complejo, como uno que configura los vértices de un objeto para sombreado Phong, podría parecerse más a esto. En este caso, estamos aprovechando el hecho de que los vectores y los normales se interpolan para aproximarse a una superficie de aspecto suave.

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

Revisión del sombreador de píxeles

Este sombreador de píxeles de este ejemplo es posiblemente la cantidad mínima absoluta de código que puede tener en un sombreador de píxeles. Toma los datos de color de píxel interpolado generados durante la rasterización y los devuelve como salida, donde se escribirá en un destino de representación. ¡Qué aburrido!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

La parte importante es la semántica del valor del sistema SV_TARGET en el valor devuelto. Indica que la salida se va a escribir en el destino de representación principal, que es el búfer de textura proporcionado a la cadena de intercambio para su visualización. Esto es necesario para los sombreadores de píxeles, sin los datos de color del sombreador de píxeles, Direct3D no tendría nada que mostrar.

Un ejemplo de un sombreador de píxeles más complejo para realizar sombreado Phong podría tener este aspecto. Dado que los vectores y los normales se interpolaron, no es necesario calcularlos por píxel. Sin embargo, tenemos que volver a normalizarlos debido a cómo funciona la interpolación; conceptualmente, es necesario "girar" gradualmente el vector desde la dirección en el vértice A a la dirección en el vértice B, manteniendo su longitud, mientras que la interpolación corta en su lugar una línea recta entre los dos puntos extremos del vector.

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

En otro ejemplo, el sombreador de píxeles toma sus propios búferes de constantes que contienen información de luz y material. El diseño de entrada en el sombreador de vértices se expandiría para incluir datos normales y se espera que la salida de ese sombreador de vértices incluya vectores transformados para el vértice, la luz y el vértice normal en el sistema de coordenadas de vista.

Si tiene búferes de textura y muestreadores con registros asignados (t y s, respectivamente), también puede acceder a ellos en el sombreador de píxeles.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

Los sombreadores son herramientas muy eficaces que se pueden usar para generar recursos de procedimientos como mapas de sombras o texturas de ruido. De hecho, las técnicas avanzadas requieren que piense en texturas de forma más abstracta, no como elementos visuales, sino como búferes. Contienen datos como la información de alto u otros datos que se pueden muestrear en el paso final del sombreador de píxeles o en ese marco concreto como parte de un paso de efectos de varias fases. El muestreo múltiple es una herramienta eficaz y la red troncal de muchos efectos visuales modernos.

Pasos siguientes

Esperamos que se sienta a gusto con DirectX 11 en este punto para empezar a trabajar en su proyecto. Estos son algunos vínculos para ayudar a responder a otras preguntas que puede tener sobre el desarrollo con DirectX y C++:

Trabajo con recursos de dispositivos DirectX

Descripción de la canalización de representación de Direct3D 11