Общие сведения о конвейере отрисовки Direct3D 11

Ранее вы рассмотрели, как создать окно, которое можно использовать для рисования, в разделе Работа с ресурсами устройств DirectX. Теперь вы узнаете, как создать графический конвейер и где его можно подключить.

Вы помните, что есть два интерфейса Direct3D, которые определяют графический конвейер: ID3D11Device, который предоставляет виртуальное представление GPU и его ресурсов; и ID3D11DeviceContext, который представляет обработку графики для конвейера. Как правило, экземпляр ID3D11Device используется для настройки и получения ресурсов GPU, необходимых для начала обработки графики в сцене, а id3D11DeviceContext используется для обработки этих ресурсов на каждом соответствующем этапе шейдера в графическом конвейере. Обычно методы ID3D11Device вызываются редко, то есть только при настройке сцены или при изменении устройства. С другой стороны, вы будете вызывать ID3D11DeviceContext при каждой обработке кадра для отображения.

В этом примере создается и настраивается минимальный графический конвейер, подходящий для отображения простого вращающегося куба с затенениями вершин. Он демонстрирует приблизительно наименьший набор ресурсов, необходимых для отображения. Читая сведения здесь, обратите внимание на ограничения данного примера, из-за которых может потребоваться расширить его для поддержки сцены, которую вы хотите отрисовать.

В этом примере рассматриваются два класса C++ для графики: класс диспетчера ресурсов устройства и класс отрисовщика трехмерной сцены. В этом разделе основное внимание уделяется отрисовщику трехмерной сцены.

Что делает отрисовщик куба?

Графический конвейер определяется классом отрисовщика трехмерной сцены. Отрисовщик сцены может:

  • Определите буферы констант для хранения универсальных данных.
  • Определите буферы вершин для хранения данных вершин объекта и соответствующие буферы индексов, чтобы шейдер вершин мог правильно ходить по треугольникам.
  • Создание ресурсов текстуры и представлений ресурсов.
  • Загрузите объекты шейдера.
  • Обновите графические данные для отображения каждого кадра.
  • Отрисовка (рисование) графики в цепочке буферов.

Первые четыре процесса обычно используют методы интерфейса ID3D11Device для инициализации графических ресурсов и управления ими, а последние два используют методы интерфейса ID3D11DeviceContext для управления графическим конвейером и его выполнения.

Экземпляр класса Renderer создается и управляется как переменная-член класса проекта main. Экземпляр DeviceResources управляется как общий указатель в нескольких классах, включая класс проекта main, класс app view-provider и Renderer. Если вы замените Renderer собственным классом, рассмотрите возможность объявления и назначения экземпляра DeviceResources в качестве общего элемента указателя:

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

Просто передайте указатель в конструктор класса (или другой метод инициализации) после создания экземпляра DeviceResources в методе Initialize класса App . Вы также можете передать ссылку на weak_ptr, если вместо этого хотите, чтобы класс main полностью владел экземпляром DeviceResources.

Создание отрисовщика куба

В этом примере мы упорядочим класс отрисовщика сцены с помощью следующих методов:

  • CreateDeviceDependentResources: вызывается всякий раз, когда сцена должна быть инициализирована или перезапущена. Этот метод загружает исходные данные вершин, текстуры, шейдеры и другие ресурсы, а также создает исходные константы и буферы вершин. Как правило, большая часть работы здесь выполняется с помощью методов ID3D11Device , а не методов ID3D11DeviceContext .
  • CreateWindowSizeDependentResources: вызывается при изменении состояния окна, например при изменении размера или при изменении ориентации. Этот метод перестраивает матрицы преобразования, например матрицы для камеры.
  • Обновление: обычно вызывается из части программы, которая управляет непосредственным состоянием игры; В этом примере мы просто вызываем его из класса Main . Этот метод должен считывать любые сведения о состоянии игры, которые влияют на отрисовку, например из обновлений положения объекта или кадров анимации, а также из любых глобальных игровых данных, таких как уровни освещения или изменения физики игры. Эти входные данные используются для обновления буферов констант для каждого кадра и данных объектов.
  • Render: обычно вызывается из части программы, которая управляет игровым циклом; в этом случае он вызывается из класса Main . Этот метод создает графический конвейер: привязывает шейдеры, привязывает буферы и ресурсы к этапам шейдера и вызывает рисование для текущего кадра.

Эти методы составляют тело поведения для отрисовки сцены с Direct3D с использованием ресурсов. Если вы расширяете этот пример с помощью нового класса отрисовки, объявите его в классе проекта main. Итак, вот что:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

становится

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Опять же, обратите внимание, что в этом примере предполагается, что методы имеют одинаковые сигнатуры в реализации. Если сигнатуры изменились, просмотрите цикл Main и внесите соответствующие изменения.

Давайте рассмотрим методы отрисовки сцены более подробно.

Создание зависимых от устройств ресурсов

CreateDeviceDependentResources объединяет все операции для инициализации сцены и ее ресурсов с помощью вызовов ID3D11Device . Этот метод предполагает, что устройство Direct3D было только что инициализировано (или создано повторно) для сцены. Он воссоздает или перезагружает все графические ресурсы, относящиеся к сцене, такие как вершинные и пиксельные шейдеры, буферы вершин и индексов для объектов, а также любые другие ресурсы (например, как текстуры и соответствующие представления).

Ниже приведен пример кода для 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();
}

Каждый раз, когда вы загружаете ресурсы с диска, такие как скомпилированные файлы объектов шейдера (CSO или CSO) или текстуры, сделайте это асинхронно. Это позволяет одновременно выполнять другие задачи (как и другие задачи настройки), а так как цикл main не заблокирован, вы можете отображать что-то визуально интересное для пользователя (например, анимацию загрузки для игры). В этом примере используется API Concurrency::Tasks, доступный начиная с Windows 8. Обратите внимание на лямбда-синтаксис, используемый для инкапсуляции задач асинхронной загрузки. Эти лямбда-выражения представляют функции, вызываемые off-thread, поэтому явным образом фиксируется указатель на текущий объект класса (this).

Ниже приведен пример загрузки байт-кода шейдера:

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

Ниже приведен пример создания буферов вершин и индексов.

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

В этом примере не загружается ни сетка, ни текстуры. Необходимо создать методы для загрузки типов сетки и текстур, относящихся к вашей игре, и вызвать их асинхронно.

Заполните начальные значения для буферов констант для каждой сцены. Примерами буфера констант для каждой сцены являются фиксированные источники света или другие статические элементы сцены и данные.

Реализация метода CreateWindowSizeDependentResources

Методы CreateWindowSizeDependentResources вызываются при каждом изменении размера, ориентации или разрешения окна.

Ресурсы размера окна обновляются следующим образом: статическое сообщение proc получает одно из нескольких возможных событий, указывающих на изменение состояния окна. Затем цикл main информируется о событии и вызывает CreateWindowSizeDependentResources в экземпляре класса main, который затем вызывает реализацию CreateWindowSizeDependentResources в классе отрисовщика сцены.

Основная задача этого метода — гарантировать, что в результате изменения свойств окна визуальные объекты не станут беспорядочными или недействительными. В этом примере мы обновим матрицы проекта, указав новое поле зрения (FOV) для окна с измененным или измененным размером.

Мы уже видели код для создания ресурсов окна в DeviceResources — это была цепочка буферов (с обратным буфером) и целевое представление отрисовки. Ниже показано, как отрисовщик создает преобразования, зависящие от пропорций.

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

Если сцена имеет определенный макет компонентов, который зависит от пропорций, это место для их переупорядочения в соответствии с этим соотношением сторон. Здесь также может потребоваться изменить конфигурацию поведения постобработки.

Реализация метода Update

Метод Update вызывается один раз для каждого игрового цикла. В этом примере он вызывается методом класса main с тем же именем. Он имеет простую цель: обновить геометрию сцены и состояние игры в зависимости от количества затраченного времени (или шагов времени) с момента предыдущего кадра. В этом примере мы просто поворачиваем куб один раз на кадр. В реальной игровой сцене этот метод содержит гораздо больше кода для проверки состояния игры, обновления буферов констант для каждого кадра (или других динамических) буферов, буферов геометрии и других ресурсов в памяти соответственно. Так как взаимодействие между ЦП и GPU влечет за собой дополнительные затраты, обновите только буферы, которые фактически изменились с момента последнего кадра. Буферы констант можно сгруппировать или разделить по мере необходимости, чтобы сделать это более эффективным.

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

В этом случае функция Rotate обновляет буфер констант с помощью новой матрицы преобразования для куба. Матрица будет умножена на одну вершину на этапе вершинного шейдера. Так как этот метод вызывается с каждым кадром, это хорошее место для агрегирования любых методов, которые обновляют динамические константы и буферы вершин, или для выполнения любых других операций, которые подготавливают объекты в сцене к преобразованию с помощью графического конвейера.

Реализация метода Render

Этот метод вызывается один раз для каждого игрового цикла после вызова Update. Как и Update, метод Render также вызывается из класса main. Это метод, в котором графический конвейер создается и обрабатывается для кадра с помощью методов экземпляра ID3D11DeviceContext . Это завершается окончательным вызовом ID3D11DeviceContext::D rawIndexed. Важно понимать, что этот вызов (или другие аналогичные вызовы Draw* , определенные в ID3D11DeviceContext) фактически выполняет конвейер. В частности, это происходит, когда Direct3D взаимодействует с GPU для установки состояния рисования, запускает каждый этап конвейера и записывает результаты пикселей в ресурс буфера целевого объекта отрисовки для отображения цепочкой буферов. Так как взаимодействие между ЦП и GPU влечет за собой дополнительные затраты, объедините несколько вызовов draw в один, если это возможно, особенно если в сцене много отрисованных объектов.

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

Рекомендуется задавать различные этапы графического конвейера в контексте по порядку. Как правило, порядок:

  • При необходимости обновляйте ресурсы буфера констант с новыми данными (с помощью данных из раздела Update).
  • Входная сборка (IA). Здесь мы присоединяем буферы вершин и индексов, которые определяют геометрию сцены. Необходимо прикрепить каждую вершину и буфер индекса для каждого объекта в сцене. Так как в этом примере есть только куб, это довольно просто.
  • Вершинный шейдер (VS): прикрепите все вершинные шейдеры, которые преобразуют данные в буферах вершин, и прикрепите буферы констант для вершинного шейдера.
  • Пиксельный шейдер (PS): подключите все пиксельные шейдеры, которые будут выполнять операции по пикселям в растровой сцене, и подключите ресурсы устройства для шейдера пикселей (буферы констант, текстуры и т. д.).
  • Объединение выходных данных (OM). Это этап, на котором пиксели смешиваются после завершения шейдеров. Это исключение из правила, так как вы подключаете наборы элементов глубины и отрисовываете целевые объекты перед установкой любого из других этапов. У вас может быть несколько наборов элементов и целевых объектов, если у вас есть дополнительные вершинные и пиксельные шейдеры, которые создают текстуры, такие как карты теней, карты высоты или другие методы выборки. В этом случае каждому проходу рисования потребуется установить соответствующие целевые объекты перед вызовом функции рисования.

Далее в последнем разделе (Работа с шейдерами и ресурсами шейдеров) мы рассмотрим шейдеры и обсудим, как Direct3D их выполняет.

Далее

Работа с шейдерами и ресурсами шейдеров