轉載請注明出處:http://www.cnblogs.com/Ray1024
一、概述
在前面的幾篇文章中,我們詳細介紹了Direct3D渲染所需要的數學基礎和渲染管道理論知識。從這篇文章開始,我們就正式開始Direct3D的繪制學習過程了。這篇文章中,主要講解Direct3D的繪制基礎過程,介紹配置渲染管道,定義頂點和像素着色器以及將幾何圖形提交到渲染管道進行繪制所需的Direct3DAPI接口和方法。
本文通過繪制一個彩色立方體來演示Direct3D的渲染過程,這個例子本身很簡單,但是清晰的包含了Direct3D的渲染基本步驟。因為繪制過程中涉及到Direct3D的API接口和方法,我們將在學習彩色立方體的繪制過程中詳細介紹這些API接口和方法。
二、繪圖基礎
2.1 創建頂點緩沖
在D3D中,頂點由空間位置和各種附加屬性組成。定義頂點結構體如下,由空間位置和顏色組成,我們這個例子中使用的結構體就是它:
struct Vertex { XMFLOAT3 Pos; XMFLOAT4 Color; };
為了讓GPU訪問頂點數組,我們必須把它放在頂點緩沖(vertex buffer)中,該容器由ID3D11Buffer接口表示。要創建一個頂點緩沖,我們必須執行以下步驟:
(1)填寫一個D3D11_BUFFER_DESC結構體,描述我們所要創建的緩沖區;
(2)填寫一個D3D11_SUBRESOURCE_DATA結構體,為緩沖區指定初始化數據;
(3)調用ID3D11Device::CreateBuffer方法來創建緩沖區。
D3D11_BUFFER_DESC結構體的定義如下:
typedef struct D3D11_BUFFER_DESC{ UINT ByteWidth; // 將要創建的頂點緩沖區的大小,單位為字節 D3D11_USAGE Usage; // 一個用於指定緩沖區用途的D3D11_USAGE枚舉類型成員 UINT BindFlags; // 對於頂點緩沖區,該參數應設為D3D11_BIND_VERTEX_BUFFER UINT CPUAccessFlags; // 指定CPU對資源的訪問權限 UINT MiscFlags; // 不需要為頂點緩沖區指定任何雜項(miscellaneous)標志值,所以該參數設為0 UINT StructureByteStride; // 存儲在結構化緩沖中的一個元素的大小,以字節為單位。這個屬性只用於結構化緩沖,其他緩沖可以設置為0 } D3D11_BUFFER_DESC;
D3D11_SUBRESOURCE_DATA結構體的定義如下:
typedef struct D3D11_SUBRESOURCE_DATA { const void *pSysMem; // 包含初始化數據的系統內存數組的指針 UINT SysMemPitch; // 頂點緩沖區不使用該參數 UINT SysMemSlicePitch; // 頂點緩沖區不使用該參數 } D3D11_SUBRESOURCE_DATA;
下面的代碼創建了一個只讀的頂點緩沖區,並以中心在原點上的立方體的8個頂點來初始化該緩沖區。之所以說該緩沖區是只讀的,是因為當立方體創建后相關的幾何數據從不改變——始終保持為一個立方體。另外,我們為每個頂點指定了不同的顏色,顏色類型為D3D11Util.h文件中的類型。
/************************************************************************/ /* 1.創建頂點緩沖 */ /************************************************************************/ Vertex vertices[] = { { XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White }, { XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black }, { XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red }, { XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green }, { XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue }, { XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow }, { XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan }, { XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta } }; // 准備結構體,描述緩沖區 D3D11_BUFFER_DESC vbd; vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof(Vertex) * 8; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; vbd.MiscFlags = 0; vbd.StructureByteStride = 0; // 准備結構體,為緩沖區指定初始化數據 D3D11_SUBRESOURCE_DATA vinitData; vinitData.pSysMem = vertices; // 創建緩沖區 HR(m_pD3DDevice->CreateBuffer(&vbd, &vinitData, &m_pBoxVB));
2.2 創建索引緩沖
由於索引要由GPU訪問,所以它們必須放在一個特定的資源容器中,該容器稱為索引緩沖(index buffer)。創建索引緩沖的過程與創建頂點緩沖的過程非常相似,只不過索引緩沖存儲的是索引而非頂點。所以,這里不再贅述之前討論過的內容,我們直接給出一個創建索引緩沖區的示例:
/************************************************************************/ /* 2.創建索引緩沖 */ /************************************************************************/ UINT indices[] = { // 前表面 0, 1, 2, 0, 2, 3, // 后表面 4, 6, 5, 4, 7, 6, // 左表面 4, 5, 1, 4, 1, 0, // 右表面 3, 2, 6, 3, 6, 7, // 上表面 1, 5, 6, 1, 6, 2, // 下表面 4, 0, 3, 4, 3, 7 }; // 准備結構體,描述緩沖區 D3D11_BUFFER_DESC ibd; ibd.Usage = D3D11_USAGE_IMMUTABLE; ibd.ByteWidth = sizeof(UINT) * 36; ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; ibd.CPUAccessFlags = 0; ibd.MiscFlags = 0; ibd.StructureByteStride = 0; // 准備結構體,為緩沖區指定初始化數據 D3D11_SUBRESOURCE_DATA iinitData; iinitData.pSysMem = indices; // 創建緩沖區 HR(m_pD3DDevice->CreateBuffer(&ibd, &iinitData, &m_pBoxIB));
2.3 頂點着色器和像素着色器
着色器使用一種稱為高級着色語言(HLSL)的腳本語言來編寫。在此系列中,我們將根據貫穿本書的每個演示程序所涉及的技術講解相關的HLSL概念。着色器通常保存在一種稱為effect文件(.fx)的純文本文件中。我們會在下一篇文章中討論effect文件,而現在我們主要討論頂點着色器和像素着色器。
頂點着色器(Vertex Shader)和像素着色器(Pixel Shader)是Direct3D渲染中必不可少的最基本的Shader。
(1)頂點着色器
先看一個頂點着色器的代碼:
cbuffer cbPerObject { float4x4 gWorldViewProj; }; struct VertexIn { float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexOut { float4 PosH : SV_POSITION; float4 Color : COLOR; }; VertexOut VS(VertexIn vin) { VertexOut vout; // 轉換到齊次剪裁空間 vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj); // 將頂點顏色直接傳遞到像素着色器 vout.Color = vin.Color; return vout; }
VertexIn是一個輸入結構,"POSITION”和"COLOR"用來對成員變量進行語義說明,"POSITION"指位置坐標,"COLOR"指頂點的顏色。這些語義說明在創建輸入布局時會用,並且必須要一致,這個在本文稍后介紹InputLayout時還會解釋。
VertexOut是輸出結構,注意PosH成員的語義說明SV_POSITION是固定的,代表系統值,在像素着色器階段會需要這個值來進行裁剪操作。除了系統值必須固定外,其他的語義值都是可以任意指定的。
頂點着色器是一個稱為VS的函數(可以指定任何有效的函數名),其參數為輸入頂點結構,輸出為相應的輸出頂點結構。上面這個頂點着色器的功能就是坐標變換和顏色的簡單傳遞。最常見的頂點着色器即對頂點的位置坐標、紋理坐標、法線等信息進行變換,並返回新的頂點,傳遞給下一個階段。一般針對頂點着色器的輸入與輸出,應該各指定相應的結構來存儲相應的頂點信息。
(2)像素着色器
像素着色器是針對逐個像素進行的。在光柵化階段,一個三角形在其所覆蓋的每個像素處使用插值來計算相應頂點的各個屬性,然后把插值后的頂點傳遞給像素着色器。在簡單的沒有Geometry Shader和Tessellator時,頂點着色器的輸出就是像素着色器的輸入,像素着色器最終的輸出是該像素處對應片段的顏色值。如下即是一個最簡單的像素着色器函數:
float4 PS(VertexOut pin) : SV_Target { return pin.Color; }
該函數參頂點着色器輸出結構作為參數,輸出為float4類型,注意函數名后面的語義說明:SV_TARGET,它是函數返回值的說明,顯然這也是一個系統值,不可更改,它作為相應片段的顏色值傳遞給下一個階段。
(3)常量緩沖
上面的代碼中定義了一個稱為cbPerObject的cbuffer對象(constant buffer,常量緩沖)。常量緩沖只是一個用於存儲各種變量的數據塊,這些變量可以由着色器來訪問。頂點着色器不能修改常量緩沖中的數據,但是C++程序可以通過effect框架在運行時修改常量緩沖中的內容。它為C++應用程序代碼和effect代碼提供了一種有效的通信方式。例如,因為每個物體的世界矩陣各不相同,所以每個物體的“WVP”組合矩陣也各不相同;所以,當使用上述頂點着色器繪制多個物體時,我們必須在繪制每個物體前修改gWorldViewProj變量。
通常的建議是根據變量修改的頻繁程度創建不同的常量緩沖,對常量緩沖進行分組是為了提高運行效率。當一個常量緩沖區被更新時,它里面的所有變量都會同時更新;所以,根據它們的更新頻率進行分組,可以減少不必要的更新操作,提高運行效率。
2.4 編譯着色器,創建Effect
(1)Effect框架介紹
Effect框架是微軟提供的開源代碼庫,用來把shader代碼和相應的渲染狀態合理的組織到一起,來實現一個針對性的特效。關於編譯Effect及相應的配置IDE的詳細步驟,請參見此系列的第一篇文章《Direct3D11學習:(一)開發環境配置》。
一個Effect至少包含一個頂點着色器、一個像素着色器及所需要的全局變量。此外,還包含technique11和pass,我們這里簡單介紹一下,下篇文章將詳細介紹。
technique11:一個特效可以通過多種不同的方法實現,每個方法可以作為一個technique11。
pass:每個technique11至少包含一個pass,一個pass至少包含一個頂點着色器和一個像素着色器及其相應渲染狀態。一個pass即是對特效的一次渲染。
在C++程序中,Effect對應的接口為ID3DX11Effect,technique11為ID3D11EffectTechnique。
(2)編譯着色器
編譯在.fx文件中的着色器程序可以創建一個Effect,編譯步驟如下:
// 編譯着色器程序 DWORD shaderFlags = 0; #if defined( DEBUG ) || defined( _DEBUG ) shaderFlags |= D3D10_SHADER_DEBUG; shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION; #endif ID3D10Blob* compiledShader = 0; ID3D10Blob* compilationMsgs = 0; HRESULT hr = D3DX11CompileFromFile(L"FX/color.fx", 0, 0, 0, "fx_5_0", shaderFlags, 0, 0, &compiledShader, &compilationMsgs, 0); // compilationMsgs中包含錯誤或警告信息 if( compilationMsgs != 0 ) { MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0); ReleaseCOM(compilationMsgs); } // 就算沒有compilationMsgs,也需要確保沒有其他錯誤 if(FAILED(hr)) { DXTrace(__FILE__, (DWORD)__LINE__, hr, L"D3DX11CompileFromFile", true); } // 創建Effect HR(D3DX11CreateEffectFromMemory(compiledShader->GetBufferPointer(), compiledShader->GetBufferSize(), 0, m_pD3DDevice, &m_pFX)); // 編譯完成釋放資源 ReleaseCOM(compiledShader); // 從Effect中獲取technique對象 m_pTech = m_pFX->GetTechniqueByName("ColorTech");
此外,作為C++程序與Shader程序的銜接,即更新Shader中的全局變量,在C++中定義了各種類型的接口:ID3DX11EffectMatrixVariable、ID3DX11EffectVectorVariable、ID3DX11EffectVariable等,分別對應Shader中的float4x4類型、float4類型及raw數據,即無類型。這些接口通過如下方式獲得:
g_fxWorldViewProj = g_effect->GetVariableByName("g_worldViewProj")->AsMatrix();
這里的“g_worldViewProj”即shader中的全局變量名字。之后就可以在C++程序中通過更改g_fxWorldViewProj接口來更新shader中相應的全局變量了:
g_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
2.5 創建輸入布局
在Direct3D中,頂點由空間位置和各種附加屬性組成,Direct3D可以讓我們靈活地建立屬於我們自己的頂點格式;換句話說,它允許我們定義頂點的分量。
我們使用上面定義的結構體Vertex。有了頂點結構體之后,我們必須設法描述該頂點結構體的分量結構,使Direct3D知道該如何使用每個分量。這一描述信息是以輸入布局(ID3D11InputLayout)的形式提供給Direct3D的。輸入布局是一個D3D11_INPUT_ELEMENT_DESC數組。D3D11_INPUT_ELEMENT_DESC數組中的每個元素描述了頂點結構體的一個分量。比如,當頂點結構體包含兩個分量時,對應的D3D11_INPUT_ELEMENT_DESC數組會包含兩個元素。我們將D3D11_INPUT_ELEMENT_DESC稱為輸入布局描述(input layout description)。D3D11_INPUT_ELEMENT_DESC結構體定義如下:
typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; //元素相關的字符串。它可以是任何有效的語義名。語義用於將頂點結構體中的元素映射為頂點着色器參數 UINT SemanticIndex; //附加在語義上的索引值。例如:當頂點結構體包含多組紋理坐標時,我們不是添加一個新的語義名,而是在語義名的后面加上一個索引值。 DXGI_FORMAT Format; // 一個用於指定元素格式的DXGI_FORMAT枚舉類型成員 UINT InputSlot; // 指定當前元素來自於哪個輸入槽(input slot)。 // Direct3D支持16個輸入槽(索引依次為 0到15),通過這些輸入槽我們可以向着色器傳入頂點數據。 // 如:當一個頂點由位置和顏色元素組成,既可以使用一個輸入槽傳送兩種元素,也可以將兩種元素分開。 // Direct3D可以將來自於不同輸入槽的元素重新組合為頂點。 UINT AlignedByteOffset // 對於單個輸入槽來說,該參數表示從頂點結構體的起始位置到頂點元素的起始位置之間的字節偏移量。 D3D11_INPUT_CLASSIFICATION InputSlotClass; // 目前指定為D3D11_INPUT_PER_VERTEX_DATA;其他選項用於高級實例技術。 UINT InstanceDataStepRate; // 目前指定為0;其他值只用於高級實例技術。 } D3D11_INPUT_ELEMENT_DESC;
接下來我們就可以創建輸入布局了,下面是代碼:
/************************************************************************/ /* 5.創建輸入布局 */ /************************************************************************/ // 頂點輸入布局描述 D3D11_INPUT_ELEMENT_DESC vertexDesc[] = { {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0} }; // 從technique對象中獲取pass信息 D3DX11_PASS_DESC passDesc; m_pTech->GetPassByIndex(0)->GetDesc(&passDesc); // 創建頂點輸入布局 HR(m_pD3DDevice->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, &m_pInputLayout));
上面的代碼中使用了上一節介紹的Effect框架,通過它獲取相應technique11中相應的pass的描述信息。
2.6 更新矩陣變換
在這個例子中,用鼠標點擊拖動改變攝像機的視角和遠近,在UpdateScene函數中計算變換矩陣,並更新到shader中去。這里就不詳細介紹了,有興趣的朋友請看源碼。
void D3D11BoxDemoApp::UpdateScene(float dt) { /************************************************************************/ /* 6.更新每幀的矩陣變換 */ /************************************************************************/ // 視角變換矩陣 // 將球面坐標轉換為笛卡爾坐標 float x = m_radius*sinf(m_phi)*cosf(m_theta); float z = m_radius*sinf(m_phi)*sinf(m_theta); float y = m_radius*cosf(m_phi); XMVECTOR pos = XMVectorSet(x, y, z, 1.0f); XMVECTOR target = XMVectorZero(); XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); XMMATRIX V = XMMatrixLookAtLH(pos, target, up); XMStoreFloat4x4(&m_view, V); // 把三個變換相乘,合成一個 XMMATRIX world = XMLoadFloat4x4(&m_world); XMMATRIX view = XMLoadFloat4x4(&m_view); XMMATRIX proj = XMLoadFloat4x4(&m_proj); XMMATRIX worldViewProj = world*view*proj; // 通過C++程序更新Shader相應的變量 m_pFXWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj)); }
2.7 繪制場景
到這里我們就可以開始繪制操作了。
繪制場景的步驟為:
a.清屏,清空深度/模版緩沖區;
b.指定輸入布局、圖元拓撲類型、定點緩沖、索引緩沖、渲染狀態等等;
c.從technique獲取pass並逐個渲染;
d.顯示。
具體代碼如下:
void D3D11BoxDemoApp::DrawScene() { /************************************************************************/ /* 7.場景繪制 */ /************************************************************************/ // 清屏 m_pD3DImmediateContext->ClearRenderTargetView(m_pRenderTargetView, reinterpret_cast<const float*>(&Colors::LightSteelBlue)); m_pD3DImmediateContext->ClearDepthStencilView(m_pDepthStencilView, D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0); // 指定輸入布局、圖元拓撲類型、頂點緩沖、索引緩沖、渲染狀態 m_pD3DImmediateContext->IASetInputLayout(m_pInputLayout); m_pD3DImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); UINT stride = sizeof(Vertex); UINT offset = 0; m_pD3DImmediateContext->IASetVertexBuffers(0, 1, &m_pBoxVB, &stride, &offset); m_pD3DImmediateContext->IASetIndexBuffer(m_pBoxIB, DXGI_FORMAT_R32_UINT, 0); // 是否使用線框模式 //m_pD3DImmediateContext->RSSetState(m_pWireframeRS); // 從technique獲取pass並逐個渲染 D3DX11_TECHNIQUE_DESC techDesc; m_pTech->GetDesc( &techDesc ); for(UINT p = 0; p < techDesc.Passes; ++p) { m_pTech->GetPassByIndex(p)->Apply(0, m_pD3DImmediateContext); m_pD3DImmediateContext->DrawIndexed(36, 0, 0);// 立方體有36個索引 } // 顯示 HR(m_pSwapChain->Present(0, 0)); }
到這里為止,我們的彩色立方體程序就完成了。這是運行效果圖:
需要源碼的朋友點此下載,源碼為4_D3DBoxDemo。
注意:上面代碼中使用了一個文件MathHelper.h中的內容,這個文件中定義了我們常用的數學工具(函數或宏),此文件也放在Common文件夾中。
三、結語
我們通過繪制一個彩色立方體程序演示了Direct3D的渲染過程,到這里我們自己可以根據繪制各種幾何形狀與着色或線框模式了。
這篇文章中我們簡單使用了Effect框架,下篇文章我們將詳細介紹它。