Поделиться через


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

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

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

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

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

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

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

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

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

Экземпляр класса Renderer создается и управляется в качестве переменной члена в основном классе проекта. Экземпляр DeviceResources управляется как умный указатель между несколькими классами, включая основной класс проекта , класс провайдера представления App и класс Renderer. Если заменить renderer собственным классом, рассмотрите возможность объявления и назначения экземпляра DeviceResources в качестве общего элемента указателя.

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

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

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

В этом примере мы организуем класс рендеринга сцены по следующим методам:

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

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

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) или текстуры, производите это асинхронно. Это позволяет одновременно выполнять другую работу (например, задачи установки), а так как основной цикл не блокируется, вы можете отображать что-то визуально интересное для пользователя, например, анимацию загрузки игры. В этом примере используется API Concurrency::Tasks, доступный начиная с Windows 8. Обратите внимание на синтаксис лямбда-выражений, используемый для инкапсуляции задач асинхронной загрузки. Эти лямбда-функции представляют функции, вызываемые вне потока, поэтому указатель на текущий объект класса (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 вызываются при каждом изменении размера окна, ориентации или разрешения.

Ресурсы размера окна обновляются так: статическая функция обработки сообщений получает одно из нескольких возможных событий, указывающих на изменение состояния окна. Затем основной цикл получает информацию о событии и вызывает CreateWindowSizeDependentResources на экземпляре основного класса, который затем вызывает 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 вызывается один раз в цикле игры. В этом примере он вызывается методом основного класса с тем же именем. Она имеет простую цель: обновление геометрии сцены и состояния игры на основе количества прошедшего времени (или шагов времени) с момента предыдущего кадра. В этом примере мы просто поворачиваем куб один раз на кадр. В реальной игровой сцене этот метод содержит гораздо больше кода, необходимого для проверки состояния игры, обновления кадровых (или других динамических) константных буферов, геометрических буферов и других ресурсов в памяти соответственно. Так как обмен данными между ЦП и 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;
}

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

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

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

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 выполняет их.

Впереди

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