您的位置:首页 > 其它

GraphicsLab Project之Parallel Split Shadow Map(PSSM)

2017-04-19 23:25 344 查看
作者:i_dovelemon
日期:2017/04/19
来源:CSDN
主题:Shadow Map, PSSM

引言

        在GLB渲染库中早就集成了Shadow Map的功能,只是在较大的场景中,阴影的效果会很差。所以,我使用了在GPU Gem 3中提到的Parallel Split Shadow Map的方法来改进。这篇文章将不会详细的讲解算法的每一个细节,主要讲述我在实现的过程中遇到的一些问题。

算法概述

        PSSM的算法思路十分的简单。由于我们在渲染的时候,对于较大的场景,仅靠一张Shadow Map保存阴影信息,将会丢失很多的细节。PSSM通过将视域体划分为N个不同的部分,对每一个部分单独的生成一张Shadow Map,这样就能够比较好的保存阴影的信息。所以算法的核心是如何对视域体进行划分。PSSM的划分算法是根据Shadow Map走样的原因提出的,主要分为如下两个部分:
Cs = a Clog + (1 - a)Cuni (0 < a < 1)
其中Clog和Cunim分别是两种不同的划分机制,它们各有优缺点,PSSM将他们按比例进行组合,从而得出更佳有效的划分机制。它们的机制分别如下:
Clog = n(f/n)^(i/m)  (i 表示划分区域的第几个部分, m表示总共划分的数目)Cuni = n + (f - n) * (i / m) (i, m 同上)
        当我们确定好了划分的区域之后,我们就可以为每一个区域分别渲染出一张Shadow Map,渲染Shadow Map的过程和基本Shadow Map的渲染过程一样。当我们准备好了Shadow Map之后,在实际渲染场景的时候,我们计算像素的深度,从而得出应该使用哪一张Shadow Map。

实现

搭建实验环境

        对于这样的算法问题,我们往往需要进行大量的调试,所以很有必要准备好一些必备的工具,帮助我们更快的确认算法的正确性。这里推荐大家准备一套Debug的相机和一些绘制Debug线条的功能。计算PSSM往往需要计算大量的Bounding Box,通过将Bounding Box在3D空间中绘制出来,我们就能够从视觉上判断算法的正确性。同时Debug相机能够让我们在不影响原始相机数据的情况下,观察基于原始相机数据绘制的场景。我的PSSM能够实现,很大程度上借助了这两个Debug功能。

关于光源空间的创建

        光源空间(Light Space)的创建是很容易出错的地方。首先,我们创建光源空间的目的是为了方便计算,计算出紧凑的包围体。但是在不计算出包围体的情况下,我们是不知道光源空间的原点在什么地方,所以我们先假设光源空间的原点在世界坐标的原点。这里我们将使用Parallel Light进行说明。对于Parallel Light来说,我们已经知道了光源在世界坐标下的方向,根据这个就能够计算出光源空间的三个轴在世界坐标空间下表示。计算出三个轴之后,我们就能够得到一个转换矩阵,这个矩阵能够将Light Space空间中的点转换到世界坐标系中去,该矩阵的计算过程如下代码所示:
math::Matrix RenderImp::BuildRefLightSpaceMatrix() {
math::Vector light_dir = -scene::Scene::GetLight(0).dir;  // For opengl, +z point out of the screen
light_dir.w = 0;

// Build light space
math::Vector lit_space_right = math::Cross(light_dir, math::Vector(0.0f, 1.0f, 0.0f));
lit_space_right.Normalize();
math::Vector lit_space_up = math::Cross(light_dir, lit_space_right);
lit_space_up.Normalize();
math::Matrix light_space_to_world_space;
light_space_to_world_space.MakeRotateMatrix(lit_space_right, lit_space_up, light_dir);

return light_space_to_world_space;
}


包围体计算

        计算除了参考光源空间的矩阵之后,我们就可以依次将划分的多个视域体转换到该光源空间中去,转换的方法是将视域体的8个顶点依次转换到光源空间,使用上面计算出来的矩阵的逆矩阵即可实现该功能。当有了划分视域体的8个点在光源空间中的坐标的时候,我们就可以计算出一个在光源空间中紧紧包围该视域体的包围盒,然后根据该包围和计算出一个平行投影的视域体出来,如下代码所示:
