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
Estágios de pipeline não programáveis
- Entrada de vértice
- Montador de entrada
- Janela de visualização e tesouras
- Rasterizador
- Multisampling
- Teste de profundidade e teste de estêncil
- Mistura de cores
- Estado dinâmico
- Layout de pipeline
- Conclusão
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 separadoVK_PRIMITIVE_TOPOLOGY_LINE_LIST
: a geometria é desenhada como um conjunto de segmentos de linha, cada par de vértices forma uma linha separadaVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: a geometria é desenhada como uma polilinha contínua, cada vértice subsequente adiciona um segmento à polilinhaVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: a geometria é desenhada como um conjunto de triângulos, com cada 3 vértices formando um triângulo independenteVK_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 fragmentosVK_POLYGON_MODE_LINE
: as bordas do polígono são convertidas em linhasVK_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.