My graphics engine is designed around the concept of pipes. Each pipe is an object responsible of rendering a subset of the scene graph with the same type of "processing". You can think of a pipe as a pass, although it's a bit more subtle than that.
Pipes are linked together as a tree. Each pipe has one parent (except for the root pipe), and can have many children. Each pipe has two main functions: setup and render.
The setup function is responsible of two things: preparing the rendering by calculating on the CPU the set of objects that will have to be rendered (and with which shaders); and potentially, rendering something to a texture. In that last case, the setup function is allowed to call the render function of its child pipes. The setup function is taking in argument a camera.
The render function is responsible of rendering the objects calculated in the previous setup phase.
In addition to this, each pipe has a name. The names are propagated in the child pipes (appended together), and then propagated to the shader system once rendering takes place. A shader can use conditionals to apply itself, based on the type of pipes that are being used.
The objects in the scene graph have many shaders applied to them. When rendering objects of a pipe, i use the shader whose name matches with the pipe's name.
Now, an example, and all will become clear:
The Scene Pipe is a pipe that, in its setup phase, performs culling with the given camera and stores all the visible objects in an internal array. It does not perform any rendering.
The Standard Pipe does not perform any setup operation, but when rendering, it uses the objects calculated in the scene pipe. It renders these objects with their "Standard" shader, and if an object doesn't have any "Standard" shader, it is skipped.
Let's imagine for a second that the Standard shader is only applied to opaque objects, and that a shader called "Transparency" is applied to transparent objects, at scene initialization time.
The pipes system can easily be expanded to this:
And as you see, the culling performed by the scene pipe can be reused by the transparency pipe, in a matter of seconds. The transparency pipe will only render objects with their "Transparency" shader. Note that, as i said in the beginning, an object can have many shaders - so theorically, you could have one object which has both "Standard" and "Transparency" shaders (even if in that scenario it'd be a bit incoherent).
But the best is coming. Now, imagine that you want to render reflections in the water. To do that, you need a pipe that will render the reflected scene into a water texture. That's quite simple. In the setup phase:
- call the setup phase of the children, but with the reflected camera
- enable render to reflected texture, and call the render phase of the children.
And ignore the render phase of the reflected scene.
The reflected texture can then be assigned to objects of the scene graph (like the ocean mesh), and rendered normally by the second standard pipe.
Implementing HDRI ? This is how i did it:
The HDRI pipe enables render-to-texture (with a floating point format) in the setup phase. The rest of the pipeline is rendered into the HDRI texture, but with a tweak: the sub-pipeline shaders have the name "HDRI" appended to them. So, in the Standard pipe, the shader called "Standard_HDRI" will be applied, while in the Transparency pipe, the shader called "Transparency_HDRI" will be applied.
In the render phase of the HDRI pipe, a bloom filter and a tone-mapping operator can be used on the HDRI texture, and applied to a full-screen quad.
Another advantage of my pipeline system in addition to its flexibility, is that it can be built at run-time (or even dynamically updated). Which means, if your video card does not support HDRI, the previous pipeline will be used instead.. and so on.
Long and technical article, but i hope you enjoyed reading it :)