找回密码
 立即注册
查看: 433|回复: 4

UE4中预计算遮挡剔除(PVS)及UnrealLightsmass数据 ...

[复制链接]
发表于 2021-12-29 13:16 | 显示全部楼层 |阅读模式
引言

上回我在遮挡剔除的主流方案[1]里面提及到了PVS(Potentially Visible Sets)这一词,介绍了在1990年Airey针对建筑物内部稠密遮挡场景,最早提出了采用潜在可见集PVS(Potentially Visible Sets)解决消隐问题[2]。并且概述了它的原理:该方法首先将场景分解成单元格,然后为这些单元格形成邻接图,并为每个单元(Cell)建立一个PVS,之后绘制仅与PVS相关联的几何对象,并采用标准的z-buffer完成准确的消隐。随后我又在Unreal Engine里面的可见性和遮挡剔除[3]的文章里提及到了UE里面的预计算遮挡剔除PVS(Precomputed Visibility Systerm)这一技术,通过实验的方式介绍了如何使用UE的预计算遮挡剔除PVS这一技术,但是我想上面的这一些都是一些理论的知识或者是成熟的工具模块,我们虽然会用但是不太清楚它的底层逻辑到底是怎么运行的。如果我们要继续优化预计算遮挡剔除PVS这一技术,我们有必要通过源码了解它的底层逻辑。
我们知道预计算遮挡剔除需要先把场景分解成多个Cell,进行光线采样和光线投射后,预先计算好每个Cell能够看见什么物体,看不到哪些物体,将信息序列化后保存并打包进游戏中。在运行的时候,根据相机当前的位置,读取所在的网格的可见性信息,在CPU设置可见性并提交绘制[4]。在UE4里面的预计算遮挡剔除PVS中我们可以设置Size的大小,然后进行可见性计算,计算完成后我们自然而然的会好奇Cell里面记录了那些数据格式以及怎么存储呢?以及是如何进行光线采样和光线投射来计算可见性的呢?
1、Cell里面的数据存储格式

在回答Cell里面记录了那些数据格式以及怎么存储之前,我想介绍一下LightMass里面的数据格式,为什么要介绍一下这个LightMass里面的数据格式的数据格式呢,是因为UE4中的可见性预计算PrecomputedVisibility.cpp就在UnrealLightsmass里面。LightMass烘焙的场景数据是从自定义的中间文件格式读取而不依赖UE4编辑器和Runtime的数据组织方式,LightMass依赖的中间数据格式定义在MaterialExport.h、MeshExport.h和SceneExport.h[5]
1.1LightMass数据格式
(1)SceneExport.h
SceneExport.h这里面记录很多数据比如FSceneFileHeader所包含的数据,它主要包括烘焙的相关设置的数据、该场景中各类烘焙源和目录数据的概要信息,如源模型、ImportanceVolumes 、各类灯光个数,各类LightMapping个数等[5]但是这不是我们关心的重点,我们关心的是PrecomputedVisibility.cpp定义的数据类型,如下图所示:



FPrecomputedVisibilitySetting里面定义的数据类型,在虚幻引擎根目录/Engine/Config/BaseLightmass.ini文件中有这些参数的默认值

简单说明一下MeshBoundsScale为什么要设为1.2,因为在查询Cell可见性时增加MeshBoundsScale可以减少一些可见性错误,这是UE4里面的一种保守的方法来应对隐式表面的遮挡计算(无论多少次的光线投射,都无法完全确信不可见)。



MeshBoundsScale扩大为1.2

至于PlayAreaHeigh这个值我们是可以在Engine/Config/BaseLightmass.ini修改的,NumCellSamples这个值是定义了Cell可以取多少点发射光线,至于怎么采样取点发射光线我在下面会详细解释以及为什么需要MinMeshSamples和MaxMeshSamples这两个参数后面都会一一介绍。
(2)MeshExport.h
MeshExport.h:几何Mesh信息从功能上来分包括:Mesh的World变换、LOD相关信息,网格基本信息(顶点和三角形索引信息),Mesh上渲染相应的Flag(如是否投影,是否双面,是否自阴影==,具体参见EMeshInstanceLightingFlags)等如下图所示。



MeshExport.h

在这里我将详细介绍一下Mesh这个数据类型:Mesh包括定点位置vertices和索引Indices以及UV坐标和法线Normal。从图形学的角度讲解了什么是顶点,以及Mesh是如何组织顶点。
图形学的一个环节是建模[6],这里的建模和美术所说的建模类似,主要在于如何表示一个物体。计算机中,要绘制一个物体和现实生活中一样,由点构成线,再由线构成面,最后由若干个面构成一个物体。例如下图中,左图利用三个点绘制了一个三角形,而右图则通过两个三角形得到了一个四边形:



这里需要注意的是,很明显从左图中给定的三个点,有两种方式可以得到三角图元

如果单纯使用顶点来表示(绘制)图形,是非常直观的——因为我们知道每一个顶点的位置信息,并且知道每三个点构成一个三角图元。然而从上面绘制四边形的数据中可以发现,三角图元1中有两个顶点的位置信息(p1和p3)和三角图元0是一样的。这也就增加了一些开销。假设有n个面,而且每个面之间都是两两相连的(这要求n>=3),那么也就会多增加 2 * n个顶点的开销。如果是三维的Vector3(3 * 4 byte= 12 byte),将会增加 2 * n * 12 byte = 24 * n byte的开销。而一个模型少说都有上千个面,更不用说一个顶点不止保存有位置信息了(其他还会有法线信息,贴图纹理坐标信息等)[6]。使用索引顶点triangles以后,vertices就不出现重复的数据了(当面数增加时,使用索引绝对可以减少存储开销,同时也方便管理)。
贴图纹理坐标UV用以指示某个顶点应该从哪里进行纹理采样。法线同样是一个非常重要的顶点属性,它不仅说明了面片的朝向,还可用于计算光照模型(这两个其实说的同一件事情)。
我还想补充一点就是在调用AddMeshDebugLines这个函数时会Draw the bounding boxes of each mesh and the combined bounds如下图:



蓝色的长方体线框就是实际用于计算的- AABB Bounds,黄色球体框-Mesh LOD

(3)MaterialExport.h
除FMaterialData之外,材质的四张纹理就组成了完整的LightMass材质,这四张纹理是自发光、漫反射、透明度和法线图,纹理格式都是ARGB格式-4*8为32位。


1.2、Cell里面的数据存储格式

首先在UnrealLightmass::PrecomputedVisibility.cpp里面,我们找到CalculatePrecomputedVisibility这个函数,就可以在<FPrecomputedVisibilityData>的容器中看到PrecomputedVisibilityCells里面存储的数据格式,我们可以看到VisibilityData里面储存的是Mesh的Id,并且是以二进制方式存储的(信息存储量主要度量单位是字节,1个字节(Byte)等于8位(b)二进制。位(bit,Binary Digits):存放一位二进制数,即0或1,为最小的存储单位,8个二进制位为一个字节单位。)(如下图)。



CurrentCell.VisibilityData数据储存格式及处理流程

虽然我们知道Cell里面存储的是Mesh的Id,但是系统是怎么判定bool IsMeshVisible呢?,这个时候我们就要解读AddZeroed(VisibilityMeshes.Num() / 8 + 1)和return (VisibilityData[MeshId / 8] & 1 << (MeshId % 8)) != 0这两段段关键代码,假设整个Cell里面关联了9个Mesh,我们用0和1表示不可见和可见。我们根据AddZeroed函数可以把MeshId分成两块,当我们要判断第MeshId为9的可见性时,([9/8]=1 )&1 <<(9%8),其中[]是取整符号<<是位运算符,(1&1 <<1)==9,这个时候我们就可以得到MeshId为9的可见性。(如下图)


2、UE4光线采样的底层逻辑源码

上半部分我们已经知道Cell里面存储的是MeshId,但是我们应该会有所疑惑怎么来判断MeshId是否可见呢?其实理论上我们可能已经知道可能是Cell向场景中进行射线投射,但是在哪里进行采样取点呢发射多少条射线呢?在没有看UE4光线采样与射线投射源码之前,我猜想的是另一种方案,在Cell里面按照某种规则采样,然后再按照某种规则发射相应的射线,最后与空间场景做相交测试,这个猜想中比较难确定的就是发射多少条射线以及他们的角度问题(如下图)。



起初没看源码我的猜想是在Cell里面取点,然后按照某种函数确定光线的数量和角度,最后与空间场景做相交测试

后来看了UE4光线采样与射线投射源码,发现他是另一种逻辑进行光线采样与射线投射,区域网格一般是轴对齐包围盒AABB,遮挡物是遮挡物本身模型,为了简化计算,被遮挡物会也会采用包围盒OBB甚至轴对齐包围盒AABB来求解,但是怎么从AABB到一个OBB”的表面采样光线呢?
首先我们计算CenterCellPosition(Cell的中心点),然后再求出MeshBox.GetCenter(MeshBox的中心点),然后这两个中心点相减就是一个向量(MeshToCellCenter),于此同时我们可以求出这两个中心点的距离(const float Distance),这个时候需要做一个点积操作来确定Cell中那些面是可见的面(如下代码)
for (int32 i = 0; i < 6; i++)
        {

         //        return V1.X*V2.X + V1.Y*V2.Y + V1.Z*V2.Z;         float Dot3(const FVector4& V1, const FVector4& V2)
               
                const float DotProduct = -Dot3((MeshToCellCenter / Distance), CellFaces.FaceDirection);
                if (DotProduct > 0.0f)                //点积>0认为其为可见面,具体数值作为概率密度函数(PDF)
                {
                        VisibleCellFaces.Add(i);
                        VisibleCellFacePDFs.Add(DotProduct);       
                }
        }这个点积DotProduct的值用来进行概率密度函数(PDF-Probability Density Function)和累积分布函数 (CDF-Cumulative Distribution Function)的计算。从给定步骤概率分布函数Sample1dCDF()生成样本。这个时候我们就可以确定Cell可见面上的采样点并作为光线的起点(配置文件中的NumCellSamples=24)。
//从给定步骤1d概率分布函数生成a Sample
// Generates a Sample from the given step 1D probability distribution function
Sample1dCDF(VisibleCellFacePDFs, VisibleCellFaceCDFs, UnnormalizedIntegral, RandomStream, PDF, Sample);       
//在其可见面上用CDF生成采样点作为光线起点
const int32 ChosenCellFaceIndex = FMath::TruncToInt(Sample * VisibleCellFaces.Num());
const FAxisAlignedCellFace& ChosenFace = CellFaces[VisibleCellFaces[ChosenCellFaceIndex]];
//Cell生成的Sample position
NewSample.CellPosition = ChosenFace.FaceMin + ChosenFace.FaceExtent * FVector4(RandomStream.GetFraction(), RandomStream.GetFraction(), RandomStream.GetFraction());
同理我们也可以按这个思路确定OBB上的可见面以及采样点作为光线的终点,有点不一样的是Mesh的采样数量加入一个SizeRatio 这个参数(SizeRatio = MeshSize / Distance) ,这样光线数量会根据Mesh的大小和距离来判定(如下代码),前面提及到的配置文件中的MaxMeshSamples和MinMeshSamples就是在这里和SizeRatio一起来确定NumMeshSamples的数量。
        const float MeshSize = MeshBox.GetExtent().Size();
        const float SizeRatio = MeshSize / Distance;        //光线数量会根据Mesh的大小和距离来判定
        //return X<Min ? Min : X<Max ? X : Max;----Clamp
        //Mesh的采样数量---func(SizeRatio,MaxMeshSamples,MinMeshSamples)
        const int32 NumMeshSamples = FMath::Clamp(FMath::TruncToInt(SizeRatio * PrecomputedVisibilitySettings.MaxMeshSamples), PrecomputedVisibilitySettings.MinMeshSamples, PrecomputedVisibilitySettings.MaxMeshSamples);

得到光线的起点和终点以后拿这个光线去做测试,在物理引擎中与全场景做相交测试。如果相交认为起点到终点不可见,如果不相交认为起点到终点可见。当然UE4里面还有几种保守的策越来计算可见性
(1)bool bVisible = SizeRatio > 1.0f----这条策略是说如果Mesh相对离Cell足够近和大的话,认为可见。
(2)Generate samples for explicit visibility sampling of the mesh ----显示可见性策略(两个for循环,最外层是CellSampleIndex < PrecomputedVisibilitySettings.NumCellSamples,最内层是MeshSampleIndex < NumMeshSamples,按照上述的原理得到光线的起点和终点,在物理引擎中与全场景做相交测试)。射线数量在(24*14--24*40)因为lightmass.ini配置文件中的NumMeshSamples=24、MaxMeshSamples=40、MinMeshSamples=14。
(3)隐式表面的遮挡计算策略--无论多少次的光线投射,都无法完全确信不可见,将其中最长的光线,偏转一个小角度(FMath::Cos(2.0f * PI / 180.0f)),如果能够到达目前长度的8/7,认为被遮挡物可见。



将其中最长的光线,偏转一个小角度,如果能够到达目前长度的8/7,认为被遮挡物可见。

总结:
以上就是我关于UE4预计算遮挡剔除(PVS)中Cell的数据储存格式及光线采样和光线投射源码的一些逻辑流程,预计算遮挡剔除技术可以在PC端用空间换时间来提高渲染优化,当然还可以用在移动端尤其在显存带宽和图形API的限制下,因此预计算遮挡剔除技术仍旧能够在部分应用场景中作为可选的方案之一。在我们了解PVS的底层逻辑以后,我们可以创新性的把预计算遮挡剔除(离线渲染)用到其他的领域,尤其是当今火热的智慧城市以及数字孪生城市,把GIS和虚幻引擎结合打造虚拟即现实的感觉,我们RISC研究院在全国率先实现全深圳市域、高精度实景三维空间底板应用。此次空间平台在全国率先实现了全市域覆盖优于5cm精度的倾斜摄影实景三维空间底板并率先在政府推广应用,实现了城市大场景、多源异构数据高效动态加载、高逼真渲染和空间分析[7]。
当然想要做到城市级场景PVS的计算,还有很多地方优化,最为关键的是区域网格划分这一块,UE4里面的预计算遮挡剔除的Cell Size大小总是固定的,但是在整个大场景中不同的区域Cell Size大小应该不一样,举个最简单的列子,对于城市区域这种高密集的地方Cell Size应该小一些,而对于平原草地开阔地带Cell Size应该大一点,因此场景自适应性的格网划分是城市级场景PVS的计算和优化的关键,后续我也将继续研究这个并希望有机会和大家一起学习游戏引擎一起探索城市级场景的高质量渲染。
参考


  • ^遮挡剔除的主流方案https://zhuanlan.zhihu.com/p/441754705
  • ^Towards Image Realism with Interactive Update Rates-Airey 1990
  • ^Unreal Engine里面的可见性和遮挡剔除https://zhuanlan.zhihu.com/p/441905690
  • ^UE预计算遮挡剔除(PVS)全解析https://zhuanlan.zhihu.com/p/266592981
  • ^abLightMass源码分析之中间数据格式https://zhuanlan.zhihu.com/p/74480132
  • ^ab从图形学认识Unity中的Mesh
  • ^郭仁忠院士 加“数”前行, 织密“智慧城市”之网http://www.gd.xinhuanet.com/zt20/ftym/szssn/grz/index.htm

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-12-29 13:22 | 显示全部楼层
有几个疑问。
1。 UE4 的Cell 大小是长宽固定 高是变化的吧?
2. Cell的 采样点和 被遮挡物的采样点 数量不一样,如何选择光线?
3。生成数据 如何进行压缩的?比如相邻 格子数据一样或者基本差不多的时候有没有合并之类的。
发表于 2021-12-29 13:27 | 显示全部楼层
1、UE4遮挡计算遮挡剔除中的Cell是一个Volume,PlayAreaHeight=220是可以在BaseLightmass.ini文件中修改的。
2、把点积的具体数值作为概率密度函数(PDF),根据PDF进行重要性采样在AABB上得到光线的起点,在OBB上得到光线的终点,就可以进行光线投射了。
3、计算完成之后以二进制方式存储每个Mesh的可见性。实际上如果可见性数据中存在大量的0和大量的1相连,数据压缩就有更大的发挥空间。反过来也可以启发在被遮挡物在排列序号的时候,应该尽量让聚集的模型的序号连在一起。
发表于 2021-12-29 13:33 | 显示全部楼层
第二点没看明白,cell 采样10个点,作为光线的起点,被遮挡物采样20个点,作为光线的终点。。10个起点20个终点,没有对应上啊。。。这个ue4 是如何做的?
发表于 2021-12-29 13:36 | 显示全部楼层
可以细看一下我文章,文章详细解读了。那是两个for循环,10*20那就是
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 10:41 , Processed in 0.095439 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表