|
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:
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.
|
|
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.
|
|