Category Archives: Effects

Improved shadows using dithering and temporal supersampling

To be able to implement volumetric lights I had to start with shadows for the sun light. As it wasn’t my primary focus I went for a straightforward shadow map implementation on a 2k texture. It’s easy and quick to code, but yeah, the results are ugly.

BasicShadowMaps

Yes, you can count pixels in the shadows.

 

I didn’t planned to implement a more advanced shadow map technique anytime soon, but I also didn’t want to stay with those ultra pixaleted shadows, so I tried to apply my two current favorite techniques: dithering and temporal supersampling :).

Let see what it can do !

First step, offset the shadow map bias by a value given by a Bayer matrix :


 

float shadow = 1.0f;

float ditherValue = ditherPattern[screenSpacePosition.x % 4][screenSpacePosition.y % 4];

if (shadowMapValue < worldByShadowCamera.z - bias * ditherValue)
shadow = 0.0f;

And then a bilateral blur. In my engine I already have a bilateral blur pass for the SSAO, with one free channel in the texture. So I just added the shadow value in the alpha channel of my SSAO texture and got the blur for free. It’s also convenient for the lighting stage as the tiled deferred shader can read both AO and sun light shadow in one tap.

Here is what it looks like now:

ShadowDither

It look way better, and it was really a few lines of codes ! Plus by merging it with the ambient occlusion it’s almost free !

But in some places the dither patern is really noticeable.DitherPattern

DitherPattern2

And in movement there is a lot of flickering.

It’s now time for some temporal supersampling !

Again in my case it was really easy as I already implemented this for the ambient occlusion. I just had to store a “previous frame shadows” texture and give it to my AO shader that already take care of rejecting bad previous pixels based on depth.

The flickering is almost totally gone ! But as you notice there is a lot of ghosting, especially when the sun light is moving. This is because  wrong pixels rejection is only based on their depth value, it’s good enough for AO, but not for shadows. When the sun is moving depth won’t change and yet the previous value shouldn’t be used.

So I added a pixel rejection based on the difference between the current and previous values. It means that if the value changed too much between two frames then something must be wrong. The hard part is to find the right function and the right values to be able to remove the obviously wrong datas but still keep enought informations to be efficient. It’s just my first experiments with this and I need to read some papers/presentations and do more tests to improve the result, and maybe write more about it. Here is my results so far.

It’s better ! A little of the flickering is back, but it’s still a great improvement compared to what I had at first.

But the issue with the dithering pattern being noticeable in some places isn’t resolved. To remove this I used two tricks. First I changed the dither offset used by a given pixel each frame. It would normally induce a lot of flickering, but the temporal supersampling smooth it and it become unnoticeable.

Then I randomly change the sun direction by a small amount. In not yet perfectly happy with this as it’s currently really noticeable even if it remove some artifact and had a soft shadow effect. Maybe I should use a predefined pattern instead of a completly random offset, in order to have a better temporal stability.

Temporal and jitter

 

That’s it for now ! There is still a lot of room for improvement, but  just using some quick tricks and existing resources in my engine I was able to improve the looks of my shadows, at a very limited cost and in a few hours. I really like the possibilities offered by the temporal supersampling, I will definitely use it more !

To conclude here is two screenshots of the shadows only, before and after.

ShadowsOnlyBefore

ShadowsOnlyAfter

VolumetricLights4

Volumetric lights

While waiting for a new computer that will make my experiments with voxels more comfortable (even a 64x64x64 grid is slow on my laptop) I decided to try some less expensive effects, starting with the volumetric lights as described in GPU Pro 5 by Nathan Vos from Guerilla Games.

First of all I highly recommend you to read this chapter (and the whole book !), there is a lot of useful tricks, and I only scratch the surface. There is also the excellent GDC talk “Taking Killzone Shadow Fall Image Quality into the Next Generation” by Michal Valient. You can download the slides on the Guerrilla Games publication page. It’s not just about volumetric lights, but the whole talk is really interesting (especially the part on the nice trick to achieve 60fps for multiplayer using temporal reprojection, I found it particularly awesome. But be careful, nowadays you can be sued for being awesome …).

A similar approch has been use in the upcoming game “Lords of the Fallen” and is described by Benjamin Glatzel for the conference Digital Dragon. You can see the slides here and the presentation here, it’s a really good source of informations.

I started with a directional light, the sun light in my case, that will affect the whole scene.
First of all I needed some kind of shadow map. I could have created one by ray marching in my voxel grid as I’ve done with point lights. As there is only one light it’s easy to store the distance from the occluder. But again my laptop GPU is really slow and it would have been annoying, so I started with a really basic shadow map implementation.

The idea is to “ray march” from the world position of the current pixel to the camera position, and for each step check if the current position can be seen by the light or not, using the informations from the shadow map. For each step the light scattered in the camera direction is accumulated, using the Henyey-Greenstein phase function.

It’s really simple, but as often with raymarching it can quickly become expensive as it require a certain amount of rays to capture details, and that’s why the ray marching is done in a downscaled texture.

The pseudo code for the raymarching looks like this:


// Mie scaterring approximated with Henyey-Greenstein phase function.
float ComputeScattering(float lightDotView)
{
float result = 1.0f - G_SCATTERING * G_SCATTERING;
result /= (4.0f * PI * pow(1.0f + G_SCATTERING * G_SCATTERING - (2.0f * G_SCATTERING) *      lightDotView, 1.5f));
return result;
}

float3 worldPos = getWorldPosition(input.TexCoord);
float3 startPosition = g_CameraPosition;

float3 rayVector = endRayPosition.xyz- startPosition;

float rayLength = length(rayVector);
float3 rayDirection = rayVector / rayLength;

float stepLength = rayLength / NB_STEPS;

float3 step = rayDirection * stepLength;

float3 currentPosition = startPosition;

float3 accumFog = 0.0f.xxx;

for (int i = 0; i < NB_STEPS; i++)
{
float4 worldInShadowCameraSpace = mul(float4(currentPosition, 1.0f), g_ShadowViewProjectionMatrix);
worldInShadowCameraSpace /= worldInShadowCameraSpace.w;

float shadowMapValue = shadowMap.Load(uint3(shadowmapTexCoord, 0)).r;

if (shadowMapValue > worldByShadowCamera.z)
{
accumFog += ComputeScattering(dot(rayDirection, sunDirection)).xxx * g_SunColor;

}
currentPosition += step;
}
accumFog /= NB_STEPS;

Here is the result of the raw ray marching with 100 steps:

Volumetric lights100 steps

It looks pretty good, but even with an halved resolution it’s still 7.6 ms on my Geforce 630m.

Let’s see what it looks like with a lot less, 10,  ray marching steps :

Volumetric lights 10 steps

Yeah, it’s pretty ugly.

But as with the ray marched shadows from my previous post, a bayer matrix will add some noise, and help to capture more details with less samples.


ditherPattern[4][4] = {{ 0.0f, 0.5f, 0.125f, 0.625f},
{ 0.75f, 0.22f, 0.875f, 0.375f},
{ 0.1875f, 0.6875f, 0.0625f, 0.5625},
{ 0.9375f, 0.4375f, 0.8125f, 0.3125}};

// Offset the start position.
startPosition += step * ditherValue;

Volumetric lights dither pattern

And the next step is a bilateral blur in order to have a smooth result.

Volumetric light Blur

I always find the effect of the noise + blur amazing for ray marching !

The main issue with downsampling is that when you apply the result to the full resolution scene all the edges are blurry and/or pixelated, as you can see in the screenshot. This is fixed in the last step, a bilateral upsampling.
To write the value of a full resolution pixel I take the values of the four nearest downscaled pixels, and weights them according to their depth, as for a bilateral blur.
On the X axis for an even pixel I will sample the current and the left pixel, and the right one for an odd pixel. The same apply for the Y axis.


 

float4 main(const PS_INPUT input) : SV_TARGET
{

float upSampledDepth = depth.Load(int3(screenCoordinates, 0)).x;

float3 color = 0.0f.xxx;
float totalWeight = 0.0f;

// Select the closest downscaled pixels.

int xOffset = screenCoordinates.x % 2 == 0 ? -1 : 1;
int yOffset = screenCoordinates.y % 2 == 0 ? -1 : 1;

int2 offsets[] = {int2(0, 0),
int2(0, yOffset),
int2(xOffset, 0),
int2(xOffset, yOffset)};

for (int i = 0; i < 4; i ++)
{

float3 downscaledColor = volumetricLightTexture.Load(int3(downscaledCoordinates + offsets[i], 0));

float downscaledDepth = depth.Load(int3(downscaledCoordinates, + offsets[i] 1));

float currentWeight = 1.0f;
currentWeight *= max(0.0f, 1.0f - (0.05f) * abs(downscaledDepth - upSampledDepth));

color += downscaledColor * currentWeight;
totalWeight += currentWeight;

}

float3 volumetricLight;
const float epsilon = 0.0001f;
volumetricLight.xyz = color/(totalWeight + epsilon);

return float4(volumetricLight.xyz, 1.0f);

}

 

And here is the final result :

 

Volumetric light final result

 

Some screenshots from other points of view, with 15 steps:

VolumetricLights3 VolumetricLights2 VolumetricLights VolumetricLights4

 

I’m pretty happy with the results so far. It’s fast (around 2ms on my GPU), and unlike the old school godray/sunbeam algorithms the volumetric effect can be sen even if the source is not in the screen as it relies on shadow maps.

For now, in my implementation the fog density is uniform. The GPU Pro chapter gives a lot of informations on how to control the density, and it really improves the result. My idea is to use the empty voxels of my grid to store density values. Then I should be able to update those values, with things like fog emitters, wind, collisions, etc. I tried with a 64x64x64 grid but the precision is not good enough, so I’ll try with a larger grid size.

For an alternative technique you can have a look at “Volumetric Fog: Unified compute shader based solution to atmospheric scattering”, used in Assassin’s Creed 4 and described by Bart Wronski at SIGGRAPH (slides are available here ). It’s more complicated, but it may be faster (especially for multiple lights) and using an intermediate 3D texture is really interesting as it allow to do more advanced calculations.

As I said at the beginning it’s just a start, and yet I find it already improves the sensation of depth of the scene. It reinforces the impact of lights as they no longer only impact the geometry. And with a low number of steps the cost it’s fast enough, so it’s really something I want to deepen !