Vulkan教程 - 09 固定管線

固定管線

老式圖形API爲多數圖形管線提供了默認狀態。而在Vulkan中你必須明確所有的東西,從視口大小到混合函數。本章我們會填充所有的結構體來配置這些固定管線操作。

VkPipelineVertexInputStateCreateInfo結構體描述了將要傳給頂點着色器的頂點數據的格式,它主要通過以下兩種方式描述:

綁定:數據間的距離以及數據是否是逐頂點或者逐實例的;

屬性描述:傳給頂點着色器的屬性的種類,從哪個綁定加載以及從哪個偏移處開始。

因爲我們在頂點着色器中進行硬編碼,我們會填充該結構體來明確暫時沒有頂點數據要加載,等頂點緩衝章節再回來看:

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

pVertexBindingDescriptions和pVertexAttributeDescriptions成員指向一組結構體,描述了前面提到的加載頂點數據的細節。上面這段代碼添加到createGraphicsPipeline的shaderStages數組後面。

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:每個三角形的第二和第三個頂點作爲下個三角形的頭兩個頂點。

通常頂點會根據索引從頂點緩衝中依次加載,但是有了元素緩衝,你可以明確自己要用的頂點。這就允許你進行頂點重用之類的優化了。如果你設置了primitiveRestartEnable成員爲真,就可以在_STRIP拓撲模式中使用特殊索引如0xFFFF或0xFFFFFFFF打破線和三角形。

我們整個教程就是打算畫個三角形,所以我們會用下面的數據構建結構體:

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

視口大致描述了輸出要渲染到的幀緩衝區域。這基本上都是(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;

記住交換鏈的大小和它的圖像可能會和窗口的寬高不一樣。交換鏈圖像將會被用作後續的幀緩衝,所以就保持他們的大小。

minDepth和maxDepth的值表明了幀緩衝要用的深度值範圍。這些值必須在[0.0f, 1.0f]之間,但是minDepth可能比maxDepth要高。

雖然視口定義了從圖像到幀緩衝的變換,裁剪矩形定義了像素實際上存儲到什麼區域。任何在裁剪矩形外邊的像素都會被光柵器丟棄。說是變換器,實際上它們更像是一個過濾器:

本教程我們就想要簡單繪製整個幀緩衝,所以我們會指定一個能將其完全覆蓋的裁剪矩形:

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

現在本視口和裁剪矩形需要用VkPipelineViewportStateCreateInfo結構體綁定到一個視口狀態。某些顯卡可以使用多個視口和裁剪矩形,所以它的成員會引用一組視口和裁剪矩形。多重使用則要求啓用一個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;

光柵器使用來自頂點着色器的頂點構建的幾何對象轉換成供片段着色器着色的片段。它也會進行深度測試,面剔除和裁剪測試,可以配置輸出充滿整個多邊形或者僅僅是邊(網格線渲染)的片段。這些都通過VkPipelineRasterizationStateCreateInfo結構體創建:

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

如果depthClampEnable設置爲真,那麼在近處或者遠處平面外的片段會被被截斷到它們上面而不是被丟棄。這對於一些特殊情況比較有用,比如陰影映射圖。用這個的話需要啓用一個GPU特性:

rasterizer.rasterizerDiscardEnable = VK_FALSE;

這裏如果設置爲真了,那麼幾何圖形就不會傳遞到光柵器階段。這樣就基本禁用了到幀緩衝的任何輸出:

polygonMode決定了如何爲幾何圖形生成片段:

rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

這裏有以下幾種模式可選:

VK_POLYGON_MODE_FILL:用片段填充多邊形區域;

VK_POLYGON_MODE_LINE:多邊形的邊畫作線;

VK_POLYGON_MODE_POINT:多邊形的頂點畫作點。

使用任何非填充模式需要啓用一個GPU特性:

rasterizer.lineWidth = 1.0f;

lineWidth成員就比較直白了,它描述的是根據片段數量來說的線的寬度。支持的最大線寬度取決於硬件,任何比1.0f大的寬度需要啓用GPU的wideLines特性。

剔除模式cullMode變量決定了面剔除要用的類型。你可以禁用面剔除,剔除前面,剔除後面或者全剔除。frontFace變量明確了考慮面是否爲前面的時候的頂點順序,可以順時針或者逆時針:

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

光柵器可以通過添加一個常量或者根據片段斜率偏離來改變深度值,這有時候用於陰影映射,我們這裏不用,所以就置爲假即可:

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

多重採樣通過VkPipelineMultisampleStateCreateInfo配置,這是進行抗鋸齒的方式之一。它通過組合光柵化同一像素的多個多邊形的片段着色器的結果來工作,這主要發生在邊上,也是最容易注意到的走樣發生的地方。因爲如果只有一個多邊形映射到一個像素的話,它就不用多次運行片段着色器,這比僅僅渲染到高分辨率然後壓縮尺寸會節省得多開銷。啓用的話需要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;
multisampling.alphaToOneEnable = VK_FALSE;

當前我們先禁用了,以後再來看多重採樣。

如果你要用深度或者模板緩衝,你也需要使用VkPipelineDepthStencilStateCreateInfo配置深度和模板測試。我們現在並沒有用,所以就傳個nullptr即可,以後回來看深度緩衝。

片段着色器返回一個顏色後,它需要與已經在幀緩衝中的顏色進行組合。該變換就是顏色混合,有兩種方式處理:

將新的和舊的混合產生最終顏色;

用按位操作組合新舊顏色值。

有兩種結構體來配置顏色混合。第一種是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;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
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設置爲假的情況下,來自片段着色器的新顏色會不經修改地進行傳遞。否則,執行這兩個混合操作來計算新的顏色。最終顏色和colorWriteMask進行按位與操作,確定通過哪些通道傳輸。

最常用的顏色混合方式是實現alpha混合,就是想新顏色基於舊顏色的透明度進行混合。那麼最終顏色就是這麼計算的:

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

這可以用下面的參數實現:

colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC1_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;

你可以在VkBlendFactor和VkBlendOp枚舉中找到所有可能的操作。

第二個結構體引用所有幀緩衝的結構體數組並允許你設置混合常數作爲前面提到的計算中的混合因子。

如果你想要使用第二種混合方法(按位結合),那麼你應該設置logicOpEnable爲真,按位運算類型接着在logicOp中指明。注意這會自動禁用第一種方式,如同你對每個附着的幀緩衝設置了blendEnable爲假一樣。這個模式中也會使用colorWriteMask,以確定實際要影響幀緩衝的哪一個通道。也可以兩種模式都不用,像我們這裏做的一樣,這樣片段顏色就會不加修改的寫入幀緩衝中。

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

我們在前面結構體中指定的一些狀態可以不用重建管線就進行修改。例如視口大小,線寬度和混合常數。如果你想這麼做,那麼先要像這樣填充一個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。

你可以在着色器中使用統一的值,就是一些全局變量,和動態狀態變量類似,可以在繪製的時候進行修改來改變其行爲而不用重新創建它們。它們通常用於傳遞變換矩陣給頂點着色器,或者在片段着色器中創建材質採樣。

這些統一的值需要在管線創建的過程中通過創建一個VkPipelineLayout對象來指明。雖然以後的章節中才會用到,但是我們還是要創建一個空的管線佈局。

首先創建一個類成員來存儲該對象,因爲我們以後會在其他方法中引用它:

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 = nullptr;  // optional

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

該結構體也指明瞭push constants,這是另一個傳遞動態值給着色器的方法。管線佈局的引用會貫穿整個程序生命週期,所以在最後的cleanup中銷燬:

vkDestroyPipelineLayout(device, pipelineLayout, nullptr);

這行代碼就放在cleanup最開始的地方。

這就是固定管線狀態的所有內容了。但是還有一個對象要創建,就是渲染通道,之後才能創建圖形管線,這就是後面的內容了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章