Общие сведения о конвейере отрисовки 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 их выполняет.
Связанные темы
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по