前往
大廳
主題

【程式】Direct3D、OpenGL頂點資料layout設定

Shark | 2021-09-14 23:58:51 | 巴幣 1128 | 人氣 987

先前一直略過Direct3D的input layout與OpenGL的glVertexAttribPointer(),這篇總算來介紹了。

整個程式教學系列在此
Shark流程式教學一覽



先把struct和函式宣告拿出來看看,看它們有哪些欄位和參數。
D3D的相關struct和函式:
//D3D11_INPUT_ELEMENT_DESC成員
typedef struct D3D11_INPUT_ELEMENT_DESC {
  LPCSTR                     SemanticName;
  UINT                       SemanticIndex;
  DXGI_FORMAT                Format;
  UINT                       InputSlot;
  UINT                       AlignedByteOffset;
  D3D11_INPUT_CLASSIFICATION InputSlotClass;
  UINT                       InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;

context->IASetVertexBuffers(UINT StartSlot, UINT NumBuffers,
  ID3D11Buffer* const* ppVertexBuffers, const UINT *pStrides, const UINT *pOffsets);
其中InputSlotClass和InstanceDataStepRate是用一個功能:draw instanced時才會用到,SemanticIndex用在矩陣或向量陣列,不過頂點資料很少會用到這兩個型態,本篇會講到SemanticName、Format、InputSlot、AlignedByteOffset這四個。
context->IASetVertexBuffers()的stride和offset參數也有影響。

再來OpenGL的相關函式:
void glEnableVertexAttribArray(GLuint index);

void glVertexAttribPointer(GLuint index, GLint size, GLenum type,
    GLboolean normalized, GLsizei stride, const GLvoid* pointer);
size、type、normalize是資料格式,相當於D3D的Format欄位。
    最後一個參數實際上是offset而不是指標,為何函式宣告是pointer見本篇最下面。

拿「Direct3D 11 使用貼圖」和「OpenGL 3.3 使用貼圖」的VERTEX_DATA1來說明。程式碼有些列因為超過巴哈姆特小屋寬度而分成兩列。
//頂點資料,D3D與OpenGL共用
//定義資料型態為VertexData1,變數名稱是VERTEX_DATA1的資料

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},
};

//Direct3D
//initVertexData1()內,建立input layout物件

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},
};
device->CreateInputLayout(layoutDesc, 3, vsBytecode, vsBytecodeSize, outInputLayout);
context->IASetInputLayout(*outInputLayout);

//set vertex buffer
const UINT stride=sizeof(VERTEX_DATA1[0]);
const UINT offset=0;
context->IASetVertexBuffers(0, 1, outVertexData, &stride, &offset);

//HLSL
struct VsIn{
  float2 pos :P;
  int2 texCoord :T;
  float4 color :C;
};

//OpenGL
//initVertexData1()內,建立vertex array object

glBindBuffer(GL_ARRAY_BUFFER, *outVertexData);

glGenVertexArrays(1, outVao);
glBindVertexArray(*outVao);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT,0,
  sizeof(VertexData1), 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_SHORT,0,
  sizeof(VertexData1), (void*)offsetof(VertexData1, texCoord));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, GL_BGRA, GL_UNSIGNED_BYTE,1,
  sizeof(VertexData1), (void*)offsetof(VertexData1, color));

//GLSL
layout(location=0) in vec2 inPos;
layout(location=1) in vec2 inTexCoord;
layout(location=2) in vec4 inColor;

一、首先是C/C++裡的struct/function和shader裡的變數怎麼對應。
Direct3D裡,layout設定和shader裡的變數用semantic對應。


OpenGL兩個函式的index參數,和GLSL的layout(location)對應。


二、format,「shader的輸入與輸出」有說過GPU是以float[4]為基本型態,不過D3D和OpenGL可以支援很多型態的頂點資料,GPU讀取頂點資料時自動轉換為float向量。要告訴GPU以下資訊:
  • 有幾個分量?
    可填1~4。另外還可以填BGRA,顏色有時候用BGRA比較方便,但shader裡規定分量順序是RGBA,可以叫GPU自動把R和B對調。
  • 每個分量幾byte?是整數還是浮點數?
    8bit、16bit、32bit整數和32bit浮點數在C語言有直接支援,GPU還支援一些其他bit數的整數和浮點數。
  • 如果是整數,要不要標準化(normalize,或稱為正規化)?
    標準化是把整數的整個範圍轉換成0~1或-1~1的浮點數,無號整數對應到0~1,有號整數對應到-1~1。
    像是「無號8 bit整數(unsigned byte)」原始資料是0~255,shader裡使用這個值會把它除以255變成0~1。
    「有號8 bit整數(byte)」範圍是-128~127,會把-127~127對應到-1~1,剩下的-128會特別處理,也會變成-1。
    無號16 bit、無號32 bit整數就是把0~65535、0~4294967295對應到0~1,依此類推。

    例如傳一組BGRA值(132, 167, 237, 255)給GPU,將四個分量除以255,在shader裡會變成(0.517, 0.655, 0.929, 1.0)

    如果設定成不標準化,整數會被轉換成同值的浮點數,原始資料是255,shader裡看到的就是255.0。
    浮點數沒有標準化這回事,一律維持相同的值。
D3D把以上項目排列組合,就形成DXGI_FORMAT enumeration的一大堆常數。
MSDN: DXGI_FORMAT enumeration
有些常數包含D、S、X的分量,且資料型態還有一種TYPELESS,這些是用在貼圖和framebuffer,頂點資料不會用到。

常用的資料型態有這些
FLOAT:浮點數
SINT:有號整數
UINT:無號整數
SNORM:有號整數標準化
UNORM:無號整數標準化
整數有多大是用「各分量幾bits」指定。

OpenGL用size、type、normalized這三個參數設定,能填的值參照這個函式的說明。
OpenGL wiki: glVertexAttribPointer()
這篇還列了兩個函式,glVertexAttribIPointer()只能用在整數、非標準化的情況,glVertexAttribLPointer()只能用GL_DOUBLE(64bit浮點數)型態,且要OpenGL 4.1以上才有。

本篇的資料是這樣
位置 貼圖坐標 顏色
C語言型態 float[2] short[2] uint8_t[4]
幾個分量 2 2 BGRA
分量型態 32 bit float 16 bit有號整數 8 bit無號整數
標準化
Direct3D填法 R32G32_FLOAT R16G16_SINT B8G8R8A8_UNORM
OpenGL填法
(size, type, normalize)
2,
GL_FLOAT,
0
2,
GL_SHORT,
0
GL_BGRA,
GL_UNSIGNED_BYTE,
1

三、stride和offset,這就要想像一下資料在記憶體裡是怎麼排列的。
stride:兩個頂點之間相隔幾byte。
offset:第一個頂點在buffer裡第幾個byte。
VERTEX_DATA1在記憶體裡是這樣

如上圖先是第一個頂點的位置、貼圖坐標、顏色,然後第二個頂點的位置、貼圖坐標、顏色,繼續第三個、第四個頂點,這種排列方式叫interleaved(交錯式)。
在C/C++裡可以不用自己填數字,用sizeof()和offsetof()的語法,填型態或變數名稱就自動算出byte數。

Direct3D填法

D3D有兩個offset欄位:struct D3D11_INPUT_ELEMENT_DESC有一個,IASetVertexBuffers()的參數也有一個,實際的offset是兩者相加。兩者差別是input layout物件建好之後就不可更改,IASetVertexBuffers()的參數每次呼叫函式時可修改。
stride要用IASetVertexBuffers()的參數設定,由於3項資料stride都一樣,三個InputSlot都填0就可以讓三者共用一個stride。
IASetVertexBuffers()可以傳入多個buffer物件、stride和offset,這個例子只用到一個,下面會看到傳入兩個以上的用法。

OpenGL填法

glBindBuffer()並不是將vertex buffer放進繪圖管線,只是在內部設定一個全域變數,之後glVertexAttribPointer()才會讀取它得知資料來自哪個buffer物件。可以這樣想:頂點資料可以來自複數個buffer,但全域變數GL_ARRAY_BUFFER只能儲存一個buffer物件,所以只用glBindBuffer()把vertex buffer放進繪圖管線是不夠用的。
offset是整數型態,但是函式宣告的offset參數是void*型態,要轉型才能編譯通過。
呼叫一連串glVertexAttribPointer()之後,「資料來源是哪個buffer」和「資料的layout」就儲存在vertex array object裡了,之後bind這個VAO就套用這些資訊。

D3D的layout物件和OpenGL的VAO大致都是儲存頂點layout,但兩者用法有些不同。如果有好幾個vertex buffer共用一個layout,D3D只要建立一個layout物件,之後用IASetVertexBuffers()切換buffer;OpenGL要為每個vertex buffer建立一個VAO,每次建VAO時都要呼叫一連串glVertexAttribPointer()設定layout。



接下來試試其他的layout,看了可以更了解layout怎麼填。

Direct3D 11 使用貼圖」和「OpenGL 3.3 使用貼圖」當時省略了initVertexData2(),在這裡才用上。
//Direct3D
//定義資料型態為VertexData2,變數名稱是VERTEX_DATA2的資料

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},
};

static void initVertexData2(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_DATA2);
  buDesc.Usage = D3D11_USAGE_IMMUTABLE;
  buDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
  D3D11_SUBRESOURCE_DATA data;
  data.pSysMem = &VERTEX_DATA2;
  hr=device->CreateBuffer(&buDesc, &data, outVertexData);
  const UINT stride[]={sizeof(float)*2, sizeof(short)*2, sizeof(uint32_t)};
  const UINT offset[]={0, offsetof(VertexData2, texCoord), offsetof(VertexData2, color)};
  ID3D11Buffer* buffers[]={*outVertexData, *outVertexData, *outVertexData};
  context->IASetVertexBuffers(0,3, buffers, 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,   1,0, D3D11_INPUT_PER_VERTEX_DATA ,0},
    {"C",0, DXGI_FORMAT_B8G8R8A8_UNORM,2,0, D3D11_INPUT_PER_VERTEX_DATA ,0},
  };
  hr=device->CreateInputLayout(layoutDesc, 3, vsBytecode,vsBytecodeSize,outInputLayout);
  context->IASetInputLayout(*outInputLayout);
}

//OpenGL
typedef struct{
  float pos[8];
  short texCoord[8];
  uint32_t color[4];
} VertexData2;
VertexData2 VERTEX_DATA2={
  {100,0,100,400,400,0,400,400},
  {0,0,0,400,300,0,300,400},
  {0xffffffff,0xffffffff,0xffffffff,0xffffffff},
};

static void initVertexData2(uint32_t* outVertexData, uint32_t* outVao){
  //vertex buffer
  glGenBuffers(1, outVertexData);
  glBindBuffer(GL_ARRAY_BUFFER, *outVertexData);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA2), &VERTEX_DATA2, GL_STATIC_DRAW);

  //vertex array object
  glGenVertexArrays(1,outVao);
  glBindVertexArray(*outVao);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, 0,
    sizeof(float)*2, 0); //stride(第5參數)可以填0
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_SHORT, 0,
    sizeof(short)*2, (void*)offsetof(VertexData2, texCoord));
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, GL_BGRA, GL_UNSIGNED_BYTE, 1,
    sizeof(uint32_t), (void*)offsetof(VertexData2, color));
}
C/C++與shader的對應、以及format跟上面的例子一樣,只有offset和stride要修改。

VERTEX_DATA2在記憶體裡如下

把所有頂點的位置排在一起,然後所有頂點的貼圖坐標,其他資料繼續接下去,這種好像就叫non-interleaved而沒有特別的名稱,不過在YUV色彩格式裡這種排列方式稱為planar。

Direct3D

這裡假設layout會用在不止4個頂點的情況,offset會隨頂點數量而變,因此把offset填在IASetVertexBuffers()的參數。
由於三項資料的stride和offset不是完全相同,IASetVertexBuffers()要傳入長度3的陣列,D3D11_INPUT_ELEMENT_DESC的InputSlot欄位也要配合填寫。
因為三項資料來自同一個buffer,buffers[]填三個相同的buffer物件。

OpenGL

glVertexAttribPointer()的第五、第六參數要改。
OpenGL有一種用法,如果頂點資料是一個緊接著一個,像是本例的「頂點1位置」和「頂點2位置」之間沒有其他資料,那stride可以填0,函式會用size和type參數算出stride。D3D就不能這樣做。

在「Direct3D 11 使用貼圖」和「OpenGL 3.3 使用貼圖」的程式自己把initVertexData2()加上,initSettings()裡改成呼叫initVertexData2(),shader完全不用改,繪製結果會跟initVertexData1()一樣。

再來看第三種layout。
//Direct3D
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},
};

//outVertexData必須是ID3D11Buffer*[3]
static void initVertexData3(const char* vsBytecode, int vsBytecodeSize,
ID3D11Buffer** outVertexData, ID3D11InputLayout** outInputLayout){
  HRESULT hr;
  //先填好三個buffer共通的屬性
  D3D11_BUFFER_DESC buDesc;
  ZeroMemory(&buDesc, sizeof(D3D11_BUFFER_DESC));
  buDesc.Usage = D3D11_USAGE_IMMUTABLE;
  buDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
  D3D11_SUBRESOURCE_DATA data;
  //建立三個buffer物件
  buDesc.ByteWidth = sizeof(VERTEX_DATA2.pos);
  data.pSysMem = VERTEX_DATA2.pos;
  hr=device->CreateBuffer(&buDesc, &data, outVertexData);
  buDesc.ByteWidth = sizeof(VERTEX_DATA2.texCoord);
  data.pSysMem = VERTEX_DATA2.texCoord;
  hr=device->CreateBuffer(&buDesc, &data, outVertexData+1);
  buDesc.ByteWidth = sizeof(VERTEX_DATA2.color);
  data.pSysMem = VERTEX_DATA2.color;
  hr=device->CreateBuffer(&buDesc, &data, outVertexData+2);
  const UINT stride[]={sizeof(float)*2, sizeof(short)*2, sizeof(uint32_t)};
  const UINT offset[]={0, 0, 0};
  ID3D11Buffer* buffers[]={outVertexData[0], outVertexData[1], outVertexData[2]};
  context->IASetVertexBuffers(0,3, buffers, 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,   1,0, D3D11_INPUT_PER_VERTEX_DATA ,0},
    {"C",0, DXGI_FORMAT_B8G8R8A8_UNORM,2,0, D3D11_INPUT_PER_VERTEX_DATA ,0},
  };
  hr=device->CreateInputLayout(layoutDesc, 3, vsBytecode,vsBytecodeSize,outInputLayout);
  context->IASetInputLayout(*outInputLayout);
}

//OpenGL
typedef struct{
  float pos[8];
  short texCoord[8];
  uint32_t color[4];
} VertexData2;
VertexData2 VERTEX_DATA2={
  {100,0,100,400,400,0,400,400},
  {0,0,0,400,300,0,300,400},
  {0xffffffff,0xffffffff,0xffffffff,0xffffffff},
};

//outVertexData必須是uint32_t[3]
static void initVertexData3(uint32_t* outVertexData, uint32_t* outVao){
  glGenBuffers(3, outVertexData);
  glGenVertexArrays(1,outVao);
  glBindVertexArray(*outVao);

  //set vertex array object
  //位置

  glBindBuffer(GL_ARRAY_BUFFER, outVertexData[0]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA2.pos), VERTEX_DATA2.pos,
    GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, 0, sizeof(float)*2, 0);
  //貼圖坐標
  glBindBuffer(GL_ARRAY_BUFFER, outVertexData[1]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA2.texCoord), VERTEX_DATA2.texCoord,
    GL_STATIC_DRAW);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_SHORT, 0, sizeof(short)*2, 0);
  //顏色
  glBindBuffer(GL_ARRAY_BUFFER, outVertexData[2]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA2.color), VERTEX_DATA2.color,
    GL_STATIC_DRAW);
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, GL_BGRA, GL_UNSIGNED_BYTE, 1, sizeof(uint32_t), 0);
}
跟initVertexData2()用相同的資料,不過建了3個buffer物件,傳回值的outVertexData也必須是長度3的陣列,這段要怎麼嵌入使用貼圖篇的程式就請自己研究。
資料上傳至顯示記憶體後如下:

這次不附圖了,layout怎麼設定請自己看程式碼。

D3D要呼叫三次CreateBuffer()建立物件,三個物件只有byte數和指標不一樣,其餘屬性都相同,然後IASetVertexBuffers()第三參數傳入三個buffer物件。
OpenGL的glGenBuffers()第一參數填3產生3個buffer編號,之後做三次「glBindBuffer()切換buffer物件 → glBufferData()將資料上傳 → glEnableVertexAttribArray()與glVertexAttribPointer()設定頂點layout」。

範例3的效能會比較差,因為一個頂點的資料要從三個有一段距離的地方讀取,把資料放在相鄰的地方讀取會比較快,所以儘量用範例1的交錯式儲存,如果有non-interleaved的需求也儘量如範例2把資料放進同一個buffer。



有沒有覺得奇怪,glVertexAttribPointer()最後一個參數是byte數但為什麼型態是void*,每次使用時都要轉型,這就要講歷史了。

OpenGL最早的版本用這種方法上傳頂點資料,一個函式上傳一個頂點的一項資料
glBegin(GL_TRIANGLE_STRIP);
  //頂點1
  glVertex2f(100, 0);
  glTexCoord2s(0, 0);
  glColor4ub(255,255,255,255);
  //頂點2
  glVertex2f(100, 400);
  glTexCoord2s(0, 400);
  glColor4ub(255,255,255,255);
  …………
glEnd();
如果有幾千個頂點,就得呼叫這些函式幾千次,效能消耗很大,而且當時沒有buffer物件這種東西,頂點資料是draw call之前才呼叫這些函式傳過去,不能事先把資料放在顯示記憶體。

1.1版增加這些函式
glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
glColorPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);

//用法
glVertexPointer(2, GL_FLOAT, sizeof(VertexData1), VERTEX_DATA1[0].pos);
樣子已經跟glVertexAttribPointer()有點像了,準備好像上面VERTEX_DATA1的陣列,最後一個參數填陣列指標,之後draw call時一次上傳整個陣列。
當時還是fixed-function pipeline,你能做的事是上傳位置、貼圖坐標、顏色以後照系統內定的算式計算,不能任意寫算式或增加自訂資料。

1.5版新增了buffer物件,就是本系列使用的方法,只要上傳一次,之後套用buffer物件就能使用這些資料,減少傳輸量。
但設定頂點格式沿用上面glVertexPointer()這些函式,且仍然保留即時上傳的方法:glBindBuffer()填0代表即時上傳,以上函式最後一個參數是指標,glBindBuffer()填非0代表頂點資料來自buffer物件,最後一個參數變成byte數。
(另一篇有說過,gen函式產生的物件編號不會傳回0,0作為特殊用途,具體用途依物件種類而異)

2.0版增加了shader可以自由地寫算式,如果使用glVertexPointer()這些函式傳頂點資料,GLSL裡用一些內建變數取得資料。
attribute vec4 gl_Vertex;
attribute vec4 gl_MultiTexCoord0;
attribute vec4 gl_Color;
    …………
當時頂點資料前面的保留字是attribute而不是in。
也增加了glVertexAttribPointer()能自己定義頂點資料。
attribute vec2 pos;
attribute vec2 texCoord;
attribute vec4 color;
不過2.0版沒有「layout(location=?)」的語法,設定編號要在主程式用glBindAttribLocation()。
上傳頂點資料仍然可以用即時上傳和buffer物件兩種方法,即時上傳時glVertexAttribPointer()最後一個參數是陣列指標。

3.0版把一些功能列為deprecated,廢除了即時上傳的方法與fixed-function pipeline,一定要用buffer物件儲存資料、寫shader計算頂點,且glVertexPointer()這些函式消失了,所以3.0以後設定頂點資料只能用glVertexAttribPointer(),最後一個參數變成只有byte數的用途。

glVertexAttribPointer()用一個函式設定buffer物件、index、format、stride、offset,每次呼叫都必須把所有參數填入,4.3版新增一個擴充ARB_vertex_attrib_binding,改用下面三個函式設定頂點資料
void glBindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLintptr stride);
void glVertexAttribBinding(GLuint attribindex, GLuint bindingindex);
void glVertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
  GLuint relativeoffset);
format、offset可以初始化時用glVertexAttribFormat()設定,之後想切換buffer物件只要呼叫glBindVertexBuffer()就行了。本系列介紹的是OpenGL 3.3,因此不用這些函式。

創作回應

%%鼠 拒收病婿
兩個一起學,好猛[e22]
2024-03-29 23:35:26

更多創作