[OpenGL] Cascade Shadowmap(层级阴影)

        这几天花了点时间优化了一下阴影。

        原本的阴影实现有一些比较严重的问题:

        第一个是我使用了透视投影,再加上角度问题,导致阴影看起来存在近大远小的变形,不符合太阳光产生的阴影;

        第二个是我的阴影贴图是在世界空间的固定位置生成的,且阴影贴图的大小是有限的,导致整个画面中,只有落在阴影贴图中的非常小的一部分物体能够产生阴影;如果扩大阴影的视锥体范围,则可以容纳更多的物体,但这会导致深度精度降低,阴影质量大幅下降。

       为了避免阴影只能在某个区域产生,首先需要做的改动是在相机前生成阴影区域(fit to view)。比较理想的位置是阴影贴图的区域恰好覆盖整个视锥体,但是这个实际上不太现实,因为为了显式更多的物体,视锥体的远裁剪面一般都会非常大。因此我们优先选择比较靠近相机位置的地方作为灯光空间的原点。

        我尝试着做了第一版(只有一个靠近相机的阴影贴图)阴影优化,此时确实解决了只在世界空间某个位置能生成阴影的问题,但是仍然存在一个弊端,只要相机稍微拉远一点,阴影就消失了。

        为了保证阴影能覆盖到更多区域,需要考虑使用层级阴影。通俗而言,就是在视锥体不同深度的位置分别生成阴影贴图,从而覆盖更多区域。此时,随着镜头推动,我们切换使用不同的阴影贴图,阴影精度也在发生改变。镜头比较远的时候,阴影将消失不显示,也比较符合lod的概念。

随着镜头越来越远,阴影精度降低,直到消失

 计算灯光空间变化矩阵、灯光视锥体

       我们把视锥体(上图为截面)分为多个区域,其中圆点为相机位置,射线为太阳光方向。图中我们为三个区域分别生成shadow map。

       对于太阳光而言,我们使用正交投影来生成阴影。那么灯光视锥体的形状将为一个立方体,一个比较好的选取方法就是在灯光空间取区域的AABB包围盒。比如,对于橙色区域而言,我们选取的包围盒为:

       为了能够在灯光空间求得视锥体的AABB包围盒,首先我们需要求出视锥体区域的八个顶点的世界坐标:

       计算的一个比较简单的方法是,在相机空间中,利用相似三角形先求得每个顶点的坐标,然后将它们转换到世界空间。如下代码中,将视锥体分为了四个区域,共五个截面,分别计算每个截面的顶点坐标(此处zPos就是相机空间下的多个z值,注意在OpenGL坐标系里,相机前物体的z坐标都是负数):

void Camera::UpdateVertex()
{
    float fov = RenderCommon::Inst()->GetFov();
    float aspect = RenderCommon::Inst()->GetAspect();

    QMatrix4x4 inverted_viewMatirx = viewMatrix.inverted();

    // leftTop, rightTop, leftBottom, rightBottom
    float tan_fov = tan(fov/2);
    vector<float> zPos{0, 15, 30, 100, 200};
    for(size_t i = 0;i < zPos.size(); i++)
    {
        float z = zPos[i];
        float y = z * tan_fov;
        float x = y * aspect;
        z = -z;
        QVector4D tmp_leftTopPos = QVector4D(-x, y, z, 1);
        QVector4D tmp_rightTopPos = QVector4D(x, y, z, 1);
        QVector4D tmp_leftBottomPos = QVector4D(-x, -y, z, 1);
        QVector4D tmp_rightBottomPos = QVector4D(x, -y, z, 1);

        tmp_leftTopPos = inverted_viewMatirx * tmp_leftTopPos;
        tmp_rightTopPos = inverted_viewMatirx * tmp_rightTopPos;
        tmp_leftBottomPos = inverted_viewMatirx * tmp_leftBottomPos;
        tmp_rightBottomPos = inverted_viewMatirx * tmp_rightBottomPos;

        viewPos[i][0] = QVector3D(tmp_leftTopPos.x(),tmp_leftTopPos.y(),tmp_leftTopPos.z()) / tmp_leftTopPos.w();
        viewPos[i][1] = QVector3D(tmp_rightTopPos.x(),tmp_rightTopPos.y(),tmp_rightTopPos.z()) / tmp_rightTopPos.w();
        viewPos[i][2] = QVector3D(tmp_leftBottomPos.x(),tmp_leftBottomPos.y(),tmp_leftBottomPos.z()) / tmp_leftBottomPos.w();
        viewPos[i][3] = QVector3D(tmp_rightBottomPos.x(),tmp_rightBottomPos.y(),tmp_rightBottomPos.z()) / tmp_rightBottomPos.w();
    }
}

        然后将其转换到灯光空间中。我们先构造一个转换矩阵。其中向量N代表的就是太阳光的方向。为了构造一个右手坐标系的转换矩阵,我们需要把N取反,也就是说,实际上以下代码构造的太阳光方向为(0, -1, -1)

