顶点着色器VertexShader
什么是顶点着色器?
- 顶点着色器是一组指令代码,这组指令代码在顶点被渲染时执行。
- 同一时间内,只能激活一个顶点着色器。
- 每个源顶点着色器最多拥有128条指令(DirextX8.1),而在DirectX9,则可以达到256条。
为什么大家要使用顶点着色器?
- 顶点着色器可以提高渲染场景速度(直接由显卡自动调用,无需人为干预)。
- 用顶点着色器你可以做布类仿真,高级别动画,实时修改透视效果(比如水底效果),高级光亮(需要像素着色器支持)
顶点着色器如何运作?
当渲染一个顶点时,API会执行你在顶点着色器中所写的指令。依靠这种方法,你可以自己控制每个顶点,包括渲染,确定位置,是否显示在屏幕上。
如何创建一个顶点着色器?
用一个文本编辑器就可以了!我建议你们使用notepad或者vs开发环境来创建和修改着色器。另外,必须拥有一个支持可编程着色器的显卡。写完着色器后,保存他。API就可以调用他了(Direct3D或OpenGL)。API通过一些函数来调用这些代码指令到硬件中
Vertex Shader(顶点着色器)是针对每个顶点进行操作的,它的输入是一个顶点,包含用户指定的顶点的基本信息,输出是该顶点转换后的信息。
最常见的顶点着色器即对顶点的位置坐标、纹理坐标、法线等信息进行变换,并返回新的顶点,传递给下一个阶段。一般针对顶点着色器的输入与输出,程序员各指定相应的结构来存储相应的顶点信息,如:
struct VertexIn
{
float3 pos : POSITION;
float4 color : COLOR;
};
就是一个简单的输入结构,float3和float4是HLSL内置类型,即用来存放3个float和4个float,此外还有其他很多类型诸如float4x4(4x4矩阵)等,详细可参考HLSL相关资料。pos和color即相应的成员变量名,”POSITION”和”COLOR”,同来对相应的成员变量进行语义说明,”POSITION”很显然指位置坐标,”COLOR”即顶点的颜色。这些语义说明在C++程序中创建输入布局(InputLayout)时会用来,并且必须要一致,这个在本文稍后介绍InputLayout时还会解释。
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
这个结构即顶点着色器的输出结构,与输入基本类似,但pos对应地变成了float4类型,这个是齐次坐标下的位置,即经过投影变换后的坐标,这时的顶点坐标必须是float4类型,还有一点重要的是,它对应的语义说明SV_POSITION,也是固定的,”SV“即”System Value”,系统值,在像素着色器阶段会需要这个值来进行裁剪操作。除了系统值必须固定外,其他的语义值都是可以任意指定的(虽然可以任意指定,但一般情况下显然是以变量真实作用为依据嘛)。
有了该两个结构,顶点着色器函数如下:
VertexOut VS(VertexIn vin)
{
VertexOut vout;
vout.posH = mul(float4(vin.pos,1.f),g_worldViewProj);
vout.color = vin.color;
return vout;
}
该函数名是可以任意指定的,其参数为输入顶点结构,输出为相应的输出顶点结构。在这个函数里面,它实现的是一个最简单的功能,即坐标变换和颜色的简单传递。
这里用到了几个HLSL内置函数:mul用来实现向量、矩阵的相乘,其维数是可度的,比如float3和float3x3相乘,float4和float4x4相乘,也可以进行相同维数的矩阵的相乘。由于输出坐标要求为float4类型,因些相乘时需要float4和float4x4。这里float4通过复制输入顶点后接第4个参数1.f作为构造函数生成,g_worldViewProj为一个全局变量,类型为float4x4,用于进行世界、视角、投影变换,三个变量合成到一个矩阵中实现。
顶点着色器示例
如使用如下代码
VertexShader.hlsl
struct VS_INPUT
{
float4 Pos : POSITION;
float2 Tex : TEXCOORD;
};
struct VS_OUTPUT
{
float4 Pos : SV_POSITION;
float2 Tex : TEXCOORD;
};
//VS是顶点着色器的入口-固定的,就像main一样
VS_OUTPUT VS(VS_INPUT input)
{
return input;
}
经过VS的HLSL Shader Compiler编译之后,生成VertexShader.h
而VertexShader.h生成的其实是一个数组:
const BYTE g_VS[] ={
...
};
这样就可以在D3D11中创建顶点着色器:
ID3D11VertexShader* m_VertexShader
ID3D11InputLayout* m_InputLayout
HRESULT hr;
UINT Size = ARRAYSIZE(g_VS);
hr = m_Device->CreateVertexShader(g_VS, Size, nullptr, &m_VertexShader);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create vertex shader in OUTPUTMANAGER", L"Error", hr, SystemTransitionsExpectedErrors);
}
D3D11_INPUT_ELEMENT_DESC Layout[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
UINT NumElements = ARRAYSIZE(Layout);
hr = m_Device->CreateInputLayout(Layout, NumElements, g_VS, Size, &m_InputLayout);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create input layout in OUTPUTMANAGER", L"Error", hr, SystemTransitionsExpectedErrors);
}
m_DeviceContext->IASetInputLayout(m_InputLayout);
m_DeviceContext->VSSetShader(m_VertexShader, nullptr, 0);
在顶点结构中,针对每个成员,我们要定义一个相应的D3D11_INPUT_ELEMENT_DESC。这个结构定义了该成员相应的有关信息,定义如下:
typedef struct D3D11_INPUT_ELEMENT_DESC {
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D11_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;
- SemanticName指的是我们在着色器中定义输入顶点结构时对应的语义说明,比如对于位置坐标,为“POSITION”;
- SemanticIndex为该语义说明的序号,是这样的,在一个顶点结构中,不同的成员可以使用相同的语义说明,这时要用不同的序号来区分他们,相应的序号就是这个SemanticIndex。比如一个顶点有两层纹理坐标,tex1和tex2,它们的语义说明都为”TEXTCOORD”,相应的序号则为0和1;
- Format为该成员的数据类型,对于坐标即为DXGI_FORMAT_R32G32B32_FLOAT(3个float);
- InputSlot为输入槽,一般情况下我们指定为0。此外,在个别情况下,不同的顶点信息可以通过不同的输入槽提供给GPU,这时通过该成员来指定其输入槽序号,多个输入槽的使用在以后会有学习;
- AlignedByteOffset,即该成员在顶点结构中字节偏移量,对于这里的位置坐标,为第一个成员,因此偏移为0。对于颜色值,由于位置坐标占据了12个字节,因此颜色值对应的偏移为12,依次类推;
- InputaSlotClass,对于一般情况,为D3D11_INPUT_PER_VERTEX_DATA,这时相应的最后一个成员InstanceDataStepRate就要设为0。
顶点结构中对应的每个成员,都对应一个D3D11_INPUT_ELEMENT_DESC,所有的D3D11_INPUT_ELEMENT_DESC存放在一个数组当中。因此,对于上面定义的顶点结构,我们可以如下数组:
D3D11_INPUT_ELEMENT_DESC inputDesc[2] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};
定义好这个,下一步就是用它来创建Input Layout了,相应的函数如下:
HRESULT ID3D11Device::CreateInputLayout(
[in] const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs,
[in] UINT NumElements,
[in] const void *pShaderBytecodeWithInputSignature,
[in] SIZE_T BytecodeLength,
[out] ID3D11InputLayout **ppInputLayout
);
第一个参数即我们刚定义好的D3D11_INPUT_ELEMENT_DESC数组;第二个参数为数组成员个数,这里为2;第三个和第四个成员需要用到编译好的Effect程序,获取方式如下:
D3DX11_PASS_DESC passDesc = {0};
g_effect->GetTechniqueByName("BasicDraw")->GetPassByIndex(0)->GetDesc(&passDesc);
g_device->CreateInputLayout(inputDesc,2,passDesc.pIAInputSignature,passDesc.IAInputSignatureSize,&g_inputLayout);
g_effect为事先创建好的ID3DX11Effect*类型变量,代表编译好的Effect,通过它获取相应technique11中相应的pass的描述信息,通过pass描述信息即可获取我们这里需要的两个参数,如上代码中所示,意指shader中输入结构信息,及该信息的字节长度;最后一个参数即我们要创建的input layout接口的地址。
到这里,Input Layout就创建好了。