top of page

Introduction

For a while now whenever I have worked, adding a new rendering effect like Volumetric Lighting or a post process effect like Bloom implementing it to the rendering pipeline has always been a tedious process and takes up alot of unecessary time. So, I finally decided to completely rewrite my rendering pipeline and make it as automatic as possible.
 

Automation of my engines rendering pipeline:

My Engine can be found on my Github.

OBS! This deep dive assumes that you have a somewhat of an understanding of how game engines & DirectX12 works.
 

The system is divided into 3 parts.

First is FullscreenTexture which holds the resource for the Render Target and Shader Resource View(SRV).

Second is TextureRenderer that holds the PipelineStateObject and RootSignature as well as the DescriptorHeap offset indexes for the use of assigning Render Targets, Constant Buffer Views(CBV), Depth and SRV's to the correct slot. 

Last we got TextureRendererHandler which is what brings everything together where we insert FullscreenTexture and TextureRenderer into a specified "Render Pass" where all the data on the objects is handled automatically. 

How it works:

Texture Rendering Handler

First lets start out with how this pipeline is structured. The rendering pipeline is divided into multiple parts which I decided to call Render Pass.

The Render Pass:

image.png

Every FullscreenTexture and FullscreenRenderer when created directly or inserted into the pipeline needs to be specified where in the pipeline it will fit in. For FullscreenTexture if I insert it into "PRE_SCENE_PASS_2" it means the this texture will transition from a Render Target to a Shader Resource View to be used as a texture in rendering after that certain point. So for example if a TextureRenderer in "PRE_SCENE_PASS_1" wants to use this specific FullscreenTexture as an SRV it wont work as that resource is set as a Render Target in memory and vice versa if it tries to access a SRV as an Render Target. For TextureRenderer it's just to specify where it should be rendered so that you can plan out where things needs to be inserted so that you have access to the correct data at that specific Render Pass. 
 

image.png

After the end of everyframe all the resources will be set as SRV's meaning that they can't be used as render targets and the data that's been rendered on them needs to be cleared. Here I queue all the resources to my QueueResourceTransition and transition them all at once to render targets and Immediatly clear each of their Render Targets.

Resource Handling and Automation:

After the transition, clearing and rendering of GBuffer has finished we can now start rendering each pass. m_textureLists holds all the FullscreenTextures while m_renderList holds all the TextureRenderers and for each pass I fetch a FullscreenRenderer and apply the needed data used for rendering(I will go into to detail on how this works in the FullscreenRenderer section). After all FullscreenRenderers in a Render Pass are finished I transition the FullscreenTextures that were used as Render Targets to SRV so that they can be used as textures in the other Render Passes.

Fullscreen Texture

FullscreenTextures holds the resource with each of it's respective DescriptorHeap offset index (Render Target, SRV and Depth). These indices are used to fetch the specified memory location of the resource on the CPU(Render Targets and Depth Stencils) and GPU (SRV's) and set on either a Render Target or texture slot. 

image.png

There are two different types of FullscreenTextures which is if the texture should be rendered and stored as Depth or not. Depth CPU memory is accessed from an different DescriptorHeap compared to Render Target so I thought it was best to specify if you're creating a regular texture or Depth texture.

image.png

Texture Renderer

Creating TextureRenderer

TextureRenderer holds the PipelineStateObjects and RootSignatures as well as Render Target, CBV and SRV DecriptorHeap offset indicies for every slot. It is also a virtual class so you can override the default rendering function if you ever need to. 

For the initialization of the TextureRenderer it requires a lot of data to set it up with the creation of the RootSignatures and PipelineStateObjects handled by a wrapper class that I made which stores this data and is fetchable with an ID. Further down we also set up our Render Target, CBV and SRV slots for use. 

image.png

How you assign slots is through a function (Render Targets and CBV's look and behave the same) that either takes a object or DescriptorHeap offset index and sets it at specified slot. 

image.png

Assign and Render To Texture:

When it's timet to render I get their respective DescriptorHeap GPU handler with the use of the offset index and assign it for use at it's given slot.  

image.png

The Render Targets are also then set with the default rendering option being a Fullscreen Pass and with the example of my Shadow Mapping to show the different use cases of overriding the RenderToTexture function.

Default:

image.png

Shadow Map:

image.png

Summary

Everything coming together:

And now when we put everything together, creating and adding a new FullscreenTexture & TextureRenderer to the pipeline is nice and easy with the example of my Volumetric Lighting below. 

image.png

Last but not least is to render all the Render Passes and draw to the back buffer for presentation to the swap chain.

image.png

My thoughts

I am very happy how this system turned out, making new shaders and adding them to the rendering pipeline is so much easier while also being extremely fast. Before it could take me multiple hours depending on the effect but now it's only a few minutes (though this of course depends on the complexity of a shader). I will be continue on improving this system with more features and customization.  

bottom of page