Michael's Blog

Infinite Terrain & Multithreading! DevBlog #6

Published 24th April 202010 min readDevBlog
png
Image: Placing the player in the centre of the terrain

This week on my 6th DevBlog! The main two areas I have focused on is loading and unloading terrain when the player moves. I have also focused on multithreading such as generating the terrain and creating each chunk mesh on separate threads, so the main thread isn’t blocked increasing the performance.

Loading & unloading chunks

My first task was to centre the player around the terrain, and I done that by getting the camera’s position and making the terrain in the positive and negative x,z direction. As seen above with the terrain being 5x5 the terrain is offset accordingly +2 and -2 in the x,z direction. Why is this necessary? Well the way I planned on doing it was having the player centred around the terrain so when the player moves into another chunk more chunks are generated in that direction the player has moved, also removing chunks that are behind to keep the number of draw calls down.libnoise that creates a heightmap using Perlin noise and has good tutorials and is worth checking out!

gif
Iteration 1: Rendering new chunks work but offset isn't correct

Now in my first iteration of loading and unloading terrain I managed load new chunks, but I did have the wrong bounds set for the new terrain but for now lets not leave that for a bit since the main purpose was trying to load and unload chunks. First I used an unordered map which is great because I could have a key of glm::vec3 making it so nice to look up terrain and it’s fast too. Once I set that up, I could easily remove and add chunks. I first started in the x direction by subtracting the previous position to the current position and seeing if its greater than the chunk size in the positive direction and is less than 0 for the negative direction as you would be inside the bounds of a different chunk.

gif
Iteration 2: Loading new chunks with correct offset now

As for the second iteration I realised that for the unordered map key I was just setting the vec3 coordinates as: 0,0,0 (centre) 1,0,0 (one chunk to the right) I realised this makes no sense as the chunks are 32x32x32 so I reorganised some code to make it so depending on the size of the chunk it would be more like 32,0,0 so that would be one to the right in the x direction which makes more sense. I also fixed the offset so depending of the direction a counter is increased so the terrain is offset properly.

gif
Iteration 3: Loading & unloading chunks work correctly in x axis

My third iteration is looking a lot better! I fixed the bounds of the heightmap so it was seamless again and I implemented unloading in one direction too where using an ordered map I simply erased the chunk and converted my array of VAOs into a map too so I could easily add and remove them as I please. You probably noticed in my first iteration that when I moved backwards it removed a chunk in front of me, I had the wrong variables set for erasing the chunk. Since then as you can see it works how it should.

Multidirectional Loading & Unloading Terrain

gif
Iteration 4: Loading multidirectional works but unloading isn't quite right

On my fourth iteration I implemented loading and unloading in the z direction, similarly to the x direction but when going diagonal I realised chunks were missing when moving in the x and z direction and that was because I didn’t add the chunk offset of the chunk the player was in meaning it was only removing chunks in the player’s original position which was 0,0,0. As you can see I fixed it but didn’t realise that I needed to do the same for unloading chunks.

gif
Iteration 5: Unloading now works correctly with offset & fixed heightmap bounds

Multithreading Chunk Generation

I have always loved getting the best performance out of my application, that’s why I love C++, my next big challenge was multithreading and after doing some research I came across std::async which seemed like the best option compared to std::threads as the standard library takes care of the thread pool. Now from what you can see is that it takes on average 900ms to generate all the terrain and create the meshes and in this instance, making it around a second to load all the chunks! So, when I launch my application it freezes for a second because the main thread is blocked until all the terrain generation is complete and then renders everything to screen.

image

Outcome: Takes 965.63ms to generate & setup buffers

After a while of researching I came across a really interesting video about approaching zero driver overhead I recommend watching it, If all goes well and I keep ahead of schedule I will be looking into ZDO and sharing my progress of it on here. Now I used a vector to store the std::future variable and when using std::async it returns a std::future which is very helpful since you can check if that job has finished. The bad thing about OpenGL is only one thread can use OpenGL related functions so the other threads can only generate the chunks, but I can into a problem when I iterated through the vector of std::futures and see if that job has been completed by a thread but sometimes just before finishing the terrain generation the job is marked complete and that data is trying to get accessed by the main thread to render and it throws an exceptions and they are the worst for debugging. You can see below it throws an exception.

gif
Outcome: Data being accessed while still in use causes exception

How’d I fix the problem? Well below is a snippet of the function that other threads use, I make sure to mutex so that when a thread creates the generation it locks the function so no other thread can access the chunkList cause two threads accessing a unordered map will get messy and throw exceptions everywhere, this just makes it thread safe and once the thread completes the tasks and goes out the scope std::lock_guard unlocks the mutex up to its deconstructor. Now as you can see before exiting the function a LoadList unordered map adds that chunk data to its container and then it is removed from the chunkList container. So my approach after hours of headache of figuring out the throw exception I can up with having a few containers such as: LoadList, SetupList, UnloadList, RenderList etc. so this ensures when the data is added to the LoadList all the data has been loaded and ready to be setup by OpenGL and… it worked great! As you can see below no more main thread being blocked the application loads instantly and the chunks load fast! Now there was one more issue which was I didn’t want the LoadList to be iterated over and over as it got bigger and bigger I only wanted to iterate over chunks that haven’t been set up yet so at the of the for loop I cleared the list, but this lead to chunks being missing because within the time of the container being iterated over and added to the SetupList more chunks have been added to the LoadList, so when it came to rendering the chunks I removed all the chunks that have been setup from the LoadList. Now look at the difference in the setup time compared to 900ms (almost a second) to 0.02ms, its incredible what multithreading can do!

png

Outcome: Setup function now takes 0.02ms to complete!
						
void ChunkManager::GenerateChunk(std::unordered_map>& chunkList, std::unordered_map>& LoadList, int x, int z)
{
	chunkList[glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk)] = std::make_shared();
	std::lock_guard lock(m_ChunksMutex);
	chunkList[glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk)]->SetupLandscape(x, z);
	chunkList[glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk)]->CreateMesh();
	LoadList[glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk)] = chunkList[glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk)];
	chunkList.erase(glm::ivec3(x * SizeOfChunk, 0, z * SizeOfChunk));
}
						
					
gif
Gif: Asynchronous chunk generation

Overview

This week has been a great week with getting the last two main features mostly into my voxel engine, with infinite terrain fully in I now have asynchronisation on start-up which is really fast! With just a few weeks left I just have a few goals left to achieve such as:

Goals
  • Asynchronously loading and unloading chunks as the player moves
  • Bugfixes – make std::futures list on the heap and stop all threads if the application is closed while chunks are still being loaded
  • Optimisation - frustum culling, culling unseen voxels between chunks, render distance
  • ImGui(Graphical interface) adding test framework such as changing the seed, allowing the user to customise terrain generation and showcasing features I've implemented with toggles