把「Direct3D與OpenGL的繪圖管線(上)」的圖拿出來看一下。
這次要做的是把左邊列的東西一樣一樣建出來,除了constant buffer、貼圖和sampler以外,本篇先不用到貼圖。
本篇重點是將頂點資料和shader傳給GPU。D3D9不一定要用到shader,而且有一個函式DrawPrimitiveUP()可以「上傳頂點資料→繪製」兩個工作同時做。但D3D10以後就一定要寫vertex shader和pixel shader,且「要求顯卡少女配置記憶體空間→上傳頂點資料→繪製」必須手動一步一步做,所以初學階段就得學習怎麼做這兩件事。
因為本篇新介紹的東西很多,在此把vertex buffer和input layout的一些細節略過,先寫個能用的程式再說,之後教到貼圖和shader輸入輸出時再介紹。
之前的教學可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
大部分和「Direct3D 11初始化」相同,不同的地方如下。
QueryUnbiasedInterruptTime()傳回的是電腦開機後到現在的時間,這個函式要Windows 7以上才有,之前的Windows有另一個函式「DWORD timeGetTime()」,傳回的單位是毫秒,但筆者有預留以後做Windows app的空間,timeGetTime()在Windows app不能用,所以改用QueryUnbiasedInterruptTime()。
Windows有其他時間函式可以量到小於毫秒,如QueryPerformanceCounter(),但sleep函式最細只能計時到毫秒,測量到比毫秒細也沒用。
題外話:時間超過整數能儲存的最大值會溢位(從0重新開始增加),DWORD型態毫秒可以計時到49.7天,Windows 95、98因為計時器溢位的問題開49.7天不關機就會當掉,之後的Windows有修正這個問題。
QueryUnbiasedInterruptTime()單位為100奈秒,用64位元整數儲存,溢位需要58000年以上。
那如果照本篇的寫法溢位了會怎樣?電腦計算整數加減法有環繞的現象,如果是32位元整數,0xffffffff過16ms會變成15,計算15-0xffffffff仍然可以得知經過16ms,不會出問題。
還多了這部分。
寫一組最簡單的shader,vertex shader把坐標原封不動傳給rasterizer,pixel shader把像素塗成白色,shader的輸出入、語法等等的之後另寫一篇介紹。
這裡把vertex shader和pixel shader寫在同一個字串裡,D3D有能力從裡面區分兩者,下面會看到方法。
三個頂點坐標是(0, 0.8)、(0.8, 0)、(-0.5, -0.5),以前有講過D3D和OpenGL的畫面坐標是-1 ~ 1,可以預想畫出的三角形是這樣。
initD3D()和之前大同小異。
之後的initSettings()是重點,要一步一步看
-Vertex & Pixel Shader-
先從shader開始,如果shader有錯就直接結束程式。
shader是給GPU執行的程式,跟C/C++一樣要先把程式碼編譯成binary才能給GPU使用,D3D shader的程式語言稱為HLSL,要先把code編譯成一種中間碼(byte code)再傳給驅動程式。
編譯方式有兩種:
1. 把原始碼附在程式裡,執行時使用D3DCompile()編譯。
2. 用一個工具:fxc.exe事先編譯,發佈程式時只要附byte code。
本篇用第一種,第二種以後再介紹。
MSDN的D3DCompile()說明
本篇用到的參數如下:
1:程式碼。
2:程式碼長度(byte數)。
3:shader名稱,程式執行不會用到,只用在顯示錯誤訊息,不需要可填NULL。
6:程式進入點。C/C++規定程式從main()或WinMain()開始執行,HLSL沒規定程式起點,而是用這個參數指定。
因此可以把vertex和pixel shader寫在同一個字串,用這個參數告訴compiler哪個函式是哪個shader。
7:target,指定shader版本號和種類。
版本號要跟「Direct3D 11初始化」提到的feature level對應,如果程式預定最低支援D3D10的晶片就填vs_4_0或ps_4_0,這樣如果shader裡用到D3D11以後的東西會視為error。
所有能填的值看這篇 MSDN : Specifying Compiler Targets
第10、11參數是傳回值,把byte code和錯誤訊息存在兩個ID3DBlob物件,可以如上印出錯誤訊息得知shader哪裡寫錯。
編譯出byte code之後用device->CreateVertexShader()和device->CreatePixelShader(),驅動程式會將byte code轉換成晶片固有的指令,並且用最後一個參數傳回代表這個shader的物件。
psBlob不會再用到,呼叫Release()將它刪除,vsBlob之後還會用到,先不刪除。
最後還要用context->VSSetShader()和context->PSSetShader()套用物件,如「Direct3D與OpenGL的繪圖管線(下)」所說,可以想成是叫顯卡少女把物件放在工作臺上,之後呼叫Draw()就會套用這兩個shader。
-Vertex buffer-
再來把頂點坐標從主記憶體傳給顯卡少女,先告訴顯卡少女要配置幾byte的空間,再將資料傳給她。
D3D11_BUFFER_DESC說明
ID3D11Device::CreateBuffer()說明
ID3D11DeviceContext::IASetVertexBuffers()說明
D3D11裡只要配置一塊記憶體(建立buffer或texture物件)都要填以下欄位:
ByteWidth:byte數。
Usage與BindFlags:這裡D3D11_USAGE_IMMUTABLE代表資料會在建立物件時一併上傳,之後不可修改,D3D11_BIND_VERTEX_BUFFER代表這一塊空間要儲存頂點資料,其他還能填什麼值之後再介紹。
D3D11_SUBRESOURCE_DATA:如果想在建立物件同時把資料上傳,需要填這個struct。
IASetVertexBuffers()第二參數是陣列長度,第三~第五參數可以給ID3D11Buffer*或UINT的陣列,頂點資料可以來自兩個以上的buffer物件,以後講到貼圖坐標會示範這些參數的用法。
-Input assembler-
之後都是設定值,不用把資料從CPU傳到顯示記憶體就比較簡單了。D3D11是把同一個步驟的設定值都包在一個物件裡,之後想調整設定就是套用物件。
建立物件大多是這樣的步驟:
1. 將設定值填入一個struct。
2. 把struct傳入device->CreateXXXX()產生物件,函式本身傳回錯誤碼代表是否成功,物件用指標傳回。
3. 呼叫context的成員函式套用物件。
物件建好之後就不可修改,如果畫不同物體想用不同設定值,要事先建立好幾個物件看情況套用。
不過struct裡有些目前沒用到的功能也必須填,所以以下會看到一些目前還沒教到的東西。
「Direct3D與OpenGL的繪圖管線(上)」講過,把頂點資料傳給顯卡少女時,她看到的只是一堆byte,還要告訴她資料是什麼格式。D3D的方法是將資訊填入struct D3D11_INPUT_ELEMENT_DESC再建立InputLayout物件,有幾項資料就要填幾個struct,本篇每個頂點只有位置一項資料,只要填一個struct。
D3D11_INPUT_ELEMENT_DESC說明
ID3D11Device::CreateInputLayout()說明
本篇重要的欄位只有Format一個,表示向量有幾個分量、每個分量幾byte等等,所有能填的值跟initD3D()裡framebuffer的顏色格式一樣,看這篇。
DXGI_FORMAT enumeration說明
這個例子位置有x,y二個分量,分量是32 bit float型態,填DXGI_FORMAT_R32G32_FLOAT。
有沒有覺得奇怪,這個值是明明是坐標,為何常數名稱是RGBA?以前fixed-function pipeline的時代坐標和顏色要區分開,但有了shader之後很多事都是人寫程式控制了。顏色RGBA和坐標XYZW都可視為四維向量,GPU只需要知道傳過來的向量是幾維,它實際的作用由人寫的程式決定。
SemanticName此時不方便解釋,之後教到貼圖,有兩項以上的資料比較能看出效果。
建立物件的CreateInputLayout()其中兩個參數是vertex shader的byte code,這個函式需要把SemanticName跟shader程式碼比對,做完這一步才能將vsBlob刪除。
除了套用input layout物件以外,還要呼叫IASetPrimitiveTopology()指定幾何形狀。
頂點數量不是在這裡告訴顯卡少女,而是之後呼叫Draw()時指定。
-Rasterizer-
D3D11_RASTERIZER_DESC說明
這一步比較常用的設定值是culling和multisample,這個例子要關閉這兩個功能。
除了建立RasterizerState物件以外還要設定viewport,X、Y的單位是像素而不是-1~1,Z的範圍是0~1。
忘記culling和viewport是什麼的話可以看這篇複習:Direct3D與OpenGL的繪圖管線(上)
-Depth and stencil-
D3D11_DEPTH_STENCIL_DESC說明
本篇不做depth test和stencil test,也根本沒配置depth buffer,直接用ZeroMemory()把其中兩個欄位:DepthEnable和StencilEnable填0。
-Blend-
D3D11_BLEND_DESC說明
其中一個成員「D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8]」是8個元素的陣列,D3D可以一次draw call同時畫在多個framebuffer上,每個framebuffer可以套用不同blend設定,這個例子只輸出到一個framebuffer,所以只填[0]。
Blend是指pixel的輸出和現在畫面上的像素要用什麼算式計算,本篇用pixel shader的輸出直接取代掉舊的值。
最後一步,還要告訴顯卡少女要畫在哪個framebuffer,到這裡才把所有物件都準備完成。
因為可以一次draw call同時畫在多個framebuffer,OMSetRenderTargets()第二參數是個陣列,第一參數是陣列長度。
本篇把錯誤檢查的code省略,以下函式有用一個變數hr記錄傳回值,但沒有檢查它的值。
如果struct成員或函式參數填了一個無效的值,hr會不等於S_OK(=0)。例如buDesc.ByteWidth必須填一個正數(因為配置0 byte的buffer無意義),填0會傳回錯誤碼。
但傳回值不能幫人檢查邏輯錯誤,如culling把順逆時針設錯,這時會看到傳回值沒問題,物件都有建出來,但畫面上顯示的不是我們想要的。
還有套用物件的函式「context->Set什麼的」傳回值是void,如果填錯參數或物件它不會告訴你,這時通常會看到物體沒被畫出來,但函式傳回值都沒問題。
所以D3D的程式有時候很難debug,有時候用一些debug工具會比較方便。
實際畫出畫面。用ClearRenderTargetView()把畫面塗成全黑,再用Draw()畫出三角形,第一參數是頂點數量,「頂點有3個」的資訊在這裡才告訴顯卡少女。
本篇只畫一個物體,所以各種設定值和vertex buffer、shader只在初始化時套用一次,之後就不更換,如果要畫多個物體,每次繪製之前要呼叫函式更換物件。
另外「Direct3D與OpenGL的繪圖管線(下)」有講到,呼叫Draw()的時候才真正使用這些物件,因此initSettings()裡套用物件的順序可以互換,不用照繪圖管線的步驟。
跟刪除device和context相同,刪除物件的方法是呼叫Release()。
deinitD3D()和之前一樣。
假設檔名是simplepipeline.cpp,用這個指令build。
多了一個d3dcompiler.lib是因為用到D3DCompile()。
執行的樣子
這次改良程式的syntax highlighting。
本篇的color theme是Monokai,原本是Sublime Text使用的,後來有移植到很多軟體上,Visual Studio Code安裝好後就有內建,Code::Blocks也有人做出來,但我在本篇用的配色沒完全按照VS code的。
VS code的例子。
這次要做的是把左邊列的東西一樣一樣建出來,除了constant buffer、貼圖和sampler以外,本篇先不用到貼圖。
本篇重點是將頂點資料和shader傳給GPU。D3D9不一定要用到shader,而且有一個函式DrawPrimitiveUP()可以「上傳頂點資料→繪製」兩個工作同時做。但D3D10以後就一定要寫vertex shader和pixel shader,且「要求顯卡少女配置記憶體空間→上傳頂點資料→繪製」必須手動一步一步做,所以初學階段就得學習怎麼做這兩件事。
因為本篇新介紹的東西很多,在此把vertex buffer和input layout的一些細節略過,先寫個能用的程式再說,之後教到貼圖和shader輸入輸出時再介紹。
之前的教學可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
#define UNICODE #include<windows.h> #include<d3d11.h> #include<d3dcompiler.h> //使用D3DCompile() #include<stdio.h> #include<stdint.h> const int WINDOW_W=200, WINDOW_H=200; //D3D11 global狀態 ID3D11Device* device; ID3D11DeviceContext* context; IDXGISwapChain* swapChain; ID3D11RenderTargetView* screenRenderTarget; //D3D11資料和設定值 ID3D11VertexShader* vertexShader; ID3D11PixelShader* pixelShader; ID3D11Buffer* vertexData; ID3D11InputLayout* inputLayout; ID3D11RasterizerState* rsState; ID3D11DepthStencilState* dsState; ID3D11BlendState* blendState; //要傳給GPU的資料 const float VERTICES[]={ 0, 0.8, 0.8, 0, -0.5, -0.5, }; const char SHADER[]=" \ float4 vsMain(in float2 pos:P):SV_POSITION { \ return float4(pos, 0, 1); \ } \ \ float4 psMain():SV_TARGET { \ return float4(1,1,1,1); \ } \ "; //這五個函式在下面說明 static int initD3D(HWND window){ …… } static int initSettings(){ …… } static void nextFrame(){ …… } static void deinitSettings(){ …… } static void deinitD3D(){ …… } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ switch(message){ case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd,message,wparam,lparam); } int main(){ WNDCLASS wndclass; ZeroMemory(&wndclass, sizeof(WNDCLASS)); wndclass.lpfnWndProc=WndProc; wndclass.hCursor=LoadCursor(NULL, IDC_ARROW); wndclass.lpszClassName=L"window"; RegisterClass(&wndclass); RECT rect={0,0,WINDOW_W,WINDOW_H}; AdjustWindowRect(&rect, WS_CAPTION|WS_SYSMENU|WS_VISIBLE, 0); HWND window=CreateWindow(L"window", L"simplepipeline", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL); if(initD3D(window)){ printf("Can not initialize D3D11\n"); return 0; } if(initSettings()){ return 0; } //開始訊息迴圈 timeBeginPeriod(1); MSG msg; int isEnd=0; ULONGLONG prevTime, nextTime; while(!isEnd){ QueryUnbiasedInterruptTime(&prevTime); while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)){ if(msg.message==WM_QUIT){ isEnd=1; } DispatchMessage(&msg); } //正式寫遊戲時,遊戲邏輯放在此處 nextFrame(); //更新畫面 QueryUnbiasedInterruptTime(&nextTime); //單位為100ns (10^-7 sec) ULONGLONG elapsedTime=(nextTime-prevTime)/10000; //除以10000換算成毫秒 (10^-3 sec) int32_t sleepTime=16-elapsedTime; if(sleepTime>0){ Sleep(sleepTime); } } timeEndPeriod(1); deleteObjects(); deinitD3D(); return 0; } |
- 多了一行#include<d3dcompiler.h>,用來編譯shader。
- 多了一些變數儲存pipeline裡的設定值。
本篇把這些物件宣告成全域變數,程式裡有一大堆全域變數其實不是好的寫法,本篇儘量把主題以外的東西省略,寫正式專案時可以思考一下怎樣組織資料。 - 計時器這次採用正式的做法了
1.Sleep()後立刻呼叫QueryUnbiasedInterruptTime()取得目前時間。
2.做完處埋輸入、遊戲邏輯、繪圖後再取得一次目前時間,將兩者相減算出經過的時間。
3.再算出應該暫停多久,如果經過時間>=16ms就不呼叫Sleep()了。
QueryUnbiasedInterruptTime()傳回的是電腦開機後到現在的時間,這個函式要Windows 7以上才有,之前的Windows有另一個函式「DWORD timeGetTime()」,傳回的單位是毫秒,但筆者有預留以後做Windows app的空間,timeGetTime()在Windows app不能用,所以改用QueryUnbiasedInterruptTime()。
Windows有其他時間函式可以量到小於毫秒,如QueryPerformanceCounter(),但sleep函式最細只能計時到毫秒,測量到比毫秒細也沒用。
題外話:時間超過整數能儲存的最大值會溢位(從0重新開始增加),DWORD型態毫秒可以計時到49.7天,Windows 95、98因為計時器溢位的問題開49.7天不關機就會當掉,之後的Windows有修正這個問題。
QueryUnbiasedInterruptTime()單位為100奈秒,用64位元整數儲存,溢位需要58000年以上。
那如果照本篇的寫法溢位了會怎樣?電腦計算整數加減法有環繞的現象,如果是32位元整數,0xffffffff過16ms會變成15,計算15-0xffffffff仍然可以得知經過16ms,不會出問題。
還多了這部分。
//要傳給GPU的資料 const float VERTICES[]={ 0, 0.8, 0.8, 0, -0.5, -0.5, }; const char SHADER[]=" \ float4 vsMain(in float2 pos:P):SV_POSITION { \ return float4(pos, 0, 1); \ } \ \ float4 psMain():SV_TARGET { \ return float4(1,1,1,1); \ } \ "; |
這裡把vertex shader和pixel shader寫在同一個字串裡,D3D有能力從裡面區分兩者,下面會看到方法。
三個頂點坐標是(0, 0.8)、(0.8, 0)、(-0.5, -0.5),以前有講過D3D和OpenGL的畫面坐標是-1 ~ 1,可以預想畫出的三角形是這樣。
static int initD3D(HWND window){ HRESULT hr; DXGI_SWAP_CHAIN_DESC scd; ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC)); scd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; //色彩格式 scd.SampleDesc.Count = 1; scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; scd.BufferCount = 1; scd.OutputWindow = window; //把這個視窗作為D3D的畫布 scd.Windowed = TRUE; //視窗還是全螢幕模式 //建立D3D系統物件,feature level=10.0 D3D_FEATURE_LEVEL featureLevel=D3D_FEATURE_LEVEL_10_0; hr=D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, D3D11_CREATE_DEVICE_SINGLETHREADED, &featureLevel, 1, D3D11_SDK_VERSION,&scd, &swapChain,&device,NULL,&context); if(hr!=S_OK){ return 1; } //取得畫面的framebuffer物件 ID3D11Texture2D* screenTexture; swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&screenTexture); device->CreateRenderTargetView(screenTexture, NULL, &screenRenderTarget); screenTexture->Release(); return 0; } |
之後的initSettings()是重點,要一步一步看
-Vertex & Pixel Shader-
//傳回0成功,傳回非0失敗 static int initSettings(){ HRESULT hr; //vertex shader ID3DBlob* vsBlob=0,*psBlob=0, *errorMessageBlob; hr=D3DCompile(SHADER, sizeof(SHADER), NULL, NULL, NULL, "vsMain", "vs_4_0",0, 0,&vsBlob, &errorMessageBlob); if(hr!=S_OK){ printf("vertex shader:\n%s", errorMessageBlob->GetBufferPointer()); errorMessageBlob->Release(); } //pixel shader hr=D3DCompile(SHADER, sizeof(SHADER), NULL, NULL, NULL, "psMain", "ps_4_0",0, 0,&psBlob, &errorMessageBlob); if(hr!=S_OK){ printf("pixel shader:\n%s", errorMessageBlob->GetBufferPointer()); errorMessageBlob->Release(); } //如果shader有錯誤,刪除已建立的物件並return if(vsBlob==0 || psBlob==0){ if(vsBlob){ vsBlob->Release(); } if(psBlob){ psBlob->Release(); } return 1; } |
shader是給GPU執行的程式,跟C/C++一樣要先把程式碼編譯成binary才能給GPU使用,D3D shader的程式語言稱為HLSL,要先把code編譯成一種中間碼(byte code)再傳給驅動程式。
編譯方式有兩種:
1. 把原始碼附在程式裡,執行時使用D3DCompile()編譯。
2. 用一個工具:fxc.exe事先編譯,發佈程式時只要附byte code。
本篇用第一種,第二種以後再介紹。
MSDN的D3DCompile()說明
本篇用到的參數如下:
1:程式碼。
2:程式碼長度(byte數)。
3:shader名稱,程式執行不會用到,只用在顯示錯誤訊息,不需要可填NULL。
6:程式進入點。C/C++規定程式從main()或WinMain()開始執行,HLSL沒規定程式起點,而是用這個參數指定。
因此可以把vertex和pixel shader寫在同一個字串,用這個參數告訴compiler哪個函式是哪個shader。
7:target,指定shader版本號和種類。
版本號要跟「Direct3D 11初始化」提到的feature level對應,如果程式預定最低支援D3D10的晶片就填vs_4_0或ps_4_0,這樣如果shader裡用到D3D11以後的東西會視為error。
所有能填的值看這篇 MSDN : Specifying Compiler Targets
第10、11參數是傳回值,把byte code和錯誤訊息存在兩個ID3DBlob物件,可以如上印出錯誤訊息得知shader哪裡寫錯。
//create shader objects hr=device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(),NULL, &vertexShader); hr=device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(),NULL, &pixelShader); psBlob->Release(); context->VSSetShader(vertexShader,NULL,0); context->PSSetShader(pixelShader, NULL,0); |
psBlob不會再用到,呼叫Release()將它刪除,vsBlob之後還會用到,先不刪除。
最後還要用context->VSSetShader()和context->PSSetShader()套用物件,如「Direct3D與OpenGL的繪圖管線(下)」所說,可以想成是叫顯卡少女把物件放在工作臺上,之後呼叫Draw()就會套用這兩個shader。
-Vertex buffer-
//vertex buffer D3D11_BUFFER_DESC buDesc; ZeroMemory(&buDesc, sizeof(D3D11_BUFFER_DESC)); buDesc.ByteWidth = sizeof(VERTICES); buDesc.Usage = D3D11_USAGE_IMMUTABLE; buDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; D3D11_SUBRESOURCE_DATA data; data.pSysMem = VERTICES; hr=device->CreateBuffer(&buDesc, &data, &vertexData); const UINT stride=sizeof(float)*2; const UINT offset=0; context->IASetVertexBuffers(0,1,&vertexData,&stride,&offset); |
D3D11_BUFFER_DESC說明
ID3D11Device::CreateBuffer()說明
ID3D11DeviceContext::IASetVertexBuffers()說明
D3D11裡只要配置一塊記憶體(建立buffer或texture物件)都要填以下欄位:
ByteWidth:byte數。
Usage與BindFlags:這裡D3D11_USAGE_IMMUTABLE代表資料會在建立物件時一併上傳,之後不可修改,D3D11_BIND_VERTEX_BUFFER代表這一塊空間要儲存頂點資料,其他還能填什麼值之後再介紹。
D3D11_SUBRESOURCE_DATA:如果想在建立物件同時把資料上傳,需要填這個struct。
IASetVertexBuffers()第二參數是陣列長度,第三~第五參數可以給ID3D11Buffer*或UINT的陣列,頂點資料可以來自兩個以上的buffer物件,以後講到貼圖坐標會示範這些參數的用法。
-Input assembler-
之後都是設定值,不用把資料從CPU傳到顯示記憶體就比較簡單了。D3D11是把同一個步驟的設定值都包在一個物件裡,之後想調整設定就是套用物件。
建立物件大多是這樣的步驟:
1. 將設定值填入一個struct。
2. 把struct傳入device->CreateXXXX()產生物件,函式本身傳回錯誤碼代表是否成功,物件用指標傳回。
3. 呼叫context的成員函式套用物件。
物件建好之後就不可修改,如果畫不同物體想用不同設定值,要事先建立好幾個物件看情況套用。
不過struct裡有些目前沒用到的功能也必須填,所以以下會看到一些目前還沒教到的東西。
//input assembler D3D11_INPUT_ELEMENT_DESC layoutDesc; ZeroMemory(&layoutDesc, sizeof(D3D11_INPUT_ELEMENT_DESC)); layoutDesc.SemanticName = "P"; layoutDesc.Format = DXGI_FORMAT_R32G32_FLOAT; hr=device->CreateInputLayout(&layoutDesc, 1, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(),&inputLayout); vsBlob->Release(); context->IASetInputLayout(inputLayout); context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); |
D3D11_INPUT_ELEMENT_DESC說明
ID3D11Device::CreateInputLayout()說明
本篇重要的欄位只有Format一個,表示向量有幾個分量、每個分量幾byte等等,所有能填的值跟initD3D()裡framebuffer的顏色格式一樣,看這篇。
DXGI_FORMAT enumeration說明
這個例子位置有x,y二個分量,分量是32 bit float型態,填DXGI_FORMAT_R32G32_FLOAT。
有沒有覺得奇怪,這個值是明明是坐標,為何常數名稱是RGBA?以前fixed-function pipeline的時代坐標和顏色要區分開,但有了shader之後很多事都是人寫程式控制了。顏色RGBA和坐標XYZW都可視為四維向量,GPU只需要知道傳過來的向量是幾維,它實際的作用由人寫的程式決定。
SemanticName此時不方便解釋,之後教到貼圖,有兩項以上的資料比較能看出效果。
建立物件的CreateInputLayout()其中兩個參數是vertex shader的byte code,這個函式需要把SemanticName跟shader程式碼比對,做完這一步才能將vsBlob刪除。
除了套用input layout物件以外,還要呼叫IASetPrimitiveTopology()指定幾何形狀。
頂點數量不是在這裡告訴顯卡少女,而是之後呼叫Draw()時指定。
-Rasterizer-
//rasterizer D3D11_RASTERIZER_DESC rsDesc; ZeroMemory(&rsDesc, sizeof(D3D11_RASTERIZER_DESC)); rsDesc.FillMode = D3D11_FILL_SOLID; rsDesc.CullMode = D3D11_CULL_NONE; hr=device->CreateRasterizerState(&rsDesc, &rsState); context->RSSetState(rsState); D3D11_VIEWPORT viewport; viewport.TopLeftX = 0; viewport.TopLeftY = 0; viewport.Width = WINDOW_W; viewport.Height = WINDOW_H; viewport.MinDepth = 0; viewport.MaxDepth = 1; context->RSSetViewports(1, &viewport); |
這一步比較常用的設定值是culling和multisample,這個例子要關閉這兩個功能。
除了建立RasterizerState物件以外還要設定viewport,X、Y的單位是像素而不是-1~1,Z的範圍是0~1。
忘記culling和viewport是什麼的話可以看這篇複習:Direct3D與OpenGL的繪圖管線(上)
-Depth and stencil-
//depth stencil D3D11_DEPTH_STENCIL_DESC dsDesc; ZeroMemory(&dsDesc, sizeof(D3D11_DEPTH_STENCIL_DESC)); hr=device->CreateDepthStencilState(&dsDesc, &dsState); context->OMSetDepthStencilState(dsState,0); |
本篇不做depth test和stencil test,也根本沒配置depth buffer,直接用ZeroMemory()把其中兩個欄位:DepthEnable和StencilEnable填0。
-Blend-
//blend D3D11_BLEND_DESC blendDesc; ZeroMemory(&blendDesc,sizeof(D3D11_BLEND_DESC)); D3D11_RENDER_TARGET_BLEND_DESC* blendDesc2=blendDesc.RenderTarget; //把blendDesc.RenderTarget[0]取出來讓下面的程式簡短一些, //否則以下都要寫成blendDesc.RenderTarget[0].??? blendDesc2->BlendEnable = 1; blendDesc2->RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; blendDesc2->BlendOp = D3D11_BLEND_OP_ADD; blendDesc2->SrcBlend = D3D11_BLEND_ONE; blendDesc2->DestBlend = D3D11_BLEND_ZERO; blendDesc2->BlendOpAlpha = D3D11_BLEND_OP_ADD; blendDesc2->SrcBlendAlpha = D3D11_BLEND_ONE; blendDesc2->DestBlendAlpha = D3D11_BLEND_ZERO; hr=device->CreateBlendState(&blendDesc, &blendState); context->OMSetBlendState(blendState, 0, 0xffffffff); |
其中一個成員「D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8]」是8個元素的陣列,D3D可以一次draw call同時畫在多個framebuffer上,每個framebuffer可以套用不同blend設定,這個例子只輸出到一個framebuffer,所以只填[0]。
Blend是指pixel的輸出和現在畫面上的像素要用什麼算式計算,本篇用pixel shader的輸出直接取代掉舊的值。
context->OMSetRenderTargets(1, &screenRenderTarget, 0); return 0; } |
因為可以一次draw call同時畫在多個framebuffer,OMSetRenderTargets()第二參數是個陣列,第一參數是陣列長度。
本篇把錯誤檢查的code省略,以下函式有用一個變數hr記錄傳回值,但沒有檢查它的值。
hr=device->CreateVertexShader(……); hr=device->CreatePixelShader(……); hr=device->CreateBuffer(……); hr=device->CreateInputLayout(……); hr=device->CreateRasterizerState(……); hr=device->CreateDepthStencilState(……); hr=device->CreateBlendState(……); |
但傳回值不能幫人檢查邏輯錯誤,如culling把順逆時針設錯,這時會看到傳回值沒問題,物件都有建出來,但畫面上顯示的不是我們想要的。
還有套用物件的函式「context->Set什麼的」傳回值是void,如果填錯參數或物件它不會告訴你,這時通常會看到物體沒被畫出來,但函式傳回值都沒問題。
所以D3D的程式有時候很難debug,有時候用一些debug工具會比較方便。
static void nextFrame(){ const float color[]={0,0,0,0}; context->ClearRenderTargetView(screenRenderTarget, color); context->Draw(3, 0); swapChain->Present(0, 0); } |
本篇只畫一個物體,所以各種設定值和vertex buffer、shader只在初始化時套用一次,之後就不更換,如果要畫多個物體,每次繪製之前要呼叫函式更換物件。
另外「Direct3D與OpenGL的繪圖管線(下)」有講到,呼叫Draw()的時候才真正使用這些物件,因此initSettings()裡套用物件的順序可以互換,不用照繪圖管線的步驟。
static void deinitSettings(){ vertexShader->Release(); pixelShader->Release(); inputLayout->Release(); vertexData->Release(); rsState->Release(); dsState->Release(); blendState->Release(); } |
static void deinitD3D(){ device->Release(); context->Release(); swapChain->Release(); screenRenderTarget->Release(); } |
假設檔名是simplepipeline.cpp,用這個指令build。
cl simplepipeline.cpp /Fesimplepipeline.exe /O2 /MD /link user32.lib d3d11.lib winmm.lib d3dcompiler.lib |
執行的樣子
這次改良程式的syntax highlighting。
本篇的color theme是Monokai,原本是Sublime Text使用的,後來有移植到很多軟體上,Visual Studio Code安裝好後就有內建,Code::Blocks也有人做出來,但我在本篇用的配色沒完全按照VS code的。
VS code的例子。