Compartir a través de


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

Anteriormente, ha visto cómo crear una ventana que puede usar para dibujar en Work with DirectX device resources (Trabajar con recursos de dispositivo DirectX). Ahora, aprenderá a crear la canalización de gráficos y dónde puede enlazarla.

Recordará que hay dos interfaces direct3D que definen la canalización de gráficos: ID3D11Device, que proporciona una representación virtual de la GPU y sus recursos; y ID3D11DeviceContext, que representa el procesamiento de gráficos de la canalización. Normalmente, se usa una instancia de ID3D11Device para configurar y obtener los recursos de GPU que necesita para empezar a procesar los gráficos en una escena y se usa ID3D11DeviceContext para procesar esos recursos en cada fase de sombreador adecuada de la canalización de gráficos. Normalmente, llamas a los métodos ID3D11Device con poca frecuencia, es decir, solo cuando configuras una escena o cuando cambia el dispositivo. Por otro lado, llamará a ID3D11DeviceContext cada vez que procese un marco para mostrarlo.

En este ejemplo se crea y configura una canalización de gráficos mínima adecuada para mostrar un cubo giratorio simple y sombreado de vértices. Muestra aproximadamente el conjunto de recursos más pequeño necesario para su visualización. A medida que lea la información aquí, tenga en cuenta las limitaciones del ejemplo dado donde es posible que tenga que extenderla para admitir la escena que desea representar.

En este ejemplo se tratan dos clases de C++ para gráficos: una clase de administrador de recursos de dispositivos y una clase de representador de escena 3D. Este tema se centra específicamente en el representador de escena 3D.

¿Qué hace el representador de cubos?

La canalización de gráficos se define mediante la clase de representador de escena 3D. El representador de escena puede:

  • Defina los búferes de constantes para almacenar los datos uniformes.
  • Defina los búferes de vértices para contener los datos del vértice del objeto y los búferes de índice correspondientes para permitir que el sombreador de vértices recorda los triángulos correctamente.
  • Cree recursos de textura y vistas de recursos.
  • Cargue los objetos del sombreador.
  • Actualice los datos gráficos para mostrar cada fotograma.
  • Representar (dibujar) los gráficos en la cadena de intercambio.

Los cuatro primeros procesos suelen usar los métodos de interfaz ID3D11Device para inicializar y administrar recursos gráficos, y los dos últimos usan los métodos de interfaz ID3D11DeviceContext para administrar y ejecutar la canalización de gráficos.

Se crea y administra una instancia de la clase Renderer como variable miembro en la clase de proyecto principal. La instancia deviceResources se administra como un puntero compartido entre varias clases, incluida la clase de proyecto principal, la clase de proveedor de vistas de la aplicación y el representador. Si reemplaza Renderer por su propia clase, considere la posibilidad de declarar y asignar la instancia DeviceResources como miembro de puntero compartido también:

std::shared_ptr<DX::DeviceResources> m_deviceResources;

Simplemente pase el puntero al constructor de clase (u otro método de inicialización) después de crear la instancia DeviceResources en el método Initialize de la clase App . También puede pasar una referencia de weak_ptr si, en su lugar, quiere que la clase principal sea completamente propietaria de la instancia deviceResources .

Creación del representador de cubo

En este ejemplo, organizamos la clase del representador de escena con los métodos siguientes:

  • CreateDeviceDependentResources: se llama cada vez que se debe inicializar o reiniciar la escena. Este método carga los datos iniciales de vértices, texturas, sombreadores y otros recursos, y construye las constantes iniciales y los búferes de vértices. Normalmente, la mayoría del trabajo aquí se realiza con métodos ID3D11Device , no con métodos ID3D11DeviceContext .
  • CreateWindowSizeDependentResources: se llama cada vez que cambia el estado de la ventana, como cuando se produce el cambio de tamaño o cuando cambia la orientación. Este método vuelve a generar matrices de transformación, como las de la cámara.
  • Actualización: normalmente se llama desde la parte del programa que administra el estado inmediato del juego; en este ejemplo, simplemente lo llamamos desde la clase Main . Haga que este método lea de cualquier información de estado de juego que afecte a la representación, como actualizaciones de la posición del objeto o fotogramas de animación, además de cualquier dato global del juego como niveles de luz o cambios en la física del juego. Estas entradas se usan para actualizar los búferes de constantes por fotograma y los datos de objetos.
  • Render: normalmente se llama desde la parte del programa que administra el bucle del juego; en este caso, se llama desde la clase Main . Este método construye la canalización de gráficos: enlaza sombreadores, enlaza búferes y recursos a las fases del sombreador e invoca el dibujo para el marco actual.

Estos métodos componen el cuerpo de los comportamientos para representar una escena con Direct3D mediante los recursos. Si extiende este ejemplo con una nueva clase de representación, declárela en la clase de proyecto principal. Por lo tanto, esto:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

se transforma en:

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

De nuevo, tenga en cuenta que en este ejemplo se supone que los métodos tienen las mismas firmas en la implementación. Si las firmas han cambiado, revise el bucle Main y realice los cambios en consecuencia.

Echemos un vistazo a los métodos de representación de escenas con más detalle.

Creación de recursos dependientes del dispositivo

CreateDeviceDependentResources consolida todas las operaciones para inicializar la escena y sus recursos mediante llamadas ID3D11Device . Este método supone que el dispositivo Direct3D se acaba de inicializar (o se ha vuelto a crear) para una escena. Vuelve a crear o recarga todos los recursos gráficos específicos de la escena, como los sombreadores de vértices y píxeles, los búferes de vértices e índices para los objetos y cualquier otro recurso (por ejemplo, como texturas y sus vistas correspondientes).

Este es el código de ejemplo para CreateDeviceDependentResources:

void Renderer::CreateDeviceDependentResources()
{
    // Compile shaders using the Effects library.
    auto CreateShadersTask = Concurrency::create_task(
            [this]( )
            {
                CreateShaders();
            }
        );

    // Load the geometry for the spinning cube.
    auto CreateCubeTask = CreateShadersTask.then(
            [this]()
            {
                CreateCube();
            }
        );
}

void Renderer::CreateWindowSizeDependentResources()
{
    // Create the view matrix and the perspective matrix.
    CreateViewAndPerspective();
}

Cada vez que cargue recursos desde el disco (recursos como archivos o texturas) del objeto de sombreador compilado (CSO o .cso), hágalo de forma asincrónica. Esto te permite mantener otro trabajo en marcha al mismo tiempo (como otras tareas de configuración) y porque el bucle principal no está bloqueado, puedes seguir mostrando algo visualmente interesante para el usuario (como una animación de carga para tu juego). En este ejemplo se usa la API Concurrency::Tasks que está disponible a partir de Windows 8; tenga en cuenta la sintaxis lambda usada para encapsular tareas de carga asincrónicas. Estas expresiones lambda representan las funciones llamadas off-thread, por lo que se captura explícitamente un puntero al objeto de clase actual (esto).

Este es un ejemplo de cómo se puede cargar el código de bytes del sombreador:

HRESULT hr = S_OK;

// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();

// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;

size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];

fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
    bytes,
    bytesRead,
    nullptr,
    &m_pVertexShader
    );

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
    );

delete bytes;


bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
    bytes,
    bytesRead,
    nullptr,
    m_pPixelShader.GetAddressOf()
    );

delete bytes;

CD3D11_BUFFER_DESC cbDesc(
    sizeof(ConstantBufferStruct),
    D3D11_BIND_CONSTANT_BUFFER
    );

hr = device->CreateBuffer(
    &cbDesc,
    nullptr,
    m_pConstantBuffer.GetAddressOf()
    );

fclose(vShader);
fclose(pShader);

Este es un ejemplo de cómo crear búferes de vértices e índices:

HRESULT Renderer::CreateCube()
{
    HRESULT hr = S_OK;

    // Use the Direct3D device to load resources into graphics memory.
    ID3D11Device* device = m_deviceResources->GetDevice();

    // Create cube geometry.
    VertexPositionColor CubeVertices[] =
    {
        {DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  0,   0,   0),},
        {DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  0,   0,   1),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  0,   1,   0),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  0,   1,   1),},

        {DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  1,   0,   0),},
        {DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  1,   0,   1),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  1,   1,   0),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  1,   1,   1),},
    };
    
    // Create vertex buffer:
    
    CD3D11_BUFFER_DESC vDesc(
        sizeof(CubeVertices),
        D3D11_BIND_VERTEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA vData;
    ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
    vData.pSysMem = CubeVertices;
    vData.SysMemPitch = 0;
    vData.SysMemSlicePitch = 0;

    hr = device->CreateBuffer(
        &vDesc,
        &vData,
        &m_pVertexBuffer
        );

    // Create index buffer:
    unsigned short CubeIndices [] = 
    {
        0,2,1, // -x
        1,2,3,

        4,5,6, // +x
        5,7,6,

        0,1,5, // -y
        0,5,4,

        2,6,7, // +y
        2,7,3,

        0,4,6, // -z
        0,6,2,

        1,3,7, // +z
        1,7,5,
    };

    m_indexCount = ARRAYSIZE(CubeIndices);

    CD3D11_BUFFER_DESC iDesc(
        sizeof(CubeIndices),
        D3D11_BIND_INDEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA iData;
    ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
    iData.pSysMem = CubeIndices;
    iData.SysMemPitch = 0;
    iData.SysMemSlicePitch = 0;
    
    hr = device->CreateBuffer(
        &iDesc,
        &iData,
        &m_pIndexBuffer
        );

    return hr;
}

En este ejemplo no se cargan mallas ni texturas. Debes crear los métodos para cargar los tipos de malla y textura específicos de tu juego y llamarlos de forma asincrónica.

Rellene también los valores iniciales de los búferes de constantes por escena. Algunos ejemplos de búfer de constantes por escena incluyen luces fijas u otros elementos y datos estáticos de la escena.

Implementación del método CreateWindowSizeDependentResources

Los métodos CreateWindowSizeDependentResources se llaman cada vez que cambia el tamaño, la orientación o la resolución de la ventana.

Los recursos de tamaño de ventana se actualizan de esta manera: el procedimiento de mensaje estático obtiene uno de varios eventos posibles que indican un cambio en el estado de la ventana. A continuación, el bucle principal se informa sobre el evento y llama a CreateWindowSizeDependentResources en la instancia de clase principal, que luego llama a la implementación CreateWindowSizeDependentResources en la clase representador de escena.

El trabajo principal de este método es asegurarse de que los objetos visuales no se confunden o no son válidos debido a un cambio en las propiedades de la ventana. En este ejemplo, se actualizan las matrices del proyecto con un nuevo campo de vista (FOV) para la ventana redimensionada o orientada.

Ya vimos el código para crear recursos de ventana en DeviceResources , que era la cadena de intercambio (con búfer de reserva) y la vista de destino de representación. Este es el modo en que el representador crea transformaciones dependientes de la relación de aspecto:

void Renderer::CreateViewAndPerspective()
{
    // Use DirectXMath to create view and perspective matrices.

    DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
    DirectX::XMVECTOR at  = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
    DirectX::XMVECTOR up  = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.view,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixLookAtRH(
                eye,
                at,
                up
                )
            )
        );

    float aspectRatioX = m_deviceResources->GetAspectRatio();
    float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.projection,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixPerspectiveFovRH(
                2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
                aspectRatioX,
                0.01f,
                100.0f
                )
            )
        );
}

Si la escena tiene un diseño específico de componentes que depende de la relación de aspecto, este es el lugar para reorganizarlos para que coincidan con esa relación de aspecto. Es posible que desee cambiar la configuración del comportamiento posterior al procesamiento aquí también.

Implementación del método Update

El método Update se llama una vez por bucle de juego; en este ejemplo, se llama mediante el método de la clase principal del mismo nombre. Tiene un propósito sencillo: actualizar la geometría de la escena y el estado del juego en función de la cantidad de tiempo transcurrido (o pasos de tiempo transcurrido) desde el fotograma anterior. En este ejemplo, simplemente rotamos el cubo una vez por fotograma. En una escena real del juego, este método contiene mucho más código para comprobar el estado del juego, actualizar búferes de constantes por fotograma (u otro dinámico), búferes de geometría y otros recursos en memoria en consecuencia. Puesto que la comunicación entre la CPU y la GPU conlleva sobrecarga, asegúrese de que solo actualiza los búferes que realmente han cambiado desde el último fotograma: los búferes de constantes se pueden agrupar o dividir, según sea necesario para que esto sea más eficaz.

void Renderer::Update()
{
    // Rotate the cube 1 degree per frame.
    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.world,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixRotationY(
                DirectX::XMConvertToRadians(
                    (float) m_frameCount++
                    )
                )
            )
        );

    if (m_frameCount == MAXUINT)  m_frameCount = 0;
}

En este caso, Rotate actualiza el búfer de constantes con una nueva matriz de transformación para el cubo. La matriz se multiplicará por vértice durante la fase del sombreador de vértices. Dado que se llama a este método con cada fotograma, este es un buen lugar para agregar cualquier método que actualice las constantes dinámicas y los búferes de vértices, o para realizar cualquier otra operación que prepare los objetos de la escena para su transformación por parte de la canalización de gráficos.

Implementación del método Render

Este método se llama una vez por bucle de juego después de llamar a Update. Al igual que Update, también se llama al método Render desde la clase principal. Este es el método donde se construye y procesa la canalización de gráficos para el marco mediante métodos en la instancia id3D11DeviceContext . Esto culmina en una llamada final a ID3D11DeviceContext::D rawIndexed. Es importante comprender que esta llamada (u otras llamadas de Draw* similares definidas en ID3D11DeviceContext) realmente ejecuta la canalización. En concreto, esto es cuando Direct3D se comunica con la GPU para establecer el estado de dibujo, ejecuta cada fase de canalización y escribe los resultados de píxeles en el recurso de búfer de destino de representación para que lo muestre la cadena de intercambio. Puesto que la comunicación entre la CPU y la GPU conlleva sobrecarga, combine varias llamadas de dibujo en una sola si puede, especialmente si la escena tiene muchos objetos representados.

void Renderer::Render()
{
    // Use the Direct3D device context to draw.
    ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();

    ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
    ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();

    context->UpdateSubresource(
        m_pConstantBuffer.Get(),
        0,
        nullptr,
        &m_constantBufferData,
        0,
        0
        );

    // Clear the render target and the z-buffer.
    const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
    context->ClearRenderTargetView(
        renderTarget,
        teal
        );
    context->ClearDepthStencilView(
        depthStencil,
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
        1.0f,
        0);

    // Set the render target.
    context->OMSetRenderTargets(
        1,
        &renderTarget,
        depthStencil
        );

    // Set up the IA stage by setting the input topology and layout.
    UINT stride = sizeof(VertexPositionColor);
    UINT offset = 0;

    context->IASetVertexBuffers(
        0,
        1,
        m_pVertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

    context->IASetIndexBuffer(
        m_pIndexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );
    
    context->IASetPrimitiveTopology(
        D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
        );

    context->IASetInputLayout(m_pInputLayout.Get());

    // Set up the vertex shader stage.
    context->VSSetShader(
        m_pVertexShader.Get(),
        nullptr,
        0
        );

    context->VSSetConstantBuffers(
        0,
        1,
        m_pConstantBuffer.GetAddressOf()
        );

    // Set up the pixel shader stage.
    context->PSSetShader(
        m_pPixelShader.Get(),
        nullptr,
        0
        );

    // Calling Draw tells Direct3D to start sending commands to the graphics device.
    context->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}

Es recomendable establecer las distintas fases de canalización de gráficos en el contexto en orden. Normalmente, el orden es:

  • Actualice los recursos de búfer de constantes con nuevos datos según sea necesario (mediante datos de Update).
  • Ensamblado de entrada (IA): aquí es donde adjuntamos los búferes de vértices e índices que definen la geometría de la escena. Debe adjuntar cada vértice y búfer de índice para cada objeto de la escena. Dado que este ejemplo tiene solo el cubo, es bastante sencillo.
  • Sombreador de vértices (VS): adjunte los sombreadores de vértices que transformarán los datos en los búferes de vértices y adjunte búferes de constantes para el sombreador de vértices.
  • Sombreador de píxeles (PS): adjunte los sombreadores de píxeles que realizarán operaciones por píxel en la escena rasterizada y adjunte los recursos del dispositivo para el sombreador de píxeles (búferes de constantes, texturas, etc.).
  • Fusión de salida (OM): esta es la fase en la que se combinan píxeles, una vez finalizados los sombreadores. Se trata de una excepción a la regla, ya que asocia las galerías de símbolos de profundidad y los destinos de representación antes de establecer cualquiera de las demás fases. Es posible que tenga varias galerías de símbolos y destinos si tiene sombreadores de vértices y píxeles adicionales que generan texturas como mapas de sombras, mapas de alto u otras técnicas de muestreo; en este caso, cada pase de dibujo necesitará el destino adecuado establecido antes de llamar a una función draw.

A continuación, en la sección final (Trabajar con sombreadores y recursos de sombreador), veremos los sombreadores y analizaremos cómo los ejecuta Direct3D.

Siguiente

Trabajar con sombreadores y recursos de sombreador