Меши, вершинные и индексные буферы в DirectX9

Статья запоздала на лет эдак на 15, если не на 18. Но все же в 2019 году на DirectX9 еще можно писать игры.

Рассмотрим функцию CreateVertexBuffer:

HRESULT CreateVertexBuffer(
[in] UINT Length,
[in] DWORD Usage,
[in] DWORD FVF,
[in] D3DPOOL Pool,
[out, retval] IDirect3DVertexBuffer9 **ppVertexBuffer,
[in] HANDLE *pSharedHandle
);

Пройдемся по параметрам функции, в чем нам поможет документация по DirectX9 SDK.

Length

Параметр Length, как сказано в документации, должен содержать размер вершинного буфера в байтах. Тут сказано также, что для вершинных буферов FVF длина буфера должна быть достаточно большой, чтобы в буфер могла поместиться по крайней мере одна вершина. При этом, необязательно, чтобы длина буфера была кратна размеру одной вершины. То есть якобы, можно впихнуть в буфер столько вершин, сколько в него влезет и еще может остаться пара-тройка свободных байт. Это все справедливо для FVF-буфера. Что же такое FVF вершинный буфер? В документации сказано:

«Установка значения, отличного от нуля и являющегося допустимым FVF кодом,  параметру FVF в методе IDirect3DDevice9::CreateVertexBuffer, указывает на то, что о содержимом буфера вершин можно судить по FVF кодам. Вершинный буфер, который создан с кодом FVF, называется вершинным буфером FVF».

Возвращаясь к описанию параметра «Length», дополню, что длина не-FVF буфера не проверяется.

Как рассчитать длину вершинного буфера? Очень просто, если знать размер памяти, который занимают данные одной вершины. Нужно просто умножить число вершин на размер памяти, занимаемый одной вершиной:

Length = nVertices * vertexSize.

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

Usage

Тип использования. Тип использования чего? Рассуждая логически, можно заключить, что речь идет о типе использования буфера вершин. Ведь термин буфер в программировании означает нечто вроде куска памяти. А вот интерпретация содержимого буфера, т. е. то, как его будут использовать, зависит от программиста. В DirectX 9 тип использования вершинного буфера можно задать в виде константных макросов, имя которых начинается с префикса «D3DUSAGE_». Внимание! Не все такие макросы применимы к вершинному буферу — есть соответствующая таблица, где указано, какие макросы для каких типов ресурса можно использовать. Вершинный буфер — это один из типов ресурса DirectX 9. Посмотрим на эти макросы.

D3DUSAGE_DONOTCLIP — «установить этот флаг для указания того, что содержимое вершинного буфера никогда не нужно обрезать. При рендеринге буфера, у которого установлен этот флаг, нужно установить состояние рендера D3DRS_CLIPPING в false». Немного непонятно, что значит «обрезать содержимое буфера». Из того, что я прочитал и понял в документации DirectX 9 SDK, вершинный буфер может хранить флаги отсечения тех вершин, которые не попадают в поле зрения. Флаги отсечения занимают дополнительную память, делая буфер вершин, который способен хранить подобную информацию, несколько более объемным, чем буфер вершин, который не способен нести информацию об отсечении вершин. Флаг D3DUSAGE_DONOTCLIP запрещает сопровождать вершинный буфер информацией об отсечении вершин. Такой флаг может применяться в параметре Usage только тогда, когда указано, что буфер вершин будет содержать трансформированные вершины, для чего параметр FVF должен содержать код D3DFVF_XYZRHW.

Что значит «трансформированные вершины»? Продолжение следует…

 

Чем отличаются Substance Designer и Substance Painter

Много раз задавался вопросом «В чем отличие программ Substance Designer и Substance Painter?» Просмотрев данное видео, вроде получил ответ на вопрос. Автор видео рассказывает о функциональных отличиях программ для создания PBR материалов.

Структура данных «Дерево»

Лицензия Creative Commons
Это произведение доступно по лицензии Creative Commons «Attribution» («Атрибуция») 4.0 Всемирная.

Зачем нужны деревья?

При написании игрового движка часто возникает потребность связать игровые объекты так, чтобы при перемещении одного игрового объекта по сцене вместе с ним перемещались бы объекты, которые к нему привязаны. Примеры:

  • автомобиль, состоящий из кузова и колес
  • танк с вращающейся башней
  • оружие в руке персонажа

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

