Vulkan. Guía del desarrollador. Etapas de tubería no programables

Trabajo como traductor para CG Tribe en Izhevsk y aquí publico traducciones del Tutorial de Vulkan (original - vulkan-tutorial.com ) al ruso.



Hoy quiero presentar una traducción de un nuevo capítulo en la sección sobre los conceptos básicos de la canalización de gráficos llamada Funciones fijas.



Contenido
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









Etapas de tubería no programables





Las primeras API de gráficos usaban el estado predeterminado para la mayoría de las etapas de la canalización de gráficos. En Vulkan, todos los estados deben describirse explícitamente, comenzando con el tamaño de la ventana gráfica y terminando con la función de mezcla de colores. En este capítulo, configuraremos las etapas de canalización no programables.



Entrada de vértice



La estructura VkPipelineVertexInputStateCreateInfo describe el formato de los datos de vértice que se pasan al sombreador de vértices. Hay dos tipos de descripciones:



  • Descripción de los atributos: tipo de datos que se pasa al sombreador de vértices, se vincula al búfer de datos y se desplaza en él
  • Enlace: la distancia entre los elementos de datos y cómo se enlazan los datos y la geometría de salida (enlace por instancia o vértice) (consulte Creación de instancias de geometría )


Dado que codificamos los datos de vértice en el sombreador de vértices, indicaremos que no hay datos para cargar. Para hacer esto, completemos la estructura VkPipelineVertexInputStateCreateInfo



. Volveremos a esta pregunta más adelante en el capítulo sobre búferes de vértices.



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
      
      





Miembros pVertexBindingDescriptions



y pVertexAttributeDescriptions



apuntan a una matriz de estructuras que describen los datos anteriores para cargar atributos de vértice. Agregue esta estructura a la función createGraphicsPipeline



inmediatamente después shaderStages



.



Ensamblador de entrada



La estructura VkPipelineInputAssemblyStateCreateInfo describe 2 cosas: qué geometría se forma a partir de los vértices y si se permite el reinicio de la geometría para geometrías como la tira de línea y la tira de triángulo. La geometría se indica en el campo topology



y puede tener los siguientes valores:



  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST



    : la geometría se dibuja como puntos separados, cada vértice es un punto separado
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST



    : la geometría se dibuja como un conjunto de segmentos de línea, cada par de vértices forma una línea separada
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



    : la geometría se dibuja como una polilínea continua, cada vértice subsiguiente agrega un segmento a la polilínea
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST



    : la geometría se dibuja como un conjunto de triángulos, con cada 3 vértices formando un triángulo independiente
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



    : ,


Normalmente, los vértices se cargan secuencialmente en el orden en que los coloca en el búfer de vértices. Sin embargo, con un búfer de índice, puede cambiar el orden de carga. Esto permite optimizaciones como la reutilización de vértices. Si se primitiveRestartEnable



especifica un valor en el campo VK_TRUE



, puede interrumpir las líneas y triángulos con topología VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



y VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



y empezar a dibujar nuevas primitivas utilizando el índice especial 0xFFFF



o 0xFFFFFFFF



.



En el tutorial, dibujaremos triángulos individuales, por lo que usaremos la siguiente estructura:



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





Ventana y tijeras



La ventana gráfica describe el área del framebuffer en el que se renderiza la salida. Casi siempre las coordenadas de (0, 0)



a se establecen para la ventana gráfica (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;
      
      





Tenga en cuenta que el tamaño de la cadena de intercambio y las imágenes pueden diferir de los valores WIDTH



y la HEIGHT



ventana. Posteriormente, las imágenes de la cadena de intercambio se usarán como framebuffers, por lo que debemos usar exactamente su tamaño.



minDepth



y maxDepth



determinar el rango de valores de profundidad para el framebuffer. Estos valores deben estar en el rango [0,0f, 1,0f]



y minDepth



puede haber más maxDepth



. Utilice los valores predeterminados, 0.0f



y 1.0f



si no va a hacer nada fuera de lo común.



Si la ventana gráfica determina cómo se estirará la imagen en el búfer de fotogramas, la tijera determina qué píxeles se guardarán. Todos los píxeles fuera del rectángulo de tijera se descartarán durante la rasterización. El rectángulo de recorte se utiliza para recortar la imagen, no para transformarla. La diferencia se muestra en las imágenes a continuación. Tenga en cuenta que el rectángulo de recorte de la izquierda es solo una de las muchas opciones posibles para obtener una imagen de este tipo, siempre que su tamaño sea mayor que el tamaño de la ventana gráfica.







En este tutorial, queremos renderizar la imagen a todo el framebuffer, por lo que especificaremos que el rectángulo de tijera se superpone completamente a la ventana gráfica:



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





Ahora necesitamos combinar la información sobre la ventana gráfica y la tijera usando la estructura VkPipelineViewportStateCreateInfo . En algunas tarjetas de video, se pueden usar varias ventanas gráficas y rectángulos de recorte simultáneamente, por lo que la información sobre ellos se transmite como una matriz. Para usar varias ventanas gráficas a la vez, debe habilitar la opción de GPU correspondiente.



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



El rasterizador convierte la geometría de un sombreador de vértices en varios fragmentos. Aquí también se realiza la prueba de profundidad , el sacrificio de rostros , la prueba de tijera, y se configura el método de relleno de polígonos con fragmentos: rellenar todo el polígono o solo los bordes de los polígonos (renderizado alámbrico). Todo esto está configurado en la estructura VkPipelineRasterizationStateCreateInfo .



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





Si el campo está depthClampEnable



configurado VK_TRUE



, cuyos fragmentos están fuera de los planos cercano y lejano, no se cortan y los empuja. Esto puede resultar útil, por ejemplo, al crear un mapa de sombras. Para utilizar este parámetro, debe habilitar la opción de GPU correspondiente.



rasterizer.rasterizerDiscardEnable = VK_FALSE;
      
      





Si se rasterizerDiscardEnable



establece VK_TRUE



, la etapa de rasterización se deshabilita y no se pasa ninguna salida al framebuffer.



rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
      
      





polygonMode



determina cómo se generan los fragmentos. Están disponibles los siguientes modos:



  • VK_POLYGON_MODE_FILL



    : los polígonos están completamente llenos de fragmentos
  • VK_POLYGON_MODE_LINE



    : los bordes del polígono se convierten en líneas
  • VK_POLYGON_MODE_POINT



    : los vértices de los polígonos se dibujan como puntos


Para usar estos modos, excepto VK_POLYGON_MODE_FILL



, debe habilitar la opción de GPU correspondiente.




rasterizer.lineWidth = 1.0f;
      
      





El campo lineWidth



establece el grosor de los segmentos. El ancho de fragmento máximo admitido depende de su hardware, y los fragmentos más gruesos 1,0f



requieren que la opción GPU esté habilitada wideLines



.



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





El parámetro cullMode



define el tipo de eliminación de rostros. Puede desactivar el recorte por completo o activar el recorte para caras frontales y / o no frontales. La variable frontFace



determina el orden en el que se recorren los vértices (en sentido horario o antihorario) para definir las caras frontales.



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





El rasterizador puede cambiar los valores de profundidad agregando un valor constante o compensando la profundidad según la pendiente del fragmento. Suele utilizarse al crear un mapa de sombras. No necesitamos esto, así que lo depthBiasEnable



instalaremos para VK_FALSE



.



Muestreo múltiple



La estructura VkPipelineMultisampleStateCreateInfo configura el muestreo múltiple, uno de los métodos de suavizado . Funciona principalmente en los bordes, combinando colores de diferentes polígonos que se rasterizan en los mismos píxeles. Esto le permite deshacerse de los artefactos más visibles. La principal ventaja del multimuestreo es que, en la mayoría de los casos, el sombreador de fragmentos se ejecuta solo una vez por píxel, lo que es mucho mejor, por ejemplo, que renderizar a una resolución más alta y luego reducir el tamaño. Para utilizar el multimuestreo, debe habilitar la opción de GPU correspondiente.



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
      
      





Hasta que lo incluyamos, volveremos a él en uno de los siguientes artículos.



Prueba de profundidad y prueba de esténcil



Cuando use un búfer de profundidad y / o un búfer de plantilla, debe configurarlos usando VkPipelineDepthStencilStateCreateInfo . No necesitamos esto todavía, así que simplemente lo pasaremos en nullptr



lugar de un puntero a esta estructura. Volveremos a esto en el capítulo sobre el búfer de profundidad.



Mezcla de colores



El color devuelto por el sombreador de fragmentos debe fusionarse con el color que ya está en el framebuffer. Este proceso se llama mezcla de colores y hay dos formas de hacerlo:



  • Mezcle valores antiguos y nuevos para obtener el color de salida
  • Concatenar valor antiguo y nuevo mediante operación bit a bit


Se utilizan dos tipos de estructuras para configurar la mezcla de colores: la estructura VkPipelineColorBlendAttachmentState contiene configuraciones para cada framebuffer conectado, la estructura VkPipelineColorBlendStateCreateInfo contiene configuraciones globales de mezcla de colores. En nuestro caso, solo se usa un 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;
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 estructura le VkPipelineColorBlendAttachmentState



permite personalizar la mezcla de colores de la primera forma. El siguiente pseudocódigo es la mejor demostración de todas las operaciones 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;
      
      





Si se blendEnable



establece VK_FALSE



, el color del sombreador de fragmentos se pasa sin cambios. Si se establece VK_TRUE



, se utilizan dos operaciones de fusión para calcular el nuevo color. El color final se filtra utilizando colorWriteMask



para determinar en qué canales de la imagen de salida se están escribiendo.



La combinación de colores más común es la combinación alfa, en la que el nuevo color se combina con el antiguo en función de la transparencia. finalColor



se calcula de la siguiente manera:



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





Esto se puede configurar usando las siguientes opciones:



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 las operaciones posibles se pueden encontrar en las enumeraciones VkBlendFactor y VkBlendOp en la especificación.



La segunda estructura se refiere a una matriz de estructuras para todos los framebuffers y permite especificar las constantes de mezcla que se pueden usar como factores de mezcla en los cálculos anteriores.



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 desea utilizar el segundo método de mezcla (operación en modo bit), establecido VK_TRUE



para logicOpEnable



. Luego, puede especificar la operación bit a bit en el campo logicOp



. Tenga en cuenta que el primer método se convertirá automáticamente disponible, como si cada uno está conectado con el uso de este dispositivo blendEnable



se ha encontrado VK_FALSE



! Tenga en cuenta que colorWriteMask



también se utiliza para operaciones bit a bit para determinar qué contenido de canal se cambiará. Puede desactivar ambos modos, como hicimos nosotros, en este caso los colores de los fragmentos se escribirán en el framebuffer sin cambios.



Estado dinámico



Algunos estados de la canalización de gráficos se pueden cambiar sin volver a crear la canalización, como el tamaño de la ventana gráfica, los anchos de los fragmentos y las constantes de combinación. Para hacer esto, complete la estructura 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, los valores de esta configuración no se tienen en cuenta en la etapa de creación de la canalización y debe especificarlos en el momento de la renderización. Volveremos a esto en los próximos capítulos. Puede usar en nullptr



lugar de un puntero a esta estructura si no desea usar estados dinámicos.



Disposición de la tubería



En los sombreadores, puede usar uniform



-variables - variables globales que se pueden cambiar dinámicamente para cambiar el comportamiento de los sombreadores sin tener que volver a crearlos. Por lo general, se utilizan para pasar una matriz de transformación a un sombreador de vértices o para crear muestras de textura en un sombreador de fragmentos.



Estos uniformes deben especificarse al crear la canalización con el objeto VkPipelineLayout . Aunque no usaremos estas variables por ahora, todavía necesitamos crear un diseño de canalización vacío.



Creemos un miembro de la clase para contener el objeto, como luego nos referiremos a él desde otras funciones:




VkPipelineLayout pipelineLayout;
      
      





Entonces creemos un objeto en una función 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 estructura también especifica las constantes de inserción, que son otra forma de pasar variables dinámicas a los sombreadores. Los conoceremos más tarde. Usaremos el pipeline durante todo el ciclo de vida del programa, por lo que debemos destruirlo al final:



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





Conclusión



¡Eso es todo lo que hay que saber sobre los estados no programables! Me costó mucho trabajo configurarlos desde cero, ¡pero ahora sabes casi todo lo que sucede en el proceso de gráficos!



Para crear una canalización de gráficos, queda crear el último objeto: pase de renderizado.



All Articles