void RenderImp::BuildBasicLightFrustums(math::Matrix light_space_to_world_space) {
math::Matrix view_to_world = scene::Scene::GetCamera(scene::PRIMIAY_CAM)->GetViewMatrix();
view_to_world.Inverse();
math::Matrix world_to_light = light_space_to_world_space;
world_to_light.Inverse();
math::Matrix view_to_light;
view_to_light.MakeIdentityMatrix();
view_to_light.Mul(world_to_light);
view_to_light.Mul(view_to_world);
for (int32_t i = 0; i < kPSSMSplitNum; i++) {
math::Vector points[8];
m_SplitFrustums[i].GetPoints(points);

for (int32_t j = 0; j < 8; j++) {
points[j] = view_to_light * points[j];
}

math::AABB bv(points);
bv.m_Max.z += kMaxSceneSize;  // Make frustum include all potential shadow caster objects

points[Render::NLU] = math::Vector(bv.m_Min.x, bv.m_Max.y, bv.m_Max.z);  // NLU
points[Render::NLD] = math::Vector(bv.m_Min.x, bv.m_Min.y, bv.m_Max.z);  // NLD
points[Render::NRU] = math::Vector(bv.m_Max.x, bv.m_Max.y, bv.m_Max.z);  // NRU
points[Render::NRD] = math::Vector(bv.m_Max.x, bv.m_Min.y, bv.m_Max.z);  // NRD
points[Render::FLU] = math::Vector(bv.m_Min.x, bv.m_Max.y, bv.m_Min.z);  // FLU
points[Render::FLD] = math::Vector(bv.m_Min.x, bv.m_Min.y, bv.m_Min.z);  // FLD
points[Render::FRU] = math::Vector(bv.m_Max.x, bv.m_Max.y, bv.m_Min.z);  // FRU
points[Render::FRD] = math::Vector(bv.m_Max.x, bv.m_Min.y, bv.m_Min.z);  // FRD

m_LightSpaceFrustum[i].Build(points);
}
}

        其中m_SplitFrustum保存了划分的视域体的信息,为了方便进行划分,这里面保存的实际上view空间中的坐标值,所以我们需要构建一个view_to_light的坐标转换,从而将该划分的视域体转换到光源空间中去。同时还要注意,如果仅仅是把划分的视域体进行了包围,然后依据这个构造Shadow Map会出现问题。比如说,有些物体是在视域体中不可见的,但是它的影子却会投向视域体里面,所以,我们需要将所有潜在的可能投影到视域体里面的物体也包含到我们构造的平行投影的视域体中去,这就上面代码中我为什么会加入:
bv.m_Max.z += kMaxSceneSize;  // Make frustum include all potential shadow caster objects

这一行代码的原因。kMaxSceneSize是一个比较大的数值,尽量能够将贯穿整个场景。
        上面的代码计算出来的平行投影视域体并不是最终的,我们需要根据Caster(投影的物体)和Reciever(接受投影的物体)的包围盒来调整。
void RenderImp::ShrinkLightFrustum(math::AABB casters, math::AABB receivers, Frustum& frustum) {
math::AABB frustum_bv = frustum.GetBoundBox();

frustum_bv.m_Min.x = std::max<float>(std::max<float>(receivers.m_Min.x, casters.m_Min.x), frustum_bv.m_Min.x);
frustum_bv.m_Max.x = std::min<float>(std::min<float>(receivers.m_Max.x, casters.m_Max.x), frustum_bv.m_Max.x);

frustum_bv.m_Min.y = std::max<float>(std::max<float>(receivers.m_Min.y, casters.m_Min.y), frustum_bv.m_Min.y);
frustum_bv.m_Max.y = std::min<float>(std::min<float>(receivers.m_Max.y, casters.m_Max.y), frustum_bv.m_Max.y);

frustum_bv.m_Min.z = std::max<float>(receivers.m_Min.z, frustum_bv.m_Min.z);
frustum_bv.m_Max.z = std::max<float>(casters.m_Max.z, frustum_bv.m_Max.z);

math::Vector points[8];
points[Render::NLU] = math::Vector(frustum_bv.m_Min.x, frustum_bv.m_Max.y, frustum_bv.m_Max.z);  // NLU
points[Render::NLD] = math::Vector(frustum_bv.m_Min.x, frustum_bv.m_Min.y, frustum_bv.m_Max.z);  // NLD
points[Render::NRU] = math::Vector(frustum_bv.m_Max.x, frustum_bv.m_Max.y, frustum_bv.m_Max.z);  // NRU
points[Render::NRD] = math::Vector(frustum_bv.m_Max.x, frustum_bv.m_Min.y, frustum_bv.m_Max.z);  // NRD
points[Render::FLU] = math::Vector(frustum_bv.m_Min.x, frustum_bv.m_Max.y, frustum_bv.m_Min.z);  // FLU
points[Render::FLD] = math::Vector(frustum_bv.m_Min.x, frustum_bv.m_Min.y, frustum_bv.m_Min.z);  // FLD
points[Render::FRU] = math::Vector(frustum_bv.m_Max.x, frustum_bv.m_Max.y, frustum_bv.m_Min.z);  // FRU
points[Render::FRD] = math::Vector(frustum_bv.m_Max.x, frustum_bv.m_Min.y, frustum_bv.m_Min.z);  // FRD
frustum.Build(points);
}

        上面的代码基本上就是实现了GPU Gems 3中提到的缩小平行投影视域体的相关代码。

        最终得到了如下的结果:



总结

        算法的原理其实很简单,不过实现起来是一个工程问题,需要慢慢的调试,慢慢的调整,大家一定要有耐心。

参考文献

[1] https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html  GPU Gem3 Chapter 10 Parallel Split Shadow Map on Programmable GPUs
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Shadow Map PSSM 3D