级联阴影贴图意外行为
Cascaded shadow map unexpected behavior
我正在实施级联阴影贴图技术,我得到了意想不到的结果
首先我初始化缓冲区和纹理:
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glGenTextures(NUM_CASCADES, m_shadowMap);
for (uint i = 0; i < NUM_CASCADES; i++) {
glBindTexture(GL_TEXTURE_2D, m_shadowMap[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, size, size, 0, GL_DEPTH_COMPONENT,
GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_EQUAL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap[i], 0);
}
glDrawBuffers(1, GL_NONE);
glReadBuffer(GL_NONE);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
然后我渲染到深度缓冲区:
这里我计算每个级联的光投影视图矩阵,然后从光透视渲染场景。
glUseProgram(programID);
GLfloat minDistance = 0.0f;
GLfloat nearClip = camera->getProjection().getNear();
GLfloat farClip = camera->getProjection().getFar();
GLfloat cascadeSplits[NUM_CASCADES+1] = {nearClip,(farClip-nearClip)*0.08f,(farClip-nearClip)*0.2f,(farClip-nearClip)*0.6f,farClip};
for (unsigned int cascadeIterator = 0; cascadeIterator < NUM_CASCADES; ++cascadeIterator) {
GLfloat prevSplitDistance =
cascadeIterator == 0 ? minDistance : cascadeSplits[cascadeIterator - 1];
GLfloat splitDistance = cascadeSplits[cascadeIterator];
glm::vec3 frustumCornersWS[8] = {glm::vec3(-1.0f, 1.0f, -1.0f),
glm::vec3(1.0f, 1.0f, -1.0f),
glm::vec3(1.0f, -1.0f, -1.0f),
glm::vec3(-1.0f, -1.0f, -1.0f),
glm::vec3(-1.0f, 1.0f, 1.0f),
glm::vec3(1.0f, 1.0f, 1.0f),
glm::vec3(1.0f, -1.0f, 1.0f),
glm::vec3(-1.0f, -1.0f, 1.0f),};
glm::mat4 invViewProj = glm::inverse(
camera->getProjection().getProjectionMatrix() * camera->getView().getViewMatrix());
for (unsigned int i = 0; i < 8; ++i) {
glm::vec4 inversePoint = invViewProj * glm::vec4(frustumCornersWS[i], 1.0f);
frustumCornersWS[i] = glm::vec3(inversePoint / inversePoint.w);
}
for (unsigned int i = 0; i < 4; ++i) {
glm::vec3 cornerRay = frustumCornersWS[i + 4] - frustumCornersWS[i];
glm::vec3 nearCornerRay = cornerRay * prevSplitDistance;
glm::vec3 farCornerRay = cornerRay * splitDistance;
frustumCornersWS[i + 4] = frustumCornersWS[i] + farCornerRay;
frustumCornersWS[i] = frustumCornersWS[i] + nearCornerRay;
}
glm::vec3 frustumCenter = glm::vec3(0.0f);
for (unsigned int i = 0; i < 8; ++i)
frustumCenter += frustumCornersWS[i];
frustumCenter /= 8.0f;
GLfloat radius = 0.0f;
for (unsigned int i = 0; i < 8; ++i) {
GLfloat distance = glm::length(frustumCornersWS[i] - frustumCenter);
radius = glm::max(radius, distance);
}
radius = std::ceil(radius * 16.0f) / 16.0f;
glm::vec3 maxExtents = glm::vec3(radius, radius, radius);
glm::vec3 minExtents = -maxExtents;
//Position the viewmatrix looking down the center of the frustum with an arbitrary lighht direction
glm::vec3 lightDirection =
frustumCenter - glm::normalize(light->getDirection()) * -minExtents.z;
glm::mat4 lightViewMatrix = glm::mat4(1.0f);
lightViewMatrix = glm::lookAt(lightDirection, frustumCenter, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 cascadeExtents = maxExtents - minExtents;
glm::mat4 lightOrthoMatrix = glm::ortho(minExtents.x, maxExtents.x, minExtents.y,
maxExtents.y, 0.0f, cascadeExtents.z);
// The rounding matrix that ensures that shadow edges do not shimmer
glm::mat4 shadowMatrix = lightOrthoMatrix * lightViewMatrix;
glm::vec4 shadowOrigin = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
shadowOrigin = shadowMatrix * shadowOrigin;
float mShadowMapSize = static_cast<float>(size);
shadowOrigin = shadowOrigin * mShadowMapSize / 2.0f;
glm::vec4 roundedOrigin = glm::round(shadowOrigin);
glm::vec4 roundOffset = roundedOrigin - shadowOrigin;
roundOffset = roundOffset * 2.0f / mShadowMapSize;
roundOffset.z = 0.0f;
roundOffset.w = 0.0f;
glm::mat4 shadowProj = lightOrthoMatrix;
shadowProj[3] += roundOffset;
lightOrthoMatrix = shadowProj;
//Store the split distances and the relevant matrices
const float clipDist = farClip - nearClip;
cascadeEndSpace[cascadeIterator] = (nearClip + splitDistance * clipDist) * -1.0f;
lightProjectionView[cascadeIterator] = lightOrthoMatrix * lightViewMatrix;
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
glViewport(0, 0, mShadowMapSize, mShadowMapSize);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap[cascadeIterator],0);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glCullFace(GL_FRONT);
glUniformMatrix4fv(glGetUniformLocation(programID, "lightProjectionView"), 1, GL_FALSE,
glm::value_ptr(lightProjectionView[cascadeIterator]));
for (Geometry::Object *object:objects) {
object->RenderToDepth(programID);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
glUseProgram(0);
最后渲染场景:
for (uint i = 0; i < NUM_CASCADES; i++) {
glActiveTexture(GL_TEXTURE4 + i);
glBindTexture(GL_TEXTURE_2D, m_shadowMap[i]);
const char *shadowLoc = (const char *) ("map_shadow[" + Tools::ToString(i)+"]").c_str();
glUniform1i(glGetUniformLocation(programID, shadowLoc), 4 + i);
const char *lightLoc = (const char *) ("lightProjectionView[" + Tools::ToString(i) +
"]").c_str();
glUniformMatrix4fv(glGetUniformLocation(programID, lightLoc), 1, GL_FALSE,
glm::value_ptr(lightProjectionView[i]));
int cascadeSpaceLoc = glGetUniformLocation(programID, (const char *) ("cascadeEndSpace[" +
Tools::ToString(i) +
"]").c_str());
glUniform1f(cascadeSpaceLoc, cascadeEndSpace[i]);
}
最终Shader方法为:
"float readShadowMap(){"
" float positiveViewSpaceZ = FViewPos.z;"
" int cascadeIdx = 0;"
" for(int i = 0; i < NUM_CASCADES - 1; ++i){"
" if(positiveViewSpaceZ < cascadeEndSpace[i]){"
" cascadeIdx = i + 1;"
" }"
" }"
" vec4 fragmentShadowPosition = LightSpacePos[cascadeIdx];"
" vec3 projCoords = fragmentShadowPosition.xyz / fragmentShadowPosition.w;"
" projCoords = projCoords * 0.5f + 0.5f;"
" float currentDepth = projCoords.z;"
" float pcfDepth = 0.0f;"
" if(cascadeIdx == 0)"
" pcfDepth = texture(map_shadow[0], projCoords.xy).x;"
" else if(cascadeIdx == 1)"
" pcfDepth = texture(map_shadow[1], projCoords.xy).x;"
" else if(cascadeIdx == 2)"
" pcfDepth = texture(map_shadow[2], projCoords.xy).x;"
" float shadow = currentDepth + 0.00001 > pcfDepth ? 0.5 : 1.0;"
" return shadow;"
"}"
我尝试更改纹理参数但没有任何改变。
您看到的是一个通常称为 "shadow acne" 的混叠伪像。您可以通过一些插图找到很好的解释 here。基本上,发生的情况是,由于分辨率和精度有限,物体表面的一部分最终会在自身上投射阴影。对于从相机视角渲染的每个片段,您将其位置投影到阴影贴图中并比较深度值。除非你的相机图像和阴影贴图的采样率完全匹配(这基本上不可能通过常规采样实现,除非你大量过度采样),否则会有多个片段最终投射到同一个阴影贴图像素的区域。你的碎片都来自一个平面,它通常以不同的角度朝向相机而不是朝向光线。因此,您最终会得到多个相邻片段,它们的深度都略有不同,但都映射到相同的阴影贴图像素,即与相同的深度值进行比较。这些片段中大约一半的深度小于阴影贴图中的像素,大约一半大于阴影贴图中的像素。再加上一些舍入误差噪声,您就会得到上面发布的图像。
这个问题的经典解决方案是在渲染阴影贴图或相机图像时应用斜率深度偏差,例如,使用 glPolygonOffset:
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(1.0f, 1.0f);
// render shadow map
glDisable(GL_POLYGON_OFFSET_FILL);
// render scene with shadows
我正在实施级联阴影贴图技术,我得到了意想不到的结果
首先我初始化缓冲区和纹理:
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glGenTextures(NUM_CASCADES, m_shadowMap);
for (uint i = 0; i < NUM_CASCADES; i++) {
glBindTexture(GL_TEXTURE_2D, m_shadowMap[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, size, size, 0, GL_DEPTH_COMPONENT,
GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_EQUAL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap[i], 0);
}
glDrawBuffers(1, GL_NONE);
glReadBuffer(GL_NONE);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
然后我渲染到深度缓冲区: 这里我计算每个级联的光投影视图矩阵,然后从光透视渲染场景。
glUseProgram(programID);
GLfloat minDistance = 0.0f;
GLfloat nearClip = camera->getProjection().getNear();
GLfloat farClip = camera->getProjection().getFar();
GLfloat cascadeSplits[NUM_CASCADES+1] = {nearClip,(farClip-nearClip)*0.08f,(farClip-nearClip)*0.2f,(farClip-nearClip)*0.6f,farClip};
for (unsigned int cascadeIterator = 0; cascadeIterator < NUM_CASCADES; ++cascadeIterator) {
GLfloat prevSplitDistance =
cascadeIterator == 0 ? minDistance : cascadeSplits[cascadeIterator - 1];
GLfloat splitDistance = cascadeSplits[cascadeIterator];
glm::vec3 frustumCornersWS[8] = {glm::vec3(-1.0f, 1.0f, -1.0f),
glm::vec3(1.0f, 1.0f, -1.0f),
glm::vec3(1.0f, -1.0f, -1.0f),
glm::vec3(-1.0f, -1.0f, -1.0f),
glm::vec3(-1.0f, 1.0f, 1.0f),
glm::vec3(1.0f, 1.0f, 1.0f),
glm::vec3(1.0f, -1.0f, 1.0f),
glm::vec3(-1.0f, -1.0f, 1.0f),};
glm::mat4 invViewProj = glm::inverse(
camera->getProjection().getProjectionMatrix() * camera->getView().getViewMatrix());
for (unsigned int i = 0; i < 8; ++i) {
glm::vec4 inversePoint = invViewProj * glm::vec4(frustumCornersWS[i], 1.0f);
frustumCornersWS[i] = glm::vec3(inversePoint / inversePoint.w);
}
for (unsigned int i = 0; i < 4; ++i) {
glm::vec3 cornerRay = frustumCornersWS[i + 4] - frustumCornersWS[i];
glm::vec3 nearCornerRay = cornerRay * prevSplitDistance;
glm::vec3 farCornerRay = cornerRay * splitDistance;
frustumCornersWS[i + 4] = frustumCornersWS[i] + farCornerRay;
frustumCornersWS[i] = frustumCornersWS[i] + nearCornerRay;
}
glm::vec3 frustumCenter = glm::vec3(0.0f);
for (unsigned int i = 0; i < 8; ++i)
frustumCenter += frustumCornersWS[i];
frustumCenter /= 8.0f;
GLfloat radius = 0.0f;
for (unsigned int i = 0; i < 8; ++i) {
GLfloat distance = glm::length(frustumCornersWS[i] - frustumCenter);
radius = glm::max(radius, distance);
}
radius = std::ceil(radius * 16.0f) / 16.0f;
glm::vec3 maxExtents = glm::vec3(radius, radius, radius);
glm::vec3 minExtents = -maxExtents;
//Position the viewmatrix looking down the center of the frustum with an arbitrary lighht direction
glm::vec3 lightDirection =
frustumCenter - glm::normalize(light->getDirection()) * -minExtents.z;
glm::mat4 lightViewMatrix = glm::mat4(1.0f);
lightViewMatrix = glm::lookAt(lightDirection, frustumCenter, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 cascadeExtents = maxExtents - minExtents;
glm::mat4 lightOrthoMatrix = glm::ortho(minExtents.x, maxExtents.x, minExtents.y,
maxExtents.y, 0.0f, cascadeExtents.z);
// The rounding matrix that ensures that shadow edges do not shimmer
glm::mat4 shadowMatrix = lightOrthoMatrix * lightViewMatrix;
glm::vec4 shadowOrigin = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
shadowOrigin = shadowMatrix * shadowOrigin;
float mShadowMapSize = static_cast<float>(size);
shadowOrigin = shadowOrigin * mShadowMapSize / 2.0f;
glm::vec4 roundedOrigin = glm::round(shadowOrigin);
glm::vec4 roundOffset = roundedOrigin - shadowOrigin;
roundOffset = roundOffset * 2.0f / mShadowMapSize;
roundOffset.z = 0.0f;
roundOffset.w = 0.0f;
glm::mat4 shadowProj = lightOrthoMatrix;
shadowProj[3] += roundOffset;
lightOrthoMatrix = shadowProj;
//Store the split distances and the relevant matrices
const float clipDist = farClip - nearClip;
cascadeEndSpace[cascadeIterator] = (nearClip + splitDistance * clipDist) * -1.0f;
lightProjectionView[cascadeIterator] = lightOrthoMatrix * lightViewMatrix;
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
glViewport(0, 0, mShadowMapSize, mShadowMapSize);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap[cascadeIterator],0);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glCullFace(GL_FRONT);
glUniformMatrix4fv(glGetUniformLocation(programID, "lightProjectionView"), 1, GL_FALSE,
glm::value_ptr(lightProjectionView[cascadeIterator]));
for (Geometry::Object *object:objects) {
object->RenderToDepth(programID);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
glUseProgram(0);
最后渲染场景:
for (uint i = 0; i < NUM_CASCADES; i++) {
glActiveTexture(GL_TEXTURE4 + i);
glBindTexture(GL_TEXTURE_2D, m_shadowMap[i]);
const char *shadowLoc = (const char *) ("map_shadow[" + Tools::ToString(i)+"]").c_str();
glUniform1i(glGetUniformLocation(programID, shadowLoc), 4 + i);
const char *lightLoc = (const char *) ("lightProjectionView[" + Tools::ToString(i) +
"]").c_str();
glUniformMatrix4fv(glGetUniformLocation(programID, lightLoc), 1, GL_FALSE,
glm::value_ptr(lightProjectionView[i]));
int cascadeSpaceLoc = glGetUniformLocation(programID, (const char *) ("cascadeEndSpace[" +
Tools::ToString(i) +
"]").c_str());
glUniform1f(cascadeSpaceLoc, cascadeEndSpace[i]);
}
最终Shader方法为:
"float readShadowMap(){"
" float positiveViewSpaceZ = FViewPos.z;"
" int cascadeIdx = 0;"
" for(int i = 0; i < NUM_CASCADES - 1; ++i){"
" if(positiveViewSpaceZ < cascadeEndSpace[i]){"
" cascadeIdx = i + 1;"
" }"
" }"
" vec4 fragmentShadowPosition = LightSpacePos[cascadeIdx];"
" vec3 projCoords = fragmentShadowPosition.xyz / fragmentShadowPosition.w;"
" projCoords = projCoords * 0.5f + 0.5f;"
" float currentDepth = projCoords.z;"
" float pcfDepth = 0.0f;"
" if(cascadeIdx == 0)"
" pcfDepth = texture(map_shadow[0], projCoords.xy).x;"
" else if(cascadeIdx == 1)"
" pcfDepth = texture(map_shadow[1], projCoords.xy).x;"
" else if(cascadeIdx == 2)"
" pcfDepth = texture(map_shadow[2], projCoords.xy).x;"
" float shadow = currentDepth + 0.00001 > pcfDepth ? 0.5 : 1.0;"
" return shadow;"
"}"
我尝试更改纹理参数但没有任何改变。
您看到的是一个通常称为 "shadow acne" 的混叠伪像。您可以通过一些插图找到很好的解释 here。基本上,发生的情况是,由于分辨率和精度有限,物体表面的一部分最终会在自身上投射阴影。对于从相机视角渲染的每个片段,您将其位置投影到阴影贴图中并比较深度值。除非你的相机图像和阴影贴图的采样率完全匹配(这基本上不可能通过常规采样实现,除非你大量过度采样),否则会有多个片段最终投射到同一个阴影贴图像素的区域。你的碎片都来自一个平面,它通常以不同的角度朝向相机而不是朝向光线。因此,您最终会得到多个相邻片段,它们的深度都略有不同,但都映射到相同的阴影贴图像素,即与相同的深度值进行比较。这些片段中大约一半的深度小于阴影贴图中的像素,大约一半大于阴影贴图中的像素。再加上一些舍入误差噪声,您就会得到上面发布的图像。
这个问题的经典解决方案是在渲染阴影贴图或相机图像时应用斜率深度偏差,例如,使用 glPolygonOffset:
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(1.0f, 1.0f);
// render shadow map
glDisable(GL_POLYGON_OFFSET_FILL);
// render scene with shadows