Render Pipelines desing in C++

For several months I've been thinking on a composable way of constructing render pipelines for a given 3D rendering engine.

Objects

The objects we are going to use and describe are: Cameras, Pipelines and Passes.

A Camera is like any other camera on a rendering engine. This object will have the normal data associated with it (matrices, position, etc). Aside from the common data, it is going to have a reference of one Render Pipeline.

This way we can describe that the Camera will have a relation of 1:1 with a Render Pipeline.

Something like this:

class Camera {
    public:
        explicit Camera(const Pipeline&);
        // ...
    private:
        // ...
        std::shared_ptr<Pipeline> pipeline;
};

A Pipeline will have a collection of interconnected Passes that will describe the flow of the rendering process's execution. This will describe a relation between Pipelines and Passes to be 1:N.

This Pipeline will expose an API to add new Passes and assign order of execution to them. This way, when we call the function Pipeline::render(...) all the Passes will be executed.

Something like this:

class Pipeline {
    public:
        void render(...);
        // ...
    private:
        // ...
        unsigned int executionOrder = 0;
        std::vector<std::shared_ptr<Pass>> passes;
};

A Pass is an object that from a given list of meshes and materials (plus some other data) will decide which rendering operations will be executed. Normally that list of meshes will be the meshes inside the frustum of the Camera.

Also, a Pass will include some support solution to hold the rendering output (like a Render Target) and will make that data available to other Passes of the Pipeline.

Passes can receive as inputs:

  • The list of meshes and materials to render.
  • The previous Pass outputs in the form of textures in memory (Render Targets)

And these are some concepts Passes can achieve (or output):

  • Render everything to screen.
  • Render everything to a Texture.
  • Render only a subset of objects (Opaque, Transparent, ...)
  • Render modified pixels from a given texture.
  • Render 2 or more textures composed into one.
  • etc.

The Pass class can derive into many different and specialized types of Passes, with this in mind we can have something like:

class Pass {
    public:
        explicit Pass(...);

        void render(...);
        // ...
    private:
        // ...
};

And a derived Pass that will render to texture:

class PassToTexture : public Pass {
    public :
        explicit PassToTexture(...);
        // ...

    private:
        RenderTarget target;
        // ...
};

These three ideas, Cameras, Pipelines and Passes can be composed together to construct all sort of rendering processes that can achieve almost anything on a rendering engine.

Structural Example

Here we can see an example of how the internal structure would look like:

+ Camera1       + Camera2        + Camera3
  + Pipeline1     + Pipeline2      + Pipeline3
    + PassA         + PassA          + PassA
                    + PassB (A)      + PassB (A)
                    + PassC (B, A)   + PassF (B, A)
                    + PassD (C)

Above we can see the structure of those Pipelines with their own inner Passes. We can see how the Camera1 has a very simple Pipeline using only 1 Pass.

On the Camera2 and Camera3 we can see how the Pipelines use more Passes, and how those Passes use previous Pass results to achieve the proper render process.

As far as I know, I found these Pros and Cons to this design.

Pros

  • Derivation/Inheritance can give us a great flexibility to achieve almost any rendering effect we want to make.
  • It allows the re-usability of Passes across many Pipelines.
  • Pipelines can be swapped instantly on any given Camera.
  • The complexity of a Pipeline is only there if we need to.
  • Several types of Pipelines and Passes can be pre-assembled and provided to customers without constraining customers of creating their own.
  • Because the high-frequency loop is going to be located inside the Pass::render(...) function, inheritance dereference pointers should not affect the performance of the code.

Cons

  • If Pipelines have black box API, it can be tricky to not repeat API functions among Pipelines. This is because several Pipelines can have same types of Passes inside and all of them can expose the same setPropertyABC(...).