Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 3c

Creación del pipeline de renderizado

 

Objetivos

 

Completa la  creación del pipeline de renderizado incluyendo los shaders y las opciones configurables.

 

 

La configuración del Pipeline de renderizado

 

Para describir un proceso de renderizado en Vulkan es necesario crear un objeto VkPipeline por medio de la función vkCreateGraphicsPipelines(). Realmente esta función permite crear un conjunto de objetos VkPipeline. El parámetro createInfoCount indica el número de objetos a crear y el campo pCreateInfos contiene la información para crear cada objeto. 

VkResult vkCreateGraphicsPipelines (
  VkDevice                            device,
  VkPipelineCache                     pipelineCache,
  uint32_t                            createInfoCount,
  const VkGraphicsPipelineCreateInfo* pCreateInfos,
  const VkAllocationCallbacks*        pAllocator,
  VkPipeline*                         pPipelines); 

Para destruir un objeto VkPipeline se usa la función vkDestroyPipeline().

void vkDestroyPipeline (
  VkDevice                     device,
  VkPipeline                   pipeline,
  const VkAllocationCallbacks* pAllocator);

La configuración del pipeline de renderizado se describe en estructuras VkGraphicsPipelineCreateInfo. A continuación se muestra la definición de esta estructura que, como puede apreciarse, está formada por numerosos campos.

typedef struct VkGraphicsPipelineCreateInfo {
  VkStructureType                               sType;
  const void*                                   pNext;
  VkPipelineCreateFlags                         flags;
  uint32_t                                      stageCount;
  const VkPipelineShaderStageCreateInfo*        pStages;
  const VkPipelineVertexInputStateCreateInfo*   pVertexInputState;
  const VkPipelineInputAssemblyStateCreateInfo* pInputAssemblyState;
  const VkPipelineTessellationStateCreateInfo*  pTessellationState;
  const VkPipelineViewportStateCreateInfo*      pViewportState;
  const VkPipelineRasterizationStateCreateInfo* pRasterizationState;
  const VkPipelineMultisampleStateCreateInfo*   pMultisampleState;
  const VkPipelineDepthStencilStateCreateInfo*  pDepthStencilState;
  const VkPipelineColorBlendStateCreateInfo*    pColorBlendState;
  const VkPipelineDynamicStateCreateInfo*       pDynamicState;
  VkPipelineLayout                              layout;
  VkRenderPass                                  renderPass;
  uint32_t                                      subpass;
  VkPipeline                                    basePipelineHandle;
  int32_t                                       basePipelineIndex;
} VkGraphicsPipelineCreateInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO.

El campo pNext suele dejarse nulo, aunque existen extensiones de algunos fabricantes que requieren información extra que puede introducirse en este campo.

El campo flags puede ser nulo o alguno de los siguientes valores (las extensiones han añadido muchos otros posibles valores, por ejemplo para incorporar etapas de ray tracing) :

  • VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT: indica que no es necesario optimizar el pipeline, de manera que se acelere la creación aunque el resultado sea un pipeline más lento en ejecución.

  • VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT: indica que se pueden crear nuevos pipelines derivados de este, de manera que el intercambio entre ellos pueda hacerse más rápido.

  • VK_PIPELINE_CREATE_DERIVATIVE_BIT: indica que el pipeline a crear es un derivado de un pipeline anterior.

El campo stageCount contiene el número de shaders incluidos en el pipeline y que corresponde al tamaño del array pStages.

El campo pStages contiene el conjunto de estructuras VkPipelineShaderStageCreateInfo que definen los shaders incluidos en el pipeline. El contenido de esta estructura y la forma de definir los shaders se describieron en el proyecto anterior.

El campo pVertexInputState describe los atributos de entrada utilizados en el pipeline. Estos atributos corresponden a las variables de entrada descritas en el Vertex Shader. En esta práctica no se van a utilizar atributos almacenados en memoria por lo que la descripción en este caso será muy simple.

El campo pInputAssemblyState indica la forma de las primitivas a ensamblar (puntos, líneas, triángulos o patches).

El campo pTessellationState  describe el número de puntos que forman un patch cuando el pipeline incluye teselado.

El campo pViewportState define las dimensiones del trozo de imagen a generar. Normalmente coincide con el tamaño completo de la imagen, aunque puede configurarse un pipeline que solo genere un trozo. También incluye información sobre el recorte a mostrar (scissor), que será el trozo del viewport que se copiará en la imagen.

El campo pRasterizationState describe detalles del proceso de rasterización previo al Fragment Shader. Aquí se incluyen aspectos como el modo de dibujo (puntos, arista, relleno), la forma de distinguir la cara frontal de la posterior en los triángulos, el grosor de las líneas, …

El campo pMultisampleState define la forma en que se va ejecutar el Fragment Shader sobre cada fragmento. Por defecto el color del fragmento se calcula con la posición del centro del pixel, pero se pueden configurar procesos de multisampleado en los que el color del fragmento es un promedio del cálculo realizado sobre varias posiciones.

El campo pDepthStencilState describe el test de profundidad (depth) y el test de plantilla (stencil). El test de profundidad permite saber si un píxel a dibujar ha sido dibujado previamente como parte de un objeto que está más cerca. El test de plantilla permite realizar comparaciones con otra información de estos píxeles.

El campo pColorBlendState indica la forma de mezclar colores entre fragmentos con niveles de transparencia.

El campo pDynamicState permite indicar que algunos de los aspectos configurados por las estructuras anteriores van a ser dinámicos y pueden ser modificados por comandos sin necesidad de tener que crear un pipeline diferente.

El campo layout permite describir los Descriptor Sets y las Push Constants del proceso de renderizado. Estos objetos se utilizan para asignar los valores de las variables uniformes definidas en los shaders. Por ejemplo, las matrices de transformación espacial, la información sobre el modelo de iluminación o las texturas son valores que se suelen introducir en el pipeline mediante este tipo de objetos.

El campo renderPass contiene una estructura de tipo VkRenderPass  donde se describe la forma en la que la imagen a generar se va a almacenar. La imagen final se puede generar en varias pasadas, que se pueden configurar como “subpasses” de un mismo renderPass.

El campo subpass indica el índice de la subpasada en el que se va a ejecutar el pipeline (que será 0 si solo hay una pasada).

El campo basePipelineHandle indica cual es el pipeline padre si estamos creando un pipeline derivado. En caso contrario debe dejarse a nulo.

El campo basePipelineIndex es el índice que ocupa el pipeline padre en la lista a generar por el método vkCreateGraphicsPipelines() si estamos creando un pipeline derivado. En caso contrario se deja a cero.

 

 

La estructura VkPipelineVertexInputStateCreateInfo

 

La estructura VkPipelineVertexInputStateCreateInfo describe la forma en la que se accede a los atributos de los vértices.

typedef struct VkPipelineVertexInputStateCreateInfo {
  VkStructureType                          sType;
  const void*                              pNext;
  VkPipelineVertexInputStateCreateFlags    flags;
  uint32_t                                 vertexBindingDescriptionCount;
  const VkVertexInputBindingDescription*   pVertexBindingDescriptions;
  uint32_t                                 vertexAttributeDescriptionCount;
  const VkVertexInputAttributeDescription* pVertexAttributeDescriptions;
} VkPipelineVertexInputStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO.

El campo pNext por el momento debe dejarse a nulo.

El campo flags está reservado para un uso futuro y por el momento se deja a cero.

El campo vertexBindingDescriptionCount contiene el número de estructuras descritas en el siguiente campo.

El campo pVertexBindingDescriptions es un array de estructuras que describen la forma de acceder a los atributos.

El campo vertexAttributeDescriptionCount contiene el número de atributos descritos en el siguiente campo.

El campo pVertexAttributeDescriptions define el tipo de dato correspondiente al atributo, la posición que ocupa en el Vertex Shader y la forma de acceder a él.

 

 

La estructura VkPipelineInputAssemblyStateCreateInfo

 

La estructura VkPipelineInputAssemblyStateCreateInfo permite describir la forma de las primitivas a dibujar.

typedef struct VkPipelineInputAssemblyStateCreateInfo {
  VkStructureType                         sType;
  const void*                             pNext;
  VkPipelineInputAssemblyStateCreateFlags flags;
  VkPrimitiveTopology                     topology;
  VkBool32                                primitiveRestartEnable;
} VkPipelineInputAssemblyStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo topology describe la topología, es decir, la forma de las primitivas a dibujar. Debe ser uno de los valores de la enumeración VkPrimitiveTopology.

El campo primitiveRestartEnable se utiliza para trocear las listas. Generalmente se deja a falso, pero si se activa indica que en los dibujos indexados el índice -1 marca el final de una lista.

Las topologías que se admiten en Vulkan son las siguientes (con el prefijo VK_PRIMITIVE_TOPOLOGY_):

Topologías1

Topologías2

 

 

La estructura VkPipelineTessellationStateCreateInfo

 

La estructura VkPipelineTessellationStateCreateInfo indica la configuración del proceso de teselado.

typedef struct VkPipelineTessellationStateCreateInfo {
  VkStructureType                        sType;
  const void*                            pNext;
  VkPipelineTessellationStateCreateFlags flags;
  uint32_t                               patchControlPoints;
} VkPipelineTessellationStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El único campo a completar es patchControlPoints, que indica el número de puntos que forman un PATCH.

 

 

La estructura VkPipelineViewportStateCreateInfo

 

La estructura VkPipelineViewportStateCreateInfo describe el viewport a utilizar. El viewport se refiere al tamaño del volumen de recorte en coordenadas de píxel. En coordenadas de recorte (o coordenadas clip) el volumen de recorte se describe con valores entre -1 y +1 en los tres ejes. La transformación a coordenadas viewport permite calcular en qué pixel se proyecta cada vértice. Normalmente el tamaño del viewport coincide con el tamaño de la ventana sobre la que queremos dibujar la imagen. El estado de viewport permite también definir el trozo de imagen que queremos que se vuelque en la ventana. Lo habitual es que coincida también con el tamaño de la ventana.

typedef struct VkPipelineViewportStateCreateInfo {
  VkStructureType                    sType;
  const void*                        pNext;
  VkPipelineViewportStateCreateFlags flags;
  uint32_t                           viewportCount;
  const VkViewport*                  pViewports;
  uint32_t                           scissorCount;
  const VkRect2D*                    pScissors;
} VkPipelineViewportStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo viewportCount indica el número de viewports a configurar en el pipeline. Normalmente es solo uno.

El campo pViewports es un array de estructuras VkViewport. Estas estructuras indican los valores de los extremos del volumen de recorte. La esquina superior izquierda corresponde a los valores (x,y) y la esquina inferior derecha a los valores (x+width,y+height). Los valores extremos de la componente Z son minDepth y maxDepth.

typedef struct VkViewport {
  float x;
  float y;
  float width;
  float height;
  float minDepth;
  float maxDepth;
} VkViewport;

El campo scissorCount indica el tamaño del campo posterior, es decir, el número de scissors definidos.

El campo pScissors describe el array de trozos de imagen a rellenar asociado a cada viewport.

typedef struct VkRect2D {
  VkOffset2D offset;
  VkExtent2D extent;
} VkRect2D;

typedef struct VkOffset2D {
  int32_t x;
  int32_t y;
} VkOffset2D;

typedef struct VkExtent2D {
 uint32_t width;
 uint32_t height;
} VkExtent2D;

La siguiente imagen muestra la transformación entre coordenadas clip y coordenadas viewport.

viewport transform

 

 

La estructura VkPipelineRasterizationStateCreateInfo

 

La estructura VkPipelineRasterizationStateCreateInfo configura la etapa de rasterización, es decir, la forma en la que se tratan las primitivas para decidir qué fragmentos se dibujan.

typedef struct VkPipelineRasterizationStateCreateInfo {
  VkStructureType                         sType;
  const void*                             pNext;
  VkPipelineRasterizationStateCreateFlags flags;
  VkBool32                                depthClampEnable;
  VkBool32                                rasterizerDiscardEnable;
  VkPolygonMode                           polygonMode;
  VkCullModeFlags                         cullMode;
  VkFrontFace                             frontFace;
  VkBool32                                depthBiasEnable;
  float                                   depthBiasConstantFactor;
  float                                   depthBiasClamp;
  float                                   depthBiasSlopeFactor;
  float                                   lineWidth;
} VkPipelineRasterizationStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo depthClampEnable indica el comportamiento con aquellos puntos que tengan una profundidad mayor que el límite del clipping volume. Si este campo es VK_TRUE los puntos de profundidad mayor al límite se dibujan considerando que su profundidad es la máxima. Si el campo es VK_FALSE estos puntos no se dibujan.

El campo rasterizerDiscardEnable se activa si se quiere descartar todo el proceso de rasterización. En ese caso el pipeline solo se ejecutará hasta el comienzo de esta etapa y nunca llegará a generarse la imagen. Esto se utiliza cuando se desea almacenar el resultado de las etapas anteriores en memoria para tratarlos posteriormente (posiblemente en un subpase posterior).

El campo polygonMode indica la forma en que se dibujarán las primitivas de tipo triángulo: en modo relleno (VK_POLYGON_MODE_FILL), en modo arista (VK_POLYGON_MODE_LINE) o si se dibujarán solo los vértices (VK_POLYGON_MODE_POINT).

El campo cullMode define si se quiere eliminar del proceso de creación de la imagen a aquellos triángulos dibujados por la cara posterior (VK_CULL_MODE_BACK_BIT), por la cara frontal (VK_CULL_MODE_FRONT_BIT), por cualquiera de las caras (VK_CULL_MODE_FRONT_AND_BACK) o no eliminar ninguna (VK_CULL_MODE_NONE).

El campo frontFace define si las caras frontales son las que se dibujan en el sentido horario (VK_FRONT_FACE_CLOCKWISE) o antihorario (VK_FRONT_FACE_COUNTER_CLOCKWISE).

Los campos depthBiasEnable, depthBiasConstantFactor, depthBiasClamp y depthBiasSlopeFactor permiten definir un desplazamiento sobre la profundidad de los puntos.

El campo lineWidth define el grosor de las líneas medido en pixels. Este campo afecta a las primitivas de tipo línea o a los triángulos dibujados en modo arista.

 

 

La estructura VkPipelineMultisampleStateCreateInfo

 

La estructura VkPipelineMultisampleStateCreateInfo describe la forma en la que se va a samplear cada fragmento (píxel), es decir, la forma en la que se ejecutará el Fragment Shader sobre cada pixel.

typedef struct VkPipelineMultisampleStateCreateInfo {
  VkStructureType                       sType;
  const void*                           pNext;
  VkPipelineMultisampleStateCreateFlags flags;
  VkSampleCountFlagBits                 rasterizationSamples;
  VkBool32                              sampleShadingEnable;
  float                                 minSampleShading;
  const VkSampleMask*                   pSampleMask;
  VkBool32                              alphaToCoverageEnable;
  VkBool32                              alphaToOneEnable;
} VkPipelineMultisampleStateCreateInfo;

Existen dos formas de hacerlo: multisampleado y supersampleado. El multisampleado consiste en estudiar varias posiciones dentro del pixel para ver si pertenecen o no a la primitiva, pero solo se ejecuta el Fragment Shader sobre una posición. El supersampleado ejecuta el Fragment Shader sobre varias posiciones del pixel para obtener un color promedio.

Sampling

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo rasterizationSamples define el número de puntos que se van a estudiar para cada pixel. La siguiente imagen muestra los valores admitidos y las posiciones que se analizan en cada caso.

Mulrisampling

El campo sampleShadingEnable permite elegir entre multisampleado (VK_FALSE) y supersampleado (VK_TRUE)..

El supersampleado no obliga a evaluar el Fragment Shader sobre todos los puntos, sino sobre un porcentaje mínimo que se define en el campo minSampleShading .

El campo pSampleMask se utiliza para indicar cuales de los valores generados por el Fragment Shader al hacer supersampleado se van a almacenar en el framebuffer.

El campo alphaToCoverageEnable permite activar un modo de sampleado en el que en la primera ejecución del Fragment Shader se almacena en el canal alpha un valor de covertura específico de cada pixel (como un minSampleShading generado para cada pixel).

Si el campo anterior está activo el resultado final del canal alpha se puede calcular como el resto de componentes o como el valor uno si se activa el campo alphaToOneEnable.

 

 

La estructura VkPipelineDepthStencilStateCreateInfo

 

Una vez decidido que un fragmento forma parte de una primitiva y debe ser dibujado es posible que previamente ya se hubiera calculado el color de esa misma posición. Esto ocurre cuando dos primitivas se solapan al proyectarse lo que sucede cuando cuando se dibujan varios objetos y uno está más cerca que otro. Este solape se puede producir también entre primitivas de un mismo objeto. En ese caso hay que decidir si se dibuja el nuevo fragmento o no en función de la profundidad a la que se encuentre el punto que se desea dibujar y la profundidad a la que se encontrara el punto ya dibujado. Es decir, la decisión de si el nuevo fragmento se dibuja o no se toma en función de un test de profundidad.

El funcionamiento del pipeline permite configurar este test de profundidad y otro test, denominado stencil, que en vez de la profundidad (que se almacena automáticamente) utiliza los datos de otro buffer (que hay que asignar explícitamente). La estructura VkPipelineDepthStencilStateCreateInfo define la forma en la que se realizan los test de profundidad (depth) y de plantilla (stencil) para decidir que un fragmento debe ser dibujado.

typedef struct VkPipelineDepthStencilStateCreateInfo {
  VkStructureType                        sType;
  const void*                            pNext;
  VkPipelineDepthStencilStateCreateFlags flags;
  VkBool32                               depthTestEnable;
  VkBool32                               depthWriteEnable;
  VkCompareOp                            depthCompareOp;
  VkBool32                               depthBoundsTestEnable;
  VkBool32                               stencilTestEnable;
  VkStencilOpState                       front;
  VkStencilOpState                       back;
  float                                  minDepthBounds;
  float                                  maxDepthBounds;
} VkPipelineDepthStencilStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo depthTestEnable permite activar el test de profundidad. Si el test no se activa el nuevo fragmento se dibujará siempre, machacando cualquier información almacenada anteriormente.

El campo depthWriteEnable indica si el valor de la profundidad debe almacenarse. Lo normal es que sí.

La operación de comparación a utilizar se especifica mediante el campo depthCompareOp. Suele utilizarse VK_COMPARE_OP_LESS o VK_COMPARE_OP_LESS_OR_EQUAL aunque se admite cualquier valor de tipo VkCompareOp

Otro posibilidad es comparar la profundidad con un rango. Para activar esa opción se utiliza el campo depthBoundsTestEnable y se utilizan los campos minDepthBounds y maxDepthBounds para indicar el rango.

El test de plantilla se realiza después del test de profundidad, activando el campo stencilTestEnable. Se puede configurar de forma distinta este test para los pixeles mostrados en una cara frontal (campo front) o en una cara posterior (campo back). La estuctura VkStencilOpState permite configurar tanto la operación de comparación como la acción a realizar si el test es positivo o negativo.

 

 

La estructura VkPipelineColorBlendStateCreateInfo

 

Una vez ejecutado el Fragment Shader sobre un pixel, que a su vez ha superado anteriormente el test de profundidad y de plantilla, se puede escribir la información del pixel sobre la imagen. Pero en ese momento existe un valor del pixel actual (source color) y un valor del pixel anterior (destination color). En este punto se puede decidir mezclar ambos colores para tener en cuenta la transparencia. Esto se configura por medio de la estructura VkPipelineColorBlendStateCreateInfo.

typedef struct VkPipelineColorBlendStateCreateInfo {
  VkStructureType                            sType;
  const void*                                pNext;
  VkPipelineColorBlendStateCreateFlags       flags;
  VkBool32                                   logicOpEnable;
  VkLogicOp                                  logicOp;
  uint32_t                                   attachmentCount;
  const VkPipelineColorBlendAttachmentState* pAttachments;
  float                                      blendConstants[4];
} VkPipelineColorBlendStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo logicOpEnable permite activar una opción de mezclado basado en una operación lógica simple entre los colores.

El campo logicOp indica la operación a realizar si el campo anterior está activo. Se trata de operaciones a nivel de bit entre los valores del source color y el destination color. Por ejemplo,  VK_LOGIC_OP_AND realiza una operación AND a nivel de bit entre los dos valores.

Normalmente la forma de realizar una mezcla no es por medio de una operación simple sino una combinación de los colores por medio de una función más elaborada. Estas operaciones se definen por medio de la estructura VkPipelineColorBlendAttachmentState. El campo attachmentCount indica el número de estructuras incluidas en el array pAttachments.

El campo pAttachments describe el proceso de mezcla por medio de estructuras de tipo  VkPipelineColorBlendAttachmentState. Si el framebuffer sobre el que se trabaja tiene varios buffers de color entonces cada estrutura se refiere a la configuración de mezcla en cada buffer.

El campo blendConstants incluye constantes a utilizar en el proceso de mezcla. Estas constantes se utilizan en algunos factores de mezcla que se pueden utilizar en los pAttachments.

La estructura VkPipelineColorBlendStateCreateInfo se utiliza para definir operaciones de mezcla que utilizan factores para combinar los valores de los colores a mezclar. La estructura define de forma separada la combinación de las componentes RGB y de la componente alpha.

typedef struct VkPipelineColorBlendAttachmentState {
  VkBool32              blendEnable;
  VkBlendFactor         srcColorBlendFactor;
  VkBlendFactor         dstColorBlendFactor;
  VkBlendOp             colorBlendOp;
  VkBlendFactor         srcAlphaBlendFactor;
  VkBlendFactor         dstAlphaBlendFactor;
  VkBlendOp             alphaBlendOp;
  VkColorComponentFlags colorWriteMask;
} VkPipelineColorBlendAttachmentState;

La forma más normal para realizar la mezcla es considerar el nivel de transparencia del source color, que puesto que ha verificado el test de profundidad debería encontrarse delante del fragmento antiguo (destination color).

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_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;

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

 

 

La estructura VkPipelineDynamicStateCreateInfo

 

El pipeline contiene muchísima información sobre la configuración de cada una de sus etapas. Si queremos utilizar configuraciones diferentes al dibujar distintas partes de la imagen necesitaríamos crear varios pipelines. La estructura VkPipelineDynamicStateCreateInfo permite definir un conjunto de valores de la configuración del pipeline que pueden ser modificados dinámicamente sin necesidad de crear un nuevo pipeline.

typedef struct VkPipelineDynamicStateCreateInfo {
  VkStructureType                   sType;
  const void*                       pNext;
  VkPipelineDynamicStateCreateFlags flags;
  uint32_t                          dynamicStateCount;
  const VkDynamicState*             pDynamicStates;
} VkPipelineDynamicStateCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo dynamicStateCount contiene el número de variables dinámicas descritas en el siguiente campo.

El campo pDynamicStates define las variables que se van a configurar dinámicamente. El tipo de dato VkDynamicState contiene una enumeración con todas las variables que pueden ser configuradas dinámicamente. Existen extensiones, como VK_EXT_extended_dynamic_state, que permiten extender el conjunto de variables que se pueden definir como dinámicas.

Cada posible variable dinámica tiene asociado un comando para asignar su valor al pipeline.

VkDynamicState Comando
VK_DYNAMIC_STATE_VIEWPORT vkCmdSetViewport()
VK_DYNAMIC_STATE_SCISSOR vkCmdSetScissor()
VK_DYNAMIC_STATE_LINE_WIDTH vkCmdSetLineWidth()
VK_DYNAMIC_STATE_DEPTH_BIAS vkCmdSetDepthBias()
VK_DYNAMIC_STATE_BLEND_CONSTANTS vkCmdSetBlendConstants()
VK_DYNAMIC_STATE_DEPTH_BOUNDS vkCmdSetDepthBounds()
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK vkCmdSetStencilCompareMask()
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK vkCmdSetStencilWriteMask()
VK_DYNAMIC_STATE_STENCIL_REFERENCE vkCmdSetStencilReference()

 

 

La estructura VkPipelineLayoutCreateInfo

 

La estructura VkPipelineLayoutCreateInfo permite describir la información almacenada en memoria que van a compartir los shaders. Esta información se puede representar de dos formas: Descriptor Sets y Push Constants. Los Descriptor Sets son buffers donde se suelen almacenar las matrices de transformación o las imágenes de texturas. Las Push Constants permiten introducir directamente los valores de las constantes sin necesidad de definir buffers.

En este proyecto no se van a utilizar ni Descriptor Sets ni Push Constants. Estas características se explicarán con mayor detalle en la próxima práctica.

typedef struct VkPipelineLayoutCreateInfo {
  VkStructureType              sType;
  const void*                  pNext;
  VkPipelineLayoutCreateFlags  flags;
  uint32_t                     setLayoutCount;
  const VkDescriptorSetLayout* pSetLayouts;
  uint32_t                     pushConstantRangeCount;
  const VkPushConstantRange*   pPushConstantRanges;
} VkPipelineLayoutCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO.

El campo pNext debe dejarse a nulo.

El campo flags debe dejarse a cero.

El campo setLayoutCount contiene el número de Descriptor Sets que se van a definir.

El campo pSetLayouts contiene la definición de los Descriptor Sets.

El campo pushConstantRangeCount contien el número de Push Constants que se van a definir.

El campo pPushConstantRanges define las Push Constants que va a utilizar el pipeline.

 

 

La clase GEPipelineConfig

 

Como acabamos de ver, para crear un objetro VkPipeline es necesario aportar muchísima información que se engloba en una estructura VkGraphicsPipelineCreateInfo. La mayoría de los campos de las diferentes estructuras incluidas en esta configuración tienen valores comunes en nuestros proyectos. Para definir las características de los pipelines a crear en nuestros proyectos vamos a utlizar unas estructuras mucho más simples que solo contienen los valores de los campos que nos interesan. La clase que encapsula estas propiedades se ha denominado GEPipelineConfig. El contenido de su fichero de cabecera es el siguiente:

#pragma once

#include <vulkan/vulkan.h>
#include <vector>

//
// FUNCIÓN: GEPipelineConfig
//
// PROPÓSITO: Almacena la configuración de un pipeline de renderizado
//
class GEPipelineConfig {
public:
  int vertex_shader;
  int fragment_shader;

  int attrStride;
  std::vector<VkFormat> attrFormats;
  std::vector<int> attrOffsets;

  std::vector<VkDescriptorType> descriptorTypes;
  std::vector<VkShaderStageFlags> descriptorStages;

  VkBool32 depthTestEnable;
  VkCullModeFlags cullMode;
  VkExtent2D extent;

  GEPipelineConfig();
};

El constructor de la clase se limita a almacenar los valores por defecto de los diferentes campos:

#include "GEPipelineConfig.h"


//
// FUNCIÓN: GEPipelineConfig::GEPipelineConfig()
//
// PROPÓSITO: Construye un objeto con los valores por defecto
//
GEPipelineConfig::GEPipelineConfig()
{
  vertex_shader = -1;
  fragment_shader = -1;

  attrStride = 0;
  attrFormats.resize(0);
  attrOffsets.resize(0);

  descriptorTypes.resize(0);
  descriptorStages.resize(0);

  depthTestEnable = VK_TRUE;
  cullMode = VK_CULL_MODE_BACK_BIT;
  extent.width = 800;
  extent.height = 600;
}

 

 

Modificaciones de la clase GERenderingContext

 

La definición del proceso de renderizado se ha incluido dentro de la clase GERenderingContext. Esto ha llevado a incluir nuevos campos y métodos en esta clase. Los nuevos campos son:

  • graphicsPipeline: Objeto VkPipeline con la definición del proceso de renderizado.

  • descriptorSetLayout: Plantilla de conjuntos de descriptores utilizada en el pipeline, es decir, descripción de las variables uniformes incluidas en los shaders.

  • pipelineLayout: Plantilla de renderizado, formada por las plantillas de conjuntos de descriptores.

  • viewport: Tamaño de las imágenes a generar.

  • scissor: Fragmento de las imágenes a generar. Generalmente es el tamaño completo de la imagen.

El contenido del fichero de cabecera de la clase es el siguiente.

#pragma once
#include "GERenderingContext.h"

#include <vulkan/vulkan.h>
#include <iostream>
#include "GEGraphicsContext.h"
#include "GEDrawingContext.h"
#include "GEPipelineConfig.h"

//
// CLASE: GEApplication
//
// DESCRIPCIÓN: Clase que describe un contexto de renderizado
//
class GERenderingContext
{
public:
  uint32_t imageCount;

private:
  VkFormat format;
  VkExtent2D extent;
  VkRenderPass renderPass;
  VkPipeline graphicsPipeline;
  VkDescriptorSetLayout descriptorSetLayout;
  VkPipelineLayout pipelineLayout;
  VkViewport viewport;
  VkRect2D scissor;

public:
  GERenderingContext(GEGraphicsContext* gc, GEDrawingContext* dc, 
                                            GEPipelineConfig* config);
  void destroy(GEGraphicsContext* gc);

private:
  // Métodos de creación de componentes
  void createRenderPass(GEGraphicsContext* gc);
  void createGraphicsPipeline(GEGraphicsContext* gc, GEPipelineConfig* config);

  // Métodos de definición del pipeline de renderizado
  void createPipelineLayout(GEGraphicsContext* gc, GEPipelineConfig* config);
  void createVertexShaderStageCreateInfo(
                         GEGraphicsContext* gc, 
                         int resource, 
                         VkShaderModule* vertShaderModule, 
                         VkPipelineShaderStageCreateInfo* vertShaderStageInfo);
  void createFragmentShaderStageCreateInfo(
                         GEGraphicsContext* gc, 
                         int resource, 
                         VkShaderModule* fragShaderModule, 
                         VkPipelineShaderStageCreateInfo* fragShaderStageInfo);
  void createPipelineVertexInputStateCreateInfo(
                         GEPipelineConfig* config, 
                         VkPipelineVertexInputStateCreateInfo* vertexInputInfo);
  void createPipelineInputAssemblyStateCreateInfo(
                         VkPipelineInputAssemblyStateCreateInfo* inputAssembly);
  void createPipelineViewportStateCreateInfo(
                         VkPipelineViewportStateCreateInfo* viewportState);
  void createPipelineRasterizationStateCreateInfo(
                         GEPipelineConfig* config, 
                         VkPipelineRasterizationStateCreateInfo* rasterizer);
  void createPipelineMultisampleStateCreateInfo(
                         VkPipelineMultisampleStateCreateInfo* multisampling);
  void createPipelineDepthStencilStateCreateInfo(
                         GEPipelineConfig* config,
                         VkPipelineDepthStencilStateCreateInfo* depthStencil);
  void createPipelineColorBlendStateCreateInfo(
                         VkPipelineColorBlendAttachmentState* colorBlendAttachment,
                         VkPipelineColorBlendStateCreateInfo* colorBlending);

  // Métodos auxiliares
  VkShaderModule createShaderModule(GEGraphicsContext* gc, 
                                    const std::vector<char>& code);
  std::vector<char> getFileFromResource(int resource);
};

La inclusión del pipeline en la clase GERenderingContext requiere añadir algunos métodos:

  • GERenderingContext(): El constructor de la clase añade la creación del pipeline.

  • destroy(): El método de destrucción añade la destrucción del pipeline y de las plantillas de conjuntos de descriptores.

  • createGraphicsPipeline(): Construye el pipeline con la configuración indicada. El método llama a métodos auxilizares dedicados a construir las estructuras intermedias de la configuración del objeto VkPipeline.

El código de estos métodos y de los nuevos métodos auxiliares se muestra a continuación.

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////                          Métodos públicos                               /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GERenderingContext::GERenderingContext(...)
//
// PROPÓSITO: Crea un contexto de renderizado 
//
GERenderingContext::GERenderingContext(GEGraphicsContext* gc, 
                                       GEDrawingContext* dc, 
                                       GEPipelineConfig* config)
{
  imageCount = dc->getImageCount();
  format = dc->getFormat();
  extent = dc->getExtent();
  createRenderPass(gc);
  createGraphicsPipeline(gc, config);
}

//
// FUNCIÓN: GERenderingContext::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea el estado de Vulkan
//
void GERenderingContext::destroy(GEGraphicsContext* gc)
{
  vkDestroyPipeline(gc->device, graphicsPipeline, nullptr);
  vkDestroyPipelineLayout(gc->device, pipelineLayout, nullptr);
  vkDestroyDescriptorSetLayout(gc->device, descriptorSetLayout, nullptr);
  vkDestroyRenderPass(gc->device, renderPass, nullptr);
}

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////               Métodos de creación de los componentes                    /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GERenderingContext::createGraphicsPipeline()
//
// PROPÓSITO: Crea el Pipeline de renderizado
//
void GERenderingContext::createGraphicsPipeline(GEGraphicsContext* gc,
                                                GEPipelineConfig* config)
{
  viewport = {};
  viewport.x = 0.0f;
  viewport.y = 0.0f;
  viewport.width = (float) config->extent.width;
  viewport.height = (float) config->extent.height;
  viewport.minDepth = 0.0f;
  viewport.maxDepth = 1.0f;

  scissor = {};
  scissor.offset = { 0, 0 };
  scissor.extent = config->extent;

  VkShaderModule vertShaderModule, fragShaderModule;
  VkPipelineShaderStageCreateInfo vertShaderStageInfo, fragShaderStageInfo;
  VkPipelineVertexInputStateCreateInfo vertexInputInfo;
  VkPipelineInputAssemblyStateCreateInfo inputAssembly;
  VkPipelineViewportStateCreateInfo viewportState;
  VkPipelineRasterizationStateCreateInfo rasterizer;
  VkPipelineMultisampleStateCreateInfo multisampling;
  VkPipelineDepthStencilStateCreateInfo depthStencil;
  VkPipelineColorBlendAttachmentState colorBlendAttachment;
  VkPipelineColorBlendStateCreateInfo colorBlending;

  createPipelineLayout(gc, config);
  createVertexShaderStageCreateInfo(gc, config->vertex_shader, &vertShaderModule, 
                                                             &vertShaderStageInfo);
  createFragmentShaderStageCreateInfo(gc, config->fragment_shader, &fragShaderModule,
                                                             &fragShaderStageInfo);
  VkPipelineShaderStageCreateInfo shaderStages[] = { 
                                    vertShaderStageInfo,
                                    fragShaderStageInfo };
  createPipelineVertexInputStateCreateInfo(config, &vertexInputInfo);
  createPipelineInputAssemblyStateCreateInfo(&inputAssembly);
  createPipelineViewportStateCreateInfo(&viewportState);
  createPipelineRasterizationStateCreateInfo(config, &rasterizer);
  createPipelineMultisampleStateCreateInfo(&multisampling);
  createPipelineDepthStencilStateCreateInfo(config, &depthStencil);
  createPipelineColorBlendStateCreateInfo(&colorBlendAttachment, &colorBlending);

  VkGraphicsPipelineCreateInfo pipelineInfo = {};
  pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
  pipelineInfo.stageCount = 2;
  pipelineInfo.pStages = shaderStages;
  pipelineInfo.pVertexInputState = &vertexInputInfo;
  pipelineInfo.pInputAssemblyState = &inputAssembly;
  pipelineInfo.pViewportState = &viewportState;
  pipelineInfo.pRasterizationState = &rasterizer;
  pipelineInfo.pMultisampleState = &multisampling;
  pipelineInfo.pDepthStencilState = &depthStencil;
  pipelineInfo.pColorBlendState = &colorBlending;
  pipelineInfo.pDynamicState = nullptr;
  pipelineInfo.layout = pipelineLayout;
  pipelineInfo.renderPass = renderPass;
  pipelineInfo.subpass = 0;
  pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;

  if (vkCreateGraphicsPipelines(gc->device, VK_NULL_HANDLE, 1, &pipelineInfo, 
                                         nullptr, &graphicsPipeline) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create graphics pipeline!");
  }

  vkDestroyShaderModule(gc->device, fragShaderModule, nullptr);
  vkDestroyShaderModule(gc->device, vertShaderModule, nullptr);

  std::cout << "Pipeline created!" << std::endl;
}

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////         Métodos de definición del pipeline de renderizado               /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: CAVulkanState::createPipelineLayout()
//
// PROPÓSITO: Crea el esquema de los conjuntos de descriptores
//
void GERenderingContext::createPipelineLayout(GEGraphicsContext* gc, 
                                              GEPipelineConfig* config)
{
  uint32_t bindingCount = (uint32_t)config->descriptorTypes.size();
  std::vector<VkDescriptorSetLayoutBinding> bindings(bindingCount);
  for (uint32_t i = 0; i < bindingCount; i++)
  {
    VkDescriptorSetLayoutBinding binding = {};
    binding.binding = i;
    binding.descriptorCount = 1;
    binding.descriptorType = config->descriptorTypes[i];
    binding.pImmutableSamplers = nullptr;
    binding.stageFlags = config->descriptorStages[i];

    bindings[i] = binding;
  }

  VkDescriptorSetLayoutCreateInfo layoutInfo = {};
  layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  layoutInfo.bindingCount = bindingCount;
  layoutInfo.pBindings = bindings.data();

  if (vkCreateDescriptorSetLayout(gc->device, &layoutInfo, nullptr,
                                              &descriptorSetLayout) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create descriptor set layout!");
  }

  VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
  pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  pipelineLayoutInfo.setLayoutCount = 1;
  pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
  pipelineLayoutInfo.pushConstantRangeCount = 0;

  if (vkCreatePipelineLayout(gc->device, &pipelineLayoutInfo, nullptr, 
                                                   &pipelineLayout) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create pipeline layout!");
  }
}

//
// FUNCIÓN: GERenderingContext::createVertexShaderStageCreateInfo()
//
// PROPÓSITO: Crea la información sobre el Vertex Shader
//
void GERenderingContext::createVertexShaderStageCreateInfo(
                              GEGraphicsContext* gc, 
                              int resource, 
                              VkShaderModule* vertShaderModule, 
                              VkPipelineShaderStageCreateInfo* vertShaderStageInfo)
{
  std::vector<char> vertShaderCode = getFileFromResource(resource);

  *vertShaderModule = createShaderModule(gc, vertShaderCode);

  *vertShaderStageInfo = {};
  vertShaderStageInfo->sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  vertShaderStageInfo->stage = VK_SHADER_STAGE_VERTEX_BIT;
  vertShaderStageInfo->module = *vertShaderModule;
  vertShaderStageInfo->pName = "main";
}

//
// FUNCIÓN: GERenderingContext::createFragmentShaderStageCreateInfo()
//
// PROPÓSITO: Crea la información sobre el Vertex Shader
//
void GERenderingContext::createFragmentShaderStageCreateInfo(
                             GEGraphicsContext* gc, 
                             int resource, 
                             VkShaderModule* fragShaderModule, 
                             VkPipelineShaderStageCreateInfo* fragShaderStageInfo)
{
  std::vector<char> fragShaderCode = getFileFromResource(resource);

  *fragShaderModule = createShaderModule(gc, fragShaderCode);

  *fragShaderStageInfo = {};
  fragShaderStageInfo->sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  fragShaderStageInfo->stage = VK_SHADER_STAGE_FRAGMENT_BIT;
  fragShaderStageInfo->module = *fragShaderModule;
  fragShaderStageInfo->pName = "main";
}

//
// FUNCIÓN: GERenderingContext::createPipelineVertexInputStateCreateInfo()
//
// PROPÓSITO: Crea la descripción de los atributos de los vértices
//
void GERenderingContext::createPipelineVertexInputStateCreateInfo(
                              GEPipelineConfig* config, 
                              VkPipelineVertexInputStateCreateInfo* vertexInputInfo)
{
  std::vector<VkVertexInputBindingDescription> bindingDescriptions;
  std::vector<VkVertexInputAttributeDescription> attributeDescriptions;

  int attrCount = config->attrOffsets.size();
  int bindingCount = (attrCount > 0 ? 1 : 0);

  bindingDescriptions.resize(bindingCount);
  for (int i = 0; i < bindingCount; i++)
  {
    bindingDescriptions[i] = {};
    bindingDescriptions[i].binding = i;
    bindingDescriptions[i].stride = config->attrStride;
    bindingDescriptions[i].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
  }

  attributeDescriptions.resize(attrCount);
  for (int i = 0; i < attrCount; i++)
  {
    attributeDescriptions[i] = {};
    attributeDescriptions[i].binding = 0;
    attributeDescriptions[i].location = i;
    attributeDescriptions[i].format = config->attrFormats[i];
    attributeDescriptions[i].offset = config->attrOffsets[i];
  }

  *vertexInputInfo = {};
  vertexInputInfo->sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
  vertexInputInfo->vertexBindingDescriptionCount = bindingCount;
  vertexInputInfo->vertexAttributeDescriptionCount = attrCount;
  vertexInputInfo->pVertexBindingDescriptions = bindingDescriptions.data();
  vertexInputInfo->pVertexAttributeDescriptions = attributeDescriptions.data();
}

//
// FUNCIÓN: GERenderingContext::createPipelineVertexInputStateCreateInfo()
//
// PROPÓSITO: Crea la información para el ensamblado de primitivas
//
void GERenderingContext::createPipelineInputAssemblyStateCreateInfo(
                        VkPipelineInputAssemblyStateCreateInfo* inputAssembly)
{
  *inputAssembly = {};
  inputAssembly->sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
  inputAssembly->topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
  inputAssembly->primitiveRestartEnable = VK_FALSE;
}

//
// FUNCIÓN: CAVulkanState::createPipelineViewportStateCreateInfo()
//
// PROPÓSITO: Crea la información del viewport
//
void GERenderingContext::createPipelineViewportStateCreateInfo(
                                  VkPipelineViewportStateCreateInfo* viewportState)
{
  *viewportState = {};
  viewportState->sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
  viewportState->viewportCount = 1;
  viewportState->pViewports = &viewport;
  viewportState->scissorCount = 1;
  viewportState->pScissors = &scissor;
}

//
// FUNCIÓN: GERenderingContext::createPipelineRasterizationStateCreateInfo()
//
// PROPÓSITO: Crea la información de la etapa de rasterización
//
void GERenderingContext::createPipelineRasterizationStateCreateInfo(
                 GEPipelineConfig* config, 
                 VkPipelineRasterizationStateCreateInfo* rasterizer)
{
  *rasterizer = {};
  rasterizer->sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
  rasterizer->depthClampEnable = VK_FALSE;
  rasterizer->rasterizerDiscardEnable = VK_FALSE;
  rasterizer->polygonMode = VK_POLYGON_MODE_FILL;
  rasterizer->lineWidth = 1.0f;
  rasterizer->cullMode = config->cullMode;
  rasterizer->frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
  rasterizer->depthBiasEnable = VK_FALSE;
}

//
// FUNCIÓN: GERenderingContext::createPipelineRasterizationStateCreateInfo()
//
// PROPÓSITO: Crea la información de la etapa de sampleado
//
void GERenderingContext::createPipelineMultisampleStateCreateInfo(
                               VkPipelineMultisampleStateCreateInfo* multisampling)
{
  *multisampling = {};
  multisampling->sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
  multisampling->sampleShadingEnable = VK_FALSE;
  multisampling->rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
}

//
// FUNCIÓN: GERenderingContext::createPipelineRasterizationStateCreateInfo()
//
// PROPÓSITO: Crea la información sobre los tests de profundidad y de plantilla
//
void GERenderingContext::createPipelineDepthStencilStateCreateInfo(
                               GEPipelineConfig* config, 
                               VkPipelineDepthStencilStateCreateInfo* depthStencil)
{
  *depthStencil = {};
  depthStencil->sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
  depthStencil->pNext = nullptr;
  depthStencil->flags = 0;
  depthStencil->depthTestEnable = config->depthTestEnable;
  depthStencil->depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; 
  depthStencil->depthWriteEnable = VK_TRUE;
  depthStencil->depthBoundsTestEnable = VK_FALSE;
  depthStencil->stencilTestEnable = VK_FALSE;
}

//
// FUNCIÓN: GERenderingContext::createPipelineColorBlendStateCreateInfo()
//
// PROPÓSITO: Crea la información sobre la etapa de mezcla de colores
//
void GERenderingContext::createPipelineColorBlendStateCreateInfo(
                      VkPipelineColorBlendAttachmentState* colorBlendAttachment, 
                      VkPipelineColorBlendStateCreateInfo* colorBlending)
{
  *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;

  *colorBlending = {};
  colorBlending->sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; 
  colorBlending->logicOpEnable = VK_FALSE;
  colorBlending->logicOp = VK_LOGIC_OP_COPY;
  colorBlending->attachmentCount = 1;
  colorBlending->pAttachments = colorBlendAttachment;
  colorBlending->blendConstants[0] = 0.0f;
  colorBlending->blendConstants[1] = 0.0f;
  colorBlending->blendConstants[2] = 0.0f;
  colorBlending->blendConstants[3] = 0.0f;
}

 

 

Modificaciones de la clase GEApplication

 

Para construir un objeto GERenderingContext es necesario definir una estructura GEPipelineConfig. Esto obliga a modificar la clase GEApplication para incluir la construcción de esta estructura. En este proyecto el pipeline es muy básico ya que no existen atributos de entrada ni variables uniformes por lo que el contenido de la estructura de configuración es bastante simple. Los métodos modificados de la clase GEApplication se muestran a continuación.

//
// FUNCIÓN: GEApplication::run()
//
// PROPÓSITO: Ejecuta la aplicación
//
void GEApplication::run()
{
  this->window = initWindow();
  this->windowPos = initWindowPos();
  this->gc = new GEGraphicsContext(window);
  this->dc = new GEDrawingContext(this->gc, this->windowPos);
  this->cc = new GECommandContext(this->gc, this->dc->getImageCount());
  GEPipelineConfig* config = getPipelineConfig(dc->getExtent());
  this->rc = new GERenderingContext(this->gc, this->dc, config);

  mainLoop();

  cleanup();
}

//
// FUNCIÓN: GEApplication::getPipelineConfig()
//
// PROPÓSITO: Obtiene la configuración del pipeline de renderizado
//
GEPipelineConfig* GEApplication::getPipelineConfig(VkExtent2D extent)
{
  GEPipelineConfig* config = new GEPipelineConfig();
  config->vertex_shader = IDR_HTML1;
  config->fragment_shader = IDR_HTML2;

  config->attrStride = 0;
  config->attrOffsets.resize(0);
  config->attrFormats.resize(0);

  config->descriptorTypes.resize(0);
  config->descriptorStages.resize();

  config->depthTestEnable = VK_TRUE;
  config->cullMode = VK_CULL_MODE_NONE;
  config->extent = extent;

  return config;
}

 

 

Aspecto final

 

El único cambio respecto a la aplicación es el mensaje presentado en consola.

Captura