Shadow Mapping阴影渲染
Shadow Mapping阴影渲染分为两个步骤:
- 以光源视角渲染场景,得到深度图(Shadow Map),存储为纹理;
- 以观察视角再次渲染场景,将每个点的深度值和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;
|
优化前:
优化后: