PageLOD(Page Level of Detail)
是Open Scene Graph(OSG)库中的一种LOD(Level of Detail)技术,用于管理和显示具有不同细节级别的场景数据。PageLOD模型通常用于处理大型地理空间数据,如地形或城市模型。它允许将场景分成多个页面(或块),每个页面可以包含不同级别的细节数据,以便根据观察者的位置和视野来动态加载和卸载这些页面,以提高渲染性能。
PageLOD模型通常由多个文件组成,每个文件对应于一个页面或块,并包含该页面的不同细节级别的数据。这些文件通常使用OSG的节点和模型格式(例如.osgb、.osg、.ive等)来表示。示例的文件结构如下所示(大疆智图、ContextCaptureCenter等软件生成的osgb模型都是按此格式存储的。):
- MyScene/
- root.osgb
- Page_0/
- lod0.osgb
- lod1.osgb
- lod2.osgb
- Page_1/
- lod0.osgb
- lod1.osgb
- lod2.osgb
- Page_2/
- lod0.osgb
- lod1.osgb
- lod2.osgb
root.osgb是根节点,其中存储了Page_0
、Page_1
、Page_2
三个页面的路径信息,这三个页面构成了整个场景。而这三个页面各自对应一个文件夹,下面是0、1、2细节级别的同一场景的几何数据。加载模型时,OSG库根据视点距离和视野范围选择每个页面中某个细节级别的模型加载到显存,并渲染显示。当视点距离模型特别远时,就加载最粗的一级数据,以提高渲染效率;当距离模型足够近时,才加载最精细的模型数据,满足用户的应用精度需求。
PageLOD格式保存场景有一点需要特别注意,那就是——只将root.osgb路径给osgviewer.exe,它是如何读取到其他.osgb文件的?
答案是,root.osgb中存储了各个页面的相对路径。因为,如果存储的是绝对路径,将这个文件夹拷贝到其他目录就无法加载页面数据了。
下载地址
建议直接下载编译好的库,方便配置。
与其他C++库一样,在VS中设置好包含路径、库目录,并链接所需的.lib文件,即可使用。
有时生成的osgb模型坐标是地图投影坐标系中的坐标,X和Y值特别大。OSG模型中坐标仅用32位浮点保存,所以XY坐标太大时会造成场景中拾取坐标不准确,因此需要将这样的模型作一平移,减小X和Y的值。
root.osgb中往往包含一个osg::MatrixTransform
类型的节点。正是这个节点描述了整个场景的平移、旋转。因此,我们要平移一个PageLOD模型,就需要设法把root.osgb中的这个节点修改一下。
我只想让模型中心位于原点,因此采取的做法是:加载模型—获取MatrixTransform信息—获取矩阵中的平移向量—将平移向量置为0。
代码如下:
bool TransModel(const char* strOsgb, const char* strDestOsgb)
{
osg::ref_ptr<osg::Node> loadedModel = osgDB::readNodeFile(strOsgb);
if (loadedModel.valid()) {
osg::ref_ptr<osg::MatrixTransform> matrixTransform = nullptr;
osg::ref_ptr<osg::Group> grp = loadedModel->asGroup();
if (grp->getNumChildren() == 1) {
osg::ref_ptr<osg::Node> node = grp->getChild(0);
matrixTransform = dynamic_cast<osg::MatrixTransform*>(node.get());
if (matrixTransform.valid()) {
// 找到了MatrixTransform节点
osg::Matrix mt0 = matrixTransform->getMatrix();
#pragma region Keep the old translation.
osg::Vec3d vTrans = mt0.getTrans();
char strTranslation[256];
strcpy(strTranslation, strDestOsgb);
char* pDot = strrchr(strTranslation, '.');
*pDot = '\0';
strcat(strTranslation, ".txt");
FILE* fp = fopen(strTranslation, "wt");
fprintf(fp, "%lf,%lf,%lf \n", vTrans.x(), vTrans.y(), vTrans.z());
fclose(fp);
#pragma endregion
mt0.setTrans(0, 0, 0);
matrixTransform->setMatrix(mt0);
}
return true;
}
}
return false;
}
如果模型没有osg::MatrixTransform
类型的节点,则需要获取所有图元顶点数据,修改每个点的坐标值,再将它保存下来。这种做法就麻烦很多,一般不会用到。
osg::PagedLOD
类型的节点都有_databasePath
成员变量。这个成员变量就是程序自动查找不同细节级别模型数据的路径。一般将它置为空,表示从当前文件同级目录查找模型数据。
主流商业软件生成的PageLOD结构的osgb模型中,根节点(也就是文件夹最外层的.osgb模型)都以相对路径方式记录了各页面的最粗级别数据,例如root.osgb
中就会记录'/Page_0/lod0.osgb'
、'/Page_1/lod0.osgb'
、'/Page_2/lod0.osgb'
,而DatabasePath数据都是空字符串。
但是,程序读取根节点时,会自动将根节点所在路径赋予每个以根节点为parent的osg::PagedLOD
类型节点的_databasePath
变量。
保存模型之前还需要设置DatabasePath
为空,以保证这个模型拷贝到其他地方也能正常被打开、渲染。DatabasePath数据库路径,将它置为空,表示从当前文件同级目录查找模型数据。
而不是'D:/MyDataPath/MyScene/Page_01/lod0.osgb'
这样的绝对路径。但是一旦加载到内存中,便会自动将数据的相对路径转为绝对路径,因此直接默认保存模型必定会存储各页面的绝对路径,即额外把'D:/MyDataPath/MyScene/'
存储到root.osgb
中。这样一来,即便成功地平移了模型,也无法将它和页面文件一同拷贝到其他目录使用。因为程序只会到D:/MyDataPath/MyScene/
下加载页面数据,而不会到同级目录下查找加载页面数据。
设置DatabasePath的代码如下:
传入osg::Node*类型的节点,然后递归地遍历所有PageLOD类型子节点,并调用其setDatabasePath方法将DatabasePath设为空字符串。
/// 递归地设置LOD模型中每个节点中的DatabasePath为空,以使从相对路径查找页面文件。
void SetLodDBPath(osg::Node* nd) {
/// here you have found a group.
osg::Geode* geode = dynamic_cast<osg::Geode*> (nd);
if (geode) { // SetLodDBPath the geode. If it isnt a geode the dynamic cast gives NULL.
//Do nothing.
}
else {
osg::Group* gp = dynamic_cast<osg::Group*> (nd);
osg::PagedLOD* pagedLodNode = dynamic_cast<osg::PagedLOD*>(nd);
if (pagedLodNode)
{// 多层次的osgb模型
int numChild = pagedLodNode->getNumChildren();
for (int i = 0; i < numChild; ++i)
{
osg::ref_ptr<osg::Node> childNode = pagedLodNode->getChild(i);
SetLodDBPath(childNode);
}
pagedLodNode->setDatabasePath("");// To Use Relative Path.
}
else if (gp) {
unsigned int nChild = gp->getNumChildren();
osg::notify(osg::WARN) << "Group Name: " << gp->getName() << "| Number of Children: " << nChild << std::endl;
if (nChild > 0)
{
for (unsigned int ic = 0; ic < nChild; ic++) {
SetLodDBPath(gp->getChild(ic));
}
}
}
else osg::notify(osg::WARN) << "Unknown node " << nd << std::endl;
}
}