📜 ⬆️ ⬇️

OpenSceneGraph: Levels of Detail (LOD) and Background Loading of Objects

image

Introduction


One of the most interesting tasks solved by three-dimensional graphics is the creation of “big worlds” - extended scenes containing a large number of objects with the possibility of unlimited movement around the scene. The solution to this problem rests on the understandable limitations inherent in computer hardware.

A typical example: the “big world” in the visualization of the railway on the OSG engine. What is missing is the Langoliers, devouring the world after the train ...


In this regard, there is a need to manage application resources, which boils down to an obvious solution: loading only those resources (models, textures, and so on) that are necessary to form the scene at the current time at the current observer position; reducing the detail levels of remote objects; unloading of unnecessary objects from the memory system. For the most part, graphics and game engines provide some set of tools for solving such problems. Today we look at which of them are available in OpenSceneGraph.

1. Using levels of detail (LOD)


The technique of using levels of detail allows you to display the same object more or less in detail, depending on the distance from it to the observer. The use of this technique is based on the simple consideration that small details of a three-dimensional model are indistinguishable at a great distance, which means there is no need to draw them. On the one hand, this technique allows reducing the total number of geometric primitives output to the frame buffer, and on the other hand, it does not lose the display range of scene objects, which is very useful when creating large open worlds.

OSG provides tools for implementing this trick through the osg :: LOD class, inherited from all the same osg :: Group. This class allows you to represent the same object in several levels of detail. Each level of detail is characterized by a minimum and maximum distance to the observer, subject to which the display of the object in this level of detail is switched.

osg :: LOD allows you to set this range immediately when setting a child node, or later, using the setRange () methods

osg::ref_ptr<osg::LOD> lodNode = new osg::LOD; lodNode->addChild(node2, 500.0f, FLT_MAX); lodNode->addChild(node1); . . . lodNode->setRange(1, 0.0f, 500.0f); 

We continue to torture Cessna and illustrate the technique described by example.

Lod example
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/LOD> #include <osgDB/ReadFile> #include <osgUtil/Simplifier> #include <osgViewer/Viewer> #endif 

main.h

 #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> modelL3 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> modelL2 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osg::ref_ptr<osg::Node> modelL1 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osgUtil::Simplifier simplifer; simplifer.setSampleRatio(0.5f); modelL2->accept(simplifer); simplifer.setSampleRatio(0.1f); modelL1->accept(simplifer); osg::ref_ptr<osg::LOD> root = new osg::LOD; root->addChild(modelL1.get(), 200.0f, FLT_MAX); root->addChild(modelL2.get(), 50.0f, 200.0f); root->addChild(modelL3.get(), 0.0f, 50.0f); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


To begin, load the model

 osg::ref_ptr<osg::Node> modelL3 = osgDB::readNodeFile("../data/cessna.osg"); 

Now it is necessary to generate several (we limit ourselves for the example to two) models, with a lower level of detail. To do this, copy the loaded node twice, using the method of the so-called "deep" copy of the class, for the node implemented by the clone () method

 osg::ref_ptr<osg::Node> modelL2 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osg::ref_ptr<osg::Node> modelL1 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); 

Now we reduce the geometry of these models using the class osgUtil :: Simplifer. The degree of simplification of the model is set by the setSampleRatio () method of this class - the smaller the passed parameter, the less detailed the model will be after applying the reduction procedure

 osgUtil::Simplifier simplifer; simplifer.setSampleRatio(0.5f); modelL2->accept(simplifer); simplifer.setSampleRatio(0.1f); modelL1->accept(simplifer); 

When we have models of different levels of detail, we can load them into the root node, created as a smart pointer to osg :: LOD. For each level of detail, set the display distance of this level.

 osg::ref_ptr<osg::LOD> root = new osg::LOD; root->addChild(modelL1.get(), 200.0f, FLT_MAX); root->addChild(modelL2.get(), 50.0f, 200.0f); root->addChild(modelL3.get(), 0.0f, 50.0f); 

By FLT_MAX is meant in some way an “infinitely” long distance to the observer. After launching the viewer, we get the following picture.

Level of Detail 3



Level of Detail 2



Level of Detail 1



You can see how the detail of the displayed geometry decreases when the camera is removed from the object. Using this technique, you can achieve high realism of the scene with a low consumption of resources.

2. Technique of background loading of scene nodes


In the OSG engine, the osg :: ProxyNode and osg :: PagedLOD classes are provided for balancing the load when rendering a scene. Both classes are inherited from osg :: Group.

A node such as osg :: ProxyNode reduces the time it takes to start an application before rendering, if there is a huge amount of disk-loading and displayable models in the scene. It works as an interface to external files, allowing deferred loading of models. To add child nodes, use the setFileName () method (instead of addChild) to set the name of the model file on disk and load it dynamically.

The osg :: PagedNode node inherits the osg :: LOD methods and loads and unloads detail levels in such a way as to avoid overloading the OpenGL pipeline and ensure a smooth drawing of the scene.

3. Dynamic (runtime) model loading


Let's see how the model load process is performed using osg :: ProxyNode.

Proxynode example
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/ProxyNode> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::ProxyNode> root = new osg::ProxyNode; root->setFileName(0, "../data/cow.osg"); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 


The download process is a bit different here.

 osg::ref_ptr<osg::ProxyNode> root = new osg::ProxyNode; root->setFileName(0, "../data/cow.osg"); 

Instead of explicitly loading the cow model, we specify the root node of the file name, which contains the model and the index of the child node where this model should be placed after it is loaded. When you run the program, we get this result



It can be seen that the point of view is not chosen the best way - the camera rests directly on the mirror side of the cow. This happened because the model was loaded after the launch of the render and initialization of the camera, when node 0 was not yet visible. The viewer simply could not calculate the necessary parameters of the camera. However, the model has loaded and we can configure its display mode by mouse manipulations.



What happens in the above example? osg :: ProxyNode and osg :: PagedLOD work in this case as containers. OSG's internal data manager will send requests and load data into the scene graph as the need arises for model files and levels of detail.

This mechanism works in several background threads and controls the loading of static data located in files on disk and dynamic data generated and added during program execution.

The engine automatically processes nodes that are not displayed in the current viewport and removes them from the scene graph when the render is overloaded. However, this behavior does not affect the osg :: ProxyNode nodes.

Like the proxy node, the osg :: PagedLOD class also has a setFileName () method for specifying the path to the loaded model, but for it it is necessary to set the range of the visibility distance as for the osg :: LOD node. Provided that we have a file cessna.osg and a low-poly model of the L1 level, we can organize the unloaded node as follows

 osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; pagedLOD->addChild(modelL1, 200.0f, FLT_MAX ); pagedLOD->setFileName( 1, "cessna.osg" ); pagedLOD->setRange( 1, 0.0f, 200.0f ); 

It should be understood that the modelL1 node cannot be unloaded from memory, since this is a normal child node not a proxy node.

When rendering, the difference between osg :: LOD and osg :: PagedLOD is apparently not visible if you use only one level of model detail. An interesting idea would be to organize a huge cluster of Cessna models using the class osg :: MatrixTransform. For this you can use for example such a function.

 osg::Node* createLODNode( const osg::Vec3& pos ) { osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; … osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->setMatrix( osg::Matrix::translate(pos) ); mt->addChild( pagedLOD.get() ); return mt.release(); } 

An example of a program that implements a background load of 10,000 aircraft

main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/PagedLOD> #include <osg/MatrixTransform> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osg::Node *createLODNode(const std::string &filepath, const osg::Vec3 &pos) { osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; pagedLOD->setFileName(0, filepath); pagedLOD->setRange(0, 0.0f, FLT_MAX); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->setMatrix(osg::Matrix::translate(pos)); mt->addChild(pagedLOD.get()); return mt.release(); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Group> root = new osg::Group; float dist = 50.0f; int N = 100; for (int i = 0; i < N; ++i) { float x = i * dist; for (int j = 0; j < N; ++j) { float y = j * dist; osg::Vec3 pos(x, y, 0.0f); osg::ref_ptr<osg::Node> node = createLODNode("../data/cessna.osg", pos); root->addChild(node.get()); } } osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); } 

It is assumed that the aircraft will be located on a plane with an interval of 50 coordinate units. When loading, we will see that only those Cessnes are loaded that get into the frame. Those planes that disappear from the frame disappear from the scene tree.



Conclusion


This lesson in the loop about OpenSceneGraph will be the last one done in the How To format. In the framework of twelve articles, we managed to fit the basic principles of work and the use of OpenSceneGraph in practice. I really hope that this engine has become more understandable Russian-language developer.

This does not mean that I am closing the topic of OpenSceneGraph on the resource, on the contrary, it is planned to devote future articles to more advanced techniques and techniques of using OSG in the development of graphical applications. But for this you should accumulate good material and recycle a lot of English-language sources, but it takes time.

But I do not say goodbye, thank you for your attention and see you soon!

Source: https://habr.com/ru/post/438454/