Serpentine Estate


Features I worked on

  • Render Queues
  • Entity Component System
  • Shader Pipeline
  • JSON Reader
  • Static Models & Shadows
  • Live Compilation of Shaders

Introduction

The Game This is the latest project I worked on. It’s a game about exploring a mysterious old mansion, full of monsters. We took inspiration from resident evil and had a lot of similar mechanics. Because of this we soon realized that the game would heavily depend on atmosphere and lighting to set the mood and keep the player interested.

The game was created in our own engine, the same engine we used in Spite Sunborne. From seeing the limitations of our previous project I pinpointed what I needed to prioritize for this project. Which was optimization, shader pipeline, lighting and a new Entity Component System, since the system we used for the previous project did not quite fit our needs.

Showcase of the class component constructor.

Entity component system 🖥️

I started with the ECS since that was a bottleneck for the other programmers in my group. The goals we had for the system was that it should be easy to create new components, we did not want to register the component explicitly in any sort of register and we wanted to be able to refer to components by both type and ID.

We also wanted inheritance and functions, much like what Unity has for the basic functions of the component such as OnUpdate(), OnDestroy(), OnStart().

Showcase of the ESCRegistry code.

To meet the criterias I used preprocessor commands and macros. The only requirements are that when creating a new component, it has to inherit from Component and the header must contain the macro Component_Declare() and the .cpp Component_Define().

By defining the component like this it’s visible in the system and usable by type, string or a persistent, auto generated ID. The system ended up becoming quite robust and on top of that quite fast as well.  All of the components are contained by an ECSRegistry, owned by a scene, meaning that we can have different ECSRegistries for different scenes.

Indexed Strings 🗂️

The next optimization to tackle was our use of strings and string comparisons. I created a class I called IString which is an indexed string. When you first create an IString it will check against the collection in IStringHandler to get an ID and add to the reference counter. When comparing the IStrings to each other you can just treat them as unsigned ints and compare their ID.

Showcase of the string store code.
Showcase of the StringHandler() code.

JSON Reader 📝

I then also implemented a wrapper for RapidJSON to handle constantly changing components, so that everything trying to get data from a json-document would get a default value if the key doesn’t exist, instead of just crashing the program. This allows the programmer to quite recklessly import data from json-documents without handling data not being there. It’s built in a way so that even if there is no document the JsonReader would generate values and let you iterate through the “document” as if it exists.

Showcase of the JSON reader code.

Render States & Queues 🎬

A problem left from our previous project was that the way we handled models and rendering wasn’t very generalized. We had settings for models and prefabs regarding textures, blendstates, etc. But the settings were bound to the model and I felt like a better solution would be to implement materials.

Showcase of the class component constructor.

I started by simply moving functionality from our ModelInstances to the Materials.

Along with materials I implemented a RenderState class which basically contains the parameters for rendering. This proved to be very useful and laid the foundation for the render queues

Showcase of the RenderState code.
Showcase of the render queue.

With materials and renderstates in place I started implementing render queues.  The concept is quite simple and the performance on the GPU side benefits greatly from it.

You take all of the draw calls, add them to a sorted collection. The data you use to sort the draw calls are the renderstates.  You sort it so that the different data entries are as close to each other as possible,  so that draw calls with the same models will be executed together, skipping a context call per draw call. If you do this with textures and blend states, etc. You save a lot of context calls.

After fully integrating Render Queues into the rendering pipeline I could immediately see some benefits regarding performance, mostly on the GPU. Although we also added some overhead cost since we now always sort everything we want to render every frame.

To further optimize the rendering I added static render queues. They are essentially just normal render queues but we only sort the draw calls once and then copy the render queue to be drawn each frame. This way we get rid of the overhead for static models since we don’t need to sort them every frame. This is where I started seeing some big improvements to the framerate.

Shader Stages, Buffers and Textures 🖼️

The next step was to integrate a way to easily switch out shaders on materials so that materials can be used to their full potential. I also want to make it easier for our Technical Artists to add whichever texture they want to their shaders. The same goes for variables. Our last project we faced many challenges when it comes to passing variables from C++ to the shader. In the previous project our Technical Artists had to declare constant buffers in C++ themselves.

My solution was to make something along the lines of the previously mentioned Render State but for the shaders. A shader stage, a class which describes what’s needed for the upcoming draw call. However, the shader stage only handles things needed for the shader. It can define Vertex-, Pixel- and Geometry- Shaders and if none are defined the defaults for the active stage will be used.

Showcase of the shaderstage.

I made the system quite generalized and  every stage of the rendering pipeline has a default shader stage defined in the editor.

This is how the shader stage is presented and used on a material, in the editor.

Showcase of the UI texture tab.

This is how you make textures available to the shaders. You simply add a texture to the collection by browsing for a file  and the texture is then available in the shader, starting at slot 30 + a specified slot offset.

Showcase of the shaderstage buffer.

After implementing textures I wanted the constant buffer to be easily manipulated so I created a “Dynamic” ConstantBuffer. Dynamic in a sense where the buffer on the CPU side is fully dynamic and on the GPU side constant.

Showcase of the properties code.

All of the properties in the buffer are accessible by a string key and you’re able to set, add and remove properties. These are then available in the shader.

Showcase of the ShaderBuffer code.

When the textures and buffers are set in the editor you simply add the structure (with padding) to your shader and bind it to a predefined slot (6 in our case).

The same goes for the textures. You simply bind it to the slot specified in the editor and use it as you would normally. In the future this could easily be paired with visual scripting to generate the HLSL files.

Shadows 💡

Since we wanted the game to be very atmospheric, shadows for point- and spot- lights are really important.

Showcase of the shadows.

I started by implementing shadows for spotlights, which was easy enough. We simply render the scene from the perspective of the spotlight. The main difference from the normal rendering pass is that we only render to the depth buffer which then becomes the shadow-map.

The shadow map is then passed on the main render-pass and used to calculate what parts of the image to illuminate.

Another showcase of the shadows.

The next feature was shadows for pointlights. It’s similar to the shadows for spotlights but with six cameras, instead of one.

I implemented the point lights in a similar fashion. Six render passes with six different cameras to six unique shadow maps.

I got it working but quickly noticed that the impact on our performance was really bad.

To combat the bad performance I switched from six textures to a TextureCube. Using a geometry shader I was able to do all renderpasses in a single draw call.

Finale ⌛

The final optimization for the project was to limit the max amount of lights per pixel for the deferred render pass. The previous method we used was to map all of the lights to the GBuffer and iterate through all of them per pixel in screen-space. This was performant enough when we didn’t have too many light sources and a smaller scene. The result was that the performance was heavily dependent on how many light sources there are in the scene.

My solution was to bind light sources to objects before rendering the objects. All of the lights cull which objects they affect and map their ID to the object. The IDs are then passed to the GBuffer and stored in two textures (R8G8B8A8 UINT). When doing the deferred lighting we can then get a maximum of eight lights per pixel to sample from, which is a big deal compared to 32, which was the previous max.