【行业知识】Alembic文件格式简介–设计思想和理念 4

10 九月, 2016
14
0

Alembic

深入Simple Property

记住容器层次关系:Simple Property包含Sample;Sample包含data。大多数情况下,Sample和data可以当做一
回事,大部分客户端代码用同样的方式来创建和操控property。我们将处理Abc,其以数据为核心而不是几何体。
随后,我们会介绍AbcGeom层如何使用Abc层的Property来构建几何体的数据。

OScalarProperties

首先,我们将关注“O”开头,写入的API。在Abc层,Simple Property被进一步分成有类型和无类型的,就像Scalar和Array。无类型的
OScalarProperties在lib/Alembic/Abc/OScalarProperty.h里定义,和有类型OTypedScalrProperty最大的区别在于他们接收void*形式的
数据,并且他们必须用AbcA::DataType的实例来构造,是描述样本中几何体大小和类型的类。因此Alembic可以精确跟踪资源。
OTypedScalarProperty,另一方面,可以推测传给他的数据大小和类型,因为他知道自己支持啥类型的数据。大部分客户端会处理有类型
的数据,既然OTypedScalarProperty是一个OScalarProperty,某些方法以及OTypedScalarProperty的成员都来自父类。此章重点在
OtypedScalarProperties.

请看OTypedScalarProperty的构造过程,主要过程如下:

1
2
3
4
5
6
7
8
9
10
11
//! Create a new TypedScalarProperty
//! as a child of the passed COMPOUND_PTR
//! Arguments can specify metadata, timesampling, and error handling.
template<class COMPOUND_PTR>
OTypedScalarProperty(
COMPOUND_PTR iParent,
const std::string&iName,
constArgument&iArg0 =Argument(),
constArgument&iArg1 =Argument(),
constArgument&iArg2 =Argument());

模板参数,COMPOUND_PTR,是任何可以被转换成AbcA::CompoundPropertyWritePtr(“入侵转换”就是“has-a”或者“is-a”
CompoundPropertyWriterPtr,其可以从继承类型中抽取出来,或者从继承类转换。最后的三个可选参数,Abc::Argument是
boost::variants,是一个union类型的类,会简单解释下,但给了点该传什么类型的线索。因为参数是多类型的,你可以多传,也可以少
传,而不用担心调用的位置。

如果前面的没看懂别着急,很多Alembic的内容其意义都不明确,将其分解下:

  1. 必备参数,iParent,是property的父节点:客户端构造的Alembic接口必须按照层级来,但不包括Archive。
  2. Simple property的父节点必定是一个Compound Property。
  3. 第二个参数是名字,在所属层必须唯一。
  4. simple property构造时有一个可选参数TimeSampling的实例。
  5. Abc中的类型是基于对象的封装器,封装了AbcCoreAbstract中更底层的几何类型,每个OCompoundProperty有一个
    AbcA::CompoundPropertyWriterPtr.

这些是关于Simple Property的总体思想,还有很多具体的,不是针对Simple Property,而是将Alembic作为一个整体:

  1. 前缀“Ptr”,就像AbcA::CompoundPropertyWriterPtr里面的,意味着其类型是boost::shared_pro的指针,指向前面的实例
    (前面的例子,就是AbcA::CompoundPropertyWriter);
  2. Alembic中的实体可以用metaData来构建,比如,用AbcA::MetaData的实例,其是一个string,string的关键字—值的哈希表。

实际上,用来写的Simple Property,不仅仅是OScalarProperties,都有一个Abc::OCompoundProperty。

总之,从OScalarProperities跑题了,回归正题。如果你查看lib/Alembic/Abc/OTypedScalarProperty.h,在文件最后面,有很多typedef
定义模板化OTypedScalarProperties,让其有一个好记的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedefOTypedScalarProperty<BooleanTPTraits> OBoolProperty;
typedefOTypedScalarProperty<Uint8TPTraits> OUcharProperty;
typedefOTypedScalarProperty<Int8TPTraits> OCharProperty;
typedefOTypedScalarProperty<Uint16TPTraits> OUInt16Property;
typedefOTypedScalarProperty<Int16TPTraits> OInt16Property;
typedefOTypedScalarProperty<Uint32TPTraits> OUInt32Property;
typedefOTypedScalarProperty<Int32TPTraits> OInt32Property;
typedefOTypedScalarProperty<Uint64TPTraits> OUInt64Property;
typedefOTypedScalarProperty<Int64TPTraits> OInt64Property;
typedefOTypedScalarProperty<Float16TPTraits> OHalfProperty;
typedefOTypedScalarProperty<Float32TPTraits> OFloatProperty;
typedefOTypedScalarProperty<Float64TPTraits> ODoubleProperty;
typedefOTypedScalarProperty<StringTPTraits> OStringProperty;
typedefOTypedScalarProperty<WstringTPTraits> OWstringProperty;

模板参数,各种“TPTraints”,和STL的traints是对应的,特殊点的连每个Property占多少字节都有。这些
traits在lib/Alembic/Abc/TypedPropertyTraits.h里面定义,如果你想看一眼。当你构造OTypedScalarProperty时,你会这样做:

OTypedScalarProperty myProp( parent,“this_is_the_property_name”); 

取而代之,你会调用其中的dypedef名字,例如

ODoubleProperty myDoubleProp( parent,“this_is_the_property_name”); 

所有的Alembic都会知道这将保存一个64位的浮点数。

说到property保存的值,其有一个方法set(),带签名的如下:
voidset(const value_type &iVal )

参数类型的声明,value_type,在OTypedScalarProperty.h的顶部定义,并为Property定义了traits的模板。对ODoubleProperty,
value_type是Alembic::Util::float64_t。请查看lib/Alembic/Util/PlainOldDataType.h中的具体类型。返回手头上的事:
在Property里面存储值。比如你构造ODoubleProperty,“myDoubleProperty”就像上面展示的。因为它不是靠TimeSampling实例来构
造的,那就会使用默认的TimeSampling,并假设数据从0.0开始,后面的样本会比前面的样本多1.0chrono_ts.下面的代码在
myDoubleProp上设置10个样本。

1
2
3
4
for( size_t i =0; i <10;++i )
{
myDoubleProp.set(2.0* i );
}

现在,调用myDoubleProp.getNumSamples(),将会返回10. 第0个样本为0.0, 第一个是2.0, 第二个是4.0。 下面的软件列表,如果编
译运行,将会产生Alembic的Archive其包含一个Simple Property,值如下描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<Alembic/Abc/All.h>
// the following include is for the concrete implementation of Alembic
#include<Alembic/AbcCoreHDF5/All.h>
usingnamespaceAlembic::Abc;
int main(int,char**)
{
OArchive archive(Alembic::AbcCoreHDF5::WriteArchive(),“myFirstArchive.abc”);
OObject child( archive.getTop(),“childObject”);
ODoubleProperty myDoubleProp( child.getProperties(),“doubleProp”);
for( size_t i =0; i <10;++i )
{
myDoubleProp.set(2.0* i );
}
return0;
}

哒哒,相当简单,不是吗?当然,那是HDF5的事情了,archive.getTop().getProperties(),后面会讲的更清楚。你可能会猜,虽然
getProperties()是一个返回Abc::OCompoundProperty的方法,并且你是对的。那个方法的全名是Abc::OObject::getProperties(),
这也会让你猜,正确的是 Abc::OArchive::getTop()返回Abc::OObject. 又一次,又离开了OScalarProperites的话题,随后会返回
getTop()和getProperties()的话题。

就像代码那样简单,它引起的问题比回答的要多,撇开前面图里面暗示的问题:

  1. 数据合适被写入?
  2. 如果不是ODoubleProperty,我有一个OReallyHeavyValueTypeProperty,比如我在堆上分配传给set的内存数据,何时可以安全释放?
    这些都是好问题。

你可能注意到了,代码里没有明显的资源管理。那是因为在使用标准指针时,Alembic为你做了。在Alembic中,一旦超出生存期,数据
就会被写入。因此,一旦main结束,Archive就会将自己写入硬盘,Archive的顶部对象做同样的事,顶部对象的Compound Property跟随,
最后compound property的simple property(作为doubleProp写入硬盘)。使用abcecho程序,在examples/bin/AbcEcho/AbcEcho.cpp,
你会看到结果Archive有一个simple Propety,就像你期待的:

1
2
3
4
$ abcecho myFirstArchive.abc
Object name=/childObject
ScalarProperty name=doubleProp;interpretation=;datatype=float64_t;numsamps=10
$

就像你问的第二个问题,何时可以安全释放资源?答案是 你一旦调用set()函数,Alembic拥有数据后,你就可以安全释放了。
这对Alembic中所有的properties(scalar,Array)都有用,同时AbcGeom里的高层几何类型也适用。

在这点上,从写入上岔开还是有好处的,并将源代码扩展到write/read例子中,展示生存范围的工作机制,和API的对称性很像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<Alembic/Abc/All.h>
#include<Alembic/AbcCoreHDF5/All.h>
#include<iostream>
usingnamespaceAlembic::Abc;
int main(int,char**)
{
std::string archiveName(“myFirstArchive.abc”);
{// open new scope for writing
OArchive archive(Alembic::AbcCoreHDF5::WriteArchive(),
archiveName );
OObject child( archive.getTop(),“childObject”);
ODoubleProperty myDoubleProp( child.getProperties(),“doubleProp”);
for( std::size_t i =0; i <10;++i )
{
myDoubleProp.set(2.0* i );
}
}// the Archive is written here when it goes out of scope
{// open new scope for reading
IArchive archive(Alembic::AbcCoreHDF5::ReadArchive(),
archiveName );
IObject child( archive.getTop(),“childObject”);
IDoubleProperty myDoubleProp( child.getProperties(),“doubleProp”);
for( std::size_t i =0; i < myDoubleProp.getNumSamples();++i )
{
std::cout << i <<“th sample is“<< myDoubleProp.getValue( i )
<< std::endl;
}
}// end of scope for reading
return0;
}