GraphicsLab Project之Parallel Split Shadow Map(PSSM)
来源:互联网 发布:生化奇兵 海葬 知乎 编辑:程序博客网 时间:2024/06/15 20:24
作者: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); }}
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
0 0
- GraphicsLab Project之Parallel Split Shadow Map(PSSM)
- GraphicsLab Project之HDR渲染
- GraphicsLab Project之Color Processing
- GraphicsLab Project之Normal Mapping
- GraphicsLab Project学习项目
- GraphicsLab Project之辉光(Glare,Glow)效果
- GraphicsLab Project之Screen Space Ambient Occlusion(SSAO)
- Shadow Map & Shadow Volume
- Shadow Map & Shadow Volume
- Shadow Map & Shadow Volume
- Shadow学习笔记(PSM,LiSPSM,TSM,PSSM,CSM)
- shadow map
- shadow map
- Shadow Map阴影贴图技术之探
- Shadow Map阴影贴图技术之探
- Shadow Map (Not Shadow Texture :-)! )
- Shadow Map阴影贴图技术之探Ⅰ
- shadow map 阴影贴图技术之探 4
- SpringMVC 基于注解的Controller详解
- HTML5第六课时,padding内边距
- linux目录为树结构
- Socket 通讯学习
- 利用ajax提交表单,实现数据前端后台数据交互的完整流程演示
- GraphicsLab Project之Parallel Split Shadow Map(PSSM)
- 缓存的基本知识
- 美丽的心形函数
- hive常用语句示例
- Java数组是存储在内存中的什么地方
- 4月19日,TestCommunicate,每日20行。
- Aizu
- HTML5第六课时,布局的简单应用
- postgresql数据库备份和恢复