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
Etapas de tubería no programables
- Entrada de vértice
- Ensamblador de entrada
- Ventana y tijeras
- Rasterizador
- Muestreo múltiple
- Prueba de profundidad y prueba de esténcil
- Mezcla de colores
- Estado dinámico
- Disposición de la tubería
- Conclusión
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 separadoVK_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 separadaVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: la geometría se dibuja como una polilínea continua, cada vértice subsiguiente agrega un segmento a la polilíneaVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: la geometría se dibuja como un conjunto de triángulos, con cada 3 vértices formando un triángulo independienteVK_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 fragmentosVK_POLYGON_MODE_LINE
: los bordes del polígono se convierten en líneasVK_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.