Creating the Shaders#

Shaders are a key element of rendering. They are programs that are designed to run on the GPU, operating on large quantities of data at a time, thus making use of the GPU’s highly parallel nature. The two types of shader shown in this guide are vertex shaders and fragment shaders.

Vertex shaders operate on the vertices of a model, usually transforming them between different coordinate spaces. They are often used to transform the vertex positions from 3D object space to 2D screen space (that is, from model or world coordinates to normalised device coordinates).

Fragment shaders operate on fragments. A fragment is a pixel before it is written to the framebuffer. Their ultimate goal is to determine the colour that gets written to a specific pixel of the framebuffer.

There are other types of shaders you can optionally use to perform different functions in the rendering pipeline, but these will not be covered in this example.

In Vulkan, shader code must usually be in a bytecode format called SPIR-V. Shaders can be compiled from GLSL into SPIR-V using a tool called glslangValidator. This tool is provided with the LunarG Vulkan SDK and with the PowerVR SDK. This approach avoids the need to compile GLSL at runtime, as glslangValidator can be run offline.

The SPIR-V source code of the shaders are wrapped in objects called shader modules. Shader modules are used to create the Pipeline objects. More information on pipeline stages will be given in Initialising a Pipeline.

The shaders used in this example are both simple. The vertex shader transforms vertex positions from local object space to normalised device space. The transformation matrix used in this shader is produced by combining an orthographic projection matrix with a rotation matrix to present a spinning triangle. The projection matrix handles the conversion to screen space and the rotation matrix handles the rotation of the vertices in screen space. The shader also reads texture co-ordinates from the vertex and then outputs them again so that they can be passed to the fragment shader.

The fragment shader samples a 2D texture image using the vertex texture co-ordinates. These are values that are attached to the vertex data and were passed through the vertex shader. The shader then writes this colour into the pixels of the image buffer. The GLSL code for each shader is also shown here, and is included with the source code of the example. This is the most basic form of the texture mapping technique.

initShaders() uses a custom helper function createShaderModule(), which loads the shader SPIR-V bytecode into the shader modules and sets the shader stage.

View initShaders() in context.

View createShaderModule() in context.

../../_images/shaders.png
void VulkanHelloAPI::initShaders()
{
    createShaderModule(spv_VertShader_bin, sizeof(spv_VertShader_bin), 0, VK_SHADER_STAGE_VERTEX_BIT);

    createShaderModule(spv_FragShader_bin, sizeof(spv_FragShader_bin), 1, VK_SHADER_STAGE_FRAGMENT_BIT);
}

void VulkanHelloAPI::createShaderModule(const uint32_t* spvShader, size_t spvShaderSize, int indx, VkShaderStageFlagBits shaderStage)
{
    // Populate a shader module creation info struct with a pointer to the shader source code and the size of the shader in bytes.
    VkShaderModuleCreateInfo shaderModuleInfo = {};
    shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
    shaderModuleInfo.flags = 0;
    shaderModuleInfo.pCode = spvShader;
    shaderModuleInfo.codeSize = spvShaderSize;
    shaderModuleInfo.pNext = nullptr;

    // Set the stage of the pipeline that the shader module will be associated with.
    // The shader source code entry point ("main") is also set here.
    appManager.shaderStages[indx].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    appManager.shaderStages[indx].flags = 0;
    appManager.shaderStages[indx].pName = "main";
    appManager.shaderStages[indx].pNext = nullptr;
    appManager.shaderStages[indx].stage = shaderStage;
    appManager.shaderStages[indx].pSpecializationInfo = nullptr;

    // Create a shader module and add it to the shader stage corresponding to the VkShaderStageFlagBits stage.
    debugAssertFunctionResult(vk::CreateShaderModule(appManager.device, &shaderModuleInfo, nullptr, &(appManager.shaderStages[indx].module)), "Shader Module Creation");
}

The GLSL source for the vertex shader is below:

#version 320 es

//// Vertex Shader inputs
layout(location = 0) in highp vec4 vertex;
layout(location = 1) in mediump vec2 uv;

//// Shader Resources ////
layout(std140, set = 1, binding = 0) uniform modelViewProjectionBuffer
{
    mat4 modelViewProjectionMatrix;
};

//// Per Vertex Outputs ////
layout(location = 0) out mediump vec2 UV_OUT;

void main()
{
    // Calculate the ndc position for the current vertex using the model view projection matrix.
    gl_Position = modelViewProjectionMatrix * vertex;
    UV_OUT = uv;
}

And the fragment shader:

#version 320 es

//// Shader Resources ////
layout(set = 0, binding = 0) uniform mediump sampler2D triangleTexture;

//// Vertex Inputs ////
layout(location = 0) in mediump vec2 UV;

//// Fragment Outputs ////
layout(location = 0) out mediump vec4 fragColor;

void main()
{
    // Sample the checkerboard texture and write to the frame buffer attachment.
    fragColor = texture(triangleTexture, UV);
}