[toc]

# 级联阴影需要解决的问题

  1. 透视锯齿

方向光通常模拟太阳光,单个方向光即可照亮整个场景。这意味着方向光的阴影贴图会覆盖场景的大部分,因此可能会引起称为 “透视锯齿” 的问题。透视锯齿是指靠近摄像机的阴影贴图像素看起来比那些更远的像素更大块。 上述问题产生的原因
1. 假设光源垂直于摄像机 第二张图的网格就是生成的阴影贴图,可以看出,由于透视视椎体的近平面只占了贴图的 4 个像素,而远平面则占了 20 个像素。
而阴影的计算方法,就是通过近平面上的一个片元的深度和深度图的深度进行对比,而这个过程就需要对阴影贴图进行采样,
由于最终视椎体是会转到NDC空间的 ,所以可以理解为最终的采样就是在 视椎体内阴影贴图大小 决定的(因为物体会缩放,贴图也会缩放,远平面对应的像素逻辑上会压缩,近平面对应的像素逻辑上会在 4 个像素的基础上 upscale)
所以在 shadowmap 下的阴影采样在近平面的表现上会存在很多锯齿,因为它在近平面的 逻辑采样分辨率很低

# 什么是级联阴影

为了解决上面的问题,工业界提出了 CSM (cascade shadow map), 他的思想其实很简单
- 将视椎体分为几个连续的子视椎体
- 在子视椎体内做 shadowmap
- 在当前片元所在视椎体的层级去采样对应层级的 shadowmap 前向渲染 CSM 的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//视椎体分割
frustumList = []
for i=1,maxSplit do
计算对应视椎体8个点的世界空间坐标和包围盒
插入frustumList[i]
end

Shadowmap2DArray
lightmatrix
//逐光源计算shadowamp
for i=1,maxLightCount do
for j=1,maxSplit do
//或者用加速结构剔除数量,这里只说一个简单的
for k = 1,场景中包围球个数 do
如果在frustumList[j]的包围盒范围内,计算shadowmap(drawPass)
计算逛空间的正交投影矩阵
需要加速子视椎体查找的话,也可以计算远平面在NDC空间下的范围,传给Shader,之后shader片元着色器直接判断范围后采样
end
end
end


# OpenGl 实现

对应的片元 Shader 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#version 410 core
out vec4 FragColor;

in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} fs_in;

uniform sampler2D diffuseTexture;
uniform sampler2DArray shadowMap;

uniform vec3 lightDir;
uniform vec3 viewPos;
uniform float farPlane;

uniform mat4 view;

layout (std140) uniform LightSpaceMatrices
{
mat4 lightSpaceMatrices[16];
};
uniform float cascadePlaneDistances[16];
uniform int cascadeCount; // number of frusta - 1

float ShadowCalculation(vec3 fragPosWorldSpace)
{
// select cascade layer
vec4 fragPosViewSpace = view * vec4(fragPosWorldSpace, 1.0);
float depthValue = abs(fragPosViewSpace.z);

int layer = -1;
for (int i = 0; i < cascadeCount; ++i)
{
if (depthValue < cascadePlaneDistances[i])
{
layer = i;
break;
}
}
if (layer == -1)
{
layer = cascadeCount;
}

vec4 fragPosLightSpace = lightSpaceMatrices[layer] * vec4(fragPosWorldSpace, 1.0);
// perform perspective divide
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// transform to [0,1] range
projCoords = projCoords * 0.5 + 0.5;

// get depth of current fragment from light's perspective
float currentDepth = projCoords.z;

// keep the shadow at 0.0 when outside the far_plane region of the light's frustum.
if (currentDepth > 1.0)
{
return 0.0;
}
// calculate bias (based on depth map resolution and slope)
vec3 normal = normalize(fs_in.Normal);
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
const float biasModifier = 0.5f;
if (layer == cascadeCount)
{
bias *= 1 / (farPlane * biasModifier);
}
else
{
bias *= 1 / (cascadePlaneDistances[layer] * biasModifier);
}

// PCF
float shadow = 0.0;
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, vec3(projCoords.xy + vec2(x, y) * texelSize, layer)).r;
shadow += (currentDepth - bias) > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;

return shadow;
}

void main()
{
vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = vec3(0.3);
// ambient
vec3 ambient = 0.3 * color;
// diffuse
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * lightColor;
// specular
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
vec3 specular = spec * lightColor;
// calculate shadow
float shadow = ShadowCalculation(fs_in.FragPos);
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;

FragColor = vec4(lighting, 1.0);
}


可以看到,先找到了最近的 shadowMap 层级 放到Layer参数中
然后在 PCF 采样时对对应 layer 的 shadowMap 进行采样 代码层主要添加的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//声明四级Cascade视椎体
std::vector<float> shadowCascadeLevels{
cameraFarPlane / 50.0f, cameraFarPlane / 25.0f, cameraFarPlane / 10.0f, cameraFarPlane / 2.0f
};

//声明光源的Texture2D
//创建FrameBuffer(light)
glGenFramebuffers(1, &lightFBO);
glGenTextures(1, &lightDepthMaps);
glBindTexture(GL_TEXTURE_2D_ARRAY, lightDepthMaps);
glTexImage3D(
GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH_COMPONENT32F, depthMapResolution, depthMapResolution, int(shadowCascadeLevels.size()) + 1,
0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);

// 0. UBO setup,先更新UBO,这一步就是得到所有的层级
const auto lightMatrices = getLightSpaceMatrices();
glBindBuffer(GL_UNIFORM_BUFFER, matricesUBO);
for (size_t i = 0; i < lightMatrices.size(); ++i)
{
glBufferSubData(GL_UNIFORM_BUFFER, i * sizeof(glm::mat4x4), sizeof(glm::mat4x4), &lightMatrices[i]);
}
glBindBuffer(GL_UNIFORM_BUFFER, 0);

//渲染场景阴影深度图
simpleDepthShader.use();


//对于每个子视椎体给出对应的世界空间下的坐标
std::vector<glm::vec4> getFrustumCornersWorldSpace(const glm::mat4& projview) {
const auto inv = glm::inverse(projview); //world

std::vector<glm::vec4> frustumCorners;
for (unsigned int x = 0; x < 2; ++x)
{
for (unsigned int y = 0; y < 2; ++y)
{
for (unsigned int z = 0; z < 2; ++z)
{
const glm::vec4 pt = inv * glm::vec4(2.0f * x - 1.0f, 2.0f * y - 1.0f, 2.0f * z - 1.0f, 1.0f);
frustumCorners.push_back(pt / pt.w);
}
}
}

return frustumCorners;
}

std::vector<glm::vec4> getFrustumCornersWorldSpace(const glm::mat4& proj, const glm::mat4& view)
{
return getFrustumCornersWorldSpace(proj * view);
}

glm::mat4 getLightSpaceMatrix(const float nearPlane, const float farPlane) {
const auto proj = glm::perspective(
glm::radians(camera.Zoom), (float)fb_width / (float)fb_height, nearPlane,
farPlane);
const auto corners = getFrustumCornersWorldSpace(proj, camera.GetViewMatrix());

//计算中心(世界坐标下)
glm::vec3 center = glm::vec3(0, 0, 0);
for (const auto& v : corners)
{
center += glm::vec3(v);
}
center /= corners.size();

const auto lightView = glm::lookAt(center + lightDir, center, glm::vec3(0.0f, 1.0f, 0.0f));

float minX = std::numeric_limits<float>::max();
float maxX = std::numeric_limits<float>::lowest();
float minY = std::numeric_limits<float>::max();
float maxY = std::numeric_limits<float>::lowest();
float minZ = std::numeric_limits<float>::max();
float maxZ = std::numeric_limits<float>::lowest();
for (const auto& v : corners)
{
const auto trf = lightView * v;
minX = std::min(minX, trf.x);
maxX = std::max(maxX, trf.x);
minY = std::min(minY, trf.y);
maxY = std::max(maxY, trf.y);
minZ = std::min(minZ, trf.z);
maxZ = std::max(maxZ, trf.z);
}

// Tune this parameter according to the scene
constexpr float zMult = 10.0f;
if (minZ < 0)
{
minZ *= zMult;
}
else
{
minZ /= zMult;
}
if (maxZ < 0)
{
maxZ /= zMult;
}
else
{
maxZ *= zMult;
}

const glm::mat4 lightProjection = glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);
return lightProjection * lightView;
}


std::vector<glm::mat4> getLightSpaceMatrices()
{
std::vector<glm::mat4> ret;
for (size_t i = 0; i < shadowCascadeLevels.size() + 1; ++i)
{
if (i == 0)
{
ret.push_back(getLightSpaceMatrix(cameraNearPlane, shadowCascadeLevels[i]));
}
else if (i < shadowCascadeLevels.size())
{
ret.push_back(getLightSpaceMatrix(shadowCascadeLevels[i - 1], shadowCascadeLevels[i]));
}
else
{
ret.push_back(getLightSpaceMatrix(shadowCascadeLevels[i - 1], cameraFarPlane));
}
}
return ret;
}


但是 OpenGL 教程里的这个实现没有进行实例剔除,就是粗暴的对每一级都进行了全场景的深度图渲染

# 参考

[1] LearnOpengl-Cascaded Shadow Mapping
[2] 级联阴影贴图(CSM)