Graphics pipeline

Shaders

You are very close to the creation of the graphics pipeline to draw the triangle. But a pipeline needs defined stages which means in Vulkan you have to write shaders. In this example you will need only two shaders: A vertex shader to pass your geometry to further rasterization stages and a fragment shader to pass specified values to each attached render target.


Vertex shader

This here will be your vertex shader, written in GLSL for Vulkan (you will find more detailed information about the language in other sources but it is very similar to GLSL for OpenGL if that is any help).

shaders/shader.vert

#version 450

// vertex color to be interpolated for each fragment
layout(location = 0) out vec3 fragColor;

// push constants of the pipeline
layout( push_constant ) uniform constants {
  mat4 mvp; // model-view-projection matrix
};

void main()	{
  // positions of each vertex for the triangle in model-space
  vec3 positions[3] = {
    vec3(-0.5, 0.5, 1), // upper left
    vec3( 0.5, 0.5, 1), // upper right
    vec3(0, -0.5, 1)    // lower center
  };
  
  // colors of each vertex for the triangle
  vec3 colors[3] = {
    vec3(1, 0, 0), // red
    vec3(0, 1, 0), // green
    vec3(0, 0, 1)  // blue
  };
  
  // transform the vertex position from model-space into projection-space
  vec4 position = mvp * vec4(positions[gl_VertexIndex], 1.0);
  
  // pass the vertex position in projection-space to the next stage in pipeline
  gl_Position = position;
  
  // pass the color of the individual vertex to the next stage in pipeline
  fragColor = colors[gl_VertexIndex];
}

So let's break down this shader. Why are there vertex attributes like positions and vertex colors defined in the shader as constant expressions? Is this normal in Vulkan? - No, it is not. The answer for this is that our goal is just drawing a single triangle which does not require us to use any buffers or anything to store the data. Also we would need to configure a vertex layout later on if we wanted to use a vertex buffer. So to make things easier for the moment, the vertex shader provides the actual triangle. All you need to do is setting the vertex count to 3 in a later step for it to work. No worries.


Push constants

Another thing which might throw you off when coming from typical GLSL for OpenGL is that push constants block. So what's that about? Push constants are an addition from Vulkan which makes writing and using shaders easier as well as it increases performance by reducing external memory access.

They allow you to pass data from the host (your CPU) to your device (your GPU) on a per drawcall level. This data will be used inside of your shaders as if it was a constant expression and all of that does not require you to create, manage of bind any buffers. You could say the data will be baked into your pipeline. Pretty neat, right? The only downside is that push constants are limited in size depending on your device. But they provide at least enough space for 128 bytes which is enough for a 4x4 matrix like in this example the model-view-projection matrix (which is only 64 bytes).


Fragment shader

Now let's look at the fragment shader over here. Nothing special really but the graphics pipeline wouldn't work without it.

shaders/shader.frag

#version 450

// interpolated vertex color for each fragment
layout(location = 0) in vec3 fragColor;

// the final fragment color to land on the surface
layout(location = 0) out vec3 outColor;

void main() {
  // pass the interpolated color to write to the attached render target
  outColor = fragColor;
}

Compilation

If you have put both shaders into a new "shaders" subdirectory in your projects folder, you can start thinking about shader compilation. In other graphic APIs shader compilation might be a feature integrated into it. In Vulkan it is a little bit different though. Vulkan actually expects you to write your shaders in the SPIR-V binary format which is an intermediate language. The advantage is that you can write shaders in all kinds of languages besides GLSL for Vulkan and if compiled into SPIR-V before runtime you won't get frame spikes because of the compilation. The disadvantage is that you need to compile it first before your application can do anything with it.

So here is another strength of the VkCV. Shader compilation during runtime is integrated as a module. So you don't need to look into shader compilers or libraries which could do that. Just use the "shader" module in your project as follows:

#include <vkcv/shader/GLSLCompiler.hpp>
vkcv::ShaderProgram shaderProgram; // a new shader program
vkcv::shader::GLSLCompiler compiler; // a new GLSL compiler instance from the module

compiler.compileProgram(
  shaderProgram, // the shader program as target
  {
    { vkcv::ShaderStage::VERTEX, "shaders/shader.vert" },  // the vertex shader
    { vkcv::ShaderStage::FRAGMENT, "shaders/shader.frag" } // the fragment shader
  },
  nullptr // optional callback for the event of the end of compilation
);

The code simply uses a GLSL compiler to compile your both shaders as specified vertex and fragment shader stages into one final program. This can be passed to our graphics pipeline later on. However there's still something to do.


Module integration

The current code wouldn't build without adding the include paths from the shader module and linking its library to the application target in your project's "CMakeLists.txt" configuration. So open up that file and search for the target_include_directories call as well as the target_link_libraries call. Now you should add ${vkcv_shader_compiler_include} as include directory and vkcv_shader_compiler as library. The following is how it would look like coming from the example in the part about application development.

target_include_directories(my_application SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_shader_compiler_include})
target_link_libraries(my_application vkcv ${vkcv_libraries} vkcv_shader_compiler)

Configuration

Now you have the render pass created and the shaders are compiled as one program. So now it's time for the creation of the pipeline, right? Technically you would still need to setup a vertex layout and specify the layouts of descriptor sets for this pipeline.

However because the shaders in this example don't use any vertex attributes or any descriptors, the pipeline can already be created like this:

vkcv::GraphicsPipelineHandle gfxPipeline = core.createGraphicsPipeline(
  vkcv::GraphicsPipelineConfig(
    shaderProgram, // the shader program
    renderPass,    // the render pass as pass handle
    {},            // an empty vertex layout
    {}             // an empty list of descriptor set layouts
  )
);

Previous

Next

Popular posts from this blog

Introduction

Application development

First setup