Приложение MFC D3D: руководство по Direct3D часть I

ОГЛАВЛЕНИЕ

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

·        Скачать исходные файлы - 58.6 Кб


Введение

 

Освоение Microsoft Direct3D – трудная задача для новичков в нем или в программировании 3D графики вообще. После того, как вы просмотрели примеры SDK, прочитали некоторые из руководств, опробовали некоторые из примеров кода и, наконец, решили, что вы хотите разработать свою программу или игровой движок с его использованием, вы остаетесь с вопросом, откуда начать. С этим я столкнулся около года назад, поэтому я углубился в изучение разбора программы примера применения каркаса. Я прочитал множество материалов, чтобы попытаться понять, что происходит (буквально) за кадром.

Все дело в том, что помощь SDK очень краткая, а форумы предназначены для опытных программистов графики. Более того, речь идет о версии 9 (фактически 10 в настоящее время), что означает, что технология существует уже более десятилетия. Складывается впечатление, что вся библиотека была разработана группой Direct3D для использования лишь ими самими. Но не отчаивайтесь: существует много руководств, и здесь еще одно. Прежде чем начать, рассмотрим управляемый и неуправляемый Direct3D – ради тех, кто все еще задается вопросом, что такое управляемый код. Можете сразу переходить к следующему разделу, чтобы дать делу ход!

Управляемый код против неуправляемого

Кроме различных версий, в настоящее время Direct3D существует в двух разных видах, а именно: управляемый и неуправляемый Direct3D. Неуправляемый Direct3D предназначен для программистов C++, не связанных со средой разработки .NET, напрямую обращающихся к центральному Direct3D API, и использующих служебную библиотеку расширений Direct3D (D3DX). Управляемый Direct3D – уровень абстракции для неуправляемого Direct3D, который преимущественно обеспечивает взаимно однозначное преобразование неуправляемых интерфейсов, структур и перечислений в управляемые классы, структуры и перечисления.

Здесь ваш второй шанс пропустить данный раздел, так как это все, что вам на самом деле нужно знать. Если вы все еще остаетесь здесь, то к вашему сведению, вся модель .NET работает вокруг управляемого кода. Это приводит к вопросу: "Что такое управляемый код?"

Управляемый код – код, выполнением которого управляет общеязыковая среда исполнения системы разработки .NET или CLR. Он относится к договору о сотрудничестве между выполняющимся в родном формате кодом и средой выполнения. Этот договор устанавливает, что в любой момент выполнения среда выполнения может остановиться и извлечь информацию, специфичную для текущего адреса команды центрального процессора, а именно – состояние времени выполнения, такое как содержимое регистров или стековой памяти.

Необходимая информация закодирована в промежуточном языке (IL) и наборе знаков, иначе называемых метаданными, описывающими все точки входа и конструкции (например, методы и свойства) и их характеристики. CLR – наиболее коммерчески успешная версия стандарта инфраструктуры общего языка (CLI), описывающего, как информация будет кодироваться, чтобы компилятор мог генерировать правильное кодирование. Эта система позволяет всем популярным языкам программирования - от COBOL до Camel, наряду с C#, J#, VB .Net, Jscript .Net и C++ от Microsoft – создавать управляемый код в виде файлов формата загружаемого кода (PE), содержащих IL и метаданные.

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

Напротив, неуправляемые исполняемые файлы, по сути, являются кодом исполняемой программы, загруженным в память. ОС знает о счетчике команд, и это все. Безусловно, действует защита вокруг управления памятью и ввода-вывода через порты и т.д., но система на самом деле не знает, что приложение делает. Поэтому она не может дать никаких гарантий относительно того, что произойдет, когда приложение запустится.

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

Обратно к началу

Эта статья содержит заказную версию среды разработки Direct3D 9 SDK, в состав которой входит набор классов оберток, готовящих Direct3D к внедрению в приложение MFC (Фундаментальные классы Майкрософт), и определенное руководство по Direct3D для начинающих. Разумеется, вы должны знать C++ и программирование для Windows. Вы имеете право спросить, почему я решил изобрести велосипед. Во-первых, чтобы я мог понять его, во-вторых, потому что мне нравится добавлять комментарии, так как я сам начинаю понимать его, и, наконец, потому что я предпочитаю правильно структурировать код.

Еще один отказ и сведения об авторах: некоторые куски текста непосредственно скопированы из помощи SDK. Иногда вам не нужно быть кратким, как в следующем абзаце:

Чтобы использовать Direct3D, сначала вы создаете окно приложения, а затем создаете и инициализируете объекты Direct3D. Вы используете интерфейсы компонентной модели объектов (COM), которые эти объекты реализуют, чтобы управлять ими и создавать другие объекты, требуемые для отображения сцены. Приложения, написанные на C++, напрямую обращаются к этим интерфейсам и объектам. Нам нужно окно для отображения сцены и некоторое знание COM, но незначительный его объем. Окно может быть самым верхним окном для полноэкранного приложения, что является распространенным вариантом для игры. Это может быть элемент управления в диалоговом окне, для которого может быть получен дескриптор окна. Это имеет место для, скажем, редактора уровня, в котором вам нужна отрисовка геометрии уровня и множества элементов управления (то есть инструментов редактора). Приложенный демонстрационный проект предусматривает второй вариант, но за счет небольших дополнительных усилий его можно расширить до переключения режимов.