void RenderCommon::UpdateLightSpace()
{
    QVector3D upDir(0, 1, 0);

    QVector3D N = QVector3D(0, 1, 1);
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    lightSpace.setRow(0, {U.x(), U.y(), U.z(), 0}); // x
    lightSpace.setRow(1, {V.x(), V.y(), V.z(), 0}); // y
    lightSpace.setRow(2, {N.x(), N.y(), N.z(), 0}); // z
    lightSpace.setRow(3, {0, 0, 0, 1});
}

        接下来,对于每个区域,我们需要构造两个矩阵。一个是灯光空间变换矩阵,另一个是灯光的视锥体。对于前者而言,它和刚才求得的lightSpace比较类型,但lightSpace不需要考虑位移变换,所以选取了原点即可。而灯光空间变换矩阵则需要我们确定灯光的位置,并将其作为原点。对于后者而言,如前面所提到的,我们需要使用正交投影。

        我们首先将视锥体的几个顶点转换到灯光空间,然后直接计算Xmin, Xmax, Ymin, Ymax, Zmin, Zmax,从而得到包围盒的八个顶点的位置。此时,我们将灯光的位置选定在这个包围盒((Xmin + Xmax) / 2, (Ymin + Ymax) / 2, Xmax)的位置。注意这个坐标是灯光空间下的,因此我们还需要将其转回到世界空间,此时我们就求出了灯光的坐标。

        同时,我们也得到了视锥体的长宽分别为Xmax - Xmin, Ymax - Ymin。就这样构造完成了我们所需要的几个矩阵。

void RenderCommon::UpdateLightMatrix()
{
    QVector3D upDir(0, 1, 0);
    QVector3D N = QVector3D(0, 1, 1);
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    QMatrix4x4 lightSpace_inverted = lightSpace.inverted();
    for(size_t i = 0;i < Shadow_Layer; i++)
    {
        QVector3D frontLeftTop = Camera::Inst()->GetViewPos(i, 0);
        QVector3D frontRightTop = Camera::Inst()->GetViewPos(i, 1);
        QVector3D frontLeftBottom = Camera::Inst()->GetViewPos(i, 2);
        QVector3D frontRightBottom = Camera::Inst()->GetViewPos(i, 3);

        QVector3D backLeftTop = Camera::Inst()->GetViewPos(i + 1, 0);
        QVector3D backRightTop = Camera::Inst()->GetViewPos(i + 1, 1);
        QVector3D backLeftBottom = Camera::Inst()->GetViewPos(i + 1, 2);
        QVector3D backRightBottom = Camera::Inst()->GetViewPos(i + 1, 3);

        QVector4D frontLeftTop1 = lightSpace * QVector4D(frontLeftTop, 1);
        QVector4D frontRightTop1 = lightSpace * QVector4D(frontRightTop, 1);
        QVector4D frontLeftBottom1 = lightSpace * QVector4D(frontLeftBottom, 1);
        QVector4D frontRightBottom1 = lightSpace * QVector4D(frontRightBottom, 1);

        QVector4D backLeftTop1 = lightSpace * QVector4D(backLeftTop, 1);
        QVector4D backRightTop1 = lightSpace * QVector4D(backRightTop, 1);
        QVector4D backLeftBottom1 = lightSpace * QVector4D(backLeftBottom, 1);
        QVector4D backRightBottom1 = lightSpace * QVector4D(backRightBottom, 1);

        frontLeftTop = QVector3D(frontLeftTop1.x(),frontLeftTop1.y(),frontLeftTop1.z()) / frontLeftTop1.w();
        frontRightTop = QVector3D(frontRightTop1.x(),frontRightTop1.y(),frontRightTop1.z()) / frontRightTop1.w();
        frontLeftBottom = QVector3D(frontLeftBottom1.x(),frontLeftBottom1.y(),frontLeftBottom1.z()) / frontLeftBottom1.w();
        frontRightBottom = QVector3D(frontRightBottom1.x(),frontRightBottom1.y(),frontRightBottom1.z()) / frontRightBottom1.w();

        backLeftTop = QVector3D(backLeftTop1.x(),backLeftTop1.y(),backLeftTop1.z()) / backLeftTop1.w();
        backRightTop = QVector3D(backRightTop1.x(),backRightTop1.y(),backRightTop1.z()) / backRightTop1.w();
        backLeftBottom = QVector3D(backLeftBottom1.x(),backLeftBottom1.y(),backLeftBottom1.z()) / backLeftBottom1.w();
        backRightBottom = QVector3D(backRightBottom1.x(),backRightBottom1.y(),backRightBottom1.z()) / backRightBottom1.w();

        float x1 = min({frontLeftTop.x(),frontRightTop.x(), frontLeftBottom.x(), frontRightBottom.x(),
                       backLeftTop.x(), backRightTop.x(),  backLeftBottom.x(),  backRightBottom.x() });
        float x2 = max({frontLeftTop.x(),frontRightTop.x(), frontLeftBottom.x(), frontRightBottom.x(),
                       backLeftTop.x(), backRightTop.x(),  backLeftBottom.x(),  backRightBottom.x() });

        float y1 = min({frontLeftTop.y(),frontRightTop.y(), frontLeftBottom.y(), frontRightBottom.y(),
                       backLeftTop.y(), backRightTop.y(),  backLeftBottom.y(),  backRightBottom.y() });
        float y2 = max({frontLeftTop.y(),frontRightTop.y(), frontLeftBottom.y(), frontRightBottom.y(),
                       backLeftTop.y(), backRightTop.y(),  backLeftBottom.y(),  backRightBottom.y() });

        float z1 = min({frontLeftTop.z(),frontRightTop.z(), frontLeftBottom.z(), frontRightBottom.z(),
                           backLeftTop.z(), backRightTop.z(),  backLeftBottom.z(),  backRightBottom.z() });
        float z2 = max({frontLeftTop.z(),frontRightTop.z(), frontLeftBottom.z(), frontRightBottom.z(),
                        backLeftTop.z(), backRightTop.z(),  backLeftBottom.z(),  backRightBottom.z() });

        float center_x = (x1 + x2) / 2;
        float center_y = (y1 + y2) / 2;
        QVector3D pos(center_x, center_y, z2);
        QVector4D tmp = lightSpace_inverted * QVector4D(pos.x(), pos.y(), pos.z(), 1);
        pos = QVector3D(tmp.x(), tmp.y(), tmp.z()) / tmp.w();

        lightMatrix[i].setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, pos)}); // x
        lightMatrix[i].setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, pos)}); // y
        lightMatrix[i].setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, pos)}); // z
        lightMatrix[i].setRow(3, {0, 0, 0, 1});

        orthoZFar[i] = z2 - z1;
        lightOrtho[i].setToIdentity();

        // qDebug() << orthoZFar;

        lightOrtho[i].ortho(x1 - center_x, x2 - center_x, y1 - center_y, y2 - center_y, 0, orthoZFar[i]);

    }
}

生成阴影映射

       接下来,我们执行多次阴影映射步骤。此处我使用了四个区域,因此也需要绑定四个shader函数,引用四个fragment shader,这个地方不得不说,OpenGL写起来还是不如DirectX的effect框架好用的:

void Object::GenShadowMap()
{
    for(int i = 0;i < Shadow_Layer; i++)
    {
        GenShadowMap(i);
    }
}

void Object::GenShadowMap(int i)
{
    QOpenGLFunctions* gl = QOpenGLContext::currentContext()->functions();
    int screenX = RenderCommon::Inst()->GetScreenX();
    int screenY = RenderCommon::Inst()->GetScreenY();

    string name = "ShadowMap" + to_string(i + 1);
    QOpenGLShaderProgram* shadowMapProgram = CResourceInfo::Inst()->GetProgram(name);
    shadowMapProgram->bind();

    auto shadowMapFrameBuffer = CResourceInfo::Inst()->CreateFrameBuffer("ShadowMap", screenX, screenY);

    gl->glActiveTexture(GL_TEXTURE0);
    gl->glBindTexture(GL_TEXTURE_2D, shadowMapFrameBuffer->vecTexId[0]);
    shadowMapProgram->setUniformValue("ShadowMap", 0);

    shadowMapProgram->setUniformValue("LightMatrix",   RenderCommon::Inst()->GetLightMatrix(i));
    shadowMapProgram->setUniformValue("OrthoMatrix", RenderCommon::Inst()->GetOrthoMatrix(i));
    shadowMapProgram->setUniformValue("zFar", RenderCommon::Inst()->GetOrthoZFarPlane(i));
    shadowMapProgram->setUniformValue("ModelMatrix",  modelMatrix);
    shadowMapProgram->setUniformValue("IT_ModelMatrix",  IT_modelMatrix);
    Draw(shadowMapProgram);
}

