ФЭНДОМ


Тестируем библиотекуПравить

Если вам удалось собрать библиотеку без проблем, то ее надо сначала протестировать на работоспособность.

Создайте новый проект (если вы создадите консольное приложение, то у вас будет два окна - окно нашего приложения и консоль. В консоли лог будет писать сообщения что удобно при разработке)

Создайте файл main.cpp. Укажите расположение заголовков фреймворка и расположение самой библиотеки, а также подключите ее (файл с расширением - lib).

Теперь напишите следующий код:

#include "D3D11_Framework.h"

using namespace D3D11Framework;

class MyInput : public InputListener
{
public:
	bool KeyPressed(const KeyEvent &arg)
	{ 
		printf("key press %c\n",arg.wc);
		return false; 
	}
	bool MouseMove(const MouseEvent &arg)
	{
		printf("mouse %d - %d\n",arg.x,arg.y);
		return false; 
	}
};

class MyRender : public Render
{
public:
	bool Init(HWND nwnd){return true;}
	bool Draw(){return true;}
	void Close(){}
};

int main()
{
	Framework framework;

	MyRender *render = new MyRender();
	MyInput *input = new MyInput();

	framework.SetRender(render);
	framework.Init();
	framework.AddInputListener(input);

	framework.Run();

	framework.Close();

	return 0;
}

В начале мы подключаем ранее специально созданный заголовок в котором указаны заголовки нашего фреймворка.

MyInput как вы видите наследуется от абстрактного InputListener. Далее мы полиморфируем виртуальные методы KeyPressed() и MouseMove(). В них мы пишем нужное поведение. Так как я в InputListener я дал виртуальным методам пустые тела, то мне больше не нужно ничего переопределять.

MyRender наследуется от Render. Так как мы пока не начали изучать DirectX, этот класс будет пустой.

В main() сначала мы создаем экземпляр нашего фреймворка. Затем создаем указатели на наш рендер и систему ввода. теперь мы передаем наш рендер фреймворку, инициализируем его и добавляем систему ввода. Внимание: вы должны это делать именно в таком порядке. Если попробуете сделать по другому, получите ошибку.

После запускаем фреймворк на выполнение и в конце очищаем ресурсы.

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

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

ПодготовкаПравить

Теперь, когда у нас есть окно, можно заняться непосредственно изучением самого DirectX.

Сначала подготовим весь код. Создайте новый проект, подключите все что нужно. Создайте MyRender.h

#pragma once

#include "D3D11_Framework.h"

using namespace D3D11Framework;

class MyRender : public Render
{
public:
	bool Init(HWND nwnd);
	bool Draw();
	void Close();
};

Создайте MyRender.cpp и напишите следующий код:

#include "MyRender.h"

bool MyRender::Init(HWND hwnd)
{
	return true;
}

bool MyRender::Draw()
{
	return true;
}

void MyRender::Close()
{

}

Создайте main.cpp и напишите:

#include "MyRender.h"

int main()
{
	Framework framework;

	MyRender *render = new MyRender();

	framework.SetRender(render);
	framework.Init();

	framework.Run();

	framework.Close();

	return 0;
}

Как вы наверное поняли, это тот же самый код что и при тестировании:)

Скомпилируйте.

Инициализация DirectXПравить

Наконец-то. Откройте MyRender.h и MyRender.cpp. Теперь вы в основном будете работать только с этими двумя файлами.

Сначала узнаем что такое DirectX, а точнее его часть - Direct3D.

Direct3D - это низкоуровневый графический API, который позволяет рисовать трехмерные миры используя аппаратное ускорение. Direct3D обеспечивает нас интерфейсами через которые мы управляем нашей видеокартой, но при этом являясь абстрактным средством, Direct3D позволяет нам не волноваться о разных специфичных особенностях видеокарт. Direct3D (да и вообще весь DirectX) - является COM-библиотекой. Для наших уроков вам не нужно знать что это такое. Единственное что вы должны знать - это то что для создания COM-интерфейса, мы не используем new, вместо этого используя специальные методы или функции (обычно XCreateY() ). А при удалении мы вызываем метод Release(), вместо использования delete (помните макрос _RELEASE()? вот он для этого и нужен). Всю остальную работу с памятью обеспечивают сами COM-объекты.

Первое, что нужно сделать для инициализации Direct3D, это создать три объекта - устройство (device), контекст (context) и цепь обмена (swap chain).

Сontext используется приложением для выполнения рендеринга в буфер. Контекст бывает двух видов - immediate (непосредственный) и Deferred (отложенный). Разница между ними в том что первый сразу же передает команды видеокарте, а второй заполняет специальный командный лист и используется в мультипоточных приложениях. Для простоты в уроках мы будем использовать только immediate context.

Device содержит методы для создания ресурсов.

Swap chain отвечает за смену буферов. Что это значит? Swap chain содержит два или более буферов, обычно передний и задний. Буферы - это текстуры которые устройство рендера показывает на мониторе. Передний буфер показыватся непосредственно на мониторе. Этот буфер предназначен только для чтения и не может быть изменен. Задний буфер - это то место, где в данный момент происходит рисование сцены. Как только завершаются все операции по рисованию, swap chain меняет местами буферы (передний становится задним и теперь на нем происходит рисование, задний, на котором были проведены операции рисования, теперь становится передним и показывается пользователю). Данная технология называется doublebuffer и конкретно в DirectX 11 - swap chain's buffers.

Откройте MyRender.h и в классе добавьте члены, так чтобы ваш код выглядел так:

#pragma once

#include "D3D11_Framework.h"

using namespace D3D11Framework;

class MyRender : public Render
{
public:
	MyRender();
	bool Init(HWND hwnd);
	bool Draw();
	void Close();

private:
	D3D_DRIVER_TYPE m_driverType;
	D3D_FEATURE_LEVEL m_featureLevel;
	ID3D11Device *m_pd3dDevice;
	ID3D11DeviceContext *m_pImmediateContext;
	IDXGISwapChain *m_pSwapChain;
	ID3D11RenderTargetView *m_pRenderTargetView;
};

Здесь мы к нашему классу написанному ранее добавили конструктор по умолчанию и несколько приватных членов. Первые два, расмотрим далее. ID3D11Device - это устройство (device). ID3D11DeviceContext - это контекст (context). IDXGISwapChain - Swap chain.

ID3D11RenderTargetView - это объект нашего заднего буфера в котором мы будем рисовать нашу сцену.

Для инициализации Direct3D сделайте:

  1. Опишите особенности swap chain, заполняя структуру DXGI_SWAP_CHAIN_DESC
  2. Создайте интерфейсы ID3D11Device и IDXGISwapChain используя функцию D3D11CreateDeviceAndSwapChain
  3. Создайте render target для заднего буфера swap chain
  4. Создате буфер глубины/шаблона (stencil) и свяжите его с depth/stencil view
  5. Присоедините render target и depth/stencil view к конвееру рендеринга (rendering pipeline), чтобы Direct3D мог их использовать.
  6. Установите вьюпорт (viewport).

Откройте MyRender.cpp и добавьте конструктор:

MyRender::MyRender()
{
	m_driverType = D3D_DRIVER_TYPE_NULL;
	m_featureLevel = D3D_FEATURE_LEVEL_11_0;
	m_pd3dDevice = nullptr;
	m_pImmediateContext = nullptr;
	m_pSwapChain = nullptr;
	m_pRenderTargetView = nullptr;
}

Теперь давайте заполним метод Init(). В самом начале, напишите следующий код:

bool MyRender::Init(HWND hwnd)
{
	HRESULT hr = S_OK;

	RECT rc;
	GetClientRect( hwnd, &rc );
	UINT width = rc.right - rc.left;
	UINT height = rc.bottom - rc.top;

	UINT createDeviceFlags = 0;
#ifdef _DEBUG
	createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

	D3D_DRIVER_TYPE driverTypes[] =
	{
		D3D_DRIVER_TYPE_HARDWARE,
		D3D_DRIVER_TYPE_WARP,
		D3D_DRIVER_TYPE_REFERENCE,
	};
	UINT numDriverTypes = ARRAYSIZE( driverTypes );

	D3D_FEATURE_LEVEL featureLevels[] =
	{
		D3D_FEATURE_LEVEL_11_0,
		D3D_FEATURE_LEVEL_10_1,
		D3D_FEATURE_LEVEL_10_0,
	};
	UINT numFeatureLevels = ARRAYSIZE( featureLevels );

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

Далее мы создаем RECT. и узнаем реальную ширину и высоту экрана.

createDeviceFlags используется для задания определенных флагов. В дебаге мы указываем что устройство должно быть создано в режиме отладки. В этом случае DirectX будет писать в output window вашей IDE свои сообщения.

D3D_DRIVER_TYPE - определяет тип создаваемого устройства. Для чего мы такое написали, объясню позже.

D3D_FEATURE_LEVEL - это очень интересная особенность. она позволяет использовать новый DirectX даже, если ваша видеокарта его не поддерживает.

Заполняем Swap ChainПравить

Теперь напишите ниже (весь код пишется всегда ниже текущего и выше return true)

	DXGI_SWAP_CHAIN_DESC sd;
	ZeroMemory( &sd, sizeof( sd ) );	// очищаем структуру
	sd.BufferCount = 1;			// у нас один задний буфер
	sd.BufferDesc.Width = width;		// устанавливаем ширину буфера
	sd.BufferDesc.Height = height;		// устанавливаем высоту
	sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // формат пикселя
	sd.BufferDesc.RefreshRate.Numerator = 60; // частота обновления экрана
	sd.BufferDesc.RefreshRate.Denominator = 1;
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // назначение буфера
	sd.OutputWindow = hwnd;			// десктриптор окна
	sd.SampleDesc.Count = 1;
	sd.SampleDesc.Quality = 0;
	sd.Windowed = TRUE;			// устанавливаем оконный режим

Здесь мы описываем передний буфер. ZeroMemory() очищает нашу структуру.

BufferDesc - эта структура описывает свойства заднего буфера, который мы хотим создать. Самыми важными свойствами являются ширина, высота и формат пикселя.

BufferUsage - мы указали DXGI_USAGE_RENDER_TARGET_OUTPUT, так как мы собираемся использовать его для заднего буфера (то есть, использовать его в качестве цели рендера (render target)).

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

Создание ID3D11Device и IDXGISwapChain Править

Допишите следующий код:

	for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ )
	{
		m_driverType = driverTypes[driverTypeIndex];
		hr = D3D11CreateDeviceAndSwapChain( NULL, m_driverType, 
			NULL, createDeviceFlags, 
			featureLevels, numFeatureLevels, 
			D3D11_SDK_VERSION, &sd, 
			&m_pSwapChain, &m_pd3dDevice, 
			&m_featureLevel, &m_pImmediateContext );
		if( SUCCEEDED( hr ) )
			break;
	}
	if( FAILED( hr ) )
		return false;

Что мы здесь делаем? Для начала скажу что важно здесь только D3D11CreateDeviceAndSwapChain(). Все остальное - это обвесок:). В начале мы перебираем перечисление D3D_DRIVER_TYPE. То есть, в начале мы пытаемся создать устройство использующее аппаратные средства. Если это не удалось, мы пытаемся создать так называемое WARP устройство. Если и это не удалось, то мы создаем софтварное устройство. Не буду подробно объяснять что это, просто скажу что самое эффективное - аппаратное, а самое медленное - софтварное, то есть мы перебираем от эффективного до неэффективного. этот код позволяет запускать наше приложение даже там где нет аппаратной поддержки DirectX. Так вот, мы передаем в D3D11CreateDeviceAndSwapChain все что инициализировали и настроили ранее.

Render targetПравить

	ID3D11Texture2D* pBackBuffer = NULL;
	hr = m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
	if( FAILED( hr ) )
		return false;

	hr = m_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, &m_pRenderTargetView);
	_RELEASE(pBackBuffer);
	if( FAILED( hr ) )
		return false;

До этого мы описывали передний буфер - то есть тот что непосредственно на экране.

Здесь мы создадим задний. Как вы видите, буфер - это обычная текстура (о чем я ранее уже писал). Дело в том что в DirectX - текстура, это не какое-то изображение, а массив данных или область памяти. И ее можно использовать в разных целях.

Далее, мы получаем указатель на задний буфер из swap chain. Первый параметр - индекс заднего буфера, который мы хотим получить (если их несколько). В нашем случае мы используем всего один задний буфер и поэтому ставим 0. Второй параметр - тип буфера, обычно двухмерная текстура(ID3D11Texture2D). Третий параметр возращает указатель на задний буфер.

