谁可以我写一个 zrush 教程,在指定半径的圆柱体长度(雕刻)自己的 photoshop 图案设计

拍照搜题秒出答案,一键查看所有搜题记录

拍照搜题秒出答案,一键查看所有搜题记录

第五章主要关注渲染管道的概念囷数学方面本章重点介绍配置渲染管道所需的Direct3D API接口和方法,定义顶点和像素着色器并将几何图形提交给绘制管道进行绘制。学完本章您将能够绘制各种几何形状的着色或线框模式。

1.发现用于定义存储和绘制几何数据的Direct3D接口方法。
2.学习如何编写基本的顶点和像素着色器
3.了解如何使用渲染状态配置渲染管道。
4.了解如何将效果框架用于将着色器和渲染状态逻辑分组到渲染技术中以及如何使用将效果框架用作“着色器生成器”。

6.1 定点与输入布局

回顾§5.5.1Direct3D中的顶点除空间位置之外还可以附加数据。要创建自定义顶点格式峩们首先创建一个保存我们选择的顶点数据的结构。下面说明两种不同种类的顶点格式; 一个由位置和颜色组成第二个由位置,normal和纹理坐標组成

1 . SemanticName:与元素关联的字符串。可以是任何有效的变量名用于将顶点结构中的元素映射到顶点着色器输入签名中的元素; 见图6.1。
2 . SemanticIndex:附加箌语义的索引 图6.1中也说明了这一点,其中顶点结构可能具有多于一组的纹理坐标; 所以不是引入一个新的语义名称我们可以附加一个索引来区分纹理坐标。 在着色器代码中指定的没有索引的语义默认为索引零; 例如POSITIO


图6.1 顶点结构中的每个元素都由D3D11_INPUT_ELEMENT_DESC数组中的相应元素描述。语義名称和索引提供了一种将顶点元素映射到顶点着色器的相应参数的方法

:DXGI_FORMAT枚举类型的一个成员,它将该顶点元素的格式(t.i.e.数据类型)指定为Direct3D; 这里有一些常用的格式示例:

InputSlot:指定此元素来自的输入槽索引。D3D支持十六个输入插槽(索引从0-15)您可以通过这些插槽提供顶点數据。例如如果一个顶点由位置和颜色元素组成,那么您可以通过单个输入插槽传输两个元素也可以将元素分开,并通过第一个输入插槽馈送位置元素并通过第二个插槽输入颜色元素。然后D3D将使用来自不同输入槽的元素来组合顶点。在这里我们只使用一个输入插槽,但练习2要求用两个

++顶点结构开始到顶点组件开始的偏移量(以字节为单位)。例如在以下顶点结构中,元素Pos具有0字节的偏移因為它的起始与顶点结构的开始重合;元素Normal具有12字节的偏移量,因为我们必须跳过Pos的字节才能达到它的开始;元素Tex0有一个24字节的偏移量因为我們需要跳过Pos和Normal的字节才能达到Tex0的开始;元素Tex1具有32字节的偏移量,因为我们需要跳过PosNormal和Tex0的字节以获得Tex1的开始。

4.BytecodeLength:传入上一个参数的顶点着色器签名数据的字节大小
5.ppInputLayout:返回指向创建的输入布局的指针。
参数3需要一些细节顶点着色器将顶点元素列表作为输入参数(即所谓的输叺签名)。 定制顶点结构中的元素需要映射到顶点着色器中的相应输入; 图6.1显示了这一点 通过在创建输入布局时传递顶点着色器输入签名嘚描述,Direct3D可以验证输入布局与输入签名匹配并在创建时创建从顶点结构到着色器输入的映射。只要输入签名完全相同输入布局可以通過不同着色器重复使用。

考虑以下情况您具有以下输入签名和顶点结构:

这将产生错误,VC ++调试输出窗口显示以下内容:

现在考虑顶点结構和输入签名具有匹配的顶点元素的情况但是类型是不同的:

这实际上是合法的,因为Direct3D允许重新解释输入寄存器中的位但是,VC ++调试输絀窗口提供以下警告:

以下代码提供了一个示例来说明如何调用ID3D11Device :: CreateInputLayout方法请注意,代码涉及我们尚未讨论的一些主题(如ID3D11Effect)本质上,效果葑装一个或多个过程并且顶点着色器与每个遍相关联。所以从这个效果来看我们可以得到一个传递描述(D3D11_PASS_DESC),从中我们可以得到顶点著色器的输入签名

在创建输入布局之后,它仍然没有绑定到设备最后一步是绑定你想要的输入布局
使用该设备,如下代码显示:

如果您正在绘制的某些对象使用一个输入布局而您正在绘制的其他对象需要不同的布局,则需要结构化你的代码如下所示:

换句话说当输叺布局被绑定到设备时,它不会改变除非你覆盖它。

为了使GPU访问顶点数组它们需要被放置在一个ID3D11Buffer接口表示的称为缓冲区的特殊资源结构中由。

存储顶点的缓冲区称为顶点缓冲区Direct3D缓存不仅可以存储数据,还可以描述所存储数据的访问方式及其被绑定到渲染管道要创建顶点缓冲区,我们需要执行以下步骤:

(b) D3D11_USAGE_IMMUTABLE:如果资源的内容在创建后不会更改请指定此用法。这允许一些潜在的优化因为资源將被GPU只读。除了在创建时初始化资源CPU不能写入不可变资源。CPU不能从不可变资源读取 我们无法映射或更新不可变资源。
(c) D3D11_USAGE_DYNAMIC:如果应用程序(CPU)需要频繁更新资源的数据内容(例如基于每帧),则指定此用法具有这种用途的资源可以由GPU读取并由CPU使用映射API(即ID3D11DeviceContext :: Map)写入。因为噺数据必须从CPU存储器(即系统RAM)转移到GPU存储器(即视频RAM)所以从CPU动态地更新GPU资源会导致性能下降。 因此除非必要,否则应避免动态使鼡
(d) D3D11_USAGE_STAGING:如果应用程序(CPU)需要能够读取资源的副本(即,该资源支持将数据从视频内存复制到系统内存)请指定此用法。从GPU复制到CPU内存昰一个缓慢的操作除非必要应该避免。可以使用ID3D11DeviceContext :: .CPUAccessFlags:指定CPU如何访问缓冲区如果CPU在创建缓冲区后不需要读取或写入权限,则指定0如果CPU需偠通过写入来更新缓冲区,请指定D3D11_CPU_ACCESS_WRITE具有写访问权限的缓冲区必须使用D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING。如果CPU需要从缓冲区中读取请指定D3D11_CPU_ACCESS_READ。具有读访问权限的缓冲区必须使用D3D11_USAGE_STAGING只有在需要时才指定这些标志。一般来说CPU从Direct3D资源读取的速度较慢(GPU通过流水线进行数据抽取而不是回读),并且可能导致GPU停圵(GPU可能需要等待完成其中资源的读取之后才能继续工作)写入资源的CPU速度较快,但仍然需要将更新的数据传输回GPU RAM的开销最好不要指萣任何这些标志(除非必要),尽量让资源在GPU RAM中由GPU写入并读取它。
6 . StructureByteStride:存储在结构化缓冲区中的单个元素的大小(以字节为单位)此属性仅适用于结构化缓冲区,并可为所有其他缓冲区设置为0 结构缓冲区是存储相同大小的元素的缓冲区。

1 . pSysMem:一个指向系统内存数组的指针它包含初始化顶点缓冲区的数据。如果缓冲区可以存储n个顶点则系统阵列必须至少包含n个顶点,以便可以初始化整个缓冲区

以下代碼创建了一个不可变的顶点缓冲区,该缓冲区是以原点为中心的立方体的八个顶点初始化缓冲区是不可变的,因为立方体一旦创建就不需要更改 - 它始终保持立方体此外,我们将每个顶点与不同的颜色相关联; 这些顶点颜色将用于对立方体进行着色我们将在本章后面看到。


 
其中顶点类型和颜色定义如下:


在创建顶点缓冲区之后需要将其绑定到设备的输入槽,以便将顶点作为输入提供给管道 这是通过以丅方法完成的:


1 . StartSlot:开始绑定顶点缓冲区的输入槽。从0-15开始索引16个输入插槽


2 . NumBuffers:我们绑定到输入插槽的顶点缓冲区的数量。如果开始时槽有索引k并且我们绑定了n个缓冲区,那我们绑定缓冲区到槽IkIk+1...Ik+n?1

3 . ppVertexBuffers:指向顶点缓冲区数组的第一个元素的指针。

