Comparison
Here we discuss similarities and differences between OpenGL and Vulkan

Conceptual Differences

OpenGL

Vulkan

Global state machine: For example glClearColor(), glUseShader() and glBindVertexArray() modify the global state in OpenGL. This leads to fewer objects that must be passed around but it is hard to have two separate OpenGL contexts at once.

Object oriented local state: VkInstance is one of the first objects you will encounter. It stores some of the configuration internally. The downside is that we will have many of these "book-keeping" objects around at all times. However as they encapsulate all these data using them leads to cleaner code.

Everything is preconfigured: To use the z-Buffer we just glEnable(GL_DEPTH_TEST). and later glClear(GL_DEPTH_BUFFER_BIT). All the resources necessary are configured in the background completely transparent to the programmer.

Must configure everything:. For example in order to use use the depth buffer we must allocate memory, create an image for the buffer, create an image view for the image and attach that image to the pipeline.

Dynamic: We can basically modify every aspect of the configuration at runtime. We can e.g. switch shaders, change the viewport or draw in wireframe mode by simply changing the global state.

Static: There are very few things we can change. In most cases the pipeline has to be rebuilt. Even those things that could change (the viewport dimensions) must configured accordingly.

Automatic memory management: Call glGenBuffers() and you get a finished buffer. You can pass data to it without worrying about performance penalties.

Manual memory management: For anything that resides on the GPU's memory we must allocate memory manually and bind that memory to the buffers or images. In order to upload data to the GPU we actually allocate two blocks of memory. One high performance memory on the GPU and a staging memory area that is used to transfer the data from the host to the GPU.

Render loop: Basically we loop over each object we want to draw, bind its VAO, use its shader, and draw it – a you can see here. Before that, we might want to reset the canvas to a solid color and in the end, we swap the buffers.

Command buffers: Vulkan takes a different approach here. For a static scene we record the draw calls in a so-called command buffer which is then replayed every time we want to redraw the scene. This is one of the cases where Vulkan is more efficient as this approach leads to significantly lower driver overhead. See a detailed example on the Vulkan page.

Focus on graphics: As the name already implies, OpenGL was initially created to only provide a graphics programming interface. In version 4.2 an extension was introduced that allowed compute shaders. With the help of these arbitrary calculations can be performed on the GPU.

Computing and graphics Vulkan is designed from the ground up to support graphics pipelines as well as compute pipelines. As the primary purpose of this work was to compare the graphics capabilities, we did not investigate further into this direction. However, it is worth a note — especially if you plan on combining GPU accelerated calculations with computer graphics.

OpenGL vertex shader

 1 #version 450
 2 #extension GL_ARB_separate_shader_objects : enable
 3 
 4 layout(location = 0) in vec3 inPosition;
 5 layout(location = 1) in vec3 inNormal;
 6 layout(location = 2) in vec2 inTexCoord;
 7 
 8 layout(location = 0) out vec3 outPosition;
 9 layout(location = 1) out vec3 outNormal;
10 layout(location = 2) out vec2 outTexCoord;
11 
12 
13 uniform mat4 view;
14 uniform mat4 projection;
15 
16 
17 
18 uniform mat4 model;
19 uniform mat4 normal;
20 
21 
22 
23 
24 
25 
26 void main() {
27     gl_Position = model * vec4(inPosition, 1.0);
28     outPosition = gl_Position.xyz;
29     gl_Position = projection * view * gl_Position;
30 
31     outNormal = (normal * vec4(inNormal, 0)).xyz;
32     outTexCoord = inTexCoord;
33 }

The #extension line is actually no necessary but when we use this extension we can equalize the two shaders a bit more which helps a lot in development. Because of this the in and out variables are declared identically in both shaders. In OpenGL however there is only one type of uniform which is why all the uniforms are declared at the root level. Also the implicit out variable gl_Position doesn't have to be declared.

Vulkan vertex shader

 1 #version 450
 2 #extension GL_ARB_separate_shader_objects : enable
 3 
 4 layout(location = 0) in vec3 inPosition;
 5 layout(location = 1) in vec3 inNormal;
 6 layout(location = 3) in vec2 inTexCoord;
 7 
 8 layout(location = 1) out vec3 outPosition;
 9 layout(location = 2) out vec3 outNormal;
10 layout(location = 3) out vec2 outTexCoord;
11 
12 layout(binding = 0) uniform GlobalUniform {
13     mat4 view;
14     mat4 projection;
15 } global;
16 
17 layout(binding = 2) uniform LocalUniform {
18     mat4 model;
19     mat4 normal;
20 } local;
21 
22 out gl_PerVertex {
23     vec4 gl_Position;
24 };
25 
26 void main() {
27     gl_Position = local.model * vec4(inPosition, 1.0);
28     outPosition = gl_Position.xyz;
29     gl_Position = global.projection * global.view * gl_Position;
30 
31     outNormal = (local.normal * vec4(inNormal, 0)).xyz;
32     outTexCoord = inTexCoord;
33 }

The most apparent difference is that we must declare the out variable gl_Position and the nested uniforms. As Vulkan supportes three types of uniforms they are also grouped in the shader. The three types are: 1. "normal" uniforms like those in OpenGL (called global here), 2. dynamic uniforms when working with instanced draw calls (local in the code) and 3. push constants which are a very fast – but limited in size – type of uniforms (not used here).