Friday, March 21, 2008

Parallax Occlusion Mapping: no more floating shadows!

I recently implemented Parallax Occlusion Mapping. It looks pretty good, until I add shadows. I'm not talking about self-shadows that are discussed in the POM paper, but shadows cast by other objects onto the object with POM, using any kind of shadow mapping technique. The shadows appear to float above the pits. Here's a still:



It's not very noticeable in a still, but becomes very apparent when moving around. This is unfortunate since POM requires a lot of shader horsepower to get very nice looking bumps, and then the effects gets ruined by introducing shadows.

The brute force method is to displace (with shader z exports) every point on the POM surface, and simply redo the shadow mapping algorithm. This is a very expensive operation, as export will turn off an z optimizations that your hardware would otherwise do for (hi-z, early-z). So I have a much more elegant solution, that does not involve any z exports. Here's a screeny:


As you can see, the shadows follow the curves of the bumps and pits, and the illusion of higher detailed geometry remains.

The idea behind this is to perturb the shadow map coordinates, along with the texture and normal map coordinates that is already being done by the POM algorithm. The shadow offset is not the same as texture offset, as texture space is different from shadow space, so we will have to do a 2d transformation from texture to shadow before perturbing the shadow coordinates.

If we assume that both the texture and shadow coordinates have orthogonal gradients in the x/y (this means we can't warp the textures to fit the geometry, and we can't use warping shadow mapping techniques, like PSM, LiSPSM, etc), we can use these gradients to compute the 2x2 matrix that scales and rotates the texture x gradient to the shadow x gradient. Here's a it of code that does this:

float2 tx = ddx( texcoord );
float3 sx = ddx( shadow_coord.xyz );
float3 sy = ddy( shadow_coord.xyz );

float length_scale = length(sx.xy) / length(tx);

float2 norm_tx = normalize(tx);
float2 norm_sx = normalize(sx.xy);

float cos_th = length_scale*dot(norm_tx, norm_sx);
float sin_th = length_scale*cross(float3(norm_tx,0.0),
float3(norm_sx,0.0)).z;

shadow_xform[0].x = cos_th;
shadow_xform[0].y = -sin_th;
shadow_xform[1].x = sin_th;
shadow_xform[1].y = cos_th;


After that, multiply the offset generated by POM by this matrix, and you get the xy offset to your shadow map. However, this is not good enough. The z coordinate (the distance from the light to point on the screen) also needs to be perturbed, or else you may end up with false self shadows. This can be done by calculating the slopes of the plane that this point lies on, and then taking the dot product of that with the shadow offset that was previously computed. I compute the plane slopes using gradients again. This is technically not correct since at edges of polygons, you would calculate the incorrect slope. In practice, I have not seen any issues by calculating the slopes in this way. Here's the code for it:

float2 plane_slope;
plane_slope.x = (sx.z*sy.y - sy.z*sx.y) / (sx.x*sy.y - sy.x*sx.y);
plane_slope.y = (sx.z*sy.x - sy.z*sx.x) / (sx.y*sy.x - sy.y*sx.x);

shadow_offset.z = dot( plane_slope, shadow_offset );

This technique does not require POM to be used. It can used with any offset generation technique, like plain Parallax Mapping. However, since it uses gradients, it works only with ps 3.0 and up (or ps 2.a, if you have the right nvidia card). Having said that, it is possible to compute vertex level gradients in the cpu (which is fine for our purposes), and store in vertex data, in which case, this technique can be extended to ps 2.0 cards, at the cost of extra vertex buffer data.

I'm sure there are ways to reduce the computation; I just haven't thought about it too much. One idea, is if you're lucky enough to have a ps 4.0 card, and unlucky enough to have vista, you can use dx10's geometry shader to do most of this computation, and therefore only need to do this once per primitive, instead of once per pixel.

Well, that's about it for today. Hope you enjoyed, and do let me know if you try this out, and how it worked out for you.


Cheers
-Kamal

No comments: