Skip to main content
GameDev.net gamedev.net
🔒 Locked

distance field shadow maps

Started by ArKano22 May 13, 2010 at 1:19 PM 18 replies 12.8k views
Original Post
ArKano22
ArKano22
I´m developing a project for which i need very very hard shadows, like stencil-volumes hard, but using shadowmaps. I´ve been toying around with the idea of generating 2d distance fields from shadowmaps and using them to perform the shadow test. Right now, this is my method: 4 averaged custom bilinear filtering taps on 4 different shadow test results to generate a pseudo-distance field. The output looks a lot like 3x3 bilinear filtered pcf. Then i alpha test it (>=0.5?) to obtain the final shadow. The image shows a 512x512 shadowmap. The results are ok for my needs, although since it is not a true distance field, small "wavy" artifacts can be seen on the edges of the shadow (highlighted in red). It naturally eliminates all self-occlusion artifacts due to the alpha-testing, though. But of course, the shadow jitters when moving, because of the low shadowmap resolution, but that can´t be helped. Here´s where i need a bit of help to get it 100% right: Does anyone know a good way (...or any way :( ) of generating a true distance field from a relatively low res bilinearly filtered input, so that the wavy artifacts disappear? Has anyone tried this before and managed to get it working properly? I was thinking of a method that involved using a variation of marching squares on the shadow map instead of bilinear filtering, then computing distance to the resulting segments. But it will look too "segmented" and probably be very slow too. Bilinearly filtering the shadow test twice might give good results too, but it is also an ugly hack. Some ideas?
PolyVox
PolyVox
Intersting stuff!
Quote:
Original post by ArKano22Bilinearly filtering the shadow test twice might give good results too, but it is also an ugly hack. Some ideas?

How about cubic filtering? There is an approach decribed in GPU Gems 2 (available free here) which implements cubic filtering in terms of linear filtering. I recall reading on the NVidia forums that it runs at about half the speed.
ArKano22
ArKano22
Quote:
Original post by PolyVox
How about cubic filtering? There is an approach decribed in GPU Gems 2 (available free here) which implements cubic filtering in terms of linear filtering. I recall reading on the NVidia forums that it runs at about half the speed.


Wow, there´s some nice filtering going on there. I´m going to try it because the ouput seems much more usable that my current one. thank you!!
JPulham
JPulham
damn... I had an idea like this after reading how valve does decals in team fortress. Every good idea I have is taken :P
Looks good, I'll be interested to follow your progress.
ArKano22
ArKano22
Don´t worry, it is not a really good idea :P. The problem is that valve generates the distance field from a high-res image. Here, i´m stuck with a low-res shadow map. If i compute a distance filed as usual, the jaggies show up in the reconstruction too.

I´ve tried with a different distance field computation method where i store an average distance instead of the minimum distance, but it has problems with capturing fine details... the shadow looks like a blob, all round edges. I´ve also tried bicubic filtering but the results were similar to x1 bilinear filter.

At the end, the x4 bilinear filter method i was using before gave the best results, so i´ll stick to it. I´ll post the glsl code later if anyone is interested.
Tessellator
Tessellator
They use this technique for baked shadow maps in the Unreal engine. In that instance, they can generate a huge shadowmap to get a really nice distance field before downsizing (and storing in a DXT texture IIRC). This graphicrants blog post talks a little bit about it: http://graphicrants.blogspot.com/2009/11/udk.html

T
ArKano22
ArKano22
Quote:
Original post by Tessellator
They use this technique for baked shadow maps in the Unreal engine. In that instance, they can generate a huge shadowmap to get a really nice distance field before downsizing (and storing in a DXT texture IIRC). This graphicrants blog post talks a little bit about it: http://graphicrants.blogspot.com/2009/11/udk.html

T



That´s the best use-case possible for this. If you can precompute the field from a high-res source, the results are almost flawless :). Did´t know about its use in the unreal engine, thanks for the link!
ArKano22
ArKano22


I finally managed to remove all artifacts using the mean-distance field mentioned before. The image shows a 256x256 shadowmap, with and without distance field.

It has the same "antialiasing" proposed in valve´s decal paper, which softens the edge a bit. Speed wise is slower (like x3 slower) than standard shadowmapping, but it pays off with quality.

Time to choose a name for this... dfsm sounds good enough :). Code in the next post.
Ashaman73
Ashaman73
Hey, the results looks amazing.

I've implemented valves algorithm too (for gui icons ;-) ). When I understand your approach, you will not perform valves algorithm directly on the shadowmap, right ? (this would be like blurring a shadowmap which results in shadow artifacts). You need to process the already projected shadow to get 0/1 values (like the UDK lightmaps). In your case this would be the screen or an upscaled buffer containing the screen projected shadow. It's like screenspace shadow blurring.

