shadowmap技术
shadowmap技术就是用一张深度图保存场景中看见可见表面物体的信息,把深度信息存到深度图中。
shadowMap具体存储的事2部分,第一从太阳的视角看下来的投影,第二保存太阳视角下的VP矩阵
lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
lightView = glm::lookAt(lightPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));
lightSpaceMatrix = lightProjection * lightView;
第二保存深度图,具体使用FrameBuffer,渲染整个场景的信息。
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
renderScene(depthShadowShader, &ourModel);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
最后将保存下来的深度图和VP矩阵传入要着色的Shader中,再次渲染整个场景。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderScene(drawShader, &ourModel);
这里有几个问题:
深度图范围都是[0, 1],而我们通过传入要渲染的世界坐标的点和VP矩阵相互乘积得到了[-1, 1]NDC空间中,所以我们必须转换:
vec4 projCoords = worldPos * lightSpaceMatrix;
projCoords = projCoords * 0.5 + 0.5;
阴影中的分辨率肯定没有我们相机中某部分分辨率高,所以这回导致有斑纹,
这里,我们可以添加一个当前深度值中偏移量,再去比较深度值的大小。
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
还有一种方法是根据光照来生成偏移值。
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
悬浮
悬浮 是由于偏移量有可能足够大,以至于可以看出阴影相对实际物体位置的偏移。 我们可以通过使用物体背面的深度值。
glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // 不要忘记设回原先的culling face
多余的采样
看图中,我们会发现,会出现多余的阴影,这是由于当采样的xy数值纹理超过了深度图纹理大小的时候就会产生这种情况,这种情况下的解决方案为:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
超出部分纹理值全部使用1.0,这样就是光照的区域了。
还有一部分为坐标超出了光的正交视锥的远平面。我们这样就无法设计到。
这里,我们可以加一层判断,超出视锥z值1.0之外的我们就特殊处理:
if(projCoords.z > 1.0)
shadow = 0.0;
PCF
因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。
我们可以在深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
正交投影和透视投影
正交投影之后,数据是线性的,而透视投影之后,数据就不成线性值,(渲染走同一个流程都被投影线性和非线性空间)如果我们需要调试程序(debug),那么就要将非线性转换为线性值
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // Back to NDC
return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}
float depthValue = texture(depthMap, TexCoords).r;
color = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
shadowPoint技术。(适用于点光源)
在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。这哥算法和定向阴影映射差不多:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
总结Code步骤:
1生成CUBEMAP立方体贴图(或者生成6个FrameBuffer),生成FrameBuffer
2 渲染以点光源视角6个面,保存ZBUFFER
3 渲染真实场景使用ZBUFFER中的值
//生成CUBE_MAP 6面体阴影贴图
unsigned int depthCubeMap;
glGenTextures(1, &depthCubeMap);
const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubeMap);
for (unsigned int i = 0; i < 6; i++)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);//拿边界颜色
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
生成帧缓冲
unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubeMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
渲染
shadowTransforms.clear();
lightPos.z = static_cast<float>(sin(glfwGetTime() * 0.5) * 3.0);
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(1.0, 0.0, 0.0), glm::vec3(0.0f, -1.0f, 0.0f)));//右
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(-1.0, 0.0, 0.0), glm::vec3(0.0f, -1.0f, 0.0f)));//左
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(0.0, 1.0, 0.0), glm::vec3(0.0f, 0.0f, 1.0f)));//上
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(0.0, -1.0, 0.0), glm::vec3(0.0f, 0.0f, -1.0f)));//下
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(0.0, 0.0, 1.0), glm::vec3(0.0f, -1.0f, 0.0f)));//前
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos,
lightPos + glm::vec3(0.0, 0.0, -1.0), glm::vec3(0.0f, -1.0f, 0.0f)));//后
//先渲染帧缓存部分将z值保存在FrameBUffer中
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
LightPoint.use();
for (int i = 0; i < 6; i++)
LightPoint.setMat4("ShadowMatrix[" + to_string(i) +"]", shadowTransforms[i]);
LightPoint.setFloat("farPlane", far_plane);
LightPoint.setVec3("lightPos", lightPos);
renderScene(LightPoint);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
DrawShadowPoint.use();
DrawShadowPoint.setMat4("projection", projection);
DrawShadowPoint.setMat4("view", view);
DrawShadowPoint.setVec3("camPos", camera.Position);
DrawShadowPoint.setVec3("lightPos", lightPos);
DrawShadowPoint.setFloat("far_plane", far_plane);
DrawShadowPoint.setInt("shadows", shadows);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubeMap);
renderScene(DrawShadowPoint);
级联阴影CSM技术
阴影映射有很多缺点:
阴影贴图始终围绕固定点创建,而不是在相机实际注视的区域上。如果我们能以足够的分辨率对整个场景进行阴影贴图当然是最好的,但在当前的硬件上这是不可行的。实际上,我们希望在可见的对象上创建阴影贴图,从而为重要的事情节省宝贵的 GPU 内存。
阴影贴图正射投影矩阵没有正确地适合视锥体。为了使我们的阴影贴图获得尽可能好的分辨率,正交矩阵需要尽可能紧密地适合相机视锥体,因为再次强调:如果它更大,则意味着在实际可见的对象上花费的细节更少。
如果我们希望阴影渲染距离较大,阴影贴图(即使使用高级 PCF 功能)也会变得模糊,就像在使用第一人称视角的游戏中那样。我们可以增加阴影贴图的分辨率来缓解这种情况,但 GPU 内存是我们应该保守的资源。
级联阴影映射是上面第三点的回答,但是在实现它的同时我们也解决了前两点。级联阴影贴图的核心见解是,对于远离我们的事物,我们不需要相同精度的阴影细节。我们希望近平面附近的物体有清晰的阴影,而我们对数百个单位以外的物体的模糊度绝对没问题:它根本不会引人注意。我们怎样才能做到这一点?答案很简单:只需为近处和远处的事物渲染不同的阴影贴图,并根据片段着色器中片段的深度从中采样。算法如下:
将我们的普通视锥体分成 n 个子锥体,其中第 i 个锥体的远平面是第 (i+1) 个锥体i的近平面
计算每个视锥体的紧密拟合正交矩阵
为每个视锥体渲染一个阴影贴图,就像从我们的定向光中看到的一样
将所有阴影贴图发送到我们的片段着色器
渲染场景,并根据片段的深度值从正确的阴影贴图中采样
我们将从相机位置出发,来将视锥体划分,由于视图投影矩阵之后我们所在空间是一个[-1,1]的归一化空间,所以我们通过逆矩阵,可以变回到原先的世界空间,来获取相机视锥体的几个平截头体角点。
//获取子锥体的世界空间位置 平截头体角点
//世界空间 ndc[-1,1]
std::vector<glm::vec4> getFrustumCornersWorldSpace(const glm::mat4& projview)
{
const auto inv = glm::inverse(projview);
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;
}
这里描述的投影矩阵是一个透视矩阵,使用相机的宽高比和fov,并使用当前正在分析的平截头体的近平面和远平面。视图矩阵是我们相机的视图矩阵。
光视图投影矩阵
我们所说明的光是定向光源,(是从视锥体中心上方往下看),矩阵需要正交投影。
Code获取视锥体的中心点
const glm::vec3 lightDir = glm::normalize(glm::vec3(20.0f, 50, 20.0f));
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类型值中的最大值
float minX = std::numeric_limits<float>::max();
float maxX = std::numeric_limits<float>::min();
float minY = std::numeric_limits<float>::max();
float maxY = std::numeric_limits<float>::min();
float minZ = std::numeric_limits<float>::max();
float maxZ = std::numeric_limits<float>::min();
//世界空间8个平截头体点 -》光源相机空间 排序(后面正交矩阵使用)
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);
}
在创建实际的投影矩阵之前,我们将增加光锥的近平面和远平面所覆盖的空间大小。我们通过“拉回”近平面和“推开”远平面来做到这一点(模糊效果)。
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);
接下来是生成相应的Cascaded Shadow 的多个Map,我们根据相机的近远平面,生成5个不同层级的Shadow Level
float cameraFar = 500.0f;
float cameraNear = 0.1f;
std::vector<float> shadowCascadeLevels = { cameraFar / 50.0f, cameraFar / 25.0f, cameraFar / 10.0f, cameraFar / 2.0f };
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(cameraNear, shadowCascadeLevels[i]));
}
else if (i < shadowCascadeLevels.size())
{
ret.push_back(getLightSpaceMatrix(shadowCascadeLevels[i - 1], shadowCascadeLevels[i]));
}
else
{
ret.push_back(getLightSpaceMatrix(shadowCascadeLevels[i - 1], cameraFar));
}
}
return ret;
}
我们使用二位纹理数组去定义FrameBuffer
glGenTextures(1, &depthLevelMap);
glBindTexture(GL_TEXTURE_2D_ARRAY, depthLevelMap);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH_COMPONENT32F, ShadowWidth, ShadowHeight,
int(shadowCascadeLevels.size()) + 1, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
const float bordColor[] = { 1.0,1.0,1.0,1.0 };//光源视锥体外的都是光照范围
glTexParameterfv(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BORDER_COLOR, bordColor);
glGenFramebuffers(1, &depthFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, depthFrameBuffer);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthLevelMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
我们不能以普通方式渲染到这个纹理中,我们需要做一些叫做分层渲染.我们使用几何着色器同时为我们并行生成多层Cascaded Shadow Map。
#version 460 core
//并行执行5次 gl_InvocationID执行组ID
layout(triangles, invocations = 5) in;
layout(triangle_strip, max_vertices = 3) out;
layout (std140, binding = 0) uniform LightSpaceMatrices
{
mat4 lightSpaceMatrices[16];
};
void main()
{
for (int i = 0; i < 3; ++i)
{
gl_Position =
lightSpaceMatrices[gl_InvocationID] * gl_in[i].gl_Position;
gl_Layer = gl_InvocationID;
EmitVertex();
}
EndPrimitive();
}
最后获取相应的Cascaded Shadow Map层级:
我们通过相机坐标去获取当前z的大小,通过z值去获取他在Cascaded Shadow Map的层级。阴影的绘制和shadow map相同
//获取相机坐标下的z值
vec4 fragPosInViewSpace = view * vec4(WorldPos, 1.0);
float currentDepthInViewSpace = abs(fragPosInViewSpace.z);
int layer = -1;
//遍历Cascaded Shadow Map层级个数 获取相应的索引
for(int i = 0; i < cascadeCount; ++i)
{
if(currentDepthInViewSpace < viewLevel[i])
{
layer = i;
break;
}
}
运行效果如图:
Reference: