2.頂點著色器
頂點著色器(vertex shader)是一個在顯卡的GPU上執行的程序,它替換了固定功能管道(fixed function pipeline)中的變換(transformation)和光照(lighting)階段(這不是百分之百的正確,因為頂點著色器可以被Direct3D運行時(Direct3D runtime)以軟件模擬,如果硬件不支持頂點著色器的話)。圖2.1說明了管線中頂點著色器替換的部件。

圖2.1
由于頂點著色器是我們(在HLSL中)寫的一個自定義程序,因此我們在圖形效果方面獲得了極大的自由性。我們不再受限于Direct3D的固定光照算法。此外,應用程序操縱頂點位置的能力也有了多樣性,例如:布料仿真,粒子系統的點大小操縱,還有頂點混合/變形。此外,我們的頂點數據結構更自由了,并且可以在可編程管線中包含比在固定功能管線中多的多的數據。
正如作者所在群的公告所說,“拍照不在于你對相機使用的熟練程度,而是在于你對藝術的把握。”之前的介紹使讀者對著色器的編寫和使用都有了一定的了解,下面我們將把重心從介紹如何使用著色器轉到如何實現更高級的渲染效果上來。
2.1可編程數據流模型
DirectX 8.0引入了數據流的概念,可以這樣理解數據流(圖2.2):

