Approximated contact shadows from bent normals

posted in ProjectW for project ProjectW
Published September 11, 2020
Advertisement

Hi everyone,

Today I want to talk about rendering. Last time, I implemented some random dungeons generation. Since the map can get a bit labyrinthic, I wanted to find a way to help the players orientate themselves. So I decided to add torches that auto-ignite when you get close to them;

But it means I can get a lot of point lights in the screen (around 50 easily because they have quite a big radius). And I wanted to have some shadows for them. I don't wanted to use projected shadows in a shadow map, because it would means rendering the scene 50 times (one for each light) and then blurring these 100 times (because we blur vertically then horizontally). Moreover, it would be a big waste of memory to keep all these shadow map, so it means I would have needed to start allocating/freeing my shadow maps when they enter/exit the screen. I don't wanted to do that.

Also, I wanted to add some ambient occlusion using screen space information (ie. SSAO). But I remembered this article where they do global illumination in screen space using bent normals. So I decided to try to implement contact shadows using the same technique. Here is the result, after a couple of hours of work (it will take some more time to tweak all parameters and get the best result, but it's already something):

(sorry for the low-quality gif)

And a comparaison with and without in a dungeon:

Note that I have some artifacts at the level of the bottom wall on the left

Concretely, how it works is that during the SSAO pass, in addition to computing the ambient occlusion by raymarching along random ray samples in screen space, I also compute the average of non-occluded rays (meaning the ray samples that hit nothing). This gives me the estimated least occluded direction, which we call bent normal (note that this means I need my ray samples to average to the normal of the fragment). I store this direction in rgb, and the alpha component contains the occlusion (i.e. the usual value computed in SSAO, that is the number of rays that hit something on the total number of rays). I do this in half resolution for performances:

Estimated least occluded direction (without the alpha)

Then, I upscale using a smart blur that only blur together fragments close enough in world position (I believe it's the usual thing that people do):

(note that I still need to tweak some parameters because I have some banding artifacts)

Then, I can use the alpha component (i.e. the ambient occlusion) to decrease the ambient lighting, as usual for SSAO:

/* compute ambientLighting using your favorite shading */
ambientLighting.rgb *= bentNormals.a;

For the direct lights, I mix between the NdotL (i.e. scalar product of the fragment normal and lighting direction) and the scalar product of the bent normal (i.e. rgb of the SSAO/bent normals pass) with the lighting direction, using the occlusion factor (i.e. the alpha of the SSAO/bent normals pass). I use the minimum between this new value and the old NdotL as new NdotL for computing my shading. In effect, it means that the side exposed toward the light is left unchanged, while the other side is not lighted (because the bent normal points in the direction away from the light).

vec3 lightDirection = lightPos.xyz - fragPos.xyz;
float NdotL         = max(dot(fragNormal.xyz, lightDirection), 0.0);

float BNDotL 	= max(dot(fragBentNormals.xyz, lightDirection), 0.0);
float ao 	= fragBentNormals.a;

ao = clamp(ao,0.9,1.0); //Whenever I have < .9 ao, I consider the part is shadowed to get bigger shadows
ao = (ao-.9)*10.0; 	//I renormalize to get ao in [0,1] instead of [.9, 1]

BNDotL 	= mix(BNDotL, NdotL, ao);
NdotL 	= min(NdotL, AOBNDotL);

/* compute lighting using your favorite shading, but with NdotL given above */

Of course, this technique is quite limited. It depends on the information in screen space, and thus come with the usual artefacts. Moreover, it only produces shadows that are close to the obstacle (since the bent normals are only computed locally). Thus, you don't get true projected shadows. So I realize it's not something that can be implemented for any type of game.

However, it runs in constant time, not depending on the amount of point light on the screen, and it is not really more costly than doing usual SSAO (except that maybe you need a bit more samples to get enough precision to compute good enough bent normals). And for me, it really gives the push I wanted from having a bit of dynamic shadows on screen. It's always so smooth to see your character casting a moving shadow when you pass close to a light. Also, it helps ground the other entities (like the barrels) in the environment.

In theory, it should also be possible to go in the same direction as in the article above-mentioned, and while computing the bent normal also remembering the position of the ray collision. Then, use this to produce local 1-rebound global illumination. However, last time I tried that, it was not very concluding (but probably because I did it poorly).

As a bonus, here are some more screenshots:

And the auto-ignite of the torches, but with shadows:

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement