目录
2.5. osgDB::ReaderWriter::ReadResult类
1. 前言
osg的文件读写接口主要是由osgDB库负责实现。可读写的文件包括:模型、图像、视频、字体、文本文档等。osgDB库提供了丰富的读写接口来读写这些文件,如下为osgDB库部分接口(限于篇幅,没有全部罗列出来):
osg::ref_ptr< T > readRefFile (const std::string &filename)
OSGDB_EXPORT osg::ref_ptr< osg::Image > readRefImageFile (const std::string &filename, const Options *options)
Read an osg::Image from file. More...
osg::ref_ptr< osg::Image > readRefImageFile (const std::string &filename)
Read an osg::Image from file. More...
OSGDB_EXPORT osg::ref_ptr< osg::HeightField > readRefHeightFieldFile (const std::string &filename, const Options *options)
Read an osg::HeightField from file. More...
osg::ref_ptr< osg::HeightField > readRefHeightFieldFile (const std::string &filename)
Read an osg::HeightField from file. More...
OSGDB_EXPORT osg::ref_ptr< osg::Node > readRefNodeFile (const std::string &filename, const Options *options)
Read an osg::Node from file. More...
osg::ref_ptr< osg::Node > readRefNodeFile (const std::string &filename)
Read an osg::Node from file. More...
OSGDB_EXPORT osg::ref_ptr< osg::Node > readRefNodeFiles (std::vector< std::string > &fileList, const Options *options)
Read an osg::Node subgraph from files, creating a osg::Group to contain the nodes if more than one subgraph has been loaded. More...
osg::ref_ptr< osg::Node > readRefNodeFiles (std::vector< std::string > &fileList)
Read an osg::Node subgraph from files, creating a osg::Group to contain the nodes if more than one subgraph has been loaded. More...
OSGDB_EXPORT osg::ref_ptr< osg::Node > readRefNodeFiles (osg::ArgumentParser &parser, const Options *options)
Read an osg::Node subgraph from files, creating a osg::Group to contain the nodes if more than one subgraph has been loaded. More...
osg::ref_ptr< osg::Node > readRefNodeFiles (osg::ArgumentParser &parser)
Read an osg::Node subgraph from files, creating a osg::Group to contain the nodes if more than one subgraph has been loaded. More...
OSGDB_EXPORT osg::ref_ptr< osg::Shader > readRefShaderFile (const std::string &filename, const Options *options)
Read an osg::Shader from file. More...
osg::ref_ptr< osg::Shader > readRefShaderFile (const std::string &filename)
Read an osg::Shader from file. More...
osg::ref_ptr< osg::Shader > readRefShaderFile (osg::Shader::Type type, const std::string &filename, const Options *options)
Read an osg::Shader from file and set to specified shader type. More...
osg::ref_ptr< osg::Shader > readRefShaderFile (osg::Shader::Type type, const std::string &filename)
Read an osg::Shader from file and set to specified shader type Return valid osg::Shader on success, return NULL on failure. More...
OSGDB_EXPORT osg::ref_ptr< osg::Shader > readRefShaderFileWithFallback (osg::Shader::Type type, const std::string &filename, const Options *options, const char *fallback)
Read an osg::Shader from file and set to specified shader type, if a shader isn't loaded fallback to specific shader source. More...
osg::ref_ptr< osg::Shader > readRefShaderFileWithFallback (osg::Shader::Type type, const std::string &filename, const char *fallback)
Read an osg::Shader from file and set to specified shader type, if a shader isn't loaded fallback to specific shader source. More...
OSGDB_EXPORT osg::ref_ptr< osg::Script > readRefScriptFile (const std::string &filename, const Options *options)
Read an osg::Script from file. More...
osg::ref_ptr< osg::Script > readRefScriptFile (const std::string &filename)
Read an osg::Script from file. More...
OSGDB_EXPORT bool writeObjectFile (const osg::Object &object, const std::string &filename, const Options *options)
Write an osg::Object to file. More...
bool writeObjectFile (const osg::Object &object, const std::string &filename)
Write an osg::Object to file. More...
OSGDB_EXPORT bool writeImageFile (const osg::Image &image, const std::string &filename, const Options *options)
Write an osg::Image to file. More...
bool writeImageFile (const osg::Image &image, const std::string &filename)
Write an osg::Image to file. More...
OSGDB_EXPORT bool writeHeightFieldFile (const osg::HeightField &hf, const std::string &filename, const Options *options)
Write an osg::HeightField to file. More...
bool writeHeightFieldFile (const osg::HeightField &hf, const std::string &filename)
Write an osg::HeightField to file. More...
OSGDB_EXPORT bool writeNodeFile (const osg::Node &node, const std::string &filename, const Options *options)
Write an osg::Node to file. More...
代码段1
搞清楚这些读写函数内部的实现机制,对于熟悉osg并为自己以后编写读取特定文件的插件是至关重要的。
说明:
- 本文采用osg 3.6.2版本进行讲解,读者osg版本如果不是3.6.2版本,代码上可能有细微的差别。
- 本博文以 osgDB::readNodeFile函数读取cow.osg文件为例子讲解,其它的函数思想相似。
2. 预备知识
一般地,我们都是通过类似如下代码读取模型文件到视景器中进行显示的:
int main(int argc, char *argv[])
{
osgViewer::Viewer viewer;
auto pCowNode = osgDB::readNodeFile("cow.osg");
if (nullptr == pCowNode)
{
OSG_WARN << "node is null!";
return 1;
}
auto pRoot = new osg::Group;
pRoot->addChild(pCowNode );
viewer.setSceneData(pRoot);
return viewer.run();
}
代码段2
readNodeFile等函数在include\osgDB\ReadFile文件中,readNodeFile有两个重载函数,如下:
osg::Node* readNodeFile(const std::string& filename);
osg::Node* readNodeFile(const std::string& filename,const Options* options);
代码段3
第1个函数函数内部其实是调用的第2个readNodeFile函数,只不过采用Registry类自带的Options类对象,如下:
inline osg::Node* readNodeFile(const std::string& filename)
{
return readNodeFile(filename,Registry::instance()->getOptions());
}
代码段4
总结:
- 外层调用方可以通过调用第1个函数读取模型,此时不用传入Options类对象,Registry类内部会用自己的Options类对象传入。
- 外层调用方可以通过调用第2个函数读取模型,此时需要自己构造一个Options类对象对象作为第2个参数传入。
- 第1个函数内部其实是调用的第2个函数。
2.1. osgDB::Options类
osgDB库提供了用于保存各种文件读写参数的Options类,其基本定义如下(因为接口太多,下图只列出了一部分,具体参见:include\osgDB\Options文件中Options类):
图1
Options类也可以设置读文件回调函数、将读取的模型缓存起来等。
2.2. osgDB::Registry类
Registry类是一个单类工厂,用于存储一些读写器,这些读写器在运行的时候会被链接到osgDB内部,用于读取一些非标准的文件格式。Registry类的单例功能通过instance函数实现,如下:
Registry* Registry::instance(bool erase)
{
static ref_ptr<Registry> s_registry = new Registry;
if (erase)
{
s_registry->destruct();
s_registry = 0;
}
return s_registry.get(); // will return NULL on erase
}
代码段5
在instance函数函数下面,有如下宏定义:
OSG_INIT_SINGLETON_PROXY(ProxyInitRegistry, Registry::instance())
代码段6
转到该宏定义:
#define OSG_INIT_SINGLETON_PROXY(ProxyName, Func) static struct ProxyName{ ProxyName() { Func; } } s_##ProxyName;
代码段7
注意##表示的含义,如果不知道该含义,请参见:C++/C宏定义中## 连接符与# 符的含义。
该宏为:
#define OSG_INIT_SINGLETON_PROXY(ProxyInitRegistry, Registry::instance())
展开该宏后为:
static struct ProxyInitRegistry
{
ProxyInitRegistry()
{
Registry::instance();
}
} s_ProxyInitRegistry;
代码段8
即该宏调用ProxyInitRegistry结构体的构造函数创建了一个全局的、静态的、名称为ProxyInitRegistry的结构体对象s_ProxyInitRegistry。我们知道全局对象在可执行程序一启动时(在main函数调用之前)或dll加载成功时就会被构造。而在ProxyInitRegistry结构体的构造函数中调用了Registry::instance(),这就保证程序启动的时,Registry类的单例就存在了。
Registry类构造函数主要做了如下工作:
- 查找环境变量OSG_BUILD_KDTREES,以决定是否创建KD树。关于什么是KD树,请自行百度。
- 查找环境变量OSG_EXPIRY_DELAY,以决定删除缓存对象的时期(具体参见removeExpiredObjectsInCache函数)。
- 查找环境变量OSG_FILE_CACHE,以创建文件缓存对象。
- 创建对象缓存成员变量_objectCache。
- 将osga、zip作为默认文件扩展名加入到名为_archiveExtList的vector的成员变量中。
- 注册文件扩展名的别名。将文件扩展名和文件扩展名的别名分别作为key、value插入到_extAliasMap的map容器成员变量中。
- 注册一些获取远程文件的服务协议,以便能能从远程网络端获取文件。
2.3. 文件读写插件
osg对每种数据文件的读取或写入都是以插件形式进行的,插件以动态链接库的形式存在,它们被视为osg系统的可选插件。所谓插件就是在由软件系统公开一系列公共应用程序接口的前提下,由第三方开发人员编写实现的功能模块。这些模块独立于系统的核心模块之外,不存在过多的代码耦合,只要插件的开发者的代码编写规范且经过足够的测试,插件就可以一直稳定且灵活地嵌合在主系统中,如下为主系统和插件之间的关系图:
图2
插件采用职责链的设计模式, 职责链设计模式是一种对象行为模式,即将有可能处理同一个请求的所有对象排列成一条链,并沿着这条链传递请求,直到其中一个对象处理了这个请求为止。这种机制可以有效避免请求发送者和接收者之间的耦合关系。例如,下图所示的一个软件开发任务的请求依次交由企业服务部门、装货部门、送货部门和研发部门检查,并最终决定由研发部门负责这一个工作,从中可以看出,将各个处理部门排列成链并依次检查请求,向后传递请求,直到接受请求的过程,这就是职责链机制工作的基本流程。
图3
关于职责链设计模式更详细的说明,请参考:责任链模式详解
2.4 . osgDB::ReaderWriter类
osg文件读写插件的公共接口类是osgDB::ReaderWriter类。该类接口太多,暂不列出,请自行查看include\osgDB\ReaderWriter文件。所有格式类型的文件读写类,即:无论是osg中已经提供的插件,还是用户自定义的文件格式的导入导出插件,都必须从osgDB::ReaderWriter类派生并根据业务需要重写该类的相应虚函数,否则就违背了插件和主系统的接口规约,导致插件不能嵌入到主系统,从而导致主系统不能读取或导出文件。
2.5. osgDB::ReaderWriter::ReadResult类
这个类保存了读取结果。用户插件实现的过程中可以直接将解析得到的节点、图像、或者Object对象传递给该类,也可以获取工作状态的枚举量,如:
enum ReadStatus
{
NOT_IMPLEMENTED, //!< read*() method not implemented in concrete ReaderWriter.
FILE_NOT_HANDLED, //!< File is not appropriate for this file reader, due to some incompatibility, but *not* a read error.
FILE_NOT_FOUND, //!< File could not be found or could not be read.
ERROR_IN_READING_FILE, //!< File found, loaded, but an error was encountered during processing.
FILE_LOADED, //!< File successfully found, loaded, and converted into osg.
FILE_LOADED_FROM_CACHE, //!< File found in cache and returned.
FILE_REQUESTED, //!< Asynchronous file read has been requested, but returning immediately, keep polling plugin until file read has been completed.
INSUFFICIENT_MEMORY_TO_LOAD //!< File found but not loaded because estimated required memory surpasses available memory.
};
代码段9
3. readNodeFile函数分析
osgDB::registry类的readNode函数是读取文件的入口,其定义如下:
ReaderWriter::ReadResult readNode(const std::string& fileName,const Options* options, bool buildKdTreeIfRequired=true)
{
ReaderWriter::ReadResult result;
if (options && options->getReadFileCallback()) result = options->getReadFileCallback()->readNode(fileName,options);
else if (_readFileCallback.valid()) result = _readFileCallback->readNode(fileName,options);
else result = readNodeImplementation(fileName,options);
if (buildKdTreeIfRequired) _buildKdTreeIfRequired(result, options);
return result;
}
代码段10
在该函数中,分了3种情况读取模型文件,下面详细分析这三种情况。
3.1. 无Options对象且无文件读取回调函数
即对应上述代码的
else result = readNodeImplementation(fileName,options);
代码段11
分支。这是由于调用方在调用readNodeFile函数时,没有向readNodeFile函数传递第2个类型为Options类的参数,像代码段2所示。readNodeImplementation函数会调用 Registry的readImplementation函数,该函数定义如下:
ReaderWriter::ReadResult Registry::readImplementation(const ReadFunctor& readFunctor,Options::CacheHintOptions cacheHint)
{
std::string file(readFunctor._filename);
bool useObjectCache = false;
const Options* options = readFunctor._options;
ObjectCache* optionsCache = options ? options->getObjectCache() : 0;
//Note CACHE_ARCHIVES has a different object that it caches to so it will never be used here
if ((optionsCache || _objectCache.valid()) && cacheHint!=Options::CACHE_ARCHIVES)
{
useObjectCache= options ? (options->getObjectCacheHint()&cacheHint)!=0: false;
}
if (useObjectCache)
{
// search for entry in the object cache.
osg::ref_ptr<osg::Object> object = optionsCache ? optionsCache->getRefFromObjectCache(file, options) : 0;
if (!object && _objectCache.valid()) object = _objectCache->getRefFromObjectCache(file, options);
if (object.valid())
{
if (readFunctor.isValid(object.get())) return ReaderWriter::ReadResult(object.get(), ReaderWriter::ReadResult::FILE_LOADED_FROM_CACHE);
else return ReaderWriter::ReadResult("Error file does not contain an osg::Object");
}
ReaderWriter::ReadResult rr = read(readFunctor);
if (rr.validObject())
{
// search AGAIN for entry in the object cache.
object = _objectCache->getRefFromObjectCache(file, options);
if (object.valid())
{
if (readFunctor.isValid(object.get())) return ReaderWriter::ReadResult(object.get(), ReaderWriter::ReadResult::FILE_LOADED_FROM_CACHE);
else
{
return ReaderWriter::ReadResult("Error file does not contain an osg::Object");
}
}
// update cache with new entry.
if (optionsCache) optionsCache->addEntryToObjectCache(file, rr.getObject(), 0.0, options);
else if (_objectCache.valid()) _objectCache->addEntryToObjectCache(file, rr.getObject(), 0.0, options);
}
else
{
OSG_INFO<<"No valid object found for "<<file<<std::endl;
}
return rr;
}
else
{
ReaderWriter::ReadResult rr = read(readFunctor);
return rr;
}
}
代码段12
本函数说明如下:
- 先检查传入的Options类对象是否为空。如果不为空,则进行第2步。
- 获取Options类对象中类型为ObjectCache的optionsCache,用于后续模型节点对象的缓存。
- 如果optionsCache不为nullptr或者Registry自带的模型缓冲对象_objectCache有效且缓存类型不为CACHE_ARCHIVES且外层通过Options类对象设置了需要缓存模型节点对象,则将需要缓存标志useObjectCache设置为true。
- 根据文件名和Options类对象从_objectCache等map容器中找到以前缓存的osg::Object,如果osg::Object为空,证明没有找到,则从Registry自带的模型缓冲对象_objectCache中找到以前缓存的osg::Object,如果找到了,且不为空且该对象有效,则直接将该对象返还给外层;如果没找到,则调用Registry::read函数读取文件获取模型节点,如果读取成功且有效,则将读取到的模型节点加入到optionsCache缓存对象或_objectCache缓存对象(当optionsCache为空时),然后将节点返还给调用方。
- 如果Options类对象为空,则直接调用Registry::read函数读取文件获取模型节点,如果读取成功且有效,然后将节点返还给调用方。
整个流程如下:
图4
因为无Options类对象,所以该函数前面和Options对象有关的那个if语句不会执行,只执行如下的else代码段:
ReaderWriter::ReadResult rr = read(readFunctor);
代码段13
断点进入read函数,如下:
ReaderWriter::ReadResult Registry::read(const ReadFunctor& readFunctor)
{
for(ArchiveExtensionList::iterator aitr=_archiveExtList.begin();
aitr!=_archiveExtList.end();
++aitr)
{
std::string archiveExtension = "." + (*aitr);
std::string::size_type positionArchive = readFunctor._filename.find(archiveExtension+'/');
if (positionArchive==std::string::npos) positionArchive = readFunctor._filename.find(archiveExtension+'\\');
if (positionArchive!=std::string::npos)
{
std::string::size_type endArchive = positionArchive + archiveExtension.length();
std::string archiveName( readFunctor._filename.substr(0,endArchive));
std::string fileName(readFunctor._filename.substr(endArchive+1,std::string::npos));
OSG_INFO<<"Contains archive : "<<readFunctor._filename<<std::endl;
OSG_INFO<<" archive : "<<archiveName<<std::endl;
OSG_INFO<<" filename : "<<fileName<<std::endl;
ReaderWriter::ReadResult result = openArchiveImplementation(archiveName,ReaderWriter::READ, 4096, readFunctor._options);
if (!result.validArchive()) return result;
osgDB::Archive* archive = result.getArchive();
//if valid options were passed through the read functor clone them
//otherwise make new options
osg::ref_ptr<osgDB::ReaderWriter::Options> options = readFunctor._options ?
readFunctor._options->cloneOptions() :
new osgDB::ReaderWriter::Options;
options->setDatabasePath(archiveName);
osg::ref_ptr<ReadFunctor> rf(readFunctor.cloneType(fileName, options.get()));
result = rf->doRead(*archive);
if (rf->isValid(result))
{
OSG_INFO<<"Read object from archive"<<std::endl;
return result;
}
OSG_INFO<<"Failed to read object from archive"<<std::endl;
}
}
// record the errors reported by readerwriters.
typedef std::vector<ReaderWriter::ReadResult> Results;
Results results;
// first attempt to load the file from existing ReaderWriter's
AvailableReaderWriterIterator itr(_rwList, _pluginMutex);
for(;itr.valid();++itr)
{
ReaderWriter::ReadResult rr = readFunctor.doRead(*itr);
if (readFunctor.isValid(rr)) return rr;
else results.push_back(rr);
}
// check loaded archives.
AvailableArchiveIterator aaitr(_archiveCache, _archiveCacheMutex);
for(;aaitr.valid();++aaitr)
{
ReaderWriter::ReadResult rr = readFunctor.doRead(*aaitr);
if (readFunctor.isValid(rr)) return rr;
else
{
// don't pass on FILE_NOT_FOUND results as we don't want to prevent non archive plugins that haven't been
// loaded yet from getting a chance to test for the presence of the file.
if (rr.status()!=ReaderWriter::ReadResult::FILE_NOT_FOUND) results.push_back(rr);
}
}
// now look for a plug-in to load the file.
std::string libraryName = createLibraryNameForFile(readFunctor._filename);
if (loadLibrary(libraryName)!=NOT_LOADED)
{
for(;itr.valid();++itr)
{
ReaderWriter::ReadResult rr = readFunctor.doRead(*itr);
if (readFunctor.isValid(rr)) return rr;
else results.push_back(rr);
}
}
//If the filename contains a server address and wasn't loaded by any of the plugins, try to find a plugin which supports the server
//protocol and supports wildcards. If not successfully use curl as a last fallback
if (containsServerAddress(readFunctor._filename))
{
ReaderWriter* rw = getReaderWriterForProtocolAndExtension(
osgDB::getServerProtocol(readFunctor._filename),
osgDB::getFileExtension(readFunctor._filename)
);
if (rw)
{
return readFunctor.doRead(*rw);
}
else
{
return ReaderWriter::ReadResult("Could not find the .curl plugin to read from server.");
}
}
if (results.empty())
{
return ReaderWriter::ReadResult("Could not find plugin to read objects from file \""+readFunctor._filename+"\".");
}
// sort the results so the most relevant (i.e. ERROR_IN_READING_FILE is more relevant than FILE_NOT_FOUND) results get placed at the end of the results list.
std::sort(results.begin(), results.end());
ReaderWriter::ReadResult result = results.back();
return result;
}
代码段14
首先进入到一个for循环,该循环对_archiveExtList遍历。_archiveExtList前面提到过,其里面放的是osga和zip这两个文件后缀名。先看看要加载的模型文件名是否以/结尾(Linux下),如果不是,再看看是否以\结尾(Windows下),这样做是检测输入文件名参数的时候,传入的文件名的最后一个字符是否是斜杠分割符,如:E:\osg\OpenSceneGraph-Data\cow.osg\,然后再调用openArchiveImplementation函数,在该函数中,先检查在_archiveCache中是否已经有以前加载过的对应的文件名的模型结果,如果有,则直接返回该结果;如果没,则调用代码段12读取该文件对应的模型节点。_archiveCache是个map类型,如下:
std::map<std::string, osg::ref_ptr<osgDB::Archive> >
其中,键为文件名去掉目录后的名称,值为osgDB::Archive对象的智能引用指针。osgDB::Archive继承关系图如下:
图5
可以看到它是一个osg::ReaderWriter类型。
注意:
osgDB::Archive和前面的Options或Registry类的缓存对象的区别:Options或Registry类的缓存仅仅是把以前读取过的模型对象即osg::Object保存到内存,后续再获取同名文件的模型对象时不用再读取直接将以前的osg::Object返还给调用方;而osgDB::Archive还对缓存的osg::Object做了某些操作,如:压缩等,会更节省内存等。
如果在上段提到的for循环还没读成功,则执行代码段14的48行及以后的代码。48~72行代码功能为:首先遍历_rwList存放的读写器类对象,尝试用每个读写器对象读取传入的文件名,如果读取成功且读取结果(指文件对应的Node对象)有效,则返回结果到外层调用方;类似地,遍历_archiveCache,检查每个osgDB::Archive中是否已经缓存了指定文件名的结果,如果有且结果(指文件对应的Node对象)有效,则返回结果到外层调用方。注意:如果是第一次执行到这里,_rwList、_archiveCache容器都是空的(这两个容器的值是什么时候插入的呢,参见后文描述)。
代码段的第75行调用createLibraryNameForFile函数,该函数首先将文件扩展名都转为小写,便于在_extAliasMap中查找该扩展名对应的别名,因为在Registty类的构造函数中,文件扩展名和该扩展名对应的别名都是以小写插入到_extAliasMap中的(参见前面对Registry类描述)。为何需要建立文件扩展名和扩展名别名的类型为map的_extAliasMap呢?这是因为用户可能需要强制将某种类型与指定插件相关联,如:读取JPG类型文件时,不是用默认的osgdb_jpg插件,而是需要用osgdb_gdal插件来读取,此时就可以调用如下代码:
osgDB::Registry::instance()->addFileExtensionAlias("jpg", "gdal");
之后就可以直接从注册器中得到ReaderWriterGDAL插件对象,并用它来读取jpg,如:
osgDB::ReaderWriter* pRW = osgDB::Registry::instance()->getReaderWriterForExtension("gdal");
if(nullptr != pRW )
{
// 读写jpg
}
接下来,根据不同的操作系统及debug还是release,拼接出osgdb_osgd.dll或osgdb_osg.dll 文件的路径,并在第76行通过调用Registry类的loadLibrary将Plugins osg加载到系统来。在Plugins osg工程的ReaderWriterOSG.cpp、ReaderWriterOSG2.cpp分别定义了OSGReaderWriter和OSGReaderWriter2类。ReaderWriterOSG.cpp存在如下的宏:
REGISTER_OSGPLUGIN(osg, OSGReaderWriter)
ReaderWriterOSG2.cpp存在如下的宏:
REGISTER_OSGPLUGIN( osg2, ReaderWriterOSG2 )
以第1个为例,该宏为:
#define REGISTER_OSGPLUGIN(osg, OSGReaderWriter)
展开宏(以第1个为例,第2个同理),即为如下:
extern "C" void osgdb_osg(void) {}
static osgDB::RegisterReaderWriterProxy<OSGReaderWriter> g_proxy_OSGReaderWriter;
可以看到,这里调用构造函数创建了一个全局的静态对象g_proxy_OSGReaderWriter。我们知道全局对象在可执行程序一启动时(在main函数调用之前)或dll加载成功时就会被构造,即加载osgdb_osgd.dll成功后就会创建该对象,即如下代码的构造函数会被调用:
/** Proxy class for automatic registration of reader/writers with the Registry.*/
template<class T>
class RegisterReaderWriterProxy
{
public:
RegisterReaderWriterProxy()
{
if (Registry::instance())
{
_rw = new T;
Registry::instance()->addReaderWriter(_rw.get());
}
}
........... // 其它代码略
};
而在构造函数中,new出了一个T类型,也就是文件读取器类,本例为OSGReaderWriter类,然后通过Registry类的addReaderWriter方法将构造出来的OSGReaderWriter类对象加入到前文所说的类型为vector的读写器容器_rwList中。
说明:
- 如果想要读取一个自定义格式的文件,则必须像OSGReaderWriter类一样从ReaderWriter类派生,并重写相应的虚函数。
- 假设第1步的实现文件为MyReaderWriter.cpp, 则在MyReaderWriter.cpp文件中利用REGISTER_OSGPLUGIN宏按照前文所述,将自定义读写器注册到系统内部,该宏的第1个参数为该类型文件的扩展名,第2个参数为自定义的读写器类的名称。
- 将1、2步用dll来封装实现,dll文件名格式为:osgdb_ext.dll或osgdb_extd.dll,其中ext为文件扩展名,d表示debug版,没d的表示release版,并将生成的dll放到osgPlugins-xx.yy.zz目录,其中xx.yy.zz为osg版本号。
一旦插件dll加载成功,则代码段14中的第78~83行就会遍历加载成功的插件中含有的读写器,并用该读写器类对象读文件,如果读成功且node节点是有效的,则返还给外层调用方。88~114判断文件是否是远程网络文件,如:以http、ftp等开头的文件,如果是则找到对应的插件读取网络文件,如果所有的插件都没找到,则默认用curl库来加载远程网络文件,如果读成功且node节点是有效的,则返还给外层调用方。整个流程如下图:
图6
代码段14中的第80行对插件中包含的每个读写器尝试读取文件,经过跟踪后发现进入到了OSGReaderWriter类的readNode函数,如下:
virtual ReadResult readNode(std::istream& fin, const Options* options) const
{
loadWrappers();
fin.imbue(std::locale::classic());
Input fr;
fr.attach(&fin);
fr.setOptions(options);
osg::NodeList nodeList;
// load all nodes in file, placing them in a group.
while(!fr.eof())
{
osg::ref_ptr<Node> node = fr.readNode();
if (node.valid()) nodeList.push_back(node);
else fr.advanceOverCurrentFieldOrBlock();
}
if (nodeList.empty())
{
return ReadResult("No data loaded");
}
else if (nodeList.size()==1)
{
return nodeList.front();
}
else
{
Group* group = new Group;
group->setName("import group");
for(NodeList::iterator itr=nodeList.begin();
itr!=nodeList.end();
++itr)
{
group->addChild(*itr);
}
return group;
}
}
代码段15
在该函数通过调用loadWrappers函数加载osgPlugins-3.6.2/osgdb_deprecated_osgd.dll,即Plugins osg deprecated osg插件被加入到系统,可以看到该插件中的很多cpp文件都有如下的宏定义(以Group.cpp为例讲解):
// register the read and write functions with the osgDB::Registry.
REGISTER_DOTOSGWRAPPER(Group)
(
new osg::Group,
"Group",
"Object Node Group",
&Group_readLocalData,
&Group_writeLocalData
);
代码段16
这个宏的作用就是利用osgDB::Registry类向系统注册读写相关的功能函数。将代码段16进行宏展开为:
extern "C" void dotosgwrapper_Group(void) {}
static osgDB::RegisterDotOsgWrapperProxy dotosgwrapper_proxy_Group(
new osg::Group,
"Group",
"Object Node Group",
&Group_readLocalData,
&Group_writeLocalData
);
代码段17
即调用构造函数定义了一个全局的类型为osgDB::RegisterDotOsgWrapperProxy的静态对象dotosgwrapper_proxy_Group。我们知道全局对象在可执行程序一启动时(在main函数调用之前)或dll加载成功后就会被构造。而osgDB::RegisterDotOsgWrapperProxy构造函数如下:
RegisterDotOsgWrapperProxy::RegisterDotOsgWrapperProxy(osg::Object* proto,
const std::string& name,
const std::string& associates,
DotOsgWrapper::ReadFunc readFunc,
DotOsgWrapper::WriteFunc writeFunc,
DotOsgWrapper::ReadWriteMode readWriteMode)
{
if (Registry::instance())
{
_wrapper = new DotOsgWrapper(proto,name,associates,readFunc,writeFunc,readWriteMode);
Registry::instance()->getDeprecatedDotOsgObjectWrapperManager()->addDotOsgWrapper(_wrapper.get());
}
}
代码段18
- 第1个参数proto:表示要处理的osg对象(如:osg::Group)的指针。
- 第2个参数name:表示要处理的osg对象的节点名称(如:Group)。
- 第3个参数associates:表示第1个参数在继承链向上除了osg::Referenced之外的父节点、祖父节点的名称和参数1对象自身的节点名称,且名称之间用空格隔开,如:Object Node Group。
- 第4个参数readFunc:表示用于反序列化(即从文件读取)属性到第1个参数上时调用的读取函数。
- 第5个参数writeFunc:表示将参数1指向的对象序列化(即将对象属性写入到文件)到文件时调用的写入函数。
- 在构造函数中会调用addDotOsgWrapper将osg的节点名称和名称对应的对象构成一个名称为_objectWrapperMap的map,便于后续流程对osg文件的每个节点遍历及属性绑定。如下为_objectWrapperMap的内容:
图7
之后,程序进入到代码段15的第16行,即循环文件中的每个节点对象并开始反序列化(即从文件读取)属性到代码段18中的第1个参数指向的osg对象上。osg格式的文件其实就是文本文件,用记事本打开如下:
图8
反序列化是在如下代码实现的:
osg::Object* DeprecatedDotOsgWrapperManager::readObject(DotOsgWrapperMap& dowMap,Input& fr)
{
......................................// 其它代码略
for(DotOsgWrapper::Associates::const_iterator aitr=assoc.begin();
aitr!=assoc.end();
++aitr)
{
DotOsgWrapperMap::iterator mitr = _objectWrapperMap.find(*aitr);
if (mitr==_objectWrapperMap.end())
{
......................................// 其它代码略
if (mitr!=_objectWrapperMap.end())
{
DotOsgWrapper::ReadFunc rf = mitr->second->getReadFunc();
if (rf && (*rf)(*obj,fr)) iteratorAdvanced = true;
}
}
if (!iteratorAdvanced) fr.advanceOverCurrentFieldOrBlock();
}
......................................// 其它代码略
return obj;
}
return 0L;
}
代码段19
其思路为:
- 根据节点名从_objectWrapperMap中找到对应的节点对象,如:根据“Group”找到osg::Group,然后将UniqueID和osg::Group构成键值对存入到map
- assoc容器中存放的是字符串Object、 Node、 Group(根据前文所述,其分别为Group的父或祖父或自身名称)。在for循环中遍历assoc,取出每个字符串并从_objectWrapperMap找到对应的对象,并调用该对象的读函数进行属性的读取,并将读取出来的属性绑定到对象上,如:osg:Object调用osg:Object自己的读函数、osg::Node调用osg::Node的读函数、osg::Group调用osg::Group的读函数。
- 步骤1、2完成后,对象的属性就全部获取到了,之后就把该对象返还外层。
代码段19中的16行获取的就是代码段18中第4个参数设置的读函数的指针 ,代码段19中的17行就是调用读函数读取对象属性。整个流程如下图所示:
图9
至此,readNodeFile读文件流程完成!
3.2. 有Options对象且其文件读取回调类有效
这种情况对应代码段10 中的if语句,即:
if (options && options->getReadFileCallback()) result = options->getReadFileCallback()->readNode(fileName,options);
有时要对读文件加以控制,如:
- 像前文提到,如果同一个文件需要读多次,可以先读取一次,获取到osg::Object对象后,将其缓存起来,再次获取时,直接从缓存中将对象返回给调用方,提高效率。
- 在读文件之前或之后,需要做些准备或清理工作。
- .........................。
以上可以通过设置osgDB::Options类的属性控制。osgDB::Options类的属性和方法很多,具体参见include\osgDB\Options。这里只说明缓存和读回调。
3.2.1. 利用Options设置读缓存
前文讲解了读缓存机制,下面代码:
int main(int argc, char *argv[])
{
osgViewer::Viewer viewer;
osg::ref_ptr<osgDB::Options>spOptions = new osgDB::Options();
spOptions->setObjectCacheHint(osgDB::Options::CacheHintOptions::CACHE_ALL);
osg::ref_ptr<osgDB::ObjectCache>spObjectCache = new osgDB::ObjectCache();
spOptions->setObjectCache(spObjectCache);
auto pCowNode = osgDB::readNodeFile(R"(E:\osg\OpenSceneGraph-Data\cow.osg)", spOptions);
if (nullptr == pCowNode)
{
OSG_WARN << "node is null!";
return 1;
}
auto pCowNode1 = osgDB::readNodeFile(R"(E:\osg\OpenSceneGraph-Data\cow.osg)", spOptions);
if (nullptr == pCowNode1)
{
OSG_WARN << "node is null!";
return 1;
}
auto pRoot = new osg::Group;
pRoot->addChild(pCowNode);
pRoot->addChild(pCowNode1);
viewer.setSceneData(pRoot);
return viewer.run();
}
代码段20
上面代码构造ObjectCache对象并将其设置到新构造的Options类对象上,然后再将Options类对象作为第2个参数传给readNodeFile。当读取pCowNode1节点时,就不会从文件读取,而是从缓存中直接取出osg::Object对象,具体参见前面的分析。
3.2.2. 设置读回调类
当在读文件之前或之后,需要做些准备或清理工作时,就可以设置一个读回调类对象,当读取文件时,会执行预先读回调类的readNode函数。读回调类对象可通过Registry类来设置,也可通过Options类设置,以Options类设置的回调为优先,即Options类没设置读回调类对象时,才会调用Registry类来设置的读回调类。如下:
#include<osgViewer/Viewer>
#include<osgDB/readFile>
class MyReadFileCallback : public osgDB::Registry::ReadFileCallback
{
public:
virtual osgDB::ReaderWriter::ReadResult readNode(const std::string& fileName, const osgDB::ReaderWriter::Options* options)
{
std::cout << "before readNode" << std::endl;
// note when calling the Registry to do the read you have to call readNodeImplementation NOT readNode, as this will
// cause on infinite recusive loop.
osgDB::ReaderWriter::ReadResult result = osgDB::Registry::instance()->readNodeImplementation(fileName, options);
std::cout << "after readNode" << std::endl;
return result;
}
};
int main(int argc, char *argv[])
{
osgViewer::Viewer viewer;
osg::ref_ptr<osgDB::Options>spOptions = new osgDB::Options();
auto pMyCallBack = new MyReadFileCallback();
spOptions->setReadFileCallback(pMyCallBack);
// osgDB::Registry::instance()->setReadFileCallback(pMyCallBack);
auto pCowNode = osgDB::readNodeFile(R"(E:\osg\OpenSceneGraph-Data\cow.osg)", spOptions);
if (nullptr == pCowNode)
{
OSG_WARN << "node is null!";
return 1;
}
auto pRoot = new osg::Group;
pRoot->addChild(pCowNode);
viewer.setSceneData(pRoot);
return viewer.run();
}
代码段21
在代码段21中,构造一个读回调类对象和一个Options对象,将读回调类对象设置到了Options对象上,此时读取文件时,就会调用MyReadFileCallback类的readNode函数,在该函数中做一些准备、读文件、清理工作。也可以不构造Options对象对象,而将第25行注释取消,利用Registry类设置读回调,且向osgDB::readNodeFile函数只传一个文件名的函数即可。
3.3. 整个流程宏观分析
如下为osgDB::readNodeFile的整个流程图:
图10
如下为整个流程的实现机制(以读取扩展名osg的文件为例):
图11
osgDB::ReadNodeFile三大核心功能模块为:
- osgDB::Registry提供接口用于外层向本类注册读写器,并接收转发外层的读写调用。
- 读写文件的插件如:Plugins osg 插件负责向osgDB::Registry注册扩展名为osg的文件读写器,并将读写任务转发到读写属性插件。
- 读写属性插件如:Plugins osg deprecated osg,则负责读写osg相关对象的属性,并将这些属性绑附到对象上。
以上三者相互配合,且降低耦合,便于代码维护、功能扩展。
4. 后记
osgDB::Read开头的几个读函数原理和流程和osgDB::ReadNodeFile类似,在此不再详述。看懂了上面的分析,理解osgDB::writeNodeFile函数就很简单了,顾名思义其是readNodeFile的逆函数。