圖2.2
· 一個頂點由n個數據流組成。
· 一個數據流由m個元素組成。
· 一個元素是[位置、顏色、法向、紋理坐標]。
程序中使用IDirect3DDevice9::SetStreamSource方法把一個頂點緩存綁定到一個設備數據流。
2.2頂點聲明
該小節對頂點聲明的描述絕大多數都取自翁云兵的《著色器和效果》,該文對頂點聲明的描述是我所見到最詳盡最透徹的,這里向作者表示敬意:)
到現在為止,我們已經使用自由頂點格式(flexible vertex format,FVF)來描述頂點結構中的各分量。但是,在可編程管線中,我們的頂點數據可以包含比用FVF所能表達的多的多的數據。因此,我們通常使用更具表達性的并且更強有力的頂點聲明(vertex declaration)。
注意:我們仍然可以在可編程管線中使用FVF——如果我們的頂點格式可以這樣描述。不管怎樣,這只是為了方便,因為FVF會在內部被轉換為一個頂點聲明。
2.2.1 描述頂點聲明
我們將一個頂點聲明描述為一個D3DVERTEXELEMENT9結構的數組。D3DVERTEXELEMENT9數組中的每個元素描述了一個頂點的分量。所以,如果你的頂點結構有三個分量(例如:位置、法線、顏色),那么其相應的頂點聲明將會被一個含3個元素的D3DVERTEXELEMENT9結構數組描述。
D3DVERTEXELEMENT9結構定義如下:
typedef struct _D3DVERTEXELEMENT9 {
BYTE Stream;
BYTE Offset;
BYTE Type;
BYTE Method;
BYTE Usage;
BYTE UsageIndex;
} D3DVERTEXELEMENT9;
² Stream——指定關聯到頂點分量的流;
² Offset——偏移,按字節,相對于頂點結構成員的頂點分量的開始。例如,如果頂點結構是:
struct Vertex
{
D3DXVECTOR3 pos;
D3DXVECTOR3 normal;
};
……pos分量的偏移是0,因為它是第一個分量;normal分量的偏移是12,因為sizeof(pos) == 12。換句話說,normal分量以Vertex的第12個字節為開始。
² Type——指定數據類型。它可以是D3DDECLTYPE枚舉類型的任意成員;完整列表請參見文檔。常用類型如下:
D3DDECLTYPE_FLOAT1——浮點數值
D3DDECLTYPE_FLOAT2——2D浮點向量
D3DDECLTYPE_FLOAT3——3D浮點向量
D3DDECLTYPE_FLOAT4——4D浮點向量
D3DDECLTYPE_D3DCOLOR—D3DCOLOR類型,它擴展為RGBA浮點顏色向量(r, g, b, a),其每一分量都是歸一化到區間[0, 1]了的。
² Method——指定網格化方法。我們認為這個參數是高級的,因此我們使用默認值,標識為D3DDECLMETHOD_DEFAULT。
² Usage——指定已計劃的對頂點分量的使用。例如,它是否準備用于一個位置向量、法線向量、紋理坐標等,有效的用途標識符(usage identifier)是D3DDECLUSAGE枚舉類型的:
typedef enum _D3DDECLUSAGE {
D3DDECLUSAGE_POSITION = 0, // Position.
D3DDECLUSAGE_BLENDWEIGHTS = 1, // Blending weights.
D3DDECLUSAGE_BLENDINDICES = 2, // Blending indices.
D3DDECLUSAGE_NORMAL = 3, // Normal vector.
D3DDECLUSAGE_PSIZE = 4, // Vertex point size.
D3DDECLUSAGE_TEXCOORD = 5, // Texture coordinates.
D3DDECLUSAGE_TANGENT = 6, // Tangent vector.
D3DDECLUSAGE_BINORMAL = 7, // Binormal vector.
D3DDECLUSAGE_TESSFACTOR = 8, // Tessellation factor.
D3DDECLUSAGE_POSITIONT = 9, // Transformed position.
D3DDECLUSAGE_COLOR = 10, // Color.
D3DDECLUSAGE_FOG = 11, // Fog blend value.
D3DDECLUSAGE_DEPTH = 12, // Depth value.
D3DDECLUSAGE_SAMPLE = 13 // Sampler data.
} D3DDECLUSAGE;
其中,D3DDECLUSAGE_PSIZE類型用于指定一個頂點的點的大小。它用于點精靈,因此我們可以基于每個頂點控制其大小。一個D3DDECLUSAGE_POSITION成員的頂點聲明意味著這個頂點已經被變換,它通知圖形卡不要把這個頂點送到頂點處理階段(變形和光照)。
² UsageIndex——用于標識多個相同用途的頂點分量。這個用途索引是位于區間[0, 15]間的一個整數。例如,假設我們有三個用途為D3DDECLUSAGE_NORMAL的頂點分量。我們可以為第一個指定用途索引為0,為第二個指定用途索引為1,并且為第三個指定用途索引為2。按這種方式,我們可以通過其用途索引標識每個特定的法線。
例:假設我們想要描述的頂點格式由兩個數據流組成,第一個數據流包含位置、法線、紋理坐標3個分量,第二個數據流包含位置和紋理坐標2個分量,頂點聲明可以指定如下:
D3DVERTEXELEMENT9 decl[] =
{
//第一個數據流,包含分量位置、法線、紋理坐標
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_
POSITION, 0 },
{ 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
NORMAL, 0 },
{ 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
TEXCOORD, 0 },
//第一個數據流,包含分量位置、紋理坐標
{ 1, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
POSITION, 1 },
{ 1, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
NORMAL, 1 },
D3DDECL_END()
};
D3DDECL_END宏用于初始化D3DVERTEXELEMENT9數組的最后一個頂點元素。
2.2.2創建頂點聲明
CreateVertexDeclaration函數用于創建頂點聲明,decl為指向上一小節定義的D3DVERTEXELEMENT9數組的指針,函數返回IDirect3DVertexDeclaration9指針g_Decl;
IDirect3DVertexDeclaration9 *g_Decl = NULL;
g_pd3dDevice->CreateVertexDeclaration(decl ,&g_Decl);
2.2.3設置頂點聲明
g_pd3dDevice->SetVertexDeclaration(g_Decl);
至此,可編程數據流模型、頂點聲明介紹完畢,在下面的例子中讀者將會有更連貫的理解。
2.3用頂點著色器實現漸變動畫
2.3.1漸變動畫(Morphing)
Morphing漸變是20世紀90年代出現的一種革命性的計算機圖形技術,該技術使得動畫序列平滑且易于處理,即使在低檔配置的計算機系統上也能正常運行。
漸變是指隨時間的變化把一個形狀改變為另一個形狀。對我們而言,這些形狀就是Mesh網格模型。漸變網格模型的處理就是以時間軸為基準,逐漸地改變網格模型頂點的坐標,從一個網格模型的形狀漸變到另外一個。請看圖2.3:

圖2.3
我們在程序中使用兩個網格模型——源網格模型和目標網格模型,設源網格模型中頂點1的坐標為A(Ax,Ay,Az),目標網格模型中對應頂點1的坐標為B(Bx,By,Bz),要計算漸變過程中時間點t所對應的頂點1的坐標C(Cx,Cy,Cz),我們使用如下方法:
T為源網格模型到目標網格模型漸變所花費的全部時間,得到時間點t占整個過程T的比例為:
S = t / T
那么頂點1在t時刻對應的坐標C為:
C = A * (1-S)+ B * S
這樣,在渲染過程中我們根據時間不斷調整S的值,就得到了從源網格模型(形狀一)到目標網格模型(形狀二)的平滑過渡。
接下來將在程序里使用頂點著色器實現我們的漸變動畫。
2.3.2漸變動畫中的頂點聲明
程序中,我們設定一個頂點對應兩個數據流,這兩個數據流分別包含了源網格模型的數據和目標網格模型的數據。渲染過程中,我們在著色器里根據兩個數據流中的頂點數據以及時間值確定最終的頂點信息。
個數據流包含分量如下:
源網格模型數據流:頂點位置、頂點法線、紋理坐標;
目標網格模型數據流:頂點位置、頂點法線;
注意目標網格模型數據流沒有包含紋理坐標,因為紋理對于兩個網格模型都是一樣的,所以僅使用源網格模型的紋理就可以了。
頂點聲明指定如下:
D3DVERTEXELEMENT9 decl[] =
{
//源網格模型數據流,包含分量位置、法線、紋理坐標
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_
POSITION, 0 },
{ 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
NORMAL, 0 },
{ 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
TEXCOORD, 0 },
//目標網格模型數據流,包含分量位置、紋理坐標
{ 1, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
POSITION, 1 },
{ 1, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_
NORMAL, 1 },
D3DDECL_END()
};
2.3.3漸變動畫中的頂點著色器
下面給出頂點著色器源碼,代碼存儲于vs.txt中,該頂點著色器根據源網格模型數據流和目標網格模型數據流中的信息以及時間標尺值計算出頂點最終位置信息,并對頂點做了坐標變換和光照處理。代碼中給出了詳細的注釋,幫助讀者理解。
//全局變量
//世界矩陣、觀察矩陣、投影矩陣的合矩陣,用于頂點的坐標變換
matrix WVPMatrix;
//光照方向
vector LightDirection;
//存儲2.3.1小節提到的公式S = t / T中的時間標尺S值
//注意到Scalar是一個vector類型,我們在Scalar.x中存儲了S值,Scalar.y中存儲的則是(1-S)值
vector Scalar;
//輸入
struct VS_INPUT
{
//對應源網格模型數據流中的頂點分量:位置、法線、紋理坐標
vector position : POSITION;
vector normal : NORMAL;
float2 uvCoords : TEXCOORD;
//對應目標網格模型數據流中的頂點分量:位置、法線
vector position1 : POSITION1;
vector normal1 : NORMAL1;
};
//輸出
struct VS_OUTPUT
{
vector position : POSITION;
vector diffuse : COLOR;
float2 uvCoords : TEXCOORD;
};
//入口函數
VS_OUTPUT Main(VS_INPUT input)
{
VS_OUTPUT output = (VS_OUTPUT)0;
//頂點最終位置output.position取決于源網格模型數據流中位置信息input.position和目標網格模型數據流中位置信息input.position1以及時間標尺Scalar的值
//對應2.3.1小節中的公式C = A * (1-S)+ B * S
output.position = input.position*Scalar.x + input.position1*Scalar.y;
//頂點坐標變換操作
output.position = mul(output.position, WVPMatrix);
//計算頂點最終法線值
vector normal = input.normal*Scalar.x + input.normal1*Scalar.y;
//逆光方向與法線的點積,獲得漫射色彩
output.diffuse = dot((-LightDirection), normal);
//存儲紋理坐標
output.uvCoords = input.uvCoords;
return output;
}
以上是本例用到的頂點著色器,在接下來的應用程序中,我們將給三個著色器全局變量賦值:
² WVPMatrix;
世界矩陣、觀察矩陣、投影矩陣的合矩陣,用于頂點的坐標變換;
² LightDirection
光照方向;
² Scalar
存儲2.3.1小節提到的公式S = t / T中的時間標尺S值;
注意到Scalar是一個vector類型,我們在Scalar.x中存儲了S值,Scalar.y中存儲的則是(1-S)值;
2.3.4應用程序
我們在應用程序中執行以下操作:
· 加載兩個兩個Mesh模型:源網格模型,目標網格模型;
· 創建、設置頂點聲明;
· 創建、設置頂點著色器;
· 為著色器全局賦值;
· 把兩個Mesh模型數據分別綁定到兩個數據流中;
· 渲染Mesh模型;
下面是應用程序代碼:
…
/*********************聲明變量*****************/
//兩個指向LPD3DXMESH的指針,分別用于存儲源網格模型和目標網格模型;
LPD3DXMESH g_SourceMesh;
LPD3DXMESH g_TargetMesh;
//頂點聲明指針
IDirect3DVertexDeclaration9 *g_Decl = NULL;
//頂點著色器
IDirect3DVertexShader9 *g_VS = NULL;
//常量表
ID3DXConstantTable* ConstTable = NULL;
//常量句柄
D3DXHANDLE WVPMatrixHandle = 0;
D3DXHANDLE ScalarHandle = 0;
D3DXHANDLE LightDirHandle = 0;
…
/***************程序初始化*****************/
//加載源、目標網格模型
Load_Meshes();
//頂點聲明
D3DVERTEXELEMENT9 MorphMeshDecl[] =
{
//1st stream is for source mesh - position, normal, texcoord
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 },
{ 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },
//2nd stream is for target mesh - position, normal
{ 1, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 1 },
{ 1, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 1 },
D3DDECL_END()
};
//創建頂點著色器
ID3DXBuffer* shader = NULL;
ID3DXBuffer* errorBuffer = NULL;
D3DXCompileShaderFromFile("vs.txt",
0,
0,
"Main", // entry point function name
"vs_1_1",
D3DXSHADER_DEBUG,
&shader,
&errorBuffer,
&ConstTable);
if(errorBuffer)
{
::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0);
ReleaseCOM(errorBuffer);
}
//創建頂點著色器
g_pd3dDevice->CreateVertexShader((DWORD*)shader->GetBufferPointer(), &g_VS);
//創建頂點聲明
g_pd3dDevice->CreateVertexDeclaration(MorphMeshDecl ,&g_Decl);
//得到各常量句柄
WVPMatrixHandle = ConstTable->GetConstantByName(0, "WVPMatrix");
ScalarHandle = ConstTable->GetConstantByName(0, "Scalar");
LightDirHandle = ConstTable->GetConstantByName(0, "LightDirection");
//為著色器全局變量LightDirection賦值
ConstTable->SetVector(g_pd3dDevice, LightDirHandle, &D3DXVECTOR4(0.0f, -1.0f, 0.0f, 0.0f));
//設置各著色器變量為默認值
ConstTable->SetDefaults(g_pd3dDevice);
…
/*******************渲染*******************/
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(153,153,153), 1.0f, 0 );
g_pd3dDevice->BeginScene();
//為著色器全局變量WVPMatrix賦值
D3DXMATRIX matWorld, matView, matProj;
g_pd3dDevice->GetTransform(D3DTS_WORLD, &matWorld);
g_pd3dDevice->GetTransform(D3DTS_VIEW, &matView);
g_pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj);
D3DXMATRIX matWVP;
matWVP = matWorld * matView * matProj;
ConstTable->SetMatrix(g_pd3dDevice, WVPMatrixHandle, &matWVP);
//為著色器全局變量Scalar賦值,注意程序中獲取時間標尺值Scalar的方法
float DolphinTimeFactor = (float)(timeGetTime() % 501) / 250.0f;
float Scalar =
(DolphinTimeFactor<=1.0f)?DolphinTimeFactor:(2.0f-DolphinTimeFactor);
ConstTable->SetVector(g_pd3dDevice,ScalarHandle,&D3DXVECTOR4(1.0f-Scalar, Scalar, 0.0f, 0.0f));
//設置頂點著色器和頂點聲明
g_pd3dDevice->SetVertexShader(g_VS);
g_pd3dDevice->SetVertexDeclaration(g_Decl);
//綁定目標網格模型的定點緩存到第二個數據流中
IDirect3DVertexBuffer9 *pVB = NULL;
g_TargetMesh->GetVertexBuffer(&pVB);
g_pd3dDevice->SetStreamSource(1, pVB, 0,
D3DXGetFVFVertexSize(g_TargetMesh->GetFVF()));
ReleaseCOM(pVB);
//綁定源網格模型的頂點緩存到第一個數據流中
g_SourceMesh->GetVertexBuffer(&pVB);
g_pd3dDevice->SetStreamSource(0, pVB, 0,
D3DXGetFVFVertexSize(g_TargetMesh->GetFVF()));
ReleaseCOM(pVB);
//繪制Mesh網格模型
DrawMesh(g_SourceMesh, g_pMeshTextures0, g_VS, g_Decl);
g_pd3dDevice->EndScene();
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
…
2.3.5對應用程序的一點說明
程序中我們使用SetStreamSource方法把源網格模型和目標網格模型中的頂點緩存分別綁定到兩個設備數據流,但是Direct3D對數據流中的數據的真正引用只有在調用諸如DrawPrimitive、DrawIndexedPrimitive之類的繪制方法時才發生,因此在繪制Mesh網格模型時我們不能再使用傳統的DrawSubmit方法,而是使用了DrawIndexedPrimitive,下面就如何調用DrawIndexedPrimitive繪制Mesh模型進行說明,該部分內容和HLSL著色器關系不大,在這里列出僅僅是為了大家理解程序的完整性,讀者完全可以跳過本節不看。
使用DrawIndexedPrimitive繪制Mesh模型的步驟如下:
1. 加載網格模型后使用OptimizeInPlace方法對Mesh進行優化;
2. 一旦優化了網格模型,你就可以查詢ID3DXMesh對象,得到一個D3DXATTRIBUTERANGE數據類型的數組,我們稱之為屬性列表,該數據類型被定義如下:
typedef struct_D3DXATTRIBUTERANGE{
DWORD AttribId; //子集編號
DWORD FaceStart; //這兩個變量用于圈定本子集中的多邊形
DWORD FaceCount;
DWORD VertexStart; //這兩個變量用于圈定本子集中的頂點
DWORD VertexCount;
} D3DXATTRIBUTERANGE;
我們屬性列表中的每一項都代表一個被優化后Mesh的一個子集,D3DXATTRIBUTERANGE結構的各字段描述了該子集的信息。
1. 得到屬性數據后,我們就調用DrawIndexedPrimitive方法可以精美地渲染子集了。
下面是繪制Mesh模型的程序代碼:
在Load_Meshes()函數的最后,我們使用OptimizeInPlace方法對源網格模型和目標網格模型進行優化,其他加載材質和紋理的操作和之前一樣,相信大家能夠理解:
…
//優化源網格模型
g_SourceMesh->OptimizeInplace(D3DXMESHOPT_ATTRSORT, NULL, NULL, NULL, NULL);
…
//優化目標網格模型
g_TargetMesh->OptimizeInplace(D3DXMESHOPT_ATTRSORT, NULL, NULL, NULL, NULL);
…
在Draw_Mesh()函數中,渲染模型,注意程序是如何配合屬性表調用DrawIndexedPrimitive方法進行繪制的:
…
//分別得到指向Mesh模型頂點緩存區和索引緩存區的指針
IDirect3DVertexBuffer9 *pVB = NULL;
IDirect3DIndexBuffer9 *pIB = NULL;
pMesh->GetVertexBuffer(&pVB);
pMesh->GetIndexBuffer(&pIB);
//得到Mesh模型的屬性列表
DWORD NumAttributes;
D3DXATTRIBUTERANGE *pAttributes = NULL;
pMesh->GetAttributeTable(NULL, &NumAttributes);
pAttributes = new D3DXATTRIBUTERANGE[NumAttributes];
pMesh->GetAttributeTable(pAttributes, &NumAttributes);
//設置頂點著色器和頂點聲明
g_pd3dDevice->SetVertexShader(pShader);
g_pd3dDevice->SetVertexDeclaration(pDecl);
//設置數據流
g_pd3dDevice->SetStreamSource(0, pVB, 0, D3DXGetFVFVertexSize(pMesh->GetFVF()));
g_pd3dDevice->SetIndices(pIB);
//遍歷屬性列表并配合其中的信息調用DrawIndexPrimitive繪制各個子集
for(DWORD i=0;i<NumAttributes;i++)
{
if(pAttributes[i].FaceCount)
{
//Get material number
DWORD MatNum = pAttributes[i].AttribId;
//Set texture
g_pd3dDevice->SetTexture(0, pTextures[MatNum]);
//Draw the mesh subset
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0,
pAttributes[i].VertexStart,
pAttributes[i].VertexCount,
pAttributes[i].FaceStart * 3,
pAttributes[i].FaceCount);
}
}
//Free resources
ReleaseCOM(pVB);
ReleaseCOM(pIB);
delete [] pAttributes;
…
編譯運行程序,效果如圖2.4所示,你將看到屏幕上白色的海豚上下翻騰,同時感受到頂點著色器為渲染效果所帶來的巨大改善。


圖2.4