ShadowMap ShadowPoint和CSM

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:

https://learnopengl.com/Guest-Articles/2021/CSM

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值