Xungerrrr's Blog

Shadow Mapping

Compute Graphics - Homework 7

Word count: 1.3kReading time: 5 min
2019/05/13 Share

Shadow Mapping阴影渲染

Shadow Mapping阴影渲染分为两个步骤:

  1. 以光源视角渲染场景,得到深度图(Shadow Map),存储为纹理;
  2. 以观察视角再次渲染场景,将每个点的深度值和Shadow Map中的深度值进行比较,判断点是否在阴影内。

获取Shadow Map

着色器

首先来设计着色器。因为要在光源视角渲染场景,所以顶点着色器只需要将坐标变换到光源视角空间并输出即可。使用一个变换矩阵即可完成变换。

1
2
3
4
5
6
7
8
9
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main() {
gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}

至于片段着色器,因为只需要获取深度信息而不需要颜色信息,所以可以使用一个空的片段着色器。

存储Shadow Map

为了获得Shadow Map,我们需要将渲染的深度信息存到一个2D纹理中。需要借助帧缓冲对象和纹理来实现。

首先,创建一个2D纹理。因为我们只需要存储深度值,所以将纹理格式设置为GL_DEPTH_COMPONENT。

1
2
3
4
5
6
7
8
9
10
unsigned int depthMap;
const unsigned int SHADOW_WIDTH = 4096, SHADOW_HEIGHT = 4096;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

然后,创建一个帧缓冲对象,将纹理绑定为帧缓冲的深度缓冲。因为不需要颜色缓冲,所以通过调用glDrawBuffer(GL_NONE)和glReadBuffer(GL_NONE)说明不用颜色进行渲染。

1
2
3
4
5
6
7
unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

光源空间变换

在顶点着色器中,用到了一个变换矩阵,将世界坐标变换到光源视角。变换矩阵由投影矩阵和视角矩阵相乘得到。

对于平行光,可以采用正交投影,通过glm::ortho定义矩阵。

1
2
3
float near_plane = 1.0f, far_plane = 100.0f;
glm::mat4 lightProjection = glm::mat4(1.0f);
lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);

通过glm::lookAt函数,创建观察矩阵,从光源位置看向坐标原点。

1
2
3
glm::vec3 lightPos(-3.0f, 4.0f, -1.0f);
glm::mat4 lightView = glm::mat4(1.0f);
lightView = glm::lookAt(lightPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));

相乘得到变换矩阵。

1
2
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
lightSpaceMatrix = lightProjection * lightView;

渲染

通过glBindFramebuffer绑定帧缓冲,在函数renderScene中渲染场景。渲染完成后深度信息就存储到了纹理中,得到Shadow Map。

1
2
3
4
5
6
7
8
depthShader.use();
depthShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);

glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
renderScene(depthShader, planeVAO, cubeVAO);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

阴影计算(Shadow Mapping算法)

下一步,需要从观察视角进行渲染,从光源视角对像素深度和Shadow Map中对应位置的值进行比较。如果深度大于Shadow Map的值,说明该点在其他物体后,处在阴影内。反之则不在阴影内。

着色器

顶点着色器与实现Phong光照模型的顶点着色器相似。不过,因为要比较深度值判断片段是否在阴影内,所以顶点着色器还需要输出片段变换到光源视角空间的坐标。

1
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);

片段着色器与实现Phong光照模型的片段着色器相似。不过,片段着色器还需要计算片段是否在阴影内。

首先,需要将光源视角空间的坐标进行标准化,变换到[0, 1]之间。z坐标即是当前的深度。

1
2
3
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
float currentDepth = projCoords.z;

然后,获取Shadow Map中的深度信息。比较当前深度和最近深度,判断片段是否在阴影内。

1
2
float closestDepth = texture(shadowMap, projCoords.xy).r; 
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;

如果在阴影内,则只显示环境光。否则按照Phong光照模型进行渲染。

1
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;

渲染

渲染的方法与之前的类似。在渲染前,需要绑定两个纹理。一个是物体的纹理,这里使用了木地板纹理。另一个是第一步得到的Shadow Map,用于计算阴影。

1
2
3
4
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthMap);

效果

增加光源在透视投影下的Shadow Mapping

通过ImGui选择投影方式。

1
2
3
4
5
6
ImGui::Begin("Projection Type");
ImGui::BeginGroup();
ImGui::RadioButton("Orthogonal", &projectionType, 1);
ImGui::RadioButton("Perspective", &projectionType, 2);
ImGui::EndGroup();
ImGui::End();

根据投影方式创建不同的投影矩阵。

1
2
3
4
if (projectionType == 1)
lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
else if (projectionType == 2)
lightProjection = glm::perspective(glm::radians(120.0f), 1.0f, near_plane, far_plane);

让光源动起来,使效果明显。

1
2
lightPos.z = sin(time) * 10.0f;
lightPos.y = sin(time/ 2.0f) * 2 + 4.0f;

效果

正交投影:

透视投影:

优化阴影

失真

通过加入偏移量,避免了由于Shadow Map分辨率不足导致的阴影失真。

1
2
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

优化前:

优化后:

悬浮

在渲染Shadow Map的时候,通过正面剔除,减轻了悬浮的现象。

1
2
3
glCullFace(GL_FRONT);
renderScene(depthShader, planeVAO, cubeVAO);
glCullFace(GL_BACK);

锯齿

通过使用5*5的模板进行均值滤波,减轻了阴影的锯齿化现象。

1
2
3
4
5
6
7
8
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -2; x <= 2; ++x) {
for(int y = -2; y <= 2; ++y) {
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 25.0;

优化前:

优化后:

CATALOG
  1. 1. Shadow Mapping阴影渲染
    1. 1.1. 获取Shadow Map
      1. 1.1.1. 着色器
      2. 1.1.2. 存储Shadow Map
      3. 1.1.3. 光源空间变换
      4. 1.1.4. 渲染
    2. 1.2. 阴影计算(Shadow Mapping算法)
      1. 1.2.1. 着色器
      2. 1.2.2. 渲染
    3. 1.3. 效果
  2. 2. 增加光源在透视投影下的Shadow Mapping
    1. 2.1. 效果
  3. 3. 优化阴影
    1. 3.1. 失真
    2. 3.2. 悬浮
    3. 3.3. 锯齿