This winter I had to implement simple versions of both SSR and SSAO post-effects in an OpenGL scene as an evaluation for a graphics rendering class. I decided to use the opportunity to have some fun with it in the few weeks we had and added other tasks on top of that. For those interested in more details than this post will cover -and french-speaking- here's my project report, and here is the source code.
I started by creating a simple game engine API on top of OpenGL rendering code, following Unity's Entity/Component structure with a scene hierarchy for ease of use when setting up scenes with multiple objects, and scripting behaviors like controllers for the camera for example. Then I went into OpenGL plumber mode to set up a deferred rendering pipeline. I'm much more accustomed to DirectX's API so that took a bit of time. Finally I could start having fun with shaders, and I started by adding in physically based rendering, helped by this very good resource: JoshuaSenouf/GLEngine. I also used the excellent FreePBR resource for PBR materials.
I went on to implement Screen-Space Ambient Occlusion (without blurring or downsampling so very unoptimized as it requires too many samples, I was more interested in SSR) and Screen-Space Reflections. For the latter I used this excellent resource from Morgan McGuire on how to ray-march a 3D scene in pixel space, so as to not waste ray-marching steps on the same pixel multiple times : Casual Effects SSR.
The interesting part was being able to feed the reflection hit found by the SSR to the PBR lighting functions as a specular IBL source, instead of only blending the original color with it somehow. This contributed a lot to the physically-based look of the result. Due to the SSR being computed before the final surface color itself, I had to store and use the last frame's final color buffer and reproject it to sample the SSR hit from that, so that the reflections would account for the full lighting results too and not only the albedo color (as they can vary wildly with PBR metallic surfaces). The pictured scene with SSR and SSAO applied at 1280*720 takes 11ms to render (some of which is due to the unoptimized ambient occlusion) :
I used a couple of methods to optimize the SSR algorithm in addition to doing the ray-marching pixel-by-pixel. The first is to increase the size of the ray-marching steps in pixel. The following shows the effect of that for a step size of 1, 8 and 32 pixels :
Obviously this is not usable as-is, but two tricks can correct that. The first is binary-search steps. When the ray goes beyond the z-buffer after a step, the point at which the collision is detected along the ray path can be refined by going back a step and moving forward again with a half of the previous step size. This process can be iterated for any amount of binary-search steps, but few steps can be required. The other trick is to dither the visible line artifacts by adding a random offset to the starting point of the SSR ray in pixel space. The next figure shows a reflection with a step size of 8 pixels without any correction, with 4 steps of binary search, and finally with the dithering :
This allows for a lot more performance with the SSR effect while maintaining visually acceptable results. Even with a step size of 32 pixels, the result would be valid with a bit of blurring applied :
Lastly I looked into having rougher reflections. The issue with SSR is it's only valid for 100% specular reflections, which would only happen with pure mirror surfaces. There are a few tricks one can use to simulate rough/diffuse reflections outside of the correct (brute-force) method of casting more rays and integrating them. I wanted to see the result of simply sampling the color buffer after a SSR hit at a lower level of detail depending on the roughness (implying that mip maps are generated at each frame for the stored color buffer). Here the roughness of objects in the scene is increased :
As we can see, the trick somewhat works inside the boundaries of the reflections, although the bilinear sampling pattern is visible. The edges of the reflections are still completly sharp though, as a consequence of casting a single ray with a binary collision check. A better method would be to render the SSR reflections as usual into a temporary buffer which would then be adaptively blurred depending on the roughness of surfaces.
I did personnally want to quickly try the correct brute-force approach to rough reflections out of curiosity. I simply modified the shader so that the SSR hit is done iteratively for a set amount of samples, each of them with a direction randomly offset from the central reflection direction depending on the roughness amount (in a naive, non-physically correct manner). The final reflection color is the average of all samples. Here's the result of this while modulating the roughness of objects in the scene, for both 16 samples per pixel :
And 128 samples per pixel :
It looks of course really good, but is obviously not suitable for real-time rendering. With 16 samples, the result is too noisy and already increases the render time to 150ms, while 128 samples take that up to more than a second.