Spulkan: 'Spece Generator' Port with Vulkan and Rust
Spulkan -- short for 'Space Generator' Port with Vulkan and Rust -- is a Rust port using Vulkan of my project from Computer Graphics course. The Space Generator project was written using Three.js. My main goals are to learn Vulkan and Rust and to use shader types that are not available for Three.js.
Please note that this project is very ambitious. Not all the goals will be achieved during this semester.
How to activate:
.\Project-Spulkan-win.exe2 <preset> <speed> where
<preset> is one of three possible configurations (which are
<speed> determines the speed of the simulation. This has to be
float! When set to 0.0, everything stays still, when set to 1000.0, everything is very fast! Note that the higher the
<speed> is, the less imprecise the simulation is since the time step is larger. Ideally, we want the time step to be equal to Planck time, which is impossible with current technology.
.\Project-Spulkan-win2.exe stable 2.0 will activate the simulation with a stable 3-body configuration where time is twice as fast as normal. Or
.\Project-Spulkan-win.exe2 random 20.0 will generate and place random planets all around the place, with time 20 times faster than usual. See how the chaos unfolds!
To control the camera, use WASD keys. To rotate the camera, use arrow keys. Note that the rotation is broken, so use with caution! You can also roll the camera with QE keys.
Press ESC to exit the program.
- Windows: download
Milestone 1 (06.03)
- Complete Rust tutorial (4h)
- Complete Vulkan tutorial (4h)
- Create a simple triangle (1h)
I followed the rustlings tutorial. I did not complete it fully, but all the essential parts are completed.
Then I followed the Vulkano tutorial here. I completed the 'Drawing a triangle' chapter. This part was a lot more work than I predicted.
Installation was pretty straightforward. Remember to install the drivers BEFORE software devkit!
Vulkano is a safe Rust wrapper for Vulkan API. It includes many convenience functions, which make the development easier. The user is not dependent on it, so it can be bypassed. Vulkano also includes vulkano_shaders crate, which can compile shaders to SPIR-V byte code when compiling the Rust code. More info about Vulkano: http://vulkano.rs/.
Picture of the result:
Some notable things about Vulkan:
- Everything has to be explicitly defined. For example, Three.js sets all the default values, like blending mode, backface culling, and so on. In Vulkan, everything has to be specified explicitly.
- Vulkan uses virtual devices. One physical device can be split into many virtual devices. And each virtual device can have a different render pass, render targets, etc. This might be useful in the future when I want to do physics calculations on GPU.
- Vulkan doesn't have debug messages enabled by default. Instead, the developer has to configure a validation layer that takes a callback. This callback can then print out debug, error, or just regular info messages. This means that the release build can disable the validation layer, making the application faster.
- Vulkan used SPIR-V bytecode for shaders. The compilers for SPIR-V are much more sophisticated than Three.js had. For example, I can use #include in shader code.
Milestone 2 (20.03)
- Create a graph of how Vulkan works. I want to get a better overview of how Vulkan works (2h)
- Continue with the Vulkan tutorial. Complete Vertex Buffer and Uniform Buffer chapter (4h)
- (Optional) Create a cube or a sphere (2h)
In the Vulkano repository, there are many examples, including the triangle task. I think this is much more clear than the code in the tutorial (less spaghetti and more comments). Everything is very clearly explained. Check it out here. There are many more examples of different functionalities in the parent directory.
I created a repository for my work. It can be found here.
I studied the Vulkano examples and familiarized myself with how to create different buffers (vertex buffer, normal buffer, index buffer, etc.) and how to use descriptor sets (uniforms included). Initial results can be seen in the following gif:
Creating a sphere is just a matter of adding more triangles, normals, and indices. After that, adding the planet fragment shader should be trivial.
I found a good guide on how to create icospherers here. I followed the tutorial and the end result can be seen here:
It is not quite a sphere, it is an icosahedron, the tessellation part seems to be buggy for me. It also seems that some of the normals are not calculated correctly. You can also see that one of the faces is missing!
A simple diagram of how Vulkan works:
Milestone 3 (03.04)
- Fix the sphere bugs (1h)
- Have many spheres (instancing?) (3h)
- Add planet shaders to spheres (2h)
- Clean up code and separate logical blocks into different files (1h)
I fixed the icosphere bugs. Firstly, there was one face missing in the icosahedron. This was caused because one face had a wrong vertex index. Secondly, the tessellation was broken. I fixed it by adding -1 to the returned vertex index (off by one error). Here is the result:
It looks like a smooth sphere after the third tessellation pass.
I split the logic into multiple files. I also put more rendering logic into Icosphere class, so I can create multiple instances of them. In this gif, I created two different icospheres with different radius and tessellation levels.
I tried to add the shaders from my previous project. I was partially successful, it seems that my
noise function. I tried to fix this by using
double instead of
float, but the GPU reported error code STATUS_ACCESS_VIOLATION. Perhaps I have to enable Vulkan extensions?
Milestone 4 (17.04)
- Try wireframe rendering (30m)
- Enable back-face culling (30m)
- Fix the noise function (4h)
- Make planets move (4h)
I enabled back-face culling and wireframe mode. When enabling the back-face culling, I discovered that the handedness of triangles of icospheres was wrong. I just had to change the initial faces to fix it.
I fixed the noise function, but I have no idea why it did not work before. I also added a loop unrolling to the shaders.
Here is a picture of planets with colors:
Unfortunately, I could not get the planet movement to work. I got the program compiling, but it crashes when I add a planet positional data buffer to a descriptor set. I researched the public gitter for vulkano for an answer, and I found some dude who had the same problem. I believe the solution would be to create the data structure in the shader and then use the data type inferred with the macro as a buffer type. That is how it is done with uniforms.
Milestone 5 (08.05)
- Complete the planet movement (6h)
- Fix lighting (3h)
First things first, I wanted to get the compute shader working. I made a simple compute shader, where it increments y-coordinates of planets. Then I dispatched it and read the first buffer element to see if it happened. At first, the y-coordinate remained 0. The problem was in the dispatch function, where I requested [planet_amount, 0, 0] jobs. The correct way is to use ones instead of zeros since it defines a 'space' where each voxel (or group of them) creates a thread.
The next step was to use the data in the render pass. For that, I passed the buffer to shaders as a uniform. And it worked!
For more complicated movements (such as oscillating or even gravity calculations), the time needed to be added to the compute shader. Uniforms can not be used with compute shaders. I used push constants instead. Push constants are not placed in memory, instead, they are part of a pipeline itself. They are usually small, the standard provides at least 128 bytes. Which is perfect for the time, since it takes only 4 bytes.
I implemented gravity. For each planet, I created a workgroup, and in each workgroup, I created a planets amount of threads. In total, there are n*n threads, where n = amount of planets.
I also discovered the source of a rendering bug. Namely, some pixels on the planet were rendered black, which resulted in a dotty surface, which some times created interesting patterns. The problem was in the diffuse lighting calculation, where a square root was used on a potentially negative number, which resulted in NaN. I resolved it by using
The weird specular lighting and atmosphere glow are also fixed. In this case, the problem was that I used variables that were in different spaces. The input for the noise function has to be in the object-space. This way, if the object is rotating, so is the texture on its surface. The coordinates for lighting calculations have to be in the world-space. I used the object-space coordinates for both, which resulted in the weird lighting.
The next step was to incorporate the soft shadow algorithm that we developed for the project. For this, I needed to know the position and radius of all the planets. Luckily, they were in the buffer that I had used for the movement! I had to change the shadow function a little bit since previously data-texture had been used instead of a buffer.
Finally, I also didn't like that the planets just passed through each other. So I implemented a simple bouncing using the GLSL function
reflect. In the gif, the shadows can also be seen.
Milestone 6 (22.05)
- Add the other planet types (1h)
- Add light sources (1h)
- Add camera movement (4h)
- Add initial system generation (2h)
- BONUS: Add stars to the sky (2h)
This milestone was really difficult. I wanted to use Rust 'Interface'-s, but the type system is too rigid. Before I surrendered, I spent at least 8h on it. I had to use a more hard-coded approach. More, I found many different camera libraries that were on paper perfect for me. But all of them used different Event systems, and some of them didn't even have WASD controls. So, I had to implement my own. I am not satisfied with my implementation since it doesn't even use a mouse.
I added a sun
In the picture, you can also see that the sun is the light source.
Added camera movement. It only works with buttons at the moment.
In the previous GIF, random system generation can also be seen.