双抛物面阴影映射(DPSM)实现异常求助:深度敏感与穿透伪影
双抛物面阴影映射(DPSM)实现问题排查
问题描述
- 实现DPSM时,初始出现阴影缺失问题,将灯光远平面从1000调整为50后解决,疑惑DPSM是否对深度范围异常敏感
- 灯光处于室外时,室内本应完全处于阴影中,但出现灯光穿透墙壁的伪影(已禁用剔除),手动设置深度范围为
nearZ=1e-4,farZ=200后效果略有改善,但仍残留问题 - 相同流程实现的平行光阴影映射工作正常
相关代码
阴影贴图生成代码
顶点着色器
struct VS_OUTPUT { float4 position : SV_POSITION; float ClipDepth : TEXCOORD1; float Depth : TEXCOORD2; }; VS_OUTPUT performVS(VS_INPUT input, uint instanceID) { VS_OUTPUT output; const float4x4 modelMat = meshGroupDatas[instanceID].transform; const float4 worldPosition = mul(float4(input.position, 1.0f), modelMat); output.position = mul(worldPosition, light.transform); output.position /= output.position.w; #if SHADOWMAP_INTERNAL_IDX output.position.z *= -1.0f; #endif // because the origin is at 0 the proj-vector // matches the vertex-position float fLength = length(output.position.xyz); // normalize output.position /= fLength; // save for clipping output.ClipDepth = output.position.z; // calc "normal" on intersection, by adding the // reflection-vector(0,0,1) and divide through // his z to get the texture coords output.position.x /= output.position.z + 1.0f; output.position.y /= output.position.z + 1.0f; // set z for z-buffering and neutralize w output.position.z = (fLength - light.nearZ) / (light.farZ - light.nearZ); output.position.w = 1.0f; // DP-depth output.Depth = output.position.z; return output; }
像素着色器
struct VS_OUTPUT { float4 position : SV_POSITION; float ClipDepth : TEXCOORD1; float Depth : TEXCOORD2; }; void main(VS_OUTPUT input) { clip(input.ClipDepth); }
阴影贴图采样代码
float OmniShadowMapping(Texture2DArray<float> shadowMaps, LightStruct light, float3 P, SamplerState samplerSate) { // texcoord-calculation is the same calculation as in the Depth-VS, // but texcoords have to be in range [0, 1] // transform into lightspace float3 vPosDP = mul(float4(P, 1.0f), light.transform); float fLength = length(vPosDP); // normalize vPosDP /= fLength; // compute and read according depth const float fSceneDepth = (fLength - light.nearZ) / (light.farZ - light.nearZ); float fDPDepth; if (vPosDP.z >= 0.0f) { float2 vTexFront; vTexFront.x = (vPosDP.x / (1.0f + vPosDP.z)) * 0.5f + 0.5f; vTexFront.y = 1.0f - ((vPosDP.y / (1.0f + vPosDP.z)) * 0.5f + 0.5f); fDPDepth = shadowMaps.SampleLevel(samplerSate, float3(vTexFront, light.linearIdx * NB_SHADOW_MAP_PER_LIGHT), 0).x; } else { // for the back the z has to be inverted float2 vTexBack; vTexBack.x = (vPosDP.x / (1.0f - vPosDP.z)) * 0.5f + 0.5f; vTexBack.y = 1.0f - ((vPosDP.y / (1.0f - vPosDP.z)) * 0.5f + 0.5f); fDPDepth = shadowMaps.SampleLevel(samplerSate, float3(vTexBack, light.linearIdx * NB_SHADOW_MAP_PER_LIGHT + 1u), 0).x; } const float bias = 0.0025f; if (fSceneDepth - bias > fDPDepth) return 0.0f; // in shadow return 1.0f; // lit }
灯光变换矩阵计算代码
// Here how the light transform is computed: const DirectX::XMVECTOR cPos = DirectX::XMVectorSet(lightPosition.x, lightPosition.y, lightPosition.z, 0.0f); const Math::Vec3 lightTarget = lightPosition + Math::Vec3(0.0f, 0.0f, 1.0f); const DirectX::XMVECTOR cTarg = DirectX::XMVectorSet(lightTarget.x, lightTarget.y, lightTarget.z, 0.0f); const DirectX::XMVECTOR cUp = DirectX::XMVectorSet(lightUp.x, lightUp.y, lightUp.z, 0.0f); transform = XMMatrixTranspose(DirectX::XMMatrixLookAtRH(cPos, cTarg, cUp));
平行光阴影映射代码(正常工作)
阴影贴图生成
顶点着色器
struct VS_OUTPUT { float4 position : SV_POSITION; }; VS_OUTPUT main(VS_INPUT input, uint instanceID : SV_InstanceID) { VS_OUTPUT output; const float4x4 modelMat = meshGroupDatas[instanceID].transform; const float4 worldPosition = mul(float4(input.position, 1.0f), modelMat); output.position = mul(worldPosition, light.transform); return output; }
像素着色器
struct VS_OUTPUT { float4 position : SV_POSITION; }; void main(VS_OUTPUT input) { }
采样代码
static float2 stratifiedPoissonDisk[16] = { float2(-0.825623,-0.91054), float2(-0.0928773,-0.617703), float2(0.224972,-0.971807), float2(0.760581,-0.535332), float2(-0.698438,-0.482954), float2(-0.31658,-0.148361), float2(0.119801,-0.0881411), float2(0.70214,-0.290242), float2(-0.520126,0.14676), float2(-0.204791,0.394246), float2(0.309273,0.288691), float2(0.514146,0.351331), float2(-0.692232,0.91788), float2(-0.383484,0.892866), float2(0.299836,0.581476), float2(0.595979,0.956923) }; float rand(float2 co) { return frac(sin(dot(co.xy, float2(12.9898f, 78.233f))) * 43758.5453f); } float DirectionalShadowMapping(Texture2DArray<float> shadowMaps, LightStruct light, float3 P, float2 pixelCoord, SamplerState samplerSate) { const float4 homogenous = mul(float4(P, 1.0f), light.transform); const float3 NDC = homogenous.xyz / homogenous.w; const float2 depthUV = float2( NDC.x * 0.5f + 0.5f, -NDC.y * 0.5f + 0.5f ); const float angle = rand(pixelCoord) * PI * 2.0f; const float2x2 rotation = float2x2(cos(angle), -sin(angle), sin(angle), cos(angle)); const float currentDepth = NDC.z; const float bias = 0.0025f; float shadow = 0.0f; const float radius = 0.002f; const uint numSamples = 16; for (uint i = 0u; i < numSamples; i++) { float2 offset = mul(rotation, stratifiedPoissonDisk[i]) * radius; const float closestDepth = shadowMaps.SampleLevel(samplerSate, float3(depthUV + offset, light.linearIdx * NB_SHADOW_MAP_PER_LIGHT), 0).x; shadow += currentDepth - bias > closestDepth ? 0.0f : 1.0f; } return shadow / numSamples; }
问题排查建议
1. DPSM深度范围敏感问题
DPSM使用线性深度映射 (fLength - nearZ)/(farZ - nearZ),没有透视投影的非线性深度压缩特性。当farZ远大于场景实际范围时,大部分深度值会被压缩到极小的浮点区间,导致阴影贴图无法区分近处物体的深度差异,最终出现阴影缺失。这是DPSM的固有精度限制。
解决方案:
- 动态计算灯光的
nearZ和farZ,包裹场景中需要被阴影覆盖的物体,避免过大的深度范围 - 若必须使用大范围,尝试替换为对数深度映射,提升远距离的深度精度
2. 灯光穿透墙壁伪影问题
- 坐标空间一致性检查:确认灯光变换矩阵的行/列顺序是否正确。
XMMatrixLookAtRH生成的是视图矩阵,代码中直接转置后作为light.transform使用,需确保顶点着色器和采样阶段的空间转换逻辑完全一致,避免空间错位导致深度比较错误。 - 深度偏差调整:固定bias值可能无法适配所有场景,尝试增加bias值(如从0.0025调整到0.005),或使用动态bias——根据表面法线与灯光方向的夹角调整bias大小,倾斜表面需要更大的bias来避免漏影。
- 裁剪逻辑验证:检查
ClipDepth的裁剪逻辑是否正确。前抛物面裁剪z<0的顶点,后抛物面在SHADOWMAP_INTERNAL_IDX分支中反转了z轴,需确保此时的裁剪条件对应正确的空间区域,避免错误保留被墙壁遮挡的顶点。 - 阴影贴图格式检查:确认阴影贴图使用高精度格式(如R32_FLOAT),低精度格式(如R16_FLOAT)会加剧深度精度问题,导致穿透伪影。
内容的提问来源于stack exchange,提问作者TheChamp
相关产品推荐
相关产品推荐

