55 序列化支持
登录

序列化支持

作者: osgChina 发布于2018-10-22 16:43:34 分类 : 用户指南

OSG自支持格式的升级版是在2010年提出的,它的目的是方便扩展、跨格式可持续更新且支持任何OSG已有的格式。比如它可以代表图片,可以代表heighfield,可以代表模型等等。

这个升级版的插件代码在此处: src/osgPlugins/osg/ReaderWriterOSG2.cpp and wrappers at src/osgWrappers/serializers.

它支持两种格式:

  • osgb二进制格式

  • osgt Ascii格式

支持的操作为:

  • WriteImageHint=<hint> (Export option) Hint of writing image to stream.

  • <IncludeData> writes Image::data() directly;

  • <IncludeFile> writes the image file itself to stream;

  • <UseExternal> writes only the filename;

  • <WriteOut> writes Image::data() to disk as external file.

默认情况使用osg::Image::getWriteHint() 判断是否保存文件还是只保存文件名:

  • Compressor=<name> (Export option) 使用内建的还是用户指定的压缩方式来压缩流,只影响二进制格式。

  • SchemaFile=<file> (Import/Export option) 使用一个Ascii格式的方案文件来定义读写属性顺序,可选的。

  • ForceReadingImage (Import option) 当模型中指定了纹理的文件名时,假设读不到该纹理也会生成个空的纹理,这样用于保存纹理的纪录。

  • Ascii (Import/Export option) 使用该选项会输出/输出ASCII编码方式。

主要特性:

  • Serialization I/O: 序列化的支持是为了在这台电脑上生成的文件在其它的电脑上也可以运行,读写一致,这是最基本的功能。可以通过osgDB/Serializer头文件查看详细信息。

  • Binary/ascii file format: 二进制和ascii在 osgDB/StreamOperator中是使用不同的类来实现的,可以在osgPlugins/osg文件夹中找到具体实现。

  • Object wrappers:每一个可以添加到场景的节点都有一个ObjectWrappers,作的做用是定义了该节点序列化与反序列化的属性顺序以及属性的get和set方法。可以在osgDB/ObjectWrapper头文件中查看详细信息。

  • Compressors and decompressors: 压缩和解压是一为了减小文件大小,二是为了对文件进行加解密。在osgDB/ObjectWrapper中查看实现。整个输入输出管理在osgDB/OutputStream 和osgDB/InputStream类中实现。

  • Extendability: 这套机制具备良好的可扩展性,可以随时从osg::Object派生自己的序列化与反序列化内容,并像一般的节点一样实现在osgb或osgt中被输入输出。

  • Schema Definitions: 通过外部文件定义了模型属性的读写顺序,它的作要作用是假设某个版本的OSG不提供某个属性的读写功能,可以在该文件中进行定义,以便某版本的OSG能够兼容该文件。

快速开始

1.1 使用OSGCONV

OSGCONV工具可以方便的创建OSG2格式,下面的命令是转换一个cow到osgb,然后查看这个文件:

# ./osgconv cow.osg cow.osgb
# ./osgviewer cow.osgb

ASCII:

# ./osgconv cow.osg cow.osgt
# ./osgviewer cow.osgt

To write out with specified writing image hint:

# ./osgconv cow.osg cow.osgb -O WriteImageHint=IncludeData
# ./osgviewer cow.osgb

使用内置的zlib进行压缩:

# ./osgconv cow.osg cow.osgb -O Compressor=zlib
# ./osgviewer cow.osgb

使用方案文件:

# ./osgconv cow.osg cow.osgb -O SchemaFile=osg_schema_2.9.7.txt
# ./osgviewer cow.osgb -O SchemaFile=osg_schema_2.9.7.txt

1.2 在程序中

需要的插件:osgdb_osg2.so (.dll), 以及相关的序列化扩展 osgdb_serializers_* 库,如果自定义了压缩和解压方式,还需要用户自定义的osgdb_compressor_* 库.

可以直接读取文件:

osg::ref_ptr<osg::Node> loadedModel = osgDB::readNodeFile("cow.osgb");

写一个场景到二进制当中,使用"WriteImageHint" 和 "Compressor":

osgDB::writeNodeFile(*node, "cow.osgb", new osgDB::Options("WriteImageHint=IncludeData Compressor=zlib"));

可以直接从流中获取:

osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension("osgt");
if (rw)
{
    osgDB::ReaderWriter::ReadResult rr = rw->readNode(istream);
    return rr.takeNode();
}

以Ascii方式写:

osgDB::ReaderWriter* rw = osgDB::Registry::instance()->getReaderWriterForExtension("osgt");
if (rw)
{
    rw->writeNode(*node, ostream, new osgDB::Options("Ascii"));
}

如何自定义一个扩展

2.1 BASIC STRUCTURE

一个正确的扩展需要完全能够从文件中读取该模型需要的所有属性,并正确的将属性解析出来。

一些序列化宏按顺序放用来完成属性的setting/getting操作,子类要确保连父类的属性一起输出。

一个基本的扩展写法是:

REGISTER_OBJECT_WRAPPER( Node,                      // The unique wrapper name
                         new osg::Node,             // The proto
                         osg::Node,                 // The class typename
                         "osg::Object osg::Node" )  // The inheritance relations
{
    // Serializers for different members
    ADD_OBJECT_SERIALIZER( UpdateCallback, osg::NodeCallback, NULL );
    ADD_BOOL_SERIALIZER( CullingActive, true );
    ADD_HEXINT_SERIALIZER( NodeMask, 0xffffffff );
    ...
}

一些宏在此处定义用来很简单来读取属性结构体, REGISTER_OBJECT_WRAPPER 会在初始化时加入到全局的扩展管理器中。ADD_*_SERIALIZER 这样的宏被用来定义该模型的属性顺序当然也可以把它们输出成方案文件。

注意osg::*参数非常关键,它被告知使用osgdb_serializers_osg.so来加载这些扩展,换成其它的就完全不同,比如osgParticle。

2.2 PREDEFINED SERIALIZERS(系统预置的序列化宏)

很明显ADD_BOOL_SERIALIZER(CullingActive) 会调用 setCullingActive() 和getCullingActive(),而ADD_HEXINT_SERIALIZER(NodeMask) 会调用setNodeMask() 和getNodeMask() 。下面是预定义的一些其它的序列化宏。

ADD_BOOL_SERIALIZER( NAME, DEF )

Input/output with void setNAME(bool) and bool getNAME() const methods. DEF is the default value of the proto, which will not be saved into files.

ADD_SHORT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(short) and short getNAME() const methods.

ADD_USHORT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(unsigned short) and unsigned short getNAME() const methods.

ADD_HEXSHORT_SERIALIZER( NAME, DEF )

Same as ADD_USHORT_SERIALIZER, but use hex values instead.

ADD_USHORT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(unsigned short) and unsigned short getNAME() const methods.

ADD_INT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(int) and int getNAME() const methods.

ADD_UINT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(unsigned int) and unsigned int getNAME() const methods.

ADD_HEXINT_SERIALIZER( NAME, DEF )

Same as ADD_UINT_SERIALIZER, but use hex values instead.

ADD_FLOAT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(float) and float getNAME() const methods.

ADD_DOUBLE_SERIALIZER( NAME, DEF )

Input/output with void setNAME(double) and double getNAME() const methods.

ADD_VEC3F_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Vec3f&) and const Vec3f& getNAME() const methods.

ADD_VEC3D_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Vec3d&) and const Vec3d& getNAME() const methods.

ADD_VEC3_SERIALIZER( NAME, DEF )

Same as ADD_VEC3F_SERIALIZER.

ADD_VEC4F_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Vec4f&) and const Vec4f& getNAME() const methods.

ADD_VEC4D_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Vec4d&) and const Vec4d& getNAME() const methods.

ADD_VEC4_SERIALIZER( NAME, DEF )

Same as ADD_VEC4F_SERIALIZER.

ADD_QUAT_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Quat&) and const Quat& getNAME() const methods.

ADD_PLANE_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Plane&) and const Plane& getNAME() const methods.

ADD_MATRIXF_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Matrixf&) and const Matrixf& getNAME() const methods.

ADD_MATRIXD_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Matrixd&) and const Matrixd& getNAME() const methods.

ADD_MATRIX_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const Matrix&) and const Matrix& getNAME() const methods.

ADD_STRING_SERIALIZER( NAME, DEF )

Input/output with void setNAME(const std::string&) and const std::string& getNAME() const methods.

ADD_GLENUM_SERIALIZER( NAME, TYPE, DEF )

Input/output with void setNAME(TYPE) and TYPE getNAME() const methods. TYPE here could be GLenum, GLbitfield, GLint and so on, to fit different method parameters. In ascii format, this serializer gets numerical values and saves corresponding OpenGL enumeration names to the buffer, and read it back in the opposite way. For example, it will map GL_NEVER to the string "NEVER", and vice versa.

ADD_OBJECT_SERIALIZER( NAME, TYPE, DEF )

Input/output with void setNAME(TYPE*) and const TYPE* getNAME() const methods. This serializer is used to record another object attached, that is, the wrapper of another object class will be called inside current reading/writing function and cause iteration of functions.

ADD_IMAGE_SERIALIZER( NAME, TYPE, DEF )

Same as ADD_OBJECT_SERIALIZER, but only read osg::Image* and inherited instances.

ADD_LIST_SERIALIZER( NAME, TYPE )

Input/output with void setNAME(const TYPE&) and const TYPE& getNAME() const methods. TYPE should be a std::vector like typename, because the serializer will assume a TYPE::const_iterator internal to traverse all elements.

ADD_USER_SERIALIZER( NAME )

Add a user-customizied serializer, with at least 3 static user functions for checking, reading and writing properties. See Chapter 2.3 for details.

BEGIN_ENUM_SERIALIZER( NAME, DEF )

Input/output with void setNAME(NAME) and NAME getNAME() const methods. This is used only for enum values, and the enum name and methods' names should strictly obey the naming rules. Another two macros ADD_ENUM_VALUE and END_ENUM_SERIALIZER will be also used to form a complete serializer.

下面是在osg::Object扩展中使用BEGIN_ENUM_SERIALIZER的例子:

BEGIN_ENUM_SERIALIZER( DataVariance, UNSPECIFIED );
    ADD_ENUM_VALUE( STATIC );
    ADD_ENUM_VALUE( DYNAMIC );
    ADD_ENUM_VALUE( UNSPECIFIED );
END_ENUM_SERIALIZER();

osg::Object::DataVariance 有三个枚举值: STATIC, DYNAMIC and UNSPECIFIED (默认). 它们在ASCII格式时,会被自动映射为:"STATIC", "DYNAMIC" and "UNSPECIFIED"。

如果枚举类型在 Class::NAME中不存在,则BEGIN_ENUM_SERIALIZER会失败,使用BEGIN_ENUM_SERIALIZER2则没有问题,如下:

BEGIN_ENUM_SERIALIZER2( Hint, osg::Multisample::Mode, DONT_CARE );
    ADD_ENUM_VALUE( FASTEST );
    ADD_ENUM_VALUE( NICEST );
    ADD_ENUM_VALUE( DONT_CARE );
END_ENUM_SERIALIZER();

绑定的函数为 setHint(osg::Multisample::Mode) and osg::Multisample::Mode getHint() const.

使用预定义宏可以很方便的定义简单类:

REGISTER_OBJECT_WRAPPER( Box,
                         new osg::Box,
                         osg::Box,
                         "osg::Object osg::Shape osg::Box" )
{
    ADD_VEC3_SERIALIZER( Center, osg::Vec3() );  // _center
    ADD_VEC3_SERIALIZER( HalfLengths, osg::Vec3() );  // _halfLengths
    ADD_QUAT_SERIALIZER( Rotation, osg::Quat() );  // _rotation
}

不超过10行,osg::Box类就定义好了,在二进制下会被存为一些整型,浮点型等。下面是ASCII格式:

osg::Box {
    ...
    Center 10 0 0
    HalfLengths 1 1 1
    Rotation 1 0 0 1
    ...
}

osg::Shape 和 osg::Object 的属性也会被记录,因为他们是基类。

2.3 CUSTOM SERIALIZERS(自定义序列化)

总有一些情况,无法用命名规则来简单的使用宏来处理,比如osg::StateSet下的setTextureAttribute() 和getTextureAttribute(),它们需要一个入参unit,暂时不能被任何的预定义宏支持。当前情况下就可以使用ADD_USER_SERIALIZER来实现。

我们来拿osg::Group做一个例子。一个Group有很多孩子,但是却没有一个 setChildren() 或者getChildren() 方法,实现这个功能可以如下:

static bool checkChildren( const osg::Group& node );
static bool writeChildren( osgDB::OutputStream& os, const osg::Group& node );
static bool readChildren( osgDB::InputStream& is, osg::Group& node );

REGISTER_OBJECT_WRAPPER( Group,
                         new osg::Group,
                         osg::Group,
                         "osg::Object osg::Node osg::Group" )
{
    ADD_USER_SERIALIZER( Children );  // _children
}

ADD_USER_SERIALIZER 宏名为 "Children" 为osg::Group 类服务,编译时需要三个静态函数:

  • bool checkChildren(const osg::Group&) 用来判断当前属性是否需要记录,空指针,默认值和0个孩子都不需要输出

return node.getNumChildren()>0;  // Continue only if there is any child node to write
  • bool writeChildren(osgDB::OutputStream&, const osg::Group&) 使用 OutputStream将属性输出,经常敷衍getChild功能,并输出:

unsigned int size = node.getNumChildren();
os << size << osgDB::BEGIN_BRACKET << std::endl;
for ( unsigned int i=0; i<size; ++i )
{
    os.writeObject( node.getChild(i) );
}
os << osgDB::END_BRACKET << std::endl;
return true;
  • bool readChildren(osgDB::InputStream&, osg::Group&) 用来从InputStream 中读取 Group 实例,  使用 addChild()加到组中。

unsigned int size = 0; is >> size >> osgDB::BEGIN_BRACKET;
for ( unsigned int i=0; i<size; ++i )
{
    osg::Node* child = dynamic_cast<osg::Node*>( is.readObject() );
    if ( child ) node.addChild( child );
}
is >> osgDB::END_BRACKET;
return true;

OutputStream 接受<< 重载操作,因此需要对类定义 writeObject(), writeImage(), writePrimitiveSet() 和 writeArray()。同样InputStream 重载 >> 需要对类定义 readObject(), readImage(), readPrimitiveSet() 和readArray() 来读取数据到类中。

上面还使用到了BEGIN_BRACKET 和END_BRACKET 宏,它指示了正在读取子项。另一个有用的宏是PROPERTY用来标识属性名。PROPERTY计划在XML模式下支持。

ASCII:

os << osgDB::PROPERTY("Account") << osgDB::BEGIN_BRACKET << std::endl;
os << osgDB::PROPERTY("ID") << (int)1 << std::endl;
os << osgDB::PROPERTY("Name"); os.writeWrappedString("Wang Rui"); os << std::endl;
os << osgDB::PROPERTY("Salary") << (float)25.5 << std::endl;
os << osgDB::END_BRACKET << std::endl;

结果为:

Account {
    ID 1
    Name "Wang Rui"
    Salary 25.5
}

逆过程:

std::string name; int id; float salary;
is >> osgDB::PROPERTY("Account") >> osgDB::BEGIN_BRACKET;
is >> osgDB::PROPERTY("ID") >> id;
is >> osgDB::PROPERTY("Name"); is.readWrappedString(name);
is >> osgDB::PROPERTY("Salary") >> salary;
is >> osgDB::END_BRACKET;

注意: PROPERTY 和std::string内容不能含有空格,因为它们使用了<<和>>运算符,会把空格当成分隔符,会打散字符串和读取顺序。可以使用writeWrappedString() 和 readWrappedString() 代替。

BEGIN_BRACKET, END_BRACKET 和PROPERTY在二进制结果下无任何影响。

下面还有一些常用的功能,用在用户自定义序列化当中:

  • InputStream::matchString(const std::string&)判断下一个输入的stream是否与入参相符,若不符则回滚。仅工作在ASCII模式下。

  • InputStream::advanceToCurrentEndBracket() 丢弃所有的数据,直到遇到 END_BRACKET,仅工作在ASCII模式下。

  • InputStream::throwException(const std::string&) 和 OutputStream::throwException(const std::string&) 会抛出致命的异常,终止当前解析。

  • BEGIN_USER_TABLE 宏的样子与BEGIN_ENUM_SERIALIZER有点相似,但是它会定义一个枚举,还会定义一个读和写的功能函数。仅工作在ASCII模式下。

BEGIN_USER_TABLE( Mode, osg::PolygonMode );
    ADD_USER_VALUE( POINT );
    ADD_USER_VALUE( LINE );
    ADD_USER_VALUE( FILL );
END_USER_TABLE()

USER_READ_FUNC( Mode, readModeValue )
USER_WRITE_FUNC( Mode, writeModeValue )

输出polygonMode:

writeModeValue(os, (int)pm.getMode(osg::PolygonMode::FRONT));

输入:

int value = readModeValue(is);
pm.setMode(osg::PolygonMode::FRONT, static_cast<osg::PolygonMode::Mode>(value));

2.4 自定义compressor/decompressor

我们已经自定义了一个ZLIB的压缩器,在src/osgDB/Compressor.cpp中,但是我们可能希望自定义压缩器,只需要重写两个虚函数就可以了。

下面的例子会在输出文件头增加版权信息,注意REGISTER_COMPRESSOR宏需要在别处定义:

class TestCompressor : public osgDB::BaseCompressor
{
public:
    TestCompressor() {}
    
    virtual bool compress( std::ostream& fout, const std::string& src )
    {
        std::string info("Written by Wang Rui, (C) 2010");
        int infoSize = info.size();
        fout.write( (char*)&infoSize, INT_SIZE );
        fout.write( info.c_str(), infoSize );
        
        int size = src.size();
        fout.write( (char*)&size, INT_SIZE );
        fout.write( src.c_str(), src.size() );
        return true;
    }
    
    virtual bool decompress( std::istream& fin, std::string& target )
    {
        std::string info;
        int infoSize = 0; fin.read( (char*)&infoSize, INT_SIZE );
        if ( infoSize )
        {
            info.resize( infoSize );
            fin.read( (char*)info.c_str(), infoSize );
            osg::notify(osg::INFO) << info << std::endl;
        }
        
        int size = 0; fin.read( (char*)&size, INT_SIZE );
        if ( size )
        {
            target.resize( size );
            fin.read( (char*)target.c_str(), size );
        }
        return true;
    }
};

REGISTER_COMPRESSOR( "test", TestCompressor )

新的TestCompressor类会被放在osgdb_compressor_test.so 库中,使用下面的命令可以使用它:

# ./osgconv cow.osg cow.osgb -O Compressor=test
# ./osgviewer cow.osgb

下一步

  1. 完成所有OSG核心类的wrappers,除了osg, osgText, osgParticle,做足测试以尽快的把新机制和插件发布。

  2. 在当前的基础上小改即可支持XML。

  3. 压缩浮点型和整型数组,以减小文件大小。

  4. 二进制版本的schema文件是需要的,它可以有效的改进不同版本之间的OSG模型文件的兼容性。我们会把每个schema文件内置发布在OSG版本中,自动匹配当前版本。

  5. 考虑重写 osgIntrospection,该类提供了 introspection/reflection framework for runtime querying and calling of class properties. This may be done by serializers in a slightly different way now.

  6. So, what is next? :)