
Введение
Где-то в предыдущих уроках уже говорилось о том, что OSG поддерживает загрузку разного рода ресурсов типа растровых изображений, 3D-моделей различных форматов, или, например, шрифтов через собственную систему плагинов. Плагин OSG является отдельным компонентом, расширяющим функционал движка и обладающий интерфейсом, стандартизированным в пределах OSG. Плагин реализуется как динамическая разделяемая библиотека (dll в Windows, so в Linux и т.д). Имена библиотек плагинов соответствуют определенному соглашению
osgdb_<расширение файла>.dll
то есть в имени плагина всегда присутствует префикс osgdb_. Расширение файла указывает движку какой плагин следует использовать для загрузки файла с данным расширением. Например, когда мы пишем в коде функцию
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
движок видит расширение osg и загружает плагин с именем osgdb_osg.dll (или osgdb_osg.so в случает Linux). Код плагина выполняет всю черную работу, возвращая нам указатель на ноду, описывающую модель цессны. Аналогичным образом, попытка загрузки изображения формата PNG
osg::ref_ptr<osg:Image> image = osgDB::readImageFile("picture.png");
приведет к тому, что будет загружен плагин osgdb_png.dll, в котором реализован алгоритм чтения данных из картинки в формате PNG и помещение этих данных в объект типа osg::Image.
Все операции по работе с внешними ресурсами реализуются функциями библиотеки osgDB, с которой мы неизменно линкуем программы из примера в пример. Эта библиотека опирается на систему плагинов OSG. На сегодняшний день в комплект OSG входит множество плагинов, обеспечивающих работу с большинством используемых на практике форматов изображений, 3D-моделей и шрифтов. Плагины обеспечивают как чтение данных (импорт) определенного формата, так и, в большинстве случаем запись данных в файл необходимого формата (экспорт). На систему плагинов опирается в частности утилита osgconv, позволяющая преобразовать данные из одного формата в другой, например
$ osgconv cessna.osg cessna.3ds
легко и непринужденно конвертирует osg-модель цессны в формат 3DS, который потом может быть импортирован в 3D-редактор, например в Blender (кстати сказать для Blender существует расширение для работы с osg непосредственно)

Существует официальный перечень стандартных плагинов OSG с описанием их назначения, но он длинный и мне лень приводить его здесь. Проще заглянуть по пути установки библиотеки в папку bin/ospPlugins-x.y.z, где x, y, z — номер версии OSG. Из имени файл плагина легко понять какой формат он обрабатывает.
Если OSG собран компилятором MinGW, то к стандартному имени плагина добавляется дополнительный префикс mingw_, то есть имя будет выглядеть так
mingw_osgdb_<расширение файла>.dll
Версия плагина, собранная в конфигурации DEBUG дополнительно снабжается суффиксом d в конце имени, то есть формат будет такой
osgdb_<расширение файла>d.dll
или
mingw_osgdb_<расширение файла>d.dll
при сборке MinGW.
1. Плагины псевдо-загрузчики
Некоторые плагины OSG выполняют функции так называемых псевдо-загрузчиков — это означает что они не привязаны к конкретному расширению файла, но, путем добавления суффикса в конец имени файла, можно указать какой плагин необходимо использовать для загрузки данного файла, например
$ osgviewer worldmap.shp.ogr
В данном случае реальное имя файла на диске worldmap.shp — этот файл хранит в себе карту мира в формате ESRI shapefile. Суффикс .ogr указывает библиотеке osgDB использовать плагин osgdb_ogr для загрузки этого файла; в противном случае будет использован плагин osgdb_shp.
Другим хорошим примером является плагин osgdb_ffmpeg. Библиотека FFmpeg поддерживает свыше 100 различных кодеков. Для чтения любого из них мы можем просто добавить суффикс .ffmpeg после имени медиафайла.
Вдобавок к этому, некоторые псевдо-загрузчики позволяют передавать через суффикс ряд параметров, влияющих на состояние загружаемого объекта, и с этим мы уже сталкивались в одном из примеров с анимацией
node = osgDB::readNodeFile("cessna.osg.0,0,90.rot");
Строка 0,0,90 указывает плагину osgdb_osg параметры начальной ориентации загружаемой модели. Некоторые псевдо-загрузчики требуют для своей работы задания совершенно специфичных параметров.
2. API для разработки сторонних плагинов
Совершенно логично, если после всего прочитанного у вас возникла мысль о том, что наверняка не составит труда написать собственный плагин к OSG, который будет позволять импортировать нестандартный формат 3D-моделей или изображений. И это верная мысль! Механизм плагинов как раз и предназначен для расширения функциональности движка без изменения самого OSG. Чтобы понять основные принципы написания плагина, попробуем реализовать простейший пример.
Разработка плагина заключается в расширении виртуального интерфейса чтения-записи данных, предоставляемого OSG. Данный функционал обеспечивается виртуальным классом osgDB::ReaderWriter. Этот класс предоставляет ряд виртуальных методов, переопределяемых разработчиком плагина
| Метод | Описание | 
|---|---|
| supportsExtensions() | Принимает два строковых параметра: расширение файла и описание. Метод всегда вызывается в конструкторе подкласса | 
| acceptsExtension() | Возвращает true если расширение, переданное в качестве аргумента поддерживается плагином | 
| fileExists() | Позволяет определить, существует ли данный файл (путь передается в качестве параметра) на диске (возвращает true в случае успеха) | 
| readNode() | Принимает имя файла и опции в виде объекта osgDB::Option. Функции по чтению данных из файла реализуются разработчиком | 
| writeNode() | Принимает имя ноды, желаемое имя файла и опции. Функции по записи данных на диск реализуются разработчиком | 
| readImage() | Чтение данных о растровом изображении с диска | 
| writeImage() | Запись растрового изображения на диск | 
Реализация методf readNode() может быть описана следующим кодом
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;
}
Немного удивляет, что вместо указателя на ноду графа сцены метод возвращает тип osgDB::ReaderWriter::ReadResult. Этот тип — объект результата чтения, и он может использоваться как контейнер узла, изображение, перечислитель состояния (например FILE_NOT_FOUND), другой специальный объект или даже как строка сообщения об ошибке. Он имеет множество неявных конструкторов для реализации описанных функций.
Другим полезным классом является osgDB::Options. Он может позволяет задать или получить строку опций загрузки методами setOptionString() и getOptionString(). Допускается так же передача данной строки в конструктор этого класса в качестве аргумента.
Разработчик может управлять поведением плагина, задавая настройки в строке параметров, переданной при загрузке объекта, например таким способом
// Параметры не передаются
osg::Node* node1 = osgDB::readNodeFile("cow.osg"); 
// Параметры передаются через строку string
osg::Node* node2 = osgDB::readNodeFile("cow.osg", new osgDB::Options(string)); 
3. Обработка потока данных в плагине OSG
Базовый класс osgDB::ReaderWriter включает в себя набор методов, обрабатывающих данные потоков ввода/вывода предоставляемых стандартной библиотекой C++. Единственное отличие этих методов чтения/записи от рассмотренных выше в том, что вместо имени файла они принимают на вход потоки ввода std::istream & или поток вывода std::ostream &. Использование файлового потока ввода/вывода всегда предпочтительнее использования имени файла. Для выполнения операций чтения файла мы можем использовать следующий дизайн интерфейса:
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;
}
После реализации плагина мы можем использовать штатные функции osgDB::readNodeFile() и osgDB::readImageFile() для загрузки моделей и изображений, просто указав путь к файлу. OSG сам найдет и загрузит написанный нами плагин.
4. Пишем собственный плагин
Итак, никто не мешает нам придумать собственный формат хранения данных о трехмерной геометрии, и мы его придумаем
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
Здесь в начале файла идет список вершин с их координатами. Индексы вершин идут по порядку, начиная от нуля. После списка вершин идет список граней. Каждая грань задается перечнем индексов вершин, из которых она образована. Как видно ничего сложного. Задача — считать этот файл с диска и сформировать на его основе трехмерную геометрию.
5. Настройка проекта плагина: особенности сценария сборки
Если раньше мы собирали приложения, то теперь нам предстоит написать динамическую библиотеку, да не просто библиотеку а плагин к OSG, удовлетворяющий определенным требованиям. Выполнять эти требования начнем со сценария сборки проекта, который будет выглядеть так
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)
Отдельные нюансы разберем подробнее
TEMPLATE = lib
означает что мы будем собирать библиотеку. Чтобы не происходила генерация символических ссылок, с помощью которых в *nix системах разруливаются вопросы конфликта версий библиотек, указываем системе сборки, что данная библиотека будет плагином, то есть будет загружаться в память "на лету"
CONFIG += plugin
Далее исключаем генерацию перфикса lib, который добавляется при использовании компиляторов семейства gcc и учитывается рантаймовым окружением при загрузке библиотеки
CONFIG += no_plugin_name_prefix
Задаем имя файлу библиотеки
TARGET = osgdb_pmd
где pmd — расширение файла изобретенного нами формата 3D-моделей. Далее обязательно указываем, что в случае сборки MinGW к имени обязательно добавляется префикс mingw_
win32-g++: TARGET = $$join(TARGET,,mingw_,)
Указываем путь сборки библиотеки: для Windows
DESTDIR = $$(OSG_PLUGINS_PATH)
для Linux
DESTDIR = /usr/lib/osgPlugins-3.7.0
Для линукс, при таком указании пути (что несомненно является костылем, но я пока не нашел другого решения) даем на указанную папку с плагинами OSG права на запись от обычного пользователя
# chmod 666 /usr/lib/osgPlugins-3.7.0
Все остальные настройки сборки аналогичны применявшимся при сборке примеров-приложений ранее.
6. Настройка проекта плагина: особенности режима отладки
Так данный проект является динамической библиотекой, то должна существовать программа, которая загружает эту библиотеку в процессе своего исполнения. В качестве нее может выступать любое приложение, использующее OSG и в котором будет происходить вызов функции
node = osdDB::readNodeFile("piramide.pmd");
В этом случае произойдет загрузка нашего плагина. Чтобы не писать такую программу самостоятельно, воспользуемся готовым решением — стандартным просмотрщиком osgviewer, входящим в комплект поставки движка. Если в консоли выполнить
$ osgviewer piramide.pmd
то это так же вызовет срабатывание плагина. В настройках запуска проекта укажем путь к osgviewerd, в качестве рабочего каталога укажем тот каталог, где лежит файл piramide.pmd, и этот же файл укажем в опциях командной строки osgviewer

Теперь мы сможем запускать плагин и отлаживать его прямо из IDE QtCreator.
6. Реализуем каркас плагина
Этот пример в какой-то степени обобщает те знания, что мы уже получили об OSG из предыдущих уроков. При написании плагина нам предстоит
- Выбрать структуру данных для сохранения информации о геометрии модели, считанной из файла модели
 
- Прочитать и разобрать (распарсить) файл с данными модели
 
- Правильно настроить геометрический объект osg::Drawable по данным, прочитанным из файла
 
- Построить субграф сцены для загруженной модели
 
Итак, по традиции, приведу исходный код плагина целиком
Плагин osgdb_pmd
main.h
main.cpp
#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 )
Для начала позаботимся от структурах для хранения данных геометрии
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());
    }
};
Структура состоит содержит переменные-члены для хранения данных: vertices – для хранения массива вершин геометрического объекта; normals – массив нормалей к граням объекта; faces — список граней объекта. В конструкторе структуры сразу выполняется инициализация умных указателей
pmd_mesh_t()
        : vertices(new osg::Vec3Array)
        , normals(new osg::Vec3Array)
{
}
Кроме того, структура содержит метод, позволяющий рассчитать вектор-нормаль к грани calcFaceNormal() в качестве параметра принимающий структуру, описывающую грань. В детали реализации этого метода мы пока не будем вдаваться, разберем их несколько позже.
Таким образом, мы определились со структурами, в которых будем хранить данные геометрии. Теперь напишем каркас нашего плагина, а именно реализуем класс-наследник 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;
};
Как и рекомендуется в описании API к разработке плагинов, в данном классе переопределяем методы чтения данных из файла и преобразования их в субграф сцены. У метода readNode() делаем две перегрузки — одна принимает на вход имя файла, другая — стандартный поток ввода. Конструктор класса определяет расширения файлов, поддерживаемых плагином
ReaderWriterPMD::ReaderWriterPMD()
{
    supportsExtension("pmd", "PMD model file");
}
Первая перегрузка метода readNode() анализирует корректность имени файла и пути к нему, связывает с файлом стандартный поток ввода и вызывает вторую перегрузку, выполняющую основную работу
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);
}
Во второй перегрузке реализуем алгоритм формирования объекта для 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();
}
В конце файла main.cpp вызываем макрос REGISTER_OSGPLUGIN()
REGISTER_OSGPLUGIN( pmd, ReaderWriterPMD )
Этот макрос формирует дополнительный код, позволяющий OSG, в лице бибилиотеки osgDB, сконструировать объект типа ReaderWriterPMD и вызвать его методы для загрузки файлов типа pmd. Таким образом, каркас плагин готов, дело осталось за малым – реализовать загрузку и разбор файла pmd.
7. Парсим файл 3D-модели
Теперь весь функционал плагина упирается в реализацию метода parsePMD()
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);
и мы получим следующий результат

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