📜 ⬆️ ⬇️

OpenSceneGraph: Plugin System

image

Introduction


Somewhere in previous lessons, it has already been said that OSG supports downloading various kinds of resources such as bitmaps, 3D models of various formats, or, for example, fonts through its own plugin system. The OSG plugin is a separate component that extends the functionality of the engine and has an interface standardized within OSG. The plugin is implemented as a dynamic shared library (dll on Windows, so on Linux, etc.). Plugin library names follow a specific convention.

osgdb_<расширение файла>.dll 

that is, the prefix osgdb_ is always present in the name of the plugin. The file extension indicates the engine which plugin should be used to download a file with this extension. For example, when we write a function in code

 osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg"); 

the engine sees the osg extension and loads a plugin named osgdb_osg.dll (or osgdb_osg.so for Linux). The plug-in code does all the dirty work, returning to us a pointer to a node describing the model of Cessna. Similarly, trying to load a PNG image

 osg::ref_ptr<osg:Image> image = osgDB::readImageFile("picture.png"); 

will cause the osgdb_png.dll plugin to be loaded, which implements an algorithm for reading data from a picture in PNG format and putting this data into an object of type osg :: Image.

All operations on working with external resources are implemented by the functions of the osgDB library, with which we invariably link programs from example to example. This library relies on the OSG plugin system. Today, OSG comes with many plug-ins that work with most image formats, 3D models and fonts used in practice. Plug-ins provide both data reading (import) of a certain format, and, in most cases, writing data to a file of the required format (export). The system of plug-ins relies in particular on the osgconv utility, which allows you to convert data from one format to another, for example

 $ osgconv cessna.osg cessna.3ds 

Easily and naturally converts the Cessna's osg model to the 3DS format, which can then be imported into a 3D editor, for example, in Blender (by the way, for Blender there is an extension for working with osg directly )



There is an official list of standard OSG plug-ins with a description of their purpose, but it is long and I'm too lazy to bring it here. It is easier to look at the installation path of the library in the folder bin / ospPlugins-xyz, where x, y, z is the version number of OSG. From the name of the plugin file, it is easy to understand what format it processes.

If OSG is compiled by the MinGW compiler, then an additional mingw_ prefix is ​​added to the standard plugin name, that is, the name will look like this

 mingw_osgdb_<расширение файла>.dll 

The version of the plug-in collected in the DEBUG configuration is additionally supplied with the suffix d at the end of the name, that is, the format will be

 osgdb_<расширение файла>d.dll 

or

 mingw_osgdb_<расширение файла>d.dll 

when building MinGW.

1. Pseudo Downloaders


Some OSG plug-ins perform the functions of so-called pseudo-downloaders - this means that they are not tied to a specific file extension, but by adding a suffix to the end of the file name, you can specify which plug-in to use to download this file, for example

 $ osgviewer worldmap.shp.ogr 

In this case, the real name of the file on the worldmap.shp disk - this file stores the world map in the ESRI shapefile format. The .ogr suffix tells the osgDB library to use the osgdb_ogr plugin to load this file; otherwise, the osgdb_shp plugin will be used.

Another good example is the osgdb_ffmpeg plugin. The FFmpeg library supports over 100 different codecs. To read any of them, we can simply add the suffix .ffmpeg after the name of the media file.

In addition to this, some pseudo-loaders allow you to pass through a suffix a number of parameters that affect the state of the loaded object, and we have already encountered in one of the examples with animation

 node = osgDB::readNodeFile("cessna.osg.0,0,90.rot"); 

Line 0, 0.90 specifies the osgdb_osg plugin for the parameters of the initial orientation of the loaded model. Some pseudo-loaders require for the operation of the job very specific parameters.

2. API for developing third-party plug-ins


It is quite logical if, after all you have read, you had the idea that it is certainly not difficult to write your own OSG plugin, which will allow you to import non-standard format of 3D models or images. And this is the right idea! The plug-in mechanism is precisely designed to expand the functionality of the engine without changing OSG itself. To understand the basic principles of writing a plugin, we will try to implement the simplest example.

The development of the plug-in is to extend the virtual data read / write interface provided by OSG. This functionality is provided by the virtual class osgDB :: ReaderWriter. This class provides a number of virtual methods that the plugin developer overrides.
MethodDescription
supportsExtensions ()It accepts two string parameters: file extension and description. The method is always called in the constructor of a subclass.
acceptsExtension ()Returns true if the extension passed in as an argument is supported by the plugin.
fileExists ()Allows you to determine whether this file (the path is passed as a parameter) on the disk (returns true if successful)
readNode ()Accepts the file name and options as an osgDB :: Option object. Functions for reading data from a file are implemented by the developer.
writeNode ()Accepts the name of the node, the desired file name and options. Functions for writing data to disk are implemented by the developer.
readImage ()Reading raster image data from disk
writeImage ()Burn bitmap to disk

The implementation of the readNode () method can be described with the following code.

 osgDB::ReaderWriter::ReadResult readNode( const std::string &file, const osgDB::Options *options) const { // Проверяем что расширение файла поддерживается и файл существует bool recognizableExtension = ...; bool fileExists = ...; if (!recognizableExtension) return ReadResult::FILE_NOT_HANDLED; if (!fileExists) return ReadResult::FILE_NOT_FOUND; // Конструируем подграф сцены в соответствии со спецификацией загружаемого формата osg::Node *root = ...; // В случае ошибок в процессе выполнения каких-либо операций возвращаем сообщения об ошибке. // В случае успеха - возвращаем корневую ноду подграфа сцены bool errorInParsing = ...; if (errorInParsing) return ReadResult::ERROR_IN_READING_FILE; return root; } 

It is a little surprising that instead of a pointer to the node of the scene graph, the method returns the type osgDB :: ReaderWriter :: ReadResult. This type is a read result object, and it can be used as a node container, an image, a state enumerator (for example, FILE_NOT_FOUND), another special object, or even as an error message string. It has many implicit constructors for implementing the described functions.

Another useful class is osgDB :: Options. It can allow you to set or get a string of boot options with the setOptionString () and getOptionString () methods. It is also allowed to pass this string to the constructor of this class as an argument.

The developer can control the behavior of the plug-in by specifying the settings in the parameter line passed when the object is loaded, for example in this way

 // Параметры не передаются osg::Node* node1 = osgDB::readNodeFile("cow.osg"); // Параметры передаются через строку string osg::Node* node2 = osgDB::readNodeFile("cow.osg", new osgDB::Options(string)); 

3. Processing data stream in OSG plugin


The base class osgDB :: ReaderWriter includes a set of methods that process the I / O stream data provided by the C ++ standard library. The only difference between these read / write methods and the ones discussed above is that instead of the file name, they accept input streams std :: istream & or output stream std :: ostream &. Using a file I / O stream is always preferable to using a file name. To perform file read operations, we can use the following interface design:

 osgDB::ReaderWriter::ReadResult readNode( const std::string &file, const osgDB::Options *options) const { ... osgDB::ifstream stream(file.c_str(), std::ios::binary); if (!stream) return ReadResult::ERROR_IN_READING_FILE; return readNode(stream, options); } ... osgDB::ReaderWriter::ReadResult readNode( std::istream &stream, const osgDB::Options *options) const { // Формируем граф сцены в соответствии с форматом файла osg::Node *root = ...; return root; } 

After implementing the plugin, we can use the osgDB :: readNodeFile () and osgDB :: readImageFile () functions to load models and images, simply by specifying the path to the file. OSG will find and download the plugin written by us.

4. Write your own plugin



So, no one bothers us to come up with our own three-dimensional geometry data storage format, and we will invent it.

piramide.pmd

 vertex: 1.0 1.0 0.0 vertex: 1.0 -1.0 0.0 vertex: -1.0 -1.0 0.0 vertex: -1.0 1.0 0.0 vertex: 0.0 0.0 2.0 face: 0 1 2 3 face: 0 3 4 face: 1 0 4 face: 2 1 4 face: 3 2 4 

Here at the beginning of the file is a list of vertices with their coordinates. Vertex indices go in order, starting from zero. After the list of vertices is a list of faces. Each face is set by the list of indexes of the vertices from which it is formed. As you can see nothing complicated. The task is to read this file from disk and form a three-dimensional geometry on its basis.

5. Configuring the plugin project: features of the build script


If earlier we collected applications, now we have to write a dynamic library, and not just a library, but an OSG plugin that meets certain requirements. We will begin to fulfill these requirements from the script of the assembly of the project which will look so

plugin.pro

 TEMPLATE = lib CONFIG += plugin CONFIG += no_plugin_name_prefix TARGET = osgdb_pmd win32-g++: TARGET = $$join(TARGET,,mingw_,) win32 { OSG_LIB_DIRECTORY = $$(OSG_BIN_PATH) OSG_INCLUDE_DIRECTORY = $$(OSG_INCLUDE_PATH) DESTDIR = $$(OSG_PLUGINS_PATH) CONFIG(debug, debug|release) { TARGET = $$join(TARGET,,,d) LIBS += -L$$OSG_LIB_DIRECTORY -losgd LIBS += -L$$OSG_LIB_DIRECTORY -losgViewerd LIBS += -L$$OSG_LIB_DIRECTORY -losgDBd LIBS += -L$$OSG_LIB_DIRECTORY -lOpenThreadsd LIBS += -L$$OSG_LIB_DIRECTORY -losgUtild } else { LIBS += -L$$OSG_LIB_DIRECTORY -losg LIBS += -L$$OSG_LIB_DIRECTORY -losgViewer LIBS += -L$$OSG_LIB_DIRECTORY -losgDB LIBS += -L$$OSG_LIB_DIRECTORY -lOpenThreads LIBS += -L$$OSG_LIB_DIRECTORY -losgUtil } INCLUDEPATH += $$OSG_INCLUDE_DIRECTORY } unix { DESTDIR = /usr/lib/osgPlugins-3.7.0 CONFIG(debug, debug|release) { TARGET = $$join(TARGET,,,d) LIBS += -losgd LIBS += -losgViewerd LIBS += -losgDBd LIBS += -lOpenThreadsd LIBS += -losgUtild } else { LIBS += -losg LIBS += -losgViewer LIBS += -losgDB LIBS += -lOpenThreads LIBS += -losgUtil } } INCLUDEPATH += ./include HEADERS += $$files(./include/*.h) SOURCES += $$files(./src/*.cpp) 

Separate nuances we will disassemble in more detail

 TEMPLATE = lib 

means that we will build the library. To prevent the generation of symbolic links, with the help of which conflicts of library versions are resolved in * nix systems, we indicate to the build system that this library will be a plugin, that is, it will be loaded into memory on the fly

 CONFIG += plugin 

Next, we exclude the generation of the lib perfix, which is added when using compilers of the gcc family and is taken into account by the runtime environment when loading the library

 CONFIG += no_plugin_name_prefix 

Set the name of the library file

 TARGET = osgdb_pmd 

where pmd is the file extension of the invented 3D model format. Next, be sure to indicate that in the case of the MinGW assembly, the prefix mingw_ must be added to the name

 win32-g++: TARGET = $$join(TARGET,,mingw_,) 

Specify library build path: for Windows

 DESTDIR = $$(OSG_PLUGINS_PATH) 

for linux

 DESTDIR = /usr/lib/osgPlugins-3.7.0 

For Linux, with this indication of the path (which is undoubtedly a crutch, but I haven’t yet found another solution) we give write access to the specified folder with OSG plug-ins from a regular user

 # chmod 666 /usr/lib/osgPlugins-3.7.0 

All other build settings are similar to those used earlier for building sample applications.

6. Configuring the plugin project: debug mode features


Since this project is a dynamic library, then there must be a program that loads this library during its execution. It can be any application that uses OSG and in which the function call will occur

 node = osdDB::readNodeFile("piramide.pmd"); 

In this case, our plug-in will be loaded. In order not to write such a program on our own, we will use the ready-made solution - the standard osgviewer viewer included in the engine delivery set. If in the console to execute

 $ osgviewer piramide.pmd 

this will also trigger the plug-in. In the project launch settings, specify the path to osgviewerd, specify the directory where the piramide.pmd file is located as the working directory, and specify the same file in the command line options osgviewer



Now we can run the plugin and debug it directly from the IDE QtCreator.

6. We implement the plug-in framework


This example to some extent summarizes the knowledge that we have already learned about OSG from previous lessons. When writing a plugin we have to

  1. Select a data structure to store information about the geometry of the model, read from the model file
  2. Read and parse (parse) the file with the model data
  3. Correctly adjust the geometry object osg :: Drawable according to the data read from the file
  4. Build a scene subgraph for the loaded model

So, according to tradition, I will give the source code of the plugin entirely

Plugin osgdb_pmd
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Geometry> #include <osg/Geode> #include <osgDB/FileNameUtils> #include <osgDB/FileUtils> #include <osgDB/Registry> #include <osgUtil/SmoothingVisitor> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ struct face_t { std::vector<unsigned int> indices; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ struct pmd_mesh_t { osg::ref_ptr<osg::Vec3Array> vertices; osg::ref_ptr<osg::Vec3Array> normals; std::vector<face_t> faces; pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { } osg::Vec3 calcFaceNormal(const face_t &face) const { osg::Vec3 v0 = (*vertices)[face.indices[0]]; osg::Vec3 v1 = (*vertices)[face.indices[1]]; osg::Vec3 v2 = (*vertices)[face.indices[2]]; osg::Vec3 n = (v1 - v0) ^ (v2 - v0); return n * (1 / n.length()); } }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class ReaderWriterPMD : public osgDB::ReaderWriter { public: ReaderWriterPMD(); virtual ReadResult readNode(const std::string &filename, const osgDB::Options *options) const; virtual ReadResult readNode(std::istream &stream, const osgDB::Options *options) const; private: pmd_mesh_t parsePMD(std::istream &stream) const; std::vector<std::string> parseLine(const std::string &line) const; }; #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ ReaderWriterPMD::ReaderWriterPMD() { supportsExtension("pmd", "PMD model file"); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( const std::string &filename, const osgDB::Options *options) const { std::string ext = osgDB::getLowerCaseFileExtension(filename); if (!acceptsExtension(ext)) return ReadResult::FILE_NOT_HANDLED; std::string fileName = osgDB::findDataFile(filename, options); if (fileName.empty()) return ReadResult::FILE_NOT_FOUND; std::ifstream stream(fileName.c_str(), std::ios::in); if (!stream) return ReadResult::ERROR_IN_READING_FILE; return readNode(stream, options); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( std::istream &stream, const osgDB::Options *options) const { (void) options; pmd_mesh_t mesh = parsePMD(stream); osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; geom->setVertexArray(mesh.vertices.get()); for (size_t i = 0; i < mesh.faces.size(); ++i) { osg::ref_ptr<osg::DrawElementsUInt> polygon = new osg::DrawElementsUInt(osg::PrimitiveSet::POLYGON, 0); for (size_t j = 0; j < mesh.faces[i].indices.size(); ++j) polygon->push_back(mesh.faces[i].indices[j]); geom->addPrimitiveSet(polygon.get()); } geom->setNormalArray(mesh.normals.get()); geom->setNormalBinding(osg::Geometry::BIND_PER_PRIMITIVE_SET); osg::ref_ptr<osg::Geode> geode = new osg::Geode; geode->addDrawable(geom.get()); return geode.release(); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ pmd_mesh_t ReaderWriterPMD::parsePMD(std::istream &stream) const { pmd_mesh_t mesh; while (!stream.eof()) { std::string line; std::getline(stream, line); std::vector<std::string> tokens = parseLine(line); if (tokens[0] == "vertex") { osg::Vec3 point; std::istringstream iss(tokens[1]); iss >> point.x() >> point.y() >> point.z(); mesh.vertices->push_back(point); } if (tokens[0] == "face") { unsigned int idx = 0; std::istringstream iss(tokens[1]); face_t face; while (!iss.eof()) { iss >> idx; face.indices.push_back(idx); } mesh.faces.push_back(face); mesh.normals->push_back(mesh.calcFaceNormal(face)); } } return mesh; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ std::string delete_symbol(const std::string &str, char symbol) { std::string tmp = str; tmp.erase(std::remove(tmp.begin(), tmp.end(), symbol), tmp.end()); return tmp; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ std::vector<std::string> ReaderWriterPMD::parseLine(const std::string &line) const { std::vector<std::string> tokens; std::string tmp = delete_symbol(line, '\r'); size_t pos = 0; std::string token; while ( (pos = tmp.find(':')) != std::string::npos ) { token = tmp.substr(0, pos); tmp.erase(0, pos + 1); if (!token.empty()) tokens.push_back(token); } tokens.push_back(tmp); return tokens; } REGISTER_OSGPLUGIN( pmd, ReaderWriterPMD ) 


First we take care of the structures for storing the geometry data.

 struct face_t { std::vector<unsigned int> indices; }; 

- describes the face specified by the list of indexes of the vertices belonging to the given face. The model as a whole will be described by such a structure

 struct pmd_mesh_t { osg::ref_ptr<osg::Vec3Array> vertices; osg::ref_ptr<osg::Vec3Array> normals; std::vector<face_t> faces; pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { } osg::Vec3 calcFaceNormal(const face_t &face) const { osg::Vec3 v0 = (*vertices)[face.indices[0]]; osg::Vec3 v1 = (*vertices)[face.indices[1]]; osg::Vec3 v2 = (*vertices)[face.indices[2]]; osg::Vec3 n = (v1 - v0) ^ (v2 - v0); return n * (1 / n.length()); } }; 

The structure consists of member variables for storing data: vertices - for storing an array of vertices of a geometric object; normals - an array of normals to the edges of the object; faces - the list of faces of the object. In the structure's constructor, smart pointers are initialized immediately.

 pmd_mesh_t() : vertices(new osg::Vec3Array) , normals(new osg::Vec3Array) { } 

In addition, the structure contains a method that allows to calculate the vector-normal to the edge calcFaceNormal () as a parameter that takes a structure that describes the face. We will not go into the details of the implementation of this method yet; we will analyze them somewhat later.

Thus, we decided on the structures in which we will store the geometry data. Now we will write the framework of our plug-in, namely, we will implement the heir class osgDB :: ReaderWriter

 class ReaderWriterPMD : public osgDB::ReaderWriter { public: ReaderWriterPMD(); virtual ReadResult readNode(const std::string &filename, const osgDB::Options *options) const; virtual ReadResult readNode(std::istream &stream, const osgDB::Options *options) const; private: pmd_mesh_t parsePMD(std::istream &stream) const; std::vector<std::string> parseLine(const std::string &line) const; }; 

As recommended in the API description for plugin development, in this class we override methods for reading data from a file and converting it into a subgraph of the scene. We have two overloads for the readNode () method - one takes the file name as input, the other the standard input stream. The class constructor defines the file extensions supported by the plugin.

 ReaderWriterPMD::ReaderWriterPMD() { supportsExtension("pmd", "PMD model file"); } 

The first overload of the readNode () method analyzes the correctness of the file name and the path to it, links the standard input stream to the file, and causes a second overload that performs the main work.

 osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( const std::string &filename, const osgDB::Options *options) const { // Получаем расширение из пути к файлу std::string ext = osgDB::getLowerCaseFileExtension(filename); // Проверяем, поддерживает ли плагин это расширение if (!acceptsExtension(ext)) return ReadResult::FILE_NOT_HANDLED; // Проверяем, имеется ли данный файл на диске std::string fileName = osgDB::findDataFile(filename, options); if (fileName.empty()) return ReadResult::FILE_NOT_FOUND; // Связваем поток ввода с файлом std::ifstream stream(fileName.c_str(), std::ios::in); if (!stream) return ReadResult::ERROR_IN_READING_FILE; // Вызываем основную рабочую перегрузку метода readNode() return readNode(stream, options); } 

In the second overload, we implement an object formation algorithm for OSG

 osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( std::istream &stream, const osgDB::Options *options) const { (void) options; // Парсим файл *.pmd извлекая из него данные о геометрии pmd_mesh_t mesh = parsePMD(stream); // Создаем геометрию объекта osg::ref_ptr<osg::Geometry> geom = new osg::Geometry; // Задаем массив вершин geom->setVertexArray(mesh.vertices.get()); // Формируем грани объекта for (size_t i = 0; i < mesh.faces.size(); ++i) { // Создаем примитив типа GL_POLYGON с пустым списком индексов вершин (второй параметр - 0) osg::ref_ptr<osg::DrawElementsUInt> polygon = new osg::DrawElementsUInt(osg::PrimitiveSet::POLYGON, 0); // Заполняем индексы вершин для текущей грани for (size_t j = 0; j < mesh.faces[i].indices.size(); ++j) polygon->push_back(mesh.faces[i].indices[j]); // Добаляем грань к геометрии geom->addPrimitiveSet(polygon.get()); } // Задаем массив нормалей geom->setNormalArray(mesh.normals.get()); // Указываем OpenGL, что каждая нормаль применяется к примитиву geom->setNormalBinding(osg::Geometry::BIND_PER_PRIMITIVE_SET); // Создаем листовой узел графа сцены и добавляем в него сформированную нами геометрию osg::ref_ptr<osg::Geode> geode = new osg::Geode; geode->addDrawable(geom.get()); // Возвращаем готовый листовой узел return geode.release(); } 

At the end of the main.cpp file, call the REGISTER_OSGPLUGIN () macro

 REGISTER_OSGPLUGIN( pmd, ReaderWriterPMD ) 

This macro forms an additional code that allows OSG, represented by the osgDB library, to construct an object of the type ReaderWriterPMD and call its methods to load files of the pmd type. Thus, the framework of the plugin is ready, the matter remains for small - to implement the loading and parsing of the pmd file.

7. Parsim 3D model file


Now all the functionality of the plug-in rests on the implementation of the parsePMD () method

 pmd_mesh_t ReaderWriterPMD::parsePMD(std::istream &stream) const { pmd_mesh_t mesh; // Читаем файл построчно while (!stream.eof()) { // Получаем из файла очередную строку std::string line; std::getline(stream, line); // Разбиваем строку на составлящие - тип данный и параметры std::vector<std::string> tokens = parseLine(line); // Если тип данных - вершина if (tokens[0] == "vertex") { // Читаем координаты вершины из списка параметров osg::Vec3 point; std::istringstream iss(tokens[1]); iss >> point.x() >> point.y() >> point.z(); // Добавляем вершину в массив вершин mesh.vertices->push_back(point); } // Если тип данных - грань if (tokens[0] == "face") { // Читаем все индексы вершин грани из списка параметров unsigned int idx = 0; std::istringstream iss(tokens[1]); face_t face; while (!iss.eof()) { iss >> idx; face.indices.push_back(idx); } // Добавляем грань в список граней mesh.faces.push_back(face); // Вычисляем нормаль к грани mesh.normals->push_back(mesh.calcFaceNormal(face)); } } return mesh; } 

Метод parseLine() выполняет разбор строки pmd-файла

 std::vector<std::string> ReaderWriterPMD::parseLine(const std::string &line) const { std::vector<std::string> tokens; // Формируем временную строку, удаляя из текущей строки символ возврата каретки (для Windows) std::string tmp = delete_symbol(line, '\r'); size_t pos = 0; std::string token; // Ищем разделитель типа данных и параметров, разбивая строку на два токена: // тип данных и сами данные while ( (pos = tmp.find(':')) != std::string::npos ) { // Выделяем токен типа данных (vertex или face в данном случае) token = tmp.substr(0, pos); // Удаляем найденный токен из строки вместе с разделителем tmp.erase(0, pos + 1); if (!token.empty()) tokens.push_back(token); } // Помещаем оставшуюся часть строки в список токенов tokens.push_back(tmp); return tokens; } 

Этот метод превратит строку "vertex: 1.0 -1.0 0.0" в список двух строк "vertex" и " 1.0 -1.0 0.0". По первой строке мы идентифицируем тип данных — вершина или грань, из второй извлечем данные о координатах вершины. Для обеспечения работы этого метода нужна вспомогательная функция delete_symbol(), удаляющая из строки заданный символ и возвращающая строку не содержащую этого символа

 std::string delete_symbol(const std::string &str, char symbol) { std::string tmp = str; tmp.erase(std::remove(tmp.begin(), tmp.end(), symbol), tmp.end()); return tmp; } 

То есть теперь мы реализовали весь функционал нашего плагина и можем его протестировать.

8. Тестируем плагин


Компилируем плагин и запускаем отладку (F5). Будет запущена отладочная версия стандартного просмотрщика osgviewerd, которая анализирует переданный ей файл piramide.pmd, загрузит наш плагин и вызовет его метод readNode(). Если мы сделали всё правильно, то мы получим такой результат



Оказывается за списком вершин и граней в нашем придуманном фале 3D-модели скрывалась четырехугольная пирамида.

Зачем мы рассчитывали нормали самостоятельно? В одном из уроков нам предлагался следующий метод автоматического расчета сглаженных нормалей

 osgUtil::SmoothingVisitor::smooth(*geom); 

Применим эту функцию в нашем примере, вместо назначения собственных нормалей

 //geom->setNormalArray(mesh.normals.get()); //geom->setNormalBinding(osg::Geometry::BIND_PER_PRIMITIVE_SET); osgUtil::SmoothingVisitor::smooth(*geom); 

и мы получим следующий результат



Нормали влияют на расчет освещения модели, и мы видим что в данной ситуации сглаженные нормали приводят к некорректным результатам расчета освещения пирамиды. Именно по этой причине мы применили к расчету нормалей свой велосипед. Но, думаю что объяснение нюансов этого выходит за рамки данного урока.

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