Dual paraboloid point light shadows in deferred lighting setup

I've been playing around with this tutorial/sample code that demonstrates a simple implementation of light-pre-pass, which is a type of deferred lighting setup.

I'm in the process of implementing point light shadows, using dual-paraboloid shadow maps. I'm following this description of DPM: http://gamedevelop.eu/en/tutorials/dual-paraboloid-shadow-mapping.htm

I am able to create the shadow maps, and they seem to look fine.

I believe that the current problem I'm having is in my pixel shader that looks up a depth value in the shadow map when rendering point lights.

The pixel shader function of interest is PointLightMeshShadowPS.

Does anyone see a glaring error in that function?

Hopefully someone has tackled this problem before :)

As you can see in the images above, the post's shadows do not match up with the positions of the posts, so some transformation is wrong somewhere...

This is what it looks like when the point light is very close to the ground (almost touching the ground).

As the point light moves closer to the ground, the shadows come together and touch along the line where the two shadow maps meet (that is, along the plane where the light camera was flipped to capture the two shadow maps).

Edit:

Further information:

When I move the point light away from the origin, there is a line parallel to the light camera's "right" vector that clips the shadow. The above image shows the result of moving the point light to the left. If I move the point light to the right, there is an equivalent clipping line on the right instead. So I think this indicates that I'm transforming something incorrectly in the pixel shader, like I thought.

Edit: To make this question more clear, here are a few pieces of code.

Here is the code that I currently use to draw a shadowed spot light. This works, and uses shadow mapping as you'd expect.

VertexShaderOutputMeshBased SpotLightMeshVS(VertexShaderInput input)
{
output.Position = mul(input.Position, WorldViewProjection);

//we will compute our texture coords based on pixel position further
output.TexCoordScreenSpace = output.Position;
return output;
}

//////////////////////////////////////////////////////
//////////////////////////////////////////////////////
{
//as we are using a sphere mesh, we need to recompute each pixel position into texture space coords
float2 screenPos = PostProjectionSpaceToScreenSpace(input.TexCoordScreenSpace) + GBufferPixelSize;
float depthValue = tex2D(depthSampler, screenPos).r;

//if depth value == 1, we can assume its a background value, so skip it
//we need this only if we are using back-face culling on our light volumes. Otherwise, our z-buffer
//will reject this pixel anyway

//if depth value == 1, we can assume its a background value, so skip it
clip(-depthValue + 0.9999f);

// Reconstruct position from the depth value, the FOV, aspect and pixel position
depthValue*=FarClip;

//convert screenPos to [-1..1] range
float3 pos = float3(TanAspect*(screenPos*2 - 1)*depthValue, -depthValue);

//light direction from current pixel to current light
float3 lDir = LightPosition - pos;

//compute attenuation, 1 - saturate(d2/r2)
float atten = ComputeAttenuation(lDir);

// Convert normal back with the decoding function
float4 normalMap = tex2D(normalSampler, screenPos);
float3 normal = DecodeNormal(normalMap);

lDir = normalize(lDir);

// N dot L lighting term, attenuated
float nl = saturate(dot(normal, lDir))*atten;

//spot light cone
half spotAtten = min(1,max(0,dot(lDir,LightDir) - SpotAngle)*SpotExponent);
nl *= spotAtten;

//reject pixels outside our radius or that are not facing the light
clip(nl -0.00001f);

float4 lightPosition = mul(mul(float4(pos,1),CameraTransform), MatLightViewProjSpot);

// Find the position in the shadow map for this pixel
float2 shadowTexCoord = 0.5 * lightPosition.xy /
lightPosition.w + float2( 0.5, 0.5 );
//offset by the texel size

// Calculate the current pixel depth
// The bias is used to prevent floating point errors
float ourdepth = (lightPosition.z / lightPosition.w) - DepthBias;

float4 finalColor;

//As our position is relative to camera position, we dont need to use (ViewPosition - pos) here
float3 camDir = normalize(pos);

// Calculate specular term
float3 h = normalize(reflect(lDir, normal));
float spec = nl*pow(saturate(dot(camDir, h)), normalMap.b*50);
finalColor = float4(LightColor * nl, spec);

//output light
return finalColor * LightBufferScale;
}


Now here is the point light code that I'm using, which has some sort of bug in the transformation into light space when using the shadow maps:

VertexShaderOutputMeshBased PointLightMeshVS(VertexShaderInput input)
{
output.Position = mul(input.Position, WorldViewProjection);

//we will compute our texture coords based on pixel position further
output.TexCoordScreenSpace = output.Position;
return output;
}

{
// as we are using a sphere mesh, we need to recompute each pixel position
// into texture space coords
float2 screenPos =
PostProjectionSpaceToScreenSpace(input.TexCoordScreenSpace) + GBufferPixelSize;

float depthValue = tex2D(depthSampler, screenPos).r;

// if depth value == 1, we can assume its a background value, so skip it
// we need this only if we are using back-face culling on our light volumes.
// Otherwise, our z-buffer will reject this pixel anyway
clip(-depthValue + 0.9999f);

// Reconstruct position from the depth value, the FOV, aspect and pixel position
depthValue *= FarClip;

// convert screenPos to [-1..1] range
float3 pos = float3(TanAspect*(screenPos*2 - 1)*depthValue, -depthValue);

// light direction from current pixel to current light
float3 lDir = LightPosition - pos;

// compute attenuation, 1 - saturate(d2/r2)
float atten = ComputeAttenuation(lDir);

// Convert normal back with the decoding function
float4 normalMap = tex2D(normalSampler, screenPos);
float3 normal = DecodeNormal(normalMap);

lDir = normalize(lDir);

// N dot L lighting term, attenuated
float nl = saturate(dot(normal, lDir))*atten;

float4 lightPosition = mul(mul(float4(pos,1),CameraTransform), LightViewProj);

//float4 lightPosition = mul(float4(pos,1), LightViewProj);
float posLength = length(lightPosition);
lightPosition /= posLength;

float ourdepth = (posLength - NearClip) / (FarClip - NearClip) - DepthBias;
//float ourdepth = (lightPosition.z / lightPosition.w) - DepthBias;

if(lightPosition.z > 0.0f)
{
float2 vTexFront;
vTexFront.x =  (lightPosition.x /  (1.0f + lightPosition.z)) * 0.5f + 0.5f;
vTexFront.y =  1.0f - ((lightPosition.y /  (1.0f + lightPosition.z)) * 0.5f + 0.5f);

}
else
{
// for the back the z has to be inverted
float2 vTexBack;
vTexBack.x =  (lightPosition.x /  (1.0f - lightPosition.z)) * 0.5f + 0.5f;
vTexBack.y =  1.0f - ((lightPosition.y /  (1.0f - lightPosition.z)) * 0.5f + 0.5f);

}

// reject pixels outside our radius or that are not facing the light
clip(nl - 0.00001f);

float4 finalColor;
//As our position is relative to camera position, we dont need to use (ViewPosition - pos) here
float3 camDir = normalize(pos);

// Calculate specular term
float3 h = normalize(reflect(lDir, normal));
float spec = nl*pow(saturate(dot(camDir, h)), normalMap.b*100);
finalColor = float4(LightColor * nl, spec);

return finalColor * LightBufferScale;
}

• and you say shadow maps themselves are having no problem/ (I mean if you burn the shadow maps to to texturemap they'll darken correct spots?) – Ali1S232 Jun 4 '11 at 14:40
• Are you 100% sure the FOV of the camera rendering from the light source's position is correct? – Roy T. Jun 4 '11 at 15:19
• The camera rendering from the light's source position does not have a projection matrix, because the projection is done manually in order to have the paraboloid warp. I'll check that code though, good idea Roy T. – Olhovsky Jun 4 '11 at 19:01
• Gajet: "I mean if you burn the shadow maps to to texturemap they'll darken correct spots?" The shadow maps store shadows in light space, if I look at the map, there's no easy way to know for certain that they are correct because I see them in screenspace. What is a "texturemap" -- you mean a texture? Shadow maps are textures. – Olhovsky Jun 4 '11 at 19:07
• Roy T.: Moving the light around reveals that the shadow map gets clipped, so there is a problem with the transformation when actually using the shadow, not just when creating it. – Olhovsky Jun 4 '11 at 19:22

With PIX you can debug pixels isolated, maybe you find the error in this way. FOV or a projection error is a hot hint. Or have you forgot the world transformation?!

• you can also try debuging with NVidia-fxComposer – Ali1S232 Jun 4 '11 at 16:52
• I don't think that staring at the assembly code values is going to help me very much at this point, because I'm having trouble understanding how the transformation should be done in the first place. So seeing what value is in register 10, or wherever, is not really going to help. – Olhovsky Jun 4 '11 at 19:02
• "Or have you forgot the world transformation?!" I actually did forget to apply the world transform when creating the shadow maps -- doh! This works now, leaving all the shaders as I had them. – Olhovsky Jun 5 '11 at 23:10

Hey Olhovsky, nice challenging question. I know your pain, I implemented Deferred Shading, Inferred Lighting and shadows in my last job. It was really great fun, but lots of pain too when it didn't work as expected.

I think the advice with PIX is actually a good one. You don't have to mess with the assembler instructions of the shader, but you can look at the shadow maps and other render targets, select a pixel and call its pixel shader and step through it and also its vertex-shader.

General debug tricks for this kind of situations include simplification of the scene.

One that comes to my mind is: put the camera at the same position as the light source with the same fovy and other attributes as in the lighting pass. Now you can easily compare the values in the pixel-shader. The pixel-xy in the normal render-pass for your current object should be the same as the calculated pixel-xy for the lookup in the shadowmap, as long as it has the same resolution.

Another one is switch to orthographic projection, render something easy, predictable and checkable. The simpler the better you can check each calculation step.

Other than that, can you show how you create the matrix that calculates the position in the shadow-map for the current pixel, that is the transformation from screen-space to light-space?

• Just seeing the shadow map is a Parabloid, that makes it even harder to debug and the idea to put the camera at the lights position to compare current pixel position and position in the shadow-map won't work, nevermind :) – Maik Semder Jun 4 '11 at 20:30
• If you're interested, send me an email at kris@olhovsky.com, and I'll reply with a copy of my project. Otherwise: The CameraTransform matrix is actually the world matrix of the camera currently viewing the scene. The LightViewProj matrix is actually just the world matrix of the light, as the view matrix of the light is just the identity matrix. – Olhovsky Jun 4 '11 at 20:33
• Can you make a simple C++ project with it? There should also be the parabloid transformation involved right? – Maik Semder Jun 4 '11 at 20:39
• The paraboloid transformation is in the pixel shader I linked in the question. My C++ skills are too limited to drum up a quick C++ project that encapsulates the entire deferred rendering pipeline I think :) However, if you're proficient with C++, then I think it shouldn't be too hard to read my C# code. Especially since most of the concern is really in the pixel shader, and perhaps with the matricies passed to it. – Olhovsky Jun 4 '11 at 20:44
• I was refering to g_mDPView and g_mDPWorldView. Can you show how they are calculated. – Maik Semder Jun 4 '11 at 20:47