在本节中,我们将学习在屏幕上绘制三角形。我们将通过创建一系列顶点并让硬件在屏幕上绘制这些顶点来构建这个三角形。这需要很多代码。渲染三角形需要执行许多操作,本节很长,将分为以下几部分:1、首先,我们告诉GPU如何渲染我们的几何;2、第二,我们创建三角形的三个顶点;3,第三,我们将这些顶点存储在显存中;4、第四,我们告诉GPU如何读取这些顶点;5、第五,最终我们渲染三角形。
让我们从第一步开始讨论,告诉GPU如何渲染三角形。渲染过程是由渲染管道控制的,如果你还记得第一节的内容,你就知道渲染管道有一系列步骤,可生成一个渲染图像。不幸的是,管道无法自动知道该怎么做,必须首先对其进行编程,而且必须通过shaders(着色器)
进行编程。shader
是一个误导性的术语,因为shader
并不提供颜色。shader
实际上是控制管道中的某个步骤的微型程序。
shader
有几种不同的类型,每种shader
在渲染过程中都会运行很多次。例如,顶点shader
对渲染每个顶点都运行一次,而像素shader
对每个绘制的像素都运行一次。现在,我们准备渲染一个三角形,为此必须将某些shader
加载到GPU。加载shader
需要几个步骤:1、从.shader文件
加载并编译两个shader
;2、将这两个shader
封装到shader对象
中;3、将这两个shader
都设置为活动shader
。
在这一步中,我们将编译两个shader
。它是分别是顶点shader
和像素shader
,这是渲染所需的两个shader
。要加载和编译shader
,我们必须使用一个称为D3DX11CompileFromFile()
的函数,这个函数有大量的参数,但其中大部分都是高级参数,可以先设置为0。
HRESULT D3DX11CompileFromFile(
LPCTSTR pSrcFile, // .shader文件的文件名
D3D10_SHADER_MACRO *pDefines, // advanced
LPD3D10INCLUDE pInclude, // advanced
LPCSTR pFunctionName, // shader启动函数的名称
LPCSTR pProfile, // shader的配置文件
UINT Flags1, // advanced
UINT Flags2, // advanced
ID3DX11ThreadPump *pPump, // advanced
ID3D10Blob **ppShader, // 已编译shader的Blob
ID3D10Blob **ppErrorMsgs, // advanced
HRESULT *pHResult); // advanced
参数 | 描述 |
---|---|
LPCSTSTR pSrcFile |
第一个参数是包含未编译的shader 代码的文件的名称,对我们来说,这个文件是L“shaders.shader” 。尽管你可以使用任何喜欢的扩展名,但是shader 通常以扩展名.shader 存储在文件中。 |
LPCSTR pFunctionName |
这个参数其实就是shader 的名称,在.shader 的代码中,每个shader 将从一个特定的函数开始,并且这个函数名就被认为是shader的名称。在本节中,shader的名称为VShader 和PShader 。 |
LPCSTR pProfile |
shader配置文件 就是一个标志,用于告诉编译器我们正在编译的shader类型以及要编译成的版本。这个标志的格式如下:"vs_4_0" ,其中"v" 代表顶点,“s” 代表shader,"_4_0" 代表HLSL4.0 版。你可以使用像素shader 的"p" 替换"v" 。第一次调用此函数时,将使用”vs_4_0” ,第二次使用”ps_4_0” 。 |
ID3D10Blob **ppShader |
这个参数是指向一个blob对象 的指针,这个blob对象 将会用shader的编译代码填充。blob对象 是一个精巧的COM对象,用于存储数据缓冲区。我们可以使用GetBufferPointer() 和GetBufferSize() 函数来访问它的内容。 |
下面是真实调用该函数的代码,实际上很简单。
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
那么这个函数到底是做什么呢?对于顶点shader
,它将加载"shader.shader"
的内容,找到函数"VShader"
,并将其编译为4.0版本的顶点shader
,然后将编译后的结果存储在blob VS
中。这段代码将不断增长,因此让我们将其放入一个名为InitPipeline()
的新函数中。
void InitPipeline()
{
// 加载并编译这两个shader
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
}
每个shader都存储在其自己的COM对象中,这些COM对象被称为ID3D11_____Shader
:
// global
ID3D11VertexShader *pVS; // 顶点shader对象
ID3D11PixelShader *pPS; // 像素shader对象
一旦有了这两个指针,就可以使用dev-> Create _____ Shader()
来创建对应的COM对象:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// 将这两个shader都封装在shader对象中
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
}
这个函数有四个参数,其中三个都显而易见,第一个参数是已编译数据的地址,第二个参数是文件数据的大小,第四个参数是shader对象
的地址,第三个参数是高级参数,后面会进行介绍。
这一步很简单:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// 设置shader对象
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
}
函数的第一个参数是要设置的shader对象
的地址,其他两个是高级参数,后面将进行介绍。
请记住,pVS
和pPS
都是COM对象
,因此必须将它们释放。
void CleanD3D(void)
{
swapchain->SetFullscreenState(FALSE, NULL); // switch to windowed mode
// close and release all existing COM objects
pVS->Release();
pPS->Release();
swapchain->Release();
backbuffer->Release();
dev->Release();
devcon->Release();
}
总而言之,这一部分是为渲染准备了GPU,我们已经给出了将顶点转换为渲染图像所需的所有代码,下面我们需要的是一些顶点。
在之前的课程中,我们曾给出顶点的定义:3D空间中一个准确点的位置
和属性
。顶点的位置是表示顶点坐标的三个数值,顶点的属性也使用数值定义。Direct3D使用一种被称为input layout(输入布局)
的数据格式来表示顶点的位置和属性的数据布局,你可以根据需要进行修改和设置。让我们开看一下它是如何工作的。
顶点是由结构体组成,创建任何一种3D图像
(包括顶点)都需要一个结构体来提供相关的数据。为了显示图像,我们要将所有的信息复制到GPU,然后命令Direct3D
将这些数据渲染到后台缓冲区
。如果我们在传送一个顶点时,发送这个顶点的所有数据,就会发生如下图所示的情形:
上面这种方式存在问题,我们实际上只需要其中两个信息块,通过执行下面这种操作,我们可以更快地将其发送到GPU:
这就是我们使用输入布局
时发生的情况,我们选择要使用的信息,然后发送该信息,这样我们就能够在每个帧之间发送更多的顶点。
下面,让我们从最简单的制作一个顶点开始。顶点通常使用结构体创建,你可以在结构体中以任何格式放置有关这个结构的任何数据。例如,如果我们希望每个顶点都包含一个位置和一个颜色,则可以构建以下格式的结构:
struct VERTEX
{
FLOAT X, Y, Z; // 位置
D3DXCOLOR Color; // 颜色
};
如你所见,我们用三个浮点数代表位置,一个成员代表颜色。现在,让我们用这个结构体创建一个实际的顶点。我们可以这样做:
VERTEX OurVertex = {
0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)};
当然,我们也可以创建一个顶点数组:
VERTEX OurVertices[] =
{
{
0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
{
0.45f, -0.5, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
{
-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)}
};
这将产生一个三角形,我们将很快在屏幕上看到它。
在C++中创建结构时,数据存储在系统内存中,但是我们需要将其存储在显存中,但我们又无法直接访问显存。为了允许我们访问显存,Direct3D为我们提供了一个特定的COM对象
,该对象将在系统内存和显存中各自维护一个缓冲区。如何在系统内存和显存中都维护一个缓冲区呢?最开始,数据将存储在系统内存的缓存区中,当需要渲染时,Direct3D自动将其赋值到显存的缓存区中,如果显存不足,Direct3D将删除一段时间都未使用的缓冲区或被视为“低优先级”的缓冲区,为新的资源腾出空间。
为什么需要Direct3D来帮访问显存呢?因为这是非常困难的操作,根据显卡和操作系统版本的不同,访问显存的方式也不同,让Direct3D来管理这个操作将非常方便。这个COM对象称为ID3D11Buffer
,要创建它,需要使用CreateBuffer()
函数,代码如下所示:
ID3D11Buffer *pVBuffer; // global
D3D11_BUFFER_DESC bd;
ZeroMemory(&bd, sizeof(bd));
bd.Usage = D3D11_USAGE_DYNAMIC; // CPU和GPU的写访问权限
bd.ByteWidth = sizeof(VERTEX) * 3; // size是VERTEX结构的大小* 3
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 用作顶点缓冲区
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 允许CPU写入缓冲区
dev->CreateBuffer(&bd, NULL, &pVBuffer); // 创建缓冲区
代码 | 描述 |
---|---|
D3D11_BUFFER_DESC bd; |
这是一个包含缓冲区属性的结构体 |
ZeroMemory(&bd, sizeof(bd)); |
清零初始化 |
bd.Usage |
缓冲区应该设置得尽可能高效,为了正确执行此操作,Direct3D需要知道我们打算如何访问它,在本例中,我们使用动态用法 ,即GPU只可写,CPU只可读 |
bd.ByteWidth |
该值包含将要创建的缓冲区的大小,这与我们要放入其中的顶点数组的大小相同 |
bd.BindFlags |
该值告诉Direct3D创建哪种缓冲区,我们可以创建几种类型的缓冲区,在这里我们需要创建的是顶点缓冲区 ,为此,我们将使用D3D11_BIND_VERTEX_BUFFER 标志 |
bd.CPUAccessFlags |
该值用于告诉Direct3D我们访问CPU的方式,这是对Usage 的补充说明,它将和Usage 的标志结合使用,在这里我们将使用D3D11_CPU_ACCESS_WRITE ,因为我们要将数据从系统内存复制到缓冲区中。 |
dev->CreateBuffer(&bd, NULL, &pVBuffer); |
创建缓冲区的函数,第一个参数是描述缓冲区的结构体的地址,第二个参数可用于在创建时使用某些数据来初始化缓冲区,但此处我们将其设置为NULL,第三个参数是缓冲区对象的地址,在这里,pVBuffer表示指向顶点缓冲区的指针。 |
我们现在有了三角形的三个顶点,并且有一个顶点缓冲区可以放置它们,现在我们要做的就是将顶点复制到缓冲区。但是,由于Direct3D可能在后台使用缓冲区,因此它并没有给COU直接访问缓冲区的权利。为了访问缓冲区,必须将缓冲区进行映射,这意味着在映射期间,Direct3D允许对缓冲区执行任何操作,直到取消映射之后,再阻止GPU使用缓冲区。因此,为了填充顶点缓冲区,我们需要采取以下步骤:1、映射顶点缓冲区(并由此获得缓冲区的位置);2、将数据复制到缓冲区(使用mencpy()函数);3、取消映射缓冲区。
D3D11_MAPPED_SUBRESOURCE ms;
devcon->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // 映射缓冲区
memcpy(ms.pData, OurVertices, sizeof(OurVertices)); // 复制数据
devcon->Unmap(pVBuffer, NULL); // 取消映射缓冲区
代码 | 描述 |
---|---|
D3D11_MAPPED_SUBRESOURCE ms; |
这是一个结构体,一旦我们映射了它,就会填充有关缓冲区的信息,该信息将包含指向缓冲区位置的指针,我们可以使用"ms.pData"访问该指针 |
devcon->Map() |
这一行的作用是映射缓冲区,使我们可以访问它。Map()函数的蚕丝非常简单,第一个参数是缓冲区对象的地址,我们的缓冲区指针为pVBuffer;第二个是高级参数,稍后进行讨论,目前可以设为NULL;第三个参数是一组标志,使我们可以控制CPU在映射缓冲区期间对缓冲区的访问,在这里我们使用D3D11_MAP_WRITE_DISCARD 标志,表示缓冲区中新写入的内容将覆盖旧的内容;第四个参数是另一个标志,在这里我们设置为NULL或D3D11_MAP_FLAG_DO_NOT_WAIT (表示即使GPU仍在使用缓冲区,此标志也会强制程序继续运行);最后一个参数是D3D11_MAPPED_SUBRESOURCE 结构的地址。 |
memcpy( ) |
数据拷贝,使用ms.pData 作为目标,使用OurVertices (或其他任何形式)作为源,并使用sizeof(OurVertices) 作为大小 |
devcon->Unmap() |
取消映射缓冲区,这样可以降低GPU对缓冲区的访问,并重新阻塞CPU,第一个参数是缓冲区(pVBuffer),第二个是高级参数,暂设为NULL |
这一节涵盖了很多新代码,为了使用简单,我们将其包装在一个单独的函数InitGraphics()中,整个函数如下所示:
struct VERTEX{
FLOAT X, Y, Z; D3DXCOLOR Color;}; // a struct to define a vertex
ID3D11Buffer *pVBuffer; // the vertex buffer
void InitGraphics()
{
// create a triangle using the VERTEX struct
VERTEX OurVertices[] =
{
{
0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
{
0.45f, -0.5, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
{
-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)}
};
// create the vertex buffer
D3D11_BUFFER_DESC bd;
ZeroMemory(&bd, sizeof(bd));
bd.Usage = D3D11_USAGE_DYNAMIC; // write access access by CPU and GPU
bd.ByteWidth = sizeof(VERTEX) * 3; // size is the VERTEX struct * 3
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // use as a vertex buffer
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // allow CPU to write in buffer
dev->CreateBuffer(&bd, NULL, &pVBuffer); // create the buffer
// copy the vertices into the buffer
D3D11_MAPPED_SUBRESOURCE ms;
devcon->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // map the buffer
memcpy(ms.pData, OurVertices, sizeof(OurVertices)); // copy the data
devcon->Unmap(pVBuffer, NULL); // unmap the buffer
}
这是3D编程的关键部分,我们将继续使用它并进行大量修改,因此你需要仔细阅读并确保你真的理解了它们。
本节到目前为止,我们已经知道:(A)加载并设置shader
以控制管道;(B)通过使用顶点来创建形状,并将其准备好供GPU使用。当我们将顶点放在我们自己创建的结构中时,你可能想知道GPU如何读取我的顶点?GPU如何知道我们将顶点的位置放置在颜色前面?GPU如何知道我们没有其他的意思?这一切的答案就是输入布局
。如前所述,我们能够选择在每个顶点中存储哪些信息,以提高渲染速度。输入布局
是一个包含顶点结构的布局的对象,可以让GPU适当且有效地组织数据。ID3D11InputLayout
对象存储了我们的VERTEX
结构的布局,要创建这个对象,需要调用CreateInputLayout()
函数。这里主要有两个部分:1、首先需要定义顶点的每个元素;2、其次需要创建输入布局对象。
顶点布局由一个或多个输入元素
组成,输入元素
是顶点的一种属性,例如位置和颜色。每个输入元素
由一个称为D3D11_INPUT_ELEMENT_DESC
的结构体来定义,这个结构用于描述顶点的单个属性,要创建具有多个属性的顶点,我们只需将这些结构放入数组中。
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
这个结构体共有7个值。第1个值称为语义
,语义是一个字符串,它告诉GPU该值的用途,此外还有一些其他的语义值,我们稍后会进行介绍;第2个值是语义索引
,例如如果我们的顶点具有两种颜色属性,则他们都将使用COLOR语义
,为避免混淆,我们希望每个属性在此处具有不同的编号;第3个值是数据格式
,在许多语义
中,值的数量是任意的(只要其值小于4,),重要的是格式与你在顶点中使用的格式向匹配;第4个值称为输入插槽
,这是高级成员,稍后会进行介绍,这里暂设为0;第5个值是偏移量
,表示输入元素
进入结构体中的字节数,如你所见,“POSITION”
的偏移量是0,“COLOR”
的偏移量是12,这意味着,“POSITION”
是从结构体的0字节开始,“COLOR”
是从结构体的12字节开始。实际上你也可以直接填上D3D11_APPEND_ALIGNED_ELEMEN
,它会自动帮你解决问题;第6个值表示输入元素
用作什么,这里有两个可选的标志,我们目前填充的是D3D11_INPUT_PER_VERTEX_DATA
标志,稍后会介绍另一个;第7个值并不是和D3D11_INPUT_PER_VERTEX_DATA
标志一起使用的,因此我们设为0。
这是开始绘制之前的最后一件事,我们通过调用CreateInputLayout()
函数来创建一个表示顶点格式的对象。首先看一些函数原型:
HRESULT CreateInputLayout(
D3D11_INPUT_ELEMENT_DESC *pInputElementDescs,
UINT NumElements,
void *pShaderBytecodeWithInputSignature,
SIZE_T BytecodeLength,
ID3D11InputLayout **pInputLayout);
参数 | 描述 |
---|---|
D3D11_INPUT_ELEMENT_DESC *pInputElementDescs |
第一个参数是指向输入元素 描述数组的指针,我们将&ied放在这里。 |
UINT NumElements |
这个参数是值数组中元素的数量,对我们来说是2。 |
void *pShaderBytecodeWithInputSignature |
这是指向管道中第一个shader(即顶点shader)的指针,我们在这里填充的是VS-> GetBufferPointer() 。 |
SIZE_T BytecodeLength |
这是shader文件的长度,我们在这里填充的是VS-> GetBufferSize() 。 |
下面是该函数的实际调用场景:
ID3D11InputLayout *pLayout; // global
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
dev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
要运行此函数,我们需要访问VSFile和VSSize,因此我们将此代码凡在InitPipeline()函数中:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// set the shader objects
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
// create the input layout object
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
dev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
}
创建输入布局
不会对其进行任何设置,要设置输入布局
,我们需要调用IASetInputLayout()
函数,它唯一的参数就是输入布局
对象:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// set the shader objects
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
// create the input layout object
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
dev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
devcon->IASetInputLayout(pLayout);
}
我们调用三个简单的函数来进行渲染,第一个函数设置了我们打算使用哪个顶点缓冲区,第二个函数设置了我们打算使用哪种类型的图元,例如三角形列表、线带等,第三个函数才真正开始绘制形状。
IASetVertexBuffers()
第一个函数是IASetVertexBuffers()
,它告诉GPU渲染时要读取哪些顶点,它有几个简单的参数,函数原型如下:
void IASetVertexBuffers(UINT StartSlot,
UINT NumBuffers,
ID3D11Buffer **ppVertexBuffers,
UINT *pStrides,
UINT *pOffsets);
第一个是高级参数,我们暂设为0
;第二个参数告诉GPU我们要设置多少个缓冲区,因为我们只有一个缓冲区,所以在这里放置1
;第三个参数是指向顶点缓冲区数组的指针,我们只有一个,所以可以用&pVBuffer
填充它;第四个参数是一个UNIT
数组,该数组存储了每个顶点缓冲区中单个顶点的尺寸大小,要填充此参数,我们创建一个UNIT
,并赋值为sizeof(VERTEX)
,然后将UNIT的地址放在此处;第五个参数是一个UNIT数组,告诉GPU我们应该开始渲染的那个顶点缓冲区的字节数,通常为0
,因此我们创建一个UNIT并设置为0
,最后将其地址放置在此处。
UINT stride = sizeof(VERTEX);
UINT offset = 0;
devcon->IASetVertexBuffers(0, 1, &pBuffer, &stride, &offset);
IASetPrimitiveTopology()
第二个函数告诉Direct3D使用哪种类型的图元,前面已经介绍过图元的相关信息,其对应的标志如下:
FLAG | 描述 |
---|---|
D3D11_PRIMITIVE_TOPOLOGY_POINTLIST |
显示一系列点,每个顶点一个 |
D3D11_PRIMITIVE_TOPOLOGY_LINELIST |
显示一系列分割的线 |
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP |
显示一系列连接的线 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST |
显示一系列分开的三角形 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP |
显示一系列相连的三角形 |
这个函数唯一的参数就是这些标志,它的调用方法如下:
devcon->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
Draw()
到目前为止,我们已经告诉Direct3D要渲染什么样的图元,以及要从顶点缓冲区中读取什么,下面就通过Draw()
函数开始绘制顶点缓冲区中的内容,此函数将顶点缓冲区中的图元绘制到后缓冲区,下面是函数原型:
void Draw(UINT VertexCount, // 要绘制的顶点数
UINT StartVertexLocation); // 要绘制的第一个顶点
这里两个参数控制缓冲区中的哪些顶点要进行绘制,第一个参数是绘制的顶点数;第二个参数是一个数字,该数字告诉Direct3D哪个是第一个被绘制的顶点。该函数的实际调用场景:
devcon->Draw(3, 0); // 从顶点0开始绘制3个顶点
下面来看看整个render_frame()函数:
// 这个函数用于渲染单个帧
void RenderFrame(void)
{
// 将后缓冲区清除为深蓝色
devcon->ClearRenderTargetView(backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));
// 选择要显示的顶点缓冲区
UINT stride = sizeof(VERTEX);
UINT offset = 0;
devcon->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);
// 选择我们使用的图元类型
devcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
//将顶点缓冲区绘制到后缓冲区
devcon->Draw(3, 0);
// 切换后缓冲区和前缓冲区
swapchain->Present(0, 0);
}
由于本章涉及很多代码,因此让我们从头开始快速回顾一个整个过程。
shader是一个用于指导GPU渲染的mini程序,没有shader
,就不可能进行渲染。在第一节中,我们加载了两个shader
,并将它们加载到GPU,InitPipeLine()
函数如下所示:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// set the shader objects
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
}
在本函数中,我们做了三个工作:1、加载并编译两个shader;2、为每个shader创建对象;3、读两个对象进行设置。
在本节中,我们学习了如何创建一个结构体来代表顶点,以及如何创建创建一个顶点缓冲区对象来显存中的顶点。我们构建了一个名为InitGraphics()
的函数:
// global
struct VERTEX{
FLOAT X, Y, Z; D3DXCOLOR Color;}; // a struct to define a vertex
ID3D11Buffer *pVBuffer; // the vertex buffer
void InitGraphics()
{
// create a triangle using the VERTEX struct
VERTEX OurVertices[] =
{
{
0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
{
0.45f, -0.5, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
{
-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)}
};
// create the vertex buffer
D3D11_BUFFER_DESC bd;
ZeroMemory(&bd, sizeof(bd));
bd.Usage = D3D11_USAGE_DYNAMIC; // write access access by CPU and GPU
bd.ByteWidth = sizeof(VERTEX) * 3; // size is the VERTEX struct * 3
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // use as a vertex buffer
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // allow CPU to write in buffer
dev->CreateBuffer(&bd, NULL, &pVBuffer); // create the buffer
// copy the vertices into the buffer
D3D11_MAPPED_SUBRESOURCE ms;
devcon->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // map the buffer
memcpy(ms.pData, OurVertices, sizeof(OurVertices)); // copy the data
devcon->Unmap(pVBuffer, NULL); // unmap the buffer
}
在本函数中,我们主要做了三件事:1、创建了具有位置和颜色的三个顶点;2、创建了一个顶点缓冲区对象;3、通过映射将顶点复制到顶点缓冲区,然后再取消映射。
输入布局
在本节中,我们介绍了如何通过使用输入对象来协调顶点缓冲区和shader,我们没有单独设置函数,而是将代码放置在InitPipleLine()
函数中:
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// set the shader objects
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
// create the input layout object
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
dev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
devcon->IASetInputLayout(pLayout);
}
我们做了以下三件事:1、创建位置和颜色的输入元素
描述;2、使用shader
信息来创建一个输入布局
对象;3、设置输入布局
对象。
最后,我们终于开始绘制三角形,我们向RenderFrame()函数添加了几行代码:
// this is the function used to render a single frame
void RenderFrame(void)
{
// clear the back buffer to a deep blue
devcon->ClearRenderTargetView(backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));
// select which vertex buffer to display
UINT stride = sizeof(VERTEX);
UINT offset = 0;
devcon->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);
// select which primtive type we are using
devcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// draw the vertex buffer to the back buffer
devcon->Draw(3, 0);
// switch the back buffer and the front buffer
swapchain->Present(0, 0);
}
我们做了以下三件事:1、设置要使用的顶点缓冲区;2、设置要使用的图元;3、绘制三角形。
Mian.cpp
// include the basic windows header files and the Direct3D header files
#include
#include
#include
#include
#include
// include the Direct3D Library file
#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "d3dx11.lib")
#pragma comment (lib, "d3dx10.lib")
// define the screen resolution
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
// global declarations
IDXGISwapChain *swapchain; // the pointer to the swap chain interface
ID3D11Device *dev; // the pointer to our Direct3D device interface
ID3D11DeviceContext *devcon; // the pointer to our Direct3D device context
ID3D11RenderTargetView *backbuffer; // the pointer to our back buffer
ID3D11InputLayout *pLayout; // the pointer to the input layout
ID3D11VertexShader *pVS; // the pointer to the vertex shader
ID3D11PixelShader *pPS; // the pointer to the pixel shader
ID3D11Buffer *pVBuffer; // the pointer to the vertex buffer
// a struct to define a single vertex
struct VERTEX{
FLOAT X, Y, Z; D3DXCOLOR Color;};
// function prototypes
void InitD3D(HWND hWnd); // sets up and initializes Direct3D
void RenderFrame(void); // renders a single frame
void CleanD3D(void); // closes Direct3D and releases memory
void InitGraphics(void); // creates the shape to render
void InitPipeline(void); // loads and prepares the shaders
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd;
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = L"WindowClass";
RegisterClassEx(&wc);
RECT wr = {
0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
hWnd = CreateWindowEx(NULL,
L"WindowClass",
L"Our First Direct3D Program",
WS_OVERLAPPEDWINDOW,
300,
300,
wr.right - wr.left,
wr.bottom - wr.top,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hWnd, nCmdShow);
// set up and initialize Direct3D
InitD3D(hWnd);
// enter the main loop:
MSG msg;
while(TRUE)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
if(msg.message == WM_QUIT)
break;
}
RenderFrame();
}
// clean up DirectX and COM
CleanD3D();
return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
} break;
}
return DefWindowProc (hWnd, message, wParam, lParam);
}
// this function initializes and prepares Direct3D for use
void InitD3D(HWND hWnd)
{
// create a struct to hold information about the swap chain
DXGI_SWAP_CHAIN_DESC scd;
// clear out the struct for use
ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
// fill the swap chain description struct
scd.BufferCount = 1; // one back buffer
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // use 32-bit color
scd.BufferDesc.Width = SCREEN_WIDTH; // set the back buffer width
scd.BufferDesc.Height = SCREEN_HEIGHT; // set the back buffer height
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // how swap chain is to be used
scd.OutputWindow = hWnd; // the window to be used
scd.SampleDesc.Count = 4; // how many multisamples
scd.Windowed = TRUE; // windowed/full-screen mode
scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // allow full-screen switching
// create a device, device context and swap chain using the information in the scd struct
D3D11CreateDeviceAndSwapChain(NULL,
D3D_DRIVER_TYPE_HARDWARE,
NULL,
NULL,
NULL,
NULL,
D3D11_SDK_VERSION,
&scd,
&swapchain,
&dev,
NULL,
&devcon);
// get the address of the back buffer
ID3D11Texture2D *pBackBuffer;
swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
// use the back buffer address to create the render target
dev->CreateRenderTargetView(pBackBuffer, NULL, &backbuffer);
pBackBuffer->Release();
// set the render target as the back buffer
devcon->OMSetRenderTargets(1, &backbuffer, NULL);
// Set the viewport
D3D11_VIEWPORT viewport;
ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.Width = SCREEN_WIDTH;
viewport.Height = SCREEN_HEIGHT;
devcon->RSSetViewports(1, &viewport);
InitPipeline();
InitGraphics();
}
// this is the function used to render a single frame
void RenderFrame(void)
{
// clear the back buffer to a deep blue
devcon->ClearRenderTargetView(backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));
// select which vertex buffer to display
UINT stride = sizeof(VERTEX);
UINT offset = 0;
devcon->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);
// select which primtive type we are using
devcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// draw the vertex buffer to the back buffer
devcon->Draw(3, 0);
// switch the back buffer and the front buffer
swapchain->Present(0, 0);
}
// this is the function that cleans up Direct3D and COM
void CleanD3D(void)
{
swapchain->SetFullscreenState(FALSE, NULL); // switch to windowed mode
// close and release all existing COM objects
pLayout->Release();
pVS->Release();
pPS->Release();
pVBuffer->Release();
swapchain->Release();
backbuffer->Release();
dev->Release();
devcon->Release();
}
// this is the function that creates the shape to render
void InitGraphics()
{
// create a triangle using the VERTEX struct
VERTEX OurVertices[] =
{
{
0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)},
{
0.45f, -0.5, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)},
{
-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)}
};
// create the vertex buffer
D3D11_BUFFER_DESC bd;
ZeroMemory(&bd, sizeof(bd));
bd.Usage = D3D11_USAGE_DYNAMIC; // write access access by CPU and GPU
bd.ByteWidth = sizeof(VERTEX) * 3; // size is the VERTEX struct * 3
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // use as a vertex buffer
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // allow CPU to write in buffer
dev->CreateBuffer(&bd, NULL, &pVBuffer); // create the buffer
// copy the vertices into the buffer
D3D11_MAPPED_SUBRESOURCE ms;
devcon->Map(pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // map the buffer
memcpy(ms.pData, OurVertices, sizeof(OurVertices)); // copy the data
devcon->Unmap(pVBuffer, NULL); // unmap the buffer
}
// this function loads and prepares the shaders
void InitPipeline()
{
// load and compile the two shaders
ID3D10Blob *VS, *PS;
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "VShader", "vs_4_0", 0, 0, 0, &VS, 0, 0);
D3DX11CompileFromFile(L"shaders.shader", 0, 0, "PShader", "ps_4_0", 0, 0, 0, &PS, 0, 0);
// encapsulate both shaders into shader objects
dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &pVS);
dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &pPS);
// set the shader objects
devcon->VSSetShader(pVS, 0, 0);
devcon->PSSetShader(pPS, 0, 0);
// create the input layout object
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{
"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},
};
dev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &pLayout);
devcon->IASetInputLayout(pLayout);
}
shaders.shader
struct VOut
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
VOut VShader(float4 position : POSITION, float4 color : COLOR)
{
VOut output;
output.position = position;
output.color = color;
return output;
}
float4 PShader(float4 position : SV_POSITION, float4 color : COLOR) : SV_TARGET
{
return color;
}