@(读书笔记)[读书笔记, 技术交流]
前一章我们着重讲了渲染管线的概念和一
些数学技巧。本章叙述配置渲染管线的Direct3D API和方法,如何定义顶点着色器(vertex shader)和像素着色器(pixel shader),如何将几何图形提交给渲染管线去绘制。最后你将学会如何绘制各式各种的几何图形。
本章要点:
1. 介绍如何使用Direct3D的接口定义,存储和绘制几何图形数据。
2. 学习如何写一个基础的顶点着色器和像素着色器。
3. 学习如何配置渲染管线的各阶段。
4. 学习着色器和渲染的阶段如何使用特效框架完成一些渲染技术,如何使用特效框架产生着色器。
如 $5.5.1 中所说,顶点数据除了包含空间坐标外还可以包含其他的附加信息(如顶点的颜色等)。为了创建一个通用的顶点格式,我们需要定义一个结构体,根据具体所需的附加信息我们举两个例子:
struct Vertex1
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
};
接下来我们需要把我们定义的结构体传给Direct3D。通过输入布局(input layout)ID3D11InputLayout来完成传输。输入布局是一个一维数组,数据类型是D3D11_INPUT_ELEMENT_DESC。D3D11_INPUT_ELEMENT_DESC数据类型是一个顶点结构体(vertex structure)。因此若顶点结构体有两种,那么相应的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;
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
struct Vertex2
{
XMFLOAT3 Pos; // 0-byte offset
XMFLOAT3 Normal; // 12-byte offset
XMFLOAT2 Tex0; // 24-byte offset
XMFLOAT2 Tex1; // 32-byte offset
};
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方法获得输入布局的指针ID3D11InputLayout。
HRESULT ID3D11Device::CreateInputLayout(
const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs,
UINT NumElements,
const void *pShaderBytecodeWithInputSignature,
SIZE_T BytecodeLength,
ID3D11InputLayout **ppInputLayout);
解释一下上述的参数:
- pInputElementDescs:用以表示顶点结构体的D3D11_INPUT_ELEMENT_DESC数组。
- NumElements:数组长度。
- pShaderBytecodeWithInputSignature:顶点着色器的输入信号的字节代码的指针。
- BytecodeLength:顶点着色器的信号数据的长度。
- ppInputLayout:函数返回值,返回被创建出来的输入布局的指针。
解释一下上述提到的几个名词。顶点着色器需要一组顶点数据作为输入参数,把这种输入参数称之为输入信号(input signature)。自定义顶点结构体中的元素需要被映射到顶点着色器的相应输入位置。在Direct3D创建输入布局(input layout)时会检查顶点结构体到顶点着色器的映射是否正确。若使用相同的输入信号可以复用输入布局。
以下展示一个输入信号和顶点结构体:
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允许在输入寄存器(input registers)中的bits可以被重新解释。然而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能存取顶点数据,顶点数据需要放置在一些特定的资源中,称之为缓冲(buffer),对应的接口是ID3D11Buffer。
一个存储顶点的buffer被称作顶点buffer。Direct3D的buffers不止可以存储数据,还能描述如何存取数据,如何将数据绑定到渲染管线。依照以下步骤创建一个顶点buffer:
1. 填写结构体D3D11_BUFFER_DESC。
2. 填写结构体D3D11_SUBRESOURCE_DATA,用来指定buffer内容中的数据类型。
3. 调用ID3D11Device::CreateBuffer创建buffer。
D3D11_BUFFER_DESC数据结构定义如下:
typedef struct D3D11_BUFFER_DESC {
UINT ByteWidth;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
UINT StructureByteStride;
} D3D11_BUFFER_DESC;
Usage:枚举型D3D11_USAGE是指定buffer的访问方式。共有4种枚举型:
Ⅰ. D3D11_USAGE_DEFAULT:GPU可以读写资源。CPU在使用ID3D11DeviceContext::Map接口时不能读写资源。不过若使用ID3D11DeviceContext::UpdateSubresource接口则可以读写资源。
Ⅱ. D3D11_USAGE_IMMUTABLE:GPU只读资源,CPU除了初始化资源外也不能修改资源,同时无法读取资源。我们也不能映射或更新资源。
Ⅲ. D3D11_USAGE_DYNAMIC:若CPU需要频繁更新资源则可以使用此数据类型,资源可以被CPU修改也可以被GPU读取。CPU通过ID3D11DeviceContext::Map映射修改资源。此方式消耗较大因此尽量避免使用。
Ⅳ. D3D11_USAGE_STAGING:此方式可以让CPU读取资源副本(当然此资源要能支持从图像存储内存(video memory)复制到系统存储内存(system memory))。这种复制操作比较缓慢,应尽量避免。复制操作的函数是ID3D11DeviceContext::CopyResource和ID3D11DeviceContext::CopySubresourceRegio。$12.3.5是CopyResource的一个例子。
BindFlags:对于顶点buffer,这里填D3D11_BIND_VERTEX_BUFFER。
D3D11_SUBRESOURCE_DATA结构体定义如下:
typedef struct D3D11_SUBRESOURCE_DATA {
const void *pSysMem;
UINT SysMemPitch;
UINT SysMemSlicePitch;
} D3D11_SUBRESOURCE_DATA;
// 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;
};
创建完顶点buffer后,还需将其绑定到设备的输入插槽,之后就能将顶点传给渲染管线。通过以下方法完成:
void ID3D11DeviceContext::IASetVertexBuffers(
UINT StartSlot,
UINT NumBuffers,
ID3D11Buffer *const *ppVertexBuffers,
const UINT *pStrides,
const UINT *pOffsets);
IASetVertexBuffers方法是用来设置多个顶点buffer放在多个输入插槽。不过多数时候我们只使用一个输入插槽。本章结尾会给出使用两个输入插槽的练习题。
一个顶点buffer将一直绑定在同一个输入插槽(除非你修改此buffer的绑定插槽),若你希望同时使用多个顶点buffer,可以如下操作:
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... */
我们设置顶点buffer并绑定到输入插槽,但不会绘制这些顶点,只是能够让渲染管线读取到这些顶点,最终渲染需要调用ID3D11DeviceContext::Draw方法完成:
void ID3D11DeviceContext::Draw(UINT VertexCount, UINT StartVertexLocation);
上述代码中两个参数含义如图: