Vulkan. Guia do desenvolvedor. Estágios de pipeline não programáveis

Eu trabalho como tradutor para CG Tribe em Izhevsk e aqui eu publico traduções do Tutorial Vulkan (original - vulkan-tutorial.com ) para o russo.



Hoje eu quero apresentar a tradução de um novo capítulo na seção sobre noções básicas do pipeline de gráficos, chamado Funções fixas.



Contente
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









Estágios de pipeline não programáveis





As APIs de gráficos iniciais usavam o estado padrão para a maioria dos estágios do pipeline de gráficos. No Vulkan, todos os estados devem ser descritos explicitamente, começando com o tamanho da janela de visualização e terminando com a função de mistura de cores. Neste capítulo, iremos configurar os estágios de pipeline não programáveis.



Entrada de vértice



A estrutura VkPipelineVertexInputStateCreateInfo descreve o formato dos dados do vértice que são passados ​​para o sombreador de vértice. Existem dois tipos de descrições:



  • Descrição dos atributos: tipo de dados passado para o sombreador de vértice, vinculação ao buffer de dados e deslocamento nele
  • Vinculação: a distância entre os itens de dados e como os dados e a geometria de saída são vinculados (por instância ou vinculação de vértice) (consulte Instanciação de geometria )


Como codificamos os dados do vértice no sombreador de vértice, indicaremos que não há dados para carregar. Para fazer isso, vamos preencher a estrutura VkPipelineVertexInputStateCreateInfo



. Voltaremos a essa questão mais tarde no capítulo sobre buffers de vértice.



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
      
      





Membros pVertexBindingDescriptions



e pVertexAttributeDescriptions



apontam para uma matriz de estruturas que descrevem os dados acima para carregar atributos de vértice. Adicione esta estrutura à função createGraphicsPipeline



logo a seguir shaderStages



.



Montador de entrada



A estrutura VkPipelineInputAssemblyStateCreateInfo descreve 2 coisas: qual geometria é formada a partir dos vértices e se o reinício da geometria é permitido para geometrias como faixa de linha e faixa de triângulo. A geometria é indicada no campo topology



e pode ter os seguintes valores:



  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST



    : a geometria é desenhada como pontos separados, cada vértice é um ponto separado
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST



    : a geometria é desenhada como um conjunto de segmentos de linha, cada par de vértices forma uma linha separada
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



    : a geometria é desenhada como uma polilinha contínua, cada vértice subsequente adiciona um segmento à polilinha
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST



    : a geometria é desenhada como um conjunto de triângulos, com cada 3 vértices formando um triângulo independente
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



    : ,


Normalmente, os vértices são carregados sequencialmente na ordem em que você os coloca no buffer de vértices. No entanto, com um buffer de índice, você pode alterar a ordem de carregamento. Isso permite otimizações como a reutilização de vértices. Se você primitiveRestartEnable



especificar um valor no campo VK_TRUE



, você pode interromper as linhas e triângulos com topologia VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



e VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



e começar a desenhar novas primitivas usando o índice especial 0xFFFF



ou 0xFFFFFFFF



.



No tutorial, desenharemos triângulos individuais, portanto, usaremos a seguinte estrutura:



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





Janela de visualização e tesouras



A janela de visualização descreve a área do framebuffer para a qual a saída é renderizada. Quase sempre as coordenadas de (0, 0)



a são definidas para a janela de visualização (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;
      
      





Esteja ciente de que o tamanho da cadeia de troca e das imagens pode ser diferente dos valores WIDTH



e da HEIGHT



janela. Posteriormente, as imagens da cadeia de troca serão usadas como framebuffers, portanto, devemos usar exatamente o seu tamanho.



minDepth



e maxDepth



determinar a faixa de valores de profundidade para o framebuffer. Esses valores devem estar no intervalo [0,0f, 1,0f]



e minDepth



pode haver mais maxDepth



. Use os valores padrão - 0.0f



e 1.0f



se você não vai fazer nada fora do comum.



Se a janela de visualização determinar como a imagem será esticada no framebuffer, a tesoura determinará quais pixels serão salvos. Todos os pixels fora do retângulo da tesoura serão descartados durante a rasterização. O retângulo de recorte é usado para recortar a imagem, não transformá-la. A diferença é mostrada nas fotos abaixo. Observe que o retângulo de recorte à esquerda é apenas uma das muitas opções possíveis para obter tal imagem, desde que seu tamanho seja maior do que o tamanho da janela de visualização.







Neste tutorial, queremos renderizar a imagem para todo o framebuffer, portanto, especificaremos que o retângulo da tesoura se sobrepõe completamente à janela de visualização:



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





Agora precisamos combinar as informações sobre a janela de visualização e a tesoura usando a estrutura VkPipelineViewportStateCreateInfo . Em algumas placas de vídeo, vários visores e retângulos de recorte podem ser usados ​​simultaneamente, portanto, as informações sobre eles são transmitidas como um array. Para usar várias janelas de visualização ao mesmo tempo, você precisa habilitar a opção GPU correspondente.



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





Rasterizador



O rasterizador converte a geometria de um sombreador de vértice em vários fragmentos. O teste de profundidade , seleção de face , teste de tesoura também são realizados aqui, e o método de preenchimento de polígonos com fragmentos é configurado: preenchendo todo o polígono ou apenas as bordas dos polígonos (renderização de wireframe). Tudo isso é configurado na estrutura VkPipelineRasterizationStateCreateInfo .



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





Se o campo for depthClampEnable



definido VK_TRUE



, os fragmentos dos quais estão fora dos planos próximos e distantes, não são cortados e os empurra. Isso pode ser útil, por exemplo, ao criar um mapa de sombra. Para usar este parâmetro, você deve habilitar a opção GPU correspondente.



rasterizer.rasterizerDiscardEnable = VK_FALSE;
      
      





Se rasterizerDiscardEnable



definido VK_TRUE



, o estágio de rasterização é desabilitado e nenhuma saída é passada para o framebuffer.



rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
      
      





polygonMode



determina como os pedaços são gerados. Os seguintes modos estão disponíveis:



  • VK_POLYGON_MODE_FILL



    : polígonos são completamente preenchidos com fragmentos
  • VK_POLYGON_MODE_LINE



    : as bordas do polígono são convertidas em linhas
  • VK_POLYGON_MODE_POINT



    : os vértices do polígono são desenhados como pontos


Para usar esses modos, exceto VK_POLYGON_MODE_FILL



, você precisa habilitar a opção GPU correspondente.




rasterizer.lineWidth = 1.0f;
      
      





O campo lineWidth



define a espessura dos segmentos. A largura máxima do bloco suportado depende do seu hardware, e os blocos mais grossos 1,0f



exigem que a opção GPU seja habilitada wideLines



.



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





O parâmetro cullMode



define o tipo de seleção de face. Você pode desativar o recorte totalmente ou ativar o recorte para as faces frontais e / ou não frontais. A variável frontFace



determina a ordem em que os vértices são percorridos (sentido horário ou anti-horário) para definir as faces frontais.



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





O rasterizador pode alterar os valores de profundidade adicionando um valor constante ou compensando a profundidade dependendo da inclinação do fragmento. Isso geralmente é usado ao criar um mapa de sombra. Não precisamos disso, então vamos depthBiasEnable



instalá- lo para VK_FALSE



.



Multisampling



A estrutura VkPipelineMultisampleStateCreateInfo configura multisampling - um dos métodos anti-aliasing . Ele funciona principalmente nas bordas, combinando cores de diferentes polígonos que são rasterizados nos mesmos pixels. Isso permite que você se livre dos artefatos mais visíveis. A principal vantagem da multisampling é que, na maioria dos casos, o sombreador de fragmento é executado apenas uma vez por pixel, o que é muito melhor, por exemplo, do que renderizar em uma resolução mais alta e depois reduzir o tamanho. Para usar multisampling, você deve habilitar a opção GPU correspondente.



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
      
      





Até que o incluamos, retornaremos a ele em um dos artigos a seguir.



Teste de profundidade e teste de estêncil



Ao usar um buffer de profundidade e / ou um buffer de estêncil, você precisa configurá-los usando VkPipelineDepthStencilStateCreateInfo . Não precisamos disso ainda, então apenas passaremos em nullptr



vez de um ponteiro para esta estrutura. Voltaremos a isso no capítulo sobre o buffer de profundidade.



Mistura de cores



A cor retornada pelo sombreador de fragmento precisa ser mesclada com a cor que já está no framebuffer. Esse processo é chamado de mistura de cores e há duas maneiras de fazer isso:



  • Misture o valor antigo com o novo para obter a cor de saída
  • Concatene o valor antigo e o novo usando a operação bit a bit


Dois tipos de estruturas são usados ​​para configurar a mistura de cores: a estrutura VkPipelineColorBlendAttachmentState contém configurações para cada framebuffer conectado, a estrutura VkPipelineColorBlendStateCreateInfo contém configurações globais de mistura de cores. Em nosso caso, apenas um framebuffer é usado:



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
      
      





A estrutura VkPipelineColorBlendAttachmentState



permite que você personalize a mistura de cores da primeira maneira. O seguinte pseudocódigo é a melhor demonstração de todas as operações realizadas:



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;
      
      





Se blendEnable



definido VK_FALSE



, a cor do sombreador de fragmento é passada inalterada. Se definido VK_TRUE



, duas operações de mistura são usadas para calcular a nova cor. A cor final é filtrada usando colorWriteMask



para determinar em quais canais da imagem de saída estão sendo gravados.



A mesclagem de cores mais comum é a mesclagem alfa, em que a nova cor é mesclada com a cor antiga com base na transparência. finalColor



é calculado da seguinte forma:



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





Isso pode ser configurado usando as seguintes opções:



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;
      
      





Todas as operações possíveis pode ser encontrada nas VkBlendFactor e enumerações VkBlendOp na especificação.



A segunda estrutura refere-se a uma série de estruturas para todos os framebuffers e permite a especificação de constantes de mistura que podem ser usadas como fatores de mistura nos cálculos acima.



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
      
      





Se você quiser usar o segundo método de mistura (operação bit a bit), defina VK_TRUE



para logicOpEnable



. Em seguida, você pode especificar a operação bit a bit no campo logicOp



. Note que o primeiro método ficará automaticamente indisponível, como se cada um deles estivesse conectado ao framebuffer blendEnable



fosse encontrado VK_FALSE



! Observe que colorWriteMask



também é usado para operações bit a bit para determinar qual conteúdo de canal será alterado. Você pode desligar os dois modos, como fizemos, neste caso as cores dos fragmentos serão gravadas no framebuffer sem alterações.



Estado dinâmico



Alguns estados do pipeline de gráficos podem ser alterados sem recriar o pipeline, como tamanho da janela de visualização, larguras de pedaços e constantes de mesclagem. Para fazer isso, preencha a estrutura 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;
      
      





Como resultado, os valores dessas configurações não são levados em consideração no estágio de criação do pipeline e você precisa especificá-los no momento da renderização. Voltaremos a isso nos próximos capítulos. Você pode usar em nullptr



vez de um ponteiro para esta estrutura se não quiser usar estados dinâmicos.



Layout de pipeline



Em shaders, você pode usar uniform



-variables - variáveis ​​globais que podem ser alteradas dinamicamente para alterar o comportamento dos shaders sem ter que recriá-los. Eles são normalmente usados ​​para passar uma matriz de transformação para um sombreador de vértice ou para criar amostradores de textura em um sombreador de fragmento.



Esses uniformes devem ser especificados ao criar o pipeline usando o objeto VkPipelineLayout . Mesmo que não estejamos usando essas variáveis ​​por enquanto, ainda precisamos criar um layout de pipeline vazio.



Vamos criar um membro da classe para conter o objeto, como iremos nos referir mais tarde a ele em outras funções:




VkPipelineLayout pipelineLayout;
      
      





Então, vamos criar um objeto em uma função 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!");
}
      
      





A estrutura também especifica constantes push, que são outra maneira de passar variáveis ​​dinâmicas para sombreadores. Vamos conhecê-los mais tarde. Usaremos o pipeline ao longo de todo o ciclo de vida do programa, então precisamos destruí-lo bem no final:



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





Conclusão



Isso é tudo que você precisa saber sobre estados não programáveis! Deu muito trabalho configurá-los do zero, mas agora você sabe quase tudo o que acontece no pipeline gráfico!



Para criar um pipeline gráfico, resta criar o último objeto - passagem de renderização.



All Articles