Для целей данной статьи приложенный пример – это приложение на основе форм архитектуры документ/вид MFC SDI (интерфейс одного документа), созданное с помощью мастера приложений Visual Studio C++ 6.0. Вы также можете создать приложение MDI (интерфейс множества документов) и использовать заголовки Direct3D и файлы CPP. Пример строит изображение в клиентской области элемента управления, принадлежащего виду. Первая интересная вещь, которую моя программа MFC делает как приложение Direct3D – порождает класс, наследуемый от классов CWnd и CXD3D:

class CD3DWnd : public CXD3D, public CWnd

Можно сказать, что CD3DWnd имеет мать и отца.


Класс CXD3D

CD3DWnd – базовый класс для элемента управления в форме, например, графическое окно. Элемент управления будет функционировать как обычный CWnd и предоставлять его свойства CXD3D с помощью дескриптора окна визуализации, вызывать несколько функций, чтобы запустить Direct3D, и переопределить несколько методов, чтобы отобразить сцену.

Рассмотрим класс CXD3D:

//---------------------------------------------------------
// класс CXD3D: класс, класс вида будет порожден, чтобы
// предоставить дескриптор окна для построения изображения, и это
// переопределит отрисовку трехмерной сцены.
//---------------------------------------------------------
class CXD3D
{
protected:
    // внутренние переменные состояния

    bool m_bActive; // переключен на паузу, может быть запрошен после
                    // инициализации, чтобы вызвать CreateD3D
 
    bool m_bStartFullscreen; // запрашивается при
                             // ChooseInitialSettings
 
    bool m_bShowCursor;   // в полноэкранном режиме
    bool m_bClipCursor;   // в полноэкранном режиме

    bool m_bWindowed;   // запрашивается при
                        // BuildPresentParamsFromSettings

   
    bool m_bIgnoreSizeChange; // запрашивается при
                              // HandlePossibleSizeChange
   
    bool m_bDeviceLost;            // успех для: существующих
    bool m_bDeviceObjectsInited;   // InitDeviceObjects и
    bool m_bDeviceObjectsRestored; // RestoreDeviceObjects
 
    // внутренние переменные расчета времени

    FLOAT m_fTime;        // абсолютное время выполнения
    FLOAT m_fElapsedTime; // истекшее время
    FLOAT m_fFPS;         // показатель числа кадров в секунду
 
    // статистика

    TCHAR m_strDeviceStats[256]; // описание устройства
    TCHAR m_strFrameStats[16];   // число кадров в секунду
 
    // главные объекты D3D

    HWND m_hWndRender; // окно устройства
    HWND m_hWndFocus;  // активное окно с фокусом ввода

    LPDIRECT3D9           m_pd3d;       // главный объект D3D
    LPDIRECT3DDEVICE9     m_pd3dDevice; // устройство визуализации
    D3DPRESENT_PARAMETERS m_d3dpp;      // существующие параметры
 
    DWORD m_dwCreateFlags; // программная/аппаратная обработка вершин + чистое устройство

    DWORD m_dwWindowStyle; // сохранено для переключателей режимов
 
    RECT m_rcWindow;   // прямоугольники окна и клиента,
    RECT m_rcClient;   // сохранено для переключателей режимов
 
    // объекты настройки

    CXD3DEnum Enumeration;   // адаптеры, режимы, и т.д.
    CXD3DSettings Settings;  // текущие настройки дисплея
...
};

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

Активный флаг запрашивается видом после инициализации, чтобы запустить CXD3D::CreateD3D. Полноэкранный запуск и оконные флаги могут выглядеть несколько противоречивыми. Пока просто примите, что существует много ситуаций, в которых приложению Direct3D может потребоваться переключаться из полноэкранного в оконный режим и обратно. Флаг ограничения курсора (в полноэкранном режиме) задает, запирает ли приложение курсора в однобуферной прорисовке, в случае, если рабочий стол пользовательского ПК заполняет несколько мониторов. Флаг может игнорировать изменения размера, когда установлен в “ложь” (настройка по умолчанию), позволит программе с оконным интерфейсом сбрасывать среду Direct3D, когда пользователь меняет размер окна.

В остальном, последние три флага состояния относятся к устройству, что означает устройство Direct3D. Что такое устройство Direct3D?


Устройства Direct3D

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

Состояние визуализации – то, когда, где, как и какие 3D объекты отображаются. Трансформация и освещение (или TL) производят действия над вершинами объектов, выполняя все надлежащие расчеты 3D математики и цвета, соответственно. Растеризация – процесс превращения трансформированных и освещенных 3D вершин в нечто, что видеокарта ПК (и в итоге экран) может обработать, а именно 2D пиксели.

С точки зрения архитектуры, каждая операция образует отдельный модуль устройства: модуль трансформации, модуль освещения и модуль растеризации. Первые два модуля производят действия над вершинами, и поэтому они также называются обработка вершин, или VP для краткости. Вершина - это положение точки в пространстве, обычно описываемое ее декартовыми координатами x, y и z. Конвейеры программирования трехмерной графики используют вершины в качестве базовых единиц для создания треугольников или других многоугольников, буквально путем соединения точек. Надлежащее количество правильно ориентированных, окрашенных и освещенных треугольных граней создаёт иллюзию гладкости реального трехмерного объекта.

