Graphics Pipelines

Graphics Pipelines

也许vulkan的最大用处就是当作图形API使用。渲染是vulkan最基础的功能,也是众多图形化应用最核心的部分。vulkan的渲染过程可以当作是通过执行不同阶段的命令以此来在展示设备上渲染出图片的过程。该章节包含vulkan中的渲染管线的基础并且介绍我们的第一个例子。

The Logical Graphics Pipeline

vulkan中,渲染管线可以看作是一条生产流水线,命令在管线的开头进入,并且在管线内不同阶段执行。每个阶段都有诸如变换,读取命令或关联的数据,然后变成其他的数据的操作。在管线的结尾,管线内命令就会渲染出许多像素来组成最终画面。

渲染管线的许多部分是可选的,你可以取消它们或者vulkan实现可能根本不支持这部分功能。管线内唯一必须启用的功能是顶点着色器(vertex shader)。整条vulkan的渲染管线由下图展示。然而,看到该图时不必惊慌,我们会在章节内逐阶段的介绍它们,深挖它们的细节。

image

下面是对管线内每阶段的简单介绍:

  • 绘制(Draw):这是vulkan渲染管线中命令进入的起点。一般来说,vulkan设备内的一个小处理器或专用硬件会解释command buffer中的命令,并且直接用硬件与之交互。
  • 输入装配(Input assembly):该阶段会读取包含顶点信息的顶点缓冲(vertex buffer)和索引缓冲。(index buffer)
  • 顶点着色器(Vertex shader):这里是顶点着色器执行的步骤。它会将顶点的各属性当作输入,为下个阶段准备由变换操作和生成操作而产生的顶点数据。
  • 细分曲面控制着色器(Tessellation control shader):这是一个可编程的着色阶段,负责产生细分因子(tessellation factors)和其他面片(patch)数据,这些数据会被其他固定功能的细分阶段使用。
  • 细分曲面图元生成(Tessellation primitive generation):没有在图中展示,该固定功能的阶段使用在细分曲面控制着色器中产生的细分因子将片面图元打碎成更小,更简单的图元,这些打碎后的图元会在细分曲面评估着色器中使用。
  • 细分曲面评估着色器(Tessellation evaluation shader):该着色阶段会在每个由曲面细分图元生成阶段产生的新的顶点上运行。它与顶点着色器类似,不过顶点数据是程序产生的而不是从内存中读取的。
  • 几何着色器(Geometry shader):该着色阶段使用整个图元。一个图元可能是点、线、三角形或者以上的变体,包括围绕它们的额外顶点。该阶段还可以在管线中改变图元类型。
  • 图元装配(Primitive assembly):该阶段会使用上述由顶点着色器,细分曲面阶段或几何着色器产生的顶点,将它们组装成适合光栅化的图元。也会裁剪、变换图元以适应视口。
  • 裁剪(Clip and cull):该固定阶段会决定哪些图元是对最终图像生成具有贡献的,从而忽略掉那些毫无贡献的成员,然后将潜在的可视图元转发给光栅器。
  • 光栅化(Rasterizer):光栅化是vulkan中的核心基础。光栅器会读取由一系列顶点组成的图元,然后将他们变成一个个独立的片元,它们将来可能就是你最终图像的像素。
  • 片元预处理(Prefragment operations):有些处理可以发生在片元着色之前,比如深度和模板测试。
  • 片元装配(Fragment assembly):没有在图片中展示,片元装配组装光栅化后的输出和每个片元的数据,然后把他们发送给片元着色器。
  • 片元着色器(Fragment shader):该阶段是管线最后运行的着色器,负责给最后固定功能阶段计算片元数据。
  • 片元后处理(Postfragment operations):某些情况下,片元着色器修改了片元预处理过的数据,需要再次进行一边和预处理一样的操作。
  • 颜色混合(Color blending):颜色运算读取片元着色器和片元后处理产生的最后结果,然后使用它们更新到帧缓冲中。颜色运算包括混合和逻辑运算。

正如看到的,渲染管线中有很多互相关联的阶段。不像运算管线(Compute Pipeline),渲染管线不仅包含配置大量的固定功能的阶段,还包括做多五个着色器的使用。此外,根据不同实现,某些固定阶段实际上部分在驱动生成的着色器代码中执行。

vulkan中,将渲染管线看作一个对象的具体原因是为了提供尽可能多的信息,以便让管线在固定阶段和可编程着色器之间进行移动。如果我们不能在同一时间在同一对象内获取全部信息,vulkan实现就可能需要根据配置状态来重新编译着色器。渲染管线内包含的各个状态是经过细致的选择,正是为了避免上述情况,让管线的状态转换变得尽可能快。

