Link

Estágios de função fixa

Nesta subseção, preencheremos todas as estruturas para configurar os estágios de função fixa.

Estado dinâmico

Antes de começarmos a implementar os estágios de função fixa do pipeline descritos anteriormente iremos primeiro definir o estado dinâmico do pipeline. Esse estado especifica o número total de estados dinâmicos usados no pipeline atual e seus respectivos objetos no pipeline. Isso pode incluir a viewport (janela de visualização), a largura de linha do estágio de rasterização, as constantes de mesclagem do estágio de color blending e assim por diante. Cada estado dinâmico no Vulkan é representado usando o valor enum do tipo VkDynamicState.

Iremos definir os estados viewport (VK_DYNAMIC_STATE_VIEWPORT) e scissor (VK_DYNAMIC_STATE_SCISSOR) como dinâmicos.

VkDynamicState dynamicStates[] = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_SCISSOR
};

A variável dynamicStates é atribuída ao objeto de estrutura de controle VkPipelineDynamicStateCreateInfo, que posteriormente será consumido por um objeto VkGraphicsPipelineCreateInfo para criar o pipeline gráfico.

VkPipelineDynamicStateCreateInfo dynamicInfo = {};
dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicInfo.dynamicStateCount = 2;
dynamicInfo.pDynamicStates = dynamicStates;

Isso fará com que a configuração desses valores seja ignorada e seremos solicitados a especificar os dados no momento do desenho. Adicionamos essa estrutura à função initPipeline logo após o array shaderStages.

Entrada de vértices

Uma vez que os estados dinâmicos são definidos, podemos avançar e construir o próximo estágio de função fixa do pipeline. A próxima etapa desse processo é o estágio vertex input (entrada de vértices). Esse estágio especifica a ligação de entrada (VkVertexInputBindingDescription) e os descritores de atributo de vértice (VkVertexInputAttributeDescription). A ligação de entrada define o espaçamento entre dados e se os dados são por vértice ou por instância. Por outro lado, o descritor de atributo de vértice armazena informações importantes, como localização, binding (ligação), formato e assim por diante. Isso é usado para interpretar dados de vértices. Essas informações são agrupadas no objeto de estrutura VkPipelineVertexInputStateCreateInfo, que será usado posteriormente para criar o pipeline gráfico.

Como estamos codificando os dados dos vértices diretamente no vertex shader, preencheremos essa estrutura para especificar que não há dados de vértices para carregar agora. Nós voltaremos nela mais tarde.

VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;

Os membros pVertexBindingDescriptions e pVertexAttributeDescriptions apontam para um array de estruturas que descrevem os detalhes acima mencionados para o carregamento de dados de vértices.

Input assembly

O estágio input assembly (montagem de entrada) do pipeline gráfico recebe os dados de vértice e os agrupa em primitivas prontas para processamento pelo restante do pipeline. Ele é descrito por uma instância da estrutura VkPipelineInputAssemblyStateCreateInfo. Assim como no OpenGL, devemos especificar qual topologia queremos usar: pontos, linhas, triângulos, e assim por diante. A topologia de primitiva é especificada no membro topology dessa estrutura, que deve ser uma das topologias de primitivas suportadas pelo Vulkan. Estas são membros da enumeração VkPrimitiveTopology e podem ter valores como:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST: Cada vértice é usado para construir um ponto independente.
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST: Os vértices são agrupados em pares, cada par formando um segmento de linha do primeiro ao segundo vértice.
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: Os dois primeiros vértices formam um único segmento de linha. Cada novo vértice depois deles forma um novo segmento de linha a partir do último vértice processado. O resultado é uma sequência conectada de linhas.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: Vértices são agrupados em tripletos que formam triângulos.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: Os três primeiros vértices formam um único triângulo. Cada vértice subsequente forma um novo triângulo junto com os dois últimos vértices. O resultado é uma fileira conectada de triângulos, cada um compartilhando uma borda com o último.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: Os três primeiros vértices formam um único triângulo. Cada vértice subsequente forma um novo triângulo junto com o último vértice e o primeiro vértice no desenho.

Nós pretendemos desenhar triângulos ao longo deste projeto, então vamos nos ater aos seguintes dados para a estrutura:

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

O último campo nessa estrutura é primitiveRestartEnable. Esse é um sinalizador usado para permitir que topologias primitivas de strip (faixa) e fan (leque) sejam cortadas e reiniciadas. Sem isso, cada strip ou fan precisaria ser desenhado separadamente. Quando usamos a reinicialização, vários strips ou fans podem ser combinados em uma única operação de desenho. Os reinícios são efetivados somente quando são usados comandos de desenho indexados porque o ponto no qual reiniciar a strip é marcado usando um valor especial reservado no index buffer.

Rasterização

A próxima parte da criação do pipeline gráfico se aplica ao estágio de rasterização (rasterization). Devemos especificar como os polígonos serão rasterizados (transformados em fragmentos), o que significa se queremos que fragmentos sejam gerados para polígonos inteiros ou apenas suas arestas (polygon mode) ou se queremos ver a frente ou o verso ou talvez ambos lados do polígono (face culling). Também podemos fornecer parâmetros de depth bias (viés de profundidade) ou indicar se queremos ativar o clamp de profundidade. Todo esse estado é encapsulado em VkPipelineRasterizationStateCreateInfo.

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

Para depthClampEnable, usamos VK_TRUE se o valor de profundidade para fragmentos cuja profundidade está fora do intervalo minDepth/maxDepth especificado em uma viewport deve ser fixado dentro desse intervalo ou usamos VK_FALSE se fragmentos com profundidade fora desse intervalo devem ser descartados. Para ativar o clamp de profundidade, primeiro precisamos ativar o recurso depthClampEnable durante a criação do dispositivo lógico.

rasterizationInfo.rasterizerDiscardEnable = VK_FALSE;

Se rasterizerDiscardEnable for definido como VK_TRUE desativa a geração de fragmentos (descarta as primitivas antes da rasterização desativando o fragment shader).

rasterizationInfo.polygonMode = VK_POLYGON_MODE_FILL;

O campo polygonMode pode ser usado para fazer com que o Vulkan transforme triângulos em pontos ou linhas automaticamente. Os valores possíveis para polygonMode são:

  • VK_POLYGON_MODE_FILL: Esse é o modo normal usado para preencher triângulos. Os triângulos serão desenhados sólidos, e cada ponto dentro do triângulo criará um fragmento.
  • VK_POLYGON_MODE_LINE: Este modo transforma os triângulos em linhas, com cada borda de cada triângulo se tornando uma linha. Isso é útil para desenhar geometria no modo de estrutura de wireframe (arame).
  • VK_POLYGON_MODE_POINT: Este modo simplesmente desenha cada vértice como um ponto.

Os modos de linhas e pontos só podem ser usados se o recurso fillModeNonSolid tiver sido ativado durante a criação do dispositivo lógico.

rasterizationInfo.cullMode = VK_CULL_MODE_BACK_BIT;

A eliminação de faces ocultas pela orientação em relação ao observador (culling) é controlada com cullMode, que pode ser zero ou uma combinação bit a bit de um dos seguintes valores:

  • VK_CULL_MODE_FRONT_BIT: Polígonos (triângulos) que estão voltados para o observador são descartados.
  • VK_CULL_MODE_BACK_BIT: Polígonos que estão voltados para o sentido oposto ao observador são descartados.

Por conveniência, o Vulkan define VK_CULL_MODE_FRONT_AND_BACK como a combinação de VK_CULL_MODE_FRONT_BIT e VK_CULL_MODE_BACK_BIT. Definir cullMode para este valor resultará em todos os triângulos sendo descartados.

rasterizationInfo.frontFace = VK_FRONT_FACE_CLOCKWISE;

O campo frontFace especifica a ordem dos vértices para que as faces sejam consideradas voltadas para a frente e podem ser no sentido horário (VK_FRONT_FACE_CLOCKWISE) ou anti-horário (VK_FRONT_FACE_COUNTER_CLOCKWISE).

rasterizationInfo.depthBiasEnable = VK_FALSE;
rasterizationInfo.depthBiasConstantFactor = 0.0f;
rasterizationInfo.depthBiasClamp = 0.0f;
rasterizationInfo.depthBiasSlopeFactor = 0.0f;

Os próximos quatro parâmetros – depthBiasEnable, depthBiasConstantFactor, depthBiasClamp e depthBiasSlopeFactor – controlam o recurso de bias de profundidade. Esse recurso permite que os fragmentos sejam deslocados em profundidade antes do teste de profundidade e podem ser usados para evitar depth-fighting (ou Z-fighting).

rasterizationInfo.lineWidth = 1.0f;

Por fim, lineWidth é o valor escalar que controla a largura dos segmentos de linha rasterizada. A largura máxima de linha que é suportada depende do hardware e qualquer largura de linha maior do que 1.0f requer que ativemos o recurso wideLines durante a criação do dispositivo lógico.

Color blending

O estágio de color blending (mistura de cores) obtém as saídas de cores de fragmento do fragment shader e as combina com as cores nos framebuffers para os quais essas saídas são mapeadas. Os parâmetros de blending (mesclagem) podem permitir que as cores de origem e destino de cada saída sejam combinadas de várias maneiras.

Existem dois tipos de estruturas para configurar o estágio de color blending. A primeira estrutura, VkPipelineColorBlendAttachmentState, contém a configuração por framebuffer anexado. No nosso caso, temos apenas um framebuffer:

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;

Essa estrutura por framebuffer permite que misturemos o valor antigo e o novo para produzir uma cor final. Queremos que o novo valor de cor do fragmento seja passado sem modificações, para isso definimos blendEnable como VK_FALSE.

A segunda estrutura, VkPipelineColorBlendStateCreateInfo, contém as configurações globais de color blending.

VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;

Essa estrutura faz referência ao array de estruturas do tipo VkPipelineColorBlendAttachmentState dos framebuffers e permite combinar o valor antigo e novo usando uma operação bit a bit. O campo logicOpEnable especifica se é necessário executar operações lógicas entre a saída do fragment shader e o conteúdo dos anexos de cor. Quando logicOpEnable for VK_FALSE, as operações lógicas serão desativadas e os valores produzidos pelo fragment shader serão gravados no anexo de cor sem modificação.

Layout de pipeline

A última coisa que devemos fazer antes da criação do pipeline é criar um layout de pipeline adequado através de um objeto VkPipelineLayout. Os layouts de pipeline são semelhantes aos layouts dos conjuntos de descritores. Os layouts do conjunto de descritores são usados para definir quais tipos de recursos formam um determinado conjunto de descritores. Os layouts de pipeline definem quais tipos de recursos podem ser acessados por um determinado pipeline. Eles são criados a partir de layouts de conjuntos de descritores e, além disso, push constants6.

6 Push constants são outra forma de transmitir valores dinâmicos aos shaders.

Os layouts de pipeline são necessários para a criação do pipeline, pois eles especificam a interface entre os estágios do shader e os recursos do shader por meio de um endereço (set = X, binding = Y), que pode ser traduzido para: obter o recurso do local da memória Y do conjunto X. Mesmo que não usemos isso até uma seção futura, ainda precisamos criar um layout de pipeline vazio.

Criamos um membro de classe para manter esse objeto, porque nos referiremos a ele de outras funções em um momento posterior:

VkPipelineLayout m_pipelineLayout = nullptr;

Em seguida, criamos esse objeto na função initPipeline:

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;

VkResult result = m_deviceFunctions->vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout);
if (result != VK_SUCCESS)
	qFatal("Failed to create pipeline layout: %d", result);

Esse objeto VkPipelineLayout será referenciado durante toda a vida útil do programa, portanto, ele deve ser destruído no final. Para isso, implementamos a função releaseResources na classe Renderer:

void Renderer::releaseResources() {
    VkDevice device = m_window->device();

    m_deviceFunctions->vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr);
}

Isso é tudo para todo o estado de função fixa7. Como podemos notar, é muito trabalhoso criar tudo isso do zero, mas a vantagem é que agora estamos quase totalmente cientes de tudo o que está acontecendo no pipeline gráfico. Isso reduz a chance de entrar em comportamento inesperado porque o estado padrão de determinados componentes não é o esperado.

7 Na verdade, ainda temos que configurar os estados da viewport e scissor, mas como definimos esses estados como dinâmicos eles só serão configurados numa seção futura.


Anterior Próximo