前往
大廳
主題

【程式】Direct3D 11 使用貼圖

Shark | 2021-07-06 21:46:16 | 巴幣 134 | 人氣 811

前篇:Direct3D 11 架設基本繪圖管線
前一篇還沒有使用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;
}
D3D11以外的部分有這些改變:將視窗尺寸設得比較大以配合圖檔,且多出了WIC的東西:wincodec.h、wicFactory、main()裡面初始化與釋放的程式碼,這些是讀取圖檔會用到。

這次要傳給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},
};
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。
//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;
}
shader的輸入與輸出」提到的輸出入在這裡出現了,看本篇時可以跟那篇對照著看。

至於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
會產生兩個檔案:usetexture_vs和usetexture_ps,以後如果只有修改主程式而沒有修改shader,這兩個檔案不用重新編譯。

還有一種用法是後面加個/Fc參數
fxc usetexture.hlsl /T vs_4_0 /E vsMain /Fo usetexture_vs /Fc a.txt
會產生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

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();
}
initD3D()、deinitD3D和之前一樣。

static void nextFrame(){
  const float color[]={1,1,1,1};
  context->ClearRenderTargetView(screenRenderTarget, color);
  context->Draw(4, 0);
  swapChain->Present(0, 0);
}
背景換成白色且alpha=1,Draw()第一參數的頂點數量改成4。

static void deinitSettings(){
  vertexShader->Release();
  pixelShader->Release();
  inputLayout->Release();
  vertexData->Release();
  rsState->Release();
  dsState->Release();
  blendState->Release();

  constantBuffer->Release();
  sampler->Release();
  shaderResource->Release();
}
要刪除的物件增加3個。



接下來的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);
loadWholeFile()用到的Windows API函式請參照這篇:檔案操作—Windows篇,作業系統沒有直接提供「傳入檔名→讀取整個檔案」的函式,但這功能有時候會用到,寫一個函式做這件事。

剛剛編譯好的檔案就是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);
建buffer物件的方法跟「Direct3D 11 架設基本繪圖管線」一樣,input layout的話這次頂點有三項資料,要填三個struct D3D11_INPUT_ELEMENT_DESC,裡面欄位是什麼意思留待之後介紹。

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);
本篇新增的東西之一。建立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-

  //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);
本篇新增的東西之二。跟大部分D3D11物件一樣填一個struct設定參數。WRAP和Filter是什麼請參照這篇貼圖的部分:shader的輸入與輸出
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);
本篇新增的東西之三,用到一個輔助函式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
  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()結束
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
cl usetexture.cpp /Feusetexture.exe /O2 /MD /link user32.lib d3d11.lib winmm.lib
  windowscodecs.lib ole32.lib
比上次多了windowscodecs.lib和ole32.lib是因為用WIC讀取圖檔。

執行的樣子。




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);
函式名稱的VS、PS代表要設定哪一階段的shader,第一參數是起始格子編號,第二和第三參數是陣列長度和陣列,可以一次設定多個,如果傳入長度3的陣列,會設定2,3,4的格子。

不過電腦裡的世界有個地方跟現實不一樣,如果在不同階段的置物櫃放進相同的物件,記憶體裡只會佔用一個物件的空間,不會真的把物件複製好幾份。

至於置物櫃具體有幾格,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
由此可知constant buffer有14格(可填b0~b13)
由於compiler看到b200的錯誤就停止編譯,把b200改成b13再編譯一次,可看出貼圖有128格,取樣器有16格。

創作回應

ays.
原生的作法好長 Orz
2021-07-06 23:59:58

更多創作