前篇:Direct3D 11 架設基本繪圖管線
前一篇還沒有使用constant buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個軟體做的
キャラクターなんとか機 http://khmix.sakura.ne.jp/download.shtml
D3D11以外的部分有這些改變:將視窗尺寸設得比較大以配合圖檔,且多出了WIC的東西:wincodec.h、wicFactory、main()裡面初始化與釋放的程式碼,這些是讀取圖檔會用到。
這次要傳給GPU的資料如下
VERTEX_DATA1和VERTEX_DATA2是相同頂點資料用不同layout儲存,本篇只用1,2等之後寫layout的教學再使用。
這次頂點資料有三項:位置、貼圖坐標、顏色,之前說過D3D和OpenGL的畫面坐標是-1~1、貼圖坐標是0~1,但2D畫面習慣上以畫面左上角為原點、以像素為單位,這裡頂點資料就這樣給,在shader裡換算成D3D和OpenGL標準。
雖然本篇故意在vertex shader裡算,這裡只有4個頂點,在CPU把4個頂點全部算好才傳給GPU其實效能不會差多少,但如果一次畫很多個三角形(如tilemap和粒子系統),在vertex shader裡算比較方便。
坐標是這樣,右圖的ABCD是頂點順序。
shader要做一點事前準備,這次採用預先編譯成bytecode的方式。
把下列程式碼存成usetexture.hlsl。
「shader的輸入與輸出」提到的輸出入在這裡出現了,看本篇時可以跟那篇對照著看。
至於vertex shader坐標轉換的算式怎麼求出,回想一下學校學過的二元一次方程式。
畫面坐標要把左邊的換算成右邊
二元一次方程式的標準式「x'=xa+b, y'=yc+d」,把兩組坐標代進去,得到兩組聯立方程式
解出a,b,c,d如下
x' =( 2/windowW)x - 1
y' =(-2/windowH)y + 1
因為shader可以一次計算四維向量,可以寫成這樣
outPos = inPos×(2,-2)/(windowW, windowH) + (-1,1)
貼圖坐標只要除以貼圖寬高即可,內建函式GetDimensions()可以取得貼圖寬高。
顏色就原封不動傳給rasterizer內插。
pixel shader讀取貼圖裡的像素,跟頂點資料裡的顏色相乘。
至於如何編譯,先按照「Visual C++的命令列工具」的方法設好VC的環境變數,然後用這兩個指令編譯shader:
會產生兩個檔案:usetexture_vs和usetexture_ps,以後如果只有修改主程式而沒有修改shader,這兩個檔案不用重新編譯。
還有一種用法是後面加個/Fc參數
會產生a.txt,裡面有編譯後的組合語言程式碼,還有constant buffer各個變數在記憶體裡的位置,在C/C++寫對應struct的時候可以參考。
事先編譯成bytecode對於效能和方便性都比較好,編譯code需要分析字串、檢查語法等等的,事先編譯可以讓主程式省下這些工;而且能事先檢查語法錯誤,等語法確定沒錯再包裝成程式用的資料檔,不需要等執行主程式才能檢查。
bytecode的規格是D3D標準,所有廠牌的晶片都要能接收這個格式,因此bytecode只要一次編譯好就可以拿到其他電腦使用。
fxc.exe的全部參數說明可以看這篇
MSDN: Effect-Compiler Tool Syntax
有些參數跟effect framework有關。D3D有一個功能是effect framework,可以把rasterizer、blend等等的設定也寫在shader裡,載入shader就同時套用設定,但失去一些靈活度。本系列不教這個東西,有興趣的話自己看。
MSDN: Effects 11 Reference
initD3D()、deinitD3D和之前一樣。
背景換成白色且alpha=1,Draw()第一參數的頂點數量改成4。
要刪除的物件增加3個。
接下來的initSettings()是主題,用到的輔助函式會在旁邊解說。
-Vertex & Pixel Shader-
loadWholeFile()用到的Windows API函式請參照這篇:檔案操作—Windows篇,作業系統沒有直接提供「傳入檔名→讀取整個檔案」的函式,但這功能有時候會用到,寫一個函式做這件事。
剛剛編譯好的檔案就是bytecode,讀入記憶體之後直接給CreatePixelShader()和CreateVertexShader()。
跟「Direct3D 11 架設基本繪圖管線」一樣,vertex shader bytecode之後建input layout物件會用到,先不刪除。
-Vertex buffer & Input assembler-
建buffer物件的方法跟「Direct3D 11 架設基本繪圖管線」一樣,input layout的話這次頂點有三項資料,要填三個struct D3D11_INPUT_ELEMENT_DESC,裡面欄位是什麼意思留待之後介紹。
IASetPrimitiveTopology()改用另一個種幾何形狀:triangle strip,本篇要講的東西很多,先不介紹各種幾何形狀。雖然D3D10以後不再支援四邊形,只有四個頂點的情況下用triangle strip可以畫四邊形。
-Constant buffer-
本篇新增的東西之一。建立constant buffer的方法跟上面vertex buffer很像:填D3D11_BUFFER_DESC,要上傳的資料填在D3D11_SUBRESOURCE_DATA,然後呼叫CreateBuffer()。不同的地方是BindFlags改成D3D11_BIND_CONSTANT_BUFFER,代表這一塊區域要作為constant buffer使用。另外要上傳的資料:WINDOW_SIZE雖然是8 bytes,但是constant buffer大小必須是16的倍數(OpenGL無此限制),「(sizeof(WINDOW_SIZE)+15) & 0xfff0」可以求出剛好能容納的16的倍數,因為算式裡的值都是常數,編譯時就會算出結果而沒有效能消耗。
VSSetConstantBuffers()是設定vertex shader裡的constant buffer,當然還有個PSSetConstantBuffers(),本篇的pixel shader沒有用到constant buffer所以不用呼叫它。
-Sampler-
本篇新增的東西之二。跟大部分D3D11物件一樣填一個struct設定參數。WRAP和Filter是什麼請參照這篇貼圖的部分:shader的輸入與輸出
MSDN: D3D11_SAMPLER_DESC structure
AddressW用在3D貼圖,雖然本篇只用到2D貼圖,但AddressW也要填一個有效的值,否則建立物件會失敗。
-Texture-
本篇新增的東西之三,用到一個輔助函式loadTexture()。用一個Windows內建的函式庫:WIC讀取圖檔與解碼,方法參照這篇:讀取圖檔的方法-Windows篇。
PNG、JPG、webP這些編碼過的格式不能給GPU使用,因為GPU讀取貼圖需要迅速找到任意位置的像素(即隨機存取,random access),這些格式必須完全解碼才能得知每個像素的值。用在GPU的壓縮格式必須設計成能隨機存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
之後建立貼圖物件,D3D11建立貼圖物件要這樣做:建立Texture2D物件代表記憶體空間,再建立shader resource view物件關聯到Texture2D物件,使用貼圖要套用shader resource view物件。
指定貼圖格式的方法一樣是填struct。
MSDN: D3D11_TEXTURE2D_DESC structure
Width、Height不用解釋,Format是像素是什麼格式,這裡byte順序是BGRA,每個分量8 bit。byte數可以從寬高和格式求出所以不用填byte數。
跟buffer物件一樣要填Usage和BindFlags,因為貼圖跟buffer一樣是記憶體空間。這裡又出現一種bind flag:D3D11_BIND_SHADER_RESOURCE,代表這塊區域要當作貼圖被shader讀取。
所有能用的bind flag見這篇,STREAM_OUTPUT、UNORDERED_ACCESS、DECODER、VIDEO_ENCODER這四項比較少用,其他的之後的教學有機會用到。
MSDN: D3D11_BIND_FLAG enumeration
可以好幾項位元or,例如framebuffer物件要作為畫布也要作為貼圖,要填D3D11_BIND_RENDER_TARGET|D3D11_BIND_SHADER_RESOURCE。
MipLevels、ArraySize、SampleDesc.Count這三個功能目前沒用到,但必須填1而不能是0,否則建立物件會失敗。
因為這是二維貼圖,D3D11_SUBRESOURCE_DATA要多填一個SysMemPitch:你準備的pixels一列是幾bytes,一維貼圖和buffer就不用填。
CreateShaderResourceView()第二參數其實也是個struct指定格式,填NULL代表從D3D11_TEXTURE2D_DESC得知格式,有些時候這個參數不能填NULL。
建立shaderResourceView之後,用texture->Release()將CreateTexture2D()增加的reference count釋放,讓它只剩下shaderResourceView對它的reference,之後刪除shaderResourceView就會同時刪除texture物件。
從loadTexture()返回之後要套用物件,這個物件vertex shader和pixel shader都會用到,要呼叫VSSetShaderResources()和PSSetShaderResources()套用。
-Rasterizer, depth, stencil, and blend-
rasterizer、depth和stencil跟以前一樣,只有blend換了算式,改成正式繪圖軟體使用的。
(Sc, Sa):pixel shader輸出的顏色和alpha,數值範圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
註:本篇特別把畫面的alpha設為1簡化算式。如果畫面的alpha不是1,那其實D3D和OpenGL的blend設定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時做一點特別處理。
以前有寫過一篇筆記:【程式】premultiply alpha的妙用
如果檔名是usetexture.cpp,用這個指令build
比上次多了windowscodecs.lib和ole32.lib是因為用WIC讀取圖檔。
執行的樣子。
constant buffer、texture、sampler這三種物件如何將主程式和shader的物件對應,可以想成顯卡少女的工作臺有一些置物櫃,各有很多格子。
D3D11每個shader階段各有一個置物櫃,如果有使用geometry、hull或domain shader,那它們也各自有一個置物櫃。
本篇的shader裡有以下三行,其中register()裡的b2, t2, s2就是叫顯卡少女三種物件都拿2號格子的。(雖然編號是從0開始,本篇故意用2)
至於要怎麼指示顯卡少女把物件放進格子,上面程式裡有這些行
函式名稱的VS、PS代表要設定哪一階段的shader,第一參數是起始格子編號,第二和第三參數是陣列長度和陣列,可以一次設定多個,如果傳入長度3的陣列,會設定2,3,4的格子。
不過電腦裡的世界有個地方跟現實不一樣,如果在不同階段的置物櫃放進相同的物件,記憶體裡只會佔用一個物件的空間,不會真的把物件複製好幾份。
至於置物櫃具體有幾格,D3D的文件很難找到寫在哪裡,用一個方法查比較快:shader裡的register()故意填一個很大的值
編譯會跳出這個訊息
由此可知constant buffer有14格(可填b0~b13)
由於compiler看到b200的錯誤就停止編譯,把b200改成b13再編譯一次,可看出貼圖有128格,取樣器有16格。
前一篇還沒有使用constant buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個軟體做的
キャラクターなんとか機 http://khmix.sakura.ne.jp/download.shtml
#define UNICODE #include<windows.h> #include<d3d11.h> #include<stdio.h> #include<stdint.h> #include<stddef.h> //使用offsetof macro #include<wincodec.h> //讀取圖檔 const int WINDOW_W=400, WINDOW_H=400; IWICImagingFactory* wicFactory; //用來讀取圖檔 //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; //本篇新增的物件 ID3D11Buffer* constantBuffer; ID3D11ShaderResourceView* shaderResource; ID3D11SamplerState* sampler; //要傳給GPU的資料在下面說明 //這些函式在下面說明 static int initD3D(HWND window); static int initSettings(); static void nextFrame(); static void deinitSettings(); static void deinitD3D(); static char* loadWholeFile(const WCHAR* fileName, uint32_t* outFileSize); static void initVertexData1(const char* vsBytecode, int vsBytecodeSize, ID3D11Buffer** outVertexData, ID3D11InputLayout** outInputLayout); static ID3D11ShaderResourceView* loadTexture(const WCHAR* fileName); //這個函式留待之後介紹layout時解說 static void initVertexData2(const char* vsBytecode, int vsBytecodeSize, ID3D11Buffer** outVertexData, ID3D11InputLayout** outInputLayout); //以下為基本視窗程式架構 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"usetexture", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL); //初始化WIC CoInitializeEx(NULL, COINIT_MULTITHREADED); CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, (void**)&wicFactory); 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(); //結束WIC wicFactory->Release(); CoUninitialize(); return 0; } |
這次要傳給GPU的資料如下
//constant buffer const float WINDOW_SIZE[]={WINDOW_W, WINDOW_H}; //頂點資料 struct VertexData1{ float pos[2]; short texCoord[2]; uint32_t color; } VERTEX_DATA1[4]={ {100,0, 0, 0, 0xffffffff}, {100,400,0, 400,0xffffffff}, {400,0, 300,0, 0xffffffff}, {400,400,300,400,0xffffffff}, }; struct VertexData2{ float pos[8]; short texCoord[8]; uint32_t color[4]; } VERTEX_DATA2={ {100,0,100,400,400,0,400,400}, {0,0,0,400,300,0,300,400}, {0xffffffff,0xffffffff,0xffffffff,0xffffffff}, }; |
這次頂點資料有三項:位置、貼圖坐標、顏色,之前說過D3D和OpenGL的畫面坐標是-1~1、貼圖坐標是0~1,但2D畫面習慣上以畫面左上角為原點、以像素為單位,這裡頂點資料就這樣給,在shader裡換算成D3D和OpenGL標準。
雖然本篇故意在vertex shader裡算,這裡只有4個頂點,在CPU把4個頂點全部算好才傳給GPU其實效能不會差多少,但如果一次畫很多個三角形(如tilemap和粒子系統),在vertex shader裡算比較方便。
坐標是這樣,右圖的ABCD是頂點順序。
shader要做一點事前準備,這次採用預先編譯成bytecode的方式。
把下列程式碼存成usetexture.hlsl。
//constant buffer,對應到上面的WINDOW_SIZE cbuffer cbuffer1 :register(b2){ float2 windowSize; }; Texture2D texture1 :register(t2); SamplerState sampler1 :register(s2); //頂點資料,對應到struct VertexData1 struct VsIn{ float2 pos :P; int2 texCoord :T; float4 color :C; }; //vertex傳給pixel shader的資料 struct VsOut{ float4 svPos :SV_Position; float2 texCoord :T; float4 color :C; }; void vsMain(in VsIn IN, out VsOut OUT) { float2 outPos=IN.pos*float2(2,-2)/windowSize+float2(-1,1); OUT.svPos=float4(outPos, 0, 1); float2 texSize; texture1.GetDimensions(texSize.x, texSize.y); OUT.texCoord=IN.texCoord/texSize; OUT.color=IN.color; } float4 psMain(in VsOut IN):SV_Target { float4 texColor=texture1.Sample(sampler1, IN.texCoord); return texColor*IN.color; } |
至於vertex shader坐標轉換的算式怎麼求出,回想一下學校學過的二元一次方程式。
畫面坐標要把左邊的換算成右邊
二元一次方程式的標準式「x'=xa+b, y'=yc+d」,把兩組坐標代進去,得到兩組聯立方程式
X坐標 -1 = 0 + b 1 = windowW×a + b |
Y坐標 1 = 0 + d -1 = windowH×c + d |
解出a,b,c,d如下
x' =( 2/windowW)x - 1
y' =(-2/windowH)y + 1
因為shader可以一次計算四維向量,可以寫成這樣
outPos = inPos×(2,-2)/(windowW, windowH) + (-1,1)
貼圖坐標只要除以貼圖寬高即可,內建函式GetDimensions()可以取得貼圖寬高。
顏色就原封不動傳給rasterizer內插。
pixel shader讀取貼圖裡的像素,跟頂點資料裡的顏色相乘。
至於如何編譯,先按照「Visual C++的命令列工具」的方法設好VC的環境變數,然後用這兩個指令編譯shader:
fxc usetexture.hlsl /T vs_4_0 /E vsMain /Fo usetexture_vs fxc usetexture.hlsl /T ps_4_0 /E psMain /Fo usetexture_ps |
還有一種用法是後面加個/Fc參數
fxc usetexture.hlsl /T vs_4_0 /E vsMain /Fo usetexture_vs /Fc a.txt |
事先編譯成bytecode對於效能和方便性都比較好,編譯code需要分析字串、檢查語法等等的,事先編譯可以讓主程式省下這些工;而且能事先檢查語法錯誤,等語法確定沒錯再包裝成程式用的資料檔,不需要等執行主程式才能檢查。
bytecode的規格是D3D標準,所有廠牌的晶片都要能接收這個格式,因此bytecode只要一次編譯好就可以拿到其他電腦使用。
fxc.exe的全部參數說明可以看這篇
MSDN: Effect-Compiler Tool Syntax
有些參數跟effect framework有關。D3D有一個功能是effect framework,可以把rasterizer、blend等等的設定也寫在shader裡,載入shader就同時套用設定,但失去一些靈活度。本系列不教這個東西,有興趣的話自己看。
MSDN: Effects 11 Reference
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; 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; } static void deinitD3D(){ device->Release(); context->Release(); swapChain->Release(); screenRenderTarget->Release(); } |
static void nextFrame(){ const float color[]={1,1,1,1}; context->ClearRenderTargetView(screenRenderTarget, color); context->Draw(4, 0); swapChain->Present(0, 0); } |
static void deinitSettings(){ vertexShader->Release(); pixelShader->Release(); inputLayout->Release(); vertexData->Release(); rsState->Release(); dsState->Release(); blendState->Release(); constantBuffer->Release(); sampler->Release(); shaderResource->Release(); } |
接下來的initSettings()是主題,用到的輔助函式會在旁邊解說。
-Vertex & Pixel Shader-
//本身傳回檔案內容,outFileSize傳回檔案大小 //傳回來的指標要用free()釋放 static char* loadWholeFile(const WCHAR* fileName, uint32_t* outFileSize){ HANDLE file = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(file==INVALID_HANDLE_VALUE){ *outFileSize=0; return NULL; } DWORD fileSize = GetFileSize(file, NULL); char* data = (char*)malloc(fileSize); DWORD bytes; ReadFile(file, data, fileSize, &bytes, NULL); CloseHandle(file); *outFileSize = fileSize; return data; } |
//傳回0代表成功,非0代表失敗 static int initSettings(){ HRESULT hr; //shader object char* bytecode; uint32_t bytecodeSize; bytecode = loadWholeFile(L"usetexture_ps", &bytecodeSize); hr = device->CreatePixelShader(bytecode, bytecodeSize,NULL,&pixelShader); free(bytecode); bytecode = loadWholeFile(L"usetexture_vs", &bytecodeSize); hr = device->CreateVertexShader(bytecode, bytecodeSize,NULL,&vertexShader); context->VSSetShader(vertexShader,NULL,0); context->PSSetShader(pixelShader, NULL,0); |
剛剛編譯好的檔案就是bytecode,讀入記憶體之後直接給CreatePixelShader()和CreateVertexShader()。
跟「Direct3D 11 架設基本繪圖管線」一樣,vertex shader bytecode之後建input layout物件會用到,先不刪除。
-Vertex buffer & Input assembler-
static void initVertexData1(const char* vsBytecode, int vsBytecodeSize, ID3D11Buffer** outVertexData, ID3D11InputLayout** outInputLayout){ HRESULT hr; //vertex buffer D3D11_BUFFER_DESC buDesc; ZeroMemory(&buDesc, sizeof(D3D11_BUFFER_DESC)); buDesc.ByteWidth = sizeof(VERTEX_DATA1); buDesc.Usage = D3D11_USAGE_IMMUTABLE; buDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; D3D11_SUBRESOURCE_DATA data; data.pSysMem = VERTEX_DATA1; hr=device->CreateBuffer(&buDesc, &data, outVertexData); const UINT stride=sizeof(VERTEX_DATA1[0]); const UINT offset=0; context->IASetVertexBuffers(0,1,outVertexData,&stride,&offset); //input assembler D3D11_INPUT_ELEMENT_DESC layoutDesc[]={ {"P",0, DXGI_FORMAT_R32G32_FLOAT, 0,0, D3D11_INPUT_PER_VERTEX_DATA ,0}, {"T",0, DXGI_FORMAT_R16G16_SINT, 0,offsetof(VertexData1, texCoord), D3D11_INPUT_PER_VERTEX_DATA ,0}, {"C",0, DXGI_FORMAT_B8G8R8A8_UNORM,0,offsetof(VertexData1, color), D3D11_INPUT_PER_VERTEX_DATA ,0}, }; hr=device->CreateInputLayout(layoutDesc, 3, vsBytecode,vsBytecodeSize,outInputLayout); context->IASetInputLayout(*outInputLayout); } |
//initSettings()內容 //vertex data & input layout initVertexData1(bytecode, bytecodeSize, &vertexData, &inputLayout); free(bytecode); context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); |
IASetPrimitiveTopology()改用另一個種幾何形狀:triangle strip,本篇要講的東西很多,先不介紹各種幾何形狀。雖然D3D10以後不再支援四邊形,只有四個頂點的情況下用triangle strip可以畫四邊形。
-Constant buffer-
//constant buffer D3D11_BUFFER_DESC buDesc; ZeroMemory(&buDesc, sizeof(D3D11_BUFFER_DESC)); buDesc.ByteWidth = (sizeof(WINDOW_SIZE)+15) & 0xfff0; buDesc.Usage = D3D11_USAGE_IMMUTABLE; buDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; D3D11_SUBRESOURCE_DATA data; data.pSysMem = WINDOW_SIZE; hr=device->CreateBuffer(&buDesc, &data, &constantBuffer); context->VSSetConstantBuffers(2,1,&constantBuffer); |
VSSetConstantBuffers()是設定vertex shader裡的constant buffer,當然還有個PSSetConstantBuffers(),本篇的pixel shader沒有用到constant buffer所以不用呼叫它。
-Sampler-
//sampler D3D11_SAMPLER_DESC saDesc; ZeroMemory(&saDesc, sizeof(D3D11_SAMPLER_DESC)); saDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; saDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; saDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; saDesc.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT; hr=device->CreateSamplerState(&saDesc, &sampler); context->PSSetSamplers(2,1,&sampler); |
MSDN: D3D11_SAMPLER_DESC structure
AddressW用在3D貼圖,雖然本篇只用到2D貼圖,但AddressW也要填一個有效的值,否則建立物件會失敗。
-Texture-
//讀取圖檔、解碼、產生texture物件 //成功傳回物件指標,失敗傳回NULL static ID3D11ShaderResourceView* loadTexture(const WCHAR* fileName){ //用WIC讀取圖檔,把BGRA值存在一個叫pixels的變數 IWICBitmapDecoder* decoder; HRESULT hr = wicFactory->CreateDecoderFromFilename(fileName, NULL, GENERIC_READ,WICDecodeMetadataCacheOnDemand ,&decoder); if(hr!=S_OK){ return NULL; } IWICBitmapFrameDecode* frame; decoder->GetFrame(0,&frame); UINT w,h; frame->GetSize(&w,&h); UINT32* pixels = (UINT32*)malloc(w*h*4); IWICFormatConverter* converter; wicFactory->CreateFormatConverter(&converter); converter->Initialize(frame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL,0, WICBitmapPaletteTypeCustom); converter->CopyPixels(NULL, w*4, w*h*4, (BYTE*)pixels); converter->Release(); frame->Release(); decoder->Release(); //建立texture物件 D3D11_TEXTURE2D_DESC txDesc; ZeroMemory(&txDesc, sizeof(D3D11_TEXTURE2D_DESC)); txDesc.Width = w; txDesc.Height = h; txDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; txDesc.Usage = D3D11_USAGE_IMMUTABLE; txDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; txDesc.MipLevels = 1; txDesc.ArraySize = 1; txDesc.SampleDesc.Count = 1; D3D11_SUBRESOURCE_DATA srData; srData.pSysMem = pixels; srData.SysMemPitch = w*4; ID3D11Texture2D* texture; hr=device->CreateTexture2D(&txDesc, &srData, &texture); //建立shader resource view物件 ID3D11ShaderResourceView* shaderResourceView; hr=device->CreateShaderResourceView(texture,NULL,&shaderResourceView); texture->Release(); free(pixels); return shaderResourceView; } |
//initSettings()內容 //texture shaderResource = loadTexture(L"char1.png"); context->VSSetShaderResources(2,1,&shaderResource); context->PSSetShaderResources(2,1,&shaderResource); |
PNG、JPG、webP這些編碼過的格式不能給GPU使用,因為GPU讀取貼圖需要迅速找到任意位置的像素(即隨機存取,random access),這些格式必須完全解碼才能得知每個像素的值。用在GPU的壓縮格式必須設計成能隨機存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
之後建立貼圖物件,D3D11建立貼圖物件要這樣做:建立Texture2D物件代表記憶體空間,再建立shader resource view物件關聯到Texture2D物件,使用貼圖要套用shader resource view物件。
指定貼圖格式的方法一樣是填struct。
MSDN: D3D11_TEXTURE2D_DESC structure
Width、Height不用解釋,Format是像素是什麼格式,這裡byte順序是BGRA,每個分量8 bit。byte數可以從寬高和格式求出所以不用填byte數。
跟buffer物件一樣要填Usage和BindFlags,因為貼圖跟buffer一樣是記憶體空間。這裡又出現一種bind flag:D3D11_BIND_SHADER_RESOURCE,代表這塊區域要當作貼圖被shader讀取。
所有能用的bind flag見這篇,STREAM_OUTPUT、UNORDERED_ACCESS、DECODER、VIDEO_ENCODER這四項比較少用,其他的之後的教學有機會用到。
MSDN: D3D11_BIND_FLAG enumeration
可以好幾項位元or,例如framebuffer物件要作為畫布也要作為貼圖,要填D3D11_BIND_RENDER_TARGET|D3D11_BIND_SHADER_RESOURCE。
MipLevels、ArraySize、SampleDesc.Count這三個功能目前沒用到,但必須填1而不能是0,否則建立物件會失敗。
因為這是二維貼圖,D3D11_SUBRESOURCE_DATA要多填一個SysMemPitch:你準備的pixels一列是幾bytes,一維貼圖和buffer就不用填。
CreateShaderResourceView()第二參數其實也是個struct指定格式,填NULL代表從D3D11_TEXTURE2D_DESC得知格式,有些時候這個參數不能填NULL。
建立shaderResourceView之後,用texture->Release()將CreateTexture2D()增加的reference count釋放,讓它只剩下shaderResourceView對它的reference,之後刪除shaderResourceView就會同時刪除texture物件。
從loadTexture()返回之後要套用物件,這個物件vertex shader和pixel shader都會用到,要呼叫VSSetShaderResources()和PSSetShaderResources()套用。
-Rasterizer, depth, stencil, and blend-
//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); //depth stencil D3D11_DEPTH_STENCIL_DESC dsDesc; ZeroMemory(&dsDesc, sizeof(D3D11_DEPTH_STENCIL_DESC)); hr=device->CreateDepthStencilState(&dsDesc, &dsState); context->OMSetDepthStencilState(dsState,0); //blend D3D11_BLEND_DESC blendDesc; ZeroMemory(&blendDesc, sizeof(D3D11_BLEND_DESC)); D3D11_RENDER_TARGET_BLEND_DESC* blendDesc2 = blendDesc.RenderTarget; blendDesc2->BlendEnable = 1; blendDesc2->RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; blendDesc2->BlendOp = D3D11_BLEND_OP_ADD; blendDesc2->SrcBlend = D3D11_BLEND_SRC_ALPHA; blendDesc2->DestBlend = D3D11_BLEND_INV_SRC_ALPHA; blendDesc2->BlendOpAlpha = D3D11_BLEND_OP_ADD; blendDesc2->SrcBlendAlpha = D3D11_BLEND_ONE; blendDesc2->DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; hr=device->CreateBlendState(&blendDesc, &blendState); context->OMSetBlendState(blendState, 0, 0xffffffff); context->OMSetRenderTargets(1, &screenRenderTarget, 0); return 0; } //initSettings()結束 |
(Sc, Sa):pixel shader輸出的顏色和alpha,數值範圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
註:本篇特別把畫面的alpha設為1簡化算式。如果畫面的alpha不是1,那其實D3D和OpenGL的blend設定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時做一點特別處理。
以前有寫過一篇筆記:【程式】premultiply alpha的妙用
如果檔名是usetexture.cpp,用這個指令build
cl usetexture.cpp /Feusetexture.exe /O2 /MD /link user32.lib d3d11.lib winmm.lib windowscodecs.lib ole32.lib |
執行的樣子。
constant buffer、texture、sampler這三種物件如何將主程式和shader的物件對應,可以想成顯卡少女的工作臺有一些置物櫃,各有很多格子。
D3D11每個shader階段各有一個置物櫃,如果有使用geometry、hull或domain shader,那它們也各自有一個置物櫃。
本篇的shader裡有以下三行,其中register()裡的b2, t2, s2就是叫顯卡少女三種物件都拿2號格子的。(雖然編號是從0開始,本篇故意用2)
cbuffer cbuffer1 :register(b2){ Texture2D texture1 :register(t2); SamplerState sampler1 :register(s2); |
至於要怎麼指示顯卡少女把物件放進格子,上面程式裡有這些行
context->VSSetConstantBuffers(2,1,&constantBuffer); context->PSSetSamplers(2,1,&sampler); context->VSSetShaderResources(2,1,&shaderResource); context->PSSetShaderResources(2,1,&shaderResource); |
不過電腦裡的世界有個地方跟現實不一樣,如果在不同階段的置物櫃放進相同的物件,記憶體裡只會佔用一個物件的空間,不會真的把物件複製好幾份。
至於置物櫃具體有幾格,D3D的文件很難找到寫在哪裡,用一個方法查比較快:shader裡的register()故意填一個很大的值
cbuffer cbuffer1 :register(b200){ Texture2D texture1 :register(t200); SamplerState sampler1 :register(s200); |
error X4567: maximum cbuffer exceeded. target has 14 slots, manual bind to slot 200 failed |
由於compiler看到b200的錯誤就停止編譯,把b200改成b13再編譯一次,可看出貼圖有128格,取樣器有16格。