Java port of the great tutorial by Alexander Overvoorde. The original code can be found here.
- Introduction
- LWJGL
- Drawing a triangle
- Vertex buffers
- Uniform buffers
- Texture mapping
- Depth buffering
- Loading models
- Generating Mipmaps
- Multisampling
These tutorials are written to be easily followed with the C++ tutorial. However, I've made some changes to fit the Java and LWJGL styles. The repository follows the same structure as in the original one.
Every chapter have its own Java file to make them independent to each other. However, there are some common classes that many of them need:
- AlignmentUtils: Utility class for dealing with uniform buffer object alignments.
- Frame: A wrapper around all the necessary Vulkan handles for an in-flight frame (image-available semaphore, render-finished semaphore and a fence).
- ModelLoader: An utility class for loading 3D models. They are loaded with Assimp.
- ShaderSPIRVUtils: An utility class for compiling GLSL shaders into SPIRV binaries at runtime.
For maths calculations I will be using JOML, a Java math library for graphics mathematics. Its very similar to GLM.
Finally, each chapter have its own .diff file, so you can quickly see the changes made between chapters.
Please note that the Java code is more verbose than C or C++, so the source files are larger.
I'm going to be using LWJGL (Lightweight Java Game Library), a fantastic low level API for Java with bindings for GLFW, Vulkan, OpenGL, and other C libraries.
If you don't know LWJGL, it may be difficult to you to understand certain concepts and patterns you will see throughout this tutorials. I will briefly explain some of the most important concepts you need to know to properly follow the code.
Vulkan has its own handles named properly, such as VkImage, VkBuffer or VkCommandPool. These are unsigned integer numbers behind the scenes, and because Java does not have typedefs, we need to use long as the type of all of those objects. For that reason, you will see lots of long variables.
Some structs and functions will take as parameters references and pointers to other variables, for example to output multiple values. Consider this function in C:
int width;
int height;
glfwGetWindowSize(window, &width, &height);
// Now width and height contains the window dimension values
We pass in 2 int pointers, and the function writes the memory pointed by them. Easy and fast.
But how about in Java? There is no concept of pointer at all. While we can pass a copy of a reference and modify the object's contents inside a function, we cannot do so with primitives. We have two options. We can use either an int array, which is effectively an object, or to use Java NIO Buffers. Buffers in LWJGL are basically a windowed array, with an internal position and limit. We are going to use these buffers, since we can allocate them off heap, as we will see later.
Then, the above function will look like this with NIO Buffers:
IntBuffer width = BufferUtils.createIntBuffer(1);
IntBuffer height = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(window, width, height);
// Print the values
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));
Nice, now we can pass pointers to primitive values, but we are dynamically allocating 2 new objects for just 2 integers. And what if we only need these 2 variables for a short period of time? We need to wait for the Garbage Collector to get rid of those disposable variables.
Luckily for us, LWJGL solves this problem with its own memory management system. You can learn about that here.
In C and C++, we can easily allocate objects on the stack:
VkApplicationInfo appInfo = {};
// ...
However, this is not possible in Java. Fortunately for us, LWJGL allows us to kind of stack allocate variables on the stack. For that, we need a MemoryStack instance. Since a stack frame is pushed at the beginning of a function and is popped at the end, no matter what happens in the middle, we should use try-with-resources syntax to imitate this behaviour:
try(MemoryStack stack = stackPush()) {
// ...
} // By this line, stack is popped and all the variables in this stack frame are released
Great, now we are able to use stack allocation in Java. Let's see how it looks like:
try(MemoryStack stack = stackPush()) {
IntBuffer width = stack.mallocInt(1); // 1 int unitialized
IntBuffer height = stack.ints(0); // 1 int initialized with 0
glfwGetWindowSize(window, width, height);
// Print the values
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));
}
Now let's see a real Vulkan example with MemoryStack:
private void createInstance() {
try(MemoryStack stack = stackPush()) {
// Use calloc to initialize the structs with 0s. Otherwise, the program can crash due to random values
VkApplicationInfo appInfo = VkApplicationInfo.calloc(stack);
appInfo.sType(VK_STRUCTURE_TYPE_APPLICATION_INFO);
appInfo.pApplicationName(stack.UTF8Safe("Hello Triangle"));
appInfo.applicationVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.pEngineName(stack.UTF8Safe("No Engine"));
appInfo.engineVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.apiVersion(VK_API_VERSION_1_0);
VkInstanceCreateInfo createInfo = VkInstanceCreateInfo.calloc(stack);
createInfo.sType(VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
createInfo.pApplicationInfo(appInfo);
// enabledExtensionCount is implicitly set when you call ppEnabledExtensionNames
createInfo.ppEnabledExtensionNames(glfwGetRequiredInstanceExtensions());
// same with enabledLayerCount
createInfo.ppEnabledLayerNames(null);
// We need to retrieve the pointer of the created instance
PointerBuffer instancePtr = stack.mallocPointer(1);
if(vkCreateInstance(createInfo, null, instancePtr) != VK_SUCCESS) {
throw new RuntimeException("Failed to create instance");
}
instance = new VkInstance(instancePtr.get(0), createInfo);
}
}
The shaders are compiled into SPIRV at runtime using shaderc library. GLSL files are located at the resources/shaders folder.
(Will cause Validation Layer errors, but that will be fixed in the next chapter)
The models will be loaded using Assimp, a library for loading 3D models in different formats which LWJGL has bindings for. I have wrapped all the model loading stuff into the ModelLoader class.
Icons made by Icon Mafia