Теперь мы создаем сам render target (цель рисования). Для этого мы используем метод ID3D11Device::CreateRenderTargetView(). Напомню, наше устройство (device) отвечает за создание ресурсов. Первый параметр указывает на ресурс, который мы будем использовать в качестве цели рисования. То есть в этот ресурс мы будем производить рисование нашего трехмерного мира. Рисовать нам надо в задний буфер, и мы его туда передаем. Второй мы оставим пустым. третий параметр возвращает указатель на созданный render target.

После этого мы удаляем ресурс pBackBuffer.

Следующий шаг инициализации Direct3D мы пока пропускаем, так как ни буфера глубины, ни буфера шаблонов у нас нет.

Присоединение render target к конвееру рендеринга (rendering pipeline) Править

m_pImmediateContext->OMSetRenderTargets( 1, &m_pRenderTargetView, NULL );

Напомню, m_pImmediateContext отправляет команды сразу же видеокарте. Здесь мы указываем цель рендера, в которую видеокарта будет рисовать полученую картинку.

ViewportПравить

	D3D11_VIEWPORT vp;
	vp.Width = (FLOAT)width;
	vp.Height = (FLOAT)height;
	vp.MinDepth = 0.0f;
	vp.MaxDepth = 1.0f;
	vp.TopLeftX = 0;
	vp.TopLeftY = 0;
	m_pImmediateContext->RSSetViewports( 1, &vp );

Вьюпорт определяет зону рисования. Здесь мы указываем что рисовать надо начиная из точки 0,0 (параметры TopLeftX и TopLeftY), зона рисования имеет размер окна (параметры Width и Height), минимальная глубина 0.0 (MinDepth), максимальная 1.0 (MaxDepth). Последние два нужны для буфера глубины, который мы изучим позже.

На этом мы завершили инициализацию.


Рисуем на экран.Править

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

bool MyRender::Draw()
{
	float ClearColor[4] = { 0.0f, 0.125f, 0.3f, 1.0f }; //красный,зеленый,синий,альфа
	m_pImmediateContext->ClearRenderTargetView( m_pRenderTargetView, ClearColor );
	m_pSwapChain->Present( 0, 0 );
	return true;
}

В начале мы задаем цвет, затем очищаем наш контекст а затем меняем буфер. Повторюсь - когда начинается выполнение этого метода, на экран выводится передний буфер. Затем мы вызываем разные команды для рисования в задний буфер. Помните, мы устанавливали цель рисования нашему m_pImmediateContext? Так что здесь мы берем задний буфер и очищаем его заданным цветом. А вот когда рисование в задний буфер мы завершили, надо то что мы нарисовали вывести на экран, за это и отвечает swap chain. Он берет задний буфер и выводит его на экран. А передний буфер делает задним и теперь все рисование будет производится в него.

Вообщем просто запомните - как только вы завершили рисование, вызывайте IDXGISwapChain::Present(). Если вы попытаетесь рисовать после этого метода, вы скорее всего не увидите результата (так как вы это запишите в заний буфер, который при следующем вызове очистите перед рисованием).

Очищаем память.Править

Найдите метод MyRender::Close() и напишите его так:

void MyRender::Close()
{
	if( m_pImmediateContext ) 
		m_pImmediateContext->ClearState();

	_RELEASE(m_pRenderTargetView);
	_RELEASE(m_pSwapChain);
	_RELEASE(m_pImmediateContext);
	_RELEASE(m_pd3dDevice);
}

Здесь мы очищаем занятую память используя наш макрос _RELEASE

Собственно все. Урок закончен. Компилируйте, запускайте, в результате у вас должен быть синий фон (напомню, что окно мы создавали черным).

В следующем уроке мы выведем первый треугольник.

Исходный кодПравить

Скачать

В архиве три папки D3D_Framework, test и lesson. Первая содержит фреймворк, вторая тестирует его, третья - инициализация Direct3D. Чтобы скомпилировать проекты, вы сначала должны собрать D3D_Framework, а уж потом сам проект. Все проекты настроены должным образом, поэтому данный архив вы можете использовать для дальнейших уроков, чтобы каждый раз заново не настраивать. Папку test при этом можете удалить за ненадобностью.

Материалы сообщества доступны в соответствии с условиями лицензии CC-BY-SA , если не указано иное.