Vulkan. Entwicklerhandbuch. Nicht programmierbare Pipeline-Stufen

Ich arbeite als Übersetzer für CG Tribe in Ischewsk und veröffentliche hier Übersetzungen des Vulkan-Tutorials (original - vulkan-tutorial.com ) ins Russische.



Heute möchte ich eine Übersetzung eines neuen Kapitels im Abschnitt über die Grundlagen der Grafikpipeline mit dem Titel "Feste Funktionen" vorstellen.



Inhalt
1.



2.



3.



4.







  1. (pipeline)



5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Nicht programmierbare Pipeline-Stufen





Frühe Grafik-APIs verwendeten den Standardstatus für die meisten Phasen der Grafik-Pipeline. In Vulkan müssen alle Zustände explizit beschrieben werden, beginnend mit der Größe des Ansichtsfensters und endend mit der Farbmischfunktion. In diesem Kapitel werden wir die nicht programmierbaren Pipeline-Stufen einrichten.



Scheitelpunkteingabe



Die Struktur VkPipelineVertexInputStateCreateInfo beschreibt das Format der Scheitelpunktdaten, die an den Scheitelpunkt-Shader übergeben werden. Es gibt zwei Arten von Beschreibungen:



  • Beschreibung der Attribute: Datentyp, der an den Vertex-Shader übergeben, an den Datenpuffer gebunden und darin versetzt wird
  • Bindung: Der Abstand zwischen Datenelementen und wie die Daten und die Ausgabegeometrie gebunden sind (pro Instanz oder Scheitelpunktbindung) (siehe Geometrieinstanzierung ).


Da wir die Scheitelpunktdaten im Scheitelpunkt-Shader fest codiert haben, geben wir an, dass keine Daten geladen werden müssen. Füllen Sie dazu die Struktur aus VkPipelineVertexInputStateCreateInfo



. Wir werden später in diesem Kapitel über Scheitelpunktpuffer auf diese Frage zurückkommen.



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
      
      





Mitglieder pVertexBindingDescriptions



und pVertexAttributeDescriptions



verweisen auf ein Array von Strukturen, die die obigen Daten zum Laden von Scheitelpunktattributen beschreiben. Fügen Sie diese Struktur createGraphicsPipeline



direkt danach der Funktion hinzu shaderStages



.



Assembler eingeben



Die VkPipelineInputAssemblyStateCreateInfo- Struktur beschreibt zwei Dinge: Welche Geometrie wird aus Scheitelpunkten gebildet und ob ein Neustart der Geometrie für Geometrien wie Linienstreifen und Dreiecksstreifen zulässig ist. Die Geometrie wird im Feld angezeigt topology



und kann folgende Werte annehmen:



  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST



    : Geometrie wird als separater Punkt gezeichnet, jeder Scheitelpunkt ist ein separater Punkt
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST



    : Die Geometrie wird als Satz von Liniensegmenten gezeichnet. Jedes Scheitelpunktpaar bildet eine separate Linie
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



    : Die Geometrie wird als durchgehende Polylinie gezeichnet. Jeder nachfolgende Scheitelpunkt fügt der Polylinie ein Segment hinzu
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST



    : Die Geometrie wird als eine Reihe von Dreiecken gezeichnet, wobei alle 3 Eckpunkte ein unabhängiges Dreieck bilden
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



    : ,


In der Regel werden Scheitelpunkte nacheinander in der Reihenfolge geladen, in der Sie sie in den Scheitelpunktpuffer legen. Mit einem Indexpuffer können Sie jedoch die Ladereihenfolge ändern. Dies ermöglicht Optimierungen wie die Wiederverwendung von Scheitelpunkten. Wenn Sie primitiveRestartEnable



einen Wert angeben im Feld VK_TRUE



, können Sie die Linien und Dreiecke mit Topologie unterbrechen VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



und VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



und starten neue Primitive zeichnen den besonderen Index verwendet 0xFFFF



oder 0xFFFFFFFF



.



Im Tutorial werden wir einzelne Dreiecke zeichnen, daher verwenden wir die folgende Struktur:



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





Ansichtsfenster und Schere



Das Ansichtsfenster beschreibt den Bereich des Framebuffers, in den die Ausgabe gerendert wird. Fast immer werden die Koordinaten von (0, 0)



bis für das Ansichtsfenster festgelegt (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;
      
      





Bitte beachten Sie, dass die Größe der Swap-Kette und der Bilder von den Werten WIDTH



und dem HEIGHT



Fenster abweichen kann . Später werden die Bilder aus der Swap-Kette als Framebuffer verwendet, daher müssen wir genau ihre Größe verwenden.



minDepth



und maxDepth



bestimmen Sie den Bereich der Tiefenwerte für den Framebuffer. Diese Werte müssen im Bereich liegen [0,0f, 1,0f]



, und minDepth



möglicherweise sind weitere vorhanden maxDepth



. Verwenden Sie die Standardwerte - 0.0f



und 1.0f



wenn Sie nichts Außergewöhnliches tun wollen.



Wenn das Ansichtsfenster bestimmt, wie das Bild im Framebuffer gestreckt wird, bestimmt die Schere, welche Pixel gespeichert werden. Alle Pixel außerhalb des Scherenrechtecks ​​werden während der Rasterung verworfen. Das Beschneidungsrechteck wird verwendet, um das Bild zuzuschneiden, nicht um es zu transformieren. Der Unterschied ist in den folgenden Bildern dargestellt. Bitte beachten Sie, dass das Beschneidungsrechteck auf der linken Seite nur eine von vielen möglichen Optionen ist, um ein solches Bild zu erhalten, sofern es größer als das Ansichtsfenster ist.







In diesem Tutorial möchten wir das Bild für den gesamten Framebuffer rendern. Daher geben wir an, dass das Scherenrechteck das Ansichtsfenster vollständig überlappt:



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





Jetzt müssen wir die Informationen über das Ansichtsfenster und die Schere mithilfe der VkPipelineViewportStateCreateInfo- Struktur kombinieren . Bei einigen Grafikkarten können mehrere Ansichtsfenster und Beschneidungsrechtecke gleichzeitig verwendet werden, sodass Informationen über sie als Array übertragen werden. Um mehrere Ansichtsfenster gleichzeitig zu verwenden, müssen Sie die entsprechende GPU-Option aktivieren.



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



Der Rasterizer konvertiert die Geometrie von einem Vertex-Shader in mehrere Fragmente. Hier wird auch der Tiefentest , das Keulen der Fläche und der Scherentest durchgeführt , und die Methode zum Füllen von Polygonen mit Fragmenten wird konfiguriert: Füllen des gesamten Polygons oder nur der Kanten von Polygonen (Drahtgitter-Rendering). All dies wird in der VkPipelineRasterizationStateCreateInfo- Struktur konfiguriert .



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





Wenn das Feld depthClampEnable



eingestellt ist VK_TRUE



, dessen Fragmente außerhalb der nahen und fernen Ebene liegen, werden sie nicht abgeschnitten und schieben sie. Dies kann beispielsweise beim Erstellen einer Schattenkarte hilfreich sein. Um diesen Parameter verwenden zu können, müssen Sie die entsprechende GPU-Option aktivieren.



rasterizer.rasterizerDiscardEnable = VK_FALSE;
      
      





Wenn rasterizerDiscardEnable



festgelegt VK_TRUE



, ist die Rasterungsstufe deaktiviert und es wird keine Ausgabe an den Framebuffer übergeben.



rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
      
      





polygonMode



legt fest, wie Chunks generiert werden. Folgende Modi stehen zur Verfügung:



  • VK_POLYGON_MODE_FILL



    : Polygone sind vollständig mit Fragmenten gefüllt
  • VK_POLYGON_MODE_LINE



    : Polygonkanten werden in Linien konvertiert
  • VK_POLYGON_MODE_POINT



    : Polygonscheitelpunkte werden als Punkte gezeichnet


Um diese Modi zu verwenden, VK_POLYGON_MODE_FILL



müssen Sie die entsprechende GPU-Option aktivieren.




rasterizer.lineWidth = 1.0f;
      
      





Das Feld lineWidth



legt die Dicke der Segmente fest. Die maximal unterstützte Blockbreite hängt von Ihrer Hardware ab. Bei dickeren Blöcken muss 1,0f



die GPU-Option aktiviert sein wideLines



.



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





Der Parameter cullMode



definiert den Face-Culling-Typ. Sie können das Clipping vollständig deaktivieren oder das Clipping für vordere und / oder nicht vordere Flächen aktivieren. Die Variable frontFace



bestimmt die Reihenfolge, in der die Scheitelpunkte durchlaufen werden (im oder gegen den Uhrzeigersinn), um die Vorderflächen zu definieren.



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





Der Rasterer kann die Tiefenwerte ändern, indem er einen konstanten Wert hinzufügt oder die Tiefe abhängig von der Neigung des Fragments versetzt. Dies wird normalerweise beim Erstellen einer Schattenkarte verwendet. Wir brauchen das nicht, also depthBiasEnable



installieren wir es für VK_FALSE



.



Multisampling



Die Struktur VkPipelineMultisampleStateCreateInfo konfiguriert Multisampling - eine der Anti-Aliasing- Methoden . Es funktioniert hauptsächlich an den Kanten und kombiniert Farben aus verschiedenen Polygonen, die in die gleichen Pixel gerastert werden. Auf diese Weise können Sie die sichtbarsten Artefakte entfernen. Der Hauptvorteil von Multisampling besteht darin, dass der Fragment-Shader in den meisten Fällen nur einmal pro Pixel ausgeführt wird. Dies ist beispielsweise viel besser als das Rendern mit einer höheren Auflösung und das anschließende Verkleinern. Um Multisampling verwenden zu können, müssen Sie die entsprechende GPU-Option aktivieren.



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
      
      





Bis wir es aufnehmen, werden wir in einem der folgenden Artikel darauf zurückkommen.



Tiefentest und Schablonentest



Wenn Sie einen Tiefenpuffer und / oder einen Schablonenpuffer verwenden, müssen Sie diese mit VkPipelineDepthStencilStateCreateInfo konfigurieren . Wir brauchen das noch nicht, also übergeben wir es einfach nullptr



anstelle eines Zeigers auf diese Struktur. Wir werden darauf im Kapitel über den Tiefenpuffer zurückkommen.



Farbmischung



Die vom Fragment-Shader zurückgegebene Farbe muss mit der Farbe zusammengeführt werden, die sich bereits im Framebuffer befindet. Dieser Vorgang wird als Farbmischung bezeichnet, und es gibt zwei Möglichkeiten, dies zu tun:



  • Mischen Sie alten und neuen Wert, um die Ausgabefarbe zu erhalten
  • Verketten Sie alten und neuen Wert mit bitweiser Operation


Zum Konfigurieren der Farbmischung werden zwei Arten von Strukturen verwendet: Die VkPipelineColorBlendAttachmentState- Struktur enthält Einstellungen für jeden verbundenen Framebuffer, die VkPipelineColorBlendStateCreateInfo- Struktur enthält globale Farbmischungseinstellungen. In unserem Fall wird nur ein Framebuffer verwendet:



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
      
      





Mit der Struktur VkPipelineColorBlendAttachmentState



können Sie die Farbmischung zunächst anpassen. Der folgende Pseudocode ist die beste Demonstration aller durchgeführten Operationen:



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;
      
      





Wenn blendEnable



festgelegt VK_FALSE



, wird die Farbe aus dem Fragment-Shader unverändert übergeben. Wenn festgelegt VK_TRUE



, werden zwei Mischvorgänge verwendet, um die neue Farbe zu berechnen. Die endgültige Farbe wird gefiltert, um colorWriteMask



zu bestimmen, auf welche Kanäle des Ausgabebildes geschrieben wird.



Die häufigste Farbmischung ist die Alpha-Mischung, bei der die neue Farbe basierend auf der Transparenz mit der alten Farbe gemischt wird. finalColor



wird wie folgt berechnet:



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





Dies kann mit den folgenden Optionen konfiguriert werden:



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;
      
      





Alle möglichen Operationen finden Sie in den Aufzählungen VkBlendFactor und VkBlendOp in der Spezifikation.



Die zweite Struktur bezieht sich auf ein Array von Strukturen für alle Framebuffer und ermöglicht die Angabe von Mischungskonstanten, die als Mischungsfaktoren in den obigen Berechnungen verwendet werden können.



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
      
      





Wenn Sie die zweite Mischverfahren (bitweise Operation) verwenden möchten, stellen Sie VK_TRUE



für logicOpEnable



. Anschließend können Sie die bitweise Operation im Feld angeben logicOp



. Beachten Sie, dass die erste Methode automatisch nicht mehr verfügbar ist, als ob jede mit dem Framebuffer verbundene blendEnable



gefunden worden wäre VK_FALSE



! Beachten Sie, dass es colorWriteMask



auch für bitweise Operationen verwendet wird, um zu bestimmen, welcher Kanalinhalt geändert wird. Sie können beide Modi wie bisher deaktivieren. In diesem Fall werden die Farben der Fragmente unverändert in den Framebuffer geschrieben.



Dynamischer Zustand



Einige Zustände der Grafikpipeline können geändert werden, ohne die Pipeline neu zu erstellen, z. B. die Größe des Ansichtsfensters, die Blockbreite und die Mischungskonstanten. Füllen Sie dazu die Struktur VkPipelineDynamicStateCreateInfo aus :



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;
      
      





Infolgedessen werden die Werte dieser Einstellungen beim Erstellen der Pipeline nicht berücksichtigt, und Sie müssen sie direkt zum Zeitpunkt des Renderns angeben. Wir werden in den nächsten Kapiteln darauf zurückkommen. Sie können nullptr



anstelle eines Zeigers auf diese Struktur verwenden, wenn Sie keine dynamischen Zustände verwenden möchten.



Pipeline-Layout



In Shadern können Sie uniform



-variables verwenden - globale Variablen, die dynamisch geändert werden können, um das Verhalten der Shader zu ändern, ohne sie neu erstellen zu müssen. Sie werden normalerweise verwendet, um eine Transformationsmatrix an einen Vertex-Shader zu übergeben oder um Textur-Sampler in einem Fragment-Shader zu erstellen.



Diese Uniformen müssen beim Erstellen der Pipeline mit dem VkPipelineLayout- Objekt angegeben werden . Auch wenn wir diese Variablen vorerst nicht verwenden, müssen wir dennoch ein leeres Pipeline-Layout erstellen.



Erstellen wir ein Mitglied der Klasse, das das Objekt enthält, wie wir später in anderen Funktionen darauf verweisen werden:




VkPipelineLayout pipelineLayout;
      
      





Dann erstellen wir ein Objekt in einer Funktion 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!");
}
      
      





Die Struktur gibt auch Push-Konstanten an, die eine weitere Möglichkeit darstellen, dynamische Variablen an Shader zu übergeben. Wir werden sie später kennenlernen. Wir werden die Pipeline während des gesamten Lebenszyklus des Programms nutzen, daher müssen wir sie ganz am Ende zerstören:



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





Fazit



Das ist alles, was Sie über nicht programmierbare Zustände wissen müssen! Es hat viel Arbeit gekostet, sie von Grund auf neu einzurichten, aber jetzt wissen Sie fast alles, was in der Grafik-Pipeline passiert!



Um eine Grafik-Pipeline zu erstellen, muss noch der letzte Objekt-Render-Durchgang erstellt werden.



All Articles