1.10 Добавляем рендеринг Ч.2
Содержание
- 1.10 Добавляем рендеринг Ч.2
- Создание объекта устройства Direct3D
- Уничтожение объекта устройства Direct3D
- Добавляем поддержку рендеринга в функцию Run
- Добавляем поддержку рендеринга в функцию ChangeState
- Заполняем структуру Viewer Setup Structure (структура установки вирт. просмотрщика)
- Тестовая перекомпиляция Engine.lib
- Итоги главы
- Источники
Создание объекта устройства Direct3D
Мы уже достаточно обсудили кучу разных штук, необходимых для создания объекта устройства, для чего сперва необходимо подготовить к работе основные интерфейсы DirectX. Для этого добавим ещё немного кода, отвечающего за инициализацию DirectX.1Обрати внимание
В нашем случае это код именно для DirectX 9 (редакция C). В других версиях DirectX строки инициализации могут существенно различаться. В то же время, у нас всегда остаётся возможность добавить в будущем в наш движок поддержку других версий DirectX (например, 8, 10 или 11).
Программный объект устройства Direct3D создаётся в конструкторе класса Engine (Engine.cpp), сразу после завершения энумерации адаптера дисплея и создания главного окна приложения.
Изменения в Engine.cpp (Проект Engine)
- Добавь следующие строки в конструктор класса Engine, сразу после завершения энумерации адаптера дисплея и создания главного окна приложения:
// Понеслись всякие DirectX-штуки. // Готовим параметры представления (present parameters) объекта устройства. D3DPRESENT_PARAMETERS d3dpp; ZeroMemory( &d3dpp, sizeof( D3DPRESENT_PARAMETERS ) ); d3dpp.BackBufferWidth = g_deviceEnumeration->GetSelectedDisplayMode()->Width; d3dpp.BackBufferHeight = g_deviceEnumeration->GetSelectedDisplayMode()->Height; d3dpp.BackBufferFormat = g_deviceEnumeration->GetSelectedDisplayMode()->Format; d3dpp.BackBufferCount = m_setup->totalBackBuffers; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = m_window; d3dpp.Windowed = g_deviceEnumeration->IsWindowed(); d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D16; d3dpp.FullScreen_RefreshRateInHz = g_deviceEnumeration->GetSelectedDisplayMode()->RefreshRate; if( g_deviceEnumeration->IsVSynced() == true ) d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; else d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // Уничтожаем объект энумерации устройства. SAFE_DELETE( g_deviceEnumeration ); // Создаём устройство Direct3D на основе данных из заполненной выше структуры d3dpp. if( FAILED( d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_window, D3DCREATE_MIXED_VERTEXPROCESSING, &d3dpp, &m_device ) ) ) return; // Освобождаем интерфейс Direct3D, т.к. он больше не нужен. SAFE_RELEASE( d3d ); // Отключаем освещение по умолчанию. m_device->SetRenderState( D3DRS_LIGHTING, false ); // Устанавливаем фильры текстур для использования анизотропной фильтрации. m_device->SetSamplerState ( 0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState ( 0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR ); // Устанавливаем матрицу проекции. D3DXMATRIX projMatrix; D3DXMatrixPerspectiveFovLH( &projMatrix, D3DX_PI / 4, (float)d3dpp.BackBufferWidth / (float)d3dpp.BackBufferHeight, 0.1f / m_setup->scale, 1000.0f / m_setup->scale ); m_device->SetTransform( D3DTS_PROJECTION, &projMatrix ); // Сохранаяем подробные настройки видеорежима. m_displayMode.Width = d3dpp.BackBufferWidth; m_displayMode.Height = d3dpp.BackBufferHeight; m_displayMode.RefreshRate = d3dpp.FullScreen_RefreshRateInHz; m_displayMode.Format = d3dpp.BackBufferFormat; // Своп-цепочка всегда начинается с первого бэкбуфера. m_currentBackBuffer = 0; // Создаём интерфейс спрайта. D3DXCreateSprite( m_device, &m_sprite );
После этого реализация конструктора класса Engine примет следующий вид:
... //----------------------------------------------------------------------------- // The engine class constructor. //----------------------------------------------------------------------------- Engine::Engine( EngineSetup *setup ) { // Indicate that the engine is not yet loaded. // Показываем, что движок пока не загружен. m_loaded = false; // If no setup structure was passed in, then create a default one. // Otehrwise, make a copy of the passed in structure. // Если структура EngineSetup не передана, то создаём одну. // Если есть, то делаем её копию. m_setup = new EngineSetup; if( setup != NULL ) memcpy( m_setup, setup, sizeof( EngineSetup ) ); // Store a pointer to the engine in a global variable for easy access. // Сохраняем указатель на экземпляр движка в глобальную переменную. // Для более простого доступа к ней из любой части движка g_engine = this; // Prepare and register the window class. // Заполняем переменные члены оконного класса. WNDCLASSEX wcex; wcex.cbSize = sizeof( WNDCLASSEX ); wcex.style = CS_CLASSDC; wcex.lpfnWndProc = WindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = m_setup->instance; wcex.hIcon = LoadIcon( NULL, IDI_APPLICATION ); wcex.hCursor = LoadCursor( NULL, IDC_ARROW ); wcex.hbrBackground = NULL; wcex.lpszMenuName = NULL; wcex.lpszClassName = "WindowClass"; wcex.hIconSm = LoadIcon( NULL, IDI_APPLICATION ); RegisterClassEx( &wcex ); // Initialise the COM using multithreaded concurrency. // Инициализируем COM в мультизадачном режиме. CoInitializeEx( NULL, COINIT_MULTITHREADED ); // Инициализируем интерфейс Direct3D. IDirect3D9 *d3d = Direct3DCreate9( D3D_SDK_VERSION ); // Энумерируем (опрашиваем) конфигурации устройства Direct3D на адаптере по умолчанию (первичный видеодрайвер). g_deviceEnumeration = new DeviceEnumeration; if( g_deviceEnumeration->Enumerate( d3d ) != IDOK ) { SAFE_RELEASE( d3d ); return; } // Создаём окно и возвращаем его дескриптор. m_window = CreateWindow( "WindowClass", m_setup->name, g_deviceEnumeration->IsWindowed() ? WS_OVERLAPPED : WS_POPUP, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL ); // =============================== // Понеслись всякие DirectX-штуки. // =============================== // Готовим параметры представления (present parameters) объекта устройства. D3DPRESENT_PARAMETERS d3dpp; ZeroMemory( &d3dpp, sizeof( D3DPRESENT_PARAMETERS ) ); d3dpp.BackBufferWidth = g_deviceEnumeration->GetSelectedDisplayMode()->Width; d3dpp.BackBufferHeight = g_deviceEnumeration->GetSelectedDisplayMode()->Height; d3dpp.BackBufferFormat = g_deviceEnumeration->GetSelectedDisplayMode()->Format; d3dpp.BackBufferCount = m_setup->totalBackBuffers; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = m_window; d3dpp.Windowed = g_deviceEnumeration->IsWindowed(); d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D16; d3dpp.FullScreen_RefreshRateInHz = g_deviceEnumeration->GetSelectedDisplayMode()->RefreshRate; if( g_deviceEnumeration->IsVSynced() == true ) d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; else d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // Уничтожаем объект энумерации устройства. SAFE_DELETE( g_deviceEnumeration ); // Создаём устройство Direct3D на основе данных из заполненной выше структуры d3dpp. if( FAILED( d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_window, D3DCREATE_MIXED_VERTEXPROCESSING, &d3dpp, &m_device ) ) ) return; // Освобождаем интерфейс Direct3D, т.к. он больше не нужен. SAFE_RELEASE( d3d ); // Отключаем освещение по умолчанию. m_device->SetRenderState( D3DRS_LIGHTING, false ); // Устанавливаем фильры текстур для использования анизотропной фильтрации. m_device->SetSamplerState ( 0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState ( 0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR ); // Устанавливаем матрицу проекции. D3DXMATRIX projMatrix; D3DXMatrixPerspectiveFovLH( &projMatrix, D3DX_PI / 4, (float)d3dpp.BackBufferWidth / (float)d3dpp.BackBufferHeight, 0.1f / m_setup->scale, 1000.0f / m_setup->scale ); m_device->SetTransform( D3DTS_PROJECTION, &projMatrix ); // Сохранаяем подробные настройки видеорежима. m_displayMode.Width = d3dpp.BackBufferWidth; m_displayMode.Height = d3dpp.BackBufferHeight; m_displayMode.RefreshRate = d3dpp.FullScreen_RefreshRateInHz; m_displayMode.Format = d3dpp.BackBufferFormat; // Своп-цепочка всегда начинается с первого бэкбуфера. m_currentBackBuffer = 0; // Создаём интерфейс спрайта. D3DXCreateSprite( m_device, &m_sprite ); m_states = new LinkedList< State >; m_currentState = NULL; m_scriptManager = new ResourceManager< Script >; // Создаём экземпляр класса Input. m_input = new Input( m_window ); // Seed the random number generator with the current time. // Стартуем генератор сулчайных чисел на основе текущего времени. srand( timeGetTime() ); if( m_setup->StateSetup != NULL ) { m_setup->StateSetup(); } // The engine is fully loaded and ready to go. // Движок полностью загружен и готов к работе. m_loaded = true; } ...
- Сохрани Решение (Файл->Сохранить все).
Исследуем добавленный код
Начинаем с заполнения структуры D3DPRESENT_PARAMETERS, которая используется Direct3D для создания объекта устройства. Структура содержит практически все возможные параметры, необходимые для настройки устройства определённым образом. Вот её определение (прототип):typedef struct D3DPRESENT_PARAMETERS { UINT BackBufferWidth; // Ширина бэкбуфера UINT BackBufferHeight; // Высота бэкбуфера D3DFORMAT BackBufferFormat; // Формат дисплея для поверхности бэкбуфера UINT BackBufferCount; // Общее количество используемых бэкбуфкеров D3DMULTISAMPLE_TYPE MultiSampleType; // Поддержка антиалиасинга (экранного сглаживания) DWORD MultiSampleQuality; // Качество сглаживания D3DSWAPEFFECT SwapEffect; // Эффект перехода при смене бэкбуферов на экране HWND hDeviceWindow; // Дескриптор главного окна приложения BOOL Windowed; // Определяет, выполняется ли приложение в окне или нет. BOOL EnableAutoDepthStencil; // Поддержка буфера трафарета глубины (Depth-stencil buffer) D3DFORMAT AutoDepthStencilFormat; // Формат буфера трафарета глубины DWORD Flags; // Доп. флаги из структуры D3DPRESENTFLAG UINT FullScreen_RefreshRateInHz; // Частота обновления UINT PresentationInterval; // Интервал вывода изображения (для v-sync) } D3DPRESENT_PARAMETERS, *LPD3DPRESENT_PARAMETERS;
Параметров много, но большинство из них успешно работают со значениями по умолчанию. Поэтому перед использованием структуры мы очищаем отведённую для неё память сразу после её инициализации:
... // Готовим параметры представления (present parameters) объекта устройства. D3DPRESENT_PARAMETERS d3dpp; ZeroMemory( &d3dpp, sizeof( D3DPRESENT_PARAMETERS ) ); ...
Сразу после этого начинаем заполнять структуру D3DPRESENT_PARAMETERS.
В первую очередь устанавливаем ширину (width), высоту (height) и формат дисплея (Format) на основе данных, указанных пользователем в меню графических настроек.
Доступ к ним, в свою очередь, осуществляется через функцию GetSelectedDisplayMode из класса DeviceEnumeration. В ходе этого мы обращаемся к соответствующему члену структуры DISPLAYMODE, которая возвращает значение соответствующего параметра.
Устанавливаем общее число бэкбуферов, которые будут использоваться в своп-цепочке:
... d3dpp.BackBufferCount = m_setup->totalBackBuffers; ...
Эти данные берутся из структуры EngineSetup, указатель на которую (*m_setup) передаётся в конструктор.
Следующий параметр указывает устройству, каким именно образом (с каким эффектом) будут сменяться бэкбуферы в своп-цепочке:
... d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; ...
Данный параметр может принимать одно из следующих значений:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
D3DSWAPEFFECT_FLIP | Использует несколько бэкбуферов в зацикленном списке. Путём смещения по кругу циклического списка (своп-цепочки) и быстрого флипа каждый новый кадр напрямую выводится на экран. Рекомендуется для полноэкранных приложений. |
D3DSWAPEFFECT_COPY | Использует для вывода изображения на экран всего один бэкбуфер (даже если в настройках указано несколько бэкбуферов) путём копирования содержимого бэк буфера на экран. Метод копирования гарантирует, что содержимое бэкбуфера не изменится во время вывода на экран. Рекомендуется для оконных приложений. |
D3DSWAPEFFECT_DISCARD | Разрешает драйверу дисплея автоматически выбирать между двумя вышеуказанными режимами. Рекомендуется использовать во всех случаях, так как это гарантирует, что в каждом отдельном случае будет применён наиболее эффективный переход. |
В нашем случае мы будем использовать значение D3DSWAPEFFECT_DISCARD, чтобы позволить видеодрайверу самостоятельно выбирать наилучший эффект перехода.
Присваиваем параметру дескриптора окна приложения (HWND) значение m_window, что соответствует дескриптору нашего главного окна приложения:
... d3dpp.hDeviceWindow = m_window; d3dpp.Windowed = g_deviceEnumeration->IsWindowed(); ...
Также указываем объекту устройства Direct3D, в каком режиме будет работать приложение (оконный/полноэкранный), присвоив параметру d3dpp.Windowed значение нашей заранее заготовленной функции IsWindowed из класса DeviceEnumeration.
Следующий параметр передаёт управление Direct3D т.н. буфером трафарета глубины (depth stencil buffer):
... d3dpp.EnableAutoDepthStencil = true; ...
При значении true вместе с объектом устройства будет также создан буфер трафарета глубины (depth stencil buffer), который автоматически назначается целью рендеринга (render target). В этом случае также обязательно выставляем его формат:
... d3dpp.AutoDepthStencilFormat = D3DFMT_D16; ...
Данный параметр может принимать следующие значения:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
D3DFMT_D16 | 16-битный буфер глубины. |
D3DFMT_D15S1 | 16-битный буфер глубины, 1 бит - буфер трафарета. |
D3DFMT_D24X8 | 24-битный буфер глубины. |
D3DFMT_D24S8 | 24-битный буфер глубины, 8 бит - буфер трафарета. |
D3DFMT_D32 | 32-битный буфер глубины. |
Буфер глубины (depth buffer; часто его называют z-буфером) используется Direct3D для рендеринга геометрии в 3D-пространстве. Допустим, у нас есть 2 грани в 3D-пространстве, одна из которых закрывает другую. В этом случае буфер глубины используется для определения, какая из граней будет рендериться в каждом отдельном пикселе вьюера. Каждый раз, когда ты что-то рендеришь, каждый пиксель изображения проверяется относительно буфера глубины. Это гарантирует, что только ближайшая к вьюеру грань сможет повлиять на цвет пикселей изображения (если, конечно, ты не используешь специальные эффекты вроде прозрачности и альфа-смешивания (alpha blending)). Без буфера глубины у Direct3D не остаётся каких-либо других способов определения, какая из граней расположена спереди (с точки зрения вьюера). Это может привести к тому, что грани будут отрендерены в неверном порядке и скрытые грани станут видимыми.
Обрати внимание
Когда мы начнём рендеринг настоящей сцены с внушительным количеством граней, попробуй отключить буфер глубины, установив параметр EnableAutoDepthStencil в false и посмотри, что будет. Для нашего буфера трафарета глубины мы используем D3DFMT_D16 формат, что означает, что мы не будем использовать буфер трафарета (stencil buffer). Он в основном применяется в специальных приёмах как например трафаретное затенение (stencil shadowing), силуэты (silhouettes), деколи (decals) и т.д. В нашем курсе мы не будем внедрять в движок что-либо подобное (в целях сохранения доступности изложения). Поэтому нам не нужен буфер трафарета.
Устанавливаем частоту обновления (refresh rate) и интервал презентации (presentation interval = v-sync):
... d3dpp.FullScreen_RefreshRateInHz = g_deviceEnumeration->GetSelectedDisplayMode()->RefreshRate; if( g_deviceEnumeration->IsVSynced() == true ) d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; else d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; ...
Оба этих параметра также берутся из данных, которые указал пользователь в окне графических настроек. Что касается интервала презентации, мы просто вызываем вспомогательную функцию IsVSynced (она также расположена в классе DeviceEnumeration). Если функция возвращает true, то присваиваем параметру PresentationInterval значение D3DPRESENT_INTERVAL_DEFAULT, что соответствует включённой опции v-sync. Если возвращает false, то присваиваем параметру PresentationInterval значение D3DPRESENT_INTERVAL_IMMEDIATE, что соответствует выключенной опции v-sync.
Как только мы заполнили структуру D3DPRESENT_PARAMETERS, нам больше не нужен класс DeviceEnumeration. Поэтому уничтожаем его инстанс:
... // Уничтожаем объект энумерации устройства. SAFE_DELETE( g_deviceEnumeration ); ...
Сразу после этого приступаем к созданию объекта устройства:
... // Создаём устройство Direct3D на основе данных из заполненной выше структуры d3dpp. if( FAILED( d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_window, D3DCREATE_MIXED_VERTEXPROCESSING, &d3dpp, &m_device ) ) ) return; ...
Функция CreateDevice экспонирована интерфейсом Direct3D. Вот её прототип:
HRESULT CreateDevice( [in] UINT Adapter, // Адаптер дисплея. // Значение D3DADAPTER_DEFAULT означает первичный видеоадаптер. [in] D3DDEVTYPE DeviceType, // Тип устройства. // Используй значение D3DDEVTYPE_HAL для создания устройства // типа HAL (с аппаратной обработкой рендеринга и спецэффктов) [in] HWND hFocusWindow, // Дескриптор окна приложения [in] DWORD BehaviorFlags, // Опции для контроля создания объекта // устройства и его дальнейшего поведения [in, out] D3DPRESENT_PARAMETERS *pPresentationParameters, // Указатель на заполненную выше // структуру D3DPRESENT_PARAMETERS [out, retval] IDirect3DDevice9 **ppReturnedDeviceInterface // Адрес указателя, куда будет // сохранён созданный объект устройства );
Как всегда, подробные комментарии помогут разобраться в коде.
В Engine.cpp, в функции CreateDevice, мы указали следующие параметры:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
D3DADAPTER_DEFAULT | Указываем, что хотим создать объект устройства на базе первичного видеодрайвера (= видеоадаптер по умолчанию). |
D3DDEVICETYPE_HAL | Создаваемое устройство будет работать в режиме аппаратной обработки рендеринга и спецэффектов. Как вариант, вместо этого можно указать значение D3DDEVICETYPE_REF для использования драйвера программной (=процессорной) обработки графики. Например, для тестирования фич и технологий, которые данный видеоадаптер не поддерживает. |
m_window | Дескриптор окна приложения, в котором будет отображаться 3D-сцена. |
D3DCREATE_MIXED_VERTEXPROCESSING | Флаг (флаги) поведения. См. Таблицу 5. В нашем случае мы используем один флаг, означающий, что устройство может использовать как программную, так и аппаратную обработку вершин. |
_device | Указатель на ранее заполненную структуру D3DPRESENT_PARAMETERS. Это позволяет объекту устройства создать самого себя, основываясь на дополнительных параметрах, многие из которых предварительно указаны пользователем в меню графических настроек. |
m_window | Указатель объекта устройства, по которому в дальнейшем будем обращаться к нему. |
Таблица 1. Возможные значения параметра BehaviorFlags функции CreateDevice
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
3DCREATE_HARDWARE_VERTEXPROCESSING | Данные о вершинах обрабатываются аппаратно адаптером дисплея. Чаще всего это даёт большой прирост производительности. В то же время, возможности по обработке вершин ограничены возможностями конкретной видеокарты. |
3DCREATE_SOFTWARE_VERTEXPROCESSING | Данные о вершинах обрабатываются программными средствами (в основном с помощью процессора) DirectX. Данный режим далеко не всегда даёт хорошее качество картинки. В то же время, DirectX предоставляет фиксированный набор технологий, который гарантированно будет оставаться неизменным на разных компьютерах. |
D3DCREATE_MIXED_VERTEXPROCESSING | Данный режим позволяет объекту устройства производить как аппаратную так и программную обработку вершин. |
Когда мы делаем вызов функции CreateDevice, мы заключаем его в условный оператор if для проверки случая её неудачного выполнения. Здесь мы используем специальный DirectX-макрос FAILED. Неудачное выполнение может произойти по самым разным причинам. Например, из-за неверно указанного параметра.
Обрати внимание
Одной из частых причин неудачного выполнения функции CreateDevice является слишком большое значение общего числа бэкбуферов, указанное в настройках. Также, для того, чтобы минимизировать вероятность неудачи, программист должен проверить, имеется ли на целевом компьютере подходящая видеокарта и что в системе установлена подходящая (либо самая новая) версия DirectX. В случае, если создание объекта устройства завершится неудачей, то конструктор класса Engine досрочно завершит свою работу (выполнит return), так и не дойдя до установки флага m_loaded в true. Это, в свою очередь, предотвратит выполнение функции Run без созданного объекта устройства. Как видим, все компоненты движка предельно логичны и взаимосвязаны. Именно так пишутся профессиональные движки.
Если создание объекта устройства прошло успешно, нам больше не нужен интерфейс Direct3D. Поэтому спокойно удаляем его:
... // Удаляем интерфейс Direct3D, т.к. он больше не нужен. SAFE_RELEASE( d3d ); ...
На этом этапе объект устройства полностью создан и готов к работе. Вместе с тем, выше мы добавили в конструктор класса Engine (Engine.cpp, Проект Engine) ещё несколько строк, которые позволяют нашему движку работать с ним. Продолжим их подробное изучение.
Как только объект устройства был успешно создан, нам необходимо подготовить его к работе, установив некоторые опции по умолчанию:
... // Отключаем освещение по умолчанию. m_device->SetRenderState( D3DRS_LIGHTING, false ); // Устанавливаем фильры текстур для использования анизотропной фильтрации. m_device->SetSamplerState ( 0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState ( 0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR ); ...
Всего 4 опции. Все они изменяются путём вызова специальных функций, экспонированных только что созданным объектом устройства Direct3D. Вот их описание:
ОПЦИЯ | ОПИСАНИЕ |
---|---|
m_device->SetRenderState( D3DRS_LIGHTING, false ) | Освещение. Изначально оно включено (установлено в true). Это означает, что по умолчанию Direct3D будет просчитывать освещённость каждой вершины сцены. Всегда. Нам это не нужно, так как далеко не во всех случаях нам требуется просчёт освещённости вершин. Например, мы можем создать стейт меню, которому совсем не обязательна подобная процедура. Кроме того, на это тратятся вычислительные мощности, которые необходимо расходовать с умом. Тему оптимизации в компьютерных играх никто не отменял. Хорошие игры быстро загружают и показывают свои сцены, а также работают на большинстве современных компьютеров, что немаловажно для широкого охвата целевой аудитории. Поэтому для отключения просчёта освещённости вершин вызываем функцию SetRenderState, экспонированную только что созданным объектом устройства Direct3D. Здесь в первом параметре указано значение D3DRS_LIGHTING, означающее, что мы будем изменять освещение. Во втором параметре выставляем false. Если нам вдруг понадобится включить освещение, достаточно просто ещё раз вызвать данную функцию, указав во втором параметре true. |
m_device->SetSamplerState ( 0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC ); m_device->SetSamplerState ( 0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC ); | Текстурные фильтры магнификации (=увеличения) и минификации. Когда текстура применяется к грани, она часто бывает либо слишком большой, либо слишком сжатой для корректного заполнения поверхности грани (что и называется магнификация и минификация соответственно). Проблема в том, что когда текстура растягивается или сжимается подобным образом, она может заметно потерять своё качество. Например магнификация текстуры может привести к тому, что она станет похожа на кирпичную стену, так как множество экранных пикселей в этом случае будут откартированы (mapped) в один текстурный тексель (texel; см. статью в словаре). Минификация текстуры может привести к её "замыливанию", она станет более размытой (blurry), так как в этом случае множество текстурных текселей будут откартированы (mapped) в один экранный пиксел (screen pixel). |
m_device->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR ); | Когда текстура проходит стадию минификации, она может использовать набор т.н. мипмапов (mipmaps) для улучшения визуального качества изображения. Мипмапы - это уменьшенные версии оригинальной текстуры, которые предварительно генерируются, например при запуске игры. Если текстура имеет набор мипмапов, Direct3D решает, какой из них будет использоваться при рендеринге текстуры, исходя из нескольких факторов. Проблема в том, что когда 2 различных мипмапа одной и той же текстуры рендерятся рядом (стык в стык), между ними часто появляется чёткая линия. Для удаления этого визуального артефакта и улучшения переходов и стыков между мипмапами одной текстуры Direct3D использует данный мипмап-фильтр. |
Функция SetRenderState необычайно полезна. С её помощью можно устанавливать десятки параметров, влияющих на работу объекта устройства. Всё, начиная с качества рендеринга и альфа-прозрачности и заканчивая настройками тумана и буфера трафарета, можно настраивать с помощью лишь одной этой функции, указав в качестве первого параметра нужное свойство, а в качестве второго - его значение. Описание всех параметров данной функции можно найти в статье Функция SetRenderState и её параметры или в документации DirectX SDK в секции D3DRENDERSTATETYPE (энумеративный тип).
Вся суть фильтрации текстур заключается в устранении различных визуальных артефактов и повышении качества отрендеренного изображения.
Для установки трёх последних опций вызывается функция SetSamplerState, где в качестве второго параметра указывается выбранный режим фильтрации (filter mode). В Таблице 2 перечислены лишь наиболее распространённые из них.
Таблица 2. Наиболее распространённые режимы фильтрации текстур
РЕЖИМ ФИЛЬТРАЦИИ | ОПИСАНИЕ |
---|---|
D3DTEXF_POINT | Применяет ближайший тексель к желаемому значению пиксела. Наиболее примитивный и неэффективный режим фильтрации. В то же время он не требует больших вычислительных затрат. Используется по умолчанию. |
D3DTEXF_LINEAR | Билинейная интерполяция где средневычисленный бокс текселей размером 2х2 применяется к желаемому пикселу. |
D3DTEXF_ANISOTROPIC | Опцию "Анизотропная фильтрация" можно встретить во многих современных играх. Считается наилучшим режимом фильтрации текстур, который учитывает угол между текстурируемой гранью и экранной плоскостью (plane of the screen). Наиболее ресурсоёмкий режим. |
Для магнификации и минификации мы указываем D3DTEXF_ANISOTROPIC в качестве 2-го параметра функции SetSamplerState, что соответствует анизотропной фильтрации текстур. Для мипмап-фильтрации мы указываем D3DTEXF_LINEAR, что соответствует билинейному режиму фильтрации текстур.
Следующий шаг - установка так называемой матрицы проекции (projection matrix):
... // Устанавливаем матрицу проекции. D3DXMATRIX projMatrix; D3DXMatrixPerspectiveFovLH( &projMatrix, D3DX_PI / 4, (float)d3dpp.BackBufferWidth / (float)d3dpp.BackBufferHeight, 0.1f / m_setup->scale, 1000.0f / m_setup->scale ); ...
Здесь мы устанавливаем матрицу проекции по умолчанию, но при необходимости мы всегда можем сменить её на другую.
Для того, чтобы представить себе, что собой представляет матрица проекции, сравним её с обычной видеокамерой. Когда ты снимаешь на видеокамеру, ты можешь регулировать расстояние между линзами (т.н. оптический зум; присутствует в любой нормальной видеокамере), что позволяет тебе увеличивать или уменьшать поле видимости (от англ. "Field of view", далее - FOV), а также приближать и отдалять изображение в объективе. В 3D-программировании для тех же самых целей применяется матрица проекции. Представь себе точку вида (viewpoint, виртуальный наблюдатель) в 3D-пространстве, которая используется для просмотра нашей 3D-сцены (точно так же, как и обычная видеокамера). Матрица проекции используется для определения воображаемой пирамиды, одна из вершин которой совпадает с положением точки вида (вьюера) и которая расширяется по мере удаления от неё в направлении взгляда воображаемого наблюдателя (вьюера, камеры) (См. Рис.1).
Из рисунка видно, что матрица проекции также определяет т.н. ближнюю (near, переднюю) и дальнюю (far, заднюю) плоскости отсечения (clipping plane). Ближняя плоскость отсечения обозначает границу, перейдя которую (т.е. подойдя слишком близко к камере) любой геометрический объект не будет рендериться и показываться (будет игнорироваться). Дальняя плоскость отсечения делает практически тоже самое. Вся геометрия, оказавшаяся по ту сторону дальней плоскости отсечения будут рассматриваться как расположенные слишком далеко от камеры и потому не будет рендериться и отображаться (будет игнорироваться). Это один из лучших методов оптимизации и снижения числа полигонов, которые движок должен рендерить (особенно когда его комбинируют с каким-нибудь туманом). Данная техника является стандартом де-факто при разработке большинства Её принцип прост: Всё то, что не попадает в камеру, незачем обрабатывать. Ты наверняка замечал, что, играя в очередной 3D-экшн, если посмотреть вдаль, с увеличением расстояния от наблюдателя туман становится всё плотнее и плотнее до тех пор, пока ты совсем не сможешь что-либо разглядеть. Почти все движки настроены так, что дальняя плоскость отсечения расположена сразу же за точкой, где туман становится максимально плотным и ты не можешь что-либо разглядеть. Если в этом случае туман убрать, то увидим неестесственное мгновенное появление/исчезновение удалённых объектов, что выглядит непрофессионально.
Таким образом, движку нет необходимости рендерить абсолютно все объекты на дальнем плане, попавшие в объектив виртуальной камеры, а из-за тумана у игрока складывается впечатление, что дальние предметы просто не видны. Всё как в реальной жизни.
Но вернёмся к коду, устанавливающему матрицу проекции. Наша матрица проекции хранится в структуре D3DXMATRIX, которая является матрицей общего вида в DirectX и имеет размер 4х4 элемента. Для расчёта нашей матрицы проекции мы используем функцию D3DXMatrixPerspectiveFovLH, которая является служебной функцией библиотеки D3DX.
Обрати внимание
В окончании названия функции D3DXMatrixPerspectiveFovLH видим две большие буквы LH. Они означают, что мы устанавливаем матрицу проекции для т.н. леворучной системы координат, рассматриваемой в начале этой главы. При использовании праворучной системы координат, для установки матрицы проекции применяется схожая функция D3DXMatrixPerspectiveFovRH.
Вот прототип функции D3DXMatrixPerspectiveFovLH:
D3DXMATRIX* D3DXMatrixPerspectiveFovLH( _Inout_ D3DXMATRIX *pOut, // Матрица для сохранения результата _In_ FLOAT fovy, // Поле видимости (Field of View) _In_ FLOAT Aspect, // Ширина видимого пространства, поделённая на высоту _In_ FLOAT zn, // Бижняя плоскость отсечения видимости (Near plane) _In_ FLOAT zf // Дальняя плоскость отсечения видимости (Far plane) );
В нашем случае (в Engine.cpp) в функции D3DXMatrixPerspectiveFovLH мы указываем следующие значения данных параметров:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
&projMatrix | Указатель на только что созданную матрицу проекции, в которую будет сохранены результаты расчётов. |
D3DX_PI/4 | Указываем, что наша усечённая пирамида поля видимости (viewer frustum) будет сходиться в точке наблюдателя по углом 90 градусов. |
(float)d3dpp.BackBufferWidth / (float)d3dpp.BackBufferHeight | Отношение (aspect) ширины экрана к его высоте. Здесь данные берутся из бэкбуфера, который использует наш объект устройства. Например при разрешении 800х600 точек соотношение сторон составит 1,33 . |
0.1f / m_setup->scale | Расстояние от точки наблюдателя до ближней плоскости отсечения (Near clipping plane). Делим на значение масштаба, указанное ранее в структуре EngineSetup. |
1000.0f / m_setup->scale | Расстояние от точки наблюдателя до дальней плоскости отсечения (Far clipping plane). Делим на значение масштаба, указанное ранее в структуре EngineSetup. |
Матрица проекции создана, но наш объект устройства ничего не знает о ней (и даже не подозревает о её существовании). Поэтому пропишем только что созданную матрицу проекции в объекте устройства:
... m_device->SetTransform( D3DTS_PROJECTION, &projMatrix ); ...
Здесь мы вызываем специальную функцию SetTransform (экспонирована объектом устройства), которая как раз и служит для назначения всевозможных матриц трансформации (обязательно рассмотрим их позднее), одной из которых и является наша матрица проекции. Её-то (&projMatrix) мы и устанавливаем в качестве матрицы проекции нашего объекта устройства (D3DTS_PROJECTION).
Матрица проекции установлена. Назначенная матрица будет работать на протяжении всего срока жизни объекта устройства. Поэтому данную операцию не требуется проводить повторно. По крайней мере до тех пор, пока не решишь сменить матрицу проекции на другую.
Продолжим изучение добавленного кода.
... // Сохранаяем подробные настройки видеорежима. m_displayMode.Width = d3dpp.BackBufferWidth; m_displayMode.Height = d3dpp.BackBufferHeight; m_displayMode.RefreshRate = d3dpp.FullScreen_RefreshRateInHz; m_displayMode.Format = d3dpp.BackBufferFormat; // Своп-цепочка всегда начинается с первого бэкбуфера. m_currentBackBuffer = 0; // Создаём интерфейс спрайта. D3DXCreateSprite( m_device, &m_sprite ); ...
Здесь мы сохраняем подробные настройки видеорежима используемого объекта устройства в заранее созданную структуру m_displayMode (имеет тип D3DDYSPLAYMODE). Это необходимо для последующего обращения к ним в случае необходимости. После этого устанавливаем переменную m_currentBackBuffer в 0, чтобы своп-цепочка всегда начиналась с первого бэкбуфера.
Далее создаём интерфейс ID3DXSprite путём вызова функции D3DXCreateSprite. Даная функция представлена библиотекой D3DX и принимает 2 параметра:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
m_device | Объект устройства, с которым будет работать данный интерфейс. |
&m_sprite | Указатель на объект, в котором будет храниться наш новый экземпляр интерфейса ID3DXSprite. |
Уничтожение объекта устройства Direct3D
Теперь, когда мы создали всё, что нужно, неплохо бы прописать функции уничтожения созданных объектов, выполняемые при завершении работы приложения. Напомним, что уничтожение ненужных объектов происходит в деструкторе класса Engine.Изменения в Engine.cpp (Проект Engine)
- Добавь следующие строки в конец деструктора класса Engine:
SAFE_RELEASE( m_sprite ); SAFE_RELEASE( m_device );
После этого реализация деструктора класса Engine примет следующий вид:
... //----------------------------------------------------------------------------- // The engine class destructor. //----------------------------------------------------------------------------- Engine::~Engine() { // Ensure the engine is loaded. // Проверяем, загружен ли движок. if( m_loaded == true ) { // Everything will be destroyed here (such as the DirectX components). // Здесь всё уничтожаем. if( m_currentState != NULL ) { // Уничтожаем связные списки со стейтами. m_currentState->Close(); SAFE_DELETE( m_states ); // Уничтожаем объект Input. SAFE_DELETE( m_input ); // Уничтожаем менеджер скриптов. SAFE_DELETE( m_scriptManager ); SAFE_RELEASE( m_sprite ); SAFE_RELEASE( m_device ); } } // Uninitialise the COM. CoUninitialize(); // Unregister the window class. UnregisterClass( "WindowClass", m_setup->instance ); // Destroy the engine setup structure. SAFE_DELETE( m_setup ); } ...
Здесь добавляем два макроса SAFE_RELEASE, уничтожающие экземпляр интерфейса ID3DXSprite и, собственно, сам объект устройства IDirect3DDevice9.
Добавляем поддержку рендеринга в функцию Run
Только сейчас мы можем добавить в функцию Run поддержку рендеринга, подготовленную ранее. Это также позволит включить поддержку вызова функции Render из класса State.Изменения в Engine.cpp (Проект Engine)
- Добавь следующие строки в конец функции Run:
// Подготавливаем сцену. m_device->Clear( 0, NULL, viewer.viewClearFlags, 0, 1.0f, 0 ); if( SUCCEEDED( m_device->BeginScene() ) ) { // Рендерим текущий стейт, если таковой имеется. if( m_currentState != NULL ) m_currentState->Render(); // Заканчиваем готовить сцену и показываем её. m_device->EndScene(); m_device->Present( NULL, NULL, NULL, NULL ); // Отслеживаем индекс текущего бэкбуфера. if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 ) m_currentBackBuffer = 0; }
После этого функция Run класса Engine примет следующий вид:
... //----------------------------------------------------------------------------- // Enters the engine into the main processing loop. // Главный цикл обработки сообщений движка. //----------------------------------------------------------------------------- void Engine::Run() { // Ensure the engine is loaded. // Проверяем, загружен ли движок. if( m_loaded == true ) { // Show the window. // Показываем окно. ShowWindow( m_window, SW_NORMAL ); // Используется для получения настроек вьюера от приложения. ViewerSetup viewer; // Enter the message loop. // Входим в цикл обработки сообщений. MSG msg; ZeroMemory( &msg, sizeof( MSG ) ); while( msg.message != WM_QUIT ) { if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } else if( !m_deactive ) { // Calculate the elapsed time. // Подсчитываем затраченное время. unsigned long currentTime = timeGetTime(); static unsigned long lastTime = currentTime; float elapsed = ( currentTime - lastTime ) / 1000.0f; lastTime = currentTime; // Обновляем объект input, читаем ввод с клавиатуры и мыши. m_input->Update(); // Проверяем нажатие кнопки F12. // Если да, то принудительно завершаем работу приложения. if( m_input->GetKeyPress( DIK_F12 ) ) { PostQuitMessage( 0 ); } // Запрос вьюера текущего стейта (если таковой имеется). if( m_currentState != NULL ) { m_currentState->RequestViewer( &viewer ); } // Обновить текущиё стейт (если таковой имеется), // учитывая возможную смену стейтов. m_stateChanged = false; if( m_currentState != NULL ) { m_currentState->Update( elapsed ); } if( m_stateChanged == true ) continue; // Подготавливаем сцену. m_device->Clear( 0, NULL, viewer.viewClearFlags, 0, 1.0f, 0 ); if( SUCCEEDED( m_device->BeginScene() ) ) { // Рендерим текущий стейт, если таковой имеется. if( m_currentState != NULL ) m_currentState->Render(); // Заканчиваем готовить сцену и показываем её. m_device->EndScene(); m_device->Present( NULL, NULL, NULL, NULL ); // Отслеживаем индекс текущего бэкбуфера. if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 ) m_currentBackBuffer = 0; } } } } ...
Исследуем добавленный код
Первым делом вызываем функцию Clear нашего объекта устройства, которая очищает вьюпорт (viewport, viewer, FOV, - это, в принципе, синонимы):... // Подготавливаем сцену. m_device->Clear( 0, NULL, viewer.viewClearFlags, 0, 1.0f, 0 ); ...
Вьюпорт - это как "окно" в 3D-пространство, видимая на экране область 3D-сцены. Она содержит всё, что видит в данный момент виртуальная камера. Функция Clear очищает вьюпорт, чтобы в нём не осталось данных от ранее рендереных кадров. Вот её прототип:
HRESULT Clear( // Первые 2 параметра применяются для указания участка (области) экрана для очистки. // В нашем случае не используются, поэтому установлены в ноль. [in] DWORD Count, [in] const D3DRECT *pRects, [in] DWORD Flags, // Флаги, указывающие какие именно поверхности будут очищены [in] D3DCOLOR Color, // 32-битное значение цвета, которым будет залит очищенный вьюпорт. [in] float Z, // Устанавливаем значение буфера глубины. [in] DWORD Stencil // Устанавливаем значение буфера трафарета. );
Функция Clear содержит несколько параметров. Но нас интересует всего один из них - Flags, где выставляются т.н. флаги очищения (clear flags). Возможные значения данного параметра указаны в Таблице 3.
Таблица 3. Возможные значения параметра Flags функции Clear
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
D3DCLEAR_STENCIL | Очищает буфер трафарета, устанавливая значение, указанное в переменной Stencil. |
D3DCLEAR_TARGET | Очищает цель рендеринга (render target), устанавливая цвет, указанный в переменной Color. |
D3DCLEAR_ZBUFFER | Очищает буфер глубины (z-буфер), устанавливая значение, указанное в переменной Z. |
В нашем случае мы не будем указывать ни одно из этих значений, так как в данный момент не можем предположить, что именно будем очищать. Вместо этого мы передаём значение переменной viewClearFlags из структуры ViewerSetup, которое, в свою очередь, получаем из функции RequestViewer, которая обязательно вызывается в текущем стейте. Это позволяет определить, что именно необходимо очищать в каждом стейте. Например, если один стейт использовал буфер глубины, а другой - нет, то в этом случае только первый из них должен заботиться об очищении буфера глубины по завершении своей работы .
Рассмотрим остальные параметры. Когда очищаем вьюпорт, мы очищаем его весь целиком. Поэтому нам не интересны два первых параметра, указывающих определённую область вьюпорта для очищения.
Последние 3 параметра указывают значения для трёх соответствующих буферов. В нашем случае мы выставляем значения по умолчанию для каждого из них:
- 0 (ноль) соответствует чёрному цвету заливки;
- 1.0 для буфера глубины (z-буфера);
- 0 для буфера шаблона.
Обрати внимание
Напомним, что очищение этих поверхностей - весьма ресурсоёмкая операция и должна выполняться как можно реже. Для стандартного 3D-рендеринга в текущий вьюпорт (именно такой мы будем создавать в нашей будущей игре) тебе необходимо заботиться только об очищении буфера глубины (z-буфера).
... if( SUCCEEDED( m_device->BeginScene() ) ) { // Рендерим текущий стейт, если таковой имеется. if( m_currentState != NULL ) m_currentState->Render(); // Заканчиваем готовить сцену и показываем её. m_device->EndScene(); m_device->Present( NULL, NULL, NULL, NULL ); // Отслеживаем индекс текущего бэкбуфера. if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 ) m_currentBackBuffer = 0; } ...
Вызов функции BeginScene обрамлён условным оператором if для проверки успешности её выполнения. Если функция выполнена успешно, то мы без опаски можем переходить к рендерингу текущего кадра.
Внутри условного оператора if видим ещё пару таких же. Здесь мы проверяем на наличие стейта в переменной m_currentState (текущий стейт). Если m_currentState не равна нулю, то вызываем функцию Render для выполнения рендеринга текущего стейта.
Обрати внимание
Вызов функции Render прямо из стейта выполняется только в случае специализированного рендеринга, необрабатываемого движком. Как мы позднее увидим, подавляющая часть рендеринга сцены и объектов на ней выполняется средствами движка.
Как только рендеринг текущего кадра завершён, поочерёдно вызываем функции EndScene и Present, экспонированные нашим объектом устройства. Всякий раз, когда вызывается функция BeginScene, после неё обязательно должна вызываться сопутствующая ей функция EndScene, информирующая объект устройства о том, что рендеринг текущего кадра завершён и он готов к выводу на экран (с помощью функции Present). После этого функция Present выполняет флип (циклическое перелистывание) кадров своп-цепочки для представления бэк-буфера (который содержит только что отрендеренную сцену) на экране. Функция Present имеет несколько параметров, которые позволяют выполнять некоторые специальные операции, как например представление только части бэк-буфера вместо того, чтобы показывать его целиком. Мы не будем проделывать данные операции, поэтому все параметры выставляем в NULL (ноль) для выполнения стандартной операции презентации. Когда мы произвели флип своп-цепочки, необходимо проверить, какой из бэк-буферов является в данный момент фронтальным (передним, выводящимся в данный момент на экран). Для этого инкрементируем (увеличиваем на единицу) переменный член m_currentBackBuffer. Вместе с этим мы проверяем, не вызовет ли проведённое инкрементирование превышение переменной totalBackBuffers (общего числа бэк-буферов), указанной в классе EngineSetup. Если общее число буферов превышает установленное значение, то в этом случае своп-цепочка совершила один полный оборот (цикл, проход) и вернулась к своему началу (то есть первый бэк-буфер встал на место фронтального). Для обозначения этого устанавливаем переменную m_currentBackBuffer в 0 (ноль).
Добавляем поддержку рендеринга в функцию ChangeState
Раз уж речь зашла о бэк-буферах, то также внесём изменения в функцию ChangeState, т.к. при смене стейтов мы принудительно выставляем первый в своп-цепочке бэк-буфер в качестве фронтального.Изменения в Engine.cpp (Проект Engine)
- Добавь следующие строки в функцию ChangeState, сразу перед флагом m_stateChanged = true :
// Сменяем бэк-буферы до тех пор, пока первый бэк-буфер не станет фронтальным while( m_currentBackBuffer != 0 ) { m_device->Present( NULL, NULL, NULL, NULL ); if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 ) m_currentBackBuffer = 0; }
После этого функция ChangeState класса Engine примет следующий вид:
... //----------------------------------------------------------------------------- // Сменить текущий стейт на стейт с указанным ID. //----------------------------------------------------------------------------- void Engine::ChangeState( unsigned long id ) { // Итерировать через список стейтов и найти новый стейт, на который надо сменить. m_states->Iterate( true ); while( m_states->Iterate() != NULL ) { if( m_states->GetCurrent()->GetID() == id ) { // Закрываем предыдущий стейт. if( m_currentState != NULL ) m_currentState->Close(); // Устанавливаем новый стейт в качестве текущего и загружаем его. m_currentState = m_states->GetCurrent(); m_currentState->Load(); // Сменяем бэк-буферы до тех пор, пока первый бэк-буфер не станет фронтальным while( m_currentBackBuffer != 0 ) { m_device->Present( NULL, NULL, NULL, NULL ); if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 ) m_currentBackBuffer = 0; } // Указываем флаг, что стейт был изменён в данном кадре. m_stateChanged = true; break; } } } ...
Исследуем добавленный код
Всякий раз, когда один стейт сменяется на другой (путём вызова функции ChangeState), нам необходимо поместить первый бэк-буфер впереди всей своп-цепочки (т.е. установить его на место фронтального). Это необходимо в ситуациях, когда какой-либо из стейтов, использующий GDI (например для вывода диалогового окна; GDI использует всего 1 бэк-буфер), должен немедленно получить фронтальный буфер под свои нужды чтобы вывести на экран всё необходимое без глюков и графических артефактов. Для достижения этой цели, мы входим в цикл while, который прерывается как только первый бэк-буфер окажется впереди всё своп-цепочки. В каждой итерации цикла while вызывается функция Present (из объекта устройства) для флипа (сдвига) всей своп-цепочки. Вслед за этим мы инкрементируем (увеличиваем на 1) переменный член m_currentBackBuffer, сбрасывая его в ноль всякий раз, как он превысит общее число бэкбуферов в объекте устройства (m_setup->totalBackBuffers). Вложенный цикл while определяет данный сброс значения, после чего немедленно прерывается для того, чтобы остановить флиппинг своп-цепочки. Именно в этот момент система может гарантировать, что первый бэк-буфер окажется впереди своп-цепочки.Заполняем структуру Viewer Setup Structure (структура установки вирт. просмотрщика)
Готовим движок к показу бэк-буферов через вьюер.Изменения в State.h (Проект Engine)
- Размести следующие строки внутри фигурных скобок структуры ViewerSetup:
unsigned long viewClearFlags; // Флаги, применяемые для очищения вьюера. //------------------------------------------------------------------------- // The viewer setup structure constructor. //------------------------------------------------------------------------- ViewerSetup() { viewClearFlags = 0; };
После этого структура ViewerSetup примет следующий вид:
... //----------------------------------------------------------------------------- // Viewer Setup Structure //----------------------------------------------------------------------------- struct ViewerSetup { unsigned long viewClearFlags; // Флаги, применяемые для очищения вьюера. //------------------------------------------------------------------------- // The viewer setup structure constructor. //------------------------------------------------------------------------- ViewerSetup() { viewClearFlags = 0; }; }; ...
- Сохрани Решение (Файл->Сохранить все).
Тестовая перекомпиляция Engine.lib
Итак, система 3D-рендеринга на базе Direct 3D полностью интегрирована в движок.Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:
- В Обозревателе решений щёлкаем правой кнопкой мыши (ПКМ) по названию Проекта Engine. Во всплывающем меню выбираем "Перестроить".
Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).
Итоги главы
Уф! Наш объект устройства создан и настроен. Всё необходимое подготовлено к первому серьёзному рендерингу. К сожалению, у нас пока нет ничего впечатляющего для рендеринга. Даже поддержка 3D-мешей (полигональных сеток) появится не ранее, чем через пару глав. Поэтому для того, чтобы протестировать наш новенький объект устройства мы добавим в движок небольшой класс Font, который способен рендерить и выводить текст на экран. Но об этом в следующей главе.Источники
1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005
ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.11 Добавляем поддержку шрифтов
Последние изменения страницы Пятница 22 / Июль, 2022 01:11:00 MSK
Последние комментарии wiki