Aujourd'hui, je souhaite présenter une traduction d'un nouveau chapitre dans la section sur les bases du pipeline graphique appelé Fonctions fixes.
Contenu
Étages de pipeline non programmables
- Entrée Vertex
- Assembleur d'entrée
- Fenêtre et ciseaux
- Rastériseur
- Multi-échantillonnage
- Test de profondeur et test de pochoir
- Mélange de couleurs
- État dynamique
- Disposition du pipeline
- Conclusion
Les premières API graphiques utilisaient l'état par défaut pour la plupart des étapes du pipeline graphique. Dans Vulkan, tous les états doivent être décrits explicitement, en commençant par la taille de la fenêtre et en terminant par la fonction de mélange de couleurs. Dans ce chapitre, nous allons mettre en place les étapes de pipeline non programmables.
Entrée Vertex
La structure VkPipelineVertexInputStateCreateInfo décrit le format des données de sommet qui sont transmises au shader de vertex. Il existe deux types de descriptions:
- Description des attributs: type de données passé au vertex shader, liaison au tampon de données et décalage dans celui-ci
- Liaison: la distance entre les éléments de données et la manière dont les données et la géométrie en sortie sont liées (liaison par instance ou par sommet) (voir Instanciation de la géométrie )
Puisque nous avons codé en dur les données de vertex dans le vertex shader, nous indiquerons qu'il n'y a pas de données à charger. Pour ce faire, remplissons la structure
VkPipelineVertexInputStateCreateInfo
. Nous reviendrons sur cette question plus loin dans le chapitre sur les tampons de vertex.
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
Membres
pVertexBindingDescriptions
et
pVertexAttributeDescriptions
pointez sur un tableau de structures qui décrivent les données ci-dessus pour le chargement des attributs de sommet. Ajoutez cette structure à la fonction
createGraphicsPipeline
juste après
shaderStages
.
Assembleur d'entrée
La structure VkPipelineInputAssemblyStateCreateInfo décrit 2 choses: quelle géométrie est formée à partir de sommets et si le redémarrage de la géométrie est autorisé pour les géométries telles que la bande de ligne et la bande de triangle. La géométrie est indiquée dans le champ
topology
et peut avoir les valeurs suivantes:
VK_PRIMITIVE_TOPOLOGY_POINT_LIST
: la géométrie est dessinée en tant que points séparés, chaque sommet est un point distinctVK_PRIMITIVE_TOPOLOGY_LINE_LIST
: la géométrie est dessinée comme un ensemble de segments de ligne, chaque paire de sommets forme une ligne distincteVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: la géométrie est dessinée comme une polyligne continue, chaque sommet suivant ajoute un segment à la polyligneVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: la géométrie est dessinée comme un ensemble de triangles, avec tous les 3 sommets formant un triangle indépendantVK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
: ,
En règle générale, les sommets sont chargés séquentiellement dans l'ordre dans lequel vous les placez dans le tampon de sommets. Cependant, avec un tampon d'index, vous pouvez modifier l'ordre de chargement. Cela permet des optimisations telles que la réutilisation des sommets. Si vous
primitiveRestartEnable
spécifiez une valeur dans le champ
VK_TRUE
, vous pouvez interrompre les lignes et triangles avec la topologie
VK_PRIMITIVE_TOPOLOGY_LINE_STRIP
et
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
et lancer de nouvelles primitives de dessin à l' aide de l'index spécial
0xFFFF
ou
0xFFFFFFFF
.
Dans le didacticiel, nous allons dessiner des triangles individuels, nous utiliserons donc la structure suivante:
VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE;
Fenêtre et ciseaux
La fenêtre décrit la zone du framebuffer sur laquelle la sortie est rendue. Presque toujours, les coordonnées de
(0, 0)
à sont définies pour la fenêtre
(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;
Veuillez noter que la taille de la chaîne d'échange et des images peut différer des valeurs
WIDTH
et de la
HEIGHT
fenêtre. Plus tard, les images de la chaîne d'échange seront utilisées comme framebuffers, nous devons donc utiliser exactement leur taille.
minDepth
et
maxDepth
déterminer la plage de valeurs de profondeur pour le framebuffer. Ces valeurs doivent être comprises dans la plage
[0,0f, 1,0f]
et il
minDepth
peut y en avoir plus
maxDepth
. Utilisez les valeurs par défaut -
0.0f
et
1.0f
si vous n'allez rien faire d'extraordinaire.
Si la fenêtre détermine comment l'image sera étirée dans le framebuffer, les ciseaux déterminent les pixels qui seront enregistrés. Tous les pixels en dehors du rectangle de ciseaux seront ignorés lors de la pixellisation. Le rectangle de détourage est utilisé pour recadrer l'image, pas pour la transformer. La différence est montrée dans les images ci-dessous. Veuillez noter que le rectangle de détourage sur la gauche n'est qu'une des nombreuses options possibles pour obtenir une telle image, à condition que sa taille soit supérieure à la taille de la fenêtre.
Dans ce didacticiel, nous voulons rendre l'image dans tout le framebuffer, nous allons donc spécifier que le rectangle de ciseaux chevauche complètement la fenêtre:
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
Nous devons maintenant combiner les informations sur la fenêtre et le ciseau à l'aide de la structure VkPipelineViewportStateCreateInfo . Sur certaines cartes vidéo, plusieurs fenêtres et rectangles de découpage peuvent être utilisés simultanément, de sorte que les informations les concernant sont transmises sous forme de tableau. Pour utiliser plusieurs fenêtres à la fois, vous devez activer l'option GPU correspondante.
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
Rastériseur
Le rasterizer convertit la géométrie d'un vertex shader en plusieurs fragments. Le test de profondeur , la sélection des faces , le test des ciseaux sont également effectués ici, et la méthode de remplissage des polygones avec des fragments est configurée: remplissage de tout le polygone, ou uniquement des bords des polygones (rendu filaire). Tout cela est configuré dans la structure VkPipelineRasterizationStateCreateInfo .
VkPipelineRasterizationStateCreateInfo rasterizer{}; rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizer.depthClampEnable = VK_FALSE;
Si le champ est
depthClampEnable
défini
VK_TRUE
, dont les fragments sont en dehors des plans proche et éloigné, ne sont pas coupés, et les pousse. Cela peut être utile, par exemple, lors de la création d'une carte d'ombre. Pour utiliser ce paramètre, vous devez activer l'option GPU correspondante.
rasterizer.rasterizerDiscardEnable = VK_FALSE;
S'il est
rasterizerDiscardEnable
défini
VK_TRUE
, l'étape de rastérisation est désactivée et aucune sortie n'est transmise au framebuffer.
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
polygonMode
détermine la manière dont les blocs sont générés. Les modes suivants sont disponibles:
VK_POLYGON_MODE_FILL
: les polygones sont complètement remplis de fragmentsVK_POLYGON_MODE_LINE
: les arêtes de polygone sont converties en lignesVK_POLYGON_MODE_POINT
: les sommets du polygone sont dessinés sous forme de points
Pour utiliser ces modes, sauf que
VK_POLYGON_MODE_FILL
vous devez activer l'option GPU correspondante.
rasterizer.lineWidth = 1.0f;
Le champ
lineWidth
définit l'épaisseur des segments. La largeur de bloc maximale prise en charge dépend de votre matériel, et les blocs plus épais
1,0f
nécessitent l'activation de l'option GPU
wideLines
.
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
Le paramètre
cullMode
définit le type d'élimination des faces. Vous pouvez désactiver complètement le découpage ou activer le découpage pour les faces avant et / ou non avant. La variable
frontFace
détermine l'ordre dans lequel les sommets sont traversés (dans le sens horaire ou antihoraire) pour définir les faces avant.
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional
Le rastériseur peut modifier les valeurs de profondeur en ajoutant une valeur constante ou en décalant la profondeur en fonction de la pente du fragment. Ceci est généralement utilisé lors de la création d'une carte d'ombre. Nous n'avons pas besoin de cela, nous allons donc l'
depthBiasEnable
installer pour
VK_FALSE
.
Multi-échantillonnage
La structure VkPipelineMultisampleStateCreateInfo configure le multi-échantillonnage - l'une des méthodes d' anti-aliasing . Il fonctionne principalement sur les bords, combinant les couleurs de différents polygones qui sont pixellisés dans les mêmes pixels. Cela vous permet de vous débarrasser des artefacts les plus visibles. Le principal avantage du multi-échantillonnage est que, dans la plupart des cas, le shader de fragment est exécuté une seule fois par pixel, ce qui est bien meilleur, par exemple, que le rendu à une résolution plus élevée puis la réduction de taille. Pour utiliser le multi-échantillonnage, vous devez activer l'option GPU correspondante.
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
Jusqu'à ce que nous l'incluions, nous y reviendrons dans l'un des articles suivants.
Test de profondeur et test de pochoir
Lorsque vous utilisez un tampon de profondeur et / ou un tampon de stencil, vous devez les configurer à l'aide de VkPipelineDepthStencilStateCreateInfo . Nous n'en avons pas encore besoin, nous allons donc simplement le passer
nullptr
au lieu d'un pointeur vers cette structure. Nous y reviendrons dans le chapitre sur le tampon de profondeur.
Mélange de couleurs
La couleur renvoyée par le fragment shader doit être fusionnée avec la couleur déjà présente dans le framebuffer. Ce processus s'appelle le mélange de couleurs et il existe deux façons de le faire:
- Mélangez l'ancienne et la nouvelle valeur pour obtenir la couleur de sortie
- Concaténer l'ancienne et la nouvelle valeur à l'aide d'une opération au niveau du bit
Deux types de structures sont utilisés pour configurer le mélange de couleurs: la structure VkPipelineColorBlendAttachmentState contient des paramètres pour chaque framebuffer connecté, la structure VkPipelineColorBlendStateCreateInfo contient les paramètres globaux de mélange de couleurs. Dans notre cas, un seul framebuffer est utilisé:
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
La structure
VkPipelineColorBlendAttachmentState
vous permet de personnaliser le mélange de couleurs de la première manière. Le pseudo-code suivant est la meilleure démonstration de toutes les opérations effectuées:
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;
S'il est
blendEnable
défini
VK_FALSE
, la couleur du fragment shader est transmise inchangée. S'il est défini
VK_TRUE
, deux opérations de fusion sont utilisées pour calculer la nouvelle couleur. La couleur finale est filtrée en utilisant
colorWriteMask
pour déterminer sur quels canaux de l'image de sortie sont écrits.
Le mélange de couleurs le plus courant est le mélange alpha, où la nouvelle couleur est mélangée avec l'ancienne couleur en fonction de la transparence.
finalColor
est calculé comme suit:
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
Cela peut être configuré à l'aide des options suivantes:
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;
Toutes les opérations possibles se trouvent dans les énumérations VkBlendFactor et VkBlendOp de la spécification.
La deuxième structure fait référence à un tableau de structures pour tous les tampons d'image et permet de spécifier des constantes de mélange qui peuvent être utilisées comme facteurs de mélange dans les calculs ci-dessus.
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
Si vous souhaitez utiliser la deuxième méthode de mixage (opération au niveau du bit), définissez
VK_TRUE
pour
logicOpEnable
. Ensuite, vous pouvez spécifier l'opération au niveau du bit dans le champ
logicOp
. Notez que la première méthode deviendra automatiquement indisponible, comme si chacune d'entre elles était connectée au framebuffer
blendEnable
avait été trouvée
VK_FALSE
! Notez qu'il
colorWriteMask
est également utilisé pour les opérations au niveau du bit afin de déterminer le contenu du canal qui sera modifié. Vous pouvez désactiver les deux modes, comme nous l'avons fait, dans ce cas, les couleurs des fragments seront écrites dans le framebuffer sans changement.
État dynamique
Certains états du pipeline graphique peuvent être modifiés sans recréer le pipeline, tels que la taille de la fenêtre, la largeur des blocs et les constantes de fusion. Pour ce faire, remplissez la structure 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;
Par conséquent, les valeurs de ces paramètres ne sont pas prises en compte au stade de la création du pipeline et vous devez les spécifier au moment du rendu. Nous y reviendrons dans les prochains chapitres. Vous pouvez utiliser à la
nullptr
place d'un pointeur vers cette structure si vous ne souhaitez pas utiliser d'états dynamiques.
Disposition du pipeline
Dans les shaders, vous pouvez utiliser
uniform
-variables - des variables globales qui peuvent être modifiées dynamiquement pour changer le comportement des shaders sans avoir à les recréer. Ils sont généralement utilisés pour transmettre une matrice de transformation à un vertex shader ou pour créer des échantillonneurs de texture dans un fragment shader.
Ces uniformes doivent être spécifiés lors de la création du pipeline à l'aide de l'objet VkPipelineLayout . Même si nous n'utiliserons pas ces variables pour le moment, nous devons toujours créer une disposition de pipeline vide.
Créons un membre de la classe pour contenir l'objet, comme nous y ferons référence plus tard à partir d'autres fonctions:
VkPipelineLayout pipelineLayout;
Créons ensuite un objet dans une fonction
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!");
}
La structure spécifie également les constantes push, qui sont une autre façon de transmettre des variables dynamiques aux shaders. Nous apprendrons à les connaître plus tard. Nous utiliserons le pipeline tout au long du cycle de vie du programme, nous devons donc le détruire à la toute fin:
void cleanup() {
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
...
}
Conclusion
C'est tout ce qu'il y a à savoir sur les états non programmables! Il a fallu beaucoup de travail pour les configurer à partir de zéro, mais maintenant vous savez presque tout ce qui se passe dans le pipeline graphique!
Pour créer un pipeline graphique, il reste à créer le dernier objet - la passe de rendu.