顶点着色器(vertex shader)是-一段运行在图形卡GPU中的程序,它可取代固定功能流水线中的变换和光照环节(当然,这也不是绝对的,因为在硬件不支持顶点着色器的情况下,Direct3D运行时就会用软件运算方式来模拟顶点着色器)
可以看出,顶点是以局部坐标(局部坐标系中的坐标)输入顶点着色器的,而且顶点着色器必须将照亮的(上色的)顶点输出到齐次裁剪空间中。由于顶点着色器其实就是我们用HLSL语言编写的一段定制程序,这样我们在可实现的图形效果上就获得了很大的灵活性。例如借助顶点着色器,我们就可使用任何可在顶点着色器中实现的光照算法。这样,我们就不再受制于Direct3D的固定功能流水线了。而且这种对顶点位置进行操作的能力具有广泛的应用场合,例织物模拟、粒子系统的点尺寸处理、顶点融合/变形技术(morphing)等。
此外,可用的顶点数据结构也更加灵活,而且可编程流水线中的顶点结构可以包含比固定功能流水线更加丰富的数据。顶点着色器仍然是一项相对比较新的特性,目前许多图形卡尚不支持该特性,尤其是伴随DirectX9发布的那些比较新的顶点着色器版本。您可通过检查D3DCAPS9结构的成员VertexShaderVersion并与宏D3DVS_VERSION进行比较,来检测您的图形卡是否支持某个顶点着色器版本。D3DVS_VERSION宏中的俩个参数分别表示主版本号和次版本号,目前D3DXCompileShaderFromFile函数支持的顶点着色器版本为1.1、2.0和3.0。
//If the device's supported version is less than version 2.0
if(caps.VertexShaderversion < D3DVS_VERSION(2,0))
//Then vertex shader version 2.0 is not supported on this device
顶点声明
到月前为止,我们一直都在用灵活顶点格式(FVF)来描述顶点结构的分量。但是在可编程流水线中,顶点结构甚至可以包含那些超出FVF描述能力的数据。因此,我们通常使用描述能力更强、功能更丰富的顶点声明(vertex declaration)。
在可编程流水线中,如果顶点结构可用FVF来描述,我们仍可使用它。但是这仅仅是为了表示方便,实际上在可编程流水线内部,FVF最终将被转换为顶点声明。
顶点声明的描述
我们将顶点声明描述为一个D3DVERTEXELEMENT9类型的结构数组。该结构数组中的每个元素都描述了顶点结构的一个分量。所以,如果您的顶点结构具有3个分量(比如位置、法向量、颜色),则相应的顶点声明就可用一个维数为3的D3DVERTEXELEMENT9类型的结构数组来描述。
typedef struct _D3DVERTEXELEMENT9
{
WORD Stream; // Stream index
WORD Offset; // Offset in the stream in bytes
BYTE Type; // Data type
BYTE Method; // Processing method
BYTE Usage; // Semantics
BYTE UsageIndex; // Semantic index
} D3DVERTEXELEMENT9, *LPD3DVERTEXELEMENT9;
Stream:指定与顶点分量关联的数据流
Offset:自顶点数据起始点到与特定数据类型相关的数据的字节偏移量,例如顶点的结构为
struct Vertex
{
D3DXVECTOR3 pos;
D3DXVECTOR3 normal;
};
由于分量pos为该结构的第一个分量,所以其相对偏移量为0。而由于sizeof(pos)=12,所以分量normal的相对偏移量为12,即分量normal的位置始于Vertex结构的第12个字节。
Type:指定数据类型,为枚举D3DDECLTYPE的任何一个成员,常用类型为:
- D3DDECLTYPE_FLOAT1 浮点类型标量(value,0,0,1)
- D3DDECLTYPE_FLOAT2 浮点类型的2D向量(value,value,0,1)
- D3DDECLTYPE_FLOAT3 浮点类型的3D向量(value,value,value,1)
- D3DDECLTYPE_FLOAT4 浮点类型的4D向量
- D3DDECLTYPE_D3DCOLOR 一个被扩展为RGBA浮点类型颜色向量(r,g,b,a)的D3DCOLOR类型,其中颜色向量的每个分量都被规范化至区间[0,1]内
Method:指定了网格化(tesselation)方法。由于参数涉及到一些高级主题,本书中我们仪使
用默认方法,即用标识符D3DDECLMETHOD_DEFAULT来指定。
Usage:指定了顶点分量的用途。例如某一分量是作为位置向量、法向量还是纹理向量等。合法的用法标识符都取自枚举类型D3DDECLUSAGE,D3DDECLUSAGE_PSIZE类型用于指定顶点的点尺寸。该类型主要用于点精灵(point sprite),这样我们就可对每个顶点的尺寸进行控制。如果一个顶点声明中具有D3DDECLUSAGE_POSITION,则表明该顶点已经过了变换,并指示图形卡不要将该顶点输送到顶点处理环节中(变换和光照处理)。
UsageIndex:用于标识具有同用法的多个顶点分量。用法索引(usage index)是一个位于区间[0,15]内的整数。例如现在假定我们有3个顶点分量的用法都为D3DDECLUSAGE NORMAL。则我们可按序将这3个顶点分量的用法索引分别指定为0、1、2。按照这种方式,我们就可通过用法索引表示每个特定的法向量。
顶点声明示例:假定我们所要描述的顶点格式包含了一个位置向量和3个法向量,则相应的顶点声明可指定为:
D3DVERTEXELEMENT9 decl[] =
{
{0,0,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,0},
{0,24,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,1},
{0,36,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL,2},
D3DDECL_END()
}:
其中,D3DDECL_END宏用于初始化D3DVERTEXELEMENT9数组中的最后一个顶点元素。同时也请您注意一下不同法向量的用法索引。
顶点声明的创建
一旦将顶点声明描述为一个D3DVERTEXELEMENT9类型的数组,我们就可用CreateVertexDeclaration方法获得指向接IDirect3DVertexDeclaration9的指针:
HERSULT CreateVertexDeclaration(
CONST D3DVerTEXELEMENT9* pVertexElements,
IDirect3DVertexDeclaration9** ppDecl
);
//decl是一个D3DVerTEXELEMENT9类型的结构数组
IDirect3DVertexDeclaration9* _decl = 0;
hr = _device->CreateVertexDeclaration(decl,&_decl);
pVertexElements:指向-个D3DVERTEXELEMENT9类型的结构数组,该数组描述了我们想要创建的顶点声明。
ppDecl:用于返回一个指向所创建的IDirect3DVertexDeclaration9接口的指针。
顶点声明的启用
Device->SetFVF(fvf);
//可用下面的代替上面的代码 _decl为IDirect3DVertexDeclaration指针
Device->SetVertexDeclaration(_decl);
顶点数据的使用
我们需要一种方式来定义从顶点声明中的元素到顶点着色器的输入结构的数据成员的映射。我们在输入结构中通过为每个数据成员指定一种语义(:usage-type[usage-index])来定义这种映射。该语义通过用法类型和用法索引来标识顶点声明中的每个元素。由数据成员的语义所标识的那个顶点元素就是被映射到该数据成员的那个元素。例如,与前面提到的那个顶点声明对应的输入结构为:
struct VS_INPUT
{
vector position : POSITION;
vector normal : NORMAL0;
vector faceNormal1 : NORMAL1;
vector faceNormal2 : NORMAL2;
};
其中,decl数组中的由用法POSITION和用法索引0所标识的元素0被映射为输入结构VS_INPUT中的数据成员position。decl数组中的由用法NORMAL和用法索引0标识的元素1被映射为输入结构VS_ INPUT中的数据成员normal。.decl数组中的由用法NORMAL和用法索引1所标识的元素2被映射为输入结构VS_INPUT中的数据成员faceNormall。decl数组中的由用法NORMAL和用法索引2所标识的元素3被映射为输入结构VS_INPUT中的数据成员faceNormal2。如果我们略去了用法索引,就意味着该索引为0。例如,POSITION的含义与POSITION0完全相同。
顶点着色器支持的输入用法包括:n为可选整数,区间[0,15]
- POSITION[n] 位置
- BLENDWEIGHTS[n] 融合权值(blend weights)
- BLENDINDICES[n] 融合索引(blend indices)
- NORMAL[n] 法向量
- PSIZE[n] 顶点的点尺寸
- DIFFUSE[n] 漫反射颜色
- SPECULAR[n] 高光颜色(specular color)
- TEXCOORD[n] 纹理坐标
- TANGENT[n] 切向量(tangent vector)
- BINORMAL[n] 副法向量(binormal vector)
- TESSFACTOR[n] 网格化因子(tessellation factor)
对于输出结构,我们必须指定每个成员的用途。例如,该数据成员应看作位置向量、颜色向量还是纹理坐标等。对于各数据成员的用途,图形卡无丛知晓,除非您显式指定。这种指定也是在语义层次实现的,例如:
struct VS_OUTPUT
{
vector position : POSITION;
vector diffuse : COLOR0;
vector specular : COLOR1;
};
顶点着色器支持的输出用法包括:n为可选整数,区间[0,15]
- POSITION[n] 位置
- PSIZE[n] 顶点的点尺寸
- FOG[n] 雾融合值
- COLOR[n] 顶点颜色,可输出多个顶点颜色,这些颜色混合在一起生成最终颜色
- TEXCOORD[n] 顶点纹理坐标 可能输出多个顶点纹理坐标
使用点点着色器的步骤
- 编写顶点着色器程序,并进行编译
- 创建一个IDirect3DVertexShader9接口的对象,以表示基于编译的着色器代码的顶点着色器
- 用IDirect3DDevice9::SetVertexShader方法启用顶点着色器
顶点着色器的编写编译、创建、设置、销毁
首先,需要编写一个顶点着色器程序,用HLSL语言来编写着色器程序,一旦着色器代码编写好,就可以用方法D3DXCompileShaderFromFile来对着色器程序进行编译,该函数将返回一个指向ID3DXBuffer接口的指针,该接口包含了经过编译的着色器代码。
有了经过编译的着色器代码,就可以使用CreateVertexShader来获取IDirect3DVertexShader9接口指针,该接口代表了一个顶点着色器。
HRESULT IDirect3DDevice9::CreateVertexShader(
const DWORD* pFunction,
IDirect3DVertexShader9** ppShader
);
//Shader为指向ID3DXBuffer接口的指针
IDirect3DVertexShader9* ToonShader = 0;
hr = Device->CreateVertexShader((DWORD*)Shader->GetBufferPointer(),ToonShader);
pFunction:指向经过编译的代码的指针
ppSHader:返回一个指向ID3DXBuffer接口的指针
当获取了指向接口IDirect3DVertexShader9的指针后,就可以用SetVertexShader来启用顶点着色器。
HRESULT IDirect3DDevice9::SetVertexShader(
IDirect3DVertexShader9* pShader
);
Device->SetVertexShader(ToonShader);
和其他Direct3D所有其他接口一样,接口IDirect3DVertexSHader9在使用完毕之后必须调用自身的Release方法来释放它所占用的资源
d3d::Release<IDirect3DVertexShader9*>(ToonShader);
顶点着色器例程
内容:一个在方向光(平行光)光源照射下,对每个顶点按照标准漫反射光照来处理的顶点着色器,漫射光依据顶点法向量与光线向量(与光线自光源的出射方向相反的方向,即指向光源)之间的夹角来计算顶点所接收到的光线总量,该夹角越小,顶点接收到的光照就越多,如果夹角>=90,顶点将接收不到任何光照。
顶点着色器程序代码
//File:diffuse.txt
//Desc:Vertex shader that does diffuse Lighting
//用来保存视图矩阵、投影矩阵、环境光材质、漫反射材质和描述、指向光源的方向
//这些变量从应用程序中初始化
matrix ViewMatrix;
matrix ViewProjMatrix;
vector AmbientMtrl;
vector DiffuseMtrl;
vector LightDirection;
//用于保存环境光强度(ambient光源发出的光)、漫反射光强度(diffuse光源发出的光)
//这些变量在着色器中初始化
vector DiffuseLightIntensity={0.0f,0.0f,1.0f,1.0f};
vector AmbientLightIntensity={0.0f,0.0f,0.2f,1.0f};
//输入输出结构
struct VS_INPUT
{
vector position : POSITION;
vector normal : NORMAL;
};
struct VS_OUTPUT
{
vector position : POSITION;
vector diffuse : COLOR;
};
//Main
VS_OUTPUT Main(VS_INPUT input)
{
VS_OUTPUT output = (VS_OUTPUT)0;
output.position = mul(input.position,ViewProjMatrix);
LightDirection.w=0.0f;
input.normal.w=0.0f;
LightDirection=mul(LightDirection,ViewMatrix);
input.normal=mul(input.normal,ViewMatrix);
float s=dot(LightDirection,input.normal);
if(s<0.0f)
s=0.0f;
output.diffuse=(AmbientMtrl * AmbientLightIntensity) + (s*(DiffuseLightIntensity * DiffuseMtrl));
return output;
}
应用程序代码
IDirect3DVertexShader9* DiffuseShader = 0; //顶点着色器
ID3DXConstantTable* DiffuseConstTable = 0; //常量表接口指针
ID3DXMesh* Teapot = 0; //茶壶网格
D3DXHANDLE ViewMatrixHandle = 0; //D3DXHANDLE为获取常量的句柄
D3DXHANDLE ViewProjMatrixHandle = 0;
D3DXHANDLE AmbientMtrlHandle = 0;
D3DXHANDLE DiffuseMtrlHandle = 0;
D3DXHANDLE LightDirHandle = 0;
D3DXMATRIX proj;
//1.创建茶壶网格 2.编译顶点着色器 3.基于已编译的代码创建顶点着色器 4.通过常量表获取着色器程序中若干变量的句柄 5.通过常量表初始化着色器中的若干变量
bool SetUpVertexShader()
{
HRESULT hr = 0;
D3DXCreateTeapot(Device, &Teapot, 0);
ID3DXBuffer* shader = 0; //编译后的着色器代码
ID3DXBuffer* errorBuffer = 0; //存储错误代码
hr = D3DXCompileShaderFromFile(L"diffuse.txt", 0, 0, "Main", "vs_1_1", D3DXSHADER_DEBUG | D3DXSHADER_ENABLE_BACKWARDS_COMPATIBILITY, &shader, &errorBuffer, &DiffuseConstTable);
if (errorBuffer)
{
::MessageBoxA(0, (char*)errorBuffer->GetBufferPointer(), 0, 0);
d3d::Release<ID3DXBuffer*>(errorBuffer);
}
if (FAILED(hr))
{
::MessageBox(0, L"D3DXCompileShaderFromFile() FAILED", 0, 0);
return false;
}
//创建着色器
hr = Device->CreateVertexShader((DWORD*)shader->GetBufferPointer(), &DiffuseShader);
if (FAILED(hr))
{
::MessageBox(0, L"CreateVertexShader FAILED", 0, 0);
return false;
}
d3d::Release<ID3DXBuffer*>(shader);
ViewMatrixHandle = DiffuseConstTable->GetConstantByName(0, "ViewMatrix");
ViewProjMatrixHandle = DiffuseConstTable->GetConstantByName(0, "ViewProjMatrix");
AmbientMtrlHandle = DiffuseConstTable->GetConstantByName(0, "AmbientMtrl");
DiffuseMtrlHandle = DiffuseConstTable->GetConstantByName(0, "DiffuseMtrl");
LightDirHandle = DiffuseConstTable->GetConstantByName(0, "LightDirection");
D3DXVECTOR4 directionToLight(-0.57f, .057f, -0.57f, 0.0f);
DiffuseConstTable->SetVector(Device, LightDirHandle, &directionToLight);
D3DXVECTOR4 ambientMtrl(0.0f, 0.0f, 1.0f, 1.0f);
D3DXVECTOR4 diffuseMtrl(0.0f, 0.0f, 1.0f, 1.0f);
DiffuseConstTable->SetVector(Device, AmbientMtrlHandle, &ambientMtrl);
DiffuseConstTable->SetVector(Device, DiffuseMtrlHandle, &diffuseMtrl);
DiffuseConstTable->SetDefaults(Device); //此方法在应用程序的设置用应调用一次,设置常量的默认值
//计算投影矩阵
D3DXMatrixPerspectiveFovLH(&proj, D3DX_PI* 0.25f, (float)d3d::Width / (float)d3d::Height, 1.0f, 1000.0f);
//固定管线:Device->SetTransform(D3DTS_PROJECTION, &Proj);
return true;
}
bool DisplayVertexShader(float time_delta)
{
if (Device)
{
static float angle = (3.0f*D3DX_PI) / 2.0f;
static float height = 3.0f;
if (KEY_DOWN(VK_LEFT))
angle -= 0.5f * time_delta;
if (KEY_DOWN(VK_RIGHT))
angle += 0.5f * time_delta;
if (KEY_DOWN(VK_UP))
angle += 5.0f * time_delta;
if (KEY_DOWN(VK_DOWN))
angle -= 5.0f * time_delta;
D3DXVECTOR3 position(cosf(angle)* 7.0f, height, sinf(angle)* 7.0f);
D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMATRIX V;
D3DXMatrixLookAtLH(&V, &position, &target, &up);
//固定管线:Device->SetTransform(D3DTS_VIEW, &V);//设置照相机的位置
DiffuseConstTable->SetMatrix(Device, ViewMatrixHandle, &V);
D3DXMATRIX ViewProj = V * proj;
DiffuseConstTable->SetMatrix(Device, ViewProjMatrixHandle, &ViewProj);
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);
Device->BeginScene();
Device->SetVertexShader(DiffuseShader);
Teapot->DrawSubset(0);
Device->EndScene();
Device->Present(0, 0, 0, 0);
}
return true;
}
void CleanUp()
{
//顶点着色器
d3d::Release<ID3DXMesh*>(Teapot);
d3d::Release<IDirect3DVertexShader9*>(DiffuseShader);
d3d::Release<ID3DXConstantTable*>(DiffuseConstTable);
}
- 通过使用顶点着色器,我们可以取代固定功能流水线中的变换和光照模块,当用我们的定制程序(顶点着色器)取代那些固定运算时,我们在可实现的图形效果上就获得了很大的灵活性。
- 顶点声明用于指定顶点的格式,顶点声明与灵活顶点格式(FVF)非常相似,但是前者更加灵活,而且允许我们描述那些超出FVF表达范围的顶点格式,FVF在可编程流水线内部仍将被转化为顶点声明。
- 对于输入结构,用法语义指定了顶点各分量如何从顶点声明映射到HLSL程序中的变量,对于输出结构,用法语义指定了每个顶点各分量的用途(表示表示位置,颜色,纹理坐标等)