Урок 2. Вывод 3d геометрии на экран в DirectX11

22.09.2012

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

Обзор архитектуры для вывода геометрии в DirectX11

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

Все данные, которые нужно нарисовать, то есть каркас 3d объекта загружается в вершинный и индексный буфер видеоадаптера – VertexBuffer и соответственно IndexBuffer. Каждый 3d объект состоит из вершин, эти вершины группируются по три, образуя набор полигонов – то есть треугольников, фейсов. Из наборов полигонов и состоит 3d модель.

Далее в адаптер загружается шейдер, то есть программа которая поможет видеоадаптеру обработать наши наборы вершин и полигонов. Шейдеры бывают двух видов, вершинный и пиксельный, и должны быть одновременно загружены в адаптер.Вершинныйшейдер служит промежуточным звеном между пиксельным шейдером и входящими данными вершинного буфера. В основном вершинный шейдер нужен для поворота всех точек модели таким образом чтобы модель правильно отобразилась на экране с учетом точки расположения камеры. Также, осуществляется расчет перспективные — если модель далеко, то она маленькая, если близко – большая. Словом, вершинныйшейдер отвечает за правильное расположение модели на экране и обрабатывает 3d-координаты вершин.

После того как данные обработаны, они поступают из вершинного шейдера в пиксельный. В нем происходит обработка каждой точки полигона. То есть он закрашивается. При этом цвет закраски задается в пиксельном шейдере. Также пиксельный шейдер можно запрограммировать так, чтобы цвета брались из текстуры – так и происходит в 3d играх если модель имеет текстуру. Но предположим, что вершинный шейдер выдал для каждой из трех вершин свой цвет, каким должен получиться треугольник? Правильно, трехцветным. Пиксельный шейдер интерполирует цвета для всех входящих вершин, так что они будут плавно переходить от одной вершины к другой. Для этого не нужно добавлять в пиксельный шейдер специальный код, треугольник станет цветным только потому что выходящими данными вершинного шейдера является полигон – то есть набор из трех точек, а входящими данными для пиксельного шейдера уже не является полигон и координаты этих точек. Сам адаптер плавно передвигает «точку» рисования между вершинами постепенно заполняя треугольник(полигон) сплошным цветом и при этом для каждой маленькой суб-точки закраски вызывая пиксельный шейдер. Надо сказать что отсюда следует непреложный вывод: никогда не грузите пиксельный шейдер чрезмерно кодом – нескольких строк кода почти всегда достаточно, а при лишней загрузке пиксельного шейдера могут быть серьезные тормоза – точек на экране очень много.

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

Инициализация геометрии: InitGeometry

Теперь разберемся, что же нужно сделать чтобы вывести геометрию на экран в DirectX11. Перед тем как приступать к изучении примера, вы можете скачать его исходный код, расположенный тут. Напомним, что в предыдущем уроке мы создали функцию InitDevice. В этой программе также есть эта функция а также остальные, которые мы рассматривали в предыдущем примере, но мы их заново рассматривать не будем а просто включим в нашу программу. Функция InitGeometry будет выполняться перед циклом обработки сообщений, то есть после того, как мы произвели инициализацию Direct3D устройства. Итак, подготовим и создадим геометрию в функции InitGeometry. Мы разобьем все наше рисование на две части – на инициализацию – сначала мы подготовим модель для рисования и на рендеринг – далее мы можем рисовать модель столько раз сколько нужно. В данной части урока мы рассмотрим инициализацию. Первое что мы сделаем в нашей функции InitGeometry — это загрузим код шейдера. На самом деле это просто, просто укажем имя файла шейдера в формате .fx и вызовем фукнцию для загрузки:

// Загружаем шейдеры
ID3DBlob* pVSBlob = NULL;
HRESULT hr;
hr = CompileShaderFromFile( L"Article2.fx", "VS", "vs_4_0", &pVSBlob );
 
// Вершинныйшейдер
hr = g_pd3dDevice->CreateVertexShader( pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), NULL, &g_pVertexShader );
 
// Определение формата вершинного буфера
D3D11_INPUT_ELEMENT_DESClayout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE( layout );
 
// Созданиеформатабуфера
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &g_pVertexLayout );
pVSBlob->Release();
 
// Установкаформатабуфера
g_pImmediateContext->IASetInputLayout( g_pVertexLayout );
 
// Пиксельныйшейдер
ID3DBlob* pPSBlob = NULL;
hr = CompileShaderFromFile( L"Article2.fx", "PS", "ps_4_0", &pPSBlob );
 
// Пиксельныйшейдер
hr = g_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), NULL, &g_pPixelShader );
pPSBlob->Release();

Так как код шейдера состоит из пиксельного шейдера и вершинного шейдера, то функцию загрузки мы должны выполнить два раза – сначала для вершинного шейдера а затем для пиксельного. Затем, перед тем как отобразить объект на экране, мы будем также два раза назначать шейдерDirect3D устройству текущим: сначала назначим текущий вершинный шейдер, затем пиксельный; после этого отобразим модель. После того как шейдеры загружены, они представляются в приложении как два объекта: g_pVertexShader и g_pPixelShader соответственно.

Вторым этапом инициализации модели будет установка формата вершинного буфера. То, что мы будем передавать в вершинный шейдер может иметь различные форматы, и мы должны быть уверены, что в вершинный шейдер мы передаем данные нужного формата. Откроем файл Article02.fx и посмотрим на то, что принимает вершинный шейдер:

VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR ) { ... }

Данное объявление переменных входящих параметров вершинногошейдера соответствует структуре:

struct VS_INPUT
{
float4 Pos   : POSITION;
float4 Color : COLOR;
};

Итак, мы должны передать в вершинныйшейдер для каждой вершины по два числа формата float4.

// Определение формата вершинного буфера
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

Теперь осталось только установить этот формат для D3DDevice. Если в вашей программе все вершины имеют один и тот-же формат, код ниже можно выполнять только один раз в начале программы. Далее Direct3D везде будет использовать этот формат. Установим ранее объявленный формат вершинного буфера:

// Создание формата буфера
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &g_pVertexLayout );
pVSBlob->Release();
 
// Установка формата буфера
g_pImmediateContext->IASetInputLayout( g_pVertexLayout );

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

Инициализация геометрии – создание вершин: InitGeometry

Теперь зададимся вопросом: предположим у нас имеется задумка о том, что будет из себя представлять наш будущий объект, короче – мы имеем конкретные координаты 3d вершин, имеем их количество а также знаем номера вершин которые будет группироваться в треугольники. Но как все эти данные установить в 3d адаптер в качестве вершинного и индексного буфера?

Ответ несложный. Нужно сначала определить эти данные, потом создать на их основе объект вершинного буфера, и затем просто установить этот вершинный буфер в Direct3D (одновременно может быть установлен только один вершинный буфер – то есть устанавливаем буфер для одной модели, рисуем её, потом устанавливаем для другой, рисуем и т.д. Итак, создадим вершинный буфер:

// Создание геометрии для вершинного буфера
SimpleVertexvertices[] =
{
        { XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 0.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 1.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT4( 0.0f, 1.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 0.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 1.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT4( 1.0f, 1.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT4( 0.0f, 0.0f, 0.0f, 1.0f ) },
};
D3D11_BUFFER_DESC bd;
ZeroMemory( &bd, sizeof(bd) );
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( SimpleVertex ) * 8;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory( &InitData, sizeof(InitData) );
InitData.pSysMem = vertices;
hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );
if( FAILED( hr ) )
returnhr;

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

// Установка вершинного буфера
UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
g_pImmediateContext->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );

Теперь создадим индексный буфер чтобы знать какие вершины группируются в треугольники (полигоны):

// Создание индексного буфера
WORDindices[] =
{
        3,1,0,
        2,1,3,
 
        0,5,4,
        1,5,0,
 
        3,4,7,
        0,4,3,
 
        1,6,5,
        2,6,1,
 
        2,7,6,
        3,7,2,
 
        6,4,5,
        7,4,6,
};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( WORD ) * 36;        // 36 vertices needed for 12 triangles in a triangle list
bd.BindFlags = D3D11_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;
InitData.pSysMem = indices;
hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer );

Установим индексный буфер и его топологию (она всегда будет одинаковой).

 
// Установка индексного буфера
g_pImmediateContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );
// Установкатипапримитив
g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

Теперь для того, чтобы отобразить вершины нужно будет всего лишь вызывать каждый кадр метод g_pImmediateContext::DrawIndexed.  Этот код пригодиться дальше в программе, и здесь он приведен лишь для примера:

g_pImmediateContext->DrawIndexed( 36, 0, 0 );

В параметрах процедуры DrawIndexed передается количество вершин в модели.

На этом функция InitGeometry завершена и мы можем смело приступать к функции Render, где мы каждый кадр отобразим нашу модель. В нашем уроке модель представляет из себя куб. У него 8 вершин, однако чтобы образовать сплошной куб их надо соеденить 12-ю полигонами. Также не будем забывать что в Direct3D версии 11 не обойтись без шейдеров и мы уже загрузили код шейдера в нашу программу, так что бегло рассмотрим этот код, после того как разберемся с содержимым Render.

Создание буфера глубины: ZBuffer

Несмотря на то, что мы отлично создали геометрию и заполнили данными геометрии вершинный и индексный буфер, геометрия не будет отображаться правильно, если не будет создан ZBuffer. Еще более очевидно будет влияние ZBuffera, если на экране будет две примитивы, одна впереди другой. Если ZBuffer не будет создан, то один объект может некорректно перекрывать другой, то есть задний объект может неожиданно отобразиться на переднем плане.

// Создание поверхности для Z-буфера
D3D11_TEXTURE2D_DESC descDepth;
ZeroMemory( &descDepth, sizeof(descDepth) );
descDepth.Width = width;
descDepth.Height = height;
descDepth.MipLevels = 1;
descDepth.ArraySize = 1;
descDepth.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
descDepth.SampleDesc.Count = 1;
descDepth.SampleDesc.Quality = 0;
descDepth.Usage = D3D11_USAGE_DEFAULT;
descDepth.BindFlags = D3D11_BIND_DEPTH_STENCIL;
descDepth.CPUAccessFlags = 0;
descDepth.MiscFlags = 0;
hr = g_pd3dDevice->CreateTexture2D( &descDepth, NULL, &g_pDepthStencil );
 
// Создание z-буфреа
D3D11_DEPTH_STENCIL_VIEW_DESC descDSV;
ZeroMemory( &descDSV, sizeof(descDSV) );
descDSV.Format = descDepth.Format;
descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
descDSV.Texture2D.MipSlice = 0;
hr = g_pd3dDevice->CreateDepthStencilView( g_pDepthStencil, &descDSV, &g_pDepthStencilView );

Чтобы этого не произошло, в функции InitDevice мы добавим код для создания объекта типа ID3D11DepthStencilView, представляющий из себяZBuffer и далее назначим этот объект Direct3D устройству. Еще одна важная часть операций с буфером глубины заключается в том, что в функции Render мы должны очищать его содержимое.

Отображение геометрии: Render

На самом деле код Render небольшой. Это потому, что все подготовительные данные уже сделаны и каждый кадр ничего изменять не нужно, нужно просто отображать то что имеется. Посмотримнакод:

void Render()
{
// Установка трансформации для куба
g_World = XMMatrixRotationY( 3.14159f/4.0f );
 
// Очистка рендер-таргета и буфера глубины
float ClearColor[4] = { 0.0f, 0.9f, 0.5f, 1.0f }; // цвет
g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );
g_pImmediateContext->ClearDepthStencilView( g_pDepthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0 );
 
// Установка констант шейдера
ConstantBuffercb;
cb.mWorld = XMMatrixTranspose( g_World );
cb.mView = XMMatrixTranspose( g_View );
cb.mProjection = XMMatrixTranspose( g_Projection );
g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &cb, 0, 0 );
 
// Рендер куба
g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );
g_pImmediateContext->VSSetConstantBuffers( 0, 1, &g_pConstantBuffer );
g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );
g_pImmediateContext->DrawIndexed( 36, 0, 0 );       // 36 вершин образуют 12 полигонов, по три вершины на полигон
 
// Вывод на экран содержимого рендер-таргета
g_pSwapChain->Present( 0, 0 );
}

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

Обзор приложения

Теперь остается только скомпилировать и запустить приложение.

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

Обзор шейдера для нашего приложения

Практически при любом отображении чего-либо на экран в DirectX11 необходимо использовать шейдеры. Шейдерпредставляет из себя программу на языке HLSL. Этот язык подобен языку C. В предыдущих разделах мы загрузили шейдер, но совершенно его не рассмотрели. Сейчас мы займемся этим. Шейдеры делятся на два типа – вершинный и пиксельный. Рассмотрим вершинный шейдер. Его назначение – просто преобразовывать координаты вершин. Если мы не будем их преобразовывать, то все координаты будут выведены на экран в таком пространстве:

То есть, если мы отобразим треугольник, находящейся в плоскости XY, то он предстанет в неизмененном виде. Однако если треугольник будет в плоскости XZ, то он будет в виде тонкой линии или не будет виден совсем. Если же мы установим камеру, то мы сможем вращать её вокруг треугольника, при этом также будут пересчитываться его координаты для вывода в вышеуказанное пространство экрана(-1,1),(-1,1). Пересчетом координат и занимается вершинныйшейдер – для этого каждая координата умножается на несколько матриц преобразования.

Стоит отметить, что если мы не будем умножать координаты точек на матрицы, или будем умножать их на еденичные матрицы – что соответствует отсутствию всякого преобразования, то это также можно использовать, например, для отображения всевозможных надписей, элементов интерфейса и другой плоской графики поверх основной 3d графики сцены. Также такая техника подойдет для игр где графика выполнена в виде спрайтов. На самом деле она настолько востребована, что системы частиц (партиклы) в какой то мере также используеют только частичное преобразование 3d координат, в результате чего частицы всегда выводятся на экран плоско, однако облака этих частиц, конечно же находятся в некотором объеме.

Но мы отступили от темы. Рассмотрим код нашегошейдера. Обратимся к вершинному шейдеру:

//------------------------------------------------------------------------------------
// Constant Buffer Variables
//------------------------------------------------------------------------------------
cbuffer ConstantBuffer : register( b0 )
{
      matrix World;
      matrix View;
      matrix Projection;
}
 
//------------------------------------------------------------------------------------
struct VS_OUTPUT
{
    float4 Pos : SV_POSITION;
    float4 Color : COLOR0;
};
 
//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR )
{
    VS_OUTPUT output = (VS_OUTPUT)0;
    output.Pos = mul( Pos, World );
    output.Pos = mul( output.Pos, View );
    output.Pos = mul( output.Pos, Projection );
    output.Color=float4(0.5f,0.5f,0.5f,1.0f);
    returnoutput;
}

Обратите внимание на процедуру VS в нижней части кода. Это и есть вершинный шейдер. Входящими данными нашего вершинного шейдера являются параметры функции VS. Они соответствует формату, который мы рассматривали ранее в уроке, каждая вершина содержит координату и цвет. Сама процедура вершинногошейдера называется VS. Как вы можете видеть, шейдер имеет на выходе практически тот-жеформат что и на входе. В него входят координаты и выходят тоже координаты, только измененные. В текущем уроке мы умножаем координаты на единичные матрицы, что соответствует отсутствию всякого преобразования. (Для наглядности урока матрицы все же установлены так чтобы куб был повернут на 450 относительно вертикальной оси). Фактически, мы могли бы и просто приравнять output.Pos к input.Pos, тогда бы куб отображался плоским. Однако в следующих уроках мы установим матрицы для камеры и трансформаций и немного оживим сцену, поворачивая камеру на любые углы и рассматривая модель с различных сторон, а также приближая и отдаляя камеру.

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

//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
float4 PS( VS_OUTPUT input ) : SV_Target
{
    returninput.Color;
}

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

И последним разделом нашего шейдера является техника. Техника(technique) это уже как бы не код C, это что-то вроде материала. То есть для каждого вида материала мы можем выбрать произвольный вершинный и пиксельный шейдеры. То есть, в одном и том-же файле .fx может находиться много различных вершинных и пиксельных шейдеров, назначая определенный материал (но в DirectX он называется техникой).

technique11 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}

Обратите внимание где именно в технике указывается название функций для вершинного и пиксельного шейдера. Указанием имен функций мы даем понять данному материалу (технике) что именно этот вершинный и пиксельный шейдер нужно установить.

Заключение

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

Заполнен: DirectX11
Присвоен тэг:

avatar

Об Авторе ()

Планета назначения, JO - 8703 - IV, обозначается светлой точкой на объемном экране корабля. К тому времени, как Робоид нанялся на юпитерианский круговой рейс, ему следовало бы стать главным инженером, но после Суховодного пионерского его вышибли, внеся в черный список, и высадили в Луна-Сити за то, что вместо слежения за приборами он провел время, программируя синтезатор пищи корабля строжайше запрещенной корабельными инструкциями последовательностью синтеза пива.

Комментирование закрыто.

Наверх