vulkan中进行绘制的基本单元是顶点(vertex)。顶点被组装成图元(primitives)并且由管线处理。最简单的绘制命令是vkCmdDraw,原型为:

void vkCmdDraw(
	VkCommandBuffer			commandBuffer,
	uint32_t				vertexCount,
	uint32_t				instanceCount,
	uint32_t				firstVertex,
	uint32_t				firstInstance);

与其他vulkan命令类似,vkCmdDraw将一个命令附加在一个command buffer,稍后由设备执行。使用的command buffer由commandBuffer指定。管线中使用的顶点数量由vertexCount指定。如果你想用稍微不同的参数返回绘制同一组顶点,你可以在instanceCount中指定实例的数量。这种方法称为实例化(instancing),我们稍后会在该章节了解这一部分。现在,我们只将instanceCount设为1。当然,也有可能不从第0个顶点或实例开始绘制。使用firstVertexfirstInstance指定起点。同样,我们稍后了解。目前,将这两个参数设置为0.

在你绘制任何东西钱,你必须将该渲染管线绑定到command buffer中。如果你没有绑定就尝试绘制,会发生未定义行为(通常是糟糕的。)

当你调用vkCmdDrawvertexCount个顶点会生成数据并送到管线当中。每个顶点都会经过输入装配阶段,然后由顶点着色器处理。你可以选择在你提供输入之前让vulkan提供输入,但顶点着色器vulkan则不负责。因此,最简单那的渲染管线只包含一个顶点着色器。

Renderpasses

能将渲染管线和运算管线区别开了的要素之一是——通常,在你使用一个渲染管线渲染图像之后也可能进行其他处理或展示给yoghurt。在复杂的图形应用中,图像需要经过许多通道才能生成,每个通道都负责不同的部分,比如全屏幕的后处理或合成,或渲染UI元素等。

这些通道可以由vulkan的一个渲染通道对象表达出来。一个渲染通道对象封装了输出图像上的许多子通道或是渲染阶段。一个渲染通道的每个阶段称为子通道(subpass)。渲染通道物体可以包含多个子通道,即便只包含一个通道的最简单的应用程序中,渲染通道对象也包含了关于输出图像的信息。

所有绘制操作都必须包含进一个渲染通道中。此外,渲染管线需要知道它们在哪里进行渲染。因此在创建管线对象之前创建一个渲染通道对象是必要的,这样我们才能告诉管线那些将要渲染的图像。渲染通道将在多通道渲染章节中进行深度地讲述。该章节中,我们将创建最简单的渲染通道对象,能让哦我们输出图像即可。

创建渲染通道对象,调用vkCreateRenderPass

VkRenderPassCreateInfo中,pattachments是一个指向具有attachementCount个的VkAttachmentDescription结构体数组指针,它定义了该渲染通道管关联的诸多附件(attachments)。数组内每个元素都定义了单个被用来给一个或多个子通道当作输入、输出或同时具有二者功能的图像。如果没有附件,你可以将attachmentCount为0,pAttachments为nullptr。然而,在一些高级的使用方法中,几乎所有的渲染都使用至少一个附件。

VkAttachmentDescription结构中,flags字段提供附件的额外信息,唯一值为VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT。如果设置该值,表明该附件可能与同一渲染通道的另一个附件使用相同的内存。这让vulkan不要去做任何能导致该附件中发生数据不一致性的行为。该字段可能在一些需要内存优化的时候设置,大多数时候都是0。

format字段指定了附件的格式。值为VkFormat枚举,并且应该和附件使用的图像格式相匹配。同样,samples表明了用于重采样(multisampling)时的采样数量。如果不适用重采样,设置为VK_SAMPLE_COUNT_1_BIT

接下来四个字段指定了在渲染通道的开头和结尾处对附件的操作。加载操作让vulkan指定了渲染通道在开头时的操作,可以设置为:

  • VK_ATTACHMENT_LOAD_OP_LOAD:表明附件内已经存在数据并且你想继续向其中渲染。这回让vulkan在渲染通道开始时认为附件内容是合法的。
  • VK_ATTACHMENT_LOAD_OP_CLEAR:表明你想在渲染通道开始时清除附件内容。清除所使用的颜色回在渲染通道开始时指定。
  • VK_ATTACHMENT_LOAD_OP_DONT_CARE:指明渲染通道开始时不关心附件内容,vulkan可以随意操作其中的内容。如果你计划显式清除附件或者你知道你将会在通道内替换其中的内容,可以使用该值。

同样的,存储操作让vulkan了解当渲染通道结束时应该对附件进行的操作,可以为:

  • VK_ATTACHMENT_STORE_OP_STORE:表明你希望vulkan保存其中的内容,以便于未来使用。这通常意味着会把其中内容写回内存中。当你想将图像展示给用户,或之后进行读取,或给其他渲染通道当作附件使用(该通道需要指明VK_ATTACHMENT_LOAD_OP_LOAD),使用该值。
  • VK_ATTACHMENT_STORE_OP_DONT_CARE:表明渲染通道结束后便不需要该附件的内容了。一般用于中间存储或深度,模板缓冲。

如果附件是一个深度-模板附件,那么stencilLoadOpstencilStoreOp字段告诉vulkan对附件的模板部分进行的操作(深度部分由常规的loadOpstoreOp指定),允许与深度部分不同。

initialLayoutfinalLayout字段告诉vulkan当渲染通道开始时图像的布局以及当结束时图像的布局。渲染通道对象不会自动将图像转成初始布局。该布局是渲染通道开始时期望的布局。不过相反的是,渲染通道会将图像自动转成结束的布局。

你可以显式的使用屏障来将转移图像的布局,不过可能的话,最好在渲染通道内进行布局转换。这给予vulkan一个机会让渲染通道的各部分都选择合适的布局,甚至能够在渲染的时候并行执行任何转移操作。高阶用法在多通道渲染章节。

在定义完渲染通道所使用的所有附件后,你需要定义所有的子通道。每个子通道都引用附件资源的一部分当作输入或输出。这些信息由VkSubpassDescription数组指定。

VkSubpassDescription中,因为当前版本vulkan只为渲染管线支持了渲染通道,所以pipelineBindPoint设置为VK_PIPELINE_BIND_POINT_GRAPHICS。其余字段描述了改子通道使用的附件。每个子通道能含有数个输入附件,用来读取数据。颜色附件,用来当作输入。深度-模板附件用于深度缓冲和模板缓冲。这些附件在pInputAttachmentspColorAttachmentspDepthStencilAttachment指定。

一个子通道能含有的颜色附件有一个最大值,由maxColorAttachments决定。该值至少为4,所以如果没有超过该值,便不需要具体查询该字段的值。

这些指针的都指向VkAttachmentReference结构。

每个附件引用结构都很简单,只包含一个附件数组的索引值和该附件在被子通道使用时所期望的图像布局。除了输入和输出的附件引用,还有两种额外的功能的附件。

首先,为重解析附件,通过pResolveAttachments指定,它们是用来存储多重采样图像数据重解析后产生的数据的。这些附件与相应的由pColorAttachments指定的颜色附件相关,重解析附件的数量假定于颜色附件相同。

如果颜色附件中某一个为多重采样图像,但是渲染通道只需要该图像对应的解析数据,你可以让vulkan把解析图像作为渲染通道的一部分,然后丢弃原始的多重采样数据。方法是——将多重采样的颜色附件的存储操作设置为VK_ATTACHMENT_STORE_OP_DONT_CARE,将相应的重解析附件的存储操作设置为VK_ATTACHMENT_STORE_OP_STORE,这样vulkan就会保留解析后得到的单个采样数据而丢弃原始多重采样数据。

其次,如果你有一些附件想让其在子通道内存活但却不直接引用,使用pReserveAttachments。该引用会组织vulkan做任何的优化操作,以免扰乱其中的内容。

如果渲染通道内含有一个以上的子通道。vulkan可以根据附件的引用并且根据子通道间的输入和输出弄清楚子通道之间的依赖关系。也有一些不能简单由输入输出依赖决定的关系的情况。比如,一个子通道向资源中直接写入,然后另一个子通道从中读取写入的数据,这种情况vulkan无法自己弄清通道间依赖关系,所以你必须手动提供这些依赖信息。使用pDependencies字段指定依赖关系。

VkSubpassDependency中,每一个依赖关系都从源子通道开始(数据的生产者)到目标子通道结束(数据的消费者),分别由srcSubpassdstSubpass指定。二者都是组成渲染通道的所有子通道中的索引。srcStageMask指定子通道渲染管线的哪一阶段产生数据。与之相似,dstStageMask指定目标管线的哪一阶段消耗数据。

srcAccessMaskdstAccessMask指定两个子通道如何访问数据。比如,源子通道可能在顶点着色器中向图像中写入数据或者在片元着色器中向颜色附件写入输出数据。与此同时,目标子通道可能将其作为输入附件进行读取或作为图像进行加载。

当我们不使用渲染通道对象后,需要调用vkDestroyRenderPass销毁。

The FrameBuffer

帧缓冲(Framebuffer)代表由渲染管线进行渲染的一组图像。它们影响管线的最后几个阶段:深度模板测试,颜色混合,逻辑运算,多重采样等等。一个帧缓冲对象总是附着在一个渲染通道上并且可以用在多个具有相似模板编排的渲染通道中。

调用vkCreateFramebuffer创建帧缓冲对象。

VkFramebufferCreateInfo中,与帧缓冲兼容的渲染通道对象由renderpass指定。对于帧缓冲来说,两个兼容的渲染通道是指它们引用的附件是相同的。

绑定到帧缓冲对象的图像由pAttachments传递,它是一个VkImageview指针,数量由attachmentCount指定。该数量和渲染通道内指定的附件数量相同,并且图象视口和渲染通道内附件一一对应,子通道使用索引引用的附件就是对应到pAttachments中。如果你知道某个特别的渲染通道不适用某些附件,但是你依然希望帧缓冲于其兼容,那么pAttachments某些值可能需要设置为VkNullHandle

即使帧缓冲内每个图像都有自己的宽度、高度和层数,你也必须指定帧缓冲的维度大小,包括widthheightlayers。你可以通过maxFramebufferWidth查看帧缓冲宽度限制,类似的高度和层数也可以通过着这种方法查看。

如果你不需要这缓冲对象,使用vkDestroyFramebuffer销毁。

销毁一个帧缓冲对象并不影响任何附着在帧缓冲对象上的图像数据。图像可以同时绑定到多个帧缓冲中,也可在一个帧缓冲中绑定到多个附件上,以此用多种方式使用图像数据。然而,即使图像没有销毁,你应该确保不再使用已销毁的帧缓冲对象。

Create a Simple Graphics Pipeline

创建一个渲染管线调用的方法和运算管线差不多。然而,渲染管线因为包含众多的着色阶段和固定功能处理模块,所以用来创建渲染管线的信息结构体要相当复杂。调用vkCreateGraphicsPipelines创建渲染管线对象。

VkGraphicsPipelineCreateInfo非常庞大且复杂,包含许多指向其他你必须创建的结构体指针。

不过幸好,我们能很轻易的打碎这个复杂的结构体,变成一个个模块并且许多额外的信息是可选的,可以指定为nullptr。

flags字段给出了渲染管线的使用信息,比如:

  • VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT:vulkan会知道该管线不应该用到对性能要求很高的程序当中,表明你想要马上使用该管线而不是让vulkan先花大力气去优化他。在你想要快速渲染图形界面或用简单的着色器来显示闪屏时,你可能使用它。
  • VK_PIPELINE_CREATE_ALLOW_DERIVATICES_BIT & VK_PIPELINE_CREATE_DERIVATIVE_BIT:它们被用在派生(derivative)管线上。这一个特性,允许你将相似的管线组合起来,让vulkan能够在他们之间快速切换。VK_PIPELINE_CREATE_ALLOW_DERIVITAVE_BIT告诉vulkan你会在管线之上创建派生管线,VK_PIPELINE_CREATE_DERIVITAVE_BIT告诉vulkan该管线是由其他管线派生而来。

Graphics Shader Stages

接下来的两个字段,stageCountpStages是传递着色器的地方。pStages指向一个VkPipelineShaderStageCreateInfo结构数组,每一个元素描述了一个着色器阶段。layout字段和我们在运算管线中设计的管线布局是一致的,都负责管线资源。renderpass字段指定了该管线附着在哪个渲染通道上,决定了管线所使用的环境。管线必须使用在与其兼容的渲染通道上。subpass索引渲染通道内的子通道,渲染管线的具体使用位置就由子通道的位置决定。

Vertex Input State

为了渲染真正的几何体,你需要在管线的开头输入数据。你可以使用由SPIR-V提供的顶点或实例索引来程序化生成几何体或直接从缓冲区中提取几何数据。此外,你可以描述几何数据在内存中的布局,然后vulkan可以自己提取其中数据你然后直接向着色器中输入。

我们填写VkGraphicsPipelineCreateInfopVertexInputState字段。

顶点输入阶段可以分成一组顶点绑定,绑定包含存有顶点数据的缓冲区和对应的顶点属性,顶点属性会描述顶点数据在缓冲区内的布局。虽然这些缓冲区被叫做顶点缓冲区,但是任何缓冲区都可以存储定点数,同样缓冲区也可不仅仅存储顶点数据。唯一要求是,如果一个缓冲区被当作顶点缓冲区使用,我们必须使用VK_BUFFER_USAGE_VERTEX_BUFFER_BIT设置它。

VkPipelineVertexInputStateCreateInfo中,pVertexBindingDescriptions指向一个VkVertexInputBindingDescription数组。

而在VkVertexInputBindingDescription中,binding字段指定了绑定索引。每个管线都可以使用多个顶点缓冲区绑定,而且它们的索引不一定是连续的。索引的最大值被限制在maxVertexInputBindings

每个绑定可以看作包含一个某种结构的数组的缓冲区对象。这个数组的步宽——即数组内每个结构起始点的距离(用字节表示),由stride指定。如果该结构数组内存储的顶点数据,那么stride必须和顶点数据大小相等,即使着色器不会使用其中的每个顶点属性。stride的最大值由maxVertexInputBindingStride指定,保证至少为2048字节长。

此外,vulkan还能根据索引遍历顶点数组或实例数组内的数据。这是通过inputRate指定的,要么是VK_VERTEX_INPUT_RATE_VERTEX,要么是VK_VERTEX_INPUT_RATE_INSTANCE

每个顶点属性都是顶点缓冲区中至关重要的结构。每个顶点属性都能和顶底缓冲区共享其步宽和遍历模式,并且可以根据自己的属性类型和结构内偏移量获取其中属性内数据。这是通过VkVertexInputAttributeDescription实现的,由pVertexAttributeDescriptions指定。并且vertexAttributeDescriptionCount指定了顶点包含的属性数目。

VkVertexInputAttributeDescription中,location用来在顶点着色器中引用该属性。通用,顶点属性的locations也不一定是连续的,只要所有属性都由明确的描述,便不需要给每个location对应一个属性。binding描述该数据究竟来自哪一个绑定的缓冲区,并且应该和上述VkVertexInputBindingDescription的之一匹配。顶点数据的格式由format指定,属性的偏移量为offset

因为整个顶点都尺寸上限,所以偏移量也有一个上限,查看maxVertexInputAttributeOffset,该值至少为2047。

顶点属性的上限至少为16个,查看maxVertexInputAttributes具体实现。

顶点数据是从绑定到command buffer的顶点缓冲区中读取的,然后传递给顶点着色器。为了能够让顶点着色器翻译出顶点数据,必须在着色器内声明你定义过的顶点属性。在GLSL中,使用in表达的变量。

每个输入都有一个location,在GLSL中使用location布局限定符,然后它会被翻译成SPIR-V的Location符号。

也有可能给着色器传递属性的一部分组件数据。同样,属性存储在顶点缓冲区中,着色器声明你需要读取的数据。为了给顶点着色器传递输入向量的一部分分量数据,GLSL使用component布局限定符。每个着色器输入都可以从0号分量开始到3分量结束,另一种表达就是x,y,z和w通道数据。每个输入根据需要消耗连续的分量。每个数据占用一个分量。

顶点着色器还能将矩阵作为输入。在GLSL中,这和在变量上使用in一样简单。矩阵是默认是按列排列的,因此,矩阵中数个连续的数据填充矩阵的一列。

Input Assembly

输入装配阶段会将顶点数据组合成图元。由pInputAssemblyState指定,是一个VkPipelineInputAssemblyStateCreateInfo 结构。

VkPipelineInputAssemblyStateCreateInfo中,topology指定图元拓扑结构。最简单的为链状结构:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST:每个顶点数据当作一个独立的顶点。
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST:顶点数据被组合成一对,每对都代表一个线段,从第一个顶点指向第二个的顶点。
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST:顶点组合成三角形。

接下来是带状和扇形图元:

  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP:顶点接着上一个顶点连成线段。
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP:顶点接着前两个顶点组成三角形。
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN:顶点接着上一个顶点,与第一个顶点组成新的三角形。

相关内容可以查看图形学基础那本书(虎书)。

接下来是邻接图元(adjacency primitives),它们通常只在启用几何着色器时使用,能够传达原始网格中与图元相邻的额外信息:

  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY:四个顶点组合成一条直线,其中中间两个顶点就是全部的图元,左右两个只能在几何着色器中访问。

image

  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY:头四个顶点组合成一个图元,中间两个顶点组成线段,前后两个顶点只能在几何着色器中访问,提供邻接信息。接下来的每个顶点都会增加实际线段的长度。

image

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY:每六个顶点组合成一个图元。第1,3,5个顶点组合成三角形,另外三个只能在几何着色器中使用。

image

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY:同样每六个顶点组成一个图元。1,3,5组成三角形,其余只在几何着色器使用。每添加一个新三角形,奇数位为三角形需要的顶点,偶数位为邻接节点。

image

  • VK_PRIMITIVE_TOPOLOGY_PATCH_LIST:该拓扑结构在启用细分曲面功能是使用,而且需要一些额外的信息。

最后一个字段是primitiveRestartEnable,该字段允许带状和扇形图元的拓扑结构进行裁切和重启操作。如果没有开启,每个带状和扇形图片都需要分开绘制。当你使用图元重启,便可以一次绘制多个带状和扇形图元。重启操作只能用在索引绘制命令,因为标识重启的起点位置使用一个索引缓冲中的特殊值进行指定的。我们会在“绘制”章节中进行详细了解。

Tessellation State

细分曲面阶段是将大且复杂的图元分割成更细小的的图元且同时保留原始图元的相似性。vulkan可以在进行光栅化和几何着色之前将一块图元分成许多细小的点、线或三角形图元。细分曲面相关的大多数阶段都使用细分曲面控制着色器和细分曲面评估着色器进行配置。然而,由于这些着色阶段在顶点数据被顶点着色器获取并处理之后才能运行,因此需要一些信息来预先配置该阶段。

相关信息由VkPipelineTessellationStateCreateInfo指出。

如果VkPipelineInputAssemblyStateCreateInfotopology字段设置为VK_PRIMITIVE_TOPOLOGY_PATCH_LISTVkPipelineTessellationStateCreateInfo pTessellationState 必须指向一个VkPipelineTessellationStateCreateInfo 对象,否则设为nullptr。

结构体中唯一需要认证的字段为patchControlPoints,指明了一个图元中存在多少数量的控制点。细分曲面更多的相关细节将在几何处理章节中进行更深入讲解。

Viewport State

视口阶段是vulkan管线在进行光栅化之前进行最后一个坐标变换。它会将顶点从归一化的设备坐标转换到具体的窗口坐标。可以同时使用多个视口。这些视口的状态,包括活动视口的数量,以及一些相关参数由VkPipelineViewportStateCreateInfo指定。

管线中可获取的视口数量由viewportCount指定,每个视口的大小由VkViewport指定,由字段pViewports指定。

VkViewport中,x,y指定了视口的左上角位置,width,height指定了视口的大小,minDepth,maxDepth指定了视口的深度范围。视口变换相关内容可以查看https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap26.html#vertexpostproc-coord-transform。

VkPipelineViewportStateCreateInfo还可以给管线创建裁剪矩形,与视口同样,一个管线可以定义多个裁剪矩形,通过VkRect2D结构体传递矩形信息。裁剪矩形的数量由scissorCount指定。注意,视口的索引和裁剪矩形的索引是一一对应的,所以scissorCount的值必须和viewportCount一致。VkRect2D是一个简单的结构体。

多视口功能是可选的,如果支持多视口,最大值至少保证为16,具体值可以查看maxViewport

Rasterization State

光栅化是将由顶点组合成的图元变成一串准备被片元着色器着色的片元过程的基础。光栅器由VkPipelineRasterizationStateCreateInfo定义,并且控制该过程如何发生。

该结构体中,depthClampEnable决定是否启用深度收紧(clamp)操作。深度收紧可以使原本被近平面或远平面剪掉的片元转而投射到这些平面上,以此用于填补因裁剪而造成的几何体上的洞。

rasterizerDiscardEnable该字段可以忽略整个光栅化阶段,如果启用,那么没有光栅器会运行,也没有任何片元会产生。

polygonMode字段可以让vulkan把三角形自动转成点或线,可能的值有:

  • VK_POLYGON_MODE_FILL:用于填充三角形的正常模式。三角形将会被化成实心的,内部的每个点都会产生片元。
  • VK_POLYGON_MODE_LINE:该模式将三角形转成线,每条边都会变成一条线。这对于绘制几何体的线框很有用。
  • VK_POLYGON_MODE_POINT:该模式仅仅将顶点当作点绘制。

使用多边形模式将几何体变成线框或点集,而不是仅仅绘制线或者点,其优势在于仍然执行一些只用于三角形的操作,比如背面剔除。因此,那些被剔除的三角形的线不会被绘制出来,而线会绘制。

cullMode控制剔除模式,可以是0或是以下的组合:

  • VK_CULL_MODE_FRONT_BIT:被认为是朝向观察者的几何体会被忽略。
  • VK_CULL_MODE_BACK_BIT:被认为背向观察者的几何体会被忽略。

为了简便,vulkan定义了VK_CULL_MODE_FRONT_AND_BACK作为上述二者的集合。将cullMode设置为此会导致所有三角形被忽略。注意,剔除操作不会影响点或线,因为它们没有定义方向。

三角形的朝向由顶点的环绕顺序决定——在窗口空间中是顺时针还是逆时针。究竟是顺时针在前还是逆时针在前由frontFace决定——即VK_FRONT_FACE_COUNTER_CLOCKWISE或者VK_FRONT_FACE_CLOCKWISE

接下来的四个字段——depthBiasEnabledepthBiasConstantFactordepthBiasClampdepthBiasSlopeFactor控制深度偏置(depth bias)特征。该特征允许片元在进行深度测试之前进行偏移操作,可以用来防止深度冲突。更多细节将会在片元处理章节阐述。

最后,lineWidth以像素为单位设置线图元的宽度,它会应用到光栅化后的每条线上。这些线的产生可以来自管线的图元类型,几何或细分曲面着色器可以将输入图元转成线,或者多边形模式(polygonMode)设置为VK_POLYGON_MODE_LINE。注意,一些vulkan实现不支持宽线所以会忽略该字段。其他的实现可能在该值不为1.0的时候运行的很慢。如果线宽设置为0.0,一些版本可能会丢弃所有的线。因此,你应该总是把这个字段设置为1.0,除非你有别的用途。

即使支持线宽,线宽的最大值也是根据设备决定的。不过该最大值至少保证为8,查看lineWidthRange了解具体值。这是一个含有两个浮点值的数组,第一个是线宽的最小值,第二个是最大值。如果不支持线宽变化,则两个值都是1.0.

此外,当线宽被改变时,设备可能根据固定的增量来决定你的线宽增量。比如,他可能仅支持全宽像素大小的变化。这是线宽颗粒度,由VkPhysicalDeviceLimitslineWidthGranularity决定。

Multisample State

多重采样是在图像中每个像素上生成多个采样数据的过程。它用来对抗图片上的锯齿效果并且恰当使用能大幅提升图片质量。如果你使用多重采样,那么颜色附件和深度-模板附件必须是可多重采样图片。多重采样阶段由VkGraphicsPipelineCreateInfo进行恰当配置。

Depth and Stencil State

深度模板阶段控制如何进行深度模板测试,并且如果一个片元通过或未通过测试后会发生什么。深度模板测试可以发生在片元着色器之前或之后。默认情况下,这些测试发生在之后。

如果将片元着色器发生在深度测试之前,我们可以把着色器的入口点设置为SPIR-V的EarlyFragmentTests执行模式。

深度模板测试通过VkPipelineDepthStencilStateCreateInfo进行配置。

该结构中,depthTestEnable如果启动,并且启用深度测试,那么depthCompareOp来进行测试操作,值为VkCompareOp之一。可使用的深度测试操作将在片元处理章节深入了解。如果depthTestEnable未启动,但是深度测试和深度操作都指定了,那么所有的片元都将认为通过了深度测试。值得注意的是,如果深度测试未启用,那么没有东西写入到深度缓冲。

如果深度测试通过了(或者未启用深度测试),那么片元将进行模板测试。如果VkPipelineDepthStencilCreateInfostencilTestEnable 设置为VK_TRUE,那么说明启用模板测试,否则不启用。如果启用模板测试,那么每个正向或反向的片元都会赋予由frontback指定的独立状态。如果未启用模板测试,所有的片元都认为通过了模板测试。

Color Blend State

渲染管线的最后一个阶段是颜色混合阶段。该阶段负责将片元写入到颜色附件上。在许多情况下,这是一个简单的操作,仅仅是将片元着色器的输入覆盖到已有内容的颜色附件上去。然而,颜色混合允许将上述的值和已存在在帧缓冲中的值进行混合,通过简单的在片元着色器的输出和帧缓冲原有内容进行逻辑运算实现混合。

该阶段由VkPipelineColorBlendStateCreateInfo进行定义。

在该结构体中,logicalOpEnable字段指定了是否在片元着色器和颜色附件内容之间进行逻辑运算。如果设置为VK_FALSE,那么将禁止逻辑运算,片元着色器的输出将直接写入到颜色附件中。如果为VK_TRUE,那么将使用支持的逻辑运算。所有附件上的逻辑运算都是相同的并且都为VkLogicalOp的一员。我们将在片元处理章节中阐述这些含义。

每个附件都有不同的格式,并且能够支持不同的混合运算。这些都是通过VkPipelineColorBlendAttachmentState数组指定的,数量由attachmentCount指定。

每个颜色附件,VkPipelineColorBlendAttachmentState的字段控制是否开启混合,混合源和混合目的的因数,以及指定混合操作(颜色通道和alpha通道分开),以及哪些输出通道会被更新。

如果blendEnable字段设置为VK_TRUE,那么剩余参数将控制混合的状态。如果设置为VK_FALSE,那么将禁止混合操作,源内容将直接输出到附件中。

最后一个字段colorWriteMask,控制了附加中的输出图像的哪些通道会被写入。它是由VkColorComponentFlagBits组合而成。四个通道分别表示为VK_COLOR_COMPONENT_{R,G,B,A}_BIT,他们可以独立的组合成掩码。没有包含进的通道将不会被编辑,只有colorWriteMask中包含的通道会被更新到附件中。

Dynamic State

正如你所见,渲染管线对象庞大且复杂,包含许多的阶段。于是许多图形应用中,便希望能以相对高频率的改变阶段。如果每次改变都需要你新创建一个渲染管线对象,那么应用程序需要管理的对象将迅速增大。

为了能够细粒度的管理状态变化,vulkan提供了能够将渲染管线的特定部分标记为动态(dynamic),意味着它们可以直接在command buffer中使用命令进行实时更新,而不是使用一个对象。因为这减少了vulkan优化状态一些机会,所以有必要显式指定哪些阶段你想指定为动态的。这是通过VkPipelineDynamicStateCreateInfo定义的。

在该结构体中,dynamicStateCount指定由多少阶段设定为动态的。pDynamicStates告诉vulkan你想使用相关的动态阶段设置命令来改变某个阶段。VkDynamicState包含:

  • VK_DYNAMIC_STATE_VIEWPORT:视口矩形是动态的,可以通过vkCmdSetViewport更新。
  • VK_DYNAMIC_STATE_SCISSOR:裁剪矩形是动态的,可以通过vkCmdSetScissor更新。
  • VK_DYNAMIC_STATE_LINE_WIDTH:线宽是动态的,通过vkCmdSetLineWidth更新。
  • VK_DYNAMIC_STATE_DEPTH_BIAS:深度偏置是动态的,通过vkCmdSetDepthBias更新。
  • VK_DYNAMIC_STATE_BLEND_CONSTANTS:颜色混合常量是动态的,通过vkCmdSetBlendConstants更新。
  • VK_DYNAMIC_STATE_DEPTH_BOUNDS:深度边界是动态的,通过vkCmdSetDepthBounds更新。
  • VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,VK_DYNAMIC_STATE_STENCIL_WRITE_MASK和VK_DYNAMIC_STATE_STENCIL_REFERENCE:相关的模板参数是动态的,通过 vkCmdSetStencilCompareMaskvkCmdSetStencilWriteMaskvkCmdSetStencilReference更新。

如果一个阶段被指定为动态的,那么绑定管线的时候设置该阶段就变成了你的责任了。如果一个阶段不是动态的,那么在管线绑定的时候就默认为静态(static)的。

当你在相同阶段标记为动态的管线之间进行切换时,即使是在不同的绑定中,动态阶段依然会保留。

image

根据上图你可以看到,只有一种情况下在切换管线时会导致阶段变成未定义——切换前为静态,切换后为动态。其他情况,阶段都是明确的,要么明确自管线已有的,要么明确自命令设置的。

如果绑定的管线的某一个阶段是静态的,但是你将其设置为动态。那么用该管线进行渲染的结果是未定义的。有可能是忽视设置命令继续使用静态版本,也有可能使用设置命令来使用新的动态阶段,或者直接发生冲突毁掉你的应用程序。

设置动态命令然后将对应动态阶段的管线绑定就可以使用动态阶段了。然而,最好实际是首先绑定管线,然后将相关阶段也绑定好,避免出现未定义的行为。

posted @ 2022-08-30 10:41  ᴮᴱˢᵀ  阅读(408)  评论(0)    收藏  举报