
引言
在先前的课程中,曾经有人说过OSG支持通过其自身的插件系统加载各种资源,例如光栅图像,各种格式的3D模型或字体。 OSG插件是一个单独的组件,可扩展引擎的功能,并在OSG中具有标准化的接口。 该插件实现为动态共享库(Windows上为dll,在Linux等上为dll)。 插件库的名称与特定约定相对应
osgdb_< >.dll
也就是说,插件名称始终包含osgdb_前缀。 文件扩展名告诉引擎应使用哪个插件来下载具有该扩展名的文件。 例如,当我们用代码编写函数时
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
引擎会看到osg扩展名并加载名为osgdb_osg.dll的插件(如果是Linux,则为osgdb_osg.so)。 插件代码通过返回指向描述cessna模型的节点的指针来完成所有肮脏的工作。 同样,尝试加载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
它可以轻松自然地将cessna osg模型转换为3DS格式,然后可以将其导入3D编辑器,例如,导入Blender(顺便说一句,它有一个
扩展功能,可直接为Blender
使用osg )

这里有一份标准的OSG插件的正式列表,其中描述了它们的用途,但是它很长,我懒得把它带到这里。 在bin / ospPlugins-xyz文件夹中查看该库的安装路径更加容易,其中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.90行向osgdb_osg插件指示了加载模型的初始方向的参数。 一些伪加载器需要完全特定的参数才能工作。
2.用于开发第三方插件的API
在阅读完所有内容后,如果您有想法为OSG编写自己的插件可能并不困难,这将使您能够导入非标准格式的3D模型或图像,这是完全合乎逻辑的。 这是一个真实的想法! 插件机制仅用于扩展引擎的功能,而无需更改OSG本身。 为了理解编写插件的基本原理,让我们尝试实现一个简单的示例。
该插件的开发是为了扩展OSG提供的虚拟读取/写入接口。 此功能由osgDB :: ReaderWriter虚拟类提供。 此类提供了许多由插件开发人员重新定义的虚拟方法。
方法 | 内容描述 |
---|
supportExtensions() | 它接受两个字符串参数:文件扩展名和描述。 该方法总是在子类的构造函数中调用。 |
acceptsExtension() | 如果插件支持作为参数传递的扩展名,则返回true |
fileExists() | 允许您确定磁盘上是否存在给定文件(路径作为参数传递)(如果成功,则返回true) |
readNode() | 接受文件名和选项作为osgDB :: Option对象。 从文件读取数据的功能由开发人员实现 |
writeNode() | 接受节点名称,所需的文件名和选项。 将数据写入磁盘的功能由开发人员实现 |
readImage() | 读取磁盘位图数据 |
writeImage() | 将位图写入磁盘 |
readNode()方法的实现可以通过以下代码描述
osgDB::ReaderWriter::ReadResult readNode( const std::string &file, const osgDB::Options *options) const {
令人惊讶的是,该方法返回的类型不是osgDB :: ReaderWriter :: ReadResult,而是指向场景图节点的指针。 此类型是读取结果对象,可以用作节点容器,图像,状态枚举器(例如FILE_NOT_FOUND),另一个特殊对象,甚至可以用作错误消息字符串。 它具有许多用于实现所描述功能的隐式构造函数。
另一个有用的类是osgDB :: Options。 它可以允许您使用setOptionString()和getOptionString()方法设置或获取加载选项的字符串。 也可以将此字符串作为参数传递给此类的构造方法。
开发人员可以通过设置加载对象时传递的参数字符串中的设置来控制插件的行为,例如,通过这种方式
3. OSG插件中的数据流处理
基类osgDB :: ReaderWriter包括一组处理标准C ++库提供的输入/输出流的数据的方法。 这些读/写方法与上面讨论的方法之间的唯一区别是,它们接受文件std :: istream和输入流或std :: ostream和输出流而不是文件名。 使用文件I / O流总是比使用文件名更好。 要执行文件读取操作,我们可以使用以下界面设计:
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 {
实现插件后,我们只需指定文件路径,即可使用标准函数osgDB :: readNodeFile()和osgDB :: readImageFile()加载模型和图像。 OSG将找到并下载我们编写的插件。
4.我们编写自己的插件
因此,没有人会打扰我们想出我们自己的格式来将数据存储在三维几何中,而我们会想出它
吡酰胺 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前缀的生成,该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
对于Linux,使用这样的路径指示(无疑是拐杖,但我还没有找到其他解决方案),我们授予普通用户使用OSG插件写入指定文件夹的权利。
# chmod 666 /usr/lib/osgPlugins-3.7.0
所有其他构建设置与之前的示例应用程序组装中使用的设置类似。
6.插件项目设置:调试模式功能
由于该项目是一个动态库,因此必须有一个程序在其执行过程中加载该库。 它可以是使用OSG的任何应用程序,在该应用程序中将调用该函数
node = osdDB::readNodeFile("piramide.pmd");
在这种情况下,我们的插件将加载。 为了不自己编写此类程序,我们将使用现成的解决方案-标准osgviewer查看器,该解决方案包含在引擎的交付包装中。 如果在控制台中执行
$ osgviewer piramide.pmd
然后它将触发插件。 在项目启动设置中,将osgviewerd的路径指定为工作目录,指定piramide.pmd文件所在的目录,并在osgviewer命令行选项中指定相同的文件

现在,我们可以运行插件并直接从QtCreator IDE对其进行调试。
6.我们实现插件框架
这个示例在某种程度上概括了我们从先前的课程中已经获得的有关OSG的知识。 编写插件时,我们必须
- 选择数据结构以存储从模型文件读取的模型几何信息
- 读取并解析(解析)模型数据文件
- 根据从文件中读取的数据正确配置osg :: Drawable几何对象
- 为加载的模型构建场景子图
因此,按照传统,我将提供插件的完整源代码
osgdb_pmd插件主文件 #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"
首先,让我们照顾一下用于存储几何数据的结构。
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()); } };
该结构由用于存储数据的成员变量组成:顶点-用于存储几何对象的顶点数组; 法线-对象表面法线的数组; 面孔-对象的面孔列表。 结构构造函数立即初始化智能指针
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 {
在第二个重载中,我们为OSG实现了对象生成算法
osgDB::ReaderWriter::ReadResult ReaderWriterPMD::readNode( std::istream &stream, const osgDB::Options *options) const { (void) options;
在main.cpp文件的末尾,调用REGISTER_OSGPLUGIN()宏。
REGISTER_OSGPLUGIN( pmd, ReaderWriterPMD )
该宏生成附加代码,该代码允许OSG以osgDB库的形式构造类型为ReaderWriterPMD的对象,并调用其方法以加载类型为pmd的文件。 这样,插件框架就准备好了,事情仍然不大了-实现加载和解析pmd文件。
7. Parsim 3D模型文件
现在,该插件的所有功能都取决于parsePMD()方法的实现
pmd_mesh_t ReaderWriterPMD::parsePMD(std::istream &stream) const { pmd_mesh_t mesh;
ParseLine()方法解析Pmd文件的行 std::vector<std::string> ReaderWriterPMD::parseLine(const std::string &line) const { std::vector<std::string> 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);
我们在示例中应用此函数,而不是分配自己的法线
可以得到以下结果:
法线会影响照明模型的计算,并且可以看到在这种情况下,平滑的法线会导致金字塔照明计算的错误结果。出于这个原因,我们将我们的自行车应用于法线计算。但我认为,解释这一细微差别超出了本课程的范围。