Render Pass Abstraction

I noticed that one of my cpp files was getting rather large and narrowed it down to the applications run function. I use this function as a giant main function, where there is a bunch of variable declerations and setup done before entering the applications primary while(running) loop. This file contains most of the render pipeline setup like textures, framebuffers and render buffers plus all the render execution code inside the while(running) loop. I actually do not mind working with big files, but it was getting harder to conditionally execute certain passes like Bloom or Ambient Occlusion and/or extend the pipeline.

I will preface this by saying that I have done little research towards modern solutions to this particular problem, and that this is solely what I came up with and what works for me. Most software solutions come down to figuring out what works for the specific problem you are encountering, and common software patterns are often not directly applicable.

I have done some research towards implementing a frame graph data structure, which is nicely explained in this Game Developers Conference (GDC) presentation by Yuriy O’Donnell. After a weekend of attempting to implement this pattern I realised that the benefits of this data structure are features I have no need for like memory aliasing and transient resources, which are a concern for modern rendering API's, not OpenGL. What I personally wanted out of the pattern is the ability to compile and reorder my render pipeline to make it easier to enable debug passes and enable/disable graphics features like Bloom. I also noticed that if I were to use a frame graph structure the way Frostbite does it and use lambdas - for a more declarative way of of writing the rendering code -  I would end up with more code than I started with.

I ended up reverting the frame graph changes and start over. The only thing I really wanted out of the implementation was to move all the rendering code to a different file, have a class per render pass and the ability to tie one passes' input/output to another. Here is an example of what I came up with, specifically for a deferred Gbuffer pass:

class GeometryBuffer {
public:
    GeometryBuffer(Viewport& viewport);
    void execute(ECS::Scene& sceneViewport& viewport);
    void resize(Viewport& viewport);


private:
    glShader shader;
    glFramebuffer GBuffer;
    glRenderbuffer GDepthBuffer;
  
public:
    glTexture2D albedoTexture, normalTexture, positionTexture;
};


The constructor takes the application's viewport to initialize the render resources to the correct size. execute takes the active scene to loop over all entities and render them using the passes' shader. Execute also needs the viewport for its view and projection matrix.
resize simply re-inits all the render resources to the viewport size, it is only called if the viewport changed. Result resources are public, as is common throughout Raekor. This makes it easy to pass the result texture to other passes, or output it directly to the screen for debugging.

I considered making an abstract RenderPass class with virtual execute and resize functions, but most passes have their own execute function signature.  I also like how the rendering code is still rather declarative because of this:

// generate sun shadow map 
 glViewport(00, SHADOW_WIDTH, SHADOW_HEIGHT);
 shadowMapPass->execute(newScene, scene.sunCamera);


 // generate point light shadow map 
 omniShadowMapPass->execute(newScene, scene.pointLight.position);


 // generate a geometry buffer
 glViewport(00viewport.size.xviewport.size.y);
 geometryBufferPass->execute(newScene, viewport);


 if (doSSAO) {
  ambientOcclusionPass->execute(viewport, geometryBufferPass.get(), Quad.get());
 }
 

Comments

Popular posts from this blog

Ray Tracing in One Weekend - OpenGL Compute

Shadows

Screen Space Ambient Occlusion