高级着色器语言(High Level Shader Language,简称HLSL),由微软拥有及开发的一种语言,HLSL 独立的工作在 Windows 平台上,只能供微软的Direct3D使用。
顶点着色器
顶点着色器(vertex shader)是一个在显卡的GPU上执行的程序,它替换了固定功能管道(fixed function pipeline)中的变换(transformation)和光照(lighting)阶段(这不是百分之百的正确,因为顶点着色器可以被Direct3D运行时(Direct3D runtime)以软件模拟,如果硬件不支持顶点着色器的话)。图2.1说明了管线中顶点着色器替换的部件。
图2.1
由于顶点着色器是我们(在HLSL中)写的一个自定义程序,因此我们在图形效果方面获得了极大的自由性。我们不再受限于Direct3D的固定光照算法。此外,应用程序操纵顶点位置的能力也有了多样性,例如:布料仿真,粒子系统的点大小操纵,还有顶点混合/变形。此外,我们的顶点数据结构更自由了,并且可以在可编程管线中包含比在固定功能管线中多的多的数据。
可编程数据流模型
DirectX 8.0引入了数据流的概念,可以这样理解数据流(图2.2):
图2.2
· 一个顶点由n个数据流组成。
· 一个数据流由m个元素组成。
· 一个元素是[位置、颜色、法向、纹理坐标]。
程序中使用IDirect3DDevice9::SetStreamSource方法把一个顶点缓存绑定到一个设备数据流。
顶点声明
该小节对顶点声明的描述绝大多数都取自翁云兵的《着色器和效果》,该文对顶点声明的描述是我所见到最详尽最透彻的,这里向作者表示敬意:
到现在为止,我们已经使用自由顶点格式(flexible vertex format,FVF)来描述顶点结构中的各分量。但是,在可编程管线中,我们的顶点数据可以包含比用FVF所能表达的多的多的数据。因此,我们通常使用更具表达性的并且更强有力的顶点声明(vertex declaration)。
注意:我们仍然可以在可编程管线中使用FVF——如果我们的顶点格式可以这样描述。不管怎样,这只是为了方便,因为FVF会在内部被转换为一个顶点声明。
描述顶点声明
我们将一个顶点声明描述为一个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数组的最后一个顶点元素。
创建顶点声明
CreateVertexDeclaration函数用于创建顶点声明,decl为指向上一小节定义的D3DVERTEXELEMENT9数组的指针,函数返回IDirect3DVertexDeclaration9指针g_Decl;
IDirect3DVertexDeclaration9 *g_Decl = NULL;
g_pd3dDevice->CreateVertexDeclaration(decl ,&g_Decl);
设置顶点声明
g_pd3dDevice->SetVertexDeclaration(g_Decl);
到此,可编程数据流模型、顶点声明介绍完毕,在下面的例子中读者将会有更连贯的理解。
渐变动画(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的值,就得到了从源网格模型(形状一)到目标网格模型(形状二)的平滑过渡。
接下来将在程序里使用顶点着色器实现我们的渐变动画。
渐变动画中的顶点声明
程序中,我们设定一个顶点对应两个数据流,这两个数据流分别包含了源网格模型的数据和目标网格模型的数据。渲染过程中,我们在着色器里根据两个数据流中的顶点数据以及时间值确定最终的顶点信息。
个数据流包含分量如下:
源网格模型数据流:顶点位置、顶点法线、纹理坐标;
目标网格模型数据流:顶点位置、顶点法线;
注意目标网格模型数据流没有包含纹理坐标,因为纹理对于两个网格模型都是一样的,所以仅使用源网格模型的纹理就可以了。
顶点声明指定如下:
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()
};
渐变动画中的顶点着色器
下面给出顶点着色器源码,代码存储于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)值;
应用程序
我们在应用程序中执行以下操作:
· 加载两个两个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 );
…
原文:http://www.cppblog.com/liangairan/articles/57953.html