screenshot
screenshot

Blightspire

Blightspire engine is a custom engine made in Vulkan, aimed to facilitate creating the wave-based arena shooter Blightspire inspired by games like Quake, CoD zombies and Ultrakill. This project was made as part of the Game Programming course at BUAS with a team of 8-11 programmers. designers and artists over the course of 8 months.

Our project is open source, so feel free to take a look at it on GitHub. Our game is also available for free on Steam!

role icon

Graphics Programmer
Tools Programmer

team icon

1 Producer/Designer
1 Artist
7 Graphics Programmers
2 Engine Programmers

time icon

8 months

platform icon

Windows
Steam Deck

platform icon

Custom Engine

platform icon

Vulkan
ENTT
Jolt Physics
Wren Scripting

Main responsibilities

Throughout the past half year of development, I've been solely responsible for the GPU based particle system and its editor. It was inspired by Wicked Engine's particle system and is currently functional, but still in progress.
I also implemented screen space decals into our deferred renderer to further enhance our game feel.

Aside from working on graphical and engine features, I also picked up and helped with other tasks in various fields, like production, design and art, to help fill in for the lack of people specializing in those fields in our team. These tasks included organizing and conducting playtests, giving internal feedback on level and gameplay design and helping with some UI art and design.

Particles

One of the biggest struggles I faced when implementing the GPU based particles was that I had to learn Vulkan, a modern graphics API. Before this project I only had some experience with OpenGL and the PS5 graphics API. By far the hardest things to set up for the particles were the buffers and all the descriptor sets and layouts. I eventually managed to set-up everything I needed for the particle system by reverse-engineering existing graphics passes in our engine while reading up on Vulkan/modern graphics API concepts in several tutorials (mostly at vulkan-tutorial.com").

Another hurdle I encountered about 3 months in, was that I lost the original vision for our particles both visual wise, customizability wise and behavior wise. This was also due to being in my own bubble for those 3 months being the sole person working only on the particle system. To take a step back and get a better view of the state of the system and what was still needed, I sat down together with another team member to try and create various particle effects (e.g. environmental fire/dust and particles when shooting the gun). This helped greatly with identifying missing functionality and/or customizability for the particles.

The GPU based particle system is utilizing 3 compute passes and a rendering pass separate from our uber render pass to emit, simulate and render the particles. This is heavily inspired by Wicked Engine's particle system, adjusted to fit to our game's needs.

The system uses several storage buffers to keep track of the amount of particles, indices to particles that are alive and dead and the particle's data. A uniform buffer is used to copy over emitter data to the emit compute stage, where it then spawns new particles by writing these into the related storage buffers. There is another storage buffer where all particles instances to be rendered are written to that is used by the DrawIndexedIndirect render pass.

The particle behavior is fairly simplistic as of now with movement using velocity calculated with the particle's assigned mass, starting velocity and some randomness. Particle size and rotation can also be simulated. The particle rendering only supports billboard particles with sprite sheets (if desired) to aid our more stylized look.

C++ code snippet of dispatching the emitter compute stage from particle_pass.cpp

The logic for spawning emitters is still handled on the CPU using the ECS. Emitters are components and can be spawned on an entity, with parameters from an EmitterPreset, and take the velocity/position of other components of that entity, including from a Jolt rigid body. The SpawnEmitter function used to spawn emitters with is also already integrated into our scripting using wren.

The EmitterPresets are also editable in engine. This supports live EmitterPreset editing being reflected on spawned emitters and serialization of EmitterPresets. This editor is functional as of now, but still being iterated and improved upon with feedback received from my team. Some particle effects made using this editor can be seen in videos above and on the left.

As of now 121.000 particles take about 3.396ms on PC. The only major bottleneck I've recognized so far is the rendering of the particles, in this case taking about 3.148ms. Optimizing this is still on our backlog, but since it doesn't impact us as of yet and there's bigger bottlenecks to be handled, it hasn't been prioritized.

C++ code snippet for caching/loading images for emitter presets from particle_module.cpp

Screen Space Decals

To help enhance our game feel, I implemented screen space decals, currently in our lighting pass in our deferred renderer. For my implementation I followed Warhammer 40k: Space Marine's screen space decals. Since they're screen space, they can only be spawned on static meshes and in our game are used dynamically, with the decal buffer being a ring buffer wrapping around once the spawned decals hit the max amount.

Per pixel per decal, it transforms the pixel's position to decal 'box' space and checks if the pixel is within its 'box'. If it is, it will check the pixel's normal against the decal's orientation to avoid artifacts called side stretching. If they're pointing to a similar enough direction, decided by an adjustable threshold value, the albedo is fetched from the decal image to be mixed with the pixel's albedo. Since so far our models don't use (noticable) normal maps and our visuals are rather stylized, it saved us on an extra calculation for offsetting the pixel's normal with the decal's normal map.

Currently, the performance is less than ideal since I'm calculating for every decal at every pixel. Optimizations are on the schedule for this, most likely in the form of a separate compute stage and/or utilizing the clusters currently in place for our clustered lighting.

C++ decal spawning code snippet from gpu_scene.cpp
Screen space decals code snippet from lighting.frag