void ObjectInfo::Render()
{
    int screenX = RenderCommon::Inst()->GetScreenX();
    int screenY = RenderCommon::Inst()->GetScreenY();

    // 更新位置
    glClearColor(1,1,1,1);
    for(const auto& obj : vecObjs)
    {
        obj->UpdateLocation();
    }

    // 阴影绘制
    auto shadowMapFrameBuffer = CResourceInfo::Inst()->CreateFrameBuffer("ShadowMap", screenX, screenY);
    glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFrameBuffer->frameBuffer);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    for(const auto& obj : vecObjs)
    {
        if(obj->m_bRender && obj->m_bCastShadow)
        {
            obj->GenShadowMap();
        }
    }

    // ... others
}

        shadowmap.vsh

#version 450 core

uniform mat4 ProjectMatrix;
uniform mat4 LightMatrix;
uniform mat4 ModelMatrix;

in vec4 a_position;
in vec2 a_texcoord;

out vec2 v_texcoord;
out vec2 v_depth;

void main()
{
    v_texcoord = a_texcoord;
    gl_Position = ModelMatrix * a_position;
    gl_Position = LightMatrix * gl_Position;
    v_depth = gl_Position.zw;
    gl_Position = ProjectMatrix * gl_Position;
}

        shadowmap1.fsh

        这里我用了四个fsh,分别写入四个通道里的,这里就不一一列出了。这里还传入了ShadowMap参数是为了避免结果被多次写入覆盖了。

#version 450

uniform sampler2D ShadowMap;
uniform float zFar;
in vec2 v_depth;
in vec2 v_texcoord;

out vec4 fragColor;
void main()
{
    float fColor =  -(v_depth.x/v_depth.y)/zFar;
    fragColor.x = fColor;
    fragColor.y = texture(ShadowMap, v_texcoord).y;
    fragColor.z = texture(ShadowMap, v_texcoord).z;
    fragColor.w = texture(ShadowMap, v_texcoord).w;
}

后处理计算阴影

        计算阴影的时候,我们需要根据当前的深度,到不同的shadowmap采样:

float depth_layer[5] = {0.0, 15.0/zFar, 30.0/zFar, 100.0/zFar, 200.0/zFar};
float GetShadow(vec4 worldPos, float depth)
{
    float fShadow = 0.0;

    int idx = 4;
    for(int i = 0;i < 4;i++)
    {
        if(depth >= depth_layer[i] && depth < depth_layer[i + 1])
        {
            idx = i;
            break;
        }
    }
    mat4 LightMatrix;
    mat4 OrthoMatrix;
    float orthoZFar;
    if(idx == 0)
    {
        LightMatrix = LightMatrix1;
        OrthoMatrix = OrthoMatrix1;
        orthoZFar = orthoZFar1;
    }
    else if(idx == 1)
    {
        LightMatrix = LightMatrix2;
        OrthoMatrix = OrthoMatrix2;
        orthoZFar = orthoZFar2;
    }
    else if(idx == 2)
    {
        LightMatrix = LightMatrix3;
        OrthoMatrix = OrthoMatrix3;
        orthoZFar = orthoZFar3;
    }
    else if(idx == 3)
    {
        LightMatrix = LightMatrix4;
        OrthoMatrix = OrthoMatrix4;
        orthoZFar = orthoZFar4;
    }
    else
    {
        return 0.8;
    }
    vec4 lightPos = (LightMatrix * (worldPos));
    float fDistance = -lightPos.z / orthoZFar;
    lightPos = OrthoMatrix * lightPos;

    vec2 uv = lightPos.xy / lightPos.w * 0.5 + vec2(0.5, 0.5);

    uv.x = clamp(uv.x, 0, 1);
    uv.y = clamp(uv.y, 0, 1);

    float offset = 0.4 / orthoZFar;


    for(int i = -1; i <= 1; i++)
    {
        for(int j = -1; j <= 1; j++)
        {
            float fDistanceMap;
            if(idx == 0) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).x;
            else if(idx == 1) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).y;
            else if(idx == 2) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).z;
            else if(idx == 3) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).w;

            fShadow += fDistance - offset > fDistanceMap ? 0.2 : 0.8;
        }
    }

   fShadow /= 9.0;

    return fShadow;
}

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值