每日一语:
故天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身行,行拂乱其所为,所以动心忍性,曾益其所不能。吃得苦中苦,方为人上人。没有哪件事情,可以轻轻松松成功。我们应该持之以恒,坚持奋斗!
正文:
顶点着色器入门:
顶点着色器是一段运行在图形卡GPU中的程序,它可取代固定功能流水线中的变换和光照环节。(当然这不是绝对的,需要硬件支持).顶点是以局部坐标输入顶点着色器的,而且顶点着色器必须将照亮的(上色的)顶点输出到齐次裁剪空间中.(为简单起见,我们不对投影变换的细节进行深究。投影矩阵将顶点变换到那个空间叫齐次裁剪空间。所以,要将一个顶点从局部坐标系变换到齐次坐标系中,我们必须实施以下一系列变换:世界变换,取景变换以及投影变换。这些变换分别由世界变换矩阵,取景变换矩阵,投影矩阵来完成)。对于点图元,顶点着色器也可对每个顶点的顶点尺寸进行处理。
由于顶点着色器其实就是我们用HLSL语言编写的一段定制程序,这样我们在可实现的图形效果上就获得了很大的灵活性。例如,借助顶点着色器,我们就可使用任何在顶点着色器中实现的光照算法。这样,我们就不再受制于Direct3D的固定功能流水线了。而且,这种对顶点位置进行操作的能力具有广泛的应用场合。例如织物模拟(cloth simulation),粒子系统的点尺寸处理,顶点融合/变形技术等,此外,我们可用的顶点数据结构也更加灵活,而且可编程流水线中的顶点结构可以包含比固定功能流水线更加丰富的数据。
我们需要通过检查D3DCAP9结构的成员VertexShaderVersion并与宏D3DVS_VERSION进行比较,来检测图形卡是否支持某个顶点着色器版本。比如:
if (caps.VertexShaderVersion < D3DVS_VERSION(2.0))
我们可以看出,D3DVS_VERSION宏中的两个参数分别表示主版本号和次版本号。
顶点声明:
到目前为止,我们一直在用灵活顶点格式(FVF)来描述顶点结构的分量。但是,在可编程的流水线中,顶点结构甚至可以包含那些超出FVF描述能力的数据。因此,我们通常使用描述能力更强,功能更丰富的顶点声明。
注意:在可编程的流水线中,如果顶点结构可用FVF来描述,我们仍可使用它。但是,这仅仅是为了表示方便,实际上在可编程流水线内部,FVF最终将被转换为顶点声明。
顶点声明的描述:
我们将顶点声明为一个D3DVERTEXELEMENT9类型的结构数组。该结构数组中的每个元素都描述了顶点结构的一个分量。所以,如果你的顶点结构具有3个分量(比如位置,法向量,颜色),则相应的顶点声明就可用一个维数为3的D3DVERTEXELEMENT9类型的结构数组类描述。
D3DVERTEXELEMENT9结构的定义如下:
typedef struct D3DVERTEXELEMENT9 {
WORD Stream,
WORD Offset,
BYTE Type,
BYTE Method,
BYTE Usage,
BYTE UsageIndex;
}D3DVERTEXELEMENT9,*LPD3DVERTEXELEMENT;
Stream 指定与顶点分量关联的数据流。
Offset 自顶点数据起始点到特定数据类型相关的数据的字节偏移量。例如,如果顶点的结构为:
struct Vertex
{
D3DVECTOR3 pos;
D3DVECTOR3 normal;
};
由于分量pos为该结构的第一个分量,所以其相对偏移量为0.而由于sizeof(pos) = 12,所以以分量normal的相对偏移为12,即分量normal的位置始于Vertex结构的第12个字节。
Type 指定数据类型。该参数可取枚举类型D3DDELTYPE的任何一个成员,一些常用的类型如下:
D3DDELTYPE_FLOAT1 浮点类型的标量。
D3DDELTYPE_FLOAT2 浮点类型的2D向量。
D3DDELTYPE_FLOAT3 浮点类型的3D向量。
D3DDELTYPE_FLOAT4 浮点类型的4D向量。
D3DDELTYPE_D3DCOLOR 一个被扩展为RGBA浮点类型颜色向量(r,g,b,a)的D3DCOLOR类型,其中颜色向量的每个分量都被规范化至区间[0,1]内。
Method 指定了网格化方法。由于参数涉及到一些高级主题。我们仅使用默认方法,即用标识符D3DDECLMETHOD_DEFAULT来指定。
Usage 指定了顶点分量的用途。例如,即某一分量是作为位置向量,法向量还是纹理向量等。合法的用法表示符都取自枚举类型D3DDECLUSAGE.
typedef enum D3DDECLUSAGE
{
D3DDECLUSAGE_POSITION = 0,
D3DDECLUSAGE_BLENDWEIGHT = 1,
D3DDECLUSAGE_BLENDINDICES = 2,
D3DDECLUSAGE_NORMAL = 3,
D3DDECLUSAGE_PSIZE = 4,
D3DDECLUSAGE_TEXCOORD = 5,
D3DDECLUSAGE_TANGENT = 6,
D3DDECLUSAGE_BINORMAL =7,
D3DDECLUSAGE_TESSFATOR = 8,
D3DDECLUSAGE_POSITION = 9,
D3DDECLUSAGE_COLOR = 10,
D3DDECLUSAGE_FOG = 11,
D3DDECLUSAGE_DEPTH = 12,
D3DDECLUSAGE_SAMPLE = 13,
}D3DDECLUSAGE *LPD3DDECLUSAGE.
D3DDECLUSAGE_PSIZE 类型用于指定顶点的尺寸。该类型主要用于点精灵,这样我们就可对每个顶点的尺寸进行控制。如果一个顶点声明中具有D3DDECLUSAGE_POSITION,则表明该顶点已经过了变换,并指示图形卡不要将该顶点输送到顶点处理环节(变换和光照处理).
注意:上述用法类型中,我们暂时没有使用到。
UsageIndex 用于标识具有同用法的多个顶点分量。用法索引是一个位于区间[0,15]内的整数。例如,现在假定我们有3个顶点分量的用法为D3DDECLUSAGE_NORMAL。则我们可按序将这3个顶点分量的用法索引分别指定为0,1,2。按照这种方式,我们就可通过用法索引表示每个特定的法向量。
顶点声明示例:
假定我们所要描述的顶点格式包含了一个位置向量和3个法向量,则相应的顶点声明可指定为:
D3DVERTEXELEMENT9 decl[] =
{
{0,0,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,0},
{0,24,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,1},
{0,36,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,2},
D3DDECL_END();
}
其中,D3DDECL_END宏用于初始化D3DVERTEXELEMENT9数组中的最后一个顶点元素。同时也可以注意一下不同法向量的用法索引。
顶点声明的创建:
一旦将顶点声明描述为一个D3DVERTEXELEMENT9类型的数组,我们就可用如下方法获得指向接口IDirect3DVertexDeclaration9的指针。
HRESULT CreateVertexDeclaration(
CONST D3DVERTEXELEMENT9* pVertexElements,
IDirect3DVertexDeclaration** ppDecl
);
pVertexElements 指向一个D3DVERTEXELEMENT9类型的结构指针,该数组描述了我们想要创建的顶点声明。
ppDecl 用于返回一个指向所创建的IDirect3DVertexDeclaration9接口的指针。
下面是该函数的一个调用实例,其中decl 是一个D3DVERTEXELEMENT9类型的指针。
IDirect3DVertexDeclaration9 * _decl = 0;
hr = _device -> CreateVertexDeclaration(decl,&_decl);
顶点声明的启用:
前面我们提到,灵活顶点格式是一项很有用的特性,在可编程流程线内部它将转换为顶点声明。所以,直接使用顶点声明,我们无需调用。
Device -> SetFVF(fvf);
而只是调用如下函数即可:
Device -> SetVertexDeclaration(_decl);
其中,_decl是一个指向接口IDirect3DVertexDeclaration9 的指针。
顶点数据的使用:
请考虑下列顶点声明:
D3DVERTEXELEMENT9 decl[] =
{
{0,0,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,0},
{0,24,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,1},
{0,36,D3DDECLTYPE_FLOAT,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,2},
D3DDECL_END();
}
我们需要一种方式来定义从顶点声明中的元素到顶点着色器的输入结构的数据成员的映射。我们在输入结构中通过为每个数据成员指定一种语义(:usage-type[usage-index])来定义这种映射。该语义通过用法类型和用法索引来标识顶点声明中的每个元素。由数据成员的语义所标识的那个顶点元素就是被映射到该数据成员的那个元素。例如,与前面提到的那个顶点声明对应的输入结构为:
struct VS_INPUT
{
vector position : POSITION;
vector normal : NORMAL0;
vector faceNormal1 : NORMAL1;
vector faceNormal2 : NORMAL2;
}
注意:如果我们略去了用法索引,就意味着该索引为0.例如,POSITION的含义与POSITION0完全相同。
其中,decl数组中的由用法POSITION和用法索引0所标识的元素0被映射为输入结构VS_INPUT中的数据成员position.decl数组中的由用法NORMAL和用法索引0标识的元素1 被映射为输入结构的normal。decl数组中的由用法NORMAL和索引1所标识的元素2被映射为输入结构VS_INPUT中的数据成员faceNormal1。decl数组中的由用法NORMAL和索引2所标识的元素3被映射为输入结构VS_INPUT中的数据成员faceNormal2。
顶点着色器支持的输入用法包括:
POSITION[n] 位置
BLENDWEIGHT[n] 融合权值
BLENDINDICES[n] 融合索引
NORMAL[n] 法向量
PSIZE[n] 顶点的点尺寸
DIFFUSE[n] 漫反射颜色
SPECULAR[n] 高光颜色
TEXCOORD[n] 纹理坐标
TANGENT[n] 切向量
BINORMAL[n] 副法向量
TESSFATOR[n] 网格化因子
其中n为一可选整数,但必须取自区间[0,15]内。
此外,对于输出结构,我们必须指定每个成员的用途。例如,该数据成员应看作位置向量,颜色向量还是纹理坐标等。对于各数据成员的用途,图形卡无从知晓,除非显示指定。这种指定也是在语义层次实现的。
struct VS_OUT
{
vector position : POSITION;
vector diffuse : COLOR0;
vector specular : COLOR1;
}
顶点着色器支持的输出用法包括:
POSITION[n] 位置
PSIZE[n] 顶点的点尺寸
FOG[n] 雾融合值
COLOR[n] 顶点颜色。注意,可输出多个顶点颜色,这些颜色混合在一起生成最终颜色。
TEXCOORD[n] 顶点纹理坐标。注意,可能输出多个顶点纹理坐标。
其中n为一可选整数,但必须取自区间[0,15]内。
使用顶点着色器的步骤:
下面是创建和使用顶点着色器的步骤:
1,编写顶点着色器程序,并进行编译。
2,创建一个IDirect3DVertexShader9接口的对象,以表示基于所编译的着色器代码的顶点着色器。
3,用IDirect3DDevice9::SetVertexShader方法启用顶点着色器。
当然,当顶点着色器使用完毕后,必须对其进行销毁。
顶点着色器的编写和编译:
首先,我们必须编写一个顶点着色器程序。然后调用方法D3DXCompileShaderFromFile来对着色器程序进行编译。前面我们提到该函数将返回一个指向ID3DXBuffer接口的指针,其中该接口包含了经过编译的着色器代码。
顶点着色器的创建:
一旦有了经过编译好的着色器代码,我们就可借助如下方法获取IDirect3DVertexShader9接口的指针,该接口代表一个顶点着色器。
HRESULT IDirect3DDevice9::CreateVertexShader(
const DWORD * pFunction,
IDirect3DVertexShader9 **ppShader
)
pFunction 指向经过编译的代码的指针。
ppShader 返回一个指向IDirect3DVertexShader9接口的指针。
例如假定变量 shader是一个指向ID3DXBuffer接口的指针,则为了获取指向IDirect3DVertexShader9接口的指针,我们可一这样做
IDirect3DVertexShader9* ToonShader = 0;
hr = Device -> CreateVertexShader(
(DWORD *)Shader -> GetBufferPointer(),
&ToonShader);
再次强调,D3DXCompileShaderFromFile 函数能够返回经过编译的着色器代码。
顶点着色器的设置:
当我们获取了指向接口IDirect3DVertexShader9的指针后,就可用下述方法启用顶点着色器:
HRESULT IDirect3DDevice9::SetVertexShader(
IDirect3DVertexShader9 *pShader
);
该方法接收一个单个参数,我们可将一个指向希望被启用的顶点着色器的指针赋给该参数。为了启用,我们应该这样做:
Device -> SetVertexShader(ToonShader);
顶点着色器的销毁:
如同Direct3D的所有其他接口一样,接口IDirect3DVertexShader9在使用完毕后也必须调用其自身的Release方法来释放它所占用的资源。
d3d::Release
卡通着色:
下面我们将编写两个着色器分别用于对网格进行明暗处理和轮廓勾勒,以呈现出卡通效果。
卡通绘制是一类较特殊的非真实感绘制方法,有时也被称为风格化绘制。
虽然,卡通绘制并不适用于所有类型的游戏,例如暴力第一人称设计游戏,但当需要是游戏表现出一种卡通的感觉时,该方法确实有助于营造一种氛围。而且,卡通绘制方法易于实现,并能够让我们更好的演示顶点着色器。
我们将卡通绘制方法分为如下两个步骤:
1,通常,卡通绘制只有少数几种明暗亮度即,而且位置相邻的两种亮度级之间亮度是突变的。我们称这种着色方法为卡通着色。
2,卡通绘制中通常也要将物体的轮廓勾勒出来。
为了实现卡通着色,我们采用Lander描述的方法,其原理大致如下,首先创建一个包含了我们想要的不同明暗度的灰度纹理。
然后,我们在顶点着色器中进行标准漫射光计算,即计算顶点法向量N和光线向量L的点积以确定这两个向量之间夹角的余弦,这样就能够确定顶点到底接收了多少光照。
如果积小于0,说明光线向量与顶点法向量之间的夹角大于90度,也就意味着表面接收不到任何光照。所以,如果,小于0,我们就使它为0,这样总是位于[0,1]内。
在通常使用的漫反射光计算模型中,我们用s对颜色向量进行比例加权,这样各顶点的颜色就会依据所接收到的光照而不同程度地变暗:
diffuseColor = s(r,g,b,a);
但是,这样做会导致明暗度由亮到暗的平滑过渡,这就违背了我们进行卡通着色的初衷,我们所期望的是少数几种(一般选中2~4种,卡通绘制就能取得令人满意的效果)不同的明暗度之间的突变效果。
所以,用明暗因子s对颜色向量进行加权并不适宜,我们将把s作为前面提到的灰度纹理的纹理坐标的u分量。
由于标量s在区间[0.1]内,所以s是一个合法的纹理坐标。
按照这种方式,各顶点不应被均匀着色,而应呈现明暗度的突变。例如,灰度纹理可能被划分为不同的明暗度。
若s在[0,0.33],这应取Shade 0,[0.33,0.66]则应取明暗度Shader 1,若s落入[0.66,1] 选取Shader 2。当然,这些纹理之间的过度必须程序突变的效果。
轮廓的勾勒:
1,边的表示
我们可用四边形(由两个三角形单元构成)来表示网格的一条边。
我们之所以选四边形主要基于如下几点考虑:通过调整四边形的尺寸我们就可很容易地改变边的厚度。而且我们也可以通过绘制退化四边形来隐藏某些边,即那些非轮廓边的边。在Direct3D中,我们用两个三角形构造一个四边形。一个退化的四边形由两个退化的三角形构成。退化三角形就是面积为0的三角形,既由共线的3个顶点构成的三角形。如果我们将一个退化三角形输送到绘制流水线中,是什么也显示不出来的。这非常有用,因为如果我们希望将某个三角形面片隐藏,则可对其进行退化处理,这样就无需将该面片从三角形列表(即顶点缓存中)删除。前面提到我们仅想显示轮廓边-而非网格中的每一条边。
当我们初次创建一条边时,我们为该边指定4个顶点,这样由这4个顶点构成的四边形就是一个退化四边形,也就意味着该边将被隐藏(即绘制时不予显示).
注意,对于图中的两个顶点V0和V1,我们将其法向量均设为0向量,即当我们将该边的两个顶点传入着色器中时,着色器将对每一个顶点进行测试,看其是否处在轮廓边上,如果是,则顶点着色器将使顶点沿顶点法线方向进行一定量的偏移。注意,如果一个顶点的法向量为0,则该顶点不会发生偏移。这样,最终就形成了一个可以表示轮廓边的非退化四边形。
如果我们不将顶点V0和V1的法向量设置为0,则这些顶点也将发生偏移,但如果描述了一段轮廓边的4个顶点都发生了偏移,则我们只是对退化四边形进行了平移。通过保持顶点V0和V1的位置不边,而仅使顶点V2和V3发生偏移,我们就重新生成了表示该轮廓的四边形。
2,轮廓边的测试:
如果某条边为两个面片face0和face1的公共边,且这两个面片相对于观察方向具有不同的朝向,则这条边是一条轮廓边。即如果一个面片超前,而另一个朝后,则这条边是一条轮廓边。
这样,为了测试某一顶点是否位于轮廓边上,我们必须知道面片face0和face1在每个顶点处的局部法向量。这在我们采用的边顶点数据结构中得到了体现。
struct VS_INPUT
{
vector position : POSITION;
vector normal : NORMAL0;
vector faceNormal1 : NORMAL1;
vector faceNormal2 : NORMAL2;
}
该结构中,前面的两个变量比较简单,但在后面我们还看到了另外两个附加法向量,即faceNormal1和faceNormal2,这些向量描述了共享该顶点所依附的那条边的两个面片(即face0和face1)的法向量。
下面,介绍测试一个顶点是否在轮廓边上的数学原理。假定我们当前处在观察坐标系中,令向量v表示由观察坐标系原点指向待测顶点的向量,另n0和n1分别表示face0和face2的面法向量。则当下列不等式为真时,待测顶点就在轮廓边上。
(v.n0)(v.n1) < 0;
如果两个点乘异号,则左边的运算结构为负,故上面的不等式为真。回忆一下,如果两个点积异号,说明一个面片朝向前方,另一个朝向后方。我们总是认为这样的边为轮廓边。为了保证顶点着色器能够将这种边按轮廓边处理,我们可令faceNormal2 = -faceNormal1。这样,面片的法向量的方向就会彼此相背。故不等式结果为真,从而表明该边为一条轮廓边。
3,边的生成。
生成网格的每条边非常简单,我们只需遍历网格中的每个面片,然后为该面片中的每条边计算出一个四边形(即退化的四边形).
由于每个三角形都有3条边,所以每个面片也有3条边。
对于每条边的各顶点,我们还需要知道共享该边的两个面片。其中一个面片为该边所依附的那个三角形面片。例如,如果我们要计算第i个面片的边,则第i个面片共享有这条边。另外一个享有该边的面片可通过该网格的邻接信息求出。