在本教程中,我们将开始在屏幕上绘制几何图形。 我们将学习更多关于管道状态对象(PSO)和根Sigantures的信息。 我们还将学习资源堆,视口,裁剪矩形和顶点!
资源堆资源堆就像描述符堆一样,但是它们不存储描述符,而是存储资源的实际数据。 它们是在GPU或CPU上分配的内存块,具体取决于堆的类型。
与描述符堆不同,资源堆的最大大小取决于堆的类型,可用的显存(GPU)内存或系统(CPU)内存。 数据可以包括顶点缓冲区,索引缓冲区,常量缓冲区或纹理。
堆有三种类型上传堆
上传堆用于将数据上传到GPU。 GPU对此存储具有读访问权限,而CPU具有写访问权限。 您的应用程序会将资源存储在这种类型的堆中,例如顶点缓冲区,然后使用UpdateListresources()函数使用命令列表将数据从该堆复制到默认堆中。
默认堆
默认堆是GPU上的内存块。 CPU无法访问该内存,这使得在着色器中访问该内存的速度非常快。 这是您要存储着色器使用的资源的地方。 要从您的应用程序获取此堆中的资源,您需要首先创建一个上传堆,将资源存储在上传堆中,然后使用UpdateSubresources()函数将数据从上传堆复制到默认堆。 基本上,此函数将命令存储在命令列表中,并在您对命令队列调用execute时执行。
如果应用程序像每个框架一样经常更改资源,则您每次都需要上传新资源。 在这些情况下,每次都将数据复制到默认堆将是效率低下的。 取而代之的是,您只会使用一个上传堆。 对于不经常更改或仅由GPU更改的其他资源,您将使用默认堆。回读堆
回读堆是GPU可以写入并且CPU可以读取的内存块。 这些可能是来自GPU的统计信息或有关屏幕捕获的信息。
顶点和输入布局
您可以使用直接命令队列将数据上传到GPU(在本教程中我们这样做),但是有一个复制队列,然后您可以使用复制命令列表和复制命令分配器将数据上传到GPU。 这是更有效的方法(但会使代码复杂一些),因为在命令队列执行命令以将数据从上传堆复制到默认堆时,直接命令列表可能正在执行绘图命令。要绘制几何图形,管道需要有关几何图形的信息。 以顶点列表(和下一个教程中说明的索引)形式的几何图形将传递到管道的Input Assembler(IA)阶段。 IA需要知道如何读取顶点数据,这就是输入布局和原始拓扑的来源。
顶点
顶点是构成几何的要素。 顶点将始终具有位置(即使最初没有设置位置)。 它们是空间中的一个点,用于定义诸如多边形,三角形,直线和点之类的几何图形。 一个顶点组成一个点,两个顶点组成一条线,三个顶点组成一个三角形,四个顶点组成一个四边形,依此类推。
所有实体均由三角形组成,这是最小的曲面。 Direct3D仅适用于三角形的实体对象(点和线也可以使用,但它们不能组成实体对象,也没有曲面)。 例如,一个四边形由两个三角形组成。
顶点位置由三个值定义。 x,y和z。 通常,在游戏编程中,x为左右,y为上下,z为深度。 您将创建一个表示顶点的结构,并将这些对象的数组(称为顶点缓冲区)传递到GPU,并将该顶点缓冲区绑定到IA。 一个示例结构可能看起来像这样:struct ColorVertex { float x, y, z; // position float r, g, b, z; // color; }
顶点还可以包含更多信息,以描述多边形的该部分。 诸如纹理坐标,颜色,法线坐标以及动画的权重信息之类的信息。 光栅化器将在每个顶点之间的多边形上插值这些值。 这意味着多边形上某些点的值取决于它与每个顶点的接近程度。
图元拓扑
输入装配(IA)使用图元拓扑来了解如何对顶点进行分组并在整个管道中传递它们。基本拓扑类型定义顶点是组成点,线,三角形还是多边形的列表。
在创建管道状态对象(PSO)时,我们需要说一下IA会将顶点组合为哪种图元拓扑类型。 D3D12_PRIMITIVE_TOPOLOGY_TYPE枚举中有四种实际的图元拓扑类型,外加未定义类型,这是PSO的默认值(如果在PSO中使用了未定义类型,则将无法绘制任何内容):
D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED-默认值,如果设置,则无法绘制任何内容
D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT-绘制时每个顶点都是一个点
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE-一次将两个顶点分组,并在它们之间绘制一条线
D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE-一次将三个顶点组合在一起,创建一个三角形。默认情况下,三角形是填充的,否则,如果设置了线框,则会在3个顶点之间绘制线
D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH-用于细分。如果设置了“ Hull Shader”和“ Domain shader”,则它必须是图元拓扑类型。
您还必须将图元拓扑邻接关系和命令列表中的顺序设置为D3D_PRIMITIVE_TOPOLOGY枚举类型。在命令列表中设置的图元拓扑邻接和顺序必须与PSO中设置的图元拓扑类型兼容。这是输入装配(IA)在组装几何体时如何对顶点/索引进行排序的方式。例如点列表,线列表,三角形列表,三角形带,具有邻接关系的三角形带等。输入布局
输入布局描述了输入装配应如何读取顶点缓冲区中的顶点。 它描述了顶点具有的属性的布局,例如位置和颜色,以及它们的大小。 这样,输入装配阶段就知道如何将数据传递到管道阶段,顶点开始和顶点结束的地方。
输入布局的示例可能如下所示:D3D12_INPUT_ELEMENT_DESC inputLayout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } };
此输入布局描述了一个顶点,该顶点的位置由三个32位浮点值组成,而颜色由四个32位浮点值组成。 当将此设置为输入布局时,IA知道每个顶点的大小为28个字节,因此当转到下一个顶点时,它会将当前地址增加28个字节。 它知道前12个字节是位置,后16个字节是颜色。 我们将在代码中进一步讨论创建输入布局的问题。
顶点和像素着色器尽管PSO只需要定义一个顶点着色器,但实际上要将任何东西绘制到渲染目标上,我们还需要一个像素着色器。我们将为这两个可编程管线阶段创建两个非常简单的着色器功能。
在DirectX中,以称为HLSL或高级着色器语言的语言对着色器进行编程。该语言类似于C语言,因此只要您了解着色器阶段将其作为输入并作为输出返回的内容,它就非常容易理解。
要使用着色器,将着色器功能编译为一种称为字节码的东西。字节码是我们传递给GPU的大量数据。 GPU将在该着色器阶段运行该字节码。在本教程中,我们将在运行时进行编译,以便于调试,但在实际游戏中,您将使用名为fxc.exe的程序来编译着色器代码(Visual Studio将自动编译着色器文件,如果需要,可以将其关闭。您可以通过右键单击着色器文件,转到属性,然后将“不包含在构建中”设置为“是”。使用fxc.exe编译时,默认输出是.cso文件或Compiled Shader Object文件。
您会注意到,我对所有着色器函数都使用了函数名“ main”。编译着色器时,默认情况下,fxc.exe在着色器文件中查找名为“ main”的函数。您可以将其更改为所需的任何内容。
要创建着色器,请在解决方案资源管理器中右键单击资源文件夹,也可以创建一个着色器文件夹,单击“添加”,然后单击“新建”,然后在弹出的窗口中的Visual C ++下,找到一个着色器。一个名为“ HLSL”的标签。单击该按钮,然后在屏幕右侧,您会看到一个可能的着色器列表。选择所需的着色器,然后单击添加(如果需要,可以在更改名称之后)。然后,Visual Studio将为所选的着色器创建示例代码。默认情况下,Visual Studio将使用fxc.exe编译此着色器。如果您想自己执行或在运行时执行此操作,则可以将其关闭(如上所述)。顶点着色器
顶点着色器是成功创建PSO所必需的。它是在PSO中需要设置的唯一着色器。不过,很少会只设置顶点着色器,因为如果未同时设置像素着色器,则不会向渲染目标绘制任何内容。之所以只能设置一个顶点着色器而没有其他设置的原因是由于流输出,该流在顶点着色器之后从GPU输出数据,除非设置了几何着色器,然后才在几何着色器之后输出。
顶点着色器接受单个顶点,并输出单个顶点。通常,传入的顶点具有相对于其自身模型空间的位置。首先,将其位置乘以其世界矩阵,再乘以视图矩阵,最后再乘以投影矩阵(简称WVP),就需要将该位置转换为屏幕空间或视口空间,我们将在以后的教程中对此进行讨论。
对于本教程,我们有一个非常简单的顶点着色器,它不进行任何计算,只是返回输入的顶点位置。我们赋予它的顶点位置已经在屏幕空间中,因此我们无需执行任何其他操作。// simple vertex shader float4 main( float3 pos : POSITION ) : SV_POSITION { return float4(pos, 1.0f); }
像素着色器
像素着色器可处理像素片段。 像素片段可能是渲染目标上最终像素位置的候选对象。 并非所有通过像素着色器的像素片段都会最终出现在渲染目标上,这是由于深度/模板缓冲区的缘故。 您可能在场景的后面绘制了一个对象,所有像素都通过了该对象的像素着色器,但随后在该对象的前面绘制了墙。 墙壁像素将是在渲染目标上绘制的最终像素,而不是对象像素(如果在相机和墙壁之间没有任何物体)。
像素着色器可以接受返回的任何顶点着色器,并以“红色,绿色,蓝色,Alpha”,RGBA的格式输出float4颜色。
我们正在创建一个简单的像素着色器。 它不输入任何内容,并返回绿色:管道状态对象(PSO)(MSDN Managing Graphics Pipeline State in Direct3D 12)// simple pixel shader float4 main() : SV_TARGET { return float4(0.0f, 1.0f, 0.0f, 1.0f); // Red, Green, Blue, Alpha }
管道状态对象是包含着色器和管道状态的对象。 游戏将在初始化期间创建许多此类对象。 这些是使Direct3D 12的性能比以前的DirectX迭代好得多的部分原因。 原因是因为我们能够在初始化时创建许多流水线状态对象,然后设置一个已经初始化的流水线状态对象,而不是在整个帧中多次设置单个状态。 在一帧中,每次需要更改管道状态时,您都将设置一个PSO。 管道状态PSO设置包含:
- 着色器字节码-在图形管道中启用的着色器功能
- 输入布局-顶点结构的格式
- 图元拓扑类型-D3D12_PRIMITIVE_TOPOLOGY_TYPE枚举,用于说明输入汇编程序是否应将几何图形装配为点,线,三角形或面块(用于镶嵌)。这与在命令列表(三角列表,三角带等)中设置的邻接和顺序不同。
- 混合状态-D3D12_BLEND_DESC结构。这描述了输出合并将像素片段写入渲染目标时使用的混合状态。
- 光栅化状态-光栅化状态,例如剔除模式,线框/实体渲染,抗锯齿等。D3D12_RASTERIZER_DESC结构。
- 深度/模板状态-用于深度/模板测试。接下来的教程之一将解释深度缓冲区测试,而后面的教程将解释模板测试。
- 渲染目标-这是输出合并器应写入的渲染目标的列表
- 渲染目标的数量-一次可以写入多个渲染目标。
- 多重采样-解释多重采样计数和质量的参数。这必须与渲染目标相同。
- 流输出缓冲区-流输出也写入的缓冲区。如果已设置,则将流输出写入几何着色器之后的流输出缓冲区,否则将在顶点着色器之后写入流输出缓冲区
- 根签名-根签名基本上是着色器功能期望的数据参数列表。着色器必须与“根签名”兼容。
某些管道状态可以在PSO外部设置。 这些状态是:
根签名
- 资源绑定(索引缓冲区,顶点缓冲区,流输出目标,渲染目标,描述符堆)
- 视口
- 裁剪矩形
- 混合因子
- 深度/模板参考值
- IA基本拓扑顺序/邻接
在上一教程《Direct3D 12综合篇》中讨论了根描述符的概述。
视口
基本上,根签名定义了当前PSO中的着色器将使用的数据。这些参数是“根常量”,“根描述符”或“描述符表”。
定义着色器将使用的数据(根签名中的条目)的根签名中的参数称为“根参数”。在运行时更改的实际数据值称为“根参数”。
与PSO一样,根签名是在初始化时创建的。更改根签名可能会很昂贵,因此您希望在可能的情况下按根签名将PSO分组在一起(这样您就不会不断地来回更改根签名)。
在绘制调用之间更改根参数时,不必跟踪围栏或其他任何东西。原因是因为根参数会自动进行版本控制,所以每个绘图调用都会获得自己的根签名状态。但是,这不同于将资源上传到GPU。将资源上传到GPU时,您必须通过设置和观察围栏来检查自己是否已完成上传(或复制),然后再使用该资源。
在本教程中,我们仅使用根签名表示要使用输入装配。为此,我们在初始化根签名时指定D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志。默认情况下,D3D12_ROOT_SIGNATURE_FLAG_NONE是在根签名上设置的唯一标志,这意味着我们将无法将顶点数据传递到顶点着色器。可以在不绑定顶点缓冲区的情况下执行绘图调用。在这些情况下,我们只说要绘制多少个顶点,然后可以使用诸如SV_VertexID之类的系统语义在顶点着色器中设置它们以标识当前的顶点编号。我们可以使用几何体着色器或细分来从那里生成更多的几何体。可用于粒子效果引擎。
在以后的教程中,我们将设置根描述符的根常量(用于诸如视图和投影矩阵之类的东西),以及根描述符和描述符表用于诸如纹理之类的事情。视口指定将场景绘制到的渲染目标的区域。
在视口中要设置六个值。左上角X,左上角Y,宽度,高度,最小Z和最大Z。
左上角的x和y相对于渲染目标的左上角(以像素为单位)。宽度和高度以像素为单位,相对于左上角x和y定义了视口的右侧和底部。最后,最小Z和最大Z定义要绘制的场景的Z范围。超出此范围的任何内容都不会绘制。
视口将视图空间转换为屏幕空间,其中屏幕空间以像素为单位,视图空间从左到右在-1.0到1.0之间,从上到下在1.0到-1.0之间。从顶点着色器出来的位置在视图空间中。视口定义的空间中的任何内容(要渲染的任何内容)在x轴上必须介于-1.0到1.0之间,在y轴上必须介于1.0到-1.0之间,并且在Z轴上介于min Z和max Z之间。
视图空间是从顶点和几何着色器出来的空间。 (-1.0,-1.0)和(1.0,1.0)之间的任何值都将在视图空间中。视口将视图空间“拉伸”到定义的屏幕空间的宽度和高度(以像素为单位)。
视口不必覆盖整个屏幕空间。 例如,您可能有两个视口,一个视口位于屏幕的左侧,而一个视口则位于屏幕的右侧(如果您有多人游戏)。 另一个示例可能是屏幕上的雷达或小型地图。 您将雷达或迷你地图渲染到视口定义的渲染目标的一小部分。
裁剪矩形裁剪矩形指定将在其上绘制的区域。 裁剪矩形外部的任何东西(像素碎片)都将被切割,甚至不会切割到像素着色器上。
裁剪矩形有四个成员。 左,上,右和下。 这些以像素为单位,相对于渲染目标的左上角。
编码现在让我们进入代码。 只是要清楚一点,在现实生活中我不会像这样编写代码。 我决定像在这些教程中一样编写代码的唯一原因是,因为我觉得无需在类和文件之间跳转就可以更轻松地了解Directx的工作原理。 在制作自己的应用程序时,请尽量远离全局变量。
新的全局变量这是我们将在本教程中使用的新全局变量。
首先,我们有一个PSO。该PSO将包含我们的默认管道状态。在本教程中,我们只有一个管道状态对象,但是在实际的应用程序中,您将有很多。
接下来是根签名。我们将使用此根签名来表示将使用Input Assembler,这意味着我们将绑定一个包含有关每个顶点信息(例如位置)的顶点缓冲区。顶点缓冲区中的每个顶点都将传递到顶点着色器。
接下来是我们的视口。我们只有一个视口,因为我们将要绘制到整个渲染目标。
在我们的视口之后是一个裁剪。裁剪矩形会说在哪里画,在哪里不画。我已经注意到,我的一台计算机在没有设置裁剪矩形的情况下即可工作,而另一台计算机在未定义裁剪矩形的情况下不会绘制任何内容。
我们有一个ID3D12Resource,它将在其中存储我们的顶点缓冲区。此资源将是一个默认堆,我们将从一个临时上传堆上传顶点缓冲区。
最后,我们有一个顶点缓冲区视图。该视图仅描述地址,步幅(每个顶点的大小)和顶点缓冲区的总大小。顶点结构ID3D12PipelineState* pipelineStateObject; // pso containing a pipeline state ID3D12RootSignature* rootSignature; // root signature defines data shaders will access D3D12_VIEWPORT viewport; // area that output from rasterizer will be stretched to. D3D12_RECT scissorRect; // the area to draw in. pixels outside that area will not be drawn onto ID3D12Resource* vertexBuffer; // a default buffer in GPU memory that we will load vertex data for our triangle into D3D12_VERTEX_BUFFER_VIEW vertexBufferView; // a structure containing a pointer to the vertex data in gpu memory // the total size of the buffer, and the size of each element (vertex)
我们将需要定义一个顶点结构。 创建顶点缓冲区时,我们将创建许多顶点结构对象。 顶点缓冲区是一个顶点数组。
在本教程中,我们只有一个顶点位置,该位置由3个浮点值x,y和z定义。
我们正在使用DirectX数学库,因此我们将使用XMFLOAT3结构来保持顶点位置:创建根签名(MSDN Creating a Root Signature)struct Vertex { XMFLOAT3 pos; };
根签名存储在ID3D12RootSignature接口中。
要创建根签名,我们将填写CD3DX12_ROOT_SIGNATURE_DESC结构,该结构在扩展的dx12标头中定义。 这是D3D12_ROOT_SIGNATURE_DESC结构的包装:typedef struct D3D12_ROOT_SIGNATURE_DESC { UINT NumParameters; const D3D12_ROOT_PARAMETER *pParameters; UINT NumStaticSamplers; const D3D12_STATIC_SAMPLER_DESC *pStaticSamplers; D3D12_ROOT_SIGNATURE_FLAGS Flags; } D3D12_ROOT_SIGNATURE_DESC;
- NumParameters-这是我们的根签名将具有的插槽数。 插槽是一个根参数。 根参数可以是根常量,根描述符或描述符表。
- pParameters-这是D3D12_ROOT_PARAMETER结构的一种,它定义了此根签名所包含的每个根参数。 我们将在以后的教程中讨论这些内容。
- NumStaticSamplers-这是根签名将包含的静态采样器的数量。
- pStaticSamplers-D3D12_STATIC_SAMPLER_DESC结构的数组。 这些结构定义了静态采样器。
- Flags -D3D12_ROOT_SIGNATURE_FLAGS或(|)的组合。
这些是创建根签名时可以使用的标志。 创建根签名时使用的默认标志为D3D12_ROOT_SIGNATURE_FLAG_NONE。 在本教程中,我们仅使用根签名来告诉管道使用Input Assembler,以便我们可以通过管道传递顶点缓冲区。 该根签名在本教程中将没有任何根参数,尽管我们将在以后的教程中为其添加根参数。
typedef enum D3D12_ROOT_SIGNATURE_FLAGS { D3D12_ROOT_SIGNATURE_FLAG_NONE = 0, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT = 0x1, D3D12_ROOT_SIGNATURE_FLAG_DENY_VERTEX_SHADER_ROOT_ACCESS = 0x2, D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS = 0x4, D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS = 0x8, D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS = 0x10, D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS = 0x20, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_STREAM_OUTPUT = 0x40 } D3D12_ROOT_SIGNATURE_FLAGS;
要告诉管道使用输入装配,我们必须使用D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志创建一个根签名。没有此标志,将不使用输入装配,调用draw(numberOfVertices)将调用顶点着色器的NumberOfVertices次,且顶点为空。基本上,我们可以使用顶点索引在顶点着色器中创建顶点。我们可以将它们传递给几何着色器以创建更多的几何。通过不使用D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志,我们在根签名中保存了一个可用于根常量,根描述符或描述符表的插槽。这种优化是最小的。
如果指定了D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,则必须创建并使用输入布局。
在本教程中,我们将通过管道传递顶点缓冲区,因此我们指定D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志。
其他标志用于拒绝对根签名进行管道访问的阶段。使用资源或根签名时,您要拒绝不需要访问的任何着色器,以便GPU可以优化。允许所有着色器访问所有内容会降低性能。
许多Direct3d 12函数允许您传递ID3DBlob指针来存储错误消息。如果您不希望读取错误,则可以传递nullptr。您可以通过调用blob的GetBufferPointer()方法从这些函数返回的ID3DBlob中获取以null结尾的char数组。此char数组将包含错误消息。我们将在下面的部分中看到这一点。
在本教程中,我们将在运行时在代码中定义并创建根签名。但是,您可以改为在HSLS中定义根签名。
我们在这里要做的第一件事是填写CD3DX12_ROOT_SIGNATURE_DESC。我们希望使用输入装配,因此我们指定了D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志。
一旦创建了描述,我们就将根签名“序列化”为字节码。我们将使用此字节码创建根签名对象。
拥有根签名字节码后,我们通过调用设备的CreateRootSignature()方法来创建根签名。HRESULT CreateRootSignature( [in] UINT nodeMask, [in] const void *pBlobWithRootSignature, [in] SIZE_T blobLengthInBytes, REFIID riid, [out] void **ppvRootSignature );
创建顶点和像素着色器// create root signature CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ID3DBlob* signature; hr = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, nullptr); if (FAILED(hr)) { return false; } hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)); if (FAILED(hr)) { return false; }
Direct3D中的着色器是用称为高级着色语言(或更简单地说是HLSL)的语言编写的。
要在Visual Studio中创建着色器,请打开解决方案资源管理器,右键单击资源(您可以将着色器文件放置在任何位置,但是由于已经存在资源文件夹,我们将它们存储在此处),将鼠标悬停在“添加”上,然后选择“ 新建项...”。单击添加后,着色器将打开,并且将为您提供该着色器的最基本代码。 以这种方式创建的所有着色器函数都将被命名为“ main”。 您可以更改此名称,但还必须确保在c ++代码中对其进行更改,以查找正确的函数名称。 默认情况下,fxc.exe还将在编译时查找名为“ main”的函数,因此,如果您要更改此函数名称并尝试编译代码,则很可能会出现如下错误:
FXC : error X3501: 'main': entrypoint not found
要解决此错误,您可以将着色器函数的名称改回“ main”,或者打开解决方案资源管理器,右键单击着色器文件,单击属性。属性窗口将打开。在左侧面板上,单击“ HLSL编译器”。在右侧窗口中,有一个名为“入口点名称”的选项。默认情况下,该值为“ main”。将其更改为您的着色器函数的名称。
生成程序时,默认情况下还将使用fxc.exe编译hlsl着色器文件。这些文件的输出是具有默认扩展名(可以在属性中更改).cso的“已编译着色器对象”文件,其中包含着色器功能字节码。如果要在属性窗口中,可以通过指示“常规”选项卡并将“从生成中排除”选项设置为“是”,告诉Visual Studio不要编译这些文件。
好了,现在让我们讨论一下教程代码。
创建着色器时,必须提供一个指向包含着色器字节码的ID3DBlob的指针。调试时,您可能希望在运行时编译着色器文件以捕获着色器中的任何错误。我们可以在运行时使用D3DCompileFromFile()函数编译着色器代码。此函数会将着色器代码编译为着色器字节码,并将其存储在ID3DBlob对象中。发行游戏时,您希望将着色器编译为已编译的着色器目标文件,并在初始化过程中在运行时加载而不是编译着色器代码。HRESULT WINAPI D3DCompileFromFile( in LPCWSTR pFileName, in_opt const D3D_SHADER_MACRO pDefines, in_opt ID3DInclude pInclude, in LPCSTR pEntrypoint, in LPCSTR pTarget, in UINT Flags1, in UINT Flags2, out ID3DBlob ppCode, out_opt ID3DBlob ppErrorMsgs );
- pFileName-这是包含着色器代码的文件名
- pDefines-这是定义着色器宏的D3D_SHADER_MACRO结构的数组。如果不使用宏,则将其设置为nullptr。
- pInclude-这是指向ID3DInclude接口的指针,该接口用于处理着色器代码中的#includes。如果您的着色器代码中包含任何#include行并将其设置为nullptr,则着色器将无法编译。
- pEntrypoint-这是着色器入口函数的名称。在本教程中,我们保留默认的着色器函数名称“ main”。
- pTarget-这是您在编译着色器时要使用的着色器模型。我们正在使用着色器模型5.0,因此,例如,当我们编译顶点着色器时,将其设置为“ vs_5_0”。像素着色器将设置为“ ps_5_0”。
- Flags1-这些是编译选项标志或。对于本教程和调试,我们使用D3DCOMPILE_DEBUG | D3DCOMPILE_DEBUG。 D3DCOMPILE_SKIP_OPTIMIZATION标志。
- Flags2-用于效果文件的更多编译标志。如果我们不编译效果文件,则此参数将被忽略,我们可以将其设置为0
- ppCode-这是指向ID3DBlob的指针,该指针将指向已编译的着色器字节码。
- ppErrorMsgs-这是指向ID3DBlob的指针,该ID3DBlob将保存编译着色器代码时发生的任何错误。
如果在编译着色器时发生错误,我们可以使用传递到最后一个参数的ID3DBlob对其进行访问。 该消息是一个以NULL结尾的c字符串(char数组)。 我们可以通过强制转换包含错误的ID3DBlob的GetBufferPointer()返回值来获取错误消息。 我们可以使用OutputDebugString()函数在Visual Studio的输出窗口中输出错误。
创建PSO时,需要提供一个D3D12_SHADER_BYTECODE结构,其中包含着色器字节码和着色器字节码的大小。 我们可以使用传递给D3DCompileFromFile()的ID3DBlob的GetBufferPointer()方法获得指向着色器字节码的指针,并可以使用ID3DBlob的GetBufferSize()方法获得字节码的大小。创建输入布局// create vertex and pixel shaders // when debugging, we can compile the shader files at runtime. // but for release versions, we can compile the hlsl shaders // with fxc.exe to create .cso files, which contain the shader // bytecode. We can load the .cso files at runtime to get the // shader bytecode, which of course is faster than compiling // them at runtime // compile vertex shader ID3DBlob* vertexShader; // d3d blob for holding vertex shader bytecode ID3DBlob* errorBuff; // a buffer holding the error data if any hr = D3DCompileFromFile(L"VertexShader.hlsl", nullptr, nullptr, "main", "vs_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &vertexShader, &errorBuff); if (FAILED(hr)) { OutputDebugStringA((char*)errorBuff->GetBufferPointer()); return false; } // fill out a shader bytecode structure, which is basically just a pointer // to the shader bytecode and the size of the shader bytecode D3D12_SHADER_BYTECODE vertexShaderBytecode = {}; vertexShaderBytecode.BytecodeLength = vertexShader->GetBufferSize(); vertexShaderBytecode.pShaderBytecode = vertexShader->GetBufferPointer(); // compile pixel shader ID3DBlob* pixelShader; hr = D3DCompileFromFile(L"PixelShader.hlsl", nullptr, nullptr, "main", "ps_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &pixelShader, &errorBuff); if (FAILED(hr)) { OutputDebugStringA((char*)errorBuff->GetBufferPointer()); return false; } // fill out shader bytecode structure for pixel shader D3D12_SHADER_BYTECODE pixelShaderBytecode = {}; pixelShaderBytecode.BytecodeLength = pixelShader->GetBufferSize(); pixelShaderBytecode.pShaderBytecode = pixelShader->GetBufferPointer();
在这里,我们创建输入布局,该布局将顶点缓冲区内的顶点描述给输入装配。 输入装配将使用输入布局来组织顶点并将顶点传递到管线的各个阶段。
为了创建输入布局,我们填充了一个D3D12_INPUT_ELEMENT_DESC结构数组,每个数组用于顶点结构的每个属性,例如位置,纹理坐标或颜色:typedef struct D3D12_INPUT_ELEMENT_DESC { LPCSTR SemanticName; UINT SemanticIndex; DXGI_FORMAT Format; UINT InputSlot; UINT AlignedByteOffset; D3D12_INPUT_CLASSIFICATION InputSlotClass; UINT InstanceDataStepRate; } D3D12_INPUT_ELEMENT_DESC;
- SemanticName-这是参数的名称。输入装配会将此属性与着色器中具有相同语义名称的输入关联。只要这里的语义名称与顶点着色器的输入参数之一匹配,它就可以是任何东西。
- SemanticIndex-仅当多个元素具有相同的语义名称时才需要。例如,您有两个元素的语义名称为“ COLOR”。在着色器中,您有两个顶点输入,分别称为color1和color2。您可以分别为这些输入参数赋予语义名称“ COLOR0”,“ COLOR1”。此参数将说明与该属性关联的哪个(COLOR0或COLOR1)顶点输入参数。
- Format-这是DXGI_FORMAT枚举。这将定义此属性所采用的格式。例如,我们有一个位置,该位置由3个浮点值x,y和z组成。浮点值是4个字节或32位,因此我们将此参数设置为DXGI_FORMAT_R32G32B32_FLOAT,表示该属性中有3个浮点值。然后,应将其映射到着色器中的float3参数。
- InputSlot-您可以将多个顶点缓冲区绑定到输入装配。每个顶点缓冲区都绑定到一个插槽。我们一次只绑定一个顶点缓冲区,因此我们将其设置为0(第一个插槽)。
- AlignedByteOffset-这是从顶点结构的开始到该属性的开始的字节偏移量。在这里,第一个属性将始终为0。我们只有一个属性,即位置,因此我们将其设置为0。但是,当上色时,我们将具有第二个属性,即颜色,然后我们需要将颜色元素设置为12。我们将color元素设置为12个字节的偏移量,因为位置有3个浮点数,每个浮点数都是4个字节,所以4x3是12个。我们还可以查看上一个元素的格式,例如position使用的格式DXGI_FORMAT_R32G32B32_FLOAT,为96位或12个字节(96/8 = 12),因为每个字节均为8位。
- InputSlotClass-D3D12_INPUT_CLASSIFICATION枚举。这指定此元素是按顶点还是按实例。关于实例化的更多信息。现在,由于我们尚未实例化,因此我们将使用D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA。
- InstanceDataStepRate-这是在转到下一个元素之前要绘制的实例数。如果设置D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,则必须将其设置为0
创建输入元素数组后,我们将填充D3D12_INPUT_LAYOUT_DESC结构。 创建PSO时,此结构将作为参数传递。
如果我们不使用根签名中定义的输入装配,则不需要任何与该根签名相关联的PSO的输入布局。
我们可以通过使用sizeof(array)来获取c ++中数组的大小(元素数),该数组给出了数组具有的字节总数,然后将其除以sizeof(element)或元素的大小 在数组中。 D3D12_INPUT_LAYOUT_DESC包含输入元素的数量和输入元素的数组。创建管道状态对象(PSO)// create input layout // The input layout is used by the Input Assembler so that it knows // how to read the vertex data bound to it. D3D12_INPUT_ELEMENT_DESC inputLayout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } }; // fill out an input layout description structure D3D12_INPUT_LAYOUT_DESC inputLayoutDesc = {}; // we can get the number of elements in an array by "sizeof(array) / sizeof(arrayElementType)" inputLayoutDesc.NumElements = sizeof(inputLayout) / sizeof(D3D12_INPUT_ELEMENT_DESC); inputLayoutDesc.pInputElementDescs = inputLayout;
在实际的应用程序中,您通常会得到许多PSO。 对于本教程,我们只需要一个。
要创建管道状态对象,我们必须填写D3D12_GRAPHICS_PIPELINE_STATE_DESC结构:typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC { ID3D12RootSignature *pRootSignature; D3D12_SHADER_BYTECODE VS; D3D12_SHADER_BYTECODE PS; D3D12_SHADER_BYTECODE DS; D3D12_SHADER_BYTECODE HS; D3D12_SHADER_BYTECODE GS; D3D12_STREAM_OUTPUT_DESC StreamOutput; D3D12_BLEND_DESC BlendState; UINT SampleMask; D3D12_RASTERIZER_DESC RasterizerState; D3D12_DEPTH_STENCIL_DESC DepthStencilState; D3D12_INPUT_LAYOUT_DESC InputLayout; D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue; D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType; UINT NumRenderTargets; DXGI_FORMAT RTVFormats[8]; DXGI_FORMAT DSVFormat; DXGI_SAMPLE_DESC SampleDesc; UINT NodeMask; D3D12_CACHED_PIPELINE_STATE CachedPSO; D3D12_PIPELINE_STATE_FLAGS Flags; } D3D12_GRAPHICS_PIPELINE_STATE_DESC;
我指出了需要哪些参数,我很难解决这个问题。 即使某些参数可能不需要,例如InputLayout,但如果使用输入装配,则它们是必需的(如果没有它们,您的代码仍可以运行,但是不会绘制任何内容)。
- pRootSignature-必需。指向我们的根签名的指针。
- VS-必填。指向顶点着色器字节码的指针。 (用于操纵顶点,最常见的是将它们从对象空间,世界空间,投影空间,视图空间转换)
- PS-不需要。指向像素着色器字节码的指针。 (用于绘制像素片段)
- DS-不需要。指向域着色器字节码的指针(用于细分)
- HS-不需要。指向外壳着色器字节码的指针(也用于细分)
- GS-不需要。指向几何着色器的指针(用于创建几何)
- StreamOutput-不需要。用于将数据从管道(在几何着色器之后,或在顶点着色器(如果未定义几何着色器)之后)发送到您的应用。
- BlendState-必需。这用于混合,例如透明度。目前,我们具有默认的混合状态,但是我们将在以后的教程中对此进行详细说明。
- SampleMask-必填。这与多重采样有关。 0xffffffff表示使用点采样。这将在以后的教程中进行解释。
- RasterizerState-必需。这是光栅化器的状态。我们现在将使用默认状态,但是稍后将对此进行教程。
- DepthStencilState-不需要。这是深度/模板缓冲区的状态。同样,这将在以后的教程中进行解释。
- InputLayout-不需要。定义顶点布局的D3D12_INPUT_LAYOUT_DESC结构。
- IBStripCutValue-不需要。 D3D12_INDEX_BUFFER_STRIP_CUT_VALUE枚举。当定义了三角形条形拓扑时,将使用此选项。
- PrimitiveTopologyType-必需。一个D3D12_PRIMITIVE_TOPOLOGY_TYPE,用于定义将顶点放在一起的图元拓扑(点,线,三角形,面片)。
- NumRenderTargets-必需。这是RTVFormats参数中渲染目标格式的数量。
- RTVFormats [8]-必需。 DXGI_FORMAT枚举数组,用于解释每个渲染目标的格式。必须与使用的渲染目标相同的格式。
- DSVFormat-不需要。 DXGI_FORMAT枚举数组,用于解释每个深度/模板缓冲区的格式。必须与使用的深度模板缓冲区的格式相同。
- SampleDesc-必填。多次采样的样本数量和质量。必须与渲染目标相同
- NodeMask-不需要。一个位掩码,指出要使用的GPU适配器。如果仅使用一个GPU,请将其设置为0。
- CachedPSO-不需要。这是一个很酷的参数。您可以将PSO缓存到文件中,因此,下次您初始化PSO时,编译会快得多。这是D3D12_CACHED_PIPELINE_STATE结构。缓存的PSO取决于硬件,这意味着如果您在一台计算机上,则不能与另一台计算机共享缓存的PSO,否则您将得到D3D12_ERROR_ADAPTER_NOT_FOUND错误代码。另外,如果显卡驱动程序是从缓存的PSO开始更新的,则在尝试编译PSO时,您将获得D3D12_ERROR_DRIVER_VERSION_MISMATCH错误代码。您可能想在第一次运行应用程序时缓存PSO,然后每次其运行再次加载到缓存的PSO文件中。如果您遇到以上任何一个错误,只需在不缓存PSO的情况下加载它,然后再次将其保存到文件中即可。
- Flags-不需要。一个D3D12_PIPELINE_STATE_FLAGS。唯一的选项是D3D12_PIPELINE_STATE_FLAG_NONE和D3D12_PIPELINE_STATE_FLAG_TOOL_DEBUG。默认情况下,设置为D3D12_PIPELINE_STATE_FLAG_NONE。 debug选项将提供额外的信息,这些信息在调试时很有用。
查看代码,我们填写一个D3D12_GRAPHICS_PIPELINE_STATE_DESC结构,然后创建(包括编译)PSO。 我们使用设备接口的CreateGraphicsPipelineState()方法创建PSO。
创建一个顶点缓冲区// create a pipeline state object (PSO) // In a real application, you will have many pso's. for each different shader // or different combinations of shaders, different blend states or different rasterizer states, // different topology types (point, line, triangle, patch), or a different number // of render targets you will need a pso // VS is the only required shader for a pso. You might be wondering when a case would be where // you only set the VS. It's possible that you have a pso that only outputs data with the stream // output, and not on a render target, which means you would not need anything after the stream // output. D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; // a structure to define a pso psoDesc.InputLayout = inputLayoutDesc; // the structure describing our input layout psoDesc.pRootSignature = rootSignature; // the root signature that describes the input data this pso needs psoDesc.VS = vertexShaderBytecode; // structure describing where to find the vertex shader bytecode and how large it is psoDesc.PS = pixelShaderBytecode; // same as VS but for pixel shader psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; // type of topology we are drawing psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; // format of the render target psoDesc.SampleDesc = sampleDesc; // must be the same sample description as the swapchain and depth/stencil buffer psoDesc.SampleMask = 0xffffffff; // sample mask has to do with multi-sampling. 0xffffffff means point sampling is done psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // a default rasterizer state. psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // a default blent state. psoDesc.NumRenderTargets = 1; // we are only binding one render target // create the pso hr = device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineStateObject)); if (FAILED(hr)) { return false; }
顶点缓冲区是顶点结构的列表。要使用顶点缓冲区,我们必须将其放入GPU,然后将该顶点缓冲区(资源)绑定到输入装配。
要获得GPU的顶点缓冲区,我们有两个选择。第一种选择是仅使用上传堆,并且每帧将顶点缓冲区上传到GPU。这很慢,因为我们需要每帧将内存的顶点缓冲区复制到显存,因此通常您不希望这样做。第二个选项,通常是您要做的,是使用上传堆将顶点缓冲区上上传到GPU,然后将数据从上传堆复制到默认堆。默认堆将保留在内存中,直到我们覆盖或释放它。第二种方法是更可取的方法,因为您只需要在一段时间内只复制一次数据,这是我们在本教程中将使用的方法,因为它是最有效的。
我们创建一个顶点列表并将其存储在vList数组中。在这里,我们创建了已经在视图空间中定义的3个顶点,这些顶点组成一个三角形。
要创建资源堆,我们使用设备接口的CreateCommittedResource()方法:HRESULT CreateCommittedResource( [in] const D3D12_HEAP_PROPERTIES *pHeapProperties, D3D12_HEAP_FLAGS HeapFlags, [in] const D3D12_RESOURCE_DESC *pResourceDesc, D3D12_RESOURCE_STATES InitialResourceState, [in, optional] const D3D12_CLEAR_VALUE *pOptimizedClearValue, REFIID riidResource, [out, optional] void **ppvResource );
- pHeapProperties-定义堆属性的D3D12_HEAP_PROPERTIES结构。我们将使用帮助结构CD3DX12_HEAP_PROPERTIES创建所需的堆类型(上传堆和默认堆)。
- HeapFlags-D3D12_HEAP_FLAGS枚举。我们将没有任何标志,因此我们指定D3D12_HEAP_FLAG_NONE。
- pResourceDesc-描述堆的D3D12_RESOURCE_DESC结构。我们将使用辅助结构CD3DX12_RESOURCE_DESC。
- InitialResourceState-D3D12_RESOURCE_STATES枚举。这是堆将处于的初始状态。对于上传缓冲区,我们希望它处于读取状态,因此我们为该状态指定D3D12_RESOURCE_STATE_GENERIC_READ。对于默认堆,我们希望它成为复制目标,因此我们指定D3D12_RESOURCE_STATE_COPY_DEST。将顶点缓冲区复制到默认堆后,我们将使用资源屏障将默认堆从复制目标状态转换为顶点/常量缓冲区状态
- pOptimizedClearValue-D3D12_CLEAR_VALUE结构。如果这是渲染目标或深度模板,则可以将此值设置为通常会清除到的深度/模板缓冲区或渲染目标的值。 GPU可以进行一些优化,以提高清除资源的性能。我们的资源是顶点缓冲区,因此我们将此值设置为nullptr。
- riidResource-生成的资源接口的类型的唯一标识符。
- ppvResource-指向可以与该资源一起使用的资源接口对象的指针。
我们可以使用接口的SetName()方法设置堆的名称。 这对于图形调试很有用,在图形调试中,我们可以通过使用名称来区分资源。 在本教程的最后,我们将快速查看Visual Studio中的图形调试器。
创建顶点缓冲区(顶点列表)后,我们将创建并上传一个默认堆。 上传堆用于将顶点缓冲区上传到GPU,因此我们可以将数据复制到默认堆,该默认堆将保留在内存中,直到我们覆盖或释放它为止。
我们可以使用UpdateSubresources()函数将数据从上传堆复制到默认堆。UINT64 inline UpdateSubresources( _In_ ID3D12GraphicsCommandList *pCmdList, _In_ ID3D12Resource *pDestinationResource, _In_ ID3D12Resource *pIntermediate, UINT64 IntermediateOffset, _In_ UINT FirstSubresource, _In_ UINT NumSubresources, _In_ D3D12_SUBRESOURCE_DATA *pSrcData );
- pCmdList-这是我们将用于创建此命令的命令列表,该命令会将上传堆的内容复制到默认堆。
- pDestinationResource-这是复制命令的目标。在我们的情况下,它将是默认堆,但也可能是回读堆。
- pIntermediate-这是我们要从中复制数据的地方。在本教程中,它是上传堆,但也可以是默认堆。
- IntermediateOffset-这是我们要从其开始偏移的字节数。我们希望复制整个顶点缓冲区,因此我们将完全不偏移,并将其设置为0。
- FirstSubresource-这是开始复制的第一个子资源的索引。我们只有一个,因此我们将其设置为0。
- NumSubresources-这是我们要复制的子资源的数量。同样,我们只有一个,因此我们将其设置为1。
- pSrcData-这是指向D3D12_SUBRESOURCE_DATA结构的指针。这个结构包含一个指向我们数据所在的内存的指针,以及我们资源的字节大小。
创建复制命令后,我们的命令列表会将其存储在其命令分配器中,等待执行。 在使用存储在默认堆中的顶点缓冲区之前,我们必须确保已完成将其上传和复制到默认堆中。 我们关闭命令列表,然后使用命令队列执行它。 我们增加该帧的围栏值,并告诉命令队列在GPU端增加围栏。 再次增加围栏是一个命令,一旦命令列表完成执行,该命令将被执行。
在执行copy命令并设置围栏之后,我们需要填写我们的顶点缓冲区视图。 这是一个D3D12_VERTEX_BUFFER_VIEW结构,其中包含GPU地址,步幅(以字节为单位的顶点结构的大小)和缓冲区的总大小。 开始绘制场景时,将使用此结构将顶点缓冲区绑定到IA。填写视口和裁剪矩形// Create vertex buffer // a triangle Vertex vList[] = { { { 0.0f, 0.5f, 0.5f } }, { { 0.5f, -0.5f, 0.5f } }, { { -0.5f, -0.5f, 0.5f } }, }; int vBufferSize = sizeof(vList); // create default heap // default heap is memory on the GPU. Only the GPU has access to this memory // To get data into this heap, we will have to upload the data using // an upload heap device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), // a default heap D3D12_HEAP_FLAG_NONE, // no flags &CD3DX12_RESOURCE_DESC::Buffer(vBufferSize), // resource description for a buffer D3D12_RESOURCE_STATE_COPY_DEST, // we will start this heap in the copy destination state since we will copy data // from the upload heap to this heap nullptr, // optimized clear value must be null for this type of resource. used for render targets and depth/stencil buffers IID_PPV_ARGS(&vertexBuffer)); // we can give resource heaps a name so when we debug with the graphics debugger we know what resource we are looking at vertexBuffer->SetName(L"Vertex Buffer Resource Heap"); // create upload heap // upload heaps are used to upload data to the GPU. CPU can write to it, GPU can read from it // We will upload the vertex buffer using this heap to the default heap ID3D12Resource* vBufferUploadHeap; device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), // upload heap D3D12_HEAP_FLAG_NONE, // no flags &CD3DX12_RESOURCE_DESC::Buffer(vBufferSize), // resource description for a buffer D3D12_RESOURCE_STATE_GENERIC_READ, // GPU will read from this buffer and copy its contents to the default heap nullptr, IID_PPV_ARGS(&vBufferUploadHeap)); vBufferUploadHeap->SetName(L"Vertex Buffer Upload Resource Heap"); // store vertex buffer in upload heap D3D12_SUBRESOURCE_DATA vertexData = {}; vertexData.pData = reinterpret_cast<BYTE*>(vList); // pointer to our vertex array vertexData.RowPitch = vBufferSize; // size of all our triangle vertex data vertexData.SlicePitch = vBufferSize; // also the size of our triangle vertex data // we are now creating a command with the command list to copy the data from // the upload heap to the default heap UpdateSubresources(commandList, vertexBuffer, vBufferUploadHeap, 0, 0, 1, &vertexData); // transition the vertex buffer data from copy destination state to vertex buffer state commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(vertexBuffer, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER)); // Now we execute the command list to upload the initial assets (triangle data) commandList->Close(); ID3D12CommandList* ppCommandLists[] = { commandList }; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // increment the fence value now, otherwise the buffer might not be uploaded by the time we start drawing fenceValue[frameIndex]++; hr = commandQueue->Signal(fence[frameIndex], fenceValue[frameIndex]); if (FAILED(hr)) { Running = false; } // create a vertex buffer view for the triangle. We get the GPU memory address to the vertex pointer using the GetGPUVirtualAddress() method vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.StrideInBytes = sizeof(Vertex); vertexBufferView.SizeInBytes = vBufferSize;
我们需要指定一个视口和一个裁剪矩形。 我们定义的视口将覆盖整个渲染目标。 通常,屏幕空间的深度在0.0到1.0之间。 视口会将场景从视口拉伸到屏幕空间。
之后,我们创建一个裁剪矩形。 裁剪矩形在屏幕空间中定义。 裁剪矩形之外的任何东西甚至都不会进入像素着色器。渲染// Fill out the Viewport viewport.TopLeftX = 0; viewport.TopLeftY = 0; viewport.Width = Width; viewport.Height = Height; viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; // Fill out a scissor rect scissorRect.left = 0; scissorRect.top = 0; scissorRect.right = Width; scissorRect.bottom = Height;
我们终于达到了最好的状态,描绘了我们的场景!我们要做的第一件事是设置根签名。在命令列表中设置的根签名必须与创建PSO时使用的根签名相同,该根签名是在创建绘制调用时设置的。
设置根签名后,设置视口和裁剪矩形。
我们必须指定我们希望输入装配对顶点进行排序的原始拓扑顺序,在本教程中为三角形列表。这意味着每三个顶点是一个三角形(当我们开始索引时,这意味着每三个索引是一个三角形)。
现在,我们设置要绘制的顶点缓冲区。通过在命令列表上调用IASetVertexBuffers()并提供起始插槽,视图数量和顶点缓冲区视图数组,将顶点缓冲区绑定到IA。在上面创建输入布局时,我们简要介绍了插槽。我们仅使用一个插槽,因此我们将第一个参数设置为0(第一个插槽),将第二个参数设置为1(仅一个视图)。
最后,我们调用DrawInstanced()创建绘制命令。第一个参数是要绘制的顶点数,第二个参数是要绘制的实例数,第三个参数是要绘制的第一个顶点的起始索引,最后一个参数是在读取每个顶点之前添加到每个索引的值-来自顶点缓冲区的实例数据(有关更多信息,请参见后面的教程)。清理// draw triangle commandList->SetGraphicsRootSignature(rootSignature); // set the root signature commandList->RSSetViewports(1, &viewport); // set the viewports commandList->RSSetScissorRects(1, &scissorRect); // set the scissor rects commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // set the primitive topology commandList->IASetVertexBuffers(0, 1, &vertexBufferView); // set the vertex buffer (using the vertex buffer view) commandList->DrawInstanced(3, 1, 0, 0); // finally draw 3 vertices (draw the triangle)
别忘了收拾〜
调试着色器SAFE_RELEASE(pipelineStateObject); SAFE_RELEASE(rootSignature); SAFE_RELEASE(vertexBuffer);
Visual Studio附带了一个称为图形调试器的有价值的工具。 通过转到“调试”->“图形”->“开始诊断”,可以使用此工具调试图形管道。 您的应用程序将开始运行,并且您将在应用程序窗口顶部看到一些带有一些统计信息的文本。
按键盘上的打印屏幕键(PrtScn),或在Visual Studio中单击“捕获框”。 您将看到一个框架在Visual Studio中被捕获。
现在,在Visual Studio中暂停您的应用程序。 它将带您进入暂停的代码行。 通过单击选项卡中名为“ Report <number> .diagression”之类的文件,返回图形调试器选项卡。 双击您刚捕获的帧。
图形调试器将打开一个新窗口。 在此窗口中,您可以在左侧看到每个被称为该框架的命令。展开“ ExecuteCommandList”以查看通过该execute调用在GPU上执行的所有命令。
单击DrawInstanced项,然后将在右侧显示Input Assembler,Vertex Shader,Pixel Shader以及最后的输出合并的结果。 您可以在此处通过单击输出窗口下方的播放图标来调试着色器。
回头看一下屏幕左侧的“图形事件列表”,您会在某些行上看到蓝色文本,例如“ obj:11”。 在DrawInstanced行上,单击obj:<number>文本。 在右侧,您将看到管道的状态。 如果有问题,您可以在此处检查以确保每个管道阶段的状态正确。
在输入装配选项卡上,您将看到例如顶点缓冲区。 您可以单击创建的堆的名称以查看该堆包含的内容。
图形调试器是一个非常有用的工具,我建议您进一步了解它。
当然图形调试工具还有很多,比如RenderDoc等,学会这些图形调试工具对于开始来说是很有帮助的。
整个项目源代码VertexShader.hlsl
float4 main(float3 pos : POSITION) : SV_POSITION { // just pass vertex position straight through return float4(pos, 1.0f); }
PixelShader.hlsl
float4 main() : SV_TARGET { // return green return float4(0.0f, 1.0f, 0.0f, 1.0f); }
stdafx.h
#pragma once #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers. #endif #include <windows.h> #include <d3d12.h> #include <dxgi1_4.h> #include <D3Dcompiler.h> #include <DirectXMath.h> #include "d3dx12.h" #include <string> // this will only call release if an object exists (prevents exceptions calling release on non existant objects) #define SAFE_RELEASE(p) { if ( (p) ) { (p)->Release(); (p) = 0; } } // Handle to the window HWND hwnd = NULL; // name of the window (not the title) LPCTSTR WindowName = L"BzTutsApp"; // title of the window LPCTSTR WindowTitle = L"Bz Window"; // width and height of the window int Width = 800; int Height = 600; // is window full screen? bool FullScreen = false; // we will exit the program when this becomes false bool Running = true; // create a window bool InitializeWindow(HINSTANCE hInstance, int ShowWnd, bool fullscreen); // main application loop void mainloop(); // callback function for windows messages LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // direct3d stuff const int frameBufferCount = 3; // number of buffers we want, 2 for double buffering, 3 for tripple buffering ID3D12Device* device; // direct3d device IDXGISwapChain3* swapChain; // swapchain used to switch between render targets ID3D12CommandQueue* commandQueue; // container for command lists ID3D12DescriptorHeap* rtvDescriptorHeap; // a descriptor heap to hold resources like the render targets ID3D12Resource* renderTargets[frameBufferCount]; // number of render targets equal to buffer count ID3D12CommandAllocator* commandAllocator[frameBufferCount]; // we want enough allocators for each buffer * number of threads (we only have one thread) ID3D12GraphicsCommandList* commandList; // a command list we can record commands into, then execute them to render the frame ID3D12Fence* fence[frameBufferCount]; // an object that is locked while our command list is being executed by the gpu. We need as many //as we have allocators (more if we want to know when the gpu is finished with an asset) HANDLE fenceEvent; // a handle to an event when our fence is unlocked by the gpu UINT64 fenceValue[frameBufferCount]; // this value is incremented each frame. each fence will have its own value int frameIndex; // current rtv we are on int rtvDescriptorSize; // size of the rtv descriptor on the device (all front and back buffers will be the same size) // function declarations bool InitD3D(); // initializes direct3d 12 void Update(); // update the game logic void UpdatePipeline(); // update the direct3d pipeline (update command lists) void Render(); // execute the command list void Cleanup(); // release com ojects and clean up memory void WaitForPreviousFrame(); // wait until gpu is finished with command list ID3D12PipelineState* pipelineStateObject; // pso containing a pipeline state ID3D12RootSignature* rootSignature; // root signature defines data shaders will access D3D12_VIEWPORT viewport; // area that output from rasterizer will be stretched to. D3D12_RECT scissorRect; // the area to draw in. pixels outside that area will not be drawn onto ID3D12Resource* vertexBuffer; // a default buffer in GPU memory that we will load vertex data for our triangle into D3D12_VERTEX_BUFFER_VIEW vertexBufferView; // a structure containing a pointer to the vertex data in gpu memory // the total size of the buffer, and the size of each element (vertex)
main.cpp
#include "stdafx.h" using namespace DirectX; // we will be using the directxmath library struct Vertex { XMFLOAT3 pos; }; int WINAPI WinMain(HINSTANCE hInstance, //Main windows function HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { // create the window if (!InitializeWindow(hInstance, nShowCmd, FullScreen)) { MessageBox(0, L"Window Initialization - Failed", L"Error", MB_OK); return 1; } // initialize direct3d if (!InitD3D()) { MessageBox(0, L"Failed to initialize direct3d 12", L"Error", MB_OK); Cleanup(); return 1; } // start the main loop mainloop(); // we want to wait for the gpu to finish executing the command list before we start releasing everything WaitForPreviousFrame(); // close the fence event CloseHandle(fenceEvent); // clean up everything Cleanup(); return 0; } // create and show the window bool InitializeWindow(HINSTANCE hInstance, int ShowWnd, bool fullscreen) { if (fullscreen) { HMONITOR hmon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); MONITORINFO mi = { sizeof(mi) }; GetMonitorInfo(hmon, &mi); Width = mi.rcMonitor.right - mi.rcMonitor.left; Height = mi.rcMonitor.bottom - mi.rcMonitor.top; } WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.cbClsExtra = NULL; wc.cbWndExtra = NULL; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 2); wc.lpszMenuName = NULL; wc.lpszClassName = WindowName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if (!RegisterClassEx(&wc)) { MessageBox(NULL, L"Error registering class", L"Error", MB_OK | MB_ICONERROR); return false; } hwnd = CreateWindowEx(NULL, WindowName, WindowTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, Width, Height, NULL, NULL, hInstance, NULL); if (!hwnd) { MessageBox(NULL, L"Error creating window", L"Error", MB_OK | MB_ICONERROR); return false; } if (fullscreen) { SetWindowLong(hwnd, GWL_STYLE, 0); } ShowWindow(hwnd, ShowWnd); UpdateWindow(hwnd); return true; } void mainloop() { MSG msg; ZeroMemory(&msg, sizeof(MSG)); while (Running) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } else { // run game code Update(); // update the game logic Render(); // execute the command queue (rendering the scene is the result of the gpu executing the command lists) } } } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_KEYDOWN: if (wParam == VK_ESCAPE) { if (MessageBox(0, L"Are you sure you want to exit?", L"Really?", MB_YESNO | MB_ICONQUESTION) == IDYES) { Running = false; DestroyWindow(hwnd); } } return 0; case WM_DESTROY: // x button on top right corner of window was pressed Running = false; PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); } bool InitD3D() { HRESULT hr; // -- Create the Device -- // IDXGIFactory4* dxgiFactory; hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); if (FAILED(hr)) { return false; } IDXGIAdapter1* adapter; // adapters are the graphics card (this includes the embedded graphics on the motherboard) int adapterIndex = 0; // we'll start looking for directx 12 compatible graphics devices starting at index 0 bool adapterFound = false; // set this to true when a good one was found // find first hardware gpu that supports d3d 12 while (dxgiFactory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND) { DXGI_ADAPTER_DESC1 desc; adapter->GetDesc1(&desc); if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { // we dont want a software device continue; } // we want a device that is compatible with direct3d 12 (feature level 11 or higher) hr = D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr); if (SUCCEEDED(hr)) { adapterFound = true; break; } adapterIndex++; } if (!adapterFound) { return false; } // Create the device hr = D3D12CreateDevice( adapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device) ); if (FAILED(hr)) { return false; } // -- Create a direct command queue -- // D3D12_COMMAND_QUEUE_DESC cqDesc = {}; cqDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; cqDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; // direct means the gpu can directly execute this command queue hr = device->CreateCommandQueue(&cqDesc, IID_PPV_ARGS(&commandQueue)); // create the command queue if (FAILED(hr)) { return false; } // -- Create the Swap Chain (double/tripple buffering) -- // DXGI_MODE_DESC backBufferDesc = {}; // this is to describe our display mode backBufferDesc.Width = Width; // buffer width backBufferDesc.Height = Height; // buffer height backBufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // format of the buffer (rgba 32 bits, 8 bits for each chanel) // describe our multi-sampling. We are not multi-sampling, so we set the count to 1 (we need at least one sample of course) DXGI_SAMPLE_DESC sampleDesc = {}; sampleDesc.Count = 1; // multisample count (no multisampling, so we just put 1, since we still need 1 sample) // Describe and create the swap chain. DXGI_SWAP_CHAIN_DESC swapChainDesc = {}; swapChainDesc.BufferCount = frameBufferCount; // number of buffers we have swapChainDesc.BufferDesc = backBufferDesc; // our back buffer description swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // this says the pipeline will render to this swap chain swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // dxgi will discard the buffer (data) after we call present swapChainDesc.OutputWindow = hwnd; // handle to our window swapChainDesc.SampleDesc = sampleDesc; // our multi-sampling description swapChainDesc.Windowed = !FullScreen; // set to true, then if in fullscreen must call SetFullScreenState with true for full screen to get uncapped fps IDXGISwapChain* tempSwapChain; dxgiFactory->CreateSwapChain( commandQueue, // the queue will be flushed once the swap chain is created &swapChainDesc, // give it the swap chain description we created above &tempSwapChain // store the created swap chain in a temp IDXGISwapChain interface ); swapChain = static_cast<IDXGISwapChain3*>(tempSwapChain); frameIndex = swapChain->GetCurrentBackBufferIndex(); // -- Create the Back Buffers (render target views) Descriptor Heap -- // // describe an rtv descriptor heap and create D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = frameBufferCount; // number of descriptors for this heap. rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; // this heap is a render target view heap // This heap will not be directly referenced by the shaders (not shader visible), as this will store the output from the pipeline // otherwise we would set the heap's flag to D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; hr = device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvDescriptorHeap)); if (FAILED(hr)) { return false; } // get the size of a descriptor in this heap (this is a rtv heap, so only rtv descriptors should be stored in it. // descriptor sizes may vary from device to device, which is why there is no set size and we must ask the // device to give us the size. we will use this size to increment a descriptor handle offset rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); // get a handle to the first descriptor in the descriptor heap. a handle is basically a pointer, // but we cannot literally use it like a c++ pointer. CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart()); // Create a RTV for each buffer (double buffering is two buffers, tripple buffering is 3). for (int i = 0; i < frameBufferCount; i++) { // first we get the n'th buffer in the swap chain and store it in the n'th // position of our ID3D12Resource array hr = swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i])); if (FAILED(hr)) { return false; } // the we "create" a render target view which binds the swap chain buffer (ID3D12Resource[n]) to the rtv handle device->CreateRenderTargetView(renderTargets[i], nullptr, rtvHandle); // we increment the rtv handle by the rtv descriptor size we got above rtvHandle.Offset(1, rtvDescriptorSize); } // -- Create the Command Allocators -- // for (int i = 0; i < frameBufferCount; i++) { hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator[i])); if (FAILED(hr)) { return false; } } // -- Create a Command List -- // // create the command list with the first allocator hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator[frameIndex], NULL, IID_PPV_ARGS(&commandList)); if (FAILED(hr)) { return false; } // -- Create a Fence & Fence Event -- // // create the fences for (int i = 0; i < frameBufferCount; i++) { hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence[i])); if (FAILED(hr)) { return false; } fenceValue[i] = 0; // set the initial fence value to 0 } // create a handle to a fence event fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (fenceEvent == nullptr) { return false; } // create root signature CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ID3DBlob* signature; hr = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, nullptr); if (FAILED(hr)) { return false; } hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)); if (FAILED(hr)) { return false; } // create vertex and pixel shaders // when debugging, we can compile the shader files at runtime. // but for release versions, we can compile the hlsl shaders // with fxc.exe to create .cso files, which contain the shader // bytecode. We can load the .cso files at runtime to get the // shader bytecode, which of course is faster than compiling // them at runtime // compile vertex shader ID3DBlob* vertexShader; // d3d blob for holding vertex shader bytecode ID3DBlob* errorBuff; // a buffer holding the error data if any hr = D3DCompileFromFile(L"VertexShader.hlsl", nullptr, nullptr, "main", "vs_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &vertexShader, &errorBuff); if (FAILED(hr)) { OutputDebugStringA((char*)errorBuff->GetBufferPointer()); return false; } // fill out a shader bytecode structure, which is basically just a pointer // to the shader bytecode and the size of the shader bytecode D3D12_SHADER_BYTECODE vertexShaderBytecode = {}; vertexShaderBytecode.BytecodeLength = vertexShader->GetBufferSize(); vertexShaderBytecode.pShaderBytecode = vertexShader->GetBufferPointer(); // compile pixel shader ID3DBlob* pixelShader; hr = D3DCompileFromFile(L"PixelShader.hlsl", nullptr, nullptr, "main", "ps_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &pixelShader, &errorBuff); if (FAILED(hr)) { OutputDebugStringA((char*)errorBuff->GetBufferPointer()); return false; } // fill out shader bytecode structure for pixel shader D3D12_SHADER_BYTECODE pixelShaderBytecode = {}; pixelShaderBytecode.BytecodeLength = pixelShader->GetBufferSize(); pixelShaderBytecode.pShaderBytecode = pixelShader->GetBufferPointer(); // create input layout // The input layout is used by the Input Assembler so that it knows // how to read the vertex data bound to it. D3D12_INPUT_ELEMENT_DESC inputLayout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } }; // fill out an input layout description structure D3D12_INPUT_LAYOUT_DESC inputLayoutDesc = {}; // we can get the number of elements in an array by "sizeof(array) / sizeof(arrayElementType)" inputLayoutDesc.NumElements = sizeof(inputLayout) / sizeof(D3D12_INPUT_ELEMENT_DESC); inputLayoutDesc.pInputElementDescs = inputLayout; // create a pipeline state object (PSO) // In a real application, you will have many pso's. for each different shader // or different combinations of shaders, different blend states or different rasterizer states, // different topology types (point, line, triangle, patch), or a different number // of render targets you will need a pso // VS is the only required shader for a pso. You might be wondering when a case would be where // you only set the VS. It's possible that you have a pso that only outputs data with the stream // output, and not on a render target, which means you would not need anything after the stream // output. D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; // a structure to define a pso psoDesc.InputLayout = inputLayoutDesc; // the structure describing our input layout psoDesc.pRootSignature = rootSignature; // the root signature that describes the input data this pso needs psoDesc.VS = vertexShaderBytecode; // structure describing where to find the vertex shader bytecode and how large it is psoDesc.PS = pixelShaderBytecode; // same as VS but for pixel shader psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; // type of topology we are drawing psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; // format of the render target psoDesc.SampleDesc = sampleDesc; // must be the same sample description as the swapchain and depth/stencil buffer psoDesc.SampleMask = 0xffffffff; // sample mask has to do with multi-sampling. 0xffffffff means point sampling is done psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // a default rasterizer state. psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // a default blent state. psoDesc.NumRenderTargets = 1; // we are only binding one render target // create the pso hr = device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineStateObject)); if (FAILED(hr)) { return false; } // Create vertex buffer // a triangle Vertex vList[] = { { { 0.0f, 0.5f, 0.5f } }, { { 0.5f, -0.5f, 0.5f } }, { { -0.5f, -0.5f, 0.5f } }, }; int vBufferSize = sizeof(vList); // create default heap // default heap is memory on the GPU. Only the GPU has access to this memory // To get data into this heap, we will have to upload the data using // an upload heap device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), // a default heap D3D12_HEAP_FLAG_NONE, // no flags &CD3DX12_RESOURCE_DESC::Buffer(vBufferSize), // resource description for a buffer D3D12_RESOURCE_STATE_COPY_DEST, // we will start this heap in the copy destination state since we will copy data // from the upload heap to this heap nullptr, // optimized clear value must be null for this type of resource. used for render targets and depth/stencil buffers IID_PPV_ARGS(&vertexBuffer)); // we can give resource heaps a name so when we debug with the graphics debugger we know what resource we are looking at vertexBuffer->SetName(L"Vertex Buffer Resource Heap"); // create upload heap // upload heaps are used to upload data to the GPU. CPU can write to it, GPU can read from it // We will upload the vertex buffer using this heap to the default heap ID3D12Resource* vBufferUploadHeap; device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), // upload heap D3D12_HEAP_FLAG_NONE, // no flags &CD3DX12_RESOURCE_DESC::Buffer(vBufferSize), // resource description for a buffer D3D12_RESOURCE_STATE_GENERIC_READ, // GPU will read from this buffer and copy its contents to the default heap nullptr, IID_PPV_ARGS(&vBufferUploadHeap)); vBufferUploadHeap->SetName(L"Vertex Buffer Upload Resource Heap"); // store vertex buffer in upload heap D3D12_SUBRESOURCE_DATA vertexData = {}; vertexData.pData = reinterpret_cast<BYTE*>(vList); // pointer to our vertex array vertexData.RowPitch = vBufferSize; // size of all our triangle vertex data vertexData.SlicePitch = vBufferSize; // also the size of our triangle vertex data // we are now creating a command with the command list to copy the data from // the upload heap to the default heap UpdateSubresources(commandList, vertexBuffer, vBufferUploadHeap, 0, 0, 1, &vertexData); // transition the vertex buffer data from copy destination state to vertex buffer state commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(vertexBuffer, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER)); // Now we execute the command list to upload the initial assets (triangle data) commandList->Close(); ID3D12CommandList* ppCommandLists[] = { commandList }; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // increment the fence value now, otherwise the buffer might not be uploaded by the time we start drawing fenceValue[frameIndex]++; hr = commandQueue->Signal(fence[frameIndex], fenceValue[frameIndex]); if (FAILED(hr)) { Running = false; } // create a vertex buffer view for the triangle. We get the GPU memory address to the vertex pointer using the GetGPUVirtualAddress() method vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.StrideInBytes = sizeof(Vertex); vertexBufferView.SizeInBytes = vBufferSize; // Fill out the Viewport viewport.TopLeftX = 0; viewport.TopLeftY = 0; viewport.Width = Width; viewport.Height = Height; viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; // Fill out a scissor rect scissorRect.left = 0; scissorRect.top = 0; scissorRect.right = Width; scissorRect.bottom = Height; return true; } void Update() { // update app logic, such as moving the camera or figuring out what objects are in view } void UpdatePipeline() { HRESULT hr; // We have to wait for the gpu to finish with the command allocator before we reset it WaitForPreviousFrame(); // we can only reset an allocator once the gpu is done with it // resetting an allocator frees the memory that the command list was stored in hr = commandAllocator[frameIndex]->Reset(); if (FAILED(hr)) { Running = false; } // reset the command list. by resetting the command list we are putting it into // a recording state so we can start recording commands into the command allocator. // the command allocator that we reference here may have multiple command lists // associated with it, but only one can be recording at any time. Make sure // that any other command lists associated to this command allocator are in // the closed state (not recording). // Here you will pass an initial pipeline state object as the second parameter, // but in this tutorial we are only clearing the rtv, and do not actually need // anything but an initial default pipeline, which is what we get by setting // the second parameter to NULL hr = commandList->Reset(commandAllocator[frameIndex], pipelineStateObject); if (FAILED(hr)) { Running = false; } // here we start recording commands into the commandList (which all the commands will be stored in the commandAllocator) // transition the "frameIndex" render target from the present state to the render target state so the command list draws to it starting from here commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); // here we again get the handle to our current render target view so we can set it as the render target in the output merger stage of the pipeline CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize); // set the render target for the output merger stage (the output of the pipeline) commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); // Clear the render target by using the ClearRenderTargetView command const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f }; commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); // draw triangle commandList->SetGraphicsRootSignature(rootSignature); // set the root signature commandList->RSSetViewports(1, &viewport); // set the viewports commandList->RSSetScissorRects(1, &scissorRect); // set the scissor rects commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // set the primitive topology commandList->IASetVertexBuffers(0, 1, &vertexBufferView); // set the vertex buffer (using the vertex buffer view) commandList->DrawInstanced(3, 1, 0, 0); // finally draw 3 vertices (draw the triangle) // transition the "frameIndex" render target from the render target state to the present state. If the debug layer is enabled, you will receive a // warning if present is called on the render target when it's not in the present state commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); hr = commandList->Close(); if (FAILED(hr)) { Running = false; } } void Render() { HRESULT hr; UpdatePipeline(); // update the pipeline by sending commands to the commandqueue // create an array of command lists (only one command list here) ID3D12CommandList* ppCommandLists[] = { commandList }; // execute the array of command lists commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // this command goes in at the end of our command queue. we will know when our command queue // has finished because the fence value will be set to "fenceValue" from the GPU since the command // queue is being executed on the GPU hr = commandQueue->Signal(fence[frameIndex], fenceValue[frameIndex]); if (FAILED(hr)) { Running = false; } // present the current backbuffer hr = swapChain->Present(0, 0); if (FAILED(hr)) { Running = false; } } void Cleanup() { // wait for the gpu to finish all frames for (int i = 0; i < frameBufferCount; ++i) { frameIndex = i; WaitForPreviousFrame(); } // get swapchain out of full screen before exiting BOOL fs = false; if (swapChain->GetFullscreenState(&fs, NULL)) swapChain->SetFullscreenState(false, NULL); SAFE_RELEASE(device); SAFE_RELEASE(swapChain); SAFE_RELEASE(commandQueue); SAFE_RELEASE(rtvDescriptorHeap); SAFE_RELEASE(commandList); for (int i = 0; i < frameBufferCount; ++i) { SAFE_RELEASE(renderTargets[i]); SAFE_RELEASE(commandAllocator[i]); SAFE_RELEASE(fence[i]); }; SAFE_RELEASE(pipelineStateObject); SAFE_RELEASE(rootSignature); SAFE_RELEASE(vertexBuffer); } void WaitForPreviousFrame() { HRESULT hr; // swap the current rtv buffer index so we draw on the correct buffer frameIndex = swapChain->GetCurrentBackBufferIndex(); // if the current fence value is still less than "fenceValue", then we know the GPU has not finished executing // the command queue since it has not reached the "commandQueue->Signal(fence, fenceValue)" command if (fence[frameIndex]->GetCompletedValue() < fenceValue[frameIndex]) { // we have the fence create an event which is signaled once the fence's current value is "fenceValue" hr = fence[frameIndex]->SetEventOnCompletion(fenceValue[frameIndex], fenceEvent); if (FAILED(hr)) { Running = false; } // We will wait until the fence has triggered the event that it's current value has reached "fenceValue". once it's value // has reached "fenceValue", we know the command queue has finished executing WaitForSingleObject(fenceEvent, INFINITE); } // increment fenceValue for next frame fenceValue[frameIndex]++; }
参考链接:
- https://docs.microsoft.com/en-us/windows/win32/direct3d12/directx-12-programming-guide
- http://www.d3dcoder.net/
- https://www.braynzarsoft.net/viewtutorial/q16390-04-directx-12-braynzar-soft-tutorials
- https://developer.nvidia.com/dx12-dos-and-donts
- https://www.3dgep.com/learning-directx-12-1/
- https://gpuopen.com/learn/lets-learn-directx12/
- https://alain.xyz/blog/raw-directx12
- https://www.rastertek.com/tutdx12.html
- https://digitalerr0r.net/2015/08/19/quickstart-directx-12-programming/
- https://walbourn.github.io/getting-started-with-direct3d-12/
- https://docs.aws.amazon.com/lumberyard/latest/userguide/graphics-rendering-directx.html
- http://diligentgraphics.com/diligent-engine/samples/
- https://www.programmersought.com/article/2904113865/
- https://www.tutorialspoint.com/directx/directx_first_hlsl.htm
- http://rbwhitaker.wikidot.com/hlsl-tutorials
- https://digitalerr0r.net/2015/08/19/quickstart-directx-12-programming/
- https://www.ronja-tutorials.com/post/002-hlsl/
开始使用DirectX 12进行渲染画图
原创
©著作权归作者所有:来自51CTO博客作者嘿克不黑的原创作品,请联系作者获取转载授权,否则将追究法律责任
下一篇:Direct3D 12简介
提问和评论都可以,用心的回复会被更多人看到
评论
发布评论
相关文章
-
python 捕获dx12游戏
1、下载Sublime和Python,安装Python环境注意:如果不想动手亲自配置Python环境安装的时候环境变量,请在安装的界面给Add Python 3.5 To Path前面打上对号。 可以在Python官方网站下载到Python的Windows安装程序(exe文件),需要注意的是如果在Windows 7环境下安装Python 3.x,需要先安装Service
python 捕获dx12游戏 directx最终用户运行时 python中文件路径格式 python开发通达信dll shell 运行python文件路径