@

2022 October 17

Quick C Vulkan Graphics Tutorial

Introduction

Vulkan is an application programming interface (API) for using graphics processing units (GPUs). This web page provides a six-part tutorial for using Vulkan in C for graphics applications.

I am not an expert in Vulkan or even graphics. I am a mathematician who has been doing graphics as a hobbyist since 1998. My tutorial is based on the 2020 April version of Alexander Overvoorde's Vulkan tutorial and other sources. I have ported that tutorial from C++ to C and drastically refactored it. My code works, and it is well organized. Probably it's not optimal, fully scalable, or industrial-strength.

Actually, my tutorial is rather different from other tutorials in its goals. I do not try to teach you all of the concepts behind Vulkan. For example, I leave the synchronization primitives a complete mystery. Rather, I try to compartmentalize the complexity, so that you can focus on the aspects of your application that you immediately want to customize, while leaving big chunks of code as black boxes to be unpacked later.

Unfortunately, I must assume that you are familiar with the basic triangle rasterization algorithm, perhaps from prior graphics experience with OpenGL, Direct3D, or Metal. For example, I don't explain how viewing transformations work, what uniforms are, or how a mesh is built from vertices and triangle indices.

Installation

The code below is intended for the clang C compiler on macOS or Ubuntu Linux. If you run Windows, then you have two options. First, you could mimic Alexander Overvoorde's setup instructions for Visual Studio on Windows. Second, you could install the Ubuntu app (Windows Subsystem for Linux) and work inside that, per the instructions below.

Because the details of the installation vary from year to year, I'll hit just the high points, hoping that you can interpolate. First install the Vulkan Software Development Kit. Then:

GLFW is a simple user interface toolkit, that lets us make Vulkan-ready windows in a platform-neutral manner. Eventually you will want to replace GLFW with the native APIs of whatever your targeted platform is.

Finally, download the STB Image library (meaning, the file stb_image.h). Place it in your working directory.

OpenGL History

Skip this section, if your goal is to get to Vulkan as quickly as possible. This section briefly surveys the history of Vulkan's predecessor, OpenGL. It demonstrates how graphics APIs have become increasingly low-level over time, requiring the programmer to exert more and more control, with the benefit of faster and faster execution speed. This trend partially explains why Vulkan is so complicated.

You might not be able to compile all of these examples without installing additional software such as GL3W. My recommendation is: Don't bother. The examples produce pretty dull imagery. The important thing is to read the code.

370mainStrings.c: This tutorial has nothing to do with graphics. It illustrates the general idea that programmer-friendly, high-level code often executes more slowly than machine-friendly, low-level code does. Specifically, it builds a high-level string class for C and runs timing tests. On every machine that I've tried, the running times for the high-level code are about four times the the running times for the low-level equivalent.

380mainGLFW.c: This file makes a GLFW window and draws a triangle into it. This strategy for drawing triangles is easy to understand and customize. It is also crazily slow. A scene with 100,000 triangles would require something like 1,000,000 function calls from the CPU to the GPU per animation frame. That's far too many!

390mainOpenGL14.c: This is how you might make graphics, if you were using OpenGL 1.4 circa 2002. On each animation frame, triangle data are transferred from CPU to GPU in one or two big function calls. That's better than making a million tiny function calls, but it still amounts to transferring the entire scene from CPU to GPU on each frame.

400mainOpenGL15.c: In OpenGL 1.5, you can store scene data on the GPU. Then, on each animation frame, just a couple of small function calls are enough to get the GPU to render those data. Much better!

410shading.c: This helper file offers functions for making shader programs. Most of the code is error checking.

410mainOpenGL20.c: In OpenGL 2.0, you can write vertex and fragment shaders to highly customize the key "artistic" parts of the triangle rasterization algorithm. But you don't have to customize everything. For example, you can manipulate modelview and projection matrices as in OpenGL 1.

420mainOpenGL20.c: Don't be fooled by that olive branch. If you can customize features such as the modelview and projection matrices, then pretty soon you'll be required to customize them. So here's another OpenGL 2.0 tutorial that customizes more.

430mainOpenGL32.c: OpenGL 3 introduces a backward-compatibility system that's not easy to use. Also, it requires you to wrap your mesh renderings in vertex array objects (which, contrary to their name, are not arrays of vertex data). Hand-holding features such as the modelview matrix are long gone. On the plus side, OpenGL 3.2 offers killer features such as the ability to render into off-screen framebuffers.

Extrapolating this trend toward more and more control by the user, and throwing in other trends (such as GPU applications that have nothing to do with graphics), maybe it makes sense that Vulkan is what it is.

The Six-Part Tutorial