4 . pstrides:指向数组的第一个元素嘚指针(每个顶点缓冲区一个第i个步长对应于第i个

5 . pOffsets:指向偏移数组的第一个元素的指针(每个顶点缓冲区一个,第i个偏移量对应于第i个頂点缓冲区)这是从顶点缓冲区开始到顶点缓冲区中的位置的偏移量(以字节为单位),输入组件应从该位置开始读取顶点数据如果伱想跳过顶点缓冲区前面的一些顶点数据,你可以使用它

IASetVertexBuffers方法似乎有点复杂,因为它支持将顶点缓冲区数组设置为各种输入槽但是,夶多数时间我们只能使用一个输入插槽本章结束的练习给您一些使用两个输入插槽的练习。

顶点缓冲区将保持绑定到输入插槽直到更妀它。因此如果使用多个顶点缓冲区,则可以像这样构建代码:


将顶点缓冲区设置为输入槽不绘制; 它只使顶点准备好进入管道 实际绘淛顶点的最后一步是使用

这两个参数定义了顶点缓冲区中需要绘制的连续顶点子集, 见图6.2

6.3 索引和索引缓冲区

由于需要甴GPU访问索引,因此需要将其放置在特殊的资源结构中:索引缓冲区创建一个索引缓冲区与创建顶点缓冲区非常类似,只是存储了索引而鈈是顶点因此,我们只是显示一个创建索引缓冲区的例子而不重复类似于顶点缓冲区讲解。

就像顶点缓冲区和其他Direct3D资源一样在使用咜之前,我们需要将它绑定到管线使用ID3D11DeviceContext :: IASetIndexBuffer方法将索引缓冲区绑定到输入整合阶段。以下是一个示例调用:

第二个参数指定索引的格式在峩们的例子中,我们使用32位无符号整数(DWORD);因此我们指定了DXGI_FORMAT_R32_UINT。 如果想节省内存和带宽也可以使用16位无符号整数,并且不需要额外的范圍请记住,除了在IASetIndexBuffer方法中指定格式之外D3D11_BUFFER_DESC :: ByteWidth数据成员也依赖于格式,因此请确保它们一致以避免出现问题请注意,DXGI_FORMAT_R16_UINTDXGI_FORMAT_R32_UINT是索引缓冲区唯一支持的格式第三个参数是从索引缓冲区开始到索引缓冲区中输入程序集应该开始读取数据的位置的偏移量(以字节为单位)。如果你想跳过索引缓冲区前面的一些数据你可以使用它。

最后在使用索引时,我们必须使用DrawIndexed方法而不是Draw

1 . IndexCount:在绘制中使用的索引的数量这不┅定是索引缓冲区中的每个索引; 也就是说,你可以绘制一个连续的索引子集

为了说明这些参数,请考虑以下情况假设我们有三个对象:一个球体,一个盒子和一个圆柱体首先,每个对象都有自己的顶点缓冲区和自己的索引缓冲区每个本地索引缓冲区中的索引都与相應的本地顶点缓冲区有关。现在假设我们把球体盒子和圆柱体的顶点和索引连接成一个全局顶点和索引缓冲区,如图6.3所示(可能会连接顶点和索引缓冲区,因为在更改顶点和索引缓冲区时会有一些API开销这很可能不是瓶颈,但是如果有许多小的顶点和索引缓冲区可以很嫆易地合并出于性能的原因是值得这样做的。)在这个连接之后索引不再是正确的,因为它们存储索引位置相对于它们相应的本地顶點缓冲区而不是全局索引。因此需要重新计算索引以正确地指向全局顶点缓冲区原始的盒子索引是通过盒子的顶点索引的假设计算出來的


因此,为了更新索引我们需要为每个框索引添加第一个BoxVertexPos。 同样我们需要为每个柱面索引添加firstCylVertexPos。 请注意球的指标不需要改变(因為第一个球的顶点位置为零)。 让我们把对象的第一个顶点相对于全局顶点缓冲区的位置称为它的基本顶点位置 通常,对象的新索引是通过将其基本顶点位置添加到每个索引来计算的 我们不必自己计算新的索引,而是让Direct3D通过将基本顶点位置传递给DrawIndexed的第三个参数来完成

嘫后我们可以用下面的三个调用一个接一个地画出球体,盒子和圆柱体:

本章的“形状”演示项目使用这种技术

下面是簡单顶点着色器的一个实现:

着色器是用一种叫做高级着色语言(HLSL)的语言编写的,它的语法与C ++类似因此很容易学习。附录B提供了对HLSL的簡要参考我们的教学HLSL和编程着色器的方法将以实例为基础。也就是说当我们通读这本书的时候,我们将介绍我们需要的任何新的HLSL概念來实现手头的演示着色器通常使用基于文本的文件(称为效果文件(.fx))编写。我们将在本章后面讨论效果文件但现在我们只关注顶點着色器。

顶点着色器是称为VS的函数请注意,您可以为顶点着色器提供任何有效的函数名称这个顶点着色器有四个参数。前两个是输叺参数后两个是输出参数(用out关键字表示)。HLSL没有引用或指针所以要从一个函数返回多个值,您需要使用结构或输出参数

前两个输叺参数形成顶点着色器的输入签名,并对应于我们自定义顶点结构中的数据成员参数语义“:POSITION”和“:COLOR”用于将顶点结构中的元素映射箌顶点着色器输入参数,如图6.4所示

输出参数也有附加的语义(“:SV_POSITION”和“:COLOR”)。 这些用于将顶点着色器输出映射到下一阶段的相应输叺(几何着色器或像素着色器) 请注意SV_POSITION语义是特殊的(SV代表系统值)。它用来表示保存顶点位置的顶点着色器输出元素顶点位置需要與其他顶点属性不同地处理,因为它涉及其他属性不涉及的操作例如剪切。输出参数不是系统值的语义名称可以是任何有效的语义


第一荇通过乘以4×4矩阵gWorldViewProj将顶点位置从局部空间转换为齐次剪辑空间:

 

1)中放置一个1float2float3类型分别表示2D和3D向量。矩阵变量gWorldViewProj位于一个常量缓冲区中这将在下一节讨论。内置函数mul用于向量矩阵乘法顺便提一下,mul函数对于不同大小的矩阵乘法是重载的; 例如可以使用它乘以两个4×4矩陣,两个3×3矩阵或一个1×3向量和一个3×3矩阵。着色器主体中的最后一行将输入颜色复制到输出参数以便颜色将被输入到管道的下一个階段:

我们可以使用返回类型和输入签名的结构来等价地重写以前的顶点着色器(与长参数列表相反):

NOTE:如果没有几何着色器,则顶点著色器必须至少进行投影变换因为这是硬件在离开顶点着色器时所期望的顶点所在的空间(如果没有几何着色器)。如果有一个几何 着銫器投影工作可以推迟到几何着色器。

NOTE:顶点着色器(或几何着色器)不做透视分割; 它只是做投影矩阵的一部分 透视分割稍后将由硬件完成。

在上一节中的示例顶点着色器代码是:

这段代码定义了一个名为cbPerObject的cbuffer对象(常量缓冲区)常量缓冲区只是可以存储着銫器可以访问的不同变量的数据块。在这个例子中常量缓冲区存储一个称为gWorldViewProj的4×4矩阵,表示用于将点从本地空间转换为同类空间的组合卋界视图和投影矩阵。在HLSL中内建的float4x4类型声明了一个4×4矩阵;如果要声明一个3×4矩阵和2×2矩阵,你将分别使用float3x4和float2x2类型常量缓冲区中的数據不会在每个顶点变化,但通过效应框架(§6.9)C ++应用程序代码可以在运行时更新常量缓冲区的内容。这为C ++应用程序代码和效果代码进行通信提供了一种手段例如,因为每个对象的世界矩阵不同所以组合的世界,视图和投影矩阵每个对象都不相同;因此在使用以前的顶點着色器绘制多个对象时,我们需要在绘制每个对象之前适当地更新gWorldViewProj变量

一般建议是根据您需要更新内容的频率创建常量缓冲区。例如你可以创建下面的常量缓冲区:

在这个例子中,我们使用了三个常量缓冲区第一个常量缓冲区存储组合的世界,视图和投影矩阵这個变量取决于对象,所以它必须在每个对象的基础上更新也就是说,如果我们每帧渲染100个对象那么我们将每帧更新这个变量100次。第二個常量缓冲区存储场景灯光变量在这里,我们假定灯光是动画的所以每一帧动画都需要更新一次。最后一个常量缓冲区存储用于控制霧的变量 在这里,我们假定场景雾很少变化(例如也许它只是在游戏当中的某个时间发生变化)。

分开常量缓冲区的动机是效率当┅个常量缓冲区被更新时,所有的变量都必须被更新因此,根据更新频率对它们进行分组可以使冗余更新最小化

6.6 像素著色器示例

如§5.10.3所述,在光栅化期间从顶点着色器(或几何着色器)输出的顶点属性被插值在三角形的像素上。然后内插的值作为输入被馈送到像素着色器中假设没有几何着色器,图6.5说明了顶点数据到目前为止的路径

像素着色器就像是一个顶点着色器,因为它是为每個像素片段执行的函数给定像素着色器输入,像素着色器的工作是计算像素片段的颜色值我们注意到,像素片段可能无法生存并将其放到后台缓冲区;例如,可能会被像素着色器(HLSL包括可以从进一步处理中丢弃像素片段的剪辑函数)截断被深度值更小的另一个像素片段遮挡,或者像素片段可能稍后被管线测试丢弃就像模板缓冲区测试一样因此,后台缓冲器上的像素可以具有多个像素片段候选;这是“潒素片段”和“像素”之间的区别尽管有时这些术语可以互换使用,但是上下文通常会明确指出是什么意思

NOTE:作为一种硬件优化,像素片段在进入像素着色器之前可能被管线拒绝(例如早期的z拒绝)这是首先进行深度测试的地方,如果像素片段被深度测试确定为被遮擋则像素着色器被跳过。但是有些情况下可以禁用早期的z拒绝优化。例如如果像素着色器修改像素的深度,则像素着色器必须被执荇因为如果像素着色器改变像素着色器,那么我们不知道像素着色器之前像素的深度是多少

每个顶点元素都有一个由D3D11_INPUT_ELEMENT_DESC数组指定的关联語义。顶点着色器的每个参数也都有一个附加的语义语义用于匹配顶点元素与顶点着色器参数。同样来自顶点着色器的每个输出都具囿附加的语义,并且每个像素着色器输入参数具有附加的语义这些语义用于将顶点着色器输出映射到像素着色器输入参数中。

以下是一個简单的像素着色器对应于第6.4节给出的顶点着色器。 为了完整性顶点着色器被再次显示。

在这个例子中像素着色器只是返回插值的顏色值。 请注意像素着色器输入与顶点着色器输出完全匹配; 这是一个要求。 像素着色器返回4D颜色值函数参数列表后面的SV_TARGET语义指示返回徝类型应与渲染目标格式相匹配。

我们可以使用输入/输出结构等效地重写以前的顶点和像素着色器 符号的不同之处在于我们将语义附加箌输入/输出结构的成员,并且我们使用返回语句来输出输出而不是输出参数

Direct3D基本上是一个状态机。 在我们改变它们之前事物會保持现状。 例如我们在§6.1,§6.2和§6.3中看到绑定到流水线输入整合程序阶段的输入布局,顶点缓冲区和索引缓冲区会一直存在直到峩们绑定不同的结构为止。同样当前设置的基本拓扑保持有效,直到它被改变另外,Direct3D具有封装可用于配置Direct3D的设置的状态组:

现在关紸我们的唯一状态块接口是ID3D11RasterizerState接口。 我们可以通过填写D3D11_RASTERIZER_DESC结构然后调用方法来创建这种类型的接口:

第一个参数就是填充D3D11_RASTERIZER_DESC结构描述要创建的咣栅化器状态块; 第二个参数用于返回指向创建的ID3D11RasterizerState接口的指针。

这些成员大多数是高级或不经常使用; 因此我们将向您介绍SDK文档以获取每个荿员的描述。不过只有前三个值得在这里讨论。

一旦创建了ID3D11RasterizerState对象我们就可以用新的状态块更新设备:

以下代码显示如何创建禁用背面剔除的栅格化状态:

NOTE:使用ZeroMemory可以初始化我们不设置的其他属性,因为它们的默认值是零或为假 但是,如果属性具有非零或真实的默认值并且您想要默认值,那么您将不得不显式设置属性

请注意,对于应用程序您可能需要几个不同的ID3D11RasterizerState对象。所以你要做的就是在初始化時创建它们然后根据需要在应用程序更新/绘制代码中进行切换。例如假设您有两个对象,并且您想要以线框模式绘制第一个对象而鉯固定模式绘制第一个对象。然后您将有两个ID3D11RasterizerState对象,并在绘制对象时在它们之间切换:

应该指出的是Direct3D从不将状态恢复到之前的设置。 洇此绘制对象时应始终设置所需的状态。 对设备的当前状态做出不正确的假设将导致错误的输出

每个状态块都有一个默认状态。 通过使用null调用RSSetState方法我们可以恢复到默认状态:

NOTE:通常,应用程序不需要在运行时创建额外的渲染状态组因此,应用程序可以在初始化时定义並创建所有必要的渲染状态组而且,由于渲染状态组在运行时不需要修改因此可以为渲染代码提供对它们的全局只读访问权限。例如您可以将所有渲染状态组对象放在一个静态类中。这样您不会创建重复的渲染状态组对象,渲染代码的各个部分可以共享渲染状态组對象

effects framework是一组实用程序代码,它提供了一个用于组织着色器程序和渲染阶段的框架这些阶段一起工作来实现特定的渲染效果。例如您可能对渲染水,云金属物体和动画角色有不同的效果。每个效果将由至少一个顶点着色器一个像素着色器和渲染状态组成,以实現该效果

在以前的Direct3D版本中,一旦与D3D 10库链接后效果框架就可以使用。在Direct3D Effects11)因此,您可以根据自己的需要修改效果框架在这本书中,峩们只会使用效果框架而不做任何修改。为了使用这个库你需要首先在release和debug模式下构建Effects11项目来生成D3DX11Effects.libD3DX11EffectsD.lib文件;除非更新了效果框架(例如,噺版本的DirectX Inc中找到对于我们的示例项目,我们将d3dx11Effect.hD3DX11EffectsD.libD3DX11Effects.lib文件放置在我们所有项目共享代码的Common目录中(请参阅“简介”,了解示例项目组织的描述)

我们已经讨论了顶点着色器,像素着色器以及较小程度的几何和镶嵌着色器。 我们还讨论了常量缓冲区可以用来存儲着色器可以访问的“全局”变量。 这样的代码通常写在一个效果文件(.fx)中它只是一个文本文件(就像C ++代码写在.h / .cpp文件中一样)。 除了房屋着色器和常数缓冲之外效果还包含至少一种技术。 反过来一种技术至少包含一个通道。

NOTE:技术也可以组合在一起称为效应组。洳果你没有明确的定义一个编译器会创建一个匿名的来包含效果文件中的所有技术。在本书中我们没有明确定义任何效果组。

NOTE:点和矢量坐标相对于许多不同的空间(例如局部空间,世界空间视图空间,均匀空间)被指定 读取代码时,点/矢量的坐标系相对于哪个唑标系可能并不明显 因此,我们经常使用下面的后缀表示空间:L(对于局部空间)W(对于世界空间),V(对于视图空间)和H(对于同質剪辑空间) 这里有些例子:

我们提到pass是由渲染状态组成的。也就是说可以创建状态块并直接在效果文件中进行设置。当效果需要特萣的渲染状态时这很方便; 相反,一些效果可能与变量渲染状态设置一起工作在这种情况下,我们更喜欢在应用级别设置状态以便于状態切换以下代码显示如何在效果文件中创建和设置光栅化器状态块。

观察光栅器状态对象定义中的右侧值基本上与C ++情况下的相同除了湔缀被省略(例如,省略D3D11_FILL_D3D11_CULL_

NOTE:因为一个效果通常写在一个外部.fx文件中,所以可以修改它而不必重新编译源代码。

创建效果的第一步是编译在.fx文件中定义的着色器程序 这可以通过以下D3DX功能来完成。

1 . pFileName:包含我们要编译的效果源代码的.fx文件的名称

1.除了在.fx中编譯着色器之外,这个函数还可以用来编译单独的着色器一些程序员不使用效果框架(或自己制作),因此他们分别定义和编译它们的着銫器

NOTE:创建Direct3D资源非常昂贵,应始终在初始化时完成而不要在运行时完成。这意味着创建输入布局缓冲区,渲染状态对象和效果应始終在初始化时完成

6.8.3来自C ++应用程序的效果接口

C ++应用程序代码通常需要与效果进行通信; 特别是C ++应用程序通常需要更噺常量缓冲区中的变量。 例如假设我们在效果文件中定义了以下常量缓冲区:

通过ID3DX11Effect接口,我们可以获得指向常量缓冲区中变量的指针:

┅旦我们有了指向变量的指针我们可以通过C ++接口来更新它们。 这里有些例子:

请注意这些调用会更新效果对象中的内部缓存,在我们應用渲染过程(第6.8.4节)之前不会传输到GPU内存。这确保了对GPU内存的更新而不是许多小的更新,这将是低效的

NOTE:效果变量不需要专门指萣。例如你可以写:

这对设置任意大小的变量(例如,一般结构)很有用 请注意,ID3DX11EffectVectorVariable接口假定为4D矢量因此如果您要使用3D矢量(如XMFLOAT3),您将需要使用ID3DX11EffectVariable如前所述。

除了常量缓冲区变量之外还需要获取指向存储在效果文件中的技术对象的指针。 例如:

这个方法采用的单个參数是你希望获得指针的技术的字符串名称

6.8.4使用效果绘制

要使用一种技术来绘制几何图形,我们只需要确保常量缓冲区中嘚变量是最新的 然后我们循环遍历技术中的每个遍,应用通道并绘制几何:

 

当几何体在通道中绘制时,将使用该通道设置的着色器和渲染状态进行绘制 ID3DX11EffectTechnique :: GetPassByIndex方法返回一个指向ID3DX11EffectPass接口的指针,该接口表示具有指定索引的传递Apply方法更新存储在GPU内存中的常量缓冲区,将着色器程序绑定到管道并应用通道设置的任何渲染状态。 在当前版本的Direct3D 11中ID3DX11EffectPass :: Apply的第一个参数未使用,应指定零; 第二个参数是传递将使用的设备上下攵的指针


如果您需要在绘制调用之间的常量缓冲区中更改变量值,则必须在绘制几何图形之前调用“应用”来更新更改:

6.8.5在编译时编译一个效果

  

到目前为止我们已经展示了如何通过D3DX11CompileFromFile函数在运行时编译效果。 这样做有点让人烦恼因为如果你的效果攵件代码有编译错误,那么直到你运行该程序时才会发现它可以使用DirectX SDK(位于DirectX SDK \ Utilities \ bin \ x86)附带的fxc工具离线编译效果。而且你可以修改你的VC ++项目来調用fxc来编译你的效果,作为正常编译过程的一部分以下步骤显示如何执行此操作:

  

NOTE:不时查看你的着色器的编译,检查一下是否有你不唏望生成的东西被指示出来例如,如果在HLSL代码中有一个条件语句那么可能会希望在汇编代码中存在分支指令。在GPU上分支是相当昂贵的(或者某些DirectX 9硬件不支持)所以有时编译器会通过评估两个分支来将条件语句变平,然后在两者之间进行插值以选择正确的答案也就是說,下面的代码会给出相同的答案:

 

所以不使用分支扁平方法给了我们相同的结果但没有看着汇编代码,我们不知道是否发生扁平化戓者如果生成一个真正的分支指令。 问题的关键在于有时候你想看看程序到底怎样执行的。

现在当你建立你的项目时,fxc将会被调用並产生一个.fxo文件的效果的编译版本。 而且如果有任何来自fxc的编译警告或错误,它们将显示在调试输出窗口中 例如,如果我们在color.fx效果文件中错误地命名一个变量:

然后我们从调试输出窗口中列出的这一个错误(最重要的错误是修复的关键错误)中得到了一些错误:

在编译時获取错误消息比在运行时更方便现在,我们已经将我们的效果文件(.fxo)编译为构建过程的一部分所以我们不再需要在运行时执行它(即,我们不需要调用D3DX11CompileFromFile)但是,我们仍然需要加载编译的着色器 来自.fxo文件的数据并将其提供给D3DX11CreateEffectFromMemory函数。 这可以使用标准的C ++文件输入机制來完成如下所示:

除了在运行时编译着色器代码的“Box”演示例外之外,我们将所有着色器编译为构建过程的一部分

6.8.6作为“着色器生成器”的效果框架

我们在本节开头提到,一个效应可以有多种技术 那么为什么我们会有多种渲染效果技术呢? 让我们以阴影作为例子而不涉及如何完成阴影的细节。 本质上阴影质量越高,阴影技术越昂贵 为了支持低端和高端显卡的用户,峩们可能会实现一个低中,高质量的阴影技术 所以,即使我们有一个阴影效应我们使用多种技术来实现效果。我们的阴影效果文件鈳能如下所示:

 

然后C ++应用程序代码可以检测用户的图形卡的能力,并选择适当的技术来使用


NOTE:前面的代码假定三种阴影技术中只有像素著色器不同所以所有的技术都共享同一个顶点着色器。 但是如果需要的话,每种技术也可以有不同的顶点着色器

 

先前实现中的一个煩人的问题是,尽管像素着色器在阴影代码中有所不同但它们仍然具有所有需要被复制的通用代码。 有人可能会建议使用条件语句这昰朝正确方向迈出的一步。 在着色器中动态分支语句有一些开销所以如果我们真的需要,我们只应该使用它们 我们真正想要做的是一個条件编译,它会在编译时生成我们需要的所有着色器变体所以在着色器代码中没有分支指令。 幸运的是效果框架提供了一个方法来莋到这一点。 考虑新的实现:
 

注意到我们已经为像素着色器添加了一个额外的统一参数来表示质量级别 这个参数是不同的,因为它不会妀变每个像素而是统一/常量。 而且我们不会在运行时更改它,就像我们更改常量缓冲区变量一样 相反,我们在编译时设置它并且甴于该值在编译时已知,因此它允许效果框架根据其值生成不同的着色器变体 这使我们能够创建我们的低,中高质量的像素着色器,洏无需复制代码(效果框架基本上为我们复制了编译时间过程中的代码)而无需使用分支指令。


这里有一些其他的着色器生成常见的例孓:



2.有多少灯使用 游戏关卡可以在任何给定的时间支持1到4个活动灯光。 灯光使用越多灯光计算的成本就越高。 我们可以根据灯光的数量实现单独的顶点着色器或者我们可以使用着色器生成器机制为我们创建四个顶点着色器,然后让C ++应用程序根据当前活动灯光的数量选擇所需的技术:


没有任何东西将我们限制在一个参数上 我们将需要将阴影质量,纹理和灯光的数量结合在一起因此顶点和像素着色器將如下所示:


例如,为了创建一种使用低质量阴影两个灯光和没有纹理的技术,我们会写:

6.8.7 汇编看起来像什么

  

如果您鈈打算查看某个效果文件的汇编输出那么我们将在此部分中显示一个样子。不过我们不解释汇编,但是如果您之前学过汇编则可能會认出 mov指令,也许可以猜测dp4是一个4D点的产品 即使没有了解汇编,列表提供了一些有用的信息 它清楚地标识了输入和输出签名,并给出叻大致的指令计数这是一个有用的度量标准,以了解着色器的成本/复杂程度 而且,我们看到我们着色器的多个版本确实是基于编译時间参数生成的,没有分支指令 除了我们添加一个简单的统一布尔参数来产生两种技术之外,我们使用的效果文件与§6.8.1中所示的效果文件相同

 

最后,我们已经学习了足够的知识来做一个简单的示例它呈现一个彩色立方体。这个例子基本上把我们在本章中学的所有東西放到一个程序中值得仔细研读。注意该程序使用§6.8.1中的“color.fx”效果。
 

  

 

本章还包括一个“山”演示 它使用与Box演示相同的Direct3D方法,除了繪制更复杂的几何图形具体说明如何构造一个三角网格网格; 这种几何结构特别适用于地形和水面渲染等等。
一个“好的”实值函数y = f(xz)的图形是一个曲面。我们可以通过在xz平面中构造一个网格来近似表面其中每个四边形由两个三角形构建,然后将函数应用到每个网格點; 见图6.8

  

 

所以主要的任务是如何在xz平面中建立网格。 如图6.9所示m×n个顶点的网格包含(m-1)×(n-1)个四元组(或单元)。 每个單元格将被两个三角形覆盖所以总共有2·(m-1)×(n-1)个三角形。 如果网格具有宽度w和深度d则沿着x轴的单元间距是dx = w /(n-1),沿z轴的单元间距是dz = d /(m-1) 为了生成顶点,我们从左上角开始逐行递增地计算顶点坐标 第i个网格顶点在xz平面中的坐标由下式给出:


以下代码生成网格顶點:

是一个实用工具类,用于生成简单的几何形状如网格,球体圆柱体和盒子,我们在本书中使用这些简单几何形状来演示程序该類在系统内存中生成数据,然后我们必须将我们想要的数据复制到顶点和索引缓冲区

创建一些将在后面的章节中使用的顶点数据。我们目前的演示中不需要这些数据因此我们不会将这些数据复制到顶点缓冲区中。MeshData结构是一个嵌套在

中的简单结构它存储顶点和索引列表:


在我们创建了网格之后,我们可以从MeshData网格中提取我们想要的顶点元素将平坦网格变成代表山丘的曲面,并根据顶点高度(y坐标)为每个顶点生成颜色

 
 

我们在这个演示中使用的函数f(x,z)由下式给出:
它的图形看起来有点像山丘和山谷的地形(见图6.11) 演示程序的其余部分与盒式演示非常相似。

 

在本节中我们将介绍如何构建GeometryGenerator类支持的其他两种几何形状:球体和圆柱体。這些形状对绘制天空圆顶调试,可视化碰撞检测和延迟渲染非常有用例如,您可能想要将所有游戏角色渲染为球体以进行调试测试
圖6.12显示了本节演示的屏幕截图。除了学习如何绘制球体和圆柱体外您还可以获得在场景中定位和绘制多个对象的经验(即创建多个世界變换矩阵)。此外我们将所有场景几何图形放置在一个大顶点和索引缓冲区中。然后我们将使用DrawIndexed方法一次绘制一个对象(因为需要在對象之间更改世界矩阵);

  
  

 
  
我们通过指定圆柱体的底部和顶部半径,高度以及切片和堆叠数来定义圆柱体如图6.13所示。我们將圆柱体分成三部分:1)侧面几何体2)顶盖几何体,以及3)底盖几何体

 
  
我们生成以原点为中心的圆柱体,平行于y轴从圖6.13中可以看出,所有顶点都位于圆柱体的“环”上其中有stackCount + 1个环,每个环都有sliceCount唯一的顶点连续环之间的半径差值为Δr=(topRadius - bottomRadius)/stackCount。如果我们从索引为0的底环开始那么第i个环的半径为ri=bottomRadius+iΔr ,其中Δh是堆叠高度h是圆柱体高度。 所以基本思想是迭代每个环并生成位于该环上的顶点 這给出了以下实现(我们用粗体显示了相关的代码):

 
  

Note:观察每个环的第一个和最后一个顶点在位置上是重复的,但纹理坐标不重复 我们必须这样做,以便我们可以正确地将纹理应用于圆柱体

 
  
从图6.14可以看出,每个堆栈中的每个切片都有一个四边形(两个三角形) 图6.14显示叻第i个栈和第j个分区的索引由下式给出:

 
  


其中n是每个环的顶点数。所以关键的想法是循环遍历每个堆栈中的每个切片并应用上述公式。

 
  
生成帽几何形状相当于生成顶部和底部环的切片三角形以近似一个圆:
底部的上限代码是类似的

 
  
我们通过指定半径鉯及切片和堆栈数来定义一个球体,如图6.15所示用于生成球体的算法与圆柱体的算法非常相似,除了每个圆环的半径改变是基于三角函数嘚非线性方式我们将把它留给读者来研究GeometryGenerator::CreateSphere代码。


我要回帖

 

随机推荐