目前计算机无法一次性加载海量数据,必须采用动态调度技术实现大范围三维场景的漫游与应用,随着视点的移动,动态加载一定范围内的三维模型。该项技术如何在海量级的三维数字城市管理中深入应用,在当前计算机硬件条件下实现快速高效的数据加载、渲染与动态浏览,仍是一个难点,也是一个热点,需要不断地研究。
当用户需要浏览的数据量很大,比如地形模拟、虚拟小区和城市等的时候,会对计算机系统产生极大的负担。在内存中可能要存储海量数据,这些海量数据指的是数百GB甚至TB级别的数据(例如中国境内的山形地貌等),这些不可能全部载入内存中,就算未来的计算机能够将它们一次性读入,也已经损耗了太多的系统性能。
一、数据分页动态调度技术
显示三维场景时,视锥体裁剪等方法可以保证每帧中只有一部分数据被传送到渲染管道,而 LOD 可以通过牺牲一部分可以忽略的渲染质量来换取效率的提升,但是这些都无法解决“在内存中可能要存储海量数据”这一问题。
此时数据的分页就显得尤为重要,在显示当前视域中的场景元素的同时,预判下一步可以载入的数据,以及那些短时间内不可能被看到的对象,从而做出正确的数据加载和卸载处理,确保内存中始终维持有限的数据额度,,并且不会因此造成场景浏览时重要信息的丢失或者过于迟缓。
数据的动态调度可以使用多线程的工作方式,使数据的动态调度和场景的实时绘制同时进行。由于动态数据的加载、卸载可能影响到场景树的结构,因此这一工作需要在场景更新阶段完成,以免影响到裁剪和绘制。
二、数据动态调度流程
开源的 OpenSceneGraph(OSG)基于动态调度技术对一些类进行了设计与封装,在一定程度上满足了三维海量数据模型动态调度的需要。
osgDB::DatabasePager是一个OSG库中的类,是分页数据库,实现了分级多线程加载场景数据的机制,用于在运行时动态加载场景数据以提高渲染性能。
OSG使用场景图来表示复杂的三维场景,然而在处理大型场景时,这个图可能会变得相当大。如果所有的场景数据都一次性加载到内存中,可能会导致内存不足的情况发生。因此,为了避免这种情况,OSG使用了一种分级多线程加载场景数据的机制,即osgDB::DatabasePager类。
osgDB::DatabasePager维护了一个场景数据的队列,负责执行场景动态调度的工作。视景器每一帧执行到更新遍历时,都会自动将 “一段时间内始终不在当前页面上”的场景子树去除,并将 “新载入到当前页面”的场景子树加入渲染。这里所说的 “页面”指的就是用户的视野范围。分页和节点管理的工作由 DatabasePager 内置的数据线程 DatabaseThread 负责。场景数据对应的场景图节点会根据其可能性排序加入队列中。然后,多个线程会同时尝试从队列中取出场景数据,并将其加载到内存中。同时,osgDB::DatabasePager还维护了一个可用内存列表,如果应用程序需要更多的内存,osgDB::DatabasePager可以卸载一些较早的场景数据,以便为新的场景数据腾出空间。
数据的动态调度过程可以分解为以下几个环节:
- 删除过期的场景数据: 过期数据指的是那些长时间没有处于用户视域内,并且有理由认为它们不会立即显示的场景元素。场景的更新遍历负责将检索到的过期对象收集并送入到相应的过期对象列表; 而列表中的数据在数据线程中统一删除。
- 获取新的数据加载请求: 请求加载的可能是新的数据信息,也可能是已有的场景数据 ( 曾经从“当前页面”中去除,又重新回到“当前页面”中) 。
- 编译加载的数据: 如果将数据提前进行编译可以有效地提高效率,如为几何数据创建显示列表,以及将纹理对象提前加载到纹理内存,由数据线程负责预编译的工作, 可以大大减少帧的延迟,提高渲染效率。
- 将加载的数据合并至场景图形: 直接由数据线程来完成这一工作显然是不适应的,因为主线程不知道当 DatabaseThread 线程试图操作场景中结点时渲染器在做些什么,最好的方法是将数据预先存在一个链表中,并且由仿真循环负责获取和执行合并新结点的操作。
数据的动态调度流程图如下图所示:
osgDB::DatabasePager动态调度流程
三、实现方式
在OSG中,osgDB::DatabasePager类负责执行场景动态调度的工作。与osgDB::DatabasePager搭配使用的是PagedLOD和osg::ProxyNode。这里主要讨论PagedLOD。
PagedLOD既具有将大量数据或者模型使用细节层次(LOD)原则划分的特性,又有动态调度以保证渲染效率和内存管理的特性。
#动态调度教学示例
笔者在这里用osgViewer显示30*30个模型,每个模型的位置在视口距离处于[200,FLT_MAX)时,显示为白色正方体白模;在[0,200)时显示cow.osg。
白色正方体白模与cow.osg的示意图如下所示:
在这里就初步实现了动态调度,具体调度过程如下图所示:
笔者在下方贴出相关源代码:
osg::Geode* createBox(const osg::Vec3& center, float width)
{
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(
new osg::ShapeDrawable(new osg::Box(center, width)));
return geode.release();
}
osg::Group* createPagedLOD(int row, int col)
{
osg::ref_ptr<osg::Group> root = new osg::Group;
char buffer[5] = "";
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
std::string filename = "cow.osg." + std::to_string(i * 10) + "," + std::to_string(j * 10) + ",0.trans";
osg::ref_ptr<osg::PagedLOD> lod = new osg::PagedLOD;
lod->setCenter(osg::Vec3(i * 10, j * 10, 0.0));
lod->addChild(createBox(osg::Vec3(i * 10, j * 10, 0.0), 1), 200.0, FLT_MAX);
lod->setFileName(1, filename);
lod->setRange(1, 0.0, 200.0);
root->addChild(lod.get());
}
}
return root.release();
}
int main(){
osgViewer::Viewer viewer;
viewer.setSceneData(createPagedLOD(30, 30));
return viewer.run();
}
#动态调度倾斜摄影数据集
给定一个倾斜摄影数据集,同样使用PageLOD进行动态调度,在osgViewer上显示一个海量倾斜摄影数据集。
在Data目录下包含了分块的瓦片数据,每个瓦片都是一个LOD文件夹。osg能够直接读取osgb格式,理论上只需要依次加载每个LOD的金字塔层级最高的osgb,整个倾斜摄影模型数据就加载进来了。不过有点麻烦的是这类数据缺乏一个整体加载的入口,如果每次加载都遍历整个文件夹加载的话,会影响加载的效率。所以一般的数据查看软件都会为其增加一个索引。
这里就给倾斜摄影数据添加一个osgb格式的索引文件,生成后就可以通过OSG直接加载整个倾斜摄影模型数据。
osg::Geode* createBox(const osg::Vec3& center, float width)
{
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(
new osg::ShapeDrawable(new osg::Box(center, width)));
return geode.release();
}
osg::Group* createPagedLOD(int row, int col)
{
osg::ref_ptr<osg::Group> root = new osg::Group;
char buffer[5] = "";
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
std::string filename = "cow.osg." + std::to_string(i * 10) + "," + std::to_string(j * 10) + ",0.trans";
osg::ref_ptr<osg::PagedLOD> lod = new osg::PagedLOD;
lod->setCenter(osg::Vec3(i * 10, j * 10, 0.0));
lod->addChild(createBox(osg::Vec3(i * 10, j * 10, 0.0), 1), 200.0, FLT_MAX);
lod->setFileName(1, filename);
lod->setRange(1, 0.0, 200.0);
root->addChild(lod.get());
}
}
return root.release();
}
int main(){
osgViewer::Viewer viewer;
viewer.setSceneData(createPagedLOD(30, 30));
return viewer.run();
}
上述的代码只是我们实现动态调度时工作的冰山一角。对常见的应用而言,最有效的动态调度方法便是结合按需加载和动态LOD,在保证屏幕主要范围内模型清晰的同时,尽量减少系统的负载。
在Mapmost SDK for WebGL中,无需费心费力的实现动态调度,SDK已实现了常见的动态调度手段,能够自动的按需加载模型,自动根据相机的远近调用合适的LOD层级;同时Mapmost Stuido也支持降普通FBX、glTF、OBJ等模型输出成带有LOD的3DTiles模型,两者相结合使用,简单几步操作即可享受动态调度优化所带来的性能提升。
点击此处前往Mapmost官网体验,自由开发、自由定制的你的工程!如有相关需求也可私信咨询~