“Inquisitor, a dark substance is consuming the swamp. Find the source of the corruption, slay its spawn and burn it to the ground!”

My Contributions

Binary Serialization

For this project I was responsible for serializing files into a binary format. Not too long before this Zoe Thysell had held a presentation about serialization in front of my class and brought up some smart ways of keeping the serialization code simple such as serializing assets using the same function regardless if it is reading or writing.

To begin I first need a standardized way of allocating and writing/reading data to an arbitrarily large number of bytes. To do this I created the SerializerCursor class which allows the programmer to allocate bytes but it also allows the programmer to initialize the class with byte data for reading.

This class also allows you to traverse the bytes moving X amount of bytes forward each time you get the cursor which allows us to easily design recursive functions for serialization of types such as vectors.

After defining the SerializerCursor it wasn’t very difficult to build our BinarySerializer which allows us to easily serialize any type, granted that the type was either trivially copyable or had a defined serialization function.

Sometimes its important to be able to make a distinction between reading or writing for possibly unwanted side effects, such as initializing an asset twice because it was used to write a binary file. So I decided to expose the IsReading variable to the user.

When Serializing a type it calls one of two templated functions, either SerializeRead or SerializeWrite depending on if its reading or writing the type. In its simplest form its just a memcpy but it can be more complicated depending on the type. For this game I decided to add support for our most used built in types as well as some optimizations for when containers have trivial types within them.

This is an example of a complex type being serialized containing a vector of a complex type (Mesh::LODGroup).

And because all of the data within LODGroup was covered by the SerializeRead and SerializeWrite functions making a serialize function for LODGroup was as easy as writing this macro.

Threaded Asset Management

I was responsible for implementing the asynchronous loading of assets to the engine. Because most of our assets importing were in a single function as well as not thread-safe I had to not only make the processing thread-safe wherever I could, I also had to make sure that I locked the threads in the parts that couldn’t become thread-safe such as the FBX importing.

To start off I had to make a type of AssetHandle as our assets were just shared pointers to the actual asset, which would not be valid until the asset had finished loading. So I created the AsyncAsset templated class so that it could properly hold in some extra data so that the programmer that is requesting asynchronous assets can check if they’ve the asset has completed or better yet, register an event listener to call a function when the asset completed loading.

At first I wanted to make a handle class for all assets regardless if it was asynchronously loaded or not to force the programmers to write code that would be okay to load asynchronously however the use of shared pointers for assets were spread far and wide and changing it out would take way too long so the asynchronous pipeline had to stay as a separate pipeline.

ln order to keep things short I wont go into complete detail on how assets are handled. I will keep it as a general step by step overview of what each thread does when trying to fetch an asset.

First off syncs with other threads and checks if the asset is already fetched, because if its already loaded we can just fetch and return the shared pointer.

Secondly we sync with only the async fetching threads and check if another async asset exists for this path. If it does it just returns that asset as its already being loaded, if it doesn’t find one it puts one in the list of loading async assets and continues.

Thirdly it sends a job to the ThreadPool to load an asset as well as set the async assets handle to the returned assets shared pointer and calls its OnCompleted event.

Now this only works because all asset loaders need to be thread-safe and they are responsible for making sure that two fbx assets, for example, doesn’t try to load at the same time as the FBXImporter can only handle one call at a time.

Custom Shader Support

In order for our Technical Artists to do any work we needed to add support for custom pixel and vertex shaders in our materials. This was done by adding a field to the material json files where a vertex or pixel shaders path could be specified. This shader would then fetch a ShaderAsset using that file and compile the shader in runtime.

This greatly increased productivity not only for Technical Artists but also for us programmers because we could more easily debug faults in our rendering pipeline. Not having to restart the entire editor to recompile a shader improved QoL for all disciplines involved.

Worldspace Text

Worldspace Text turned out to be a very important and powerful tool for us during this project as it could be used to easily add feedback to the player. One big use of Worldspace Text was to specify the stat changes when leveling up as well as giving damage numbers to the player when they attacked enemies with different types of attacks.

The implementation was easier than expected as we already had screenspace text rendering. All that was needed was a modification to way text was rendered so that the vertex shader found the objects origin position in view space. Thats where I added the object-space position in xy along with a offset all multiplied with a scale which looked like the result above.

(WorldText_VS.hlsl)

Frustum Culling

I was responsible for implementing frustum culling for this project and I did so using LearnOpenGL’s guest article on Frustum Culling. This along with building up a frustum from a World-View-Projection matrix by Gribb and Hartman, made it really easy to figure out the frustum planes of any MVP matrix. This list of planes was then used to figure out whether or not an AABB intersects with the frustum.

The implementation of frustum culling reduced our draw calls in our most complex scene by a factor of 62x.

Before

After

The FPS counter is safe to ignore as the screenshots were taken in a debug build. But since we didn’t have any instanced rendering this was huge! This not only saved up time on the CPU dispatching the expensive draw calls (in debug) but it also made the GPU have to do less work which was a win, win for us.

The goal of most of our optimizations in Spite turned into trying to make the debug build at least playable to some degree as it helps the development process if we’re able to build unoptimized code with debug information without it lagging too much.

What the Duck?


Programmers

Filip Tripkovic

Måns Berggren

Liam Sjöholm

Herman Sjöholm

Erik Edfors

William Sigurdsson

Artists

Stephanie Madsen

Albin Mjörnstedt

Jasper Paavolainen

Emil Hagström

Animators

Oskar Lind

Jesper Walden

Lee Johansson

Technical Artists

Elina Kans

Erik Hausner

Jacob Falck

Level Designers

Kristian Sistig

Martin Trosell

Music

Einar Olsson