You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

双抛物面阴影映射(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 00:08:09