【Visual C++】遊戲開發筆記二十九 一步一步教你用優雅的Direct3D11程式碼畫一個三角形
本系列文章由zhmxy555(毛星雲)編寫,轉載請註明出處。
這個demo演示的效果是用Direct3D11在螢幕上渲染一個三角形,當然是通過這個demo進一步鞏固和學習Direct3D11,而不是單單為了畫一個三角形這麼簡單。正如之前所說,這個demo是建立在筆記二十八中講解的D3D BlankWindows Demo之上的。
那麼,我們就開門見山,直入正題吧。
有個別童鞋表示編譯原始碼時出現了“無法解析的外部符號”的一系列錯誤,這都是lib庫檔案沒新增造成的,這屬於我們的DirectX 11的開發環境沒有配置好~具體配置方法可以參考《Visual C++遊戲開發筆記》系列第25篇文章:
針對單個的程式的話,也可以在程式碼開頭#include語句附近使用pragma語句來新增庫檔案的~ 針對這個DirectX11 demo的話,在程式碼開頭新增以下幾句就好了:
#pragma comment(lib,"dxerr.lib") #pragma comment(lib,"d3d11.lib") #pragma comment(lib,"d3dx11.lib") #pragma comment(lib,"d3dcompiler.lib")
————————淺墨於 2012年 11月06日
一、 載入幾何體
我們知道,為了渲染幾何圖形,我們需要一個頂點快取,一個描述頂點佈局的輸入層,以及一系列的著色器,自DirectX10以來,著色器開始作為圖形渲染的基礎組成部分,在這個demo之中我們會指定頂點著色器與畫素著色器,渲染一種簡單的純色表面。後面我們將延伸的講解如何拓展使用這種效果來在表面對映圖形紋理。
下面就開始進行這個demo的書寫:
這個demo的核心內容當然是一個叫做TriangleDemo的類,我們為這個類定義幾個成員變數,他們分別是ID3D11VertexShader型別的取名為solidColorVS_的變數,一個ID3D11PixelShader型別的喚作solidColorPS的變數。一個ID3D11InputLayout型別的喚作inputLayout_的變數,以及一個ID3D11Buffer型別的叫做vertexBuffer_的變數。
下面就是TriangleDemo.h標頭檔案的原始碼,簡單的勾勒出了本文主角TriangleDemo類的輪廓:
程式碼段一 TriangleDemo.h標頭檔案
#include"Dx11DemoBase.h"class TriangleDemo : public Dx11DemoBase{public: TriangleDemo( ); virtual ~TriangleDemo( ); bool LoadContent( ); void UnloadContent( ); void Update( float dt ); void Render( );private: ID3D11VertexShader* solidColorVS_; ID3D11PixelShader* solidColorPS_; ID3D11InputLayout* inputLayout_; ID3D11Buffer* vertexBuffer_;};
頂點我們採用一個簡單的三分量式的浮點型結構體,在XNA Math library中一個叫做XMFLOAT3的結構體可以勝任這項殊榮。
接下來,開始豐富我們的TriangleDemo類,我們在程式碼段二中書寫頂點結構體VertexPos和TriangleDemo的類的建構函式以及解構函式
程式碼段二 TriangleDemo 頂點結構體, 建構函式和解構函式.
#include"TriangleDemo.h"#include<xnamath.h>struct VertexPos{ XMFLOAT3 pos;};TriangleDemo::TriangleDemo( ) : solidColorVS_( 0 ), solidColorPS_( 0 ),inputLayout_( 0 ), vertexBuffer_( 0 ){}TriangleDemo::~TriangleDemo( ){}
下面繼續豐富我們的TriangleDemo類,在程式碼段三中我們進行UnloadContent函式的書寫,顧名思義,UnloadContent是進行unload content工作的,與後面將書寫的LoadContent函式相對應。
程式碼段三 TriangleDemo類的UnloadContent函式的書寫
void TriangleDemo::UnloadContent( ){ if( solidColorVS_ ) solidColorVS_->Release( ); if( solidColorPS_ ) solidColorPS_->Release( ); if( inputLayout_ ) inputLayout_->Release( ); if( vertexBuffer_ ) vertexBuffer_->Release( ); solidColorVS_ = 0; solidColorPS_ = 0; inputLayout_ = 0; vertexBuffer_ = 0; }
順理成章的,下一步便是LoadContent函式的書寫。這個函式由頂點著色器載入,在檔案SolidGreenColor.fx中可以檢視。
一旦頂點著色器的原始碼編譯完成,著色器便建立一個CreateVertexShader函式的呼叫,我們接著建立頂點格式。由於頂點著色器與頂點格式相關聯,所以我們還需要將頂點著色器載入到記憶體中。
建立完頂點著色器和輸入格式後,下一步我們建立畫素著色器。下面這段程式碼實現了LoadContent方法的一半的功能:
程式碼段四 LoadContent函式著色器載入程式碼
bool TriangleDemo::LoadContent( ){ ID3DBlob* vsBuffer = 0; bool compileResult = CompileD3DShader( "SolidGreenColor.fx", "VS_Main", "vs_4_0", &vsBuffer ); if( compileResult == false ) { MessageBox( 0, "載入頂點著色器錯誤!", "編譯錯誤", MB_OK ); return false; } HRESULT d3dResult; d3dResult = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer( ),vsBuffer->GetBufferSize( ), 0, &solidColorVS_ );if( FAILED( d3dResult ) ){ if( vsBuffer ) vsBuffer->Release( ); return false;}D3D11_INPUT_ELEMENT_DESC solidColorLayout[] ={{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }};unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,totalLayoutElements, vsBuffer->GetBufferPointer( ),vsBuffer->GetBufferSize( ), &inputLayout_ );vsBuffer->Release( );if( FAILED( d3dResult ) ){ return false;}ID3DBlob* psBuffer = 0;compileResult = CompileD3DShader( "SolidGreenColor.fx","PS_Main", "ps_4_0", &psBuffer );if( compileResult == false ){ MessageBox( 0, "載入畫素著色器錯誤!", "編譯錯誤", MB_OK ); return false;}d3dResult = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),psBuffer->GetBufferSize( ), 0, &solidColorPS_ );psBuffer->Release( );if( FAILED( d3dResult ) ){ return false; }...//後接函式的下半段}
CompileD3DShader相關的程式碼在程式碼段五中進行了演繹,這段程式碼巧妙地被分離於LoadContent之外,這樣在載入多個不同的著色效果的時候便可以避免大段大段的冗餘程式碼:
程式碼段五 CompileShader 函式的實現方法
bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char*shaderModel, ID3DBlob** buffer ){ DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS; #if defined( DEBUG ) || defined( _DEBUG ) shaderFlags |= D3DCOMPILE_DEBUG; #endif ID3DBlob* errorBuffer = 0; HRESULT result; result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel, shaderFlags, 0, 0, buffer, &errorBuffer, 0 ); if( FAILED( result ) ) { if( errorBuffer != 0 ) { OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) ); errorBuffer->Release( ); } return false; } if( errorBuffer != 0 ) errorBuffer->Release( ); return true;}
上面我們介紹了上半段LoadContent函式的構成,而下半段LoadContent函式主要實現了頂點快取的建立。這段程式碼行文思路很明朗,首先定義一個簡單的三角形,沿X軸與Y軸都是0.5f(半個單位的長度)。Z軸依然設為為0.5f,來使此三角形可見。因為若鏡頭隔表面太近或者太遠,表面都不會成功的渲染。
頂點列表儲存於一個叫做vertices的陣列中,它提供了一個子資源資料,在CreateBuffer函式開始呼叫進行實際頂點快取的建立的時候,這些資料可以派上用場。
下面就是上面這段敘述的程式碼實現,LoadContent函式的下半部分書寫風格如下:
程式碼段六 LoadContent函式的幾何圖形載入程式碼
bool TriangleDemo::LoadContent( ){//前接函式的上半段 ... VertexPos vertices[] = { XMFLOAT3( 0.5f, 0.5f, 0.5f ), XMFLOAT3( 0.5f, -0.5f, 0.5f ), XMFLOAT3( -0.5f, -0.5f, 0.5f ) }; D3D11_BUFFER_DESC vertexDesc; ZeroMemory( &vertexDesc, sizeof( vertexDesc ) ); vertexDesc.Usage = D3D11_USAGE_DEFAULT; vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; vertexDesc.ByteWidth = sizeof( VertexPos ) * 3; D3D11_SUBRESOURCE_DATA resourceData; ZeroMemory( &resourceData, sizeof( resourceData ) ); resourceData.pSysMem = vertices; d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData, &vertexBuffer_ ); if( FAILED( d3dResult ) ) { return false; } return true;}
二、渲染幾何體
Direct11三角形Demo程式碼的最後兩部分由實現幾何渲染功能的程式碼和著色器本身構成。渲染幾何圖形的構成程式碼在TriangleDemo類中的Render函式中進行。函式中有
有一個條件語句,這樣可以確保在Direct3D的上下文是有效的。
接下來,我們清除渲染目標,並設定輸出程式集(input assembler)。而實際上,因為在這個demo之中的三角形是靜態的,我們並不一定非要清除渲染目標,這裡只是為了規範我們的程式碼書寫,以免養成不良的開發習慣。在輸出程式集階段的設定由我們已經建立的輸出結構(input layout)進行繫結,並提供頂點快取,設定拓撲三角形的列表。
下面貼出Render函式的書寫思路:
程式碼段七 TriangleDemo類的render函式書寫
void TriangleDemo::Render( ){ if( d3dContext_ == 0 ) return; float clearColor[4] = { 0.5, 0.5f, 0.5f, 1.0f }; //設定背景顏色 d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor ); unsigned int stride = sizeof( VertexPos ); unsigned int offset = 0; d3dContext_->IASetInputLayout( inputLayout_ ); d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset ); d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_ TRIANGLELIST ); d3dContext_->VSSetShader( solidColorVS_, 0, 0 ); d3dContext_->PSSetShader( solidColorPS_, 0, 0 ); d3dContext_->Draw( 3, 0 ); swapChain_->Present( 0, 0 );}
最後一部分要介紹的程式碼是著色器。籠統的來說,頂點著色器基於它得到的內容。詳細的來說,頂點著色器的作用是將內部得到的頂點位置傳遞到輸出處,之後,我們須處理這些資料,正確繪製出我們的圖形。但對於這個非常基礎的demo,僅僅進行頂點位置內容的傳遞就夠了。
如果沒有幾何圖形著色器繫結到輸出程式集之上,頂點著色器的輸出的資料就是畫素著色器的輸入的資料。其中,畫素著色器的輸出就是寫到輸出快取之中的顏色值。當交換鏈中的Present函式呼叫的時候,這個快取就會最終顯示給使用者。
TriangleDemo的頂點著色器和畫素著色器的書寫方法如下程式碼段八:
程式碼段八 Triangledemo著色器的實現程式碼
float4 VS_Main( float4 pos : POSITION ) : SV_POSITION{ return pos;}float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET{ return float4( 0.0f, 1.0f, 0.0f, 1.0f );}
這樣,Triangledemo類就隨著一步一步的勾勒,被我們書寫完成了。
三、Dx11DemoBase類的書寫
接下來,我們將之前講解的BlankD3DWindows Demo模板中的Dx11DemoBase類進行豐富和修改,即可得到適用於本節demo的Dx11DemoBase類。
程式碼段九 Dx11DemoBase.h
#ifndef _DEMO_BASE_H_#define _DEMO_BASE_H_ #include<d3d11.h>#include<d3dx11.h>#include<DxErr.h> class Dx11DemoBase{ public: Dx11DemoBase(); virtual ~Dx11DemoBase(); bool Initialize( HINSTANCE hInstance, HWND hwnd ); void Shutdown( ); bool CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer ); virtual bool LoadContent( ); virtual void UnloadContent( ); virtual void Update( float dt ) = 0; virtual void Render( ) = 0; protected: HINSTANCE hInstance_; HWND hwnd_; D3D_DRIVER_TYPE driverType_; D3D_FEATURE_LEVEL featureLevel_; ID3D11Device* d3dDevice_; ID3D11DeviceContext* d3dContext_; IDXGISwapChain* swapChain_; ID3D11RenderTargetView* backBufferTarget_;}; #endif
程式碼段十 Dx11DemoBase.cpp
#include"Dx11DemoBase.h"#include<D3Dcompiler.h> Dx11DemoBase::Dx11DemoBase( ) : driverType_( D3D_DRIVER_TYPE_NULL ), featureLevel_( D3D_FEATURE_LEVEL_11_0 ), d3dDevice_( 0 ), d3dContext_( 0 ), swapChain_( 0 ), backBufferTarget_( 0 ){ } Dx11DemoBase::~Dx11DemoBase( ){ Shutdown( );} bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd ){ hInstance_ = hInstance; hwnd_ = hwnd; RECT dimensions; GetClientRect( hwnd, &dimensions ); unsigned int width = dimensions.right - dimensions.left; unsigned int height = dimensions.bottom - dimensions.top; D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP, D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE }; unsigned int totalDriverTypes = ARRAYSIZE( driverTypes ); D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 }; unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels ); DXGI_SWAP_CHAIN_DESC swapChainDesc; ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) ); swapChainDesc.BufferCount = 1; swapChainDesc.BufferDesc.Width = width; swapChainDesc.BufferDesc.Height = height; swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapChainDesc.BufferDesc.RefreshRate.Numerator = 60; swapChainDesc.BufferDesc.RefreshRate.Denominator = 1; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.OutputWindow = hwnd; swapChainDesc.Windowed = true; swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0; unsigned int creationFlags = 0; #ifdef _DEBUG creationFlags |= D3D11_CREATE_DEVICE_DEBUG;#endif HRESULT result; unsigned int driver = 0; for( driver = 0; driver < totalDriverTypes; ++driver ) { result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags, featureLevels, totalFeatureLevels, D3D11_SDK_VERSION, &swapChainDesc, &swapChain_, &d3dDevice_, &featureLevel_, &d3dContext_ ); if( SUCCEEDED( result ) ) { driverType_ = driverTypes[driver]; break; } } if( FAILED( result ) ) { DXTRACE_MSG( "建立D3D裝置失敗!" ); return false; } ID3D11Texture2D* backBufferTexture; result = swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture ); if( FAILED( result ) ) { DXTRACE_MSG( "獲取交換鏈後臺快取失敗!" ); return false; } result = d3dDevice_->CreateRenderTargetView( backBufferTexture, 0, &backBufferTarget_ ); if( backBufferTexture ) backBufferTexture->Release( ); if( FAILED( result ) ) { DXTRACE_MSG( "建立渲染目標檢視失敗!" ); return false; } d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0 ); D3D11_VIEWPORT viewport; viewport.Width = static_cast<float>(width); viewport.Height = static_cast<float>(height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; d3dContext_->RSSetViewports( 1, &viewport ); return LoadContent( );} bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer ){ DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS; #if defined( DEBUG ) || defined( _DEBUG ) shaderFlags |= D3DCOMPILE_DEBUG;#endif ID3DBlob* errorBuffer = 0; HRESULT result; result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel, shaderFlags, 0, 0, buffer, &errorBuffer, 0 ); if( FAILED( result ) ) { if( errorBuffer != 0 ) { OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) ); errorBuffer->Release( ); } return false; } if( errorBuffer != 0 ) errorBuffer->Release( ); return true;} bool Dx11DemoBase::LoadContent( ){ // 進行相關過載 return true;} void Dx11DemoBase::UnloadContent( ){ // 進行相關過載} void Dx11DemoBase::Shutdown( ){ UnloadContent( ); if( backBufferTarget_ ) backBufferTarget_->Release( ); if( swapChain_ ) swapChain_->Release( ); if( d3dContext_ ) d3dContext_->Release( ); if( d3dDevice_ ) d3dDevice_->Release( ); backBufferTarget_ = 0; swapChain_ = 0; d3dContext_ = 0; d3dDevice_ = 0;}
四、 賦予程式生命——wWinMain函式的書寫
最後一步,依舊是主函式的書寫,這個函式大家應該是最熟悉的,任何想順利執行的C/C++程式中必不可少,而且這裡的書寫方式和前面demo中的也大同小異,淺墨在這裡就不多贅言了,程式碼如下:
程式碼段十一 wWinMain函式的書寫
#include<Windows.h>#include<memory>#include"TriangleDemo.h" LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow ){ UNREFERENCED_PARAMETER( prevInstance ); UNREFERENCED_PARAMETER( cmdLine ); WNDCLASSEX wndClass = { 0 }; wndClass.cbSize = sizeof( WNDCLASSEX ) ; wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = WndProc; wndClass.hInstance = hInstance; wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); wndClass.hbrBackground = ( HBRUSH )( COLOR_WINDOW + 1 ); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "DX11BookWindowClass"; if( !RegisterClassEx( &wndClass ) ) return -1; RECT rc = { 0, 0, 640, 480 }; AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE ); HWND hwnd = CreateWindowA( "DX11BookWindowClass", "Direct3D11三角形demo", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL ); if( !hwnd ) return -1; ShowWindow( hwnd, cmdShow ); TriangleDemo demo; // Demo 的初始化 bool result = demo.Initialize( hInstance, hwnd ); if( result == false ) return -1; MSG msg = { 0 }; while( msg.message != WM_QUIT ) { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } // 更新和繪製 demo.Update( 0.0f ); demo.Render( ); } // Demo 關閉 demo.Shutdown( ); return static_cast<int>( msg.wParam );} LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ){ PAINTSTRUCT paintStruct; HDC hDC; switch( message ) { case WM_PAINT: hDC = BeginPaint( hwnd, &paintStruct ); EndPaint( hwnd, &paintStruct ); break; case WM_DESTROY: PostQuitMessage( 0 ); break; default: return DefWindowProc( hwnd, message, wParam, lParam ); } return 0;}
用上面這些程式碼就可以在螢幕上輸出一個綠色的三角形,原始碼以及工程檔案在文章末尾有連結提供打包下載。
最後得到這個DirectX11三角形demo的效果圖:
文章最後和大家講些題外話,這是淺墨回國度假後專欄的第一次更新,回國度假到8月25號。
因為歐洲與國內的五個小時時差問題,回家之後一直在倒時差,白天睡的香,晚上睡不著,剛開始還是比較難受的- -,目前基
本上生物鐘正常了,可以早上起來多鍛鍊身體和看書寫程式碼了。
關於後面接著幾篇文章的更新,因為淺墨回國了,更新時間會有所更改,暫時定在每週週一的中午。
感謝一直支援【Visual C++】遊戲開發筆記系列專欄的朋友們。
【Visual C++】遊戲開發 系列文章才剛剛展開一點而已,因為遊戲世界實在是太博大精深了~
但我們不能著急,得慢慢打好基礎。做學問最忌好高騖遠,不是嗎?
淺墨希望看到大家的留言,希望與大家共同交流,希望得到睿智的評論(即使是批評)。
你們的支援是我寫下去的動力~
精通遊戲開發的路還很長很長,非常希望能和大家一起交流,共同學習,共同進步。
大家看過後覺得值得一看的話,可以頂一下這篇文章,你們的支援是我繼續寫下去的動力~
如果文章中有什麼疏漏的地方,也請大家指正。也希望大家可以多留言來和我探討相關的問題。
最後,謝謝你們一直的支援~~~
——————————淺墨於2012年7月23日