Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.10 Добавляем рендеринг (продолж.)
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.10 Добавляем рендеринг (продолжение)


Начало Главы см здесь: Программируем 3D FPS. 1.10 Добавляем рендеринг

Создание объекта устройства Direct3D

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

Закрыть
noteОбрати внимание

В нашем случае это код именно для DirectX 9 (редакция C). В других версиях DirectX строки инициализации могут существенно различаться. В то же время, у нас всегда остаётся возможность добавить в будущем в наш движок поддержку других версий DirectX (например, 8, 10 или 11).

Устройство создаётся в конструкторе класса Engine (Engine.cpp), сразу после завершения энумерации адаптера дисплея и создания главного окна приложения.

Изменения в Engine.cpp (Проект Engine)

  • Добавь следующие строки в конструктор класса Engine, сразу после завершения энумерации адаптера дисплея и создания главного окна приложения:


// Готовим параметры представления объекта устройства
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.
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 примет следующий вид:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;

	// Подготавливаем оконный класс и регистрируем его.
	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 );

	// Инициализируем 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 );

	// Готовим параметры представления объекта устройства
	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.
	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 );

	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// Позволяет приложению произвести настройку всех необходимых стейтов.
	if( m_setup->StateSetup != NULL )
		m_setup->StateSetup();

	// Движок полностью загружен и готов к работе.
	m_loaded = true;
}
...
...

Исследуем добавленный код


Мы начинаем с заполнения структуры D3DPRESENT_PARAMETERS, которая используется Direct3D для создания объекта устройства. Структура содержит практически все возможные параметры, необходимые для настройки устройства определённым образом. Вот её определение (прототип):

Определение (прототип) структуры D3DPRESENT_PARAMETERS
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;	// Формат буфера трафарета глубины (Depth-stencil buffer)
	DWORD Flags;	// Доп. флаги из структуры D3DPRESENTFLAG

	UINT FullScreen_RefreshRateInHz;	//	Частота обновления
	UINT FullScreen_PresentationInterval;	// Интервал вывода изображения (для v-sync)
} D3DPRESENT_PARAMETERS;

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

Фрагмент Engine.cpp (Проект Engine)
...
	// Готовим параметры представления объекта устройства
	D3DPRESENT_PARAMETERS d3dpp;
	ZeroMemory( &d3dpp, sizeof( D3DPRESENT_PARAMETERS ) );
...


Сразу после этого начинаем заполнять структуру DPRESENT_PARAMETERS.

В первую очередь устанавливаем ширину (width), высоту (height) и формат дисплей (Format) на основе данных, указанных пользователем в меню графических настроек.

Фрагмент Engine.cpp (Проект Engine)
...
	d3dpp.BackBufferWidth = g_deviceEnumeration->GetSelectedDisplayMode()->Width;
	d3dpp.BackBufferHeight = g_deviceEnumeration->GetSelectedDisplayMode()->Height;
	d3dpp.BackBufferFormat = g_deviceEnumeration->GetSelectedDisplayMode()->Format;
...

Доступ к ним, в свою очередь, осуществляется через функцию GetSelectedDisplayMode из класса DeviceEnumeration. В ходе этого мы обращаемся к соответствующему члену структуры DISPLAYMODE, которая возвращает значение соответствующего параметра.

Устанавливаем общее число бэкбуферов, которые будут использоваться в своп-цепочке:

Фрагмент Engine.cpp (Проект Engine)
...
	d3dpp.BackBufferCount = m_setup->totalBackBuffers;
...

Эти данные берутся из структуры EngineSetup, указатель на которую (*m_setup) передаётся в конструктор.

Следующий параметр указывает устройству, каким именно образом (с каким эффектом) будут сменяться бэкбуферы в своп-цепочке:

Фрагмент Engine.cpp (Проект Engine)
...
	d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
...

Данный параметр может принимать одно из следующих значений:

Значение Описание
D3DSWAPEFFECT_FLIP Использует несколько бэкбуферов в зацикленном списке. Путём смещения по кругу циклического списка (своп-цепочки) и быстрого флипа каждый новый кадр напрямую выводится на экран. Рекомендуется для полноэкранных приложений.
D3DSWAPEFFECT_COPY Использует для вывода изображения на экран всего один бэкбуфер (даже если в настройках указано несколько бэкбуферов) путём копирования содержимого бэк буфера на экран. Метод копирования гарантирует, что содержимое бэкбуфера не изменится во время вывода на экран. Рекомендуется для оконных приложений.
D3DSWAPEFFECT_DISCARD Разрешает драйверу дисплея автоматически выбирать между двумя вышеуказанными режимами. Рекомендуется использовать во всех случаях, так как это гарантирует, что в каждом отдельном случае будет применён наиболее эффективный переход.

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

Присваиваем параметру дескриптора окна приложения (HWND) значение m_window, что соответствует дескриптору нашего главного окна приложения:

Фрагмент Engine.cpp (Проект Engine)
...
	d3dpp.hDeviceWindow = m_window;
	d3dpp.Windowed = g_deviceEnumeration->IsWindowed();
...

Также указываем объекту устройства Direct3D, в каком режиме будет работать приложение (оконный/полноэкранный), присвоив параметру d3dpp.Windowed значение нашей заранее заготовленной функции IsWindowed из класса DeviceEnumeration.

Следующий параметр передаёт управление Direct3D т.н. буфером трафарета глубины (depth stencil buffer(external link)):

Фрагмент Engine.cpp (Проект Engine)
...
	d3dpp.EnableAutoDepthStencil = true;
...

При значении true вместе с объектом устройства будет также создан буфер трафарета глубины (depth stencil buffer), который автоматически назначается целью рендеринга (render target). В этом случае также обязательно выставляем его формат:

Фрагмент Engine.cpp (Проект Engine)
...
	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 не остаётся каких-либо других способов определения, какая из граней расположена спереди (с точки зрения вьюера). Это может привести к тому, что грани будут отрендерены в неверном порядке и скрытые грани станут видимыми.

Закрыть
noteОбрати внимание

Когда мы начнём рендеринг настоящей сцены с внушительным количеством граней, попробуй отключить буфер глубины, установив параметр EnableAutoDepthStencil в false и посмотри, что будет. Для нашего буфера трафарета глубины мы используем D3DFMT_D16 формат, что означает, что мы не будем использовать буфер трафарета (stencil buffer). Он в основном применяется в специальных приёмах как например трафаретное затенение (stencil shadowing), силуэты (silhouettes), деколи (decals) и т.д. В нашем курсе мы не будем внедрять в движок что-либо подобное (в целях сохранения доступности изложения). Поэтому нам не нужен буфер трафарета.


Устанавливаем частоту обновления (refresh rate) и интервал презентации (presentation interval = v-sync):

Фрагмент Engine.cpp (Проект Engine)
...
	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. Поэтому уничтожаем его инстанс:

Фрагмент Engine.cpp (Проект Engine)
...
	// Уничтожаем объект энумерации устройства.
	SAFE_DELETE( g_deviceEnumeration );
...

Сразу после этого приступаем к созданию объекта устройства:

Фрагмент Engine.cpp (Проект Engine)
...
	// Создаём устройство Direct3D.
	if( FAILED( d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_window, D3DCREATE_MIXED_VERTEXPROCESSING, &d3dpp, &m_device ) ) )
		return;
...

Функция CreateDevice экспонирована интерфейсом Direct3D. Вот её прототип:

Прототип функции CreateDevice
HRESULT CreateDevice(
// Адаптер дисплея. Значение D3DADAPTER_DEFAULT означает первичный видеоадаптер.
UINT                  Adapter,

// Тип устройства. Используй значение D3DDEVTYPE_HAL для создания устройства типа HAL (с аппаратной обработкой рендеринга и спецэффктов)
D3DDEVTYPE            DeviceType,

HWND                  hFocusWindow,   // Дескриптор окна приложения
DWORD                 BehaviorFlags,   // Опции для контроля создания объекта устройства и его дальнейшего поведения
D3DPRESENT_PARAMETERS *pPresentationParameters,   // Указатель на заполненную нами ранее структуру D3DPRESENT_PARAMETERS
IDirect3DDevice9      **ppReturnedDeviceInterface   // Адрес указателя, куда будет сохранён созданный объект устройства
);

Как всегда, подробные комментарии помогут разобраться в коде.
В функции CreateDevice мы указали следующие параметры (в порядке их указания):

Значение параметра Описание
D3DADAPTER_DEFAULT Указываем, что хотим создать объект устройства на базе первичного видеодрайвера (= видеоадаптер по умолчанию).
D3DDEVICETYPE_HAL Создаваемое устройство будет работать в режиме аппаратной обработки рендеринга и спецэффектов. Как вариант, вместо этого можно указать значение D3DDEVICETYPE_REF для использования драйвера программной (=процессорной) обработки графики. Например, для тестирования фич и технологий, которые данный видеоадаптер не поддерживает.
m_window Дескриптор окна приложения, в котором будет отображаться 3D-сцена.
D3DCREATE_MIXED_VERTEXPROCESSING Флаг (флаги) поведения. См. Таблицу 5. В нашем случае мы используем один флаг, означающий, что устройство может использовать как программную, так и аппаратную обработку вершин.
&d3dpp Указатель на ранее заполненную структуру D3DPRESENT_PARAMETERS. Это позволяет объекту устройства создать самого себя, основываясь на дополнительных параметрах, многие из которых предварительно указаны пользователем в меню графических настроек.
&m_device Указатель объекта устройства, по которому в дальнейшем будем обращаться к нему.


Таблица 5. Возможные значения параметра BehaviorFlags функции CreateDevice

Значение параметра Описание
D3DCREATE_HARDWARE_VERTEXPROCESSING Данные о вершинах обрабатываются аппаратно адаптером дисплея. Чаще всего это даёт большой прирост производительности. В то же время, возможности по обработке вершин ограничены возможностями конкретной видеокарты.
D3DCREATE_SOFTWARE_VERTEXPROCESSING Данные о вершинах обрабатываются программными средствами (в основном с помощью процессора) DirectX. Данный режим далеко не всегда даёт хорошее качество картинки. В то же время, DirectX предоставляет фиксированный набор технологий, который гарантированно будет оставаться неизменным на разных компьютерах.
D3DCREATE_MIXED_VERTEXPROCESSING Данный режим позволяет объекту устройства производить как аппаратную так и программную обработку вершин.


Когда мы делаем вызов функции CreateDevice, мы заключаем его в условный оператор if для проверки случая её неудачного выполнения. Здесь мы используем специальный DirectX-макрос FAILED. Неудачное выполнение может произойти по самым разным причинам. Например, из-за неверно указанного параметра.

Закрыть
noteОбрати внимание

Одной из частых причин неудачного выполнения функции CreateDevice является слишком большое значение общего числа бэкбуферов, указанное в настройках. Также, для того, чтобы минимизировать вероятность неудачи, программист должен проверить, имеется ли на целевом компьютере подходящая видеокарта и что в системе установлена подходящая (либо самая новая) версия DirectX. В случае, если создание объекта устройства завершится неудачей, то конструктор класса Engine досрочно завершит свою работу (выполнит return), так и не дойдя до установки флага m_loaded в true. Это, в свою очередь, предотвратит выполнение функции Run без созданного объекта устройства. Как видим, все компоненты движка предельно логичны и взаимосвязаны. Именно так пишутся профессиональные движки.


Если создание объекта устройства прошло успешно, нам больше не нужен интерфейс Direct3D. Поэтому спокойно удаляем его:

Фрагмент Engine.cpp (Проект Engine)
...
	// Освобождаем интерфейс Direct3D, т.к. он больше не нужен.
	SAFE_RELEASE( d3d );
...


На этом этапе объект устройства полностью создан и готов к работе. Вместе с тем, выше мы добавили в конструктор класса Engine (Engine.cpp, Проект 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 необычайно полезна. С её помощью можно устанавливать десятки параметров, влияющих на работу объекта устройства. Всё, начиная с качества рендеринга и альфа-прозрачности и заканчивая настройками тумана и буфера трафарета, можно настраивать с помощью лишь одной этой функции, указав в качестве первого параметра нужное свойство, а в качестве второго - его значение. Описание всех параметров данной функции можно найти в статье Функция IDirect3DDevice9::SetRenderState и её параметры или в документации DirectX SDK в секции D3DRENDERSTATETYPE (энумеративный тип).
Вся суть фильтрации текстур заключается в устранении различных визуальных артефактов и повышении качества отрендеренного изображения.
Для установки трёх последних опций вызывается функция SetSamplerState, где в качестве второго параметра указывается выбранный режим фильтрации (filter mode). В Таблице 6 перечислены лишь наиболее распространённые из них.

Таблица 6. Наиболее распространённые режимы фильтрации текстур

Значение параметра Описание
D3DTEXF_POINT Применяет ближайший тексель к желаемому значению пиксела. Наиболее примитивный и неэффективный режим фильтрации. В то же время он не требует больших вычислительных затрат. Используется по умолчанию.
D3DTEXF_LINEAR Билинейная интерполяция где средневычисленный бокс текселей размером 2х2 применяется к желаемому пикселу.
D3DTEXF_ANISOTROPIC Опцию "Анизотропная фильтрация" можно встретить во многих современных играх. Считается наилучшим режимом фильтрации текстур, который учитывает угол между текстурируемой гранью и экранной плоскостью (plane of the screen). Наиболее ресурсоёмкий режим.

Для магнификации и минификации мы указываем D3DTEXF_ANISOTROPIC в качестве 2-го параметра функции SetSamplerState, что соответствует анизотропной фильтрации текстур. Для мипмап-фильтрации мы указываем D3DTEXF_LINEAR, что соответствует билинейному режиму фильтрации текстур.

Следующий шаг - установка так называемой матрицы проекции (projection matrix):

Фрагмент Engine.cpp (Проект Engine)
...
	// Устанавливаем матрицу проекции.
	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 );
...
Рис. 1 Матрица проекции похожа на пирамиду, которая расширяется в направлении взгляда наблюдателя в 3D-пространстве
Рис. 1 Матрица проекции похожа на пирамиду, которая расширяется в направлении взгляда наблюдателя в 3D-пространстве

Здесь мы устанавливаем матрицу проекции по умолчанию, но при необходимости мы всегда можем сменить её на другую.

Для того, чтобы представить себе, что собой представляет матрица проекции, сравним её с обычной видеокамерой. Когда ты снимаешь на видеокамеру, ты можешь регулировать расстояние между линзами (т.н. оптический зум; присутствует в любой нормальной видеокамере), что позволяет тебе увеличивать или уменьшать поле видимости (от англ. "Field of view", далее - FOV), а также приближать и отдалять изображение в объективе. В 3D-программировании для тех же самых целей применяется матрица проектирования. Представь себе точку вида (viewpoint, виртуальный наблюдатель) в 3D-пространстве, которая используется для просмотра нашей 3D-сцены (точно так же, как и обычная видеокамера). Матрица проекции используется для определения воображаемой пирамиды, одна из вершин которой совпадает с положением точки вида (вьюера) и которая расширяется по мере удаления от неё в направлении взгляда воображаемого наблюдателя (вьюера, камеры) (См. Рис. 1).

Из рисунка видно, что матрица проекции также определяет т.н. ближнюю (near, переднюю) и дальнюю (far, заднюю) плоскости отсечения(external link) (clipping plane). Ближняя плоскость отсечения обозначает границу, перейдя которую (т.е. подойдя слишком близко к камере) любой геометрический объект не будет рендериться и показываться (будет игнорироваться). Дальняя плоскость отсечения делает практически тоже самое. Вся геометрия, оказавшаяся по ту сторону дальней плоскости отсечения будут рассматриваться как расположенные слишком далеко от камеры и потому не будут рендериться и отображаться (будут игнорироваться). Это один из лучших методов оптимизации и снижения числа полигонов, которые движок должен рендерить (особенно когда его комбинируют с каким-нибудь туманом). Данная техника является стандартом де-факто при разработке большинства 3D-игр. Её принцип прост: Всё то, что не попадает в камеру, незачем обрабатывать. Ты наверняка замечал, что, играя в очередной 3D-экшн, если посмотреть вдаль, с увеличением расстояния от наблюдателя туман становится всё плотнее и плотнее до тех пор, пока ты совсем не сможешь что-либо разглядеть. Почти все движки настроены так, что дальняя плоскость отсечения расположена сразу же за точкой, где туман становится максимально плотным и ты не можешь что-либо разглядеть. Если в этом случае туман убрать, то увидим неестесственное мгновенное появление/исчезновение удалённых объектов, что выглядит непрофессионально.

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

Но вернёмся к коду, устанавливающему матрицу проекции:

Фрагмент Engine.cpp (Проект Engine)
...
	// Устанавливаем матрицу проекции.
	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 );
...

Наша матрица проекции хранится в структуре D3DXMATRIX, которая является матрицей общего вида в DirectX и имеет размер 4х4 элемента. Для расчёта нашей матрицы проекции мы используем функцию D3DXMatrixPerspectiveFovLH, которая является служебной функцией библиотеки D3DX.

Закрыть
noteОбрати внимание

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

Вот прототип функции D3DXMatrixPerspectiveFovLH:

Прототип функции D3DXMatrixPerspectiveFovLH
D3DXMATRIX *WINAPI 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.

Матрица проекции создана, но наш объект устройства ничего не знает о ней (и даже не подозревает о её существовании). Поэтому пропишем только что созданную матрицу проекции в объекте устройства:

Фрагмент Engine.cpp (Проект Engine)
...
	m_device->SetTransform( D3DTS_PROJECTION, &projMatrix );
...

Здесь мы вызываем специальную функцию SetTransform (экспонирована объектом устройства), которая как раз и служит для назначения всевозможных матриц трансформации (обязательно рассмотрим их позднее), одной из которых и является наша матрица проекции. Её-то (&projMatrix) мы и устанавливаем в качестве матрицы проекции нашего объекта устройства (D3DTS_PROJECTION).
Матрица проекции установлена. Назначенная матрица будет работать на протяжении всего срока жизни объекта устройства. Поэтому данную операцию не требуется проводить повторно. По крайней мере до тех пор, пока не решишь сменить матрицу проекции на другую.

Исследуем добавленный код далее:

Фрагмент Engine.cpp (Проект Engine)
...
	// Сохранаяем подробные настройки видеорежима.
	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 примет следующий вид:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).

		// Уничтожаем связные списки со стейтами.
		if( m_currentState != NULL )
			m_currentState->Close();
		SAFE_DELETE( m_states );

		// Уничтожаем ранее созданные объекты.
		SAFE_DELETE( m_input );
		SAFE_DELETE( m_scriptManager );

		// Уничтожаем интерфейс спрайта.
		SAFE_RELEASE( m_sprite );

		// Уничтожаем объект устройства.
		SAFE_RELEASE( m_device );
	}
...


Здесь добавляем два макроса 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 примет следующий вид:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Вводит движок в главный цикл выборки сообщений.
//-----------------------------------------------------------------------------
void Engine::Run()
{
	// Убеждаемся, что движок загружен.
	if( m_loaded == true )
	{
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

		// Используется для получения деталей настроек вьюера от приложения.
		ViewerSetup viewer;

		// Входим в цикл выборки сообщений.
		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 )
			{
				// Подсчитываем затраченное время.
				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;
				}
			}
		}
	}

	// Уничтожаем движок.
	SAFE_DELETE( g_engine );
}
...

Исследуем добавленный код

Первым делом вызываем функцию Clear нашего объекта устройства, которая очищает вьюпорт (viewport, viewer, FOV, - это, в принципе, синонимы):

Фрагмент Engine.cpp (Проект Engine)
...
				// Подготавливаем сцену.
				m_device->Clear( 0, NULL, viewer.viewClearFlags, 0, 1.0f, 0 );
...

Вьюпорт - это как "окно" в 3D-пространство, видимая на экране область 3D-сцены. Она содержит всё, что видит в данный момент виртуальная камера. Функция Clear очищает вьюпорт, чтобы в нём не осталось данных от ранее рендереных кадров. Вот её прототип:

Прототип функции 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). Возможные значения данного параметра указаны в Таблице 7.
Таблица 7. Возможные значения параметра Flags функции Clear

Параметр Описание
D3DCLEAR_STENCIL Очищает буфер трафарета, устанавливая значение, указанное в переменной Stencil.
D3DCLEAR_TARGET Очищает цель рендеринга (render target), устанавливая цвет, указанный в переменной Color.
D3DCLEAR_ZBUFFER Очищает буфер глубины (z-буфер), устанавливая значение, указанное в переменной z.

В нашем случае мы не будем указывать ни одно из этих значений, так как в данный момент не можем предположить, что именно будем очищать. Вместо этого мы передаём переменную viewClearFlags из структуры ViewerSetup, которое, в свою очередь, получаем из функции RequestViewer, которая обязательно вызывается в текущем стейте. Это позволяет определить, что именно необходимо очищать в каждом стейте. Например, если один стейт использовал буфер глубины, а другой - нет, то в этом случае только первый из них должен заботиться об очищении буфера глубины по завершении своей работы.
Рассмотрим остальные параметры. Когда очищаем вьюпорт, мы очищаем его весь целиком. Поэтому нам не интересны два первых параметра, указывающих определённую область вьюпорта для очищения.
Последние 3 параметра указывают значения для трёх соответствующих буферов. В нашем случае мы выставляем значения по умолчанию для каждого из них:

  • 0 (ноль) соответствует чёрному цвету заливки;
  • 1.0 для буфера глубины (z-буфера);
  • 0 для буфера шаблона.
Закрыть
noteОбрати внимание

Напомним, что очищение этих поверхностей - весьма ресурсоёмкмя операция и должна выполняться как можно реже. Для стандартного 3D-рендеринга в текущий вьюпорт (именно такой мы будем создавать в нашей будущей игре) тебе необходимо заботиться только об очищении буфера глубины (z-буфера).


После того, как мы выполнили все очистки вьюпорта, следующий шаг - позволить объекту устройства подготовить себя к рендерингу текущего кадра. Эта цель достигается путём вызова функции BeginScene, экспонированную объектом устройства Direct3D:

Фрагмент Engine.cpp (Проект Engine)
...
				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 для проверки успешности её выполнения (применяем DirectX-макрос SUCCEEDED). Если функция выполнена успешно, то мы без опаски можем переходить к рендерингу текущего кадра.
Внутри условного оператора if видим ещё пару таких же. Здесь мы проверяем на наличие стейта в переменной m_currentState (текущий стейт). Если m_currentState не равна нулю, то вызываем функцию Render для выполнения рендеринга текущего стейта.

Закрыть
noteОбрати внимание

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

Как только рендеринг текущего кадра завершён, поочерёдно вызываем функции EndScene и PresentScene, экспонированные нашим объектом устройства. Всякий раз, когда вызывается функция 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 примет следующий вид:

Фрагмент Engine.cpp (Проект 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 примет следующий вид:

Фрагмент State.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Viewer Setup Structure (структура установки вирт. просмотрщика)
//-----------------------------------------------------------------------------
struct ViewerSetup
{
		unsigned long viewClearFlags; // Флаги, применяемые для очищения вьюера.

	//-------------------------------------------------------------------------
	// The viewer setup structure constructor.
	//-------------------------------------------------------------------------
	ViewerSetup()
	{
		viewClearFlags = 0;
	};
};
...

Здесь сначала объявляем новую переменную viewClearFlags, а затем в конструкторе структуры присваиваем ей по умолчанию нулевое значение.

Тестовая перекомпиляция Engine.lib

Итак, система скриптов полностью интегрирована в движок.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

Image
По окончании компиляции (обычно, при создании небольших проектов, она занимает менее 1 секунды) в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции.
В нашем случае, несмотря на многочисленные предупреждения, компиляция прошла успешно.
Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).

Итоги главы

Уф! Наш объект устройства создан и настроен. Всё необходимое подготовлено к первому серьёзному рендерингу. К сожалению, у нас пока нет ничего впечатляющего для рендеринга. Даже поддержка 3D-мешей (полигональных сеток) появится не ранее, чем через пару глав. Поэтому для того, чтобы протестировать наш новенький объект устройства мы добавим в движок небольшой класс Font, который способен рендерить и выводить текст на экран. Но об этом в следующей главе.


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.10 Добавляем рендеринг (продолж.)

Contributors to this page: slymentat .
Последнее изменение страницы Среда 30 / Ноябрь, 2016 19:56:25 MSK автор slymentat.

Последние комментарии

No records to display