先前一直略過Direct3D的input layout與OpenGL的glVertexAttribPointer(),這篇總算來介紹了。
整個程式教學系列在此
Shark流程式教學一覽
先把struct和函式宣告拿出來看看,看它們有哪些欄位和參數。
D3D的相關struct和函式:
其中InputSlotClass和InstanceDataStepRate是用一個功能:draw instanced時才會用到,SemanticIndex用在矩陣或向量陣列,不過頂點資料很少會用到這兩個型態,本篇會講到SemanticName、Format、InputSlot、AlignedByteOffset這四個。
context->IASetVertexBuffers()的stride和offset參數也有影響。
再來OpenGL的相關函式:
size、type、normalize是資料格式,相當於D3D的Format欄位。
最後一個參數實際上是offset而不是指標,為何函式宣告是pointer見本篇最下面。
拿「Direct3D 11 使用貼圖」和「OpenGL 3.3 使用貼圖」的VERTEX_DATA1來說明。程式碼有些列因為超過巴哈姆特小屋寬度而分成兩列。
一、首先是C/C++裡的struct/function和shader裡的變數怎麼對應。
Direct3D裡,layout設定和shader裡的變數用semantic對應。
OpenGL兩個函式的index參數,和GLSL的layout(location)對應。
二、format,「shader的輸入與輸出」有說過GPU是以float[4]為基本型態,不過D3D和OpenGL可以支援很多型態的頂點資料,GPU讀取頂點資料時自動轉換為float向量。要告訴GPU以下資訊:
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以上才有。
本篇的資料是這樣
三、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(),在這裡才用上。
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。
跟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最早的版本用這種方法上傳頂點資料,一個函式上傳一個頂點的一項資料
如果有幾千個頂點,就得呼叫這些函式幾千次,效能消耗很大,而且當時沒有buffer物件這種東西,頂點資料是draw call之前才呼叫這些函式傳過去,不能事先把資料放在顯示記憶體。
1.1版增加這些函式
樣子已經跟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而不是in。
也增加了glVertexAttribPointer()能自己定義頂點資料。
不過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,改用下面三個函式設定頂點資料
format、offset可以初始化時用glVertexAttribFormat()設定,之後想切換buffer物件只要呼叫glBindVertexBuffer()就行了。本系列介紹的是OpenGL 3.3,因此不用這些函式。
整個程式教學系列在此
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); |
context->IASetVertexBuffers()的stride和offset參數也有影響。
再來OpenGL的相關函式:
void glEnableVertexAttribArray(GLuint index); void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer); |
最後一個參數實際上是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。
浮點數沒有標準化這回事,一律維持相同的值。
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)); } |
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); } |
資料上傳至顯示記憶體後如下:
這次不附圖了,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(); |
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); |
當時還是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; ………… |
也增加了glVertexAttribPointer()能自己定義頂點資料。
attribute vec2 pos; attribute vec2 texCoord; attribute vec4 color; |
上傳頂點資料仍然可以用即時上傳和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); |