1. 程式人生 > >【圖形學與遊戲程式設計】開發筆記-基礎篇2:DX11初始化

【圖形學與遊戲程式設計】開發筆記-基礎篇2:DX11初始化

(本系列文章由pancy12138編寫,轉載請註明出處:http://blog.csdn.net/pancy12138)

這篇文章應該屬於開始程式設計的第一節了,這一節我們將要講解如何使用C++初始化directx,以及一些新的除錯技巧。因為windows程式框架相關的內容在入門篇的時候講過了,而且也非常簡單,如當初所說,大部分都是些傀儡程式碼,沒什麼難處。大家很容易就能看懂。所以這裡不會再繼續講解這些東西,以圖形學相關的知識為重點吐舌頭

首先,我們需要知道directx11是面向物件的,這一點和openGL有本質上的區別。在dx11裡面,其所有API函式幾乎都被封裝在了兩個類裡面,一個是d3ddevice(也就是d3d裝置),一個是d3dcontex(也就是渲染描述表)。這兩個類一個負責管理資源,一個負責管理繪製。這與當年的dx9是不同的,dx9當初把所有的API函式都封裝在了d3ddevice裡面,並沒有把渲染部分的API拆成一個單獨的類。因此dx11的架構可以說是更為細緻和方便了一些。那麼我們程式的書寫方式也就很明顯了,既然所有的API函式都存放在這兩個類裡面,那麼我們自然應該首先註冊和初始化這兩個類了。那麼註冊這兩個類需要神馬東西呢。很顯然,至少我們得需要一些視窗的資訊,不然的話d3d是無法知道該在哪個視窗上做繪製工作的。那麼也就是說在註冊這兩個類之前我們必須要先把視窗給註冊了。那麼下面我擷取一部分winmain函式的程式碼來大致說明一下directx的兩個重要的類(d3d裝置以及d3d描述表)在winmain函式裡的註冊方式:

wndclass.lpfnWndProc = WndProc;                                   //確定視窗的回撥函式,當視窗獲得windows的回撥訊息時用於處理訊息的函式。
	wndclass.cbClsExtra = 0;                                         //為視窗類末尾分配額外的位元組。
	wndclass.cbWndExtra = 0;                                         //為視窗類的例項末尾額外分配的位元組。
	wndclass.hInstance = hInstance;                                 //建立該視窗類的視窗的控制代碼。
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);          //視窗類的圖示控制代碼。
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);              //視窗類的游標控制代碼。
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);     //視窗類的背景畫刷控制代碼。
	wndclass.lpszMenuName = NULL;                                      //視窗類的選單。
	wndclass.lpszClassName = TEXT("pancystar_engine");                                 //視窗類的名稱。

	if (!RegisterClass(&wndclass))                                      //註冊視窗類。
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			TEXT("pancystar_engine"), MB_ICONERROR);
		return E_FAIL;
	}
	RECT R = { 0, 0, window_width, window_hight };
	AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
	int width = R.right - R.left;
	int height = R.bottom - R.top;

	hwnd = CreateWindow(TEXT("pancystar_engine"), // window class name建立視窗所用的視窗類的名字。
		TEXT("pancystar_engine"), // window caption所要建立的視窗的標題。
		WS_OVERLAPPEDWINDOW,        // window style所要建立的視窗的型別(這裡使用的是一個擁有標準視窗形狀的型別,包括了標題,系統選單,最大化最小化等)。
		CW_USEDEFAULT,              // initial x position視窗的初始位置水平座標。
		CW_USEDEFAULT,              // initial y position視窗的初始位置垂直座標。
		width,               // initial x size視窗的水平位置大小。
		height,               // initial y size視窗的垂直位置大小。
		NULL,                       // parent window handle其父視窗的控制代碼。
		NULL,                       // window menu handle其選單的控制代碼。
		hInstance,                  // program instance handle視窗程式的例項控制代碼。
		NULL);                     // creation parameters建立視窗的指標
	if (hwnd == NULL) 
	{
		return E_FAIL;
	}
	ShowWindow(hwnd, SW_SHOW);   // 將視窗顯示到桌面上。
	UpdateWindow(hwnd);           // 重新整理一遍視窗(直接重新整理,不向windows訊息迴圈佇列做請示)。
	ZeroMemory(&msg, sizeof(msg));
	d3d_pancy_1 *d3d11_test = new d3d_pancy_1(hwnd, viewport_width, viewport_height, hInstance);
	if (d3d11_test->init_create() == S_OK)
	{
		while (msg.message != WM_QUIT)
		{
			if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
			{
				TranslateMessage(&msg);//訊息轉換
				DispatchMessage(&msg);//訊息傳遞給視窗過程函式
				d3d11_test->update();
				d3d11_test->display();
			}
			else
			{
				d3d11_test->update();
				d3d11_test->display();
			}
		}
		d3d11_test->release();
	}

上面d3d11->initcreate()函式就是我們用來註冊d3d裝置,描述表以及在他們兩個類管理下的其他資源的函式。那麼現在我們來看看如何才能初始化這兩個重要的類以及在他們管理下的諸多資源,當然,因為這是第一個最簡單的程式,因此不會有太多的資源被註冊,大家只需要關心一些比較重要的資源註冊情況。首先看那兩個重要的類的註冊方法,其實所謂的註冊兩大核心類,只需要一個函式D3D11CreateDevice()就可以了:

HRESULT WINAPI D3D11CreateDevice(
    _In_opt_ IDXGIAdapter* pAdapter,
    D3D_DRIVER_TYPE DriverType,
    HMODULE Software,
    UINT Flags,
    _In_reads_opt_( FeatureLevels ) CONST D3D_FEATURE_LEVEL* pFeatureLevels,
    UINT FeatureLevels,
    UINT SDKVersion,
    _Out_opt_ ID3D11Device** ppDevice,
    _Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel,
    _Out_opt_ ID3D11DeviceContext** ppImmediateContext );
上面就是註冊函式的定義部分,第一個引數是你的顯示卡標識,填上NULL就是預設顯示卡啦,那有人就問了,啥是預設顯示卡呢,大部分人的顯示卡肯定有分集顯和獨顯,一般驅動也會帶一個調配節能/高效能的軟體,一般來說這個驅動軟體工作在神馬效能上,你的預設顯示卡就算是哪個效能的顯示卡了。第二個引數是建立的驅動型別,也就是渲染的時候究竟用CPU(軟體模擬)還是GPU(硬體模擬)神馬的。這個一般來說只是對於那些老掉牙的機器才會出現顯示卡驅動不支援的情況,對於現在大部分機器來說,這塊不用想了,直接填硬體模擬就行了,千萬別用CPU去做渲染,到時候遊戲幀率之慢能嚇死你大笑。不過對於有些顯示卡你在直接指定更高階顯示卡的時候填D3D_DRIVER_TYPE_HARDWARE會出現不識別的情況,這個時候填D3D_DRIVER_TYPE_UN KNOWN也是可以讓他做硬體模擬的,不過除非它提示你不識別,否則就不要這麼開了。第三個引數是標識軟體模擬引數的,如果前面都已經指定了要用硬體模擬的話,這裡直接賦值為NULL就可以了。第四個引數是建立格式,也就是debug或者release格式,後者的渲染速度要快於前者,但是會喪失一些除錯資訊,具體對我們的影響就是vs的那個圖形偵錯程式會變得很慢很慢。所以大家在寫程式調程式的時候最好選debug格式,然後釋出的時候挑一個NULL就可以了。後面緊接著的三個引數是指定directx版本的,這裡直接填上預設的引數NULL NULL和D3D11_SDK_VERSION就可以了。然後最後面的引數就是我們需要註冊的d3d裝置和d3d描述表,當然還有一個featurelevel用於檢驗是不是真的建立了dx11版本的裝置和描述表。

OK這樣大家就瞭解這兩個最重要的類是怎麼創建出來的了。下面貼出整個建立的函式,來講解建立完兩大基礎類之後我們需要做的事情。

bool d3d_pancy_basic::init(HWND hwnd_need, UINT width_need, UINT hight_need)
{
	UINT create_flag = 0;
	bool if_use_HIGHCARD = true;
	//debug格式下選擇返回除錯資訊
#if defined(DEBUG) || defined(_DEBUG)
	create_flag = D3D11_CREATE_DEVICE_DEBUG;
	if_use_HIGHCARD = false;
#endif
	//~~~~~~~~~~~~~~~~~建立d3d裝置以及d3d裝置描述表
	HRESULT hr;
	if (if_use_HIGHCARD == true)
	{
		std::vector<IDXGIAdapter1*> vAdapters;
		IDXGIFactory1* factory;
		CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory);
		IDXGIAdapter1 * pAdapter = 0;
		DXGI_ADAPTER_DESC1 pancy_star;
		UINT i = 0;
		HRESULT check_hardweare;
		while (factory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND)
		{
			vAdapters.push_back(pAdapter);
			++i;
		}
		vAdapters[1]->GetDesc1(&pancy_star);
		hr = D3D11CreateDevice(vAdapters[1], D3D_DRIVER_TYPE_UNKNOWN, NULL, create_flag, 0, 0, D3D11_SDK_VERSION, &device_pancy, &leave_need, &contex_pancy);
		int a = 0;
	}
	else
	{
		hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, create_flag, 0, 0, D3D11_SDK_VERSION, &device_pancy, &leave_need, &contex_pancy);
	}
	if (FAILED(hr))
	{
		MessageBox(hwnd_need, L"d3d裝置建立失敗", L"提示", MB_OK);
		return false;
	}
	if (leave_need != D3D_FEATURE_LEVEL_11_0)
	{
		MessageBox(hwnd_need, L"顯示卡不支援d3d11", L"提示", MB_OK);
		return false;
	}
	//return true;
	//~~~~~~~~~~~~~~~~~~檢查是否支援四倍抗鋸齒
	device_pancy->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &check_4x_msaa);
	if (create_flag == D3D11_CREATE_DEVICE_DEBUG)
	{
		assert(check_4x_msaa > 0);
	}
	//~~~~~~~~~~~~~~~~~~設定交換鏈的緩衝區格式資訊
	DXGI_SWAP_CHAIN_DESC swapchain_format;//定義緩衝區結構體
	swapchain_format.BufferDesc.Width = width_need;
	swapchain_format.BufferDesc.Height = hight_need;
	swapchain_format.BufferDesc.RefreshRate.Numerator = 60;
	swapchain_format.BufferDesc.RefreshRate.Denominator = 1;
	swapchain_format.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	swapchain_format.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	swapchain_format.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	//設定緩衝區的抗鋸齒資訊
	if (check_4x_msaa > 0)
	{
		swapchain_format.SampleDesc.Count = 4;
		swapchain_format.SampleDesc.Quality = check_4x_msaa - 1;
	}
	else
	{
		swapchain_format.SampleDesc.Count = 1;
		swapchain_format.SampleDesc.Quality = 0;
	}
	swapchain_format.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;//渲染格式為渲染到緩衝區
	swapchain_format.BufferCount = 1;                              //僅使用一個緩衝區作為後臺快取
	swapchain_format.OutputWindow = hwnd_need;                     //輸出的視窗控制代碼
	swapchain_format.Windowed = true;                              //視窗模式
	swapchain_format.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;        //讓渲染驅動選擇最高效的方法
	swapchain_format.Flags = 0;                                    //是否全屏調整
	//~~~~~~~~~~~~~~~~~~~~~~~建立交換鏈
	IDXGIDevice *pDxgiDevice(NULL);
	HRESULT hr1 = device_pancy->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&pDxgiDevice));
	IDXGIAdapter *pDxgiAdapter(NULL);
	hr1 = pDxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&pDxgiAdapter));
	IDXGIFactory *pDxgiFactory(NULL);
	hr1 = pDxgiAdapter->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&pDxgiFactory));
	hr1 = pDxgiFactory->CreateSwapChain(device_pancy, &swapchain_format, &swapchain);
	//釋放介面  
	pDxgiFactory->Release();
	pDxgiAdapter->Release();
	pDxgiDevice->Release();
	change_size();
	return true;
}
當兩個基本的類被建立完畢之後,接下來我們要做的就是建立交換鏈和緩衝區了。那麼神馬是交換鏈,神馬又是緩衝區呢。下面我先講交換鏈。首先,交換鏈是一個比較著名的遊戲演算法“雙緩衝抗閃屏”。這個演算法不僅僅是在3D遊戲中會用到,以往的2D遊戲也會經常的用到。如果大家以前寫過一些2D的遊戲的話肯定已經接觸過這個演算法了。那麼這個演算法究竟有什麼用呢,我們可以看下面的圖片來講解:

這就是一個 模仿flappy bird的2D遊戲,現在我們來分析,在每一幀,我們應該怎麼給螢幕上繪製這些東西。這個時候有些人肯定會說了,這個還不簡單,先給螢幕上貼上背景,然後再一根根的把柱子畫上去,最後再把精靈貼上去不就齊活了........當然,這種做法可以達到目的,但是會出現一個問題,當幀率不是很高的時候,貼柱子,帖背景,貼精靈這些事情中間是有間隔的,那到最後觀眾就會感覺整個螢幕上一大堆東西在閃來閃去的。這就是所謂的“閃屏”。那麼怎麼解決這個問題呢? 很簡單,我們先把要貼的東西不直接貼到顯示屏上,先一個個的貼到一個後臺記憶體裡面,最後把這個記憶體直接一整張的貼到到螢幕上就可以了。這種演算法就叫做雙快取,也就是說我們準備一張後臺緩衝區,然後所有的繪製操作都繪製在它上面,最後再一股腦的把它的資料交換到顯示屏上。當然,在3D遊戲中,我們也是把每個模型的效果都先投影到這個螢幕上,然後再交換給前臺。這也就是交換鏈的作用,通過不停地把前後臺數據作交換來防止閃屏的出現。上面的程式碼裡面就是設定交換鏈緩衝區的程式碼,每一個引數的作用我都寫在後面的註釋裡了,大家可以看一看,其實主要就是告訴d3d後臺緩衝區的大小啊,格式啊這些資訊,大部分引數都不是很重要直接copy就好了。這裡給大家需要詳細介紹的是抗鋸齒,神馬是抗鋸齒呢,我們知道,無論是3D模型,還是投影之後的2D向量圖,都是不能直接在螢幕上顯示的,因此就需要一些演算法來進行向量->光柵圖的轉換,這一轉換就會出現一些邊緣的鋸齒現象。那麼如何才能把這些鋸齒的邊緣消除呢,當然最簡單的辦法是提升解析度。但是很明顯我們電腦的解析度一般不怎麼變。這裡一般都採用多重取樣抗鋸齒,也就是對於每一個畫素,轉換的時候看看他周圍畫素的光柵化情況來決定這個畫素是保持全部的顏色,還是隻保持一部分顏色。具體的演算法我就不詳細說了,很多圖形學課本上會講到,而且以後大家學到ssao或者shadow map的時候也會用到相關的演算法,到時候我們再詳談。那麼究竟光柵化的時候參考周圍的幾個畫素就成了一個問題,多了影響效率,少了影響效果。那麼這裡設定的抗鋸齒倍數就是根據機器的效能來決定了。現在大部分的電腦都是支援4倍抗鋸齒的,至於8倍或者16倍估計有些電腦就不支援了。不過抗鋸齒屬於一種效果不明顯但是消耗特別大的演算法,在玩遊戲的時候一般都是能不開儘量不開的。這裡作為參考,大家可以在程式裡面根據自己的喜好來設定這個引數。

當交換鏈完成之後,我們就只剩下最後一個步驟了,設定緩衝區以及視口了。下面最後一部分的程式碼:

	//~~~~~~~~~~~~~~~~~~~~~~~建立檢視資源
	ID3D11Texture2D *backBuffer = NULL;
	//獲取後緩衝區地址 
	HRESULT hr;
	hr = swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backBuffer));
	//建立檢視
	if (FAILED(hr))
	{
		MessageBox(wind_hwnd, L"change size error", L"tip", MB_OK);
		return false;
	}
	hr = device_pancy->CreateRenderTargetView(backBuffer, 0, &m_renderTargetView);
	if (FAILED(hr))
	{
		MessageBox(wind_hwnd, L"change size error", L"tip", MB_OK);
		return false;
	}
	//釋放後緩衝區引用  
	backBuffer->Release();
	//~~~~~~~~~~~~~~~~~~~~~~~建立深度及模板緩衝區
	D3D11_TEXTURE2D_DESC dsDesc;
	dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
	dsDesc.Width = wind_width;
	dsDesc.Height = wind_hight;
	dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	dsDesc.MipLevels = 1;
	dsDesc.ArraySize = 1;
	dsDesc.CPUAccessFlags = 0;
	dsDesc.MiscFlags = 0;
	dsDesc.Usage = D3D11_USAGE_DEFAULT;
	if (check_4x_msaa > 0)
	{
		dsDesc.SampleDesc.Count = 4;
		dsDesc.SampleDesc.Quality = check_4x_msaa - 1;
	}
	else
	{
		dsDesc.SampleDesc.Count = 1;
		dsDesc.SampleDesc.Quality = 0;
	}
	ID3D11Texture2D* depthStencilBuffer;
	device_pancy->CreateTexture2D(&dsDesc, 0, &depthStencilBuffer);
	device_pancy->CreateDepthStencilView(depthStencilBuffer, 0, &depthStencilView);
	depthStencilBuffer->Release();
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~繫結檢視資訊到渲染管線
	contex_pancy->OMSetRenderTargets(1, &m_renderTargetView, depthStencilView);
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~設定視口變換資訊
	viewPort.Width = static_cast<FLOAT>(wind_width);
	viewPort.Height = static_cast<FLOAT>(wind_hight);
	viewPort.MaxDepth = 1.0f;
	viewPort.MinDepth = 0.0f;
	viewPort.TopLeftX = 0.0f;
	viewPort.TopLeftY = 0.0f;
	contex_pancy->RSSetViewports(1, &viewPort);
	return true;

先說緩衝區,其實之前我們設定交換鏈的時候設定的後臺緩衝區也算是一種緩衝區,所謂緩衝區其實可以看做就是一個跟螢幕大小一樣的二維陣列。而後臺緩衝區的作用就是在每幀儲存並積累每個光柵資訊,最後將儲存的資訊交換到顯示螢幕上。而在這裡需要我們設定的緩衝區就一個,就是深度+模板緩衝區。這時候大家估計又要問了,我去你這到底是一個緩衝區還是兩個緩衝區啊,說是一個咋後面變倆了。當然,這是一個緩衝區,但是被拆開來做兩個用處了,每個畫素資料的前24位用來做深度緩衝區,後面8位作為模板緩衝區。前者用於做深度測試,後者用於做模板測試。我們先說什麼是深度測試,我們都知道投影的時候,如果前一個物體在投影方向上遮住了後面一個物體,那麼最終出現在投影螢幕上的只應該是前面的物體,如何表達這種遮擋關係呢,這裡我們就在投影物體到螢幕上的時候順便記錄下螢幕上這一點的至今為止最近的深度,然後根據深度來判斷這個物體是否應該被投影下來。而深度緩衝區就是做這個記錄的工作的,這樣我們才能正確的表達物體之間的深度關係。而模板緩衝區是為使用者預留的一個緩衝區,和深度緩衝區不同,我們依靠這個緩衝區可以隨意根據我們的想法來決定什麼物體應該被投影到螢幕上,一般來說這個緩衝區很少用到,比較著名的用法就是shadow valume陰影體演算法。
最後就是介紹視口,其實這個概念比較簡單,我們的投影演算法是把三維世界整個投影到一個x∈[-1,1],y∈[-1,1]的一張虛擬圖片上,之後我們要把這個虛擬圖片的向量資訊光柵化就需要知道整個遊戲視窗的大小,也就是我們一開始建立的視窗的寬高。只要設定了這個寬高硬體就能進行向量->光柵座標的轉換了,非常的容易。

那麼,當上面這些步驟完成的時候,其實整個基本的directx就算是註冊完畢了。接下來就是在winmain函式的訊息迴圈裡面不斷地繪製,更新,並且重新整理螢幕畫素就好了。winmain部分的重新整理程式碼在第一段程式碼裡面就已經給出了,可以看到無論是否有訊息,我們都強制螢幕進行一次繪製,也就是說我們不會去等待windows訊息的回撥來決定神馬時候繪製。一般來說,我們設計的遊戲分成update和display兩個部分,前一部分負責更新遊戲的資訊,後一部分根據更新的資訊進行繪製。這一節我們只是講解如何註冊並初始化d3d11,所以我們神馬都不需要繪製,只要把螢幕每一幀都清成紅色就可以了。下面是display裡面的程式碼:非常簡單,每次繪製之前,把我們之前註冊的兩個緩衝區先清空了(後臺緩衝,深度/模板緩衝)。然後在下面就可以進行我們的渲染工作,最後呼叫交換鏈把緩衝區的資訊交換到螢幕就可以了。

void d3d_pancy_1::display()
{
	//初始化
	XMVECTORF32 color = { 1.0f,0.0f,0.0f,1.0f };
	contex_pancy->ClearRenderTargetView(m_renderTargetView, reinterpret_cast<float*>(&color));
	contex_pancy->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.f, 0);
	//交換到螢幕
	HRESULT hr = swapchain->Present(0, 0);
	int a = 0;
}
非常簡單,每次繪製之前,把我們之前註冊的兩個緩衝區先清空了(後臺緩衝,深度/模板緩衝)。然後在下面就可以進行我們的渲染工作,最後呼叫交換鏈把緩衝區的資訊交換到螢幕就可以了。


最後的效果就是這個樣子,只要清空出一個紅色的螢幕出來就算是成功啦。

上述就是整個d3d初始化的方式,而openGL的話初始化的方法跟directx基本上差別不是很大。比較大的區別呢是OpenGl因為不是windows自帶的API,所以不能根據簡單的根據視窗控制代碼來找到windows的渲染裝置,這就得先獲取windows的裝置描述表HDC,然後封裝成自己的描述表RC才能成功註冊,因為之前說過教程以directx為主,所以在這裡就不繼續細講OpenGL了,但是我會把我寫的OpenGL初始化的程式碼給大家用於參考和學習。下面是本次教程的程式碼地址: