人一旦停下來不做事情,就會變得懶惰。自從停止寫《Direct3D 快速上手》后,就老不想寫東西,這些天一直都在想著要寫一些關(guān)于游戲引擎的文章,遲遲未敢動手,因?yàn)橛X得這個(gè)主題實(shí)在比較大,自己水平有限,寫不好還要被人笑話。為了督促自己再次像個(gè)陀螺轉(zhuǎn)起來,開始寫一些關(guān)于HLSL以及Shader的東西,當(dāng)然,我學(xué)這個(gè)也不久,只能講到一些膚淺的東西,意在拋磚引玉。
這里我假設(shè)你已經(jīng)明白以寫有關(guān)流水線的基本得知識,以及明白為什么要使用Shader這之類的基本的問題,我就不多花時(shí)間介紹這些基本得知識,相關(guān)的知識大家可以在MSDN的網(wǎng)站上看到,以及很多有名的樹上都有詳細(xì)的介紹,例如《Microsoft DirectX 9 Programmable Graphics Pipeline》
好那我們就開始。這里我不再使用C#作為編寫程序的語言,雖然我很喜歡它,但是考慮到用Shader的目的就是為了效率,那在語言的選擇上自然選擇C++,編譯工具選用VC.Net。
在以后的文章中,我或許會提到Cg這個(gè)語言,這個(gè)微軟和NVIDIA聯(lián)合開發(fā)的一門語言,其實(shí)和HLSL只不過是叫了2個(gè)不同的名字罷了,完全可以兼容運(yùn)行。
作為第一章,還是弄一點(diǎn)比較簡單的例子,這個(gè)例子里面我們著重探討的是Vertex Shader以及PS的語法,以及怎么把它們運(yùn)用到DirectX的程序中去,這個(gè)例子沒有任何的實(shí)用性,It is only a sample.
以下是運(yùn)行圖例:
.JPG)
首先看看我們程序的結(jié)構(gòu):
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void init(void); //初始化所有資源
void shutDown(void); //釋放資源
void render(void); //渲染函數(shù)
void initShader( void ); //初始化Shader
很簡單對吧,其實(shí)也可以用微軟提供給我們的DX的框架,這個(gè)看個(gè)人的喜好了。
我們先來看看Vertex Shader :
建立一個(gè)新的文件,存為vertex_shad.vsh,然后輸入:
float4x4 worldViewProj;
struct VS_INPUT
{
float3 position : POSITION; //位置
float4 color0 : COLOR0; //顏色
float2 texcoord0 : TEXCOORD0; //紋理坐標(biāo)
};
struct VS_OUTPUT
{
float4 hposition : POSITION;
float4 color0 : COLOR0;
float2 texcoord0 : TEXCOORD0;
};
VS_OUTPUT main( VS_INPUT IN ) //入口函數(shù)
{
VS_OUTPUT OUT;
float4 v = float4( IN.position.x,IN.position.y, IN.position.z,1.0f );
OUT.hposition = mul( v, worldViewProj ); //矩陣變換
OUT.color0 = IN.color0; //輸出的顏色=輸入的顏色
OUT.texcoord0 = IN.texcoord0; //復(fù)制紋理坐標(biāo)
return OUT;
}
這里首先定義了2個(gè)結(jié)構(gòu),都分別定義了位置,顏色,和紋理坐標(biāo)。這里我們看到程序里面使用了很多我們沒見過的數(shù)據(jù)類型,這是HLSL內(nèi)置的數(shù)據(jù)結(jié)構(gòu),例如float4x4。
我們注意下float4這個(gè)類型,它是一個(gè)有4個(gè)向量的浮點(diǎn)類型,你可以把它理解為一個(gè)浮點(diǎn)的數(shù)組,當(dāng)然嚴(yán)格的說,這里應(yīng)該是壓縮數(shù)組。
float3 position : POSITION; //位置
這一句話聲明了一個(gè)float3的變量,變量的名字叫做position,這里我們看到在后面還有一個(gè)冒號和POSITION,這個(gè)冒號和其后的POSRITION叫做“語義”。語義的作用相當(dāng)于在HLSL出現(xiàn)以前的Shader中用到的寄存器,這里我們指明了把position這個(gè)變量將與流水線的POSRITION寄存器相連接。在其后的COLOR0, TEXCOORD0也是一樣的意思。
我們看到Shader的入口函數(shù)main,這個(gè)函數(shù)有一個(gè)參數(shù)和一個(gè)返回值,都是我們在Shader的開頭定義的結(jié)構(gòu)。
OUT.hposition = mul( v, worldViewProj ); //矩陣變換
我們將向量和矩陣相乘,這樣得到的仍然是一個(gè)向量,這個(gè)向量就是經(jīng)過我們的透視變換的向量。這里之所以要進(jìn)行這個(gè)變換是為了讓所有的定點(diǎn)都在視區(qū)內(nèi)并且在選轉(zhuǎn)的時(shí)候符合透視。
在將所有的返回值賦值完成后,程序返回。
接下來我們建立pixel_shader.psh文件,這是pixel的Shader文件:
struct VS_OUTPUT
{
float4 hposition : POSITION;
float4 color0 : COLOR0;
float2 texcoord0 : TEXCOORD0;
};
struct PS_OUTPUT
{
float4 color : COLOR;
};
sampler testTexture; //樣本對象
PS_OUTPUT main( VS_OUTPUT IN )
{
PS_OUTPUT OUT;
OUT.color = tex2D( testTexture, IN.texcoord0 ) + IN.color0; // Add texel color to vertex color
return OUT;
}
相對于VS來說PS的代碼簡單很多,程序聲明了一個(gè)樣本對象,然后在PS的入口程序中用到了tex2D函數(shù),這個(gè)函數(shù)可以用制定的紋理坐標(biāo)集存取不同類型的樣本并返回一個(gè)向量結(jié)果。
OUT.color = tex2D( testTexture, IN.texcoord0 ) + IN.color0; 的結(jié)果是把顏色疊加在原來紋理的地方。
我們該看看我們的主程序了,這里我只介紹最重要的initShader
void initShader( void )
{
D3DXCreateTextureFromFile( g_pd3dDevice, "test.bmp", &g_pTexture );
D3DVERTEXELEMENT9 declaration[] =
{
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 12, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0 },
{ 0, 16, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },
D3DDECL_END()
};
g_pd3dDevice->CreateVertexDeclaration( declaration, &g_pVertexDeclaration );
HRESULT hr;
LPD3DXBUFFER pCode;
DWORD dwShaderFlags = 0;
LPD3DXBUFFER pBufferErrors = NULL;
// Assemble the vertex shader from the file
hr = D3DXCompileShaderFromFile( "vertex_shader.vsh", NULL, NULL, "main",
"vs_1_1", dwShaderFlags, &pCode,
&pBufferErrors, &g_pConstantTableVS );
if( FAILED(hr) )
{
LPVOID pCompilErrors = pBufferErrors->GetBufferPointer();
MessageBox(NULL, (const char*)pCompilErrors, "Vertex Shader Compile Error",
MB_OK|MB_ICONEXCLAMATION);
}
// Create the vertex shader
g_pd3dDevice->CreateVertexShader( (DWORD*)pCode->GetBufferPointer(),
&g_pVertexShader );
pCode->Release();
//
// Create a HLSL based pixel shader.
//
// Assemble the vertex shader from the file
hr = D3DXCompileShaderFromFile( "pixel_shader.psh", NULL, NULL, "main",
"ps_1_1", dwShaderFlags, &pCode,
&pBufferErrors, &g_pConstantTablePS );
if( FAILED(hr) )
{
LPVOID pCompilErrors = pBufferErrors->GetBufferPointer();
MessageBox(NULL, (const char*)pCompilErrors, "Pixel Shader Compile Error",
MB_OK|MB_ICONEXCLAMATION);
}
// Create the vertex shader
g_pd3dDevice->CreatePixelShader( (DWORD*)pCode->GetBufferPointer(),
&g_pPixelShader );
pCode->Release();
}
一開始我們定義了一個(gè)D3DVERTEXELEMENT9結(jié)構(gòu),這個(gè)結(jié)構(gòu)的作用是描述頂點(diǎn)數(shù)據(jù)的用途等等屬性:
typedef struct _D3DVERTEXELEMENT9 {
WORD Stream; //Stream number
WORD Offset; //數(shù)據(jù)的偏移量
BYTE Type; //種類,也是一個(gè)結(jié)構(gòu),詳細(xì)情況查閱MSDN
BYTE Method; //制定方格化的操作,為default時(shí),值將被拷貝如寄存器
BYTE Usage; //用途
BYTE UsageIndex; //修改用途,允許用戶指定多種用途
} D3DVERTEXELEMENT9
接下來,我們就要建立一個(gè)VertexDeclaration。
HRESULT CreateVertexDeclaration(
CONST D3DVERTEXELEMENT9* pVertexElements,
Direct3DVertexDeclaration9** ppDecl
);
然后我們從文件中讀入Shader的信息。PS的讀入和VS如出一轍。我們看看渲染的部分。
void render( void )
{
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_COLORVALUE(0.0f,1.0f,0.0f,1.0f), 1.0f, 0 );
g_pd3dDevice->BeginScene();
D3DXMATRIX matTrans;
D3DXMATRIX matRot;
D3DXMatrixTranslation( &matTrans, 0.0f, 0.0f, 4.0f );
D3DXMatrixRotationYawPitchRoll( &matRot,
D3DXToRadian(g_fSpinX),
D3DXToRadian(g_fSpinY),
0.0f );
g_matWorld = matRot * matTrans;
D3DXMatrixIdentity( &g_matView );
if( g_bUseShaders == true )
{
//
// Use vertex and pixel shaders...
//
D3DXMATRIX worldViewProjection = g_matWorld * g_matView * g_matProj;
g_pConstantTableVS->SetMatrix( g_pd3dDevice, "worldViewProj", &worldViewProjection );
g_pd3dDevice->SetVertexDeclaration( g_pVertexDeclaration );
g_pd3dDevice->SetVertexShader( g_pVertexShader );
g_pd3dDevice->SetTexture( 0, g_pTexture );
g_pd3dDevice->SetPixelShader( g_pPixelShader );
g_pd3dDevice->SetFVF( Vertex::FVF_Flags );
g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0,sizeof(Vertex) );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
g_pd3dDevice->SetVertexShader( NULL );
g_pd3dDevice->SetPixelShader( NULL );
}
else
{
//
// Render the normal way...
//
g_pd3dDevice->SetTransform( D3DTS_WORLD, &g_matWorld );
g_pd3dDevice->SetTexture( 0, g_pTexture );
g_pd3dDevice->SetFVF( Vertex::FVF_Flags );
g_pd3dDevice->SetStreamSource( 0, g_pVertexBuffer, 0,sizeof(Vertex) );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
}
g_pd3dDevice->EndScene();
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
g_pConstantTableVS->SetMatrix( g_pd3dDevice, "worldViewProj", &worldViewProjection );
這一句將視覺矩陣和我們在Vertex Shader中定義的矩陣關(guān)聯(lián)起來。然后我們通過
g_pd3dDevice->SetVertexDeclaration( g_pVertexDeclaration );
g_pd3dDevice->SetVertexShader( g_pVertexShader );
來通知程序,所有的頂點(diǎn)的處理都要由我們的頂點(diǎn)Shader來處理,頂點(diǎn)Shader中用到的所有頂點(diǎn)的一些屬性都在VertexDeclaration中聲明了。然后建立頂點(diǎn)的Shader。
在建立完VS,紋理,PS后,執(zhí)行畫圖指令,最后我們需要釋放Shader,恢復(fù)固定流水線的功能。g_pd3dDevice->SetVertexShader( NULL );g_pd3dDevice->SetPixelShader( NULL );
好了,這里我們的程序就完成了,接下來的日子里面,我將盡力為大家講解一些Shader的有用的特性,呵呵呵,當(dāng)然,這里我沒有說,不是所有的顯卡都支持Shader的,所以你需要在程序中判斷你顯卡所支持的類型,這里我不再多些,2句話就可以搞定。
By sssa2000
5/15/2005