零距离接触HLSL 一
By sssa2000
人一旦停下来不做事情,就会变得懒惰。自从停止写《Direct3D 快速上手》后,就老不想写东西,这些天一直都在想着要写一些关于游戏引擎的文章,迟迟未敢动手,因为觉得这个主题实在比较大,自己水平有限,写不好还要被人笑话。为了督促自己再次像个陀螺转起来,开始写一些关于HLSL以及Shader的东西,当然,我学这个也不久,只能讲到一些肤浅的东西,意在抛砖引玉。
这里我假设你已经明白以写有关流水线的基本得知识,以及明白为什么要使用Shader这之类的基本的问题,我就不多花时间介绍这些基本得知识,相关的知识大家可以在MSDN的网站上看到,以及很多有名的树上都有详细的介绍,例如《Microsoft DirectX 9 Programmable Graphics Pipeline》
好那我们就开始。这里我不再使用C#作为编写程序的语言,虽然我很喜欢它,但是考虑到用Shader的目的就是为了效率,那在语言的选择上自然选择C++,编译工具选用VC.Net。
在以后的文章中,我或许会提到Cg这个语言,这个微软和NVIDIA联合开发的一门语言,其实和HLSL只不过是叫了2个不同的名字罢了,完全可以兼容运行。
作为第一章,还是弄一点比较简单的例子,这个例子里面我们着重探讨的是Vertex Shader以及PS的语法,以及怎么把它们运用到DirectX的程序中去,这个例子没有任何的实用性,It is only a sample.
以下是运行图例:
首先看看我们程序的结构:
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); //渲染函数
void initShader( void ); //初始化Shader
很简单对吧,其实也可以用微软提供给我们的DX的框架,这个看个人的喜好了。
我们先来看看Vertex Shader :
建立一个新的文件,存为vertex_shad.vsh,然后输入:
float4x4 worldViewProj;
struct VS_INPUT
{
float3 position : POSITION; //位置
float4 color0 : COLOR0; //颜色
float2 texcoord0 : TEXCOORD0; //纹理坐标
};
struct VS_OUTPUT
{
float4 hposition : POSITION;
float4 color0 : COLOR0;
float2 texcoord0 : TEXCOORD0;
};
VS_OUTPUT main( VS_INPUT IN ) //入口函数
{
VS_OUTPUT OUT;
float4 v = float4( IN.position.x,IN.position.y, IN.position.z,
OUT.hposition = mul( v, worldViewProj ); //矩阵变换
OUT.color0 = IN.color0; //输出的颜色=输入的颜色
OUT.texcoord0 = IN.texcoord0; //复制纹理坐标
return OUT;
}
这里首先定义了2个结构,都分别定义了位置,颜色,和纹理坐标。这里我们看到程序里面使用了很多我们没见过的数据类型,这是HLSL内置的数据结构,例如float4x4。
我们注意下float4这个类型,它是一个有4个向量的浮点类型,你可以把它理解为一个浮点的数组,当然严格的说,这里应该是压缩数组。
float3 position : POSITION; //位置
这一句话声明了一个float3的变量,变量的名字叫做position,这里我们看到在后面还有一个冒号和POSITION,这个冒号和其后的POSRITION叫做“语义”。语义的作用相当于在HLSL出现以前的Shader中用到的寄存器,这里我们指明了把position这个变量将与流水线的POSRITION寄存器相连接。在其后的COLOR0, TEXCOORD0也是一样的意思。
我们看到Shader的入口函数main,这个函数有一个参数和一个返回值,都是我们在Shader的开头定义的结构。
OUT.hposition = mul( v, worldViewProj ); //矩阵变换
我们将向量和矩阵相乘,这样得到的仍然是一个向量,这个向量就是经过我们的透视变换的向量。这里之所以要进行这个变换是为了让所有的定点都在视区内并且在选转的时候符合透视。
在将所有的返回值赋值完成后,程序返回。
接下来我们建立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的代码简单很多,程序声明了一个样本对象,然后在PS的入口程序中用到了tex2D函数,这个函数可以用制定的纹理坐标集存取不同类型的样本并返回一个向量结果。
OUT.color = tex2D( testTexture, IN.texcoord0 ) + IN.color0; 的结果是把颜色叠加在原来纹理的地方。
我们该看看我们的主程序了,这里我只介绍最重要的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();
}
一开始我们定义了一个D3DVERTEXELEMENT9结构,这个结构的作用是描述顶点数据的用途等等属性:
typedef struct _D3DVERTEXELEMENT9 {
WORD Stream; //Stream number
WORD Offset; //数据的偏移量
BYTE Type; //种类,也是一个结构,详细情况查阅MSDN
BYTE Method; //制定方格化的操作,为default时,值将被拷贝如寄存器
BYTE Usage; //用途
BYTE UsageIndex; //修改用途,允许用户指定多种用途
} D3DVERTEXELEMENT9
接下来,我们就要建立一个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(
g_pd3dDevice->BeginScene();
D3DXMATRIX matTrans;
D3DXMATRIX matRot;
D3DXMatrixTranslation( &matTrans,
D3DXMatrixRotationYawPitchRoll( &matRot,
D3DXToRadian(g_fSpinX),
D3DXToRadian(g_fSpinY),
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中定义的矩阵关联起来。然后我们通过
g_pd3dDevice->SetVertexDeclaration( g_pVertexDeclaration );
g_pd3dDevice->SetVertexShader( g_pVertexShader );
来通知程序,所有的顶点的处理都要由我们的顶点Shader来处理,顶点Shader中用到的所有顶点的一些属性都在VertexDeclaration中声明了。然后建立顶点的Shader。
在建立完VS,纹理,PS后,执行画图指令,最后我们需要释放Shader,恢复固定流水线的功能。g_pd3dDevice->SetVertexShader( NULL );g_pd3dDevice->SetPixelShader( NULL );
好了,这里我们的程序就完成了,接下来的日子里面,我将尽力为大家讲解一些Shader的有用的特性,呵呵呵,当然,这里我没有说,不是所有的显卡都支持Shader的,所以你需要在程序中判断你显卡所支持的类型,这里我不再多些,2句话就可以搞定。
By sssa2000