feat: CSM 软阴影——sampler2DShadow 硬件 PCF + 3×3 纹素 tap

- 阴影采样器开启硬件深度比较(compareOp=eLess + linear filter):
  单次采样 = 2×2 深度比较插值,clamp-to-border 白色语义不变
- ShadowFactor 改 3×3 纹素偏移 tap(等效 36 次比较),影中保留
  0.3 环境感并按 PCF 结果插值——阴影边缘软过渡,消除二值硬边锯齿
This commit is contained in:
2026-09-23 23:18:36 +08:00
parent 960bdf2662
commit 8c0a11d2e8
2 changed files with 32 additions and 14 deletions
+5 -2
View File
@@ -103,7 +103,9 @@ namespace Engine::Passes
ENGINE_FATAL("SceneGpu: failed to create shader objects"); ENGINE_FATAL("SceneGpu: failed to create shader objects");
} }
// 阴影采样器:clamp-to-border 白色 = 光视锥之外无阴影 // 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
//(省 shader 出界分支);开启硬件深度比较(sampler2DShadow)
// ——单次采样即 2×2 PCF,配合 shader 3×3 tap 得软阴影
vk::SamplerCreateInfo shadowSamplerInfo; vk::SamplerCreateInfo shadowSamplerInfo;
shadowSamplerInfo.setMagFilter(vk::Filter::eLinear) shadowSamplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear) .setMinFilter(vk::Filter::eLinear)
@@ -113,7 +115,8 @@ namespace Engine::Passes
.setAddressModeW(vk::SamplerAddressMode::eClampToBorder) .setAddressModeW(vk::SamplerAddressMode::eClampToBorder)
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite) .setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
.setUnnormalizedCoordinates(VK_FALSE) .setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE); .setCompareEnable(VK_TRUE)
.setCompareOp(vk::CompareOp::eLess);
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo); shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑) // per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
+27 -12
View File
@@ -2,6 +2,7 @@
const int MAX_LIGHTS = 4; const int MAX_LIGHTS = 4;
const float PI = 3.14159265359; const float PI = 3.14159265359;
const int kCascadeRes = 1024; // CSM 级联图边长(与引擎 SceneTypes.h 同步)
struct Light { struct Light {
vec4 color; // rgb = 颜色,a = 强度 vec4 color; // rgb = 颜色,a = 强度
@@ -43,16 +44,18 @@ layout(set = 1, binding = 5) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子 vec4 factors; // x = metallic 因子,y = roughness 因子
} matParams; } matParams;
layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0(最近) // CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1 layout(set = 2, binding = 0) uniform sampler2DShadow shadowMap0; // CSM 级联 0(最近)
layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远) layout(set = 2, binding = 1) uniform sampler2DShadow shadowMap1; // 级联 1
layout(set = 2, binding = 2) uniform sampler2DShadow shadowMap2; // 级联 2(最远)
layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②) layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②)
layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③) layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③)
layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias) layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias)
// 第 0 盏方向光(KeyLight)的 CSM 阴影因子:0.3 影中 / 1.0 受光。 // 第 0 盏方向光(KeyLight)的 CSM 阴影因子。
// 级联按视图深度选择;采样器均 clamp-to-border 白色: // 级联按视图深度选择;3×3 纹素偏移采样(每 tap 硬件 2×2 PCF,
// 光视锥外自动 1.0(无阴影),省出界分支。 // 等效 36 次比较)→ 软阴影边缘;采样器 clamp-to-border 白色:
// 光视锥外自动 1.0(无阴影),省出界分支
float ShadowFactor(vec3 N, float viewDepth) { float ShadowFactor(vec3 N, float viewDepth) {
int cascade = 2; int cascade = 2;
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
@@ -63,15 +66,27 @@ float ShadowFactor(vec3 N, float viewDepth) {
vec3 proj = ls.xyz / ls.w; vec3 proj = ls.xyz / ls.w;
vec2 uv = proj.xy * 0.5 + 0.5; vec2 uv = proj.xy * 0.5 + 0.5;
float closest;
if (cascade == 0) closest = texture(shadowMap0, uv).r;
else if (cascade == 1) closest = texture(shadowMap1, uv).r;
else closest = texture(shadowMap2, uv).r;
vec3 L = normalize(-ubo.lights[0].direction.xyz); vec3 L = normalize(-ubo.lights[0].direction.xyz);
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012); float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012);
return (proj.z - bias > closest) ? 0.3 : 1.0; float texel = 1.0 / float(kCascadeRes); // 与引擎 kCascadeRes 同步
float shadow = 0.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
float lit;
if (cascade == 0)
lit = texture(shadowMap0, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else if (cascade == 1)
lit = texture(shadowMap1, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else
lit = texture(shadowMap2, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
shadow += mix(0.3, 1.0, lit); // 影中保留 0.3 环境感
}
}
return shadow / 9.0;
} }
// 法线贴图 → 世界空间法线(TBN 构建)。 // 法线贴图 → 世界空间法线(TBN 构建)。