Если вы все еще здесь, вернемся на правильный путь. Устройства Direct3D могут поддерживать аппаратную VP, в зависимости от адаптера и драйвера дисплея. Это означает, что конкретный адаптер может иметь специализированное аппаратное обеспечение, которое может взять необработанные данные вершин и выполнить трансформацию и освещение, освободив Direct3D от таких задач. Проблема в том, что другой адаптер дисплея может не предоставлять такую функцию. Вообще, приложения должны предоставлять внутри одного устройства функциональность аппаратной и программной VP, чтобы использовать преимущества обеих или даже совмещать их. Однако они должны возвращаться обратно к программной VP (которая доступна практически всегда),  если аппаратная VP недоступна.

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

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

Между прочим, определение устройства в SDK заканчивается словом "поверхность", а не "экран" или "окно". Поверхность – определенное обобщение, иногда она может представлять лишь часть экрана или окна. Чтобы быть точным, в контексте Direct3D, поверхность представляет линейную область видеопамяти. На практике, это прямоугольная часть видеопамяти.
Перерыв закончился, переходим к типам устройств.

Типы устройств Direct3D

Приложения, использующие Direct3D, не обращаются к платам видеографики напрямую; они вызывают функции и методы Direct3D. Direct3D, в свою очередь, обращается к аппаратному обеспечению через абстрактный аппаратный слой (HAL). HAL - аппаратно-зависимый, специфичный для производителя интерфейс (встроенный в драйвер и поставляемый в виде DLL), который Direct3D использует для работы непосредственно с аппаратным обеспечением дисплея, ограждая приложения от специфичных для платы деталей реализации. Майкрософт  подтверждает, что если компьютер, на котором работает ваше приложение, поддерживает устройство типа HAL, оно достигнет наилучшей производительности путем его использования. Однако есть другие типы создаваемых устройств Direct3D.

Второй тип устройства, которое вы можете создать, - образцовое устройство, использующее специальные команды центрального процессора всегда, когда оно может. Виды специальных команд сильно зависят от аппаратного обеспечения. Они включают набор команд 3DNow! на некоторых процессорах AMD (американские микроустройства), набор команд MMX (расширения для мультимедиа), поддерживаемый многими процессорами Intel,  и набор команд SSE – потоковые SIMD (с одним потоком команд и многими потоками данных) расширения на некоторых процессорах Intel. Любой тип устройства Direct3D может использовать эти наборы команд для ускорения VP, но только образцовому устройству разрешено использовать эти наборы команд для растеризации всегда, когда оно может.

Приложения не могут полагаться на образцовые устройства (иначе называемые средства программной прорисовки),  они не могут работать на каждой машине пользователя из-за явных аппаратных несоответствий. Вот почему Майкрософт рекомендует использовать образцовые устройства только для тестирования функций или демонстрации. Фактически, часто бывает так, что Direct3D успешно создает образцовое устройство, но устройство вообще не способно строить изображение! Чтобы создать рабочее образцовое устройство, вам нужна новейшая графическая плата.

Третий тип устройства – программное устройство. Если ПК пользователя не обеспечивает никакого специального аппаратного ускорения для растеризации, ваше приложение может использовать программное устройство для его эмуляции. Оно также будет использовать специальные наборы команд при наличии возможности, но оно однозначно будет работать медленнее, чем устройство HAL. Программные устройства не общедоступны. Более того, они предназначены для использования разработчиками драйверов, опирающихся на средство разработки драйверов Direct3D (DDK). Даже не тратьте силы на их поиск, так как даже Майкрософт не может их предложить. Кроме того, они должны быть загружены приложением и зарегистрированы в качестве подключаемого модуля к интерфейсу Direct3D. В статье они более не упоминаются.

Наконец,  в довершение всего, хотя не признанно как тип, существует разновидность создаваемых устройств Direct3D, именуемых чистыми устройствами. Чистое устройство может повысить производительность, но требует аппаратной VP, не поддерживает запрос некоторых состояний Direct3D и не фильтрует никакие лишние изменения состояний. Это значит, что если вы имеете, скажем, 1000 изменений состояния визуализации на каждый кадр, для вас было бы лучше, если бы фильтрация избыточности осуществлялась автоматически нечистым устройством. Что такое изменения состояния визуализации? Все, что движется от одного кадра к следующему, но, что хуже, анимация не распространяется только на геометрию. Вы можете анимировать путем изменения цветов, текстур, визуальных эффектов.

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

Следует надеяться, что ваш компьютер поддерживает тип устройства HAL, инкапсулирующий характеристики аппаратуры,  который в большинстве случаев является лучшим вариантом. То есть, пока вы не убедитесь, что ваше приложение работает безотказно и в 3 раза быстрее на чистом аппаратном устройстве обработки вершин, или пока вы не станете серьезным конкурентом Id Software! Да, создатели Doom.

Вернемся к классу CXD3D...

Любопытные вещи начинают происходить в CXD3D::CreateD3D.

//----------------------------------------------------------
// CreateD3D(): если m_hWnd был инициализирован, он
// создает экземпляр объекта d3d, выбирает начальные установки d3d
// и инициализирует элементы d3d.
//----------------------------------------------------------
HRESULT CXD3D::CreateD3D()
{
    HRESULT hr;

    // проверяет на наличие окна, в котором формируется изображение
    if (m_hWndRender == NULL)
        return DisplayErrorMsg(D3DAPPERR_NOWINDOW, MSGERR_CANNOTCONTINUE);
 
    // создает экземпляр объекта D3D
    if ((m_pd3d = Direct3DCreate9(D3D_SDK_VERSION)) == NULL)
        return DisplayErrorMsg(D3DAPPERR_NODIRECT3D, MSGERR_CANNOTCONTINUE);

    // создает список адаптеров, режимов и устройств D3D
    if (FAILED(hr = Enumeration.Enumerate(m_pd3d)))
    {
        SAFE_RELEASE(m_pd3d);
       
        return DisplayErrorMsg(hr, MSGERR_CANNOTCONTINUE);
    }

    // использует окно устройства в качестве активного окна, если не задано иное
    if (m_hWndFocus == NULL)
        m_hWndFocus = m_hWndRender;

    // сохраняет несколько свойств окна в членах класса
    m_dwWindowStyle = GetWindowLong(m_hWndRender, GWL_STYLE);
   
    GetWindowRect(m_hWndRender, &m_rcWindow);
    GetClientRect(m_hWndRender, &m_rcClient);

    // выбирает лучшие настройки для формирования изображения
    if (FAILED(hr = ChooseInitialSettings()))
    {
        SAFE_RELEASE(m_pd3d);
   
        return DisplayErrorMsg(hr, MSGERR_CANNOTCONTINUE);
    }

    // инициализирует таймер
    DXUtil_Timer(TIMER_START);

    // инициализирует пользовательские элементы приложения (перед созданием устройства)
    if (FAILED(hr = OneTimeSceneInit()))
    {
        SAFE_RELEASE(m_pd3d);
       
        return DisplayErrorMsg(hr, MSGERR_CANNOTCONTINUE);
    }

    // инициализирует трехмерную среду, создавая устройство
    if (FAILED(hr = InitializeEnvironment()))
    {
        SAFE_RELEASE(m_pd3d);
       
        return DisplayErrorMsg(hr, MSGERR_CANNOTCONTINUE);
    }

    // D3D готово к работе, поэтому активизируем его
    Pause(false);
    return S_OK;
}

Первая вещь, которую CXD3D::CreateD3D проверяет, - ненулевой член m_hWndRender. Затем он создаст экземпляр объекта D3D с Direct3DCreate9(D3D_SDK_VERSION). При успехе инициализируется член-указатель объекта D3D на интерфейс COM IDirect3D9 , поэтому мы можем продолжать. Затем нужно создать список адаптеров дисплея, режимов и устройств, используя объект внутренних настроек «перечисление» CXD3D, объект класса CXD3DEnum.

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


Класс перечисления

Давайте взглянем на класс перечисления: он устанавливает ограничения приложения на разрешение, цвет, альфа, форматы отображения, форматы буфера невидимых поверхностей, форматы буфера глубины/трафарета, типы множественной выборки, интервалы воспроизведения, использование буфера глубины и использование смешанной (аппаратной и программной) VP. Слишком страшно? Не беспокойтесь: я тщательно разберу значение каждого из них в умеренном темпе. Сейчас вы должны понять, что класс перечисления хранит требования приложения, относящиеся к устройству D3D.

//----------------------------------------------------------
// класс CXD3DEnum: перечисляет адаптеры, устройства D3D и т.д.
//----------------------------------------------------------
class CXD3DEnum
{
...
    // ограничения приложения

    bool AppUsesMixedVP;     // может ли приложение использовать
                             // смешанный тип обработки вершин
  
    UINT AppMinFullscreenWidth;   // минимальная ширина приложения в полноэкранном режиме
    UINT AppMinFullscreenHeight;  // минимальная высота приложения в полноэкранном режиме

UINT AppMinRGBBits;      // минимальное количество красных, жёлтых, зеленых битов на /
/каждый канал
    UINT AppMinAlphaBits;    // минимальное количество альфа битов на каждый пиксель
    
    bool AppUsesDepthBuffer; // использует ли приложение буфер глубины
    
    UINT AppMinDepthBits;   // минимальное количество битов глубины
    UINT AppMinStencilBits; // минимальное количество битов трафарета

    // списки допустимых ограничений приложения

    DWORDARRAY AppDisplayFormats;
    DWORDARRAY AppBackBufferFormats;
    DWORDARRAY AppDepthStencilFormats;
    DWORDARRAY AppMultiSamplingTypes;
   
    // список перечисленных AdapterInfos
    AdapterInfoArray AdapterInfos;
...
};

Помните, что главное в настройке и обеспечении готовности к работе Direct3D - перечислять, перечислять и снова перечислять. Мы начинаем с набора минимальных требований, чтобы все, что ниже их, отфильтровывалось. Это реализуется с помощью серии ограничений приложения, набора переменных с префиксом "App" в CXD3DEnum.

Первое ограничение, AppUsesMixedVP, позволяет среде разработки включать или выключать использование смешанной VP, хотя не во время выполнения. Смешанная VP упоминалась при рассмотрении устройств, поэтому вернитесь назад, если вы пропустили ее. Полноэкранные ширина и высота в пикселях,  иначе называемые “разрешением режима дисплея”, ограничены. По умолчанию, минимум установлен в 640x480. Глубина бита цвета или количество битов, используемых для каждого цветового канала, и глубина альфа бита - т.е. уровень непрозрачности или прозрачности – также ограничены AppMinRGBBits и AppMinAlphaBits, соответственно. Каждое из остальных ограничений приложения заслуживает заголовка, а значит, они будут рассмотрены подробно.

Форматы отображения

Здесь есть нечто интересное. Количество битов для цвета и альфа, поддерживаемые конкретным устройством, образуют формат отображения. Класс перечисления хранит список форматов отображения в AppDisplayFormats, который по умолчанию содержит все возможные форматы, которые Direct3D может обработать:

D3DFMT_R5G6B5         // 16-битовый, 6 для зеленого
D3DFMT_X1R5G5B5    // 16-битовый, 5 на каждый канал
D3DFMT_A1R5G5B5    // 16-битовый, 1 для альфа
D3DFMT_X8R8G8B8    // 32-битовый, 8 на каждый канал
D3DFMT_A8R8G8B8    // 32-битовый, 8 для альфа
D3DFMT_A2R10G10B10 // 32-битовый, 2 для альфа

Как бесполезное упражнение, вы можете попытаться выяснить, сколько цветов каждый формат может породить. Заметьте, что добавленные в список форматы должны быть согласованы с AppMinRGBBits и AppMinAlphaBits, т.е. если ваше приложение требует 8-битового альфа, не стоит добавлять форматы меньше 8-битового альфа в список.

Замечание по спискам и перечислениям Direct3D

Форматы Direct3D определены в огромном перечислении в d3d9types.h по имени D3DFORMAT, принудительно приведенном к 32-битовому (DWORD) размеру, как и большинство перечислений Direct3D. DWORDARRAY – основанный на шаблоне массив значений DWORD, typedef для CTArray<DWORD>. Класс шаблона, CTArray, реализованный в tarray.h, работает как стандартный массив объектов любого типа - т.е. вы можете использовать оператор [] – но он инкапсулирует другие функции обработки массива, такие как Append(добавить в конец), Find(найти) и Sort(сортировать), и они убирают за собой. Класс CTArray заменяет стандартное использование средой разработки SDK класса CArrayList, включенный в dxutil.h/dxutil.cpp. Классы на основе шаблонов из библиотеки STL std::list или std::vector не применяются.

Буферы невидимых поверхностей

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

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

Поэтому буфер невидимых поверхностей является невидимой поверхностью (т.е. блоком памяти),  на которой могут быть нарисованы растровые и другие изображения. Это противоположно видимому кадровому буферу, отображающему видимое в текущий момент изображение. Цепочка свопинга – набор буферов, которые переставляются по очереди, чтобы создать анимацию с плавными переходами между кадрами. Буферы невидимых поверхностей также имеют формат цвета/альфа, и, по умолчанию, список перечисления хранит все возможные форматы, которые Direct3D определяет в AppBackBufferFormats, который продолжает такой же список форматов отображения.

Для приложений с оконным интерфейсом формат буфера невидимых поверхностей не обязан совпадать с форматом режима отображения, если аппаратное обеспечение поддерживает преобразование цветов, например, превращение X8R8G8B8 в R5G6B5. В этом случае среда выполнения позволит воспроизводить любой допустимый формат буфера невидимых поверхностей на любом формате рабочего стола. Исключение сделано для режимов 8 битов на пиксель (256 цветов), так как устройства, как правило, больше не работают в таких режимах, хотя 10 с лишним лет назад они применялись.

Полноэкранные приложения не могут выполнять преобразование цветов. Поэтому формат буфера невидимых поверхностей должен по всем параметрам совпадать с форматом отображения, за исключением битов альфа-канала. Причина состоит в том, что форматы отображения полноэкранных приложений не могут содержать альфа-канал, а буферы невидимых поверхностей могут. Поэтому если формат отображения - D3DFMT_X1R5G5B5, то допустимые форматы буфера невидимых поверхностей включают D3DFMT_X1R5G5B5 и D3DFMT_A1R5G5B5, но исключают D3DFMT_R5G6B5 (обратите внимание на 6-битовый зеленый). В целом, для приложений лучше отсутствие преобразования цветов и совпадение форматов буфера невидимых поверхностей и отображения.

Кстати, последний формат – 10 битов на канал – доступен, как формат отображения,  только в полноэкранных  режимах и на быстрых ПК с новейшими графическими платами. Можно мечтать, что через пару лет ПК, адаптеры дисплея и Direct3D все будут поддерживать 32-бит на канал в качестве формата отображения. Это будет нечто, выходящее за пределы истинной цветопередачи! Назовем это сверхистинной цветопередачей! Вернемся к реальности, хотя, вероятно, художники ILM и Dreamworks уже используют эти форматы ежедневно. Хватит говорить об этом.

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

Буфер глубины, часто называемый z-буфером, хранит данные о глубине (значения координаты z), используемые для определения того, как 3D объекты перекрывают друг друга. Обычно реализуемые аппаратным обеспечением, z-буферы решают проблему определения, какие элементы в сцене рисуются перед другими, чтобы они могли скрыть те, которые находятся позади них. Это экономит немалое время выполнения и объем памяти. Существуют также w-буферы, использующие однородные w-координаты из местоположения точки (x,y,z,w) в пространстве проекции. Однако они не поддерживаются столь широко в аппаратном обеспечении, как z-буферы, и вам придется совершенствовать свою алгебру матриц, чтобы использовать их.

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

D3DFMT_D16     // 16-битовый z-буфер
D3DFMT_D15S1   // 16-битовый z-буфер, 1-битовый трафарет
D3DFMT_D24X8   // 32-битовый z-буфер, 24-битовая глубина
D3DFMT_D24S8   // 32-битовый z-буфер, 8-битовый трафарет
D3DFMT_D24X4S4 // 32-битовый z-буфер, 4-битовый трафарет
D3DFMT_D32     // 32-битовый z-буфер

Таким образом, комбинация формата глубины/трафарета и единственная функция перечисления. Список форматов глубины/трафарета класса перечисления, AppDepthStencilFormats, разрешает каждый из этих форматов по умолчанию. Как и в случае форматов отображения и буфера невидимых поверхностей, форматы глубины/трафарета должны соответствовать ограничениям AppMinDepthBits и AppMinStencilBits. Кроме того, можно вообще отключить использование буфера глубины путем установки AppUsesDepthBuffer в false(ложь).

Множественная выборка

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

Типы множественной выборки Direct3D непосредственно показывают количество выборок, доступное для сглаживания всей сцены. Исключение - D3DMULTISAMPLE_NONMASKABLE (со значением 1), который скорее активирует уровень качества множественной выборки. Уровень качества, введенный в версии Direct3D 9.0, может использоваться для разложения на множители количества выборок и достижения определенного отношения визуального качества-к-производительности. Допустим, вы находите поддержку сглаживания для 6 выборок с 4 уровнями качества. Вы можете использовать и сравнить отношения 6/1, 6/2 и 6/3 для воспроизведения. Это означает, что итоговая глубина цвета (каждого канала RGB) будет разложена на столько множителей, и вы сможете определить, который из них обеспечивает наилучшее соотношение производительности и качества.

Класс перечисления добавляет все типы множественной выборки в соответствующий список, AppMultiSamplingTypes, по умолчанию.


Структуры перечисления

Теперь, когда перечисление имеет набор ограничений приложения, разберемся в структурах и методах его реализации. Чтобы хранить информацию об адаптерах, устройствах для каждого адаптера, настройках и возможностях каждого устройства, класс перечисления реализует серию иерархических структур. Посмотрите снова на объявление класса CXD3DEnum. Последний член класса – массив структур AdapterInfo. Здесь приведено объявление AdapterInfo:

//----------------------------------------------------------
// AdapterInfo: информация об адаптере дисплея, и typedef
// для их массива
//----------------------------------------------------------
struct AdapterInfo
{
     int                    AdapterOrdinal;
     D3DADAPTER_IDENTIFIER9 AdapterIdentifier;
     D3DDISPLAYMODEARRAY    DisplayModes;
     DeviceInfoArray        DeviceInfos;
};
typedef CTArray<AdapterInfo> AdapterInfoArray;

AdapterInfo хранит порядковый номер адаптера (0 для основного или стандартного адаптера дисплея), структура D3DADAPTER_IDENTIFIER9 , массив DisplayModes адаптера и массив DeviceInfos. Прямо над объявлением вы найдете оператор typedef CTArray<D3DDISPLAYMODE>D3DDISPLAYMODEARRAY;. По сети, AdapterInfo хранит информацию об адаптере, режимах отображения, которые он может обработать, и списке устройств, которые он может предоставить, инкапсулированную последним массивом.

//----------------------------------------------------------
// DeviceInfo: информация об устройстве D3D; используются массивы их,
// поэтому typedef
//----------------------------------------------------------
struct DeviceInfo
{
    int              AdapterOrdinal;
    D3DDEVTYPE       DevType;
    D3DCAPS9         Caps;  
    DeviceComboArray DeviceCombos;
};
typedef CTArray<DeviceInfo> DeviceInfoArray;

Структура DeviceInfo наследует порядковый номер адаптера от своего родителя, AdapterInfo. Она хранит тип устройства - HAL, образцовое или программное – как определено перечислением D3DDEVTYPE, а также структурой возможностей устройства D3DCAPS9 и массивом DeviceCombos.

//----------------------------------------------------------
// класс DeviceCombo: комбинация формата адаптера и
// формата буфера невидимых поверхностей, совместимых с конкретным
// устройством D3D и приложением. Также будут использоваться их массивы,
// поэтому typedef.
//----------------------------------------------------------
struct DeviceCombo
{
    int        AdapterOrdinal;
    D3DDEVTYPE DevType;  
    D3DFORMAT  DisplayFormat;
    D3DFORMAT  BackBufferFormat;  
    bool       Windowed;
    DWORDARRAY VPTypes;
    DWORDARRAY DSFormats;
    DWORDARRAY MSTypes;
    DWORDARRAY MSQualityLevels;
    DSMSConflictArray DSMSConflicts;
    DWORDARRAY PresentIntervals;
};
typedef CTArray<DeviceCombo> DeviceComboArray;

Структура DeviceCombo наследует порядковый номер адаптера и тип устройства от своего родителя DeviceInfo. Она также хранит формат отображения и формат буфера невидимых поверхностей. Комбинация обоих форматов дает структуре ее имя. Кроме того, она хранит флаг оконный/полноэкранный и списки DWORD (двойное слово) типов VP, форматы глубины/трафарета, типы множественной выборки и уровни качества множественной выборки. Она также следит за тем, какие форматы глубины/трафарета несовместимы с какими типами множественной выборки в массиве DSMSConflict и, наконец, список DWORD интервалов воспроизведения. Последние два пункта будут рассмотрены позже.

Пошаговый разбор перечисления

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

Enumeration
|
+-- AdapterInfos[0]
| |
| +-- DisplayModes[0]
| +-- DisplayModes[1]
| ...
| |
| +-- DeviceInfos[0]
| | |
| | +-- DeviceCombos[0]
| | | |
| | | +-- VPTypes
| | | +-- DSFormats
| | | +-- MSTypes
| | | +-- MSQualityLevels
| | | +-- DSMSConflicts
| | | +-- PresentIntervals
| | +-- DeviceCombos[1]
| | ...
| +-- DeviceInfos[1]
| ...
+-- AdapterInfos[1]
...

Перечисление адаптеров

Оно начинается с функции CXD3DEnum::Enumerate, принимающей указатель на интерфейс Direct3D. Она будет хранить локальную ссылку на него в своем объекте m_pd3d, использовать его для получения количества адаптеров и обходить адаптеры, чтобы сохранить идентификаторы в массиве AdapterInfos. После того, как функция сохранит идентификатор адаптера, она использует методы Direct3D GetAdapterModeCount и EnumAdapterModes, чтобы извлечь и сохранить режимы отображения. Оба метода принимают формат отображения – улучшение версии Direct3D 9.0, – поэтому им передаются каждый из наших определенных для приложения форматов отображения. Перечисленные режимы возвращаются в структуре Direct3D D3DDISPLAYMODE:

typedef struct _D3DDISPLAYMODE 
{
    UINT Width;
    UINT Height;
    UINT RefreshRate;
    D3DFORMAT Format;
}
D3DDISPLAYMODE;

Сразу можно проверить размеры режима отображения, глубину бита цвета и глубину альфа-бита на совместимость с приложением. Снова: получаем идентификатор адаптера, обходим определенные для приложения форматы отображения и перечисляем режимы отображения для каждого формата. Доступный режим отображения, соответствующий требованиям приложения, добавляется в список DisplayModes. Снова взгляните на схему для лучшего понимания. Некоторые режимы отображения могут не попасть в список, так как формат отображения вообще не поддерживается (и GetAdapterModeCount возвращает 0), или так как он не прошел тесты, например, полноэкранный режим 320x200, когда AppMinFullscreenWidth равняется 640. Однако, когда режим попадает в список, формат отображения добавляется во временный список, позже используемый для перечисления устройств в каждом адаптере.

После обхода всех адаптеров и сбора всех возможных режимов отображения, подходящих для приложения, мы сортируем режимы отображения, чтобы наименьший и наибыстрейший всплыли вверху списка. Класс CTArray реализует сортировку с помощью qsort, принимая функцию сортировки обратного вызова. Чтобы отсортировать режимы отображения, перечисление использует функцию SortModesCallback вверху файла CXD3DEnum.cpp. На данном этапе Enumerate идентифицировал адаптер дисплея и перечислил все режимы отображения, которые он может обработать, и которые допустимы для приложения. Функция передает адаптер и временную подгруппу форматов отображения функции EnumerateDevices.

Перечисление устройств

Это процесс заполнения списка DeviceInfo для переданного адаптера. Существует не более трех DeviceInfos для каждого адаптера, а именно – HAL, образцовое и программное устройство. Мы спрашиваем Direct3D о поддержке каждого типа и сохраняем его возможности (если поддерживается) в члене Caps, с единственным вызовом IDirect3D9::GetDeviceCaps. Типы устройств, не пережившие вызов, пропускаются. Те, которые обращаются к их соответствующему перечислению DeviceCombos, снова используют переданный список форматов отображения.

Перечисление DeviceCombo

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

IDirect3D9::CheckDeviceType.

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

Перечисление типов VP

Устройства HAL могут поддерживать три разных типа VP: программный, аппаратный и смешанный, как описано ранее. Чтобы обнаружить поддержку аппаратной VP, среда разработки проверяет Caps.DevCaps на наличие флага D3DDEVCAPS_HWTRANSFORMANDLIGHT. Если флаг установлен, устройство поддерживает ее и, следовательно, поддерживает смешанный тип VP, хотя последнее будет использоваться, только если флаг AppUsesMixedVP установлен. Функция перечисления типов VP также проверяет возможности на наличие типа чистого устройства, устанавливая тип VP в чистую аппаратную VP, чтобы среда разработки могла воспользоваться ее преимуществами и создать чистое устройство. Если аппаратные трансформации и освещение не поддерживаются, среда разработки возвращается назад к программной VP, которая всегда доступна.


Замечание о возможностях устройства

Среда разработки проверяет возможности устройства на наличие поддержки аппаратной VP, интервалов воспроизведения или нулевых образцовых устройств. Однако вашему приложению может требоваться проверка на наличие других характеристик для работы. Допустим, ваше приложение требует аппаратной поддержки объемных текстур, для чего бы они ни были нужны, поэтому в какой-то момент ему придется проверить член Caps.TextureCaps на наличие флага D3DPTEXTURECAPS_VOLUMEMAP. Если флаг не установлен, аппаратное обеспечение не поддерживает их, и ваше приложение должно достойно завершить работу и сообщить пользователю, чтобы он установил новый адаптер дисплея.

Перечисление множественной выборки

Еще одно ограничение приложения – допустимые типы множественной выборки, по умолчанию заполненное всеми возможными типами в конструкторе CXD3DEnum. Эта функция перечисления спрашивает Direct3D о поддержке каждого типа на поверхности однобуферной прорисовки (буфер невидимых поверхностей), отфильтровывая неподдерживаемые типы с помощью вызова Direct3D CheckDeviceMultiSampleType. Вызов также возвращает, при успехе, количество уровней качества для типа. Оба значения – тип и уровни качества – попадают в соответствующие им списки. Разные типы множественной выборки могут иметь разные уровни качества, поэтому обязательно обращайтесь к обоим спискам в синхронном режиме при передаче значений другим вызовам и структурам API.

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

Эта функция обходит допустимые для приложения форматы глубины/трафарета и проверяет каждый формат по отношению к соответствующим значениям "AppMin", на всякий случай. Затем она задает Direct3D два вопроса о формате: может ли он использоваться в устройстве, и совместим ли он с форматами отображения и буфера невидимых поверхностей устройства. Если и только если ответ на оба вопроса ”да”, то формат буфера глубины/трафарета попадает в текущий список DSFormats в DeviceCombo. Обычно мы получаем подгруппы определенных для приложения форматов глубины/трафарета.

Перечисление конфликтов глубины/трафарета и множественной выборки

Если ваше приложение будет использовать поверхности глубины/трафарета, они должны допускать множественную выборку. Кроме того, если такие поверхности будут использоваться вместе с любой поверхностью однобуферной прорисовки (буфер невидимых поверхностей), обе поверхности требуют один и тот же тип множественной выборки. Поэтому нужно проверять каждый формат глубины/трафарета на совместимость по отношению к каждому типу множественной выборки, попавшему в текущий DeviceCombo.  Это делается с помощью вызова API устройства, который при неудаче регистрирует конфликт в соответствующем списке. Цель всего этого – не дать запрещенной комбинации попасть в настройки отображения.

Перечисление интервалов воспроизведения

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

Кадровая синхронизация – количество обновления экрана в секунду, обычно около 60 раз для большинства CRT и TV. Задумывались ли вы когда-либо, как работает ваш монитор CRT (электронно-лучевая трубка)? Электронный луч отклоняется нагретым катодом, чтобы луч ударялся о покрытую люминофором вакуумную трубку в непрерывной развертке слева направо и сверху вниз. Когда луч доходит до правого нижнего угла, он движется обратно кверху вдоль диагонали и повторяет развертку. Количество выполненных разверток в секунду равняется пресловутым 60 Гц –  кадровой синхронизации. Число горизонтальных строк в развертке отличает стандартный NTSC TV (525 строк) от HDTV (1125 строк). Не спрашивайте о плоских экранах или ЖК-дисплеях, так как они работают по-другому, но тоже имеют частоту обновления. Это все, что касается ускоренного курса видеоэлектроники. Завершим его.

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

Мгновенный интервал воспроизведения всегда доступен, но его стоит проверить, чтобы предотвратить взрыв некоторых старых плат. Просто шутка! В настоящее время большинство ПК должны быть в состоянии справиться с ней. Ваше приложение может получить прирост в 10 кадров в секунду за счет него без помех отображения или, при наличии помех, получить такую же частоту кадров, как и при интервале по умолчанию. Интервал воспроизведения по умолчанию, эквивалентный интервалу воспроизведения один, всегда доступен. Перечисление поместит его  вверху списка, на тот случай, если приложение работает в оконном режиме. В данном случае это помогает уменьшить мерцание мыши по сравнению с мгновенным интервалом. Поддержка интервалов 2, 3 и 4 зависит от аппаратного обеспечения, поэтому мы проверяем возможности устройства для них.

И это все для объекта перечисления! Мы имеем полный набор адаптеров, режимов отображения и возможностей устройства, соответствующих требованиям приложения, поэтому мы можем выбирать среди них, чтобы отобразить нашу трехмерную сцену. Этого хватит для части I. Мы на полпути к установке Direct3D, но в первой части слишком много материала. Надеюсь, что это придаст новичкам больше храбрости, чтобы  прочитать ее, и что это всколыхнет существующее положение старых мастеров D3D, вызвав исправления и случайные угрозы убийством.