Tuesday, November 21, 2017

A better depth buffer for raymarching

When doing any type of raymarching over a depth buffer, it is very easy to determine if there is no occluder – the depth in the buffer is farther away than the current point on the ray. However, when the depth in the buffer is closer you might be occluded or you might not, depending on a) the thickness of the occluder and b) if there are any other occluders behind the first one and their thickness. It seems most people assume a) is either infinite or a constant value and b) is ignored alltogether.

Since my new renderer is entirely based around screen space raymarching I wanted to improve on this to make it more accurate. This has been done before, but mostly in the context of order independent transparency (I think).

Let's look at a scene where the occluders are assumed to have infinite depth (I have tweaked the lighting for more distinct shadows to get a better look at raymarching artefacts, so the lighting does not exactly match the environment in these screenshot).

At a first glance it may look okay, but at certain angles, it is very evident that something is off:

Even an object that is visibly thin will receive a shadow as if infinitely thick. The go-to trick in this situation is to hardcode a thickness and tweak until it looks acceptable:

Still artefacts, but much better. However, for most scenes it's just not possible to find one single thickness that works for everything. What we ideally want is the actual object thickness per pixel. One relatively cheap way of approximating depth is to render a depth buffer for back faces. As long as objects don't overlap, are closed and reasonably convex, the difference between front face depth and back face depth is actually a pretty accurate representation of the object thickness.

I store front face and back face depth in different channels of the same texture, so I just retrieve RG instead of R for each pixel and compare the depth to both values when raymarching, making it really cheap. This removes a lot of artefacts, but there is still room for improvement.

It is hard to visualize in a still image, but with a moving camera it becomes very clear that shadows are only visible for the first layer of objects. As soon as an object disappears behind something, its shadow is also gone. This is of course particularly evident with long shadows from, say, a sunset.

Creating another layer of depth information is called depth peeling and there are several ways to do it. I use the stencil buffer, but it can also be done by discarding fragments in a shader. I already mentioned that I store front and back face depth in two different channels of the same texture, so why not add another layer of front and back face depth and make it a full, four channel texture? All four depth values (first front, first back, second front, second back) can still be fetched as a single texture read, making it really fast.

One could imagine doing even more depth layers, but the visual improvement would be hard to notice.

Thursday, November 9, 2017

Upscaling half resolution screen space effects

When working with diffuse lighting and ambient occlusion in screen space it is often very tempting to do computations in lower resolution. Most of it is blurry anyway, and for any kind of GI/path tracing, diffuse lighting is undoubtedly the bottleneck. Here is a test scene with all colours set to white and no textures.

Enabling only the diffuse lighting, the image looks strangely familiar.

You quickly realise that diffuse lighting is the lion's share of the entire image. Since everything is the same colour, two overlapping objects can be told apart only because they differ in diffuse lighting. Therefore, lowering the resolution of diffuse lighting also means that a lot of edges will be half resolution and the same diffuse lighting suddenly looks like this.

Not acceptable (click on image to view full resolution), but note that the image looks perfectly fine over larger areas where there are no edges, and also at the contours towards the skybox. I've come to think of two solutions to this problem:

1) Render at half resolution. Detect edges and re-render pixels near edges during upsampling. This would probably work very well, but I didn't try it yet.

2) A cheaper solution would be to cover up faulty pixels on the edges using neighbouring pixels from the same surface (it's all blurry, remember?), practically retouching the edges much the same way you retouch images in photoshop.

I decided to try the latter and got some interesting results. First I create a 2D "retouching" vector field. It is basically just a distance offset, telling each pixel where to fetch it's samples. In the middle of a surface this will be (0,0) and near an edge it will point away from the edge. If you have any way of classifying surfaces in a shader this is actually really cheap to do. I just use a unique number for each smoothing group to identify smooth surfaces and for each pixel, I check the eight neighboring pixels, average the offset of the ones that are in the same smoothing group. Ta-da, the average offset will now point in a direction away from each edge, and the retouch vector field looks something like this (here visualized upscaled and with absolute values):

Now if you process the downscaled, half resolution, diffuse lighting through this retouch field during upscaling, the resulting image will magically look like this:

Congratulations, you just saved ~75% processing time for your diffuse lighting. However, there are artifacts, as always. But I found the results to be acceptable in most situations. Computing diffuse lighting in half resolution (quarter pixel count) allowed me to do eight samples per pixel instead of two, resulting in more accurate lighting and less noise.

Another really nice property of the retouch vector field is that once you've created it, you can reuse the same field for any screen space upscaling you might do. I for instance reuse the same field when upscaling screen space reflections, and I'm hoping to use it also for smoke particles once I get there.

Sunday, October 15, 2017

Depth of field in VR

I have always been very fascinated by depth of field in computed graphics. For me, it often defines photo realism, mimicking the shortcomings of a real camera. Naturally it is a poor match for interactive applications because the computer doesn't know what the user is looking at, but I've tried to squeeze in depth of field in as many of the Mediocre games as I could get away with. In Smash Hit I wanted to use it for everything, but Henrik though it looked too weird and made it harder to aim (he was probably right), so we ended up only enabling only it in the near field. In Does not Commute and PinOut, which both have fixed camera angles I'm doing a wonderful trick, enabling a slight depth of field by seamlessly blurring the upper and lower parts of the screen. This is super cheap and takes away depth artefacts completely.

Anyway, since I'm so obsessed with depth of field I've started experimenting with it in VR. This has opened up a whole can of new problems and frustrations and I'd like to share some of my findings so far.

When I first tried it out with a fixed focal length it just looked weird and I couldn't really put my finger on why it was so different from a flat screen. Being able to focus on the blur itself gives an extremely artificial look. Some people say your eyes can't focus on different things in VR because the screen is always at the same distance from your eyes. This is only partly correct. You can still turn your eyes independently in VR. A lot of what is focus in real life is not related to the lens in the eye, but the angle of your eyes (vergence). This is what causes double vision behind or in front of what you're focusing on and is probably a far more important depth cue than the blur itself. This is already in VR "for free", it wasn't until I tried depth of field in VR that I understood why all VR experiences I've tried has been a bit "messy". Like a three dimensional clutter of too much information. When the depth of field is there and coincides with the double vision that is already there it gives a certain calmness to the image that is very pleasant.

Using depth of field as tool for directing the viewers attention towards a specific area is probably never going to work in VR. I'm still not sure why it works so extremely well in 2D but fails so miserably in VR, but it's probably because the viewer in VR can in fact "focus" on the out of focus areas by adjusting the angle of the eyes. However, depth of field could still add a lot to VR by removing the visual clutter, not really adding anything new but making the experience less painful.

In order to make an adaptive depth of field, one that adjusts the focal length dynamically, I've implemented a system that is pretty similar to what has been used in (D)SLR cameras for decades – a set of focus points that are all weighed together, making the central points more dominant. For each focus point I shape cast a small sphere from the camera and record the hit distance. The reason I'm using sphere shape casting instead of raycasting is that I want focus points to ignore minor gaps between geometry. It also gives smoother focus transitions when moving the camera.

The focus point system works relatively well, but it tends to ignore small objects in the foreground, not because the shape cast will miss them (it does not) but because very few of them hit, only making a minor contribution to the final focal length. To overcome this I introduced a minimum and maximum forced focus range, in which everything is in focus. We are now leaving physical territory, because with a real lens there is only a singular depth where objects are in focus. However, a forced focus range is definitely not any less accurate then having focus everywhere, so who cares. The forced focus range starts with the focus point-computed focal length. For any focus point closer than that I simply adjust the range to include that distance. This modification turned out pretty good. It tends to keep most objects in the center area of the screen in focus all the time, so to some extent it cancels out the whole point of adding depth of field, but having out of focus objects in the peripheral vision and behind the main objects in the center gives a much more pleasant looking image.

It does have some drawbacks of course. You can still focus on the blur, but it really only looks weird when it happens in the near field, which with the focus range modification can only happen if you look away from the center (and honestly, the lenses in todays HMDs are so bad that everything is blurry there anyway).

So is it worth the hassle? I'm not sure, but I think so. When it works it looks truly awesome, and when it fails it's definitely annoying. I'll keep experimenting with this one.

Finally, here is how I do the actual depth of field. Probably nothing new in there, but everyone does it a little differently. Here is my version:

1) Fill up the DOF (depth of field) buffer using the final composited image before bloom and tone mapping. Store in in an RGBA texture, where the alpha channel represents the amount of blur. I use a half size texture for this. To compute the blur amount, I'm using the formula k*(1/focalPoint-1/distance).

2) Blur the alpha channel of the DOF buffer horizontally and vertically. The size of the blur is based on alpha value. The blur must be depth aware, so that: A) Fragments further away than the current fragment do not contribute to the blur. This prevents objects behind something to blur what is in front, and B) Fragments that are closer do contribute, but only scaled by their alpha value. This will make blurry objects in front of something sharp bleed out over their physical extent.

3) Blur the RGB values of the DOF buffer horizontally and vertically based on the blurred alpha. This pass must also be depth aware in exactly the same way as B) above.

4) Mix the RGB values of the DOF buffer into the final image using the blurred alpha channel.

Wednesday, September 13, 2017

Adventures in Screen Space

Eight years ago, just when I first started writing this blog my second post was about screen space ambient occlusion. I used that renderer for all my physics experiments, leading up to the fluid simulation that became Sprinkle. At that point I left desktop computing in favor of mobile devices. Ten games later I'm now back to desktop machines and I'm completely blown away by all the computing power.

For the first Sprinkle game I had to make dedicated geometry with holes in it when drawing large alpha blended overlays because the fill rate was so terrible. Now I'm running hundreds of lines of code really doing complex computations per pixel. Sorry, you have probably already adjusted but this will take me a while. 

So what would be more fitting than to freshen up that old physics renderer (well more like starting from scratch, but still). I have been wanting to experiment with physics in VR for a while and now is the time. For this I need a renderer that can handle a truly dynamic world with no precomputed lighting.

I have implemented screen space ambient occlusion with temporal reprojection filtering that takes a lot of the noise away without smearing out the result. I've always hated shadow maps. They are hard to implement and the result is usually disappointing, so for this renderer I tried doing shadows entirely in screen space using ray marching towards the light source. It's a bit of an experiment, but I find the results really interesting. The characteristics are very different from regular shadow maps – instead of getting precise but jagged shadows this one gives imprecise and smooth, blurry shadows. I can't really decide if I like it or not. For a sunny outdoor setting, regular shadow maps are probably better, but for the more diffuse, indoor lighting this is quite promising.

There is also depth of field close to the camera done in four passes on half resolution and motion blur on everything. I'm going for a old, analogue look on the final result, so any imperfections that can tone down the artificial computer graphics characteristics is a good thing. The ambient occlusion and screen space shadows do add a little bit of noise, but there is one cheap and paradoxically efficient way of hiding unwanted noise: add more noise. So at the final stages of the pipeline I add 5-7% of greyscale noise which hides some of the noise in occluded areas and adds to the analogue look.

I have a bloom pass as well and I just started playing with tone mapping. I'm not sure I'm really getting it, but I'll keep experimenting. For anti-aliasing my friend Ludde Andersson over at Scaupa pointed me to a temporal reprojection method that I found very interesting. Since I'm already doing temporal repojection for the occlusion and shadows it was quite easy to do the same for anti-aliasing. The idea is to move the viewport at sub-pixel resolution every frame and smooth out the result with an accumulation buffer. It also turned out that one of me absolute favourite games Inside has a great presentation on the topic from last years GDC. The results are absolutely stunning. I'm not sure I have ever come a cross a new rendering technique that is so clever and simple yet produces so fantastic results with almost no computational overhead. Am I missing something or why aren't everybody using this?