Как известно, для анимации персонажа используется технология скелетной анимации. Так вот, скелет персонажа также можно представить в форме дерева.

Часто в играх используются одинаковые предметы, разбросанные по уровню. Это разнообразные деревья, камни, ящики, бочки и т.д. Нужно отметить, что в узел дерева сцены можно добавить какие угодно данные. Например, если добавить в узел дерева сцены указатель на объект 3D-модели, то можно будет получить возможность использовать одну 3D-модель для нескольких экземпляров игровых объектов одного типа. Например, если нужно расположить несколько одинаковых бочек на уровне, нужно будет загрузить в память всего одну 3D-модель бочки и связать с ней столько узлов дерева сцены, сколько бочек данного вида необходимо на уровне. Получаем экономию ресурсов. Конечно, это упрощенное описание, но суть оно передает.

Структура дерева

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

cdn_article_tree

Как программно представить дерево?

Так как узлы связаны друг с другом, то нужно сначала создать класс узла. В общем случае у каждого узла есть:

  • родитель;
  • множество дочерних узлов.
class TreeNode {
public:
  TreeNode* parent_; // указатель на родительский узел
  std::list<TreeNode*> childList_; // список дочерних узлов
};

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

class TreeNode {
public:
  TreeNode* parent_; // указатель на родительский узел
  TreeNode* firstChild_; // указатель на первый дочерний узел

  // указатели для перебора дочерних узлов родителя данного узла
  TreeNode* next_;
  TreeNode* prev_;
};

Первый подход быстрее в реализации, поскольку вам самим не придется программировать вставку и удаление дочерних узлов в список. Также список std::list предоставляет удобный способ прохода по дочерним узлам. Но впоследствии нужно будет думать о применении оптимального аллокатора (распределителя) памяти, потому что список std::list создает копии хранимых элементов через оператор new (если вы не задали свой аллокатор), что при частой вставке и удалении дочерних узлов приведет к дефрагментации памяти.

Правила программирования вставки и удаления узлов из дерева

1. Сначала отсоедини, потом присоединяй.

Вставлять в дерево можно только свободные узлы — то есть узлы, которые не привязаны ни к какому дереву. Как не трудно догадаться, когда узел отсоединяется от дерева, то он сам становится деревом. Т.е. свободные узлы — это деревья. Пусть даже если дерево состоит из одного корня.

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

2. Связь узлов  — двусторонняя.

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

3. В дереве не может быть циклов.

Нельзя делать узел, который находится выше по иерархии, чем данный узел, дочерним узлом данного. Грубо говоря, родитель или (пра-)дед данного узла не может быть его дочерним узлом или (пра-)внуком. Это правило называется исключением циклов в дереве. Ведь есть риск подцепить дерево к своей же ветке, поскольку дерево является свободным узлом.

4. Предусмотрите управление распределением памяти для узлов.

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

Поведение дерева

Паттерн «грязный бит»

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

Существующие библиотеки

Я реализовал библиотеку дерева узлов, она может служить базой для реализации класса узла сцены, останется лишь добавить данные трансформации и оперирование ими, может еще кастование к указателям на новый класс. Вот репозиторий. Если найдете ошибки или будет непонятно как использовать, пишите — попробую исправить.

Ошибки геймдизайна на примере игры Dreamkiller

Как большой поклонник игры Painkiller и жанра «аркадный шутер», я не устаю собирать различные сведения о ней и разработке. Мне повезло, сегодня я посмотрел ролик на ютубе, где на примере игры Dreamkiller, которая является некоей попыткой сделать Painkiller-подобную игру, рассказывается об основных ошибках в ее геймдизайне.

Приятного просмотра:

Инициализация Direct3D9 в приложении

Инициализация Direct3D9

Лицензия Creative Commons
Это произведение доступно по лицензии Creative Commons «Attribution» («Атрибуция») 4.0 Всемирная.

Данная статья является попыткой составления общей схемы инициализации библиотеки Direct3D9 при написании приложений, использующих 3D-графику. Исходным материалом послужили результаты анализа справочной системы DirectX SDK9 и нескольких графических и игровых движков с открытым исходным кодом.

Для того, чтобы можно было работать с библиотекой DirectX9, а именно для использования ее части Direct3D9 для рендера графики, необходимо вначале создать два объекта. Первый объект, который нужно создать, это объект типа IDirect3D9 – главный интерфейс Direct3D9. Он представляет собой некий менеджер для создания всех остальных объектов Direct3D9 (устройств, текстур, вершинных и индексных буферов, шейдеров и пр.). Чтобы создать объект типа IDirect3D9, необходимо вызвать функцию Direct3DCreate9:

// Пример — создание объекта IDirect3D9
LPDIRECT3D9 g_pD3D = NULL;
if( 0 == (g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)))
    return E_FAIL;

Второй объект, который необходимо создать — это устройство Direct3D9. Его назначение — предоставлять программисту возможности по созданию различных ресурсов и рендера объектов в зависимости от возможностей, которыми обладает видеокарта. Для создания объекта устройства, в DirectX9 SDK реализован метод интерфейса IDirect3D9 с именем CreateDevice. Рассмотрим его прототип:

HRESULT CreateDevice(UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow,
    DWORD BehaviorFlags,D3DPRESENT_PARAMETERS *pPresentationParameters,
    IDirect3DDevice9 **ppReturnedDeviceInterface);

Adapter это номер видеокарты в системе. Дело в том, что на компьютере пользователя может быть установлено более одной видеокарты. Типичный случай для начала 2000-х — наличие одной встроенной видеокарты и одной внешней.

DeviceType тип устройства. DirectX может эмулировать аппаратные возможности видеокарты в случае, если они не поддерживаются видеокартой. На практике используются два типа устройств. Устройство REF используется для разработки новейших эффектов и шейдеров в режиме эмуляции, если они не поддерживаются аппаратно видеокартой. Устройство HAL — это устройство Direct3D9, в котором задействованы те аппаратные возможности видеокарты, которые доступны на данной видеокарте. В итоговом приложении (игре, редакторе), которое будет запускаться конечным пользователем, устройство Direct3D9 должно быть всегда создано с типом HAL и никогда с типом REF! Устройство типа REF используется исключительно разработчиками движков на стадии отладки или эмуляции недоступных аппаратно возможностей видеокарт. В силу этого рендер на устройстве REF практически всегда будет очень медленным.

hFocusWindow идентификатор окна. Устройство Direct3D9 всегда должно быть связано с каким-либо окном.

BehaviorFlags в этом параметре обычно передают способ обработки вершин, будет ли рассчитываться трансформации вершин аппаратно на видеокарте или программно на CPU.

pPresentationParametersв этом параметре нужно передать структуру, которая описывает параметры представления устройства Direct3D9. Об этом чуть дальше.

ppReturnedDeviceInterface сюда нужно передать указатель на указатель интерфейса устройства, который будет хранить адрес созданного устройства Direct3D9.

Теперь рассмотрим все параметры по порядку более детально.

Номер видеокарты (видеоадаптера)

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

Для тех, кто хочет в своем коде обеспечить поддержку выбора пользователем определенной видеокарты, при помощи метода GetAdapterCount() интерфейса IDirect3D9 необходимо узнать число установленных в системе видеокарт, которые распознал DirectX. Номера видеоадаптеров находятся в интервале [0, число видеоадаптеров). То есть от нуля до числа видеоадаптеров минус единица (обратите внимание на круглую скобку, которая в теории множеств означает, что правая граница не включается в интервал). Левая граница 0 диапазона задается типом UINT, правая числом видеоадаптеров в системе.

Тип устройства

Как уже было сказано, если вас не волнует разработка новейших эффектов и задействование самых последних возможностей видеокарт, то можно всегда применять тип устройства HAL. В Direct3D9 тип устройства HAL задается значением D3DDEVTYPE_HAL, а тип устройства REF – значением D3DDEVTYPE_REF. Существуют и другие типы, информацию о которых можно узнать из справки, поставляемой вместе с SDK. Проще всего поступать так. Если вы пишете графический движок, используйте HAL. А когда вам нужно разработать какой-то шейдер и на вашей видеокарте не реализованы нужные возможности (которые есть в DirectX9), используйте REF устройство. Но после того как убедитесь, что шейдер «работает», измените тип устройства обратно на HAL. REF устройство очень медленно рисует графику и в режиме реального времени не применимо. Немного упрощенно, но в целом, я думаю, понятно.

Окно фокуса

Как говорится в SDK окно фокуса используется Direct3D9 для того, чтобы Direct3D9 был в курсе того, что окно, в которое происходит рендер, потеряло фокус. Окно теряет фокус, когда нажимается комбинация клавиш Alt+Tab или когда его сворачивают, нажимая на кнопку «-» (тире). Когда окно, в котором производится рендер, теряет свой фокус, Direct3D9 не может продолжать работать в прежнем режиме и должен перенастроиться. А чтобы Direct3D смог перенастроиться, ему надо сообщить, что окно потеряло фокус. Вот для этого и служит этот параметр. Далее еще вернемся к этому вопросу.

Обработка вершин

Существуют три флага для задания способа обработки вершин на устройстве типа HAL:

D3DCREATE_SOFTWARE_VERTEXPROCESSING

D3DCREATE_HARDWARE_VERTEXPROCESSING

D3DCREATE_MIXED_VERTEXPROCESSING

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

Флаг D3DCREATE_SOFTWARE_VERTEXPROCESSING требует, чтобы обработка вершин выполнялась на CPU программно.

Флаг D3DCREATE_MIXED_VERTEXPROCESSING требует, чтобы обработка вершин могла выполнялась и аппаратно, и программно. Этот режим обработки позволяет переключаться между режимом программной и аппаратной обработки вершин уже после создания устройства Direct3D9.

Обычный подход, который применяется для выбора обработки вершин — метод последовательных проб, иллюстрируемый примером ниже:

////
// LPDIRECT3D9 d3d; // создан ранее
// LPDIRECT3DDEVICE9 device; 
// HWND hwnd; // создано ранее
// D3DPRESENT_PARAMETERS presentPars; // заполнена ранее
D3DCAPS9 devcaps;
d3d->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &devcaps);
if ( (D3DDEVCAPS_HWTRANSFORMANDLIGHT & devcaps.DevCaps) != 0 ){
    HRESULT hr = d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, 
    D3DCREATE_HARDWARE_VERTEXPROCESSING, &presentPars, &device);
    if (FAILED(hr)){
        hr = d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, 
                D3DCREATE_MIXED_VERTEXPROCESSING, &presentPars, &device);
        if (FAILED(hr)){
            hr = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, 
                    D3DCREATE_SOFTWARE_VERTEXPROCESSING, 
                    &presentPars, &device);
            if (FAILED(hr)) { // ошибка — невозможно создать устройство }
       }
    }
}
else{
    HRESULT hr = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, 
                   D3DCREATE_SOFTWARE_VERTEXPROCESSING, 
                   &presentPars, &device);
    if (FAILED(hr)) { // ошибка — невозможно создать устройство }
}

В структуре D3DCAPS9 есть поле DevCaps. Если это поле содержит битовый флаг D3DDEVCAPS_HWTRANSFORMANDLIGHT, то трансформация и обработка вершин на устройстве Direct3D9 может выполняться аппаратно. Сама структура D3DCAPS9 перед проверкой ее полей должна быть заполнена при помощи метода GetDeviceCaps() главного интерфейса Direct3D9.

Параметры представления

Формат пикселя

В DirectX существует понятие формата пикселя, который описывает, какие цветовые компоненты используются форматом и сколько бит они занимают в одном пикселе. Под видеорежимом понимается совокупность следующих параметров: ширина и высота экрана, формат пикселей экрана и частота развертки. Каждый видеорежим поддерживает только определенные форматы пикселей. В Direct3D9 для задания видеорежима доступны следующие форматы пикселя (согласно DX SDK):

  • D3DFMT_A2R10G10B10

  • D3DFMT_X8R8G8B8

  • D3DFMT_A8R8G8B8

  • D3DFMT_R5G6B5

  • D3DFMT_X1R5G5B5

  • D3DFMT_A1R5G5B5

Любой из этих режимов может быть использован для заднего буфера. Для пикселей экрана форматы D3DFMT_A8R8G8B8 и D3DFMT_A1R5G5B5 использоваться не могут. А формат D3DFMT_A2R10G10B10 может использоваться для пикселей экрана только в полноэкранном режиме.

Как правило, пользователь движка (программист, который использует движок) или конечный пользователь не имеет понятия о каких-то форматах, но имеет понятие или слышал о глубине цвета. Глубина цвета — это сколько бит используется для задания цветового значения пикселя. Бывают две глубины цвета: по 16 и 32 бит на пиксель. В связи с этим, сначала необходимо отобразить значение желаемой глубины цвета в формат пикселя согласно нижеследующей таблице:

Формат пикселя

Глубина цвета

D3DFMT_A2R10G10B10

32

D3DFMT_X8R8G8B8

32

D3DFMT_A8R8G8B8

32

D3DFMT_R5G6B5

16

D3DFMT_X1R5G5B5

16

D3DFMT_A1R5G5B5

16

Как выбрать теперь конкретный формат? Что тут можно посоветовать. Как правило, разработчики движков либо выбирают какой-то один наиболее универсальный формат на каждое значение глубины цвета так, чтобы на каждую компоненту цвета приходилось бы примерно одинаковое число бит, либо же используют какую-то стратегию последовательного перебора форматов от «лучшего» к «худшему» пока не будет найден подходящий поддерживаемый формат. Например, существует такая стратегия — попробовать найти режим дисплея сначала для формата с дополнительным полем «X», потом для формата с альфа-каналом и затем для формата с меньшим числом бит на альфа-канал или с отсутствием такового:

D3DFMT_X8R8G8B8,

D3DFMT_A8R8G8B8,

D3DFMT_A2B10G10R10,

D3DFMT_X1R5G5B5,

D3DFMT_A1R5G5B5,

D3DFMT_R5G6B5

То есть если какой-то формат поддерживается (об этом позже), то дальше останавливаем перебор и используем этот формат. Из анализа исходников открытых движков, могу сказать, что существует тенденция использования формата D3DFMT_X8R8G8B8 для глубины пикселя 32 бит и формата D3DFMT_R5G6B5 для глубины пикселя в 16 бит. Для формата пикселей дисплея (экрана) выбор формата становится проще в силу ранее сделанного замечания относительно форматов с альфа-каналом. Существует только одно правило: если для в качестве формата пикселя дисплея выбран определенный формат, то для заднего буфера нужно выбрать формат с таким же распределением бит по компонентам. Например, если для формата пикселей дисплея был выбран формат D3DFMT_X1R5G5B5, то для формата пикселей заднего буфера нужно выбрать либо формат D3DFMT_X1R5G5B5, либо формат D3DFMT_A1R5G5B5, но никак не формат D3DFMT_R5G6B5.

Проверка возможности использования устройства на видеокарте

Далее можно проверить, сможет ли устройство Direct3D9 работать на базе видеокарты пользователя. Для этого предназначен метод CheckDeviceType() главного интерфейса Direct3D9. Суть состоит в передаче в функцию номера адаптера, типа устройства, формата пикселей дисплея, формата заднего буфера и флага, который помечает будет ли приложение оконным или нет (в этом случае оно будет полноэкранным), и проверке возвращаемого функцией значения на успешность. То есть нужно, например, проверить, будет ли устройство Direct3D9 типа HAL работать на видеокарте, выставленной как основная в настройках Windows, с определенным форматом дисплея в полноэкранном режиме, ведь устройство типа HAL требует аппаратной растеризации. И нужно проверить, сможет ли текущая видеокарта работать в таком режиме. Ведь видеокарта может поддерживать определенный список видеорежимов дисплея для данного формата пикселей, но поддерживает ли она именно аппаратную растеризацию при данном формате пикселей — это вопрос. Также формат заднего буфера может отличаться от формата дисплея наличием альфа-канала. Совместимы ли формат пикселей дисплея и формат пикселей заднего буфера? Чтобы проверить это, вы должны вызвать примерно следующий код:

if(SUCCEEDED(pD3Device->CheckDeviceType(D3DADAPTER_DEFAULT, 
    D3DDEVTYPE_HAL, DisplayFormat, BackBufferFormat, FALSE)))
    return S_OK;

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

Формат буфера глубины

Так же как и для заднего буфера выбирается формат буфера глубины. Здесь имеет смысл следовать такой стратегии. Перебирать форматы в последовательности уменьшения числа бит на буфер трафарета. То есть если определенный формат поддерживается, останавливаем перебор и используем этот формат глубины. Вот последовательность форматов буфера глубины от «лучшего» к «худшему»: D3DFMT_D24S8, D3DFMT_D24X4S4, D3DFMT_D24X8, D3DFMT_D15S1, D3DFMT_D32, D3DFMT_D16. Причем, если у вас в качестве формата заднего буфера выбран формат с глубиной пикселя в 16 бит, то формат буфера глубины следует выбрать из урезанной последовательности D3DFMT_D15S1, D3DFMT_D16.

Выбор видеорежима и проверка поддержки форматов пикселей

Перед тем как создать устройство Direct3D9, необходимо определить видеорежим, в котором будет работать создаваемое устройство. Как это сделать? Для начала нужно определить какие вообще режимы дисплея доступны на видеокарте, для чего следует воспользоваться методами GetAdapterModeCount() и EnumAdapterModes() интерфейса IDirect3D9. Необходимо организовать перебор видеорежимов. Входными данными обычно выступают глубина цвета, ширина и высота заднего буфера (разрешение экрана) и флаг использования полноэкранного режима.

Функция GetAdapterModeCount() позволяет узнать число видеорежимов, поддерживаемых видеокартой, а функция EnumAdapterModes() позволяет получить информацию о конкретном видеорежиме по его индексу. Видеорежимы видеокарты в Direct3D9 индексируются от нуля до их общего числа минус единица.

Как уже было сказано выше в стратегии выбора «лучшего» формата вы по глубине цвета выбираете формат пикселя дисплея. Затем получаете число доступных видеорежимов для данного формата дисплея через функцию GetAdapterModeCount(). После чего перебираете видеорежимы при помощи функции EnumAdapterModes(), сравнивая ширину и высоту видеорежима на видеокарте со значениями, которые задает пользователь. Далее нужно проверить поддержку буфера глубины, для чего используется метод CheckDeviceFormat():

// Пример проверки формата буфера глубины

BOOL IsDepthFormatExisting( D3DFORMAT DepthFormat, D3DFORMAT AdapterFormat ) {
    HRESULT hr = pD3D->CheckDeviceFormat( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
        AdapterFormat, D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, DepthFormat);
    return SUCCEEDED( hr );
}

Иногда выполняется более сложная проверка:

// Более сложная проверка поддержки буфера глубины и трафарета
BOOL IsDepthFormatOk(D3DFORMAT DepthFormat, D3DFORMAT AdapterFormat, 
    D3DFORMAT BackBufferFormat){
    // Verify that the depth format exists
    HRESULT hr = pD3D->CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
        AdapterFormat, D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, DepthFormat);
    if(FAILED(hr)) return FALSE;
    // Verify that the depth format is compatible
    hr = pD3D->CheckDepthStencilMatch(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
        AdapterFormat, BackBufferFormat, DepthFormat);
    return SUCCEEDED(hr);
}

Насколько я смог понять из DX SDK функция CheckDeviceFormat() используется, чтобы проверить можно ли использовать заданный формат пикселя в качестве буфера глубины, текстуры или цели рендера на данной видеокарте и данном устройстве Direct3D9 при выбранном формате дисплея — грубо говоря, «совместимость формата и способа его использования». В нашем случае при помощи функции CheckDeviceFormat() проверяется «совместимость» формата буфера глубины и способа использования этого формата именно для буфера глубины. А функция CheckDepthStencilMatch() производит проверку совместимости формата буфера глубины и формата заднего буфера.

Мультисэмплинг

Чтобы добиться исчезновения эффекта лестницы, можно проверить поддержку так называемого мультисэмплинга. Проверка выполняется посредством метода CheckDeviceMultiSampleType() главного интерфейса Direct3D9 примерно так:

// пример проверки поддержки мультисэмплинга
if( SUCCEEDED(pD3D->CheckDeviceMultiSampleType( AdapterOrdinal, 
    DeviceType, BackBufferFormat, FALSE, D3DMULTISAMPLE_3_SAMPLES, 
    pQualityLevels ) ) &&
    SUCCEEDED(pD3D->CheckDeviceMultiSampleType( AdapterOrdinal, 
    DeviceType, DepthBufferFormat, FALSE, D3DMULTISAMPLE_3_SAMPLES, 
    pQualityLevels ) ) )
return S_OK;

Из SDK ясно только, что 5-й параметр MultiSampleType описывает технику мультисэмплинга. Обычно именно пользователь задает уровень мультисэмплинга в расширенных настройках видео. А выбор пользователя нужно отобразить на значение перечислимого типа D3DMULTISAMPLE_TYPE. Нужно производить проверку поддержки мультисэмплинга как для заднего буфера, так и для буфера глубины. В pQualityLevels нужно передать указатель на беззнаковое длинное целое число, в которое будет записано число уровней качества мультисэмплинга. Более подробно (например о качестве мультисэмплинга я ничего сказать не могу, поскольку не имел опыта работы с ним). В проанализированных движках особо не заморачивались с качеством мультисэмплинга и выставляли его в 0 в поле MultiSampleQuality структуры D3DPRESENT_PARAMETERS.

Цепочка буферов переключения

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

Частота обновления экрана обычно намного ниже (60Гц, 75 Гц и т. д.) по сравнению со скоростью визуализации графики, которую способна обеспечить видеокарта. Допустим, монитор успел обновить верхнюю половину экрана. Если обновить содержимое переднего буфера в этот момент, отрендерив новый кадр, то когда монитор закончит обновление экрана, в нижней половине экрана будет визуализировано содержание нового кадра. В результате в верхней половине будет прорисовано содержание старого кадра, а в нижней половине — нового кадра. Этот эффект носит название тиринга.

Монитор обычно обновляет экран, начиная прорисовку с левого верхнего угла, строчка за строчкой. После обновления последней строки экрана, монитор производит перекалибровку (возврат к левому верхнему углу экрана), которая носит название вертикальной синхронизации.

Чтобы избавиться от тиринга, придумали метод переключения буферов. Дело в том, что отрендерить сцену можно в любую поверхность (поверхность, грубо говоря, это двумерный массив пикселей), а не только в передний буфер. Любая такая поверхность напрямую никогда не отображается на экране, вследствие чего носит название заднего буфера. Использование задних буферов позволяет освобождает от необходимости синхронизации с вертикальной разверткой монитора, так как рендеринг в задний буфер может производиться независимо от обновления экрана в те моменты, когда, например, очередь сообщений окна приложения пуста. После того как визуализация в задний буфер будет закончена, можно поменять местами передний и задний буфер практически мгновенно с помощью обмена значений указателей на поверхности переднего и заднего буферов. Таким образом задний буфер становится передним, а прежний передний буфер — новым задним буфером. Этот эффект получил название флиппинг поверхностей или переключение буферов. Сам процесс переключения буферов называется в Direct3D9 презентацией (метод Present). Для переключения буферов вводится один или более задних буферов.

С устройством Direct3D9 создается и связывается по крайней мере одна так называемая цепочка переключений (swap chain), которая состоит из переднего и всех задних буферов. Число задних буферов в цепочке переключений задается в поле BackBufferCount структуры D3DPRESENT_PARAMETERS. Задание 0 в параметре числа задних буферов равносильно заданию 1 заднего буфера. Наиболее всего применяют следующие способы переключений буферов:

D3DSWAPEFFECT_FLIP — буферы переключаются в круговом порядке последовательно друг за другом. Этот тип сохраняет содержимое заднего буфера, для чего Direct3D9 использует скрытые дополнительные буферы. Обеспечивает гладкий переход между кадрами, когда включена синхронизация с вертикальной разверткой.

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

Поле PresentationInterval структуры D3DPRESENT_PARAMETERS определяет максимальную скорость преключения буферов. Как правило используются значения D3DPRESENT_INTERVAL_ONE и D3DPRESENT_INTERVAL_IMMEDIATE. Значение D3DPRESENT_INTERVAL_ONE означает, что переключение буферов будет происходить один раз во время перекалибровки после вертикальной развертки, то есть с такой же частотой, с какой обновляется экран монитора. Как правило, это отображение выбора пользователем параметра «синхронизировать с вертикальной разверткой» в расширенных настройках видеопараметров. Если задано значение D3DPRESENT_INTERVAL_IMMEDIATE, то видеокарта не будет дожидаться наступления перекалибровки и переключение буферов будет произведено немедленно. Что лучше — пусть выбирает пользователь, для этого и вводятся расширенные настройки в меню. На производительность системы рендеринга влияет много факторов.

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

Частота монитора

если приложение будет работать в оконном режиме, то в поле FullScreen_RefreshRateInHz структуры D3DPRESENT_PARAMETERS необходимо задать 0, если в полноэкранном — значение берется из описания видеорежима, полученного при вызове функции EnumAdapterModes.

Как описано в справке метода CreateDevice если задать значение частоты монитора, отличное от списка поддерживаемых, то оно будет округлено в меньшую сторону к ближайшей поддерживаемой частоте.

Дескриптор окна устройства

Это окно играет роль основного холста, на котором устройство Direct3D9 будет размещать результаты визуализации. Его дескриптор нужно указать в поле hDeviceWindow структуры D3DPRESENT_PARAMETERS. Для приложений, работающих в оконном режиме, можно задать здесь 0, в этом случае в качестве дескриптора окна будет использоваться окно под фокусом из параметров метода CreateDevice.

Экспорт моделей из программ 3D моделирования

Сглаженные и граненные сетки

Для того, чтобы получить корректную 3D модель, необходимо придерживаться определенных правил при подготовке модели к экспорту и конвертированию. Дело в том, что часто достаточно большая доля поверхности 3D модели должна иметь сглаженный криволинейный вид. Такое свойство имеют модели персонажей, транспорта, оружия. Поверхность моделей зданий, мебели, напротив,  состоит из плоских участков. Чтобы получить сглаженную модель, нормаль каждой вершины сглаженного участка сетки должна быть усреднением нормалей полигонов, в состав которых входит эта вершина.

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

smoothed_cube_edges
Рис.1. Одна нормаль на вершину – ребра выглядят сглаженными.

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

sharp_cube_edges
Рис.2. Три нормали на вершину – ребра выглядят жесткими, гранеными.

В данном случае каждая из вершин куба входит в состав трех полигонов.

Конечно, куб – это не лучший пример для демонстрации сглаженной поверхности, но например, боковая часть цилиндра может быть составлена, скажем из нескольких полигонов (6-8). И тогда, чтоб цилиндр выглядел как цилиндр, а не как обрубленное полено, нужно сгладить нормали у вершин боковой поверхности. Раньше в играх можно было наблюдать такие угловатые цилиндры в моделях бочек и колес автомобилей, и понять, что модели не хватает полигонов, можно было только если посмотреть на бочку сверху, а на колесо – сбоку. Так например, необычно выглядели рули автомобилей в игре GTA Vice City, сделанные в виде цилиндров с 5-ю сторонами. Конечно, если вам необходима модель граненого стакана, то сглаживать нормали не нужно.

Продолжение следует…

Порядок подключаемых либ в Code::Blocks

Сегодня я столкнуля с одной странной вещью. Вернее, я уже сталкивался с этим, но как-то подзабыл уже. Суть в следующем. Я обычно использую среду разработки Code::Blocks. Итак у меня была уже созданная мной статическая либа — назовем ее Lib1. Сегодня я создал либу (назовем ее Lib2), которая использует функции из либы Lib1. Сегодня же я попытался собрать небольшое консольное приложение (назовем его Test), которое использует класс из либы Lib2. Явно приложение Test не вызывает ничего из либы Lib1. Но  в проекте Test все равно надо прописать все используемые либы, поскольку при создании статической либы другие используемые ею статические либы не линкуются к ней. Итак я прописал либы так:

Lib1

Lib2

Компоновщик сыпал сообщениями «undefined reference to …», где вместо многоточия были названия функций из либы Lib1. «Что за фак?» — подумал я, ведь либы подключены. По мере течения времени, затрачиваемого на поиск причины этого бага, я начинал произносить фразы, описывающее мое отношение к этой ситуации, в более жесткой форме.

Я попробовал явно вызвать одну из функций либы Lib1 в main(). Надо же! Проект Test собирается. Ладно. Я создаю в либе Lib1 специальную пустую функцию, думая, что в Test нужно вызвать любую функцию из либы Lib1 явно. Но какого … Проект Test опять не собирается. Я перезагрузил комп, думая, что дело в засоре оперативной памяти…

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

Lib2

Lib1

Охренеть! Проект собрался. Да. Это что-то совсем странное. Вообщем, я пока не планирую менять среду разработки — довольно длительное время я вполне эффективно работал в ней и особых багов не возникало. Но решил сообщить об этом в блоге. Да и самому пригодится, думаю. Если позабуду.