Vulkan Tutorial 12 Fixed functions

操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Visual Studio 2017



早起的图形API在图形渲染管线的许多阶段提供了默认的状态。在Vulkan中,从viewport的大小到混色函数,需要凡事做到亲历亲为。在本章节中我们会填充有关固有功能操作的所有结构体。

Vertex input



VkPipelineVertexInputStateCreateInfo结构体描述了顶点数据的格式,该结构体数据传递到vertex shader中。它以两种方式进行描述:

  • Bindings:根据数据的间隙,确定数据是每个顶点或者是每个instance(instancing)
  • Attribute 描述:描述将要进行绑定及加载属性的顶点着色器中的相关属性类型。

因为我们将顶点数据硬编码到vertex shader中,所以我们将要填充的结构体没有顶点数据去加载。我们将会在vertex buffer章节中回来操作。

VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional

pVertexBindingDescriptionspVertexAttributeDescriptions成员指向结构体数组,用于进一步描述加载的顶点数据信息。在createGraphicsPipeline函数中的shaderStages数组后添加该结构体。

Input assembly



VkPipelineInputAssemblyStateCreateInfo结构体描述两件事情:顶点数据以什么类型的几何图元拓扑进行绘制及是否启用顶点索重新开始图元。图元的拓扑结构类型topology枚举值如下:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST:顶点到点
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST:两点成线,顶点不共用
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP:两点成线,次索引的顶点作为后一个顶点的开始顶点,即顶点共用
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: 三点成面,顶点不共用
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP:三点成面,每第三个顶点作为下一个三角形的首个顶点,即顶点共用

正常情况下,顶点数据按照缓冲区中的序列作为索引,但是也可以通过element buffer缓冲区自行指定顶点数据的索引。通过复用顶点数据提升性能。如果设置primitiveRestartEnable成员为VK_TRUE,可以通过0xFFFF或者0xFFFFFFFF作为特殊索引来分解线和三角形在_STRIP模式下的图元拓扑结构。

通过本教程绘制三角形,所以我们坚持按照如下格式填充数据结构:

VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;

Viewports and scissors



viewport用于描述framebuffer作为渲染输出结果目标区域。它的数值在本教程中总是设置在(0, 0)(width, height)

VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float) swapChainExtent.width;
viewport.height = (float) swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;

记得交换链和它的images图像大小WIDTHHEIGHT会根据不同的窗体而不同。交换链图像将会在帧缓冲区framebuffers使用,所以我们应该坚持它们的大小。

minDepthmaxDepth数值指定framebuffer中深度的范围。这些数值必须收敛在[0.0f, 1.0f]区间冲,但是minDepth可能会大于maxDepth。如果你不做任何指定,建议使用标准的数值0.0f和1.0f

viewports定义了image图像到framebuffer帧缓冲区的转换关系,裁剪矩形定义了哪些区域的像素被存储。任何在裁剪巨型外的像素都会在光栅化阶段丢弃。它们的功能更像过滤器而不是定义转换关系。这个区别如下图所示。需要注意的是,对于图像比viewport尺寸大的情形,左侧的裁剪矩形只是众多可能的一个表现。

在本教程中我们需要将图像绘制到完整的帧缓冲区framebuffer中,所以我们定义裁剪矩形覆盖到整体图像:

VkRect2D scissor = {};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;

viewport和裁剪矩形需要借助VkPipelineViewportStateCreateInfo结构体联合使用。可以使用多viewports和裁剪矩形在一些图形卡,通过数组引用。使用该特性需要GPU支持该功能,具体看逻辑设备的创建。

VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor; 

Rasterizer



光栅化通过顶点着色器及具体的几何算法将顶点进行塑形,并将图形传递到片段着色器进行着色工作。它也会执行深度测试depth testing、面裁切face culling和裁剪测试,它可以对输出的片元进行配置,决定是否输出整个图元拓扑或者是边框(线框渲染)。所有的配置通过VkPipelineRasterizationStateCreateInfo结构体定义。

VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;

它的depthClampEnable设置为VK_TRUE,超过远近裁剪面的片元会进行收敛,而不是丢弃它们。它在特殊的情况下比较有用,像阴影贴图。使用该功能需要得到GPU的支持。

rasterizer.rasterizerDiscardEnable = VK_FALSE;

如果rasterizerDiscardEnable设置为VK_TRUE,那么几何图元永远不会传递到光栅化阶段。这是基本的禁止任何输出到framebuffer帧缓冲区的方法。

rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

polygonMode决定几何产生图片的内容。下列有效模式:

  • VK_POLYGON_MODE_FILL: 多边形区域填充
  • VK_POLYGON_MODE_LINE: 多边形边缘线框绘制
  • VK_POLYGON_MODE_POINT: 多边形顶点作为描点绘制

使用任何模式填充需要开启GPU功能。

rasterizer.lineWidth = 1.0f;

lineWidth成员是直接填充的,根据片元的数量描述线的宽度。最大的线宽支持取决于硬件,任何大于1.0的线宽需要开启GPU的wideLines特性支持。

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

cullMode变量用于决定面裁剪的类型方式。可以禁止culling,裁剪front faces,cull back faces 或者全部。frontFace用于描述作为front-facing面的顶点的顺序,可以是顺时针也可以是逆时针。

rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional

光栅化可以通过添加常量或者基于片元的斜率来更改深度值。一些时候对于阴影贴图是有用的,但是我们不会在章节中使用,设置depthBiasEnableVK_FALSE

Multisampling



VkPipelineMultisampleStateCreateInfo结构体用于配置多重采样。所谓多重采样是抗锯齿anti-aliasing的一种实现。它通过组合多个多边形的片段着色器结果,光栅化到同一个像素。这主要方法在边缘,这也是最醒目的锯齿发生的地方。如果只有一个多边形映射到像素是不需要多次运行片段着色器进行采样的,相比高分辨率来说,它会花费较低的开销。开启该功能需要GPU支持。

VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; // Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional

在本教程中我们不会使用多重采样,但是可以随意的尝试,具体的参数请参阅规范。

Depth and stencil testing



如果使用depth 或者 stencil缓冲区,需要使用VkPipelineDepthStencilStateCreateInfo配置。我们现在不需要使用,所以简单的传递nullptr,关于这部分会专门在深度缓冲区章节中讨论。

Color blending



片段着色器输出具体的颜色,它需要与帧缓冲区framebuffer中已经存在的颜色进行混合。这个转换的过程成为混色,它有两种方式:

  • 将old和new颜色进行混合产出一个最终的颜色
  • 使用按位操作混合old和new颜色的值

有两个结构体用于配置颜色混合。第一个结构体VkPipelineColorBlendAttachmentState包括了每个附加到帧缓冲区的配置。第二个结构体VkPipelineColorBlendStateCreateInfo包含了全局混色的设置。在我们的例子中仅使用第一种方式:

VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional

这种针对每个帧缓冲区配置混色的方式,使用如下伪代码进行说明:

if (blendEnable) {
    finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
    finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
    finalColor = newColor;
}

finalColor = finalColor & colorWriteMask;

如果blendEnable设置为VK_FALSE,那么从片段着色器输出的新颜色不会发生变化,否则两个混色操作会计算新的颜色。所得到的结果与colorWriteMask进行AND运算,以确定实际传递的通道。

大多数的情况下使用混色用于实现alpha blending,新的颜色与旧的颜色进行混合会基于它们的opacity透明通道。finalColor作为最终的输出:

finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;

可以通过一下参数完成:

colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

可以在规范中找到所有有关VkBlendFactorVkBlendOp的枚举值。

第二个结构体持有所有帧缓冲区的引用,它允许设置混合操作的常量,该常量可以作为后续计算的混合因子:

VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // Optional
colorBlending.blendConstants[1] = 0.0f; // Optional
colorBlending.blendConstants[2] = 0.0f; // Optional
colorBlending.blendConstants[3] = 0.0f; // Optional

如果需要使用第二种方式设置混合操作(bitwise combination), 需要设置logicOpEnableVK_TURE。二进制位操作在logicOp字段中指定。在第一种方式中会自动禁止,等同于为每一个附加的帧缓冲区framebuffer关闭混合操作,blendEnableVK_FALSEcolorWriteMask掩码会用确定帧缓冲区中具体哪个通道的颜色受到影响。它也可以在两种方式下禁止,截至目前,片段缓冲区向帧缓冲区中输出的颜色不会进行任何变化。

Dynamic state



之前创建的一些结构体的状态可以在运行时动态修改,而不必重新创建。比如viewport的大小,line width和blend constants。如果需要进行这样的操作,需要填充VkPipelineDynamicStateCreateInfo结构体:

VkDynamicState dynamicStates[] = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_LINE_WIDTH
};

VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;

在绘制的过程中指定这些数据,这会导致忽略之前的相关数值。我们会在后续的章节中回过头来讨论。如果没有任何需要动态修改的数值清设置为nullptr

Pipeline layout



可以在着色器中使用uniform,它是类似与动态状态变量的全局变量,可以在绘画时修改,可以更改着色器的行为而无需重新创建它们。它们通常用于将变换矩阵传递到顶点着色器或者在片段着色器冲创建纹理采样器。

这些uniform数值需要在管线创建过程中,通过VkPipelineLayout对象指定。即使在后续内容中用到,我们也仍然需要创建一个空的pipeline layout。

创建类成员变量持有该对象,因为我们在后续章节中的函数中引用它:

VkPipelineLayout pipelineLayout;

createGraphicsPipeline函数中创建对象:

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = 0; // Optional

if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
    throw std::runtime_error("failed to create pipeline layout!");
}

该结构体还指定了push常量,这是将动态值传递给着色器的拎一个方式。pipeline layout可以在整个程序的生命周期内引用,所以它在程序退出的时候进行销毁。

void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    ...
}

Conclusion



这就是所有有关fixed-function的内容,看起来有很多的工作去做,值得庆幸的是我们几乎了解了所有有关渲染管线的内容。这个过程减少了因为不了解某些组件的默认状态,而造成运行时碰到未知行为的可能性。

然而,在我们可以最终创建图形管线之前,还有一个对象需要创建,它就是render pass。

项目代码 GitHub地址。

时间: 2024-07-31 15:30:50

Vulkan Tutorial 12 Fixed functions的相关文章

Vulkan Tutorial 02 编写Vulkan应用程序框架原型

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 General structure 在上一节中,我们创建了一个正确配置.可运行的的Vulkan应用程序,并使用测试代码进行了测试.本节中我们从头开始,使用如下代码构建一个基于GLFW的Vulkan应用程序原型框架的雏形. #include <vulkan/vulkan.h> #include <iostream> #include <stdexcept>

Vulkan Tutorial 05 物理设备与队列簇

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Selecting a physical device 通过VkInstance初始化Vulkan后,我们需要在系统中查找并选择一个支持我们所需功能的显卡.实际上,我们可以选择任意数量的显卡并同时使用他们,但在本小节中,我们简单的设定选择规则,即将查找到的第一个图形卡作为我们适合的物理设备. 我们添加函数pickPhysicalDevice并在initVulkan函数中调用. vo

Vulkan Tutorial 01 开发环境搭建之Windows

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 相信很多人在开始学习Vulkan开发的起始阶段都会在开发环境的配置上下一些功夫,那么本问将会引导大家快速的完成Vulkan在Windows下的开发环境,并使用几个常用的开发工具库. Vulkan SDK 开发Vulkan应用程序所需的最重要的组件就是SDK.它包括核心头文件.标准的Validation layers及调试工具集.和驱动Loader,如果现在这些关键词不是很明白的话,

Vulkan Tutorial 03 理解Instance

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Creating an instance 与Vulkan打交道,通常的步骤是创建一个intance去初始化Vulkan library.这个instance是您的应用程序与Vulkan库之间的连接桥梁,通常创建过程中,需要向驱动程序提供一些应用层的信息. 首先添加一个createInstance函数,并在initVulkan函数中调用. void initVulkan() { cr

Vulkan Tutorial 04 理解Validation layers

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 What are validation layers? Vulkan API的设计核心是尽量最小化驱动程序的额外开销,所谓额外开销更多的是指向渲染以外的运算.其中一个具体的表现就是默认条件下,Vulkan API的错误检查的支持非常有限.即使遍历不正确的值或者将需要的参数传递为空指针,也不会有明确的处理逻辑,并且直接导致崩溃或者未定义的异常行为.之所以这样,是因为Vulkan要求每

Vulkan Tutorial 06 逻辑设备与队列

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在选择要使用的物理设备之后,我们需要设置一个逻辑设备用于交互.逻辑设备创建过程与instance创建过程类似,也需要描述我们需要使用的功能.因为我们已经查询过哪些队列簇可用,在这里需要进一步为逻辑设备创建具体类型的命令队列.如果有不同的需求,也可以基于同一个物理设备创建多个逻辑设备. 首先添加一个新的类成员来存储逻辑设备句柄. VkDevice devic

Vulkan Tutorial 18 重构交换链

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 现在我们已经成功的在屏幕上绘制出三角形,但是在某些情况下,它会出现异常情况.窗体surface会发生改变,使得交换链不在于其兼容.可能导致这种情况发生的原因之一是窗体的大小变化.我们必须在这个时机重新创建交换链. Recreating the swap chain 添加新的函数recreateSwapChain并调用createSwapChain及依赖于交

Vulkan Tutorial 13 Render passes

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Setup 在我们完成管线的创建工作,我们接下来需要告诉Vulkan渲染时候使用的framebuffer帧缓冲区附件相关信息.我们需要指定多少个颜色和深度缓冲区将会被使用,指定多少个采样器及如何在整个渲染操作中处理它们.所有的这些信息都被封装在一个叫做render pass的对象中,我们新添加一个createRenderPass函数.在initVulkan函数中确保createGr

Vulkan Tutorial 09 图像与视图

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 使用任何的VkImage,包括在交换链或者渲染管线中的,我们都需要创建VkImageView对象.从字面上理解它就是一个针对图像的视图或容器,通过它具体的渲染管线才能够读写渲染数据,换句话说VkImage不能与渲染管线进行交互.除此之外,图像视图可以进一步定义具体Image的格式,比如定义为2D贴图,那么本质上就不需要任何级别的mipmapping. 在本章节我们会新增一个crea