You can archive similar results as the valve algorithm by just blurring the image (I've to look up the link,sry), it might be simpler or faster then generating a distance field (blur is separable).

ArKano22
ArKano22
Quote:
Original post by Ashaman73
Hey, the results looks amazing.

I've implemented valves algorithm too (for gui icons ;-) ). When I understand your approach, you will not perform valves algorithm directly on the shadowmap, right ? (this would be like blurring a shadowmap which results in shadow artifacts). You need to process the already projected shadow to get 0/1 values (like the UDK lightmaps). In your case this would be the screen or an upscaled buffer containing the screen projected shadow. It's like screenspace shadow blurring.

You can archive similar results as the valve algorithm by just blurring the image (I've to look up the link,sry), it might be simpler or faster then generating a distance field (blur is separable).


The method is as follows: I´m performing a 3x3 filter 4 times over the shadowmap, which computes an average of distances from the kernel´s center to the shadowed texels of the shadowmap (this gives better results than blurring for several reasons...you are storing distance to a smoothed shadow boundary, not coverage percentage). Then i bilinearly filter the 4 results and perform the alpha testing. I don´t do it in screen space because it would probably jitter too much when moving the camera. However it would be cool to separate the 3x3 filter and the bilinear filter.

I tried blurring but the results were pretty bad, jagged edges all over the place. It might be preferable when speed is more important, though.

[Edited by - ArKano22 on May 15, 2010 4:24:41 AM]
Hodgman
Hodgman
This is really cool =D

At what stage in the rendering do you do this "dfsm" filter?
e.g. do you do 3x3x4 SM samples in the pixel shader for the floor/insect material to construct the 4 distance values?
ArKano22
ArKano22
Quote:
Original post by Hodgman
This is really cool =D

At what stage in the rendering do you do this "dfsm" filter?
e.g. do you do 3x3x4 SM samples in the pixel shader for the floor/insect material to construct the 4 distance values?


Exactly as you say. In the pixel shader, i perform the 4 filters and then bilinearly filter the 4 distances.
Hodgman
Hodgman
Thanks for sharing, I'm very intrigued ;)
Do the 4 different 3x3 filters overlap at all (allowing 16 samples, instead of 36 samples total), or do they need to be spread out to get a decent distance-field reconstruction?
e.g.
  Non-overlapping sample pattern|.|.|.|.|.|.| OR overlapping sample pattern|.|X|.|.|X|.|    |.|.|.|.||.|.|.|.|.|.|    |.|X|X|.||.|.|.|.|.|.|    |.|X|X|.||.|X|.|.|X|.|    |.|.|.|.||.|.|.|.|.|.|
ArKano22
ArKano22
Quote:
Original post by Hodgman
Thanks for sharing, I'm very intrigued ;)
Do the 4 different 3x3 filters overlap at all (allowing 16 samples, instead of 36 samples total), or do they need to be spread out to get a decent distance-field reconstruction?
e.g.
  Non-overlapping sample pattern|.|.|.|.|.|.| OR overlapping sample pattern|.|X|.|.|X|.|    |.|.|.|.||.|.|.|.|.|.|    |.|X|X|.||.|.|.|.|.|.|    |.|X|X|.||.|X|.|.|X|.|    |.|.|.|.||.|.|.|.|.|.|


The pattern overlaps. I´ve not tried with non-overlapping pattern but i assume that the bilinear filter would look a bit crappy with it... and it is crucial that the resulting distance value is as precise as possible!

InvalidPointer
InvalidPointer
Did somebody say 'distance fields' and 'shadow maps'? Sounds like a case for repurposing smoothies!

For this, you actually have the benefit of infinite resolution for your distance fields considering that the distances themselves are computed as vertex attributes. The only downside to this approach (that I can see) is that you'd likely get a sort of 'fattening' effect due to the fins, but this may or may not be a big problem.

I am quite frankly amazed more people don't make use of smoothies, as I find them to be one of the most obscenely useful base methods for generating shadows ever created :)
clb: At the end of 2012, the positions of jupiter, saturn, mercury, and deimos are aligned so as to cause a denormalized flush-to-zero bug when computing earth's gravitational force, slinging it to the sun.
ArKano22
ArKano22
Here´s the glsl code:

varying vec4 ShadowProj;uniform sampler2D ShadowMap;const float textureSize = 512.0; //size of the textureconst float texelSize = 1.0 / textureSize; //size of one texel const float bias = 0.0001; float texture2DBilinear(vec2 proj ,float tl, float tr, float bl, float br){    vec2 f = fract( proj.st * textureSize );     float tA = mix( tl, tr, f.x );     float tB = mix( bl, br, f.x );         return mix( tA, tB, f.y ); }float distanceField(in vec2 uv,float depth){  vec2 distance = vec2(0,0);  int ks = 1;  float count = 0;  for(int i=-ks; i <= ks; i++){     for(int j=-ks; j <= ks; j++){        vec2 sample = vec2(i*texelSize ,j*texelSize );        //Shadow test        float test = min(bias,depth - texture2D(ShadowMap, uv+sample).x)/bias;        //This texel´s contribution to distance calculation:	  distance += sample*test;         count+=test;     }  }  //No shadowed texels, maximum distance:  if (count < 0.1) return 1;  //Return length of the average sample vector:  return length(distance/count)/(ks*texelSize);}void main(){    //shadows    vec2 proj = ShadowProj.st/ShadowProj.q;     float depth = ShadowProj.z/ShadowProj.w;        float d1 = distanceField(proj,depth);    float d2 = distanceField(proj+vec2(texelSize ,0),depth);    float d3 = distanceField(proj+vec2(0,texelSize),depth);    float d4 = distanceField(proj+vec2(texelSize ,texelSize),depth);    float shadow = texture2DBilinear(proj,d1,d2,d3,d4);    shadow = smoothstep(0.0,1.0,(shadow-0.5)/(1.0-0.5));        gl_FragColor = vec4(vec3(shadow),1.0);}


Now some thoughts about the algorithm:

Pros:
-Eliminates jagged edges.
-Easy to implement.

Cons:
-Slower than standard shadow-maps.
-Does not allow very soft shadows (but you can antialias the edges a bit).

So if you want to use this, only do so if you need hard shadows but you don´t want to rely on shadow volumes or silhouette shadowmaps.

Also be warned that , while the apparent resolution at edges is higher than with a standard shadowmap, due to the distance filter the minimum detail that can be captured by the shadow map is lower: 1 texel width details are ommited and flagged as shadowed. For a detail to be correctly captured, it must be at least 2 texels wide. This is because of the way the distance filter averages distances.

So, use with caution. If you are searching for a more general "modern" shadow map algorithm, probably vsm/esm or some close relative are a better choice.
ArKano22
ArKano22
Quote:
Original post by InvalidPointer
Did somebody say 'distance fields' and 'shadow maps'? Sounds like a case for repurposing smoothies!

For this, you actually have the benefit of infinite resolution for your distance fields considering that the distances themselves are computed as vertex attributes. The only downside to this approach (that I can see) is that you'd likely get a sort of 'fattening' effect due to the fins, but this may or may not be a big problem.

I am quite frankly amazed more people don't make use of smoothies, as I find them to be one of the most obscenely useful base methods for generating shadows ever created :)


The only showstopper (for me) in smoothies is that you have to find silhouette edges in object space, just like in shadow volumes. Honestly i´ve never actually implemented them but if there´s another way to create the smoothie buffer, i would be glad to try them out.

The shadow fattening is not a big problem, i suppose. It doesn´t really bug me :).
ArKano22
ArKano22
When thinking about smoothies (thanks InvalidPointer!), i came up with this idea and i want to know what you think of it:

Take the zbuffer from the light p.o.v and apply a filter over it that, for each pixel, samples a few neighbours and computes a new value based on distance from each pixel to its neighbours: black for very near samples and white for far away samples, both in z difference and radial distance. Kinda like a badly implemented ssao, the ones that look like an edge enhancing filter, where the occlusion function always goes up with distance. Call the result the pseudo-smoothie buffer.

Then, compute the shadow map as usual and the final shadow value is the max between the shadow map and the value contained in the pseudo-smoothie buffer, which can be bilinearly filtered by hardware.

When creating the pseudo-smoothie buffer, if you make the sampling radius dependent on depth value, the look will be like that of pcss: penumbra size dependent on caster´s distance to receiver and light.

The downside is that, like usual smoothies and unlike pcss, there´s only outer penumbra. The good news are that you can skip the object space silhouette detection needed by conventional smoothies, and you can skip the occluder search needed by pcss.

All theory off my head, it can wreck if you try to implement it. Do you like the idea? :)

EDIT: more or less rediscovered shadow skirts: http://www.whdeboer.com/papers/smooth_penumbra_trans.pdf

[Edited by - ArKano22 on May 15, 2010 8:32:06 PM]
Krypt0n
Krypt0n
Quote:
Original post by ArKano22
I´m developing a project for which i need very very hard shadows, like stencil-volumes hard, but using shadowmaps.
You're solution looks quite cool.

When I was toying around with PSM I had the issue that those shadows were very hard-edged like stencil shadows. Maybe you want to check those out. they might be also a solution for your issue.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.