Heute möchte ich eine Übersetzung eines neuen Kapitels im Abschnitt über die Grundlagen der Grafikpipeline mit dem Titel "Feste Funktionen" vorstellen.
Inhalt
Nicht programmierbare Pipeline-Stufen
- Scheitelpunkteingabe
- Assembler eingeben
- Ansichtsfenster und Schere
- Rasterizer
- Multisampling
- Tiefentest und Schablonentest
- Farbmischung
- Dynamischer Zustand
- Pipeline-Layout
- Fazit
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 PunktVK_PRIMITIVE_TOPOLOGY_LINE_LIST
: Die Geometrie wird als Satz von Liniensegmenten gezeichnet. Jedes Scheitelpunktpaar bildet eine separate LinieVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: Die Geometrie wird als durchgehende Polylinie gezeichnet. Jeder nachfolgende Scheitelpunkt fügt der Polylinie ein Segment hinzuVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: Die Geometrie wird als eine Reihe von Dreiecken gezeichnet, wobei alle 3 Eckpunkte ein unabhängiges Dreieck bildenVK_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ülltVK_POLYGON_MODE_LINE
: Polygonkanten werden in Linien konvertiertVK_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.