Here is my Vulkan tutorial. Each of the six parts is simply a few files annotated with comments. By the end, the reusable helper files total 2,182 lines of code, and the last main.c is 965 lines (not including the shaders).

Tutorial 1: Basics

This first part provides the extreme rudiments of a Vulkan setup for graphics.

440gui.c: This helper file provides a simple window based on the GLFW toolkit. It doesn't do any Vulkan.

440vulkan.c: This helper file provides crucial machinery: Vulkan instance, physical device, logical device, extensions, validation layers.

440mainVulkan.c: This file should compile and run, showing a blank window. It should print responses to certain keyboard and mouse events. If you instead get errors, then try reconfiguring the constants according to the directions.

Tutorial 2: Swap Chain

Unfortunately, we need a lot more machinery to get Vulkan going, before we can run a proper animation loop. The crucial concept here is the swap chain: roughly speaking, the queue of raster images to be rendered and shown to the user.

450buffer.c: This helper file lets you use chunks of memory on the GPU.

450image.c: This helper file lets you use chunks of memory on the GPU that are optimized for holding raster images.

450swap.c: This helper file provides the swap chain machinery.

450mainSwap.c: This file should compile and run, showing a black window and throwing an error on each animation frame.

Tutorial 3: Shader and Meshes

This next part of the tutorial actually produces some imagery. Hooray. Also, we glimpse our first OpenGL Shading Language (GLSL) code.

460shader.vert: This is a vertex shader written in GLSL.

460shader.frag: This is a simple fragment shader written in GLSL.

460shader.c: This helper file lets you build shader programs from compiled GLSL shaders.

460mesh.c: This helper file lets you build meshes as vertex buffers and index (triangle) buffers.

460mainMeshes.c: In the instructions at the top, notice that you must compile not only this C file but also the two shader files. The running application should produce a static image in bright pinks, greens, and cyans.

Tutorial 4: Scene-Wide Uniforms

This fourth part shows how to declare and set uniforms that have a single value across the entire scene. Practical examples might include time, the camera, and lights. An important concept here is descriptors, which specify how the uniforms connect to the shader program.

480shader.vert: The camera matrix is now being passed into the shaders as a uniform.

480shader.frag: We also pass a color into the shaders, just as another example.

480uniform.c: This helper file lets us allocate the memory needed to pass data into shaders through uniforms.

480description.c: This helper file lets us communicate the structure of the uniforms to the shaders.

480mainUniforms.c: Don't forget to compile the two shader files. The running application should produce a rotating version of the previous tutorial's imagery. In the setSceneUniforms function, change the uniform color, to check that it produces the correct effect in the fragment shader.

Tutorial 5: Body-Specific Uniforms

Let's use the term "body" to mean "object in the scene". A scene might contain many bodies: a landscape, a tree, a bird, etc. Let's assume that all of the bodies have the same kinds of uniforms; for example, each body might be positioned and oriented using a 4x4 modeling matrix. However, they usually don't have the same values for those uniforms; for example, they don't all use the same modeling matrix. This fifth part of the tutorial shows how to declare uniforms that can be set with body-specific values.

500shader.vert: The modeling matrix is now passed into the vertex shader as a uniform.

500mainUniforms.c: If everything is working, then one of the two bodies rotates, the other does not, and the camera revolves around them.

Tutorial 6: Textures

In this final part of the tutorial, we introduce texture mapping. Technically, textures are a kind of uniform. In some ways they are treated like other uniforms; for example, they require descriptors. In other ways they are different from other uniforms; for example, they don't live in uniform buffer objects, and the process of setting them scene-wide or per-body is a bit surprising.

reddish.png, bluish.png, grayish.png: Download these three textures.

520shader.vert: We share the body uniforms between the vertex shader and the fragment shader, so we have to change the vertex shader, even though only the fragment shader benefits from the new body uniforms.

520shader.frag: The fragment shader has access to all three textures in the scene. It samples from two of them as chosen by the body uniforms.

520texture.c: This helper file provides two kinds of machinery: textures, and the samplers that sample from them.

520mainTextures.c: The running application texture-maps each mesh with two textures.

Conclusion

To build a graphics engine, that you could use to make a real application, you probably want to augment this tutorial with decent libraries for linear algebra, building meshes, rendering bodies or scene graphs, etc. Sorry, but I'm not giving you mine.

Before you build your own graphics engine, you might want to make sure that you have the best ideas about how to manage Vulkan. (The code that I provide is without warranty or guarantee of merchantability or fitness for any particular purpose.) To that end, here is some further reading, including three of the many sources that I have used in constructing my tutorial.

In the 2022 April version of Alexander Overvoorde's tutorial, the handling of the swap chain is noticeably different from that in the 2020 April version. Someday perhaps I will update this tutorial accordingly. Or maybe you can.

That's it. Good luck!