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

02.08.2012

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

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

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

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

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

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

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

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

//загружаем эффект
hr = D3DX10CreateEffectFromFile( L"Article02.fx", NULL, NULL, "fx_4_0", dwShaderFlags, 0, g_pd3dDevice, NULL, NULL, &g_pEffect, NULL, NULL );
 
// Получаемтехнику
g_pTechnique = g_pEffect->GetTechniqueByName( "Render" );

Итак, для нашей будущей геометрии мы инициализировали еще две переменные: g_pEffectи g_pTechnique. Эти переменные пригодятся для рисования.

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

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

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

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

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

UINT numElements = sizeof( layout ) / sizeof( layout[0] );
 
// Создание объекта для описания входящего формата
D3D10_PASS_DESC PassDesc;
g_pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, PassDesc.pIAInputSignature,
PassDesc.IAInputSignatureSize, &g_pVertexLayout );
if( FAILED( hr ) )
returnhr;
 
// Установка формата в Direct3D
g_pd3dDevice->IASetInputLayout( g_pVertexLayout );

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

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

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

SimpleVertex vertices[] =
{
        { D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 1.0f, 1.0f ) },
        { D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 0.0f, 1.0f ) },
        { D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 1.0f, 1.0f ) },
        { D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 0.0f, 1.0f ) },
        { D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 1.0f, 1.0f ) },
        { D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 0.0f, 1.0f ) },
        { D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 1.0f, 1.0f ) },
        { D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 1.0f ) },
};
D3D10_BUFFER_DESC bd;
bd.Usage = D3D10_USAGE_DEFAULT;
bd.ByteWidth = sizeof( SimpleVertex ) * 8;
bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;
bd.MiscFlags = 0;
D3D10_SUBRESOURCE_DATA InitData;
InitData.pSysMem = vertices;
hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );
if( FAILED( hr ) )
return hr;

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

Теперь установим этот буфер как вершинный буфер Direct3Dпо умолчанию:

UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
g_pd3dDevice->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );

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

DWORD indices[] =
{
        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 = D3D10_USAGE_DEFAULT;
bd.ByteWidth = sizeof( DWORD ) * 36;        // 36 vertices needed for 12 triangles in a triangle list
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;
bd.MiscFlags = 0;
InitData.pSysMem = indices;
hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer );

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

 
g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );
g_pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

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

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

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

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

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

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

// Clear the back buffer
float ClearColor[4] = { 0.0f, 0.125f, 0.4f, 1.0f }; // red,green,blue,alpha
g_pd3dDevice->ClearRenderTargetView( g_pRenderTargetView, ClearColor );
 
// Update variables
g_pWorldVariable->SetMatrix( ( float* )&g_World );
g_pViewVariable->SetMatrix( ( float* )&g_View );
g_pProjectionVariable->SetMatrix( ( float* )&g_Projection );
 
// Renders a vertex buffer
D3D10_TECHNIQUE_DESC techDesc;
g_pTechnique->GetDesc( &techDesc );
for( UINT p = 0; p < techDesc.Passes; ++p )
{
g_pTechnique->GetPassByIndex( p )->Apply( 0 );
g_pd3dDevice->DrawIndexed( 36, 0, 0 );
}
// Present our back buffer to our front buffer
g_pSwapChain->Present( 0, 0 );

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

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

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

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

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

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

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

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

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

matrixWorld;
matrixView;
matrixProjection;
//------------------------------------------------------------------------------
struct VS_INPUT
{
float4 Pos : POSITION;
float4 Color : COLOR;
}
//------------------------------------------------------------------------------
struct VS_OUTPUT
{
float4 Pos : SV_POSITION;
float4 Color : COLOR0;
};
//------------------------------------------------------------------------------
// VertexShader
//------------------------------------------------------------------------------
VS_OUTPUT VS( VS_INPUT input )
{
VS_OUTPUT output = (VS_OUTPUT)0;
output.Pos = mul( input.Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Color=input.Color;
return output;
}

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

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

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

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

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

//------------------------------------------------------------------------------
// Technique
//------------------------------------------------------------------------------
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}

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

Заключение

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

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

avatar

Об Авторе ()

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

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

Наверх