赞
踩
第五章主要关注渲染管道的概念和数学方面。本章重点介绍配置渲染管道所需的Direct3D API接口和方法,定义顶点和像素着色器,并将几何图形提交给绘制管道进行绘制。学完本章,您将能够绘制各种几何形状的着色或线框模式。
学习目标:
1.发现用于定义,存储和绘制几何数据的Direct3D接口方法。
2.学习如何编写基本的顶点和像素着色器。
3.了解如何使用渲染状态配置渲染管道。
4.了解如何将效果框架用于将着色器和渲染状态逻辑分组到渲染技术中,以及如何使用将效果框架用作“着色器生成器”。
回顾§5.5.1,Direct3D中的顶点除空间位置之外还可以附加数据。要创建自定义顶点格式,我们首先创建一个保存我们选择的顶点数据的结构。下面说明两种不同种类的顶点格式; 一个由位置和颜色组成,第二个由位置,normal和纹理坐标组成。
struct Vertex1
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
};
一旦我们定义了一个顶点结构,我们需要向Direct3D提供我们的顶点结构的描述,以便它知道如何处理每个组件。该描述以输入布局(ID3D11InputLayout)的形式提供给Direct3D。输入布局由D3D11_INPUT_ELEMENT_DESC元素的数组指定。D3D11_INPUT_ELEMENT_DESC数组中的每个元素都描述并对应于顶点结构中的一个组件。所以如果顶点结构有两个组件,对应的D3D11_INPUT_ELEMENT_DESC数组将有两个元素。我们将D3D11_INPUT_ELEMENT_DESC数组称为输入布局描述。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;
1 . SemanticName:与元素关联的字符串。可以是任何有效的变量名。用于将顶点结构中的元素映射到顶点着色器输入签名中的元素; 见图6.1。
2 . SemanticIndex:附加到语义的索引。 图6.1中也说明了这一点,其中顶点结构可能具有多于一组的纹理坐标; 所以不是引入一个新的语义名称,我们可以附加一个索引来区分纹理坐标。 在着色器代码中指定的没有索引的语义默认为索引零; 例如,POSITIO N相当于图6.1中的POSITION 0。
DXGI_FORMAT_R32_FLOAT // 1D 32-bit float scalar
DXGI_FORMAT_R32G32_FLOAT // 2D 32-bit float vector
DXGI_FORMAT_R32G32B32_FLOAT // 3D 32-bit float vector
DXGI_FORMAT_R32G32B32A32_FLOAT // 4D 32-bit float vector
DXGI_FORMAT_R8_UINT // 1D 8-bit unsigned integer scalar
DXGI_FORMAT_R16G16_SINT // 2D 16-bit signed integer vector
DXGI_FORMAT_R32G32B32_UINT // 3D 32-bit unsigned integer vector
DXGI_FORMAT_R8G8B8A8_SINT // 4D 8-bit signed integer vector
DXGI_FORMAT_R8G8B8A8_UINT // 4D 8-bit unsigned integer vector
4 . InputSlot:指定此元素来自的输入槽索引。D3D支持十六个输入插槽(索引从0-15),您可以通过这些插槽提供顶点数据。例如,如果一个顶点由位置和颜色元素组成,那么您可以通过单个输入插槽传输两个元素,也可以将元素分开,并通过第一个输入插槽馈送位置元素,并通过第二个插槽输入颜色元素。然后,D3D将使用来自不同输入槽的元素来组合顶点。在这里,我们只使用一个输入插槽,但练习2要求用两个。
5 .AlignedByteOffset:对于单个输入插槽,这是从C ++顶点结构开始到顶点组件开始的偏移量(以字节为单位)。例如,在以下顶点结构中,元素Pos具有0字节的偏移,因为它的起始与顶点结构的开始重合;元素Normal具有12字节的偏移量,因为我们必须跳过Pos的字节才能达到它的开始;元素Tex0有一个24字节的偏移量,因为我们需要跳过Pos和Normal的字节才能达到Tex0的开始;元素Tex1具有32字节的偏移量,因为我们需要跳过Pos,Normal和Tex0的字节以获得Tex1的开始。
struct Vertex2
{
XMFLOAT3 Pos; // 0-byte offset
XMFLOAT3 Normal; // 12-byte offset
XMFLOAT2 Tex0; // 24-byte offset
XMFLOAT2 Tex1; // 32-byte offset
};
6 . InputSlotClass:现在指定D3D11_INPUT_PER_VERTEX_DATA; 实例化中的另一个高级技术选项。
7 . InstanceDataStepRate:目前为0; 其他值仅用于实例的高级技术。对于前两个示例顶点结构Vertex1和Vertex2,相应的输入布局描述将是:
D3D11_INPUT_ELEMENT_DESC desc1[] =
{
{"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}
};
D3D11_INPUT_ELEMENT_DESC desc2[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 32,
D3D11_INPUT_PER_VERTEX_DATA, 0}
};
一旦指定了输入布局描述,我们可以使用ID3D11Device :: CreateInputLayout方法获取指向D3D11InputLayout接口的指针,该接口代表输入布局。
HRESULT ID3D11Device::CreateInputLayout(
const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs,
UINT NumElements,
const void *pShaderBytecodeWithInputSignature,
SIZE_T BytecodeLength,
ID3D11InputLayout **ppInputLayout);
1.pInputElementDescs:描述顶点结构的D3D11_INPUT_ELEMENT_DESC元素数组。
2.NumElements:D3D11_INPUT_ELEMENT_DESC元素数组中的元素数。
3.pShaderBytecodeWithInputSignature:指向顶点着色器的输入签名的着色器字节代码的指针。
4.BytecodeLength:传入上一个参数的顶点着色器签名数据的字节大小。
5.ppInputLayout:返回指向创建的输入布局的指针。
参数3需要一些细节。顶点着色器将顶点元素列表作为输入参数(即所谓的输入签名)。 定制顶点结构中的元素需要映射到顶点着色器中的相应输入; 图6.1显示了这一点。 通过在创建输入布局时传递顶点着色器输入签名的描述,Direct3D可以验证输入布局与输入签名匹配,并在创建时创建从顶点结构到着色器输入的映射。只要输入签名完全相同,输入布局可以通过不同着色器重复使用。
考虑以下情况,您具有以下输入签名和顶点结构:
VertexOut VS(float3 Pos : POSITION, float4 Color : COLOR,
float3 Normal : NORMAL) { }
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
这将产生错误,VC ++调试输出窗口显示以下内容:
D3D11: ERROR: ID3D11Device::CreateInputLayout: The provided input signature expects to read an element with SemanticName/Index: ‘NORMAL’/0, but the declaration doesn’t provide a matching name.
现在考虑顶点结构和输入签名具有匹配的顶点元素的情况,但是类型是不同的:
VertexOut VS(int3 Pos : POSITION, float4 Color : COLOR) { }
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
这实际上是合法的,因为Direct3D允许重新解释输入寄存器中的位。但是,VC ++调试输出窗口提供以下警告:
D3D11: WARNING: ID3D11Device::CreateInputLayout: The provided input signature expects to read an element with SemanticName/Index: ‘POSITION’/0 and component(s) of the type ‘int32’. However, the matching entry in the Input Layout declaration, element[0], specifies mismatched format: ‘R32G32B32_FLOAT’. This is not an error, since behavior is well defined: The element format determines what data conversion algorithm gets applied before it shows up in a shader register. Independently, the shader input signature defines how the shader will interpret the data that has been placed in its input registers, with no change in the bits stored. It is valid for the application to reinterpret data as a different type once it is in the vertex shader, so this warning is issued just in case reinterpretation was not intended by the author.
以下代码提供了一个示例来说明如何调用ID3D11Device :: CreateInputLayout方法。请注意,代码涉及我们尚未讨论的一些主题(如ID3D11Effect)。本质上,效果封装一个或多个过程,并且顶点着色器与每个遍相关联。所以从这个效果来看,我们可以得到一个传递描述(D3D11_PASS_DESC),从中我们可以得到顶点着色器的输入签名。
ID3DX11Effect* mFX;
ID3DX11EffectTechnique* mTech;
ID3D11InputLayout* mInputLayout;
/* ...create the effect... */
mTech = mFX->GetTechniqueByName("Tech");
D3DX11_PASS_DESC passDesc;
mTech->GetPassByIndex(0)->GetDesc(&passDesc);
HR(md3dDevice->CreateInputLayout(vertexDesc, 4, passDesc.
pIAInputSignature, passDesc.IAInputSignatureSize, &mInputLayout));
在创建输入布局之后,它仍然没有绑定到设备。最后一步是绑定你想要的输入布局
使用该设备,如下代码显示:
ID3D11InputLayout* mInputLayout;
/* ...create the input layout... */
md3dImmediateContext->IASetInputLayout(mInputLayout);
如果您正在绘制的某些对象使用一个输入布局,而您正在绘制的其他对象需要不同的布局,则需要结构化你的代码如下所示:
md3dImmediateContext->IASetInputLayout(mInputLayout1);
/* ...draw objects using input layout 1... */
md3dImmediateContext->IASetInputLayout(mInputLayout2);
/* ...draw objects using input layout 2... */
换句话说,当输入布局被绑定到设备时,它不会改变,除非你覆盖它。
为了使GPU访问顶点数组,它们需要被放置在一个ID3D11Buffer接口表示的称为缓冲区的特殊资源结构中由。
存储顶点的缓冲区称为顶点缓冲区。Direct3D缓存不仅可以存储数据,还可以描述所存储数据的访问方式及其被绑定到渲染管道。要创建顶点缓冲区,我们需要执行以下步骤:
1.填写描述我们要创建的缓冲区的D3D11_BUFFER_DESC结构。
2.填写一个D3D11_SUBRESOURCE_DATA结构,指定我们要初始化缓冲区内容的数据。
3.调用ID3D11Device :: CreateBuffer来创建缓冲区。
D3D11_BUFFER_DESC结构定义如下:
typedef struct D3D11_BUFFER_DESC {
UINT ByteWidth;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
UINT StructureByteStride;
} D3D11_BUFFER_DESC;
1 . ByteWidth:我们要创建的顶点缓冲区的大小(以字节为单位)。
2 .Usage:D3D11_USAGE枚举类型的成员,指定缓冲区的使用方式。 四个值为:
(a) D3D11_USAGE_DEFAULT:如果GPU可读、写该资源,请指定此用法。使用映射API(即ID3D11DeviceContext :: Map),CPU无法读取或写入资源。但是,它可以使用ID3D11DeviceContext :: UpdateSubresource。§6.14中将讨论ID3D11DeviceContext :: Map。
(b) D3D11_USAGE_IMMUTABLE:如果资源的内容在创建后不会更改,请指定此用法。这允许一些潜在的优化,因为资源将被GPU只读。除了在创建时初始化资源,CPU不能写入不可变资源。CPU不能从不可变资源读取。 我们无法映射或更新不可变资源。
(c) D3D11_USAGE_DYNAMIC:如果应用程序(CPU)需要频繁更新资源的数据内容(例如,基于每帧),则指定此用法。具有这种用途的资源可以由GPU读取并由CPU使用映射API(即ID3D11DeviceContext :: Map)写入。因为新数据必须从CPU存储器(即系统RAM)转移到GPU存储器(即视频RAM),所以从CPU动态地更新GPU资源会导致性能下降。 因此,除非必要,否则应避免动态使用。
(d) D3D11_USAGE_STAGING:如果应用程序(CPU)需要能够读取资源的副本(即,该资源支持将数据从视频内存复制到系统内存),请指定此用法。从GPU复制到CPU内存是一个缓慢的操作,除非必要应该避免。可以使用ID3D11DeviceContext :: CopyResource和ID3D11DeviceContext :: CopySubresourceRegion方法。 §12.3.5显示了一个CopyResource的例子。
3 . BindFlags:对于顶点缓冲区,请指定D3D11_BIND_VERTEX_BUFFER。
4 .CPUAccessFlags:指定CPU如何访问缓冲区。如果CPU在创建缓冲区后不需要读取或写入权限,则指定0。如果CPU需要通过写入来更新缓冲区,请指定D3D11_CPU_ACCESS_WRITE。具有写访问权限的缓冲区必须使用D3D11_USAGE_DYNAMIC或D3D11_USAGE_STAGING。如果CPU需要从缓冲区中读取,请指定D3D11_CPU_ACCESS_READ。具有读访问权限的缓冲区必须使用D3D11_USAGE_STAGING。只有在需要时才指定这些标志。一般来说,CPU从Direct3D资源读取的速度较慢(GPU通过流水线进行数据抽取而不是回读),并且可能导致GPU停止(GPU可能需要等待完成其中资源的读取之后才能继续工作)。写入资源的CPU速度较快,但仍然需要将更新的数据传输回GPU RAM的开销。最好不要指定任何这些标志(除非必要),尽量让资源在GPU RAM中,由GPU写入并读取它。
5 . MiscFlags:顶点缓冲区中我们不需要任何Misc标志; 指定0,在SDK文档中查看D3D11_RESOURCE_MISC_FLAG枚举类型以了解更多。
6 . StructureByteStride:存储在结构化缓冲区中的单个元素的大小(以字节为单位)。此属性仅适用于结构化缓冲区,并可为所有其他缓冲区设置为0。 结构缓冲区是存储相同大小的元素的缓冲区。
D3D11_SUBRESOURCE_DATA结构定义如下:
typedef struct D3D11_SUBRESOURCE_DATA {
const void *pSysMem;
UINT SysMemPitch;
UINT SysMemSlicePitch;
} D3D11_SUBRESOURCE_DATA;
1 . pSysMem:一个指向系统内存数组的指针,它包含初始化顶点缓冲区的数据。如果缓冲区可以存储n个顶点,则系统阵列必须至少包含n个顶点,以便可以初始化整个缓冲区。
2 . SysMemPitch:不用于顶点缓冲区。
3 . SysMemSlicePitch:不用于顶点缓冲区。
以下代码创建了一个不可变的顶点缓冲区,该缓冲区是以原点为中心的立方体的八个顶点初始化。缓冲区是不可变的,因为立方体一旦创建就不需要更改 - 它始终保持立方体。此外,我们将每个顶点与不同的颜色相关联; 这些顶点颜色将用于对立方体进行着色,我们将在本章后面看到。
// Colors namespace defined in d3dUtil.h.
//
// #define XMGLOBALCONST extern CONST __declspec(selectany)
// 1. extern so there is only one copy of the variable, and not a
// separate private copy in each .obj.
// 2. __declspec(selectany) so that the compiler does not complain
// about multiple definitions in a .cpp file (it can pick anyone
// and discard the rest because they are constant--all the same).namespace Colors
{
XMGLOBALCONST XMVECTORF32 White = {1.0f, 1.0f, 1.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Black = {0.0f, 0.0f, 0.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Red = {1.0f, 0.0f, 0.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Green = {0.0f, 1.0f, 0.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Blue = {0.0f, 0.0f, 1.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Yellow = {1.0f, 1.0f, 0.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Cyan = {0.0f, 1.0f, 1.0f, 1.0f};
XMGLOBALCONST XMVECTORF32 Magenta = {1.0f, 0.0f, 1.0f, 1.0f};
}
//define raw vertex data
Vertex vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White },
{ XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black },
{ XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red },
{ XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green },
{ XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue },
{ XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow },
{ XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan },
{ XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }
};
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
ID3D11Buffer* mVB;
HR(md3dDevice->CreateBuffer(
&vbd, // description of buffer to create
&vinitData, // data to initialize buffer with
& mVB)); // return the created buffer
其中顶点类型和颜色定义如下:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
在创建顶点缓冲区之后,需要将其绑定到设备的输入槽,以便将顶点作为输入提供给管道。 这是通过以下方法完成的:
void ID3D11DeviceContext::IASetVertexBuffers(
UINT StartSlot,
UINT NumBuffers,
ID3D11Buffer *const *ppVertexBuffers,
const UINT *pStrides,
const UINT *pOffsets);
1 . StartSlot:开始绑定顶点缓冲区的输入槽。从0-15开始索引16个输入插槽。
2 . NumBuffers:我们绑定到输入插槽的顶点缓冲区的数量。如果开始时槽有索引k,并且我们绑定了n个缓冲区,那我们绑定缓冲区到槽Ik,Ik+1,...,Ik+n−1。
3 . ppVertexBuffers:指向顶点缓冲区数组的第一个元素的指针。
4 . pstrides:指向数组的第一个元素的指针(每个顶点缓冲区一个,第i个步长对应于第i个
顶点缓冲区)。步幅是相应顶点缓冲区中元素的大小(以字节为单位)。
5 . pOffsets:指向偏移数组的第一个元素的指针(每个顶点缓冲区一个,第i个偏移量对应于第i个顶点缓冲区)。这是从顶点缓冲区开始到顶点缓冲区中的位置的偏移量(以字节为单位),输入组件应从该位置开始读取顶点数据。如果你想跳过顶点缓冲区前面的一些顶点数据,你可以使用它。
IASetVertexBuffers方法似乎有点复杂,因为它支持将顶点缓冲区数组设置为各种输入槽。但是,大多数时间我们只能使用一个输入插槽。本章结束的练习给您一些使用两个输入插槽的练习。
顶点缓冲区将保持绑定到输入插槽,直到更改它。因此,如果使用多个顶点缓冲区,则可以像这样构建代码:
ID3D11Buffer* mVB1; // stores vertices of type Vertex1
ID3D11Buffer* mVB2; // stores vertices of type Vertex2
/*...Create the vertex buffers...*/
UINT stride = sizeof(Vertex1);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB1, &stride, &offset);
/* ...draw objects using vertex buffer 1... */
stride = sizeof(Vertex2);
offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB2, &stride, &offset);
/* ...draw objects using vertex buffer 2... */
void ID3D11DeviceContext::Draw(UINT VertexCount, UINT StartVertexLocation);
这两个参数定义了顶点缓冲区中需要绘制的连续顶点子集, 见图6.2。
由于需要由GPU访问索引,因此需要将其放置在特殊的资源结构中:索引缓冲区。创建一个索引缓冲区与创建顶点缓冲区非常类似,只是存储了索引而不是顶点。因此,我们只是显示一个创建索引缓冲区的例子,而不重复类似于顶点缓冲区讲解。
UINT indices[24] = {
0, 1, 2, // Triangle 0
0, 2, 3, // Triangle 1
0, 3, 4, // Triangle 2
0, 4, 5, // Triangle 3
0, 5, 6, // Triangle 4
0, 6, 7, // Triangle 5
0, 7, 8, // Triangle 6
0, 8, 1 // Triangle 7
};
// Describe the index buffer we are going to create. Observe the
// D3D11_BIND_INDEX_BUFFER bind flag
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * 24;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
// Specify the data to initialize the index buffer.
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
// Create the index buffer.
ID3D11Buffer* mIB;
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));
就像顶点缓冲区和其他Direct3D资源一样,在使用它之前,我们需要将它绑定到管线。使用ID3D11DeviceContext :: IASetIndexBuffer方法将索引缓冲区绑定到输入整合阶段。以下是一个示例调用:
md3dImmediateContext->IASetIndexBuffer(mIB, DXGI_FORMAT_R32_UINT, 0);
第二个参数指定索引的格式。在我们的例子中,我们使用32位无符号整数(DWORD);因此,我们指定了DXGI_FORMAT_R32_UINT。 如果想节省内存和带宽,也可以使用16位无符号整数,并且不需要额外的范围。请记住,除了在IASetIndexBuffer方法中指定格式之外,D3D11_BUFFER_DESC :: ByteWidth数据成员也依赖于格式,因此请确保它们一致以避免出现问题。请注意,DXGI_FORMAT_R16_UINT和DXGI_FORMAT_R32_UINT是索引缓冲区唯一支持的格式。第三个参数是从索引缓冲区开始到索引缓冲区中输入程序集应该开始读取数据的位置的偏移量(以字节为单位)。如果你想跳过索引缓冲区前面的一些数据,你可以使用它。
最后,在使用索引时,我们必须使用DrawIndexed方法而不是Draw:
void ID3D11DeviceContext::DrawIndexed(
UINT IndexCount,
UINT StartIndexLocation,
INT BaseVertexLocation);
1 . IndexCount:在绘制中使用的索引的数量。这不一定是索引缓冲区中的每个索引; 也就是说,你可以绘制一个连续的索引子集。
2 . StartIndexLocation:指向索引缓冲区中的一个元素的索引,该元素标志着开始读取索引的起始点。
3 . BaseVertexLocation BaseVertexLocation:在获取顶点之前,添加到此绘图调用中使用的索引的整数值。
为了说明这些参数,请考虑以下情况。假设我们有三个对象:一个球体,一个盒子和一个圆柱体。首先,每个对象都有自己的顶点缓冲区和自己的索引缓冲区。每个本地索引缓冲区中的索引都与相应的本地顶点缓冲区有关。现在假设我们把球体,盒子和圆柱体的顶点和索引连接成一个全局顶点和索引缓冲区,如图6.3所示。(可能会连接顶点和索引缓冲区,因为在更改顶点和索引缓冲区时会有一些API开销,这很可能不是瓶颈,但是如果有许多小的顶点和索引缓冲区可以很容易地合并,出于性能的原因是值得这样做的。)在这个连接之后,索引不再是正确的,因为它们存储索引位置相对于它们相应的本地顶点缓冲区,而不是全局索引。因此需要重新计算索引以正确地指向全局顶点缓冲区。原始的盒子索引是通过盒子的顶点索引的假设计算出来的
0, 1, …, numBoxVertices-1
但合并后,他们变为
**firstBoxVertexPos,
firstBoxVertexPos+1,
…,
firstBoxVertexPos+numBoxVertices-1**
因此,为了更新索引,我们需要为每个框索引添加第一个BoxVertexPos。 同样,我们需要为每个柱面索引添加firstCylVertexPos。 请注意,球的指标不需要改变(因为第一个球的顶点位置为零)。 让我们把对象的第一个顶点相对于全局顶点缓冲区的位置称为它的基本顶点位置。 通常,对象的新索引是通过将其基本顶点位置添加到每个索引来计算的。 我们不必自己计算新的索引,而是让Direct3D通过将基本顶点位置传递给DrawIndexed的第三个参数来完成。
然后我们可以用下面的三个调用一个接一个地画出球体,盒子和圆柱体:
md3dImmediateContext->DrawIndexed(numSphereIndices, 0, 0);
md3dImmediateContext->DrawIndexed(numBoxIndices, firstBoxIndex, firstBoxVertexPos);
md3dImmediateContext->DrawIndexed(numCylIndices, firstCylIndex, firstCylVertexPos);
本章的“形状”演示项目使用这种技术。
下面是简单顶点着色器的一个实现:
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
void VS(float3 iPosL : POSITION,
float4 iColor : COLOR,
out float4 oPosH : SV_POSITION,
out float4 oColor : COLOR)
{
// Transform to homogeneous clip space.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
oColor = iColor;
}
着色器是用一种叫做高级着色语言(HLSL)的语言编写的,它的语法与C ++类似,因此很容易学习。附录B提供了对HLSL的简要参考。我们的教学HLSL和编程着色器的方法将以实例为基础。也就是说,当我们通读这本书的时候,我们将介绍我们需要的任何新的HLSL概念来实现手头的演示。着色器通常使用基于文本的文件(称为效果文件(.fx))编写。我们将在本章后面讨论效果文件,但现在我们只关注顶点着色器。
顶点着色器是称为VS的函数。请注意,您可以为顶点着色器提供任何有效的函数名称。这个顶点着色器有四个参数。前两个是输入参数,后两个是输出参数(用out关键字表示)。HLSL没有引用或指针,所以要从一个函数返回多个值,您需要使用结构或输出参数。
前两个输入参数形成顶点着色器的输入签名,并对应于我们自定义顶点结构中的数据成员。参数语义“:POSITION”和“:COLOR”用于将顶点结构中的元素映射到顶点着色器输入参数,如图6.4所示。
输出参数也有附加的语义(“:SV_POSITION”和“:COLOR”)。 这些用于将顶点着色器输出映射到下一阶段的相应输入(几何着色器或像素着色器)。 请注意SV_POSITION语义是特殊的(SV代表系统值)。它用来表示保存顶点位置的顶点着色器输出元素。顶点位置需要与其他顶点属性不同地处理,因为它涉及其他属性不涉及的操作,例如剪切。输出参数不是系统值的语义名称可以是任何有效的语义
名称。
第一行通过乘以4×4矩阵gWorldViewProj将顶点位置从局部空间转换为齐次剪辑空间:
// Transform to homogeneous clip space.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
构造函数语法float4(iPosL,1.0f)构造一个4D向量,相当于float4(iPosL.x,iPosL.y,iPosL.z,1.0f);因为我们知道顶点的位置是点而不是向量, 在第四个组件(w = 1)中放置一个1。float2和float3类型分别表示2D和3D向量。矩阵变量gWorldViewProj位于一个常量缓冲区中,这将在下一节讨论。内置函数mul用于向量矩阵乘法。顺便提一下,mul函数对于不同大小的矩阵乘法是重载的; 例如,可以使用它乘以两个4×4矩阵,两个3×3矩阵,或一个1×3向量和一个3×3矩阵。着色器主体中的最后一行将输入颜色复制到输出参数,以便颜色将被输入到管道的下一个阶段:
oColor = iColor;
我们可以使用返回类型和输入签名的结构来等价地重写以前的顶点着色器(与长参数列表相反):
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
vout.Color = vin.Color;
return vout;
}
NOTE:如果没有几何着色器,则顶点着色器必须至少进行投影变换,因为这是硬件在离开顶点着色器时所期望的顶点所在的空间(如果没有几何着色器)。如果有一个几何 着色器,投影工作可以推迟到几何着色器。
NOTE:顶点着色器(或几何着色器)不做透视分割; 它只是做投影矩阵的一部分。 透视分割稍后将由硬件完成。
在上一节中的示例顶点着色器代码是:
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
这段代码定义了一个名为cbPerObject的cbuffer对象(常量缓冲区)。常量缓冲区只是可以存储着色器可以访问的不同变量的数据块。在这个例子中,常量缓冲区存储一个称为gWorldViewProj的4×4矩阵,表示用于将点从本地空间转换为同类空间的组合世界,视图和投影矩阵。在HLSL中,内建的float4x4类型声明了一个4×4矩阵;如果要声明一个3×4矩阵和2×2矩阵,你将分别使用float3x4和float2x2类型。常量缓冲区中的数据不会在每个顶点变化,但通过效应框架(§6.9),C ++应用程序代码可以在运行时更新常量缓冲区的内容。这为C ++应用程序代码和效果代码进行通信提供了一种手段。例如,因为每个对象的世界矩阵不同,所以组合的世界,视图和投影矩阵每个对象都不相同;因此,在使用以前的顶点着色器绘制多个对象时,我们需要在绘制每个对象之前适当地更新gWorldViewProj变量。
一般建议是根据您需要更新内容的频率创建常量缓冲区。例如,你可以创建下面的常量缓冲区:
cbuffer cbPerObject
{
float4x4 gWVP;
};
cbuffer cbPerFrame
{
float3 gLightDirection;
float3 gLightPosition;
float4 gLightColor;
};
cbuffer cbRarely
{
float4 gFogColor;
float gFogStart;
float gFogEnd;
};
在这个例子中,我们使用了三个常量缓冲区。第一个常量缓冲区存储组合的世界,视图和投影矩阵。这个变量取决于对象,所以它必须在每个对象的基础上更新。也就是说,如果我们每帧渲染100个对象,那么我们将每帧更新这个变量100次。第二个常量缓冲区存储场景灯光变量。在这里,我们假定灯光是动画的,所以每一帧动画都需要更新一次。最后一个常量缓冲区存储用于控制雾的变量。 在这里,我们假定场景雾很少变化(例如,也许它只是在游戏当中的某个时间发生变化)。
分开常量缓冲区的动机是效率。当一个常量缓冲区被更新时,所有的变量都必须被更新。因此,根据更新频率对它们进行分组可以使冗余更新最小化。
如§5.10.3所述,在光栅化期间,从顶点着色器(或几何着色器)输出的顶点属性被插值在三角形的像素上。然后内插的值作为输入被馈送到像素着色器中。假设没有几何着色器,图6.5说明了顶点数据到目前为止的路径。
像素着色器就像是一个顶点着色器,因为它是为每个像素片段执行的函数。给定像素着色器输入,像素着色器的工作是计算像素片段的颜色值。我们注意到,像素片段可能无法生存,并将其放到后台缓冲区;例如,可能会被像素着色器(HLSL包括可以从进一步处理中丢弃像素片段的剪辑函数)截断,被深度值更小的另一个像素片段遮挡,或者像素片段可能稍后被管线测试丢弃就像模板缓冲区测试一样。因此,后台缓冲器上的像素可以具有多个像素片段候选;这是“像素片段”和“像素”之间的区别,尽管有时这些术语可以互换使用,但是上下文通常会明确指出是什么意思。
NOTE:作为一种硬件优化,像素片段在进入像素着色器之前可能被管线拒绝(例如早期的z拒绝)。这是首先进行深度测试的地方,如果像素片段被深度测试确定为被遮挡,则像素着色器被跳过。但是,有些情况下可以禁用早期的z拒绝优化。例如,如果像素着色器修改像素的深度,则像素着色器必须被执行,因为如果像素着色器改变像素着色器,那么我们不知道像素着色器之前像素的深度是多少。
图6.5 每个顶点元素都有一个由D3D11_INPUT_ELEMENT_DESC数组指定的关联语义。顶点着色器的每个参数也都有一个附加的语义。语义用于匹配顶点元素与顶点着色器参数。同样,来自顶点着色器的每个输出都具有附加的语义,并且每个像素着色器输入参数具有附加的语义。这些语义用于将顶点着色器输出映射到像素着色器输入参数中。
以下是一个简单的像素着色器,对应于第6.4节给出的顶点着色器。 为了完整性,顶点着色器被再次显示。
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
void VS(float3 iPos : POSITION, float4 iColor : COLOR,
out float4 oPosH : SV_POSITION,
out float4 oColor : COLOR)
{
// Transform to homogeneous clip space.
oPosH = mul(float4(iPos, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
oColor = iColor;
}
float4 PS(float4 posH : SV_POSITION, float4 color : COLOR) : SV_Target
{
return pin.Color;
}
在这个例子中,像素着色器只是返回插值的颜色值。 请注意,像素着色器输入与顶点着色器输出完全匹配; 这是一个要求。 像素着色器返回4D颜色值,函数参数列表后面的SV_TARGET语义指示返回值类型应与渲染目标格式相匹配。
我们可以使用输入/输出结构等效地重写以前的顶点和像素着色器。 符号的不同之处在于我们将语义附加到输入/输出结构的成员,并且我们使用返回语句来输出输出而不是输出参数。
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 Pos : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.Pos, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
Direct3D基本上是一个状态机。 在我们改变它们之前,事物会保持现状。 例如,我们在§6.1,§6.2和§6.3中看到,绑定到流水线输入整合程序阶段的输入布局,顶点缓冲区和索引缓冲区会一直存在,直到我们绑定不同的结构为止。同样,当前设置的基本拓扑保持有效,直到它被改变。另外,Direct3D具有封装可用于配置Direct3D的设置的状态组:
1 . ID3D11RasterizerState:该接口表示用于配置流水线栅格化阶段的状态组。
2 . ID3D11BlendState:该接口表示用于配置混合操作的状态组。我们将在关于混合的章节中讨论这些状态; 默认情况下,混合是禁用的,所以我们现在不需要担心。
3 . ID3D11DepthStencilState:该接口表示用于配置深度和模板测试的状态组。我们将在关于模板缓冲区的章节中讨论这些状态。默认情况下,模版被禁用,所以我们现在不需要担心。默认的深度测试设置被设置为执行§4.1.5中描述的标准深度测试。
现在,关注我们的唯一状态块接口是ID3D11RasterizerState接口。 我们可以通过填写D3D11_RASTERIZER_DESC结构然后调用方法来创建这种类型的接口:
HRESULT ID3D11Device::CreateRasterizerState(
const D3D11_RASTERIZER_DESC *pRasterizerDesc,
ID3D11RasterizerState **ppRasterizerState);
第一个参数就是填充D3D11_RASTERIZER_DESC结构,描述要创建的光栅化器状态块; 第二个参数用于返回指向创建的ID3D11RasterizerState接口的指针。
D3D11_RASTERIZER_DESC结构定义如下:
typedef struct D3D11_RASTERIZER_DESC {
D3D11_FILL_MODE FillMode; // Default: D3D11_FILL_SOLID
D3D11_CULL_MODE CullMode; // Default: D3D11_CULL_BACK
BOOL FrontCounterClockwise; // Default: false
INT DepthBias; // Default: 0
FLOAT DepthBiasClamp; // Default: 0.0f
FLOAT SlopeScaledDepthBias; // Default: 0.0f
BOOL DepthClipEnable; // Default: true
BOOL ScissorEnable; // Default: false
BOOL MultisampleEnable; // Default: false
BOOL AntialiasedLineEnable; // Default: false
} D3D11_RASTERIZER_DESC;
这些成员大多数是高级或不经常使用; 因此,我们将向您介绍SDK文档以获取每个成员的描述。不过,只有前三个值得在这里讨论。
1 . FillMode:为线框渲染指定D3D11_FILL_WIREFRAME,或为实体渲染指定D3D11_FILL_SOLID。 固体渲染是默认的。
2 . CullMode:指定D3D11_CULL_NONE禁用剔除,D3D11_CULL_BACK剔除背向三角,D3D11_CULL_FRONT剔除前向三角。 背面三角形默认为剔除。
3 . FrontCounterClockwise:如果您希望顺时针(相对于相机)排列的三角形被视为正面,逆时针(相对于相机)排列的三角形被视为背面,则指定false。 如果您希望将逆时针(相对于相机)排列的三角形视为正面,并将顺时针(相对于相机)排列的三角形视为背面,请指定true。 此状态默认为false。
一旦创建了ID3D11RasterizerState对象,我们就可以用新的状态块更新设备:
void ID3D11DeviceContext::RSSetState(
ID3D11RasterizerState *pRasterizerState);
以下代码显示如何创建禁用背面剔除的栅格化状态:
D3D11_RASTERIZER_DESC rsDesc;
ZeroMemory(&rsDesc, sizeof(D3D11_RASTERIZER_DESC));
rsDesc.FillMode = D3D11_FILL_SOLID;
rsDesc.CullMode = D3D11_CULL_NONE;
rsDesc.FrontCounterClockwise = false;
rsDesc.DepthClipEnable = true;
HR(md3dDevice->CreateRasterizerState(&rsDesc, &mNoCullRS));
NOTE:使用ZeroMemory可以初始化我们不设置的其他属性,因为它们的默认值是零或为假。 但是,如果属性具有非零或真实的默认值,并且您想要默认值,那么您将不得不显式设置属性。
请注意,对于应用程序,您可能需要几个不同的ID3D11RasterizerState对象。所以你要做的就是在初始化时创建它们,然后根据需要在应用程序更新/绘制代码中进行切换。例如,假设您有两个对象,并且您想要以线框模式绘制第一个对象,而以固定模式绘制第一个对象。然后,您将有两个ID3D11RasterizerState对象,并在绘制对象时在它们之间切换:
// Create render state objects as initialization time.
ID3D11RasterizerState* mWireframeRS;
ID3D11RasterizerState* mSolidRS;
...
// Switch between the render state objects in the draw function.
md3dImmediateContext->RSSetState(mSolidRS);
DrawObject();
md3dImmediateContext->RSSetState(mWireframeRS);
DrawObject();
应该指出的是,Direct3D从不将状态恢复到之前的设置。 因此,绘制对象时应始终设置所需的状态。 对设备的当前状态做出不正确的假设将导致错误的输出。
每个状态块都有一个默认状态。 通过使用null调用RSSetState方法,我们可以恢复到默认状态:
// Restore default state.
md3dImmediateContext->RSSetState( 0 );
NOTE:通常,应用程序不需要在运行时创建额外的渲染状态组。因此,应用程序可以在初始化时定义并创建所有必要的渲染状态组。而且,由于渲染状态组在运行时不需要修改,因此可以为渲染代码提供对它们的全局只读访问权限。例如,您可以将所有渲染状态组对象放在一个静态类中。这样,您不会创建重复的渲染状态组对象,渲染代码的各个部分可以共享渲染状态组对象。
effects framework是一组实用程序代码,它提供了一个用于组织着色器程序和渲染阶段的框架,这些阶段一起工作来实现特定的渲染效果。例如,您可能对渲染水,云,金属物体和动画角色有不同的效果。每个效果将由至少一个顶点着色器,一个像素着色器和渲染状态组成,以实现该效果。
在以前的Direct3D版本中,一旦与D3D 10库链接后,效果框架就可以使用。在Direct3D 11中,效果框架已经被移到了D3DX库中,并且您必须包含一个单独的头文件(d3dx11Effect.h),并链接到单独的库(D3DX11Effects.lib用于发布版本,D3DX11EffectsD.lib用于调试版本)。另外,在Direct3D 11中,它们为您提供了效果库代码的完整源代码(DirectX SDK \ Samples \ C ++ \ Effects11)。因此,您可以根据自己的需要修改效果框架。在这本书中,我们只会使用效果框架,而不做任何修改。为了使用这个库,你需要首先在release和debug模式下构建Effects11项目来生成D3DX11Effects.lib和D3DX11EffectsD.lib文件;除非更新了效果框架(例如,新版本的DirectX SDK可能会更新这些文件,因此您可能需要重新生成.lib文件以获取最新版本),否则只需要执行一次该操作。d3dx11Effect.h头文件可以在DirectX SDK \ Samples \ C ++ \ Effects11 \ Inc中找到。对于我们的示例项目,我们将d3dx11Effect.h,D3DX11EffectsD.lib和D3DX11Effects.lib文件放置在我们所有项目共享代码的Common目录中(请参阅“简介”,了解示例项目组织的描述)。
我们已经讨论了顶点着色器,像素着色器,以及较小程度的几何和镶嵌着色器。 我们还讨论了常量缓冲区,可以用来存储着色器可以访问的“全局”变量。 这样的代码通常写在一个效果文件(.fx)中,它只是一个文本文件(就像C ++代码写在.h / .cpp文件中一样)。 除了房屋着色器和常数缓冲之外,效果还包含至少一种技术。 反过来,一种技术至少包含一个通道。
1.技术:一种技术由一个或多个用于创建特定渲染技术的通道组成。 对于每个通道,几何体都以不同的方式呈现,并且每个通道的结果以某种方式进行组合以获得期望的结果。例如,地形渲染技术可以使用多遍纹理化技术。请注意,多遍技术通常很昂贵,因为几何图形每次都要重新绘制; 然而,多通道技术需要实现一些渲染技术。
2.通道:通过包括顶点着色器,可选的几何着色器,可选镶嵌相关着色器,像素着色器和渲染状态。这些组件指示如何处理和遮挡此通道的几何图形。我们注意到像素着色器也可以是可选的(很少)。例如,我们可能只想渲染到深度缓冲区而不是后台缓冲区; 在这种情况下,我们不需要使用像素着色器遮挡任何像素。
NOTE:技术也可以组合在一起,称为效应组。如果你没有明确的定义一个,编译器会创建一个匿名的来包含效果文件中的所有技术。在本书中,我们没有明确定义任何效果组。
以下是本章演示的整个效果文件:
cbuffer cbPerObject
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 Pos : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.Pos, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
technique11 ColorTech
{
pass P0
{
SetVertexShader( CompileShader( vs_5_0, VS() ) );
SetPixelShader( CompileShader( ps_5_0, PS() ) );
}
}
NOTE:点和矢量坐标相对于许多不同的空间(例如,局部空间,世界空间,视图空间,均匀空间)被指定。 读取代码时,点/矢量的坐标系相对于哪个坐标系可能并不明显。 因此,我们经常使用下面的后缀表示空间:L(对于局部空间),W(对于世界空间),V(对于视图空间)和H(对于同质剪辑空间)。 这里有些例子:
float3 iPosL; // local space
float3 gEyePosW; // world space
float3 normalV; // view space
float4 posH; // homogeneous clip space
我们提到pass是由渲染状态组成的。也就是说,可以创建状态块并直接在效果文件中进行设置。当效果需要特定的渲染状态时,这很方便; 相反,一些效果可能与变量渲染状态设置一起工作,在这种情况下,我们更喜欢在应用级别设置状态以便于状态切换。以下代码显示如何在效果文件中创建和设置光栅化器状态块。
RasterizerState WireframeRS
{
FillMode = Wireframe;
CullMode = Back;
FrontCounterClockwise = false;
// Default values used for any properties we do not set.
};
technique11 ColorTech
{
pass P0
{
SetVertexShader( CompileShader( vs_5_0, VS() ) );
SetPixelShader( CompileShader( ps_5_0, PS() ) );
SetRasterizerState(WireframeRS);
}
}
观察光栅器状态对象定义中的右侧值基本上与C ++情况下的相同,除了前缀被省略(例如,省略D3D11_FILL_和D3D11_CULL_)。
NOTE:因为一个效果通常写在一个外部.fx文件中,所以可以修改它,而不必重新编译源代码。
创建效果的第一步是编译在.fx文件中定义的着色器程序。 这可以通过以下D3DX功能来完成。
HRESULT D3DX11CompileFromFile(
LPCTSTR pSrcFile,
CONST D3D10_SHADER_MACRO *pDefines,
LPD3D10INCLUDE pInclude,
LPCSTR pFunctionName,
LPCSTR pProfile,
UINT Flags1,
UINT Flags2,
ID3DX11ThreadPump *pPump,
ID3D10Blob **ppShader,
ID3D10Blob **ppErrorMsgs,
HRESULT *pHResult);
1 . pFileName:包含我们要编译的效果源代码的.fx文件的名称。
2 .pDefines:我们不使用的高级选项;请参阅SDK文档。我们总是在本书中指定null。
3 .pInclude:我们不使用的高级选项;请参阅SDK文档。我们总是在本书中指定null。
4 .pFunctionName:着色器功能名称入口点。这仅在单独编译着色器程序时使用。使用效果框架时,指定为null,因为在效果文件内部定义的技术指定了着色器入口点。
5 .pProfile:指定我们正在使用的着色器版本的字符串。对于Direct3D 11效果,我们使用着色器版本5.0(“fx_5_0”)。
6 . Flags1:来指定应该如何编译着色器代码的标志。SDK中列出了相当多的这些标志文档,但是我们在此使用的唯一两个是:
(a)D3D10_SHADER_DEBUG:以调试模式编译着色器。
(b)D3D10_SHADER_SKIP_OPTIMIZATION:指示编译器跳过优化(用于调试)。
7 . Flags2:我们不使用的高级效果编译选项;请参阅SDK文档。
8 . pPump:我们不用于异步编译着色器的高级选项;请参阅SDK文档。我们总是指定
在这本书中为null。
9 . ppShader:返回指向存储编译着色器的ID3D10Blob数据结构的指针。
10 . ppErrorMsgs:返回指向ID3D10Blob数据结构的指针,该数据结构存储包含编译错误的字符串(如果有)。
11 . pHResult:用于获取异步编译时返回的错误代码。如果为pPump指定了null,则指定null。
1.除了在.fx中编译着色器之外,这个函数还可以用来编译单独的着色器。一些程序员不使用效果框架(或自己制作),因此他们分别定义和编译它们的着色器。
2.上一个函数中对“D3D10”的引用不是拼写错误。 D3D11编译器是建立在D3D10编译器上的,所以Direct3D 11团队并不打扰重命名某些标识符。
3. ID3D10Blob类型只是一个通用的内存块,有两种方法:
(a)LPVOID GetBufferPointer:返回一个void *给数据,所以在使用前它必须被转换成合适的类型(见下面的例子)。
(b)SIZE_T GetBufferSize:返回缓冲区的字节大小。
一旦效果着色器被编译,我们可以通过如下函数创建一个效果(由ID3DXEffect11接口表示):
HRESULT D3DX11CreateEffectFromMemory(
void *pData,
SIZE_T DataLength,
UINT FXFlags,
ID3D11Device *pDevice,
ID3DX11Effect **ppEffect);
1 . pData:指向编译效果数据的指针。
2 . DataLength:编译的效果数据的字节大小。
3 . FXFlags:效果标志应与D3DX11CompileFromFile函数中为Flags2指定的标志匹配。
4 . pDevice:指向Direct3D 11设备的指针。
5 . ppEffect:返回一个指向创建效果的指针。
以下代码显示如何编译和创建一个效果:
DWORD shaderFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
shaderFlags |= D3D10_SHADER_DEBUG;
shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;
#endif
ID3D10Blob* compiledShader = 0;
ID3D10Blob* compilationMsgs = 0;
HRESULT hr = D3DX11CompileFromFile(L"color.fx", 0,
0, 0, "fx_5_0", shaderFlags,
0, 0, &compiledShader, &compilationMsgs, 0);
// compilationMsgs can store errors or warnings.
if(compilationMsgs != 0)
{
MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);
ReleaseCOM(compilationMsgs);
}
// Even if there are no compilationMsgs, check to make sure there
// were no other errors.
if(FAILED(hr))
{
DXTrace(__FILE__, (DWORD)__LINE__, hr,
L"D3DX11CompileFromFile", true);
}
ID3DX11Effect* mFX;
HR(D3DX11CreateEffectFromMemory(
compiledShader->GetBufferPointer(),
compiledShader->GetBufferSize(),
0, md3dDevice, &mFX));
// Done with compiled shader.
ReleaseCOM(compiledShader);
NOTE:创建Direct3D资源非常昂贵,应始终在初始化时完成,而不要在运行时完成。这意味着创建输入布局,缓冲区,渲染状态对象和效果应始终在初始化时完成。
C ++应用程序代码通常需要与效果进行通信; 特别是C ++应用程序通常需要更新常量缓冲区中的变量。 例如,假设我们在效果文件中定义了以下常量缓冲区:
cbuffer cbPerObject
{
float4x4 gWVP;
float4 gColor;
float gSize;
int gIndex;
bool gOptionOn;
};
通过ID3DX11Effect接口,我们可以获得指向常量缓冲区中变量的指针:
ID3DX11EffectMatrixVariable* fxWVPVar;
ID3DX11EffectVectorVariable* fxColorVar;
ID3DX11EffectScalarVariable* fxSizeVar;
ID3DX11EffectScalarVariable* fxIndexVar;
ID3DX11EffectScalarVariable* fxOptionOnVar;
fxWVPVar = mFX->GetVariableByName("gWVP")->AsMatrix();
fxColorVar = mFX->GetVariableByName("gColor")->AsVector();
fxSizeVar = mFX->GetVariableByName("gSize")->AsScalar();
fxIndexVar = mFX->GetVariableByName("gIndex")->AsScalar();
fxOptionOnVar = mFX->GetVariableByName("gOptionOn")->AsScalar();
ID3DX11Effect :: GetVariableByName方法返回ID3DX11EffectVariable类型的指针。 这是一个通用的效果变量类型; 要获得指向特定类型的指针(例如,矩阵,向量,标量),必须使用适当的As方法(例如,AsMatrix,AsVector,AsScalar)。
一旦我们有了指向变量的指针,我们可以通过C ++接口来更新它们。 这里有些例子:
fxWVPVar->SetMatrix((float*)&M ); // assume M is of type XMMATRIX
fxColorVar->SetFloatVector( (float*)&v ); // assume v is of type XMVECTOR
fxSizeVar->SetFloat( 5.0f );
fxIndexVar->SetInt( 77 );
fxOptionOnVar->SetBool( true );
请注意,这些调用会更新效果对象中的内部缓存,在我们应用渲染过程(第6.8.4节)之前,不会传输到GPU内存。这确保了对GPU内存的更新,而不是许多小的更新,这将是低效的。
NOTE:效果变量不需要专门指定。例如,你可以写:
ID3DX11EffectVariable* mfxEyePosVar;
mfxEyePosVar = mFX->GetVariableByName("gEyePosW");
…
mfxEyePosVar->SetRawValue(&mEyePos, 0, sizeof(XMFLOAT3));
这对设置任意大小的变量(例如,一般结构)很有用。 请注意,ID3DX11EffectVectorVariable接口假定为4D矢量,因此如果您要使用3D矢量(如XMFLOAT3),您将需要使用ID3DX11EffectVariable,如前所述。
除了常量缓冲区变量之外,还需要获取指向存储在效果文件中的技术对象的指针。 例如:
ID3DX11EffectTechnique* mTech;
mTech = mFX->GetTechniqueByName("ColorTech");
这个方法采用的单个参数是你希望获得指针的技术的字符串名称。
要使用一种技术来绘制几何图形,我们只需要确保常量缓冲区中的变量是最新的。 然后我们循环遍历技术中的每个遍,应用通道,并绘制几何:
// Set constants
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
// Draw some geometry.
md3dImmediateContext->DrawIndexed(36, 0, 0);
}
当几何体在通道中绘制时,将使用该通道设置的着色器和渲染状态进行绘制。 ID3DX11EffectTechnique :: GetPassByIndex方法返回一个指向ID3DX11EffectPass接口的指针,该接口表示具有指定索引的传递。Apply方法更新存储在GPU内存中的常量缓冲区,将着色器程序绑定到管道,并应用通道设置的任何渲染状态。 在当前版本的Direct3D 11中,ID3DX11EffectPass :: Apply的第一个参数未使用,应指定零; 第二个参数是传递将使用的设备上下文的指针。
如果您需要在绘制调用之间的常量缓冲区中更改变量值,则必须在绘制几何图形之前调用“应用”来更新更改:
for(UINT i = 0; i < techDesc.Passes; ++i)
{
ID3DX11EffectPass* pass = mTech->GetPassByIndex(i);
// Set combined world-view-projection matrix for land geometry.
worldViewProj = landWorld*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
pass->Apply(0, md3dImmediateContext);
mLand.draw();
// Set combined world-view-projection matrix for wave geometry.
worldViewProj = wavesWorld*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
pass->Apply(0, md3dImmediateContext);
mWaves.draw();
}
到目前为止,我们已经展示了如何通过D3DX11CompileFromFile函数在运行时编译效果。 这样做有点让人烦恼,因为如果你的效果文件代码有编译错误,那么直到你运行该程序时才会发现它。可以使用DirectX SDK(位于DirectX SDK \ Utilities \ bin \ x86)附带的fxc工具离线编译效果。而且,你可以修改你的VC ++项目来调用fxc来编译你的效果,作为正常编译过程的一部分。以下步骤显示如何执行此操作:
1.确保在项目的VC ++目录选项卡中的“可执行目录”下列出了DirectX SDK \ Utilities \ bin \ x86的路径,如介绍中所述。
2.将效果文件添加到您的项目。
3.对于每个效果,右键单击解决方案资源管理器中的效果文件,选择属性,然后添加一个自定义生成工具(见图6.6)。
调试模式:
fxc /Fc /Od /Zi /T fx_5_0 /Fo “%(RelativeDir)\%(Filename).fxo” “%(FullPath)”
发布模式:
fxc /T fx_5_0 /Fo “%(RelativeDir)\%(Filename).fxo” “%(FullPath)”
您可以查看SDK文档以获取fxc编译标志的完整列表。 我们用于调试模式的三个标志“/ Fc / Od / Zi”分别输出汇编列表,禁用优化和启用调试信息。
NOTE:不时查看你的着色器的编译,检查一下是否有你不希望生成的东西被指示出来。例如,如果在HLSL代码中有一个条件语句,那么可能会希望在汇编代码中存在分支指令。在GPU上分支是相当昂贵的(或者某些DirectX 9硬件不支持),所以有时编译器会通过评估两个分支来将条件语句变平,然后在两者之间进行插值以选择正确的答案。也就是说,下面的代码会给出相同的答案:
条件 | 展平 |
---|---|
float x = 0; // s == 1 (true) or s == 0 (false) if( s ) { x = sqrt(y); } else { x = 2*y; } | float a=2 * y; float b = sqrt(y); float x = a + s * (b-a); // s == 1: // x = a + b – a = b = sqrt(y) // s == 0: // x = a + 0 * (b-a) = a = 2 * y |
所以不使用分支扁平方法给了我们相同的结果,但没有看着汇编代码,我们不知道是否发生扁平化,或者如果生成一个真正的分支指令。 问题的关键在于,有时候你想看看程序到底怎样执行的。
现在,当你建立你的项目时,fxc将会被调用,并产生一个.fxo文件的效果的编译版本。 而且,如果有任何来自fxc的编译警告或错误,它们将显示在调试输出窗口中。 例如,如果我们在color.fx效果文件中错误地命名一个变量:
// Should be gWorldViewProj, not worldViewProj!
vout.PosH = mul(float4(vin.Pos, 1.0f), worldViewProj);
然后我们从调试输出窗口中列出的这一个错误(最重要的错误是修复的关键错误)中得到了一些错误:
error X3004: undeclared identifier ‘worldViewProj’
error X3013: ‘mul’: intrinsic function does not take 2 parameters
error X3013: Possible intrinsic functions are:
error X3013: mul(float, float)
…
在编译时获取错误消息比在运行时更方便。现在,我们已经将我们的效果文件(.fxo)编译为构建过程的一部分,所以我们不再需要在运行时执行它(即,我们不需要调用D3DX11CompileFromFile)。但是,我们仍然需要加载编译的着色器 来自.fxo文件的数据,并将其提供给D3DX11CreateEffectFromMemory函数。 这可以使用标准的C ++文件输入机制来完成,如下所示:
std::ifstream fin("fx/color.fxo", std::ios::binary);
fin.seekg(0, std::ios_base::end);
int size = (int)fin.tellg();
fin.seekg(0, std::ios_base::beg);
std::vector<char> compiledShader(size);
fin.read(&compiledShader[0], size);
fin.close();
HR(D3DX11CreateEffectFromMemory(&compiledShader[0], size,
0, md3dDevice, &mFX));
除了在运行时编译着色器代码的“Box”演示例外之外,我们将所有着色器编译为构建过程的一部分。
我们在本节开头提到,一个效应可以有多种技术。 那么为什么我们会有多种渲染效果技术呢? 让我们以阴影作为例子,而不涉及如何完成阴影的细节。 本质上,阴影质量越高,阴影技术越昂贵。 为了支持低端和高端显卡的用户,我们可能会实现一个低,中,高质量的阴影技术。 所以,即使我们有一个阴影效应,我们使用多种技术来实现效果。我们的阴影效果文件可能如下所示:
// Omit constant buffers, vertex structures, etc...
VertexOut VS(VertexIn vin) {/* Omit implementation details */}
float4 LowQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* Do low quality specific stuff */
/* Do more work common to all quality levels */
}
float4 MediumQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* Do medium quality specific stuff */
/* Do more work common to all quality levels */
}
float4 HighQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* Do high quality specific stuff */
/* Do more work common to all quality levels */
}
technique11 ShadowsLow
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, LowQualityPS()));
}
}
technique11 ShadowsMedium
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader( CompileShader(ps_5_0, MediumQualityPS()));
}
}
technique11 ShadowsHigh
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader( CompileShader(ps_5_0, HighQualityPS()));
}
}
然后,C ++应用程序代码可以检测用户的图形卡的能力,并选择适当的技术来使用
渲染。
NOTE:前面的代码假定三种阴影技术中只有像素着色器不同,所以所有的技术都共享同一个顶点着色器。 但是,如果需要的话,每种技术也可以有不同的顶点着色器。
先前实现中的一个烦人的问题是,尽管像素着色器在阴影代码中有所不同,但它们仍然具有所有需要被复制的通用代码。 有人可能会建议使用条件语句,这是朝正确方向迈出的一步。 在着色器中动态分支语句有一些开销,所以如果我们真的需要,我们只应该使用它们。 我们真正想要做的是一个条件编译,它会在编译时生成我们需要的所有着色器变体,所以在着色器代码中没有分支指令。 幸运的是,效果框架提供了一个方法来做到这一点。 考虑新的实现:
// Omit constant buffers, vertex structures, etc...
VertexOut VS(VertexIn vin) {/* Omit implementation details */}
#define LowQuality 0
#define MediumQuality 1
#define HighQuality 2
float4 PS(VertexOut pin, uniform int gQuality) : SV_Target
{
/* Do work common to all quality levels */
if(gQuality == LowQuality)
{
/* Do low quality specific stuff */
}
else if(gQuality == MediumQuality)
{
/* Do medium quality specific stuff */
}
else
{
/* Do high quality specific stuff */
}
/* Do more work common to all quality levels */
}
technique11 ShadowsLow
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(LowQuality)));
}
}
technique11 ShadowsMedium
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader( CompileShader(ps_5_0, PS(MediumQuality)));
}
}
technique11 ShadowsHigh
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(HighQuality)));
}
}
注意到我们已经为像素着色器添加了一个额外的统一参数来表示质量级别。 这个参数是不同的,因为它不会改变每个像素,而是统一/常量。 而且,我们不会在运行时更改它,就像我们更改常量缓冲区变量一样。 相反,我们在编译时设置它,并且由于该值在编译时已知,因此它允许效果框架根据其值生成不同的着色器变体。 这使我们能够创建我们的低,中,高质量的像素着色器,而无需复制代码(效果框架基本上为我们复制了编译时间过程中的代码),而无需使用分支指令。
这里有一些其他的着色器生成常见的例子:
1.应用纹理? 应用程序可能希望将纹理应用于某些对象,但不适用于其他对象。 一种解决方案是创建两个像素着色器,一个应用纹理,另一个不应用。 或者我们可以使用着色器生成器机制为我们创建两个像素着色器,然后让C ++应用程序选择所需的技术。
float4 PS(VertexOut pin, uniform bool gApplyTexture) : SV_Target
{
/* Do common work */
if(gApplyTexture)
{
/* Apply texture */
}
/* Do more common work */
}
technique11 BasicTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(false)));
}
}
technique11 TextureTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(true)));
}
}
2.有多少灯使用? 游戏关卡可以在任何给定的时间支持1到4个活动灯光。 灯光使用越多,灯光计算的成本就越高。 我们可以根据灯光的数量实现单独的顶点着色器,或者我们可以使用着色器生成器机制为我们创建四个顶点着色器,然后让C ++应用程序根据当前活动灯光的数量选择所需的技术:
VertexOut VS(VertexOut pin, uniform int gLightCount)
{
/* Do common work */
for(int i = 0; i < gLightCount; ++i)
{
/* do lighting work */
}
/* Do more common work */
}
technique11 Light1
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(1)));
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}
technique11 Light2
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(2)));
SetPixelShader( CompileShader(ps_5_0, PS()));
}
}
technique11 Light3
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(3)));
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}
technique11 Light4
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(4)));
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}
没有任何东西将我们限制在一个参数上。 我们将需要将阴影质量,纹理和灯光的数量结合在一起,因此顶点和像素着色器将如下所示:
VertexOut VS(VertexOut pin, uniform int gLightCount)
{}
float4 PS(VertexOut pin,
uniform int gQuality,
uniform bool gApplyTexture) : SV_Target
{}
例如,为了创建一种使用低质量阴影,两个灯光和没有纹理的技术,我们会写:
technique11 LowShadowsTwoLightsNoTextures
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(2)));
SetPixelShader(CompileShader(ps_5_0, PS(LowQuality, false)));
}
}
如果您不打算查看某个效果文件的汇编输出,那么我们将在此部分中显示一个样子。不过,我们不解释汇编,但是如果您之前学过汇编,则可能会认出 mov指令,也许可以猜测dp4是一个4D点的产品。 即使没有了解汇编,列表提供了一些有用的信息。 它清楚地标识了输入和输出签名,并给出了大致的指令计数,这是一个有用的度量标准,以了解着色器的成本/复杂程度。 而且,我们看到,我们着色器的多个版本确实是基于编译时间参数生成的,没有分支指令。 除了我们添加一个简单的统一布尔参数来产生两种技术之外,我们使用的效果文件与§6.8.1中所示的效果文件相同。
float4 PS(VertexOut pin, uniform bool gUseColor) : SV_Target
{
if(gUseColor)
{
return pin.Color;
}
else
{
return float4(0, 0, 0, 1);
}
}
technique11 ColorTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(true)));
}
}
technique11 NoColorTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetPixelShader(CompileShader(ps_5_0, PS(false)));
}
}
//
// FX Version: fx_5_0
//
// 1 local buffer(s)
//
cbuffer cbPerObject
{
float4x4 gWorldViewProj; // Offset: 0, size: 64
}
//
// 1 groups(s)
//
fxgroup
{
//
// 2 technique(s)
//
technique11 ColorTech
{
pass P0
{
VertexShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
// Buffer Definitions:
//
// cbuffer cbPerObject
// {
//
// float4x4 gWorldViewProj; // Offset: 0 Size: 64
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim Slot Elements
// ------------------- ----- ------ ----------- ---- --------
// cbPerObject cbuffer NA NA 0 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// POSITION 0 xyz 0 NONE float xyz
// COLOR 0 xyzw 1 NONE float xyzw
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float xyzw
// COLOR 0 xyzw 1 NONE float xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 1
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 o0.x, r0.xyzw, cb0[0].xyzw
dp4 o0.y, r0.xyzw, cb0[1].xyzw
dp4 o0.z, r0.xyzw, cb0[2].xyzw
dp4 o0.w, r0.xyzw, cb0[3].xyzw
mov o1.xyzw, v1.xyzw
ret
// Approximately 8 instruction slots used
};
PixelShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float
// COLOR 0 xyzw 1 NONE float xyzw
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_Target 0 xyzw 0 TARGET float xyzw
//
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_input_ps linear v1.xyzw
dcl_output o0.xyzw
mov o0.xyzw, v1.xyzw
ret
// Approximately 2 instruction slots used
};
}
}
t
echnique11 NoColorTech
{
pass P0
{
VertexShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
// Buffer Definitions:
//
// cbuffer cbPerObject
// {
//
// float4x4 gWorldViewProj; // Offset: 0 Size: 64
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim Slot Elements
// ------------------- ----- ------ ----------- ---- --------
// cbPerObject cbuffer NA NA 0 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// POSITION 0 xyz 0 NONE float xyz
// COLOR 0 xyzw 1 NONE float xyzw
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float xyzw
// COLOR 0 xyzw 1 NONE float xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 1
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 o0.x, r0.xyzw, cb0[0].xyzw
dp4 o0.y, r0.xyzw, cb0[1].xyzw
dp4 o0.z, r0.xyzw, cb0[2].xyzw
dp4 o0.w, r0.xyzw, cb0[3].xyzw
mov o1.xyzw, v1.xyzw
ret
// Approximately 8 instruction slots used
};
PixelShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float
// COLOR 0 xyzw 1 NONE float
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_Target 0 xyzw 0 TARGET float xyzw
//
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_output o0.xyzw
mov o0.xyzw, l(0,0,0,1.000000)
ret
// Approximately 2 instruction slots used
};
}
}
}
最后,我们已经学习了足够的知识来做一个简单的示例,它呈现一个彩色立方体。这个例子基本上把我们在本章中学的所有东西放到一个程序中。值得仔细研读。注意,该程序使用§6.8.1中的“color.fx”效果。
//*********************************************************************
// BoxDemo.cpp by Frank Luna (C) 2011 All Rights Reserved.
//
// Demonstrates rendering a colored box.
//
// Controls:
// Hold the left mouse button down and move the mouse to rotate.
// Hold the right mouse button down to zoom in and out.
//
//*********************************************************************
#include "d3dApp.h"
#include "d3dx11Effect.h"
#include "MathHelper.h"
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
class BoxApp : public D3DApp
{
public:
BoxApp(HINSTANCE hInstance);
~BoxApp();
bool Init();
void OnResize();
void UpdateScene(float dt);
void DrawScene();
void OnMouseDown(WPARAM btnState, int x, int y);
void OnMouseUp(WPARAM btnState, int x, int y);
void OnMouseMove(WPARAM btnState, int x, int y);
private:
void BuildGeometryBuffers();
void BuildFX();
void BuildVertexLayout();
private:
ID3D11Buffer* mBoxVB;
ID3D11Buffer* mBoxIB;
ID3DX11Effect* mFX;
ID3DX11EffectTechnique* mTech;
ID3DX11EffectMatrixVariable* mfxWorldViewProj;
ID3D11InputLayout* mInputLayout;
XMFLOAT4X4 mWorld;
XMFLOAT4X4 mView;
XMFLOAT4X4 mProj;
float mTheta;
float mPhi;
float mRadius;
POINT mLastMousePos;
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
PSTR cmdLine, int showCmd)
{
// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
BoxApp theApp(hInstance);
if(!theApp.Init())
return 0;
return theApp.Run();
}
BoxApp::BoxApp(HINSTANCE hInstance)
: D3DApp(hInstance), mBoxVB(0), mBoxIB(0), mFX(0), mTech(0),
mfxWorldViewProj(0), mInputLayout(0),
mTheta(1.5f*MathHelper::Pi), mPhi(0.25f*MathHelper::Pi), mRadius(5.0f)
{
mMainWndCaption = L"Box Demo";
mLastMousePos.x = 0;
mLastMousePos.y = 0;
XMMATRIX I = XMMatrixIdentity();
XMStoreFloat4x4(&mWorld, I);
XMStoreFloat4x4(&mView, I);
XMStoreFloat4x4(&mProj, I);
}
BoxApp::~BoxApp()
{
ReleaseCOM(mBoxVB);
ReleaseCOM(mBoxIB);
ReleaseCOM(mFX);
ReleaseCOM(mInputLayout);
}
bool BoxApp::Init()
{
if(!D3DApp::Init())
return false;
BuildGeometryBuffers();
BuildFX();
BuildVertexLayout();
return true;
}
void BoxApp::OnResize()
{
D3DApp::OnResize();
// The window resized, so update the aspect ratio and recomputed
// the projection matrix.
XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathHelper::Pi,
AspectRatio(), 1.0f, 1000.0f);
XMStoreFloat4x4(&mProj, P);
}
void BoxApp::UpdateScene(float dt)
{
// Convert Spherical to Cartesian coordinates.
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
// Build the view matrix.
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
XMStoreFloat4x4(&mView, V);
}
void BoxApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Blue));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
md3dImmediateContext->IASetInputLayout(mInputLayout);
md3dImmediateContext->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
UINT stride = sizeof(Vertex);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mBoxVB,
&stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mBoxIB,
DXGI_FORMAT_R32_UINT, 0);
// Set constants
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc( &techDesc );
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
// 36 indices for the box.
md3dImmediateContext->DrawIndexed(36, 0, 0);
}
HR(mSwapChain->Present(0, 0));
}
void BoxApp::OnMouseDown(WPARAM btnState, int x, int y)
{
mLastMousePos.x = x;
mLastMousePos.y = y;
SetCapture(mhMainWnd);
}
void BoxApp::OnMouseUp(WPARAM btnState, int x, int y)
{
ReleaseCapture();
}
void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
{
if( (btnState & MK_LBUTTON) != 0 )
{
// Make each pixel correspond to a quarter of a degree.
float dx = XMConvertToRadians(
0.25f*static_cast<float>(x - mLastMousePos.x));
float dy = XMConvertToRadians(
0.25f*static_cast<float>(y - mLastMousePos.y));
// Update angles based on input to orbit camera around box.
mTheta += dx;
mPhi += dy;
// Restrict the angle mPhi.
mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi-0.1f);
}
else if( (btnState & MK_RBUTTON) != 0 )
{
// Make each pixel correspond to 0.005 unit in the scene.
float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
// Update the camera radius based on input.
mRadius += dx - dy;
// Restrict the radius.
mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
}
mLastMousePos.x = x;
mLastMousePos.y = y;
}
void BoxApp::BuildGeometryBuffers()
{
// Create vertex buffer
Vertex vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White },
{ XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black },
{ XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red },
{ XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green },
{ XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue },
{ XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow },
{ XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan },
{ XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }
};
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mBoxVB));
// Create the index buffer
UINT indices[] = {
// front face
0, 1, 2,
0, 2, 3,
// back face
4, 6, 5,
4, 7, 6,
// left face
4, 5, 1,
4, 1, 0,
// right face
3, 2, 6,
3, 6, 7,
// top face
1, 5, 6,
1, 6, 2,
// bottom face
4, 0, 3,
4, 3, 7
};
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mBoxIB));
}
void BoxApp::BuildFX()
{
DWORD shaderFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
shaderFlags |= D3D10_SHADER_DEBUG;
shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;
#endif
ID3D10Blob* compiledShader = 0;
ID3D10Blob* compilationMsgs = 0;
HRESULT hr = D3DX11CompileFromFile(L"FX/color.fx", 0, 0, 0,
"fx_5_0", shaderFlags,
0, 0, &compiledShader, &compilationMsgs, 0);
// compilationMsgs can store errors or warnings.
if(compilationMsgs != 0)
{
MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);
ReleaseCOM(compilationMsgs);
}
// Even if there are no compilationMsgs, check to make sure there
// were no other errors.
if(FAILED(hr))
{
DXTrace(__FILE__, (DWORD)__LINE__, hr,
L"D3DX11CompileFromFile", true);
}
HR(D3DX11CreateEffectFromMemory(
compiledShader->GetBufferPointer(),
compiledShader->GetBufferSize(),
0, md3dDevice, &mFX));
// Done with compiled shader.
ReleaseCOM(compiledShader);
mTech = mFX->GetTechniqueByName("ColorTech");
mfxWorldViewProj = mFX->GetVariableByName(
"gWorldViewProj")->AsMatrix();
}
void BoxApp::BuildVertexLayout()
{
// Create the vertex input layout.
D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{"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}
};
// Create the input layout
D3DX11_PASS_DESC passDesc;
mTech->GetPassByIndex(0)->GetDesc(&passDesc);
HR(md3dDevice->CreateInputLayout(vertexDesc, 2,
passDesc.pIAInputSignature,
passDesc.IAInputSignatureSize, &mInputLayout));
}
本章还包括一个“山”演示。 它使用与Box演示相同的Direct3D方法,除了绘制更复杂的几何图形。具体说明如何构造一个三角网格网格; 这种几何结构特别适用于地形和水面渲染等等。
一个“好的”实值函数y = f(x,z)的图形是一个曲面。我们可以通过在xz平面中构造一个网格来近似表面,其中每个四边形由两个三角形构建,然后将函数应用到每个网格点; 见图6.8。
所以主要的任务是如何在xz平面中建立网格。 如图6.9所示,m×n个顶点的网格包含(m-1)×(n-1)个四元组(或单元)。 每个单元格将被两个三角形覆盖,所以总共有2·(m-1)×(n-1)个三角形。 如果网格具有宽度w和深度d,则沿着x轴的单元间距是dx = w /(n-1),沿z轴的单元间距是dz = d /(m-1)。 为了生成顶点,我们从左上角开始逐行递增地计算顶点坐标。 第i个网格顶点在xz平面中的坐标由下式给出:
void GeometryGenerator::CreateGrid(float width, float depth,
UINT m, UINT n, MeshData& meshData)
{
UINT vertexCount = m*n;
UINT faceCount = (m-1)*(n-1)*2;
//
// Create the vertices.
//
float halfWidth = 0.5f*width;
float halfDepth = 0.5f*depth;
float dx = width / (n-1);
float dz = depth / (m-1);
float du = 1.0f / (n-1);
float dv = 1.0f / (m-1);
meshData.Vertices.resize(vertexCount);
for(UINT i = 0; i < m; ++i)
{
float z = halfDepth - i*dz;
for(UINT j = 0; j < n; ++j)
{
float x = -halfWidth + j*dx;
meshData.Vertices[i*n+j].Position = XMFLOAT3(x, 0.0f, z);
meshData.Vertices[i*n+j].Normal = XMFLOAT3(0.0f, 1.0f, 0.0f);
meshData.Vertices[i*n+j].TangentU = XMFLOAT3(1.0f, 0.0f, 0.0f);
// Ignore for now, used for texturing.
meshData.Vertices[i*n+j].TexC.x = j*du;
meshData.Vertices[i*n+j].TexC.y = i*dv;
}
}
}
class GeometryGenerator
{
public:
struct Vertex
{
Vertex(){}
Vertex(const XMFLOAT3& p,
const XMFLOAT3& n,
const XMFLOAT3& t,
const XMFLOAT2& uv)
: Position(p), Normal(n), TangentU(t), TexC(uv){}
Vertex(
float px, float py, float pz,
float nx, float ny, float nz,
float tx, float ty, float tz,
float u, float v)
: Position(px,py,pz), Normal(nx,ny,nz),
TangentU(tx, ty, tz), TexC(u,v){}
XMFLOAT3 Position;
XMFLOAT3 Normal;
XMFLOAT3 TangentU;
XMFLOAT2 TexC;
};
struct MeshData
{
std::vector<Vertex> Vertices;
std::vector<UINT> Indices;
};
...
};
在计算顶点后,我们需要通过指定索引来定义网格三角形。为此,我们在每个四元组上重复遍历,从左上角开始逐行重复计算索引,以定义四元组的两个三角形; 参考图6.10,对于m×n顶点网格,两个三角形的线性阵列索引计算如下:
ΔABC = (i · n + j, i · n + j + 1, (i + 1) · n + j)
ΔCBD = ((i + 1) ·n + j, i ·n + j + 1, (i + 1) · n + j + 1)
meshData.Indices.resize(faceCount*3); // 3 indices per face
// Iterate over each quad and compute indices.
UINT k = 0;
for(UINT i = 0; i < m-1; ++i)
{
for(UINT j = 0; j < n-1; ++j)
{
meshData.Indices[k] = i*n+j;
meshData.Indices[k+1] = i*n+j+1;
meshData.Indices[k+2] = (i+1)*n+j;
meshData.Indices[k+3] = (i+1)*n+j;
meshData.Indices[k+4] = i*n+j+1;
meshData.Indices[k+5] = (i+1)*n+j+1;
k += 6; // next quad
}
}
}
在我们创建了网格之后,我们可以从MeshData网格中提取我们想要的顶点元素,将平坦网格变成代表山丘的曲面,并根据顶点高度(y坐标)为每个顶点生成颜色。
// Not to be confused with GeometryGenerator.Vertex.
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
void HillsApp::BuildGeometryBuffers()
{
GeometryGenerator::MeshData grid;
GeometryGenerator geoGen;
geoGen.CreateGrid(160.0f, 160.0f, 50, 50, grid);
mGridIndexCount = grid.Indices.size();
//
// Extract the vertex elements we are interested and apply the
// height function to each vertex. In addition, color the vertices
// based on their height so we have sandy looking beaches, grassy low
// hills, and snow mountain peaks.
//
std::vector<Vertex> vertices(grid.Vertices.size());
for(size_t i = 0; i < grid.Vertices.size(); ++i)
{
XMFLOAT3 p = grid.Vertices[i].Position;
p.y = GetHeight(p.x, p.z);
vertices[i].Pos = p;
// Color the vertex based on its height.
if( p.y < -10.0f )
{
// Sandy beach color.
vertices[i].Color = XMFLOAT4(1.0f, 0.96f, 0.62f, 1.0f);
}
else if(p.y < 5.0f)
{
// Light yellow-green.
vertices[i].Color = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
}
else if(p.y < 12.0f)
{
// Dark yellow-green.
vertices[i].Color = XMFLOAT4(0.1f, 0.48f, 0.19f, 1.0f);
}
else if(p.y < 20.0f)
{
// Dark brown.
vertices[i].Color = XMFLOAT4(0.45f, 0.39f, 0.34f, 1.0f);
}
else
{
// White snow.
vertices[i].Color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
}
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * grid.Vertices.size();
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &vertices[0];
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mVB));
//
// Pack the indices of all the meshes into one index buffer.
//
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * mGridIndexCount;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &grid.Indices[0];
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));
}
float HillsApp::GetHeight(float x, float z)const
{
return 0.3f*(z*sinf(0.1f*x) + x*cosf(0.1f*z));
}
它的图形看起来有点像山丘和山谷的地形(见图6.11)。 演示程序的其余部分与盒式演示非常相似。
在本节中,我们将介绍如何构建GeometryGenerator类支持的其他两种几何形状:球体和圆柱体。这些形状对绘制天空圆顶,调试,可视化碰撞检测和延迟渲染非常有用。例如,您可能想要将所有游戏角色渲染为球体以进行调试测试。
图6.12显示了本节演示的屏幕截图。除了学习如何绘制球体和圆柱体外,您还可以获得在场景中定位和绘制多个对象的经验(即创建多个世界变换矩阵)。此外,我们将所有场景几何图形放置在一个大顶点和索引缓冲区中。然后,我们将使用DrawIndexed方法一次绘制一个对象(因为需要在对象之间更改世界矩阵); 所以你会看到一个使用DrawIndexed的StartIndexLocation和BaseVertexLocation参数的例子。
我们通过指定圆柱体的底部和顶部半径,高度以及切片和堆叠数来定义圆柱体,如图6.13所示。我们将圆柱体分成三部分:1)侧面几何体,2)顶盖几何体,以及3)底盖几何体。
我们生成以原点为中心的圆柱体,平行于y轴。从图6.13中可以看出,所有顶点都位于圆柱体的“环”上,其中有stackCount + 1个环,每个环都有sliceCount唯一的顶点。连续环之间的半径差值为Δr=(topRadius - bottomRadius)/stackCount。如果我们从索引为0的底环开始,那么第i个环的半径为ri=bottomRadius+iΔr,第i个环的高度为hi=−h2+iΔh,其中Δh是堆叠高度,h是圆柱体高度。 所以基本思想是迭代每个环并生成位于该环上的顶点。 这给出了以下实现(我们用粗体显示了相关的代码):
void GeometryGenerator::CreateCylinder(float bottomRadius, float topRadius, float height, UINT sliceCount, UINT
stackCount, MeshData& meshData)
{
meshData.Vertices.clear();
meshData.Indices.clear();
//
// Build Stacks.
//
float stackHeight = height / stackCount;
// Amount to increment radius as we move up each stack
// level from bottom to top.
float radiusStep = (topRadius - bottomRadius) / stackCount;
UINT ringCount = stackCount+1;
// Compute vertices for each stack ring starting at
// the bottom and moving up.
for(UINT i = 0; i < ringCount; ++i)
{
float y = -0.5f*height + i*stackHeight;
float r = bottomRadius + i*radiusStep;
// vertices of ring
float dTheta = 2.0f*XM_PI/sliceCount;
for(UINT j = 0; j <= sliceCount; ++j)
{
Vertex vertex;
float c = cosf(j*dTheta);
float s = sinf(j*dTheta);
vertex.Position = XMFLOAT3(r*c, y, r*s);
vertex.TexC.x = (float)j/sliceCount;
vertex.TexC.y = 1.0f - (float)i/stackCount;
// Cylinder can be parameterized as follows, where we
// introduce v parameter that goes in the same direction
// as the v tex-coord so that the bitangent goes in the
// same direction as the v tex-coord.
// Let r0 be the bottom radius and let r1 be the
// top radius.
// y(v) = h - hv for v in [0,1].
// r(v) = r1 + (r0-r1)v
//
// x(t, v) = r(v)*cos(t)
// y(t, v) = h - hv
// z(t, v) = r(v)*sin(t)
//
// dx/dt = -r(v)*sin(t)
// dy/dt = 0
// dz/dt = +r(v)*cos(t)
//
// dx/dv = (r0-r1)*cos(t)
// dy/dv = -h
// dz/dv = (r0-r1)*sin(t)
// TangentU us unit length.
vertex.TangentU = XMFLOAT3(-s, 0.0f, c);
float dr = bottomRadius-topRadius;
XMFLOAT3 bitangent(dr*c, -height, dr*s);
XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
XMVECTOR B = XMLoadFloat3(&bitangent);
XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B));
XMStoreFloat3(&vertex.Normal, N);
meshData.Vertices.push_back(vertex);
}
}
Note:观察每个环的第一个和最后一个顶点在位置上是重复的,但纹理坐标不重复。 我们必须这样做,以便我们可以正确地将纹理应用于圆柱体。
实际的方法GeometryGenerator::CreateCylinder会创建额外的顶点数据,例如法线向量和纹理坐标,这些将在未来的演示中很有用。 现在不要担心这些数量。
从图6.14可以看出,每个堆栈中的每个切片都有一个四边形(两个三角形)。 图6.14显示了第i个栈和第j个分区的索引由下式给出:
// Add one because we duplicate the first and last vertex per ring
// since the texture coordinates are different.
UINT ringVertexCount = sliceCount+1;
// Compute indices for each stack.
for(UINT i = 0; i < stackCount; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j+1);
}
}
BuildCylinderTopCap(bottomRadius, topRadius,
height, sliceCount, stackCount, meshData);
BuildCylinderBottomCap(bottomRadius, topRadius,
height, sliceCount, stackCount, meshData);
}
生成帽几何形状相当于生成顶部和底部环的切片三角形以近似一个圆:
void GeometryGenerator::BuildCylinderTopCap(float bottomRadius,
float topRadius, float height, UINT sliceCount,
UINT stackCount, MeshData& meshData)
{
UINT baseIndex = (UINT)meshData.Vertices.size();
float y = 0.5f*height;
float dTheta = 2.0f*XM_PI/sliceCount;
// Duplicate cap ring vertices because the texture coordinates
// and normals differ.
for(UINT i = 0; i <= sliceCount; ++i)
{
float x = topRadius*cosf(i*dTheta);
float z = topRadius*sinf(i*dTheta);
// Scale down by the height to try and make top cap
// texture coord area proportional to base.
float u = x/height + 0.5f;
float v = z/height + 0.5f;
meshData.Vertices.push_back(
Vertex(x, y, z,
0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f,
u, v));
}
// Cap center vertex.
meshData.Vertices.push_back(
Vertex(0.0f, y, 0.0f,
0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.5f, 0.5f));
// Index of center vertex.
UINT centerIndex = (UINT)meshData.Vertices.size()-1;
for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(centerIndex);
meshData.Indices.push_back(baseIndex + i+1);
meshData.Indices.push_back(baseIndex + i);
}
}
底部的上限代码是类似的。
我们通过指定半径以及切片和堆栈数来定义一个球体,如图6.15所示。用于生成球体的算法与圆柱体的算法非常相似,除了每个圆环的半径改变是基于三角函数的非线性方式。我们将把它留给读者来研究GeometryGenerator::CreateSphere代码。
从图6.15中可以看出,球体的三角形没有相同的面积。这在某些情况下可能不合需要。一个地圈用三角形近似一个球体,几何面积相等,边长相等(见图6.16)。
为了生成地圈,我们从二十面体开始,细分三角形,然后将新的顶点投影到具有给定半径的球体上。 我们可以重复这个过程来改善曲面细分。
图6.17显示了一个三角形可以细分为四个相等大小的三角形。只需沿原始三角形边缘的中点找到新的顶点。然后通过将顶点投影到单位球上然后标量乘以r:V′=rv||v||来将新顶点投影到半径为r的球体上。
void GeometryGenerator::CreateGeosphere(float radius, UINT numSubdivisions, MeshData& meshData)
{
// Put a cap on the number of subdivisions.
numSubdivisions = MathHelper::Min(numSubdivisions, 5u);
// Approximate a sphere by tessellating an icosahedron.
const float X = 0.525731f;
const float Z = 0.850651f;
XMFLOAT3 pos[12] =
{
XMFLOAT3(-X, 0.0f, Z), XMFLOAT3(X, 0.0f, Z),
XMFLOAT3(-X, 0.0f, -Z), XMFLOAT3(X, 0.0f, -Z),
XMFLOAT3(0.0f, Z, X), XMFLOAT3(0.0f, Z, -X),
XMFLOAT3(0.0f, -Z, X), XMFLOAT3(0.0f, -Z, -X),
XMFLOAT3(Z, X, 0.0f), XMFLOAT3(-Z, X, 0.0f),
XMFLOAT3(Z, -X, 0.0f), XMFLOAT3(-Z, -X, 0.0f)
};
DWORD k[60] =
{
1,4,0, 4,9,0, 4,5,9, 8,5,4, 1,8,4,
1,10,8, 10,3,8, 8,3,5, 3,2,5, 3,7,2,
3,10,7, 10,6,7, 6,11,7, 6,0,11, 6,1,0,
10,1,6, 11,0,9, 2,11,9, 5,2,9, 11,2,7
};
meshData.Vertices.resize(12);
meshData.Indices.resize(60);
for(size_t i = 0; i < 12; ++i)
meshData.Vertices[i].Position = pos[i];
for(size_t i = 0; i < 60; ++i)
meshData.Indices[i] = k[i];
for(size_t i = 0; i < numSubdivisions; ++i)
Subdivide(meshData);
// Project vertices onto sphere and scale.
for(size_t i = 0; i < meshData.Vertices.size(); ++i)
{
// Project onto unit sphere.
XMVECTOR n = XMVector3Normalize(XMLoadFloat3(
&meshData.Vertices[i].Position));
// Project onto sphere.
XMVECTOR p = radius*n;
XMStoreFloat3(&meshData.Vertices[i].Position, p);
XMStoreFloat3(&meshData.Vertices[i].Normal, n);
// Derive texture coordinates from spherical coordinates.
float theta = MathHelper::AngleFromXY(
meshData.Vertices[i].Position.x,
meshData.Vertices[i].Position.z);
float phi = acosf(meshData.Vertices[i].Position.y / radius);
meshData.Vertices[i].TexC.x = theta/XM_2PI;
meshData.Vertices[i].TexC.y = phi/XM_PI;
// Partial derivative of P with respect to theta
meshData.Vertices[i].TangentU.x = -radius*sinf(phi)*sinf(theta);
meshData.Vertices[i].TangentU.y = 0.0f;
meshData.Vertices[i].TangentU.z = +radius*sinf(phi)*cosf(theta);
XMVECTOR T = XMLoadFloat3(&meshData.Vertices[i].TangentU);
XMStoreFloat3(&meshData.Vertices[i].TangentU,
XMVector3Normalize(T));
}
}
在本节中,我们将回顾“形状”演示和前两个演示之间的主要区别。 忽略我们绘制不同的几何图形,主要区别是我们绘制多个对象。 每个物体都有一个世界矩阵,用于描述物体相对于世界空间的局部空间,它指定了物体在世界中的位置。 请注意,即使我们在本演示中绘制了多个球体和圆柱体,我们只需要一个球体和圆柱体几何体的副本。 我们简单地重绘同一个球体和圆柱体网格多次,但使用不同的世界矩阵; 回想一下这就是所谓的实例。 这些矩阵是在初始化时创建的,如下所示:
// Define transformations from local spaces to world space.
XMFLOAT4X4 mSphereWorld[10];
XMFLOAT4X4 mCylWorld[10];
XMFLOAT4X4 mBoxWorld;
XMFLOAT4X4 mGridWorld;
XMFLOAT4X4 mCenterSphere;
XMMATRIX I = XMMatrixIdentity();
XMStoreFloat4x4(&mGridWorld, I);
XMMATRIX boxScale = XMMatrixScaling(2.0f, 1.0f, 2.0f);
XMMATRIX boxOffset = XMMatrixTranslation(0.0f, 0.5f, 0.0f);
XMStoreFloat4x4(&mBoxWorld, XMMatrixMultiply(boxScale, boxOffset));
XMMATRIX centerSphereScale = XMMatrixScaling(2.0f, 2.0f, 2.0f);
XMMATRIX centerSphereOffset = XMMatrixTranslation(0.0f, 2.0f, 0.0f);
XMStoreFloat4x4(&mCenterSphere, XMMatrixMultiply(centerSphereScale, centerSphereOffset));
// We create 5 rows of 2 cylinders and spheres per row.
for(int i = 0; i < 5; ++i)
{
XMStoreFloat4x4(&mCylWorld[i*2+0],
XMMatrixTranslation(-5.0f, 1.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mCylWorld[i*2+1],
XMMatrixTranslation(+5.0f, 1.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mSphereWorld[i*2+0],
XMMatrixTranslation(-5.0f, 3.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mSphereWorld[i*2+1],
XMMatrixTranslation(+5.0f, 3.5f, -10.0f + i*5.0f));
}
我们将所有网格顶点和索引打包到一个顶点和索引缓冲区中。 这是通过连接顶点和索引数组完成的。 这意味着当我们绘制一个对象时,我们只绘制了顶点和索引缓冲区的一个子集。 我们需要知道三个量才能绘制几何图形的一个子集(回忆图6.3)。 我们需要知道连接索引缓冲区中每个对象的起始索引,并且我们需要知道每个对象的索引计数。 我们需要的第三件事是连接顶点缓冲区中每个对象的第一个顶点的索引偏移量。 这是因为当我们连接顶点数组时,我们没有抵消索引来补偿(回想第5章练习2); 然而,如果我们将索引偏移到每个对象的第一个顶点,我们可以将它传递给ID3D11DeviceContext :: DrawIndexed的第三个参数,并且该偏移量将被添加到该绘制调用中的所有索引以为我们执行偏移。
下面的代码显示了如何创建几何缓冲区,如何缓存必要的绘图数量以及如何绘制对象。
void ShapesApp::BuildGeometryBuffers()
{
GeometryGenerator::MeshData box;
GeometryGenerator::MeshData grid;
GeometryGenerator::MeshData sphere;
GeometryGenerator::MeshData cylinder;
GeometryGenerator geoGen;
geoGen.CreateBox(1.0f, 1.0f, 1.0f, box);
geoGen.CreateGrid(20.0f, 30.0f, 60, 40, grid);
geoGen.CreateSphere(0.5f, 20, 20, sphere);
geoGen.CreateCylinder(0.5f, 0.3f, 3.0f, 20, 20, cylinder);
// Cache the vertex offsets to each object in the concatenated
// vertex buffer.
mBoxVertexOffset = 0;
mGridVertexOffset = box.Vertices.size();
mSphereVertexOffset = mGridVertexOffset + grid.Vertices.size();
mCylinderVertexOffset = mSphereVertexOffset + sphere.Vertices.size();
// Cache the index count of each object.
mBoxIndexCount = box.Indices.size();
mGridIndexCount = grid.Indices.size();
mSphereIndexCount = sphere.Indices.size();
mCylinderIndexCount = cylinder.Indices.size();
// Cache the starting index for each object in the concatenated
// index buffer.
mBoxIndexOffset = 0;
mGridIndexOffset = mBoxIndexCount;
mSphereIndexOffset = mGridIndexOffset + mGridIndexCount;
mCylinderIndexOffset = mSphereIndexOffset + mSphereIndexCount;
UINT totalVertexCount =
box.Vertices.size() +
grid.Vertices.size() +
sphere.Vertices.size() +
cylinder.Vertices.size();
UINT totalIndexCount =
mBoxIndexCount +
mGridIndexCount +
mSphereIndexCount +
mCylinderIndexCount;
//
// Extract the vertex elements we are interested in and pack the
// vertices of all the meshes into one vertex buffer.
//
std::vector<Vertex> vertices(totalVertexCount);
XMFLOAT4 black(0.0f, 0.0f, 0.0f, 1.0f);
UINT k = 0;
for(size_t i = 0; i < box.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = box.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < grid.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = grid.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < sphere.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = sphere.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < cylinder.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = cylinder.Vertices[i].Position;
vertices[k].Color = black;
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * totalVertexCount;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &vertices[0];
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mVB));
//
// Pack the indices of all the meshes into one index buffer.
//
std::vector<UINT> indices;
indices.insert(indices.end(), box.Indices.begin(), box.Indices.end());
indices.insert(indices.end(), grid.Indices.begin(), grid.Indices.end());
indices.insert(indices.end(), sphere.Indices.begin(),
sphere.Indices.end());
indices.insert(indices.end(), cylinder.Indices.begin(),
cylinder.Indices.end());
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * totalIndexCount;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &indices[0];
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));
}
void ShapesApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Blue));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
md3dImmediateContext->IASetInputLayout(mInputLayout);
md3dImmediateContext->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
md3dImmediateContext->RSSetState(mWireframeRS);
UINT stride = sizeof(Vertex);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB, &stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mIB, DXGI_FORMAT_R32_UINT, 0);
// Set constants
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX viewProj = view*proj;
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
// Draw the grid.
XMMATRIX world = XMLoadFloat4x4(&mGridWorld);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mGridIndexCount, mGridIndexOffset, mGridVertexOffset);
// Draw the box.
world = XMLoadFloat4x4(&mBoxWorld);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mBoxIndexCount, mBoxIndexOffset, mBoxVertexOffset);
// Draw center sphere.
world = XMLoadFloat4x4(&mCenterSphere);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mSphereIndexCount, mSphereIndexOffset, mSphereVertexOffset);
// Draw the cylinders.
for(int i = 0; i < 10; ++i)
{
world = XMLoadFloat4x4(&mCylWorld[i]);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mCylinderIndexCount,
mCylinderIndexOffset, mCylinderVertexOffset);
}
// Draw the spheres.
for(int i = 0; i < 10; ++i)
{
world = XMLoadFloat4x4(&mSphereWorld[i]);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSphereIndexCount,
mSphereIndexOffset, mSphereVertexOffset);
}
}
HR(mSwapChain->Present(0, 0));
}
虽然箱子,网格,球体和圆柱体可以满足本书中的一些演示,但是一些演示将通过渲染更复杂的几何体而受益。 稍后我们将介绍如何从流行的3D建模格式加载3D网格。 与此同时,我们已经将头骨网格的几何图形(图6.18)导出为一个简单的顶点列表(仅限位置和法向量)和索引。 我们可以简单地使用标准C ++文件I / O从文件中读取顶点和索引,并将它们复制到我们的顶点和索引缓冲区中。 文件格式是一个非常简单的文本文件:
VertexCount: 31076
TriangleCount: 60339
VertexList (pos, normal)
{
0.592978 1.92413 -2.62486 0.572276 0.816877 0.0721907
0.571224 1.94331 -2.66948 0.572276 0.816877 0.0721907
0.609047 1.90942 -2.58578 0.572276 0.816877 0.0721907
…
}
TriangleList
{
0 1 2
3 4 5
6 7 8
…
}
到目前为止,我们已经使用了在初始化时固定的静态缓冲区。相反,动态缓冲区的内容通常是每帧更改。当我们需要动画时,通常会使用动态缓冲区。例如,假设我们正在进行波浪模拟,并且我们求解解函数f(x,z,t)的波动方程。该函数表示时间t时xz平面中每个点处的波高。如果我们要使用这个函数来绘制波浪,我们可以像使用峰和谷一样使用三角形网格网格,并将f(x,z,t)应用到每个网格点以获得波峰高度网格点。因为这个函数也依赖于时间t(即波面随时间变化),所以我们需要在很短的时间内(例如每1/30秒)将这个函数重新应用到网格点上以获得平滑的动画。因此,我们需要一个动态顶点缓冲区,以便随着时间的推移更新三角形网格顶点的高度。导致动态顶点缓冲区的另一种情况是具有复杂物理和碰撞检测的粒子系统。在每一帧中,我们将在CPU上进行物理和碰撞检测以找到粒子的新位置。由于粒子位置正在改变每一帧,我们需要一个动态顶点缓冲区来更新用于绘制每帧的粒子位置。
回想一下,为了使缓冲区动态化,我们必须指定使用D3D11_USAGE_DYNAMIC; 另外,因为我们将写入缓冲区,所以我们需要CPU访问标志D3D11_CPU_ACCESS_WRITE:
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_DYNAMIC;
vbd.ByteWidth = sizeof(Vertex) * mWaves.VertexCount();
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
vbd.MiscFlags = 0;
HR(md3dDevice->CreateBuffer(&vbd, 0, &mWavesVB));
然后我们可以使用ID3D11DeviceContext::Map函数来获取指向缓冲区内存块开始的指针并写入它:
HRESULT ID3D11DeviceContext::Map(
ID3D11Resource *pResource,
UINT Subresource,
D3D11_MAP MapType,
UINT MapFlags,
D3D11_MAPPED_SUBRESOURCE *pMappedResource);
1.pResource:指向我们想要读取/写入的资源的指针。 缓冲区是Direct3D 11资源的一种类型,但其他资源也可以使用此方法访问,例如纹理资源。
2.Subresource:资源中包含的子资源的索引。 我们将会看到后面如何使用它; 我们的缓冲区没有子资源,所以指定0。
3.MapType:通用标志是以下之一:
D3D11_MAP_WRITE_DISCARD:指示硬件放弃缓冲区并返回一个指向新分配的缓冲区的指针; 当我们写入新分配的缓冲区时,通过允许硬件从丢弃的缓冲区继续渲染,可以防止硬件停顿。
D3D11_MAP_WRITE_NO_OVERWRITE:告诉我们只打算写入缓冲区未初始化部分的硬件; 这也可以防止硬件停滞,因为它允许它在我们写入缓冲区的未初始化部分的同时继续渲染先前写入的几何图形。
D3D11_MAP_READ:与分段缓冲区一起使用,您需要将GPU缓冲区的副本读入系统内存。
4.MapFlags:我们不使用的可选标志,因此指定为0; 有关详细信息,请参阅SDK文档。
5.pMappedResource:返回一个指向D3D11_MAPPED_SUBRESOURCE的指针,从中我们可以访问资源数据进行读写。
D3D11_MAPPED_SUBRESOURCE结构定义如下:
typedef struct D3D11_MAPPED_SUBRESOURCE {
void *pData;
UINT RowPitch;
UINT DepthPitch;
} D3D11_MAPPED_SUBRESOURCE;
1.pData:指向读/写资源的原始内存的指针。 您必须将其转换为资源存储的适当数据格式。
2.RowPitch:资源中一行数据的字节大小。 例如,对于2D纹理,这是一行的字节大小。
3.DepthPitch:资源中一页数据的字节大小。 例如,对于3D纹理,这是3D纹理的一个2D图像子集的字节大小。
RowPitch和DepthPitch之间的区别在于2D和3D资源(可以考虑2D或3D阵列)。 对于基本上是一维数据数组的顶点/索引缓冲区,RowPitch和DepthPitch被赋予相同的值,并且它等于顶点/索引缓冲区的字节大小。
以下代码显示了我们如何更新“Waves”演示中的顶点缓冲区:
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(md3dImmediateContext->Map(mWavesVB, 0,
D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
Vertex* v = reinterpret_cast<Vertex*>(mappedData.pData);
for(UINT i = 0; i < mWaves.VertexCount(); ++i)
{
v[i].Pos = mWaves[i];
v[i].Color = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
}
md3dImmediateContext->Unmap(mWavesVB, 0);
完成更新缓冲区后,必须调用ID3D11DeviceContext :: Unmap函数。
使用动态缓冲区时会有一些开销,因为新数据必须从CPU内存传输回GPU内存。 因此,静态缓冲区应优先于动态缓冲区,只要提供静态缓冲区即可。 最新版本的Direct3D引入了新功能,以减少对动态缓冲区的需求。 例如:
1.简单的动画可以在顶点着色器中完成。
2.通过渲染纹理或计算着色器,以及顶点纹理获取功能,可以实现波浪模拟
就像之前描述的完全在GPU上运行的那样。
3.几何着色器提供了GPU创建或破坏原语的能力,这是通常需要在没有几何着色器的情况下在CPU上完成的任务。
索引缓冲区也可以是动态的。但是,在“Waves”演示中,三角形拓扑保持不变,只有顶点高度发生变化; 因此,只有顶点缓冲区需要是动态的。
本章的“Waves”演示使用一个动态顶点缓冲区来实现一个简单的波浪模拟,就像本节开头所描述的那样。对于本书,我们并不关心波浪仿真的实际算法细节(参见[Lengyel02]),但更多的是用于说明动态缓冲区的过程:更新CPU上的仿真,然后相应地更新顶点缓冲区 使用Map / Unmap。
Note:在“Waves”演示中,我们以线框模式渲染波形; 这是因为没有照明,很难在固体填充模式下看到波浪运动。
我们再次提到,可以使用更高级的方法在GPU上实现此演示,例如渲染纹理功能或计算着色器以及顶点纹理获取。由于我们尚未涉及这些主题,因此我们在CPU上执行波浪模拟并使用动态顶点缓冲区更新新顶点。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。