Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 3a

Creación de shaders

 

Objetivos

 

Añadir al proyecto la creación y compilación del VertexShader y el FragmentShader e introducirlos como recursos de la aplicación.

 

 

Shaders

 

El proceso de generación de imágenes, conocido como Pipeline de renderizado, incluye algunas etapas programables. Estos programas, denominados shaders, se suelen escribir en lenguajes de programación de alto nivel como GLSL o HLSL. Vulkan requiere que los shaders se introduzcan en un lenguaje intermedio llamado SPIR-V. Se trata de un lenguaje definido por Khronos Group, que ha creado también un compilador para generar el código SPIR-V a partir de la definición de los shaders en GLSL. Este compilador, llamado glslangValidator, se incluye en el entorno de desarrollo de Vulkan. Existen otros compiladores de GLSL a SPIR-V que también pueden ser utilizados, como glslc desarrollado por Google.

El compilador glslangValidator requiere que los ficheros de entrada tengan una extensión que indica el tipo de shader a compilar:

  • .vert - vertex shader

  • .tesc - tessellation control shader

  • .tese - tessellation evaluation shader

  • .geom - geometry shader

  • .frag - fragment shader

  • .comp - compute shader

Para generar el código binario en SPIR-V hay que utilizar la opción -V. Para generar código SPIR-V en modo texto se utiliza la opción -H. Los nombres de los ficheros generados por el compilador también están prefijados. Por ejemplo, al compilar un Vertex Shader se genera el fichero "vert.spv" y al compilar un Fragment Shader se genera el fichero "frag.spv".

Para incorporar un shader en una aplicación es necesario crear un objeto VkShaderModule. Para ello es necesario utilizar la función vkCreateShaderModule().

VkResult vkCreateShaderModule(
  VkDevice                        device,
  const VkShaderModuleCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*    pAllocator,
  VkShaderModule*                 pShaderModule);

La información necesaria para crear un shader se almacena en una estructura VkShaderModuleCreateInfo. El contenido del campo sType debe ser VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO. Los campos pNext y flags de momento no se utilizan por lo que deben dejarse a nulo. El código SPIR-V del shader se introduce en el campo pCode. El campo codeSize debe indicar el número de bytes del código.

typedef struct VkShaderModuleCreateInfo {
  VkStructureType           sType;
  const void*               pNext;
  VkShaderModuleCreateFlags flags;
  size_t                    codeSize;
  const uint32_t*           pCode;
} VkShaderModuleCreateInfo;

Para incluir los shaders en la definición del pipeline se utiliza la estrudtura VkPipelineShaderStageCreateInfo. El campo sType debe tener el valor VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO. Los campos pNext y flags por el momento deben dejarse a nulo.  El campo stage indica el tipo de shader a incluir en el pipeline. Los valores permitidos corresponden a la enumeración VkShaderStageFlagBits que puede tomar los valores:

  • VK_SHADER_STAGE_VERTEX_BIT

  • VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT

  •  VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT

  •  VK_SHADER_STAGE_GEOMETRY_BIT

  • VK_SHADER_STAGE_FRAGMENT_BIT

  • VK_SHADER_STAGE_COMPUTE_BIT

Además de estos valores, las nuevas características de las tarjetas gráficas (como las etapas de ray tracing) pueden configurarse con valores específicos incluidos en algunas extensiones. El campo module se refiere al objeto que almacena el código del shader. El campo pName es el nombre de la función principal del shader (típicamente, main). El campo pSpecializationInfo permite introducir información adicional, que no es necesario en la mayoría de los sahders.

typedef struct VkPipelineShaderStageCreateInfo {
  VkStructureType                  sType;
  const void*                      pNext;
  VkPipelineShaderStageCreateFlags flags;
  VkShaderStageFlagBits            stage;
  VkShaderModule                   module;
  const char*                      pName;
  const VkSpecializationInfo*      pSpecializationInfo;
} VkPipelineShaderStageCreateInfo;

 

 

Vertex shader

 

El objetivo de la primera aplicación es generar un simple triángulo. Como veremos en las prácticas siguientes la forma de dibujar un objeto es por medio de una malla (mesh) formada por numerosos vértices. Estos vértices tienen asociados diferentes atributos y se almacenan en buffers de memoria (Vertex buffers). El Vertex Shader se ejecuta sobre cada vértice leyendo sus atributos desde la memoria.

En este primer ejemplo no vamos a utilizar el almacenamiento en memoria sino que se van a considerar los datos incrustados en el código. Evidentemente esto solo tiene sentido en un caso tan simple como este. Para ello se han definido en el código dos variables. La variable positions es un array de tres vectores bidimensionales que almacenan la posición de cada vértice. La variable colors es un array de tres vectores que almacenan el color de cada vértice en formato RGB. El método main() utiliza la variable de entrada predefinida gl_VertexIndex para identificar el vértice sobre el que se ejecuta el shader en cada caso. El shader define la variable de salida fragColor y obtiene el color a partir del array colors y la posición a partir del array positions.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

 

 

Fragment shader

 

El Fragment Shader se ejecuta sobre cada fragmento, es decir, sobre cada pixel de las primitivas que se están dibujando. En este caso solo se va a dibujar una primitiva de tipo triángulo y el shader se ejecutará sobre cada píxel de ese triángulo. El Fragment Shader debe definir como entradas las mismas variables que aparecen en el Vertex Shader como salidas. En este caso la única entrada será la variable fragColor que almacena el color del fragmento. El valor de las entradas se obtiene por interpolación entre los valores de los vértices. En este caso el color de cada fragmento se obtendrá como interpolación de los colores de los vértices incluidos en el Vertex Shader.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

 

 

Modificaciones de la clase GERenderinigContext

 

Para introducir los shaders se han incluido varios métodos en la clase GERenderingContext.

#pragma once
#include "GERenderingContext.h"

#include <vulkan/vulkan.h>
#include <iostream>
#include "GEGraphicsContext.h"
#include "GEDrawingContext.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;

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

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


  // Métodos de definición del pipeline de renderizado
  void createVertexShaderStageCreateInfo(GEGraphicsContext* gc, 
                            VkShaderModule* vertShaderModule, 
                            VkPipelineShaderStageCreateInfo* vertShaderStageInfo);
  void createFragmentShaderStageCreateInfo(GEGraphicsContext* gc, 
                            VkShaderModule* fragShaderModule, 
                            VkPipelineShaderStageCreateInfo* fragShaderStageInfo);

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


Los métodos añadidos son los siguientes:

  •  createGraphicsPipeline(): Tiene como objetivo la creación del pipeline. En este proyecto solo se va a considerar la creación de las estructuras VkPipelineShaderStageCreateInfo que formarán parte de la configuración del pipeline. En proyectos posteriores se completará este método.

  •  createVertexShaderStageCreateInfo(): Genera la información del Vertex Shader para ser incluida en el pipeline.

  • createFragmentShaderStageCreateInfo(): Genera la información del Fragment Shader a incluir en la descripción del pipeline.

  • createShaderModule(): crea una estructura VkShaderModule a partir del código SPIR-V de un shader.

  •  getFileFromResource(): Lee en tiempo de ejecución el código de un fichero incrustado en la aplicación como un recurso. Este método utiliza constantes y funciones definidas en la cabecera windows.h y en la descripción de recursos de la aplicación contenida en la cabecera resource.h.

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

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

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

//
// FUNCIÓN: GERenderingContext::createGraphicsPipeline()
//
// PROPÓSITO: Crea el Pipeline de renderizado
//
void GERenderingContext::createGraphicsPipeline(GEGraphicsContext* gc)
{
  VkShaderModule vertShaderModule, fragShaderModule;
  VkPipelineShaderStageCreateInfo vertShaderStageInfo, fragShaderStageInfo;

  createVertexShaderStageCreateInfo(gc, &vertShaderModule, &vertShaderStageInfo);
  createFragmentShaderStageCreateInfo(gc, &fragShaderModule, &fragShaderStageInfo);

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

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

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

  *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, 
                               VkShaderModule* fragShaderModule, 
                               VkPipelineShaderStageCreateInfo* fragShaderStageInfo)
{
  std::vector<char> fragShaderCode = getFileFromResource(IDR_HTML2);

  *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";
}

///////////////////////////////////////////////////////////////////////////////////
/////                                                                         /////
/////                         Métodos auxiliares                              /////
/////                                                                         /////
///////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GERenderingContext::createShaderModule()
//
// PROPÓSITO: Crea un shader a partir de su código en SPIR-V
//
VkShaderModule GERenderingContext::createShaderModule(GEGraphicsContext* gc, 
                                                    const std::vector<char>& code)
{
  VkShaderModuleCreateInfo createInfo = {};
  createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
  createInfo.codeSize = code.size();
  createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

  VkShaderModule shaderModule;
  if (vkCreateShaderModule(gc->device, &createInfo, nullptr, &shaderModule) 
                                                                      != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create shader module!");
  }

  return shaderModule;
}

//
// FUNCIÓN: GERenderingContext::getFileFromResource(int resource)
//
// PROPÓSITO: Extrae el contenido de un fichero incluido como recurso de la aplicación
//
std::vector<char> GERenderingContext::getFileFromResource(int resource)
{
  HRSRC shaderHandle = FindResource(NULL, MAKEINTRESOURCE(resource), RT_HTML);
  HGLOBAL shaderGlobal = LoadResource(NULL, shaderHandle);
  LPCTSTR shaderPtr = static_cast<LPCTSTR>(LockResource(shaderGlobal));
  DWORD shaderSize = SizeofResource(NULL, shaderHandle);

  std::vector<char> shader(shaderSize);
  memcpy(shader.data(), shaderPtr, shaderSize);
  UnlockResource(shaderGlobal);
  FreeResource(shaderGlobal);
  return shader;
}

Los ficheros de código de los shaders (vert.spv y frag.spv) se deben incluir como recursos HTML de la aplicación. Estos recursos se pueden crear fácilmente en la Vista de recursos. Al crear los recursos IDR_HTML1 y IDR_HTML2 hay que modificar los ficheros de origen para indicar que sean los correspondientes a los shaders. Es importante tener en cuenta que al crear el recurso se crean estos ficheros sin contenido por lo que si los ficheros ya existían serán sustituidos por ficheros en blanco. Hay que compilar siempre los shaders después de haber creado los recursos.

Recursos

 

 

Aspecto final

 

El aspecto de la aplicación sigue siendo una ventana en negro. En este caso podemos ver el mensaje en consola indicando que se han creado los shaders con éxito.

Captura