2025 May 7,
The second part of CS 311 deals with the same triangle rasterization algorithm as in the first part. However, instead of implementing it ourselves, we learn how to use Vulkan to run the algorithm on the graphics processing unit (GPU). Then we layer new techniques atop the basic algorithm.
Remember our policies: Except where otherwise noted, each homework assignment is due at the start of the next class meeting. If you don't have an assignment finished on time, then don't panic, but do e-mail me about it, so that we can formulate a plan for getting back on track.
We do some of these tutorials in class on Day 14. Finish the others.
We are not learning OpenGL. We are just skimming it, to deepen our understanding of how the CPU controls the GPU. As you skim, keep these two questions in mind: How much communication takes place between the CPU and the GPU? How finely do we control the GPU (as opposed to having it do stuff for us by default and automatically)?
380mainOpenGL10.c: Skim, focusing on the render function and the call to glfwSwapBuffers. This code uses OpenGL 1.0 (1992). Why is it inefficient?
390mainOpenGL11.c: Skim, focusing on render. This code uses OpenGL 1.1 (1997). How is it more efficient, and how is it still inefficient?
400mainOpenGL15.c: Skim, focusing on render. This code uses OpenGL 1.5 (2003). How is it more efficient?
410shading.c: Skim. This file offers helper functions for making shader programs. Most of the code is error checking.
410mainOpenGL20.c: Skim. This code uses OpenGL 2.0 (2004). What is the big change from OpenGL 1.5 to OpenGL 2.0?
420mainOpenGL20.c: Skim, focusing on the comments. This code also uses OpenGL 2.0. The change from the previous tutorial is small, but it epitomizes my point about the programmer exerting more control.
430mainOpenGL32.c: Skim. This code uses OpenGL 3.2 (2009). You probably can't compile it. That's okay. There are several changes. How do they illustrate my point?
Now we start learning Vulkan. You are not expected to memorize specific function calls or structure definitions. You are expected to understand the high-level concepts and remember which files do what, so that you can look details up when you need them.
This first tutorial provides the extreme rudiments of a Vulkan setup for graphics (as opposed to, say, BitCoin mining or neural network training).
440gui.c: Skim. This file provides a simple window based on the GLFW toolkit. It doesn't do any Vulkan.
440vulkan.c: Skim. This file provides key machinery: Vulkan instance, physical device, logical device, etc.
440mainVulkan.c: Skim. 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.
Unfortunately, we need a lot more machinery to get Vulkan going, before we can run a proper animation loop. The key concept here is the swap chain. Roughly speaking, it's the queue of raster images to be rendered and shown to the user.
450buffer.c: Skim. This file lets you use chunks of memory on the GPU.
450image.c: Skim. This file lets you use chunks of memory on the GPU, that are optimized for holding raster images.
450swap.c: Skim. This file provides the swap chain machinery.
450mainSwap.c: Skim. This file should compile and run, showing a black window.
This next program actually shows some imagery. Hooray. Also, we glimpse our first OpenGL Shading Language (GLSL) code.
460shader.vert: Study. This is a vertex shader written in GLSL. Even though I haven't taught you GLSL, can you guess what each line does?
460shader.frag: Study. This is a simple fragment shader written in GLSL.
460shader.c: Skim. This file lets you build shader programs from compiled GLSL shaders.
460vesh.c: Skim. This file lets you build meshes as vertex buffers and index (triangle) buffers. "Vesh" is my shorthand for "Vulkan mesh".
460pipeline.c: Skim. This code constructs the pipeline, which records a bunch of rendering settings, including how data are fed into the shaders.
460mainMeshes.c: Skim. 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.
The work, that we do now, is the first work to be handed in for this second course project. The goal is to package meshes better, so that we can write larger applications more easily. Along the way, we re-introduce some of our old graphics engine.
We do everything in single-precision floats rather than double-precision doubles, because a little extra precision is not worth doubling the memory required. Similarly, we use 16-bit uint16_ts for our triangle indices, even though they limit our meshes to 65,536 vertices each.
470vector.c: Make a copy of 240vector.c. Replace all doubles with floats. (I was able to accomplish this task using a simple search-and-replace.)
470mesh.c: Download. Starting with a copy of 360mesh.c, I have replaced all doubles with floats. I've also adjusted a bunch of ints to uint16_ts. Finally, I've deleted meshRender and its helpers. I'll never forget you, meshRender!
470mesh2D.c: In a copy of 180mesh2D.c, replace all doubles with floats.
470mesh3D.c: Download. Starting with a copy of 240mesh3D.c, I have replaced all doubles with floats, and I've replaced several ints with uint16_ts.
470vesh.c: Study. There are two methods for you to implement. I recommend that you implement them by copying a bunch of functions from 460vesh.c into the private section of this file and then calling those functions from the methods. In the end, this file should not rely on 460vesh.c, because that file is not part of our engine.
470shader.vert: Make a copy of 460shader.vert. Meshes built by the functions in 470mesh3D.c have a certain attribute structure: XYZ, ST, NOP. Update the vertex shader accordingly. Make the varying color however you like, as long as you use at least two of S, T, N, O, P. Compile this new shader to 470vert.spv.
470mainMeshes.c: Make a copy of 460mainMeshes.c. Make sure that 460vesh.c is not included! Adjust the rest of the code to initialize, render, and finalize two decent meshes, as follows. Include the five "470....c" files. In initializeArtwork, make sure that 470vert.spv is loaded. There are six global variables and one global constant pertaining to mesh style. Replace them with a single veshStyle global variable. In initializeArtwork, replace the call to veshGetStyle with a call to veshInitializeStyle. Don't forget to finalize the style in finalizeArtwork. Wherever pipeInitialize is called, you need to pass it certain members of the style. Remove all 16 global variables pertaining to meshes A and B. Replace them with two global variables holding veshVeshes. In initializeArtwork, initialize them from two temporary 3D meshMeshes of your choosing. Don't forget to finalize the veshes in finalizeArtwork. Elsewhere in this file, there is a chunk of code that renders the two veshes. Carefully replace that chunk with two calls to veshRender. Then you should be done. Here's a screenshot of my version, which uses a box and a capsule:
You have five files to hand in: 470vector.c, 470mesh2D.c, 470vesh.c, 470shader.vert, and 470mainMeshes.c. Make sure that each C file credits both partners in a comment at the top. (In some C files, the old partners should be credited too!) Make sure that each file is working, clean, and commented appropriately. Make sure that both partners have a copy of each file. Then, just one partner submits the files to their hand-in folder on the COURSES file server.
This tutorial shows how to declare and set uniforms that have a single value across the entire scene. Practical examples include the camera and the lights (which we study later). An important concept here is descriptors, which specify how the uniforms connect to the shader program.
480shader.vert: Study. The camera matrix is now being passed into the shaders as a uniform.
480shader.frag: Study. We also pass a color into the shaders, just as another example.
480uniform.c: Skim. This file helps us allocate the memory needed to pass data into shaders through uniforms.
480description.c: Skim. This file helps us communicate the structure of the uniforms to the shaders.
480mainUniforms.c: Study. Don't forget to compile the two shader files. The running application should produce a rotating version of the previous tutorial's imagery. In setSceneUniforms, change the uniform color, to check that it produces the correct effect in the fragment shader.
The preceding tutorial uses a pretty crummy camera. Let's replace it with the camera machinery from our first course project. (We leave the meshes unimproved this time.)
490matrix.c: In a copy of 280matrix.c, replace all doubles with floats.
490isometry.c: In a copy of 300isometry.c, replace all doubles with floats.
490camera.c: In a copy of 300camera.c, replace all doubles with floats. Near the top of the file, declare the following constant matrix. It is needed, to make our projections match Vulkan's conventions, as follows. The function camGetOrthographic should output camVulkan times what it used to output. Delete the comment just before that function, because it is no longer accurate. Delete camGetInverseOrthographic, because it is no longer accurate and we don't need it. Then repeat these steps for the perspective projection. Check that camGetProjectionInverseIsometry is using these new projection matrices.
const float camVulkan[4][4] = { {1.0, 0.0, 0.0, 0.0}, {0.0, -1.0, 0.0, 0.0}, {0.0, 0.0, 0.5, 0.5}, {0.0, 0.0, 0.0, 1.0}};
490mainCamera.c: In a copy of 480mainUniforms.c, #include 470vector.c and the 490....c files. Declare global variables for the camera's ρ (rho), φ (phi), and θ (theta), to be used in camLookAt and camSetFrustum. (Don't forget that camSetFrustum's focal length should always match camLookAt's ρ.) Configure a camCamera as part of the artwork, using camSetProjectionType and those two camera functions, getting the width and height from swap.extent.width and swap.extent.height. In setSceneUniforms, set the uniform matrix using the camera and mat4Transpose. Delete code that's no longer needed. Test.
490mainCamera.c: Add a keyboard handler that lets the user increase/decrease θ with L/J and lets them switch projection type with P. Test.
490mainCamera.c: Have you tried resizing the window? Try making it really tall and narrow or short and wide. Probably the image appears distorted, because the camera no longer knows the window size. We need to fix that. When the user resizes the window, finalizeInitializeSwapChainPipeline is called, and inside that function swapInitialize is called. At any time after that swapInitialize — either in finalizeInitializeSwapChainPipeline or during the next frame's rendering — the camera can call camSetFrustum to update the width and height. Implement this idea now. The exact details depend on how and where you update the camera. When it's working correctly, the window size may affect the size of the rendered meshes, but it should not affect their shape. They should not be distorted.
You have four files to hand in: 490matrix.c, 490isometry.c, 490camera.c, and 490mainCamera.c. Make sure that they are credited, working, clean, and commented. Make sure that both partners have copies. Then one partner submits them to COURSES.
In the preceding exercise, we inform the camera that the window size has changed. What other part of the triangle rasterization algorithm needs to be informed of window size changes? Apparently it is happening correctly, somewhere in our code. Where?
The code introduced today works on macOS, Linux, and probably Windows, but it does not work on Windows Subsystem for Linux (WSL). If you are using WSL, then shift to working on the lab computers in Olin 304 for today and the rest of this project. Sorry for the inconvenience. :(
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 tutorial shows how to declare uniforms that can be set with body-specific values.
500shader.vert: Study. The modeling matrix is now passed into the vertex shader as a uniform.
500mainUniforms.c: Study. If everything is working, then one of the two bodies rotates, the other does not, and the camera revolves around them.
In this final Vulkan 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.
510shader.vert: Study. 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.
510shader.frag: Study. The fragment shader has access to all three textures in the scene. It samples from two of them as chosen by the body uniforms.
510texture.c: Skim. This file provides two kinds of machinery: textures, and the samplers that sample from them.
510mainTextures.c: Study. The running application texture-maps each mesh with two textures.
The code for the textures is messy, in that we maintain 14 variables to handle three textures and two samplers. What is a good way to abstract this code? Should we make a texTexture class? How would that class improve the code? Might it make the code worse? Should we make a texArray class? How might it improve or worsen the code?