Granted, this won’t be your typical coding how-to but due to the surprising lack of information on the net, I thought I name this post as such nonetheless. You see, I’ve been trying to add Alembic file format support to my 3D modeler project for months but there were severe problems of compiling the library. And what did I discover once I had successfully linked against it? There isn’t much usable documentation or code examples to be found anywhere…
So this is yet another “I’ll write it down in case someone else runs into the same problem” post.
First, a few words on Alembic itself: It’s a geometry exchange format that has been designed to be a robust part in a production pipeline. As such, it doesn’t contain things you might be used from other formats (such as material/texture information). However, it is gaining wide support as a new “OBJ”: being totally reliable because it only has a small feature set that everyone supports.
Installing the Library
So let’s start at the beginning… installing the library is a bit of a pain as it requires several other libraries to be compiled and installed correctly (e.g. OpenEXR, ilmbase, HDF5, etc). I’m currently using Alembic 1.5.6 which seemed to resolve a couple of problems I had before when compiling on OSX. I tried a homebrew recipe I found on the net to automate installation but it did not work for me.
The most important thing is to stick to the build instruction in the alembic folder! The main readme specifies exactly what you have to do for alembic and the doc folder contains files for the required libraries.
- Boost: You need boost 1.44 (versions higher than 1.48 won’t work), it needs to be static and have a few non-standard libraries enabled. In the alembic source bundle, there is a file
doc/Boost-howtobuild.txt
which gives the exact parameters it should be compiled with - HDF5: Same as above, there is a file that specifies the exact build settings
- Zlib: Although OSX comes with the zlib header file, it does not come with a static library version so you have to download and install it as well.
With previous versions of Alembic, I had additional problems (probably due to the LLVM compiler and/or boost, cannot really remember) but 1.5.6 worked for me.
When you run the alembic bootstrap script, make sure to enter the correct header/library search paths. I at one point had compile problems because the script somehow included boost files from a previous compile attempt that were not compiled correctly yet.
Documentation & Concepts
Now comes the weird part: I assume the natural goal of anyone downloading Alembic is to first write a file importer. However, the examples in the SDK did not contain anything on how to parse the scene tree and read meshes. There is some good information hidden in the wiki on the alembic google code website but in general, it felt way harder to collect the necessary information that it should be.
So here is the management summary: A file is called an archive and once you read it, it provides the top node of the file structure. An alembic file consist of a tree of objects which each have a set of properties. There is no explicit concept of a node, group, a mesh or anything else you might be used to from other formats. Instead, alembic defines a set of schemas.
In a nutshell, each object can have child objects, thus functioning as a group and forming the scene tree structure. But an object can also contain the information for a mesh. To check this, the object’s meta data can be matched against the IPolyMeshSchema schema. If the meta data contains properties that represent positional and face information, it is/can be considered a mesh. Simple as that… if you for example use the AbcEcho tool (contained in the example section of the source bundle) on an alembic file, it won’t mention the word “mesh”:
AbcEcho for Alembic 1.5.6 (built Oct 24 2014 23:23:34) file written by: Maya 2012-2.1.SPI x64 AbcExport v1.0 using Alembic : Alembic 1.0.0 (built Aug 5 2011 16:14:46) written on : Tue Aug 9 13:14:24 2011 user description : Exported from: /mcp/Alembic_Octopus_Example/alembic_octopus.mb ScalarProperty name=statistics;interpretation=;datatype=string;arraysize=1;numsamps=1 ScalarProperty name=1.samples;interpretation=;datatype=uint32_t;arraysize=1;numsamps=1 ScalarProperty name=.childBnds;interpretation=box;datatype=float64_t[6];arraysize=6;numsamps=31 Object name=/octopus_low CompoundProperty name=.xform;schema=AbcGeom_Xform_v3 ScalarProperty name=.ops;interpretation=;datatype=uint8_t[5];arraysize=5;numsamps=31 ArrayProperty name=.animChans;interpretation=;datatype=uint32_t;arraysize=0;numsamps=31 ScalarProperty name=isNotConstantIdentity;interpretation=;datatype=bool_t;arraysize=1;numsamps=1 ScalarProperty name=.inherits;interpretation=;datatype=bool_t;arraysize=1;numsamps=31 ScalarProperty name=.vals;interpretation=;datatype=float64_t[9];arraysize=9;numsamps=31 Object name=/octopus_low/octopus_lowShape CompoundProperty name=.geom;schema=AbcGeom_PolyMesh_v1 ArrayProperty name=.faceCounts;interpretation=;datatype=int32_t;arraysize=94016;numsamps=31 ArrayProperty name=.faceIndices;interpretation=;datatype=int32_t;arraysize=376064;numsamps=31 ArrayProperty name=P;interpretation=point;datatype=float32_t[3];arraysize=94018;numsamps=31 ScalarProperty name=.selfBnds;interpretation=box;datatype=float64_t[6];arraysize=6;numsamps=31
The file contains three objects, the first being the root node and thus not explicitly listed as an object in the print out above. The second object contains the properties for an “xform” (=a translation) and the third object contains a geometry which in this case confirms to the polymesh schema (as opposed to a Subdivision-Surface geometry).
Time and Samples
One interesting concept is that Alembic does not store any procedural animations (as in “move this object along this keyframe/spline animation”). Instead, it explicitly bakes the properties of the geometry for each frame. For example, if a mesh is animated with a bone animation in a 3D modeling application, Alembic stores the deformed vertex positions for each frame! This makes it foolproof for other applications to play back the same animation as they don’t have to employ any code on how to do bones, keyframes and so on.
As such, each property in Alembic contains one or more samples (=sets of data for a frame). If for example the UVs of a mesh stay the same along an animation, there is only one sample data. But if the vertex position changes, there are multiple sets. So when you query Alembic for the position information of a mesh, you have to tell it at which time.
Import Code
Due to the limited number of available test files (i.e. one (!) ), I only have a partial implementation right now and it of course relies on my modeling core API. However, I believe seeing the code in one place might make it easier for the reader to see how Alembic works. I had to piece that code together from various places on the web and the python documentation of Alembic:
std::shared_ptr<SceneGraph::Node> importNode(Core::API * coreAPI, SceneGraph::Scene * scene, Alembic::Abc::IObject const & object)
{
std::shared_ptr<SceneGraph::Node> result;
if ( Alembic::AbcGeom::IPolyMeshSchema::matches(object.getMetaData()) )
{
assert(object.getNumChildren() == 0 );
auto geometry = scene->createGeometry();
auto mesh = coreAPI->createMesh();
geometry->setName(object.getName());
geometry->setMesh(mesh);
auto abcMeshObj = Alembic::AbcGeom::IPolyMesh(object, Alembic::Abc::kWrapExisting);
auto abcMesh = abcMeshObj.getSchema();
auto abcMeshSamp = abcMesh.getValue();
auto positions = abcMeshSamp.getPositions();
std::vector<Geometry::Mesh::VertexHandle> vertices;
for ( int i = 0; i < positions->size(); ++i )
{
auto position = (*positions)[i];
vertices.push_back(mesh->addVertex(Math::Vector3(position[0], position[1], position[2])));
}
auto abcFaceCounts = abcMeshSamp.getFaceCounts();
auto abcFaceIndices = abcMeshSamp.getFaceIndices();
int faceIndex = 0;
std::vector<Geometry::Mesh::VertexHandle> indices;
for ( int i = 0; i < abcFaceCounts->size(); ++i )
{
// Note: Face-order is clockwise in alembic, so we have to reverse it here.
int const faceVertexCount = (*abcFaceCounts)[i];
indices.resize(faceVertexCount);
for ( int j = 0; j < faceVertexCount; ++j )
{
int const index = (*abcFaceIndices)[faceIndex++];
indices[faceVertexCount - j - 1] = vertices[index];
}
mesh->addFace(indices);
}
auto abcNormals = abcMesh.getNormalsParam();
if ( abcNormals.valid() )
{
Log::error("ExtensionAlembic", "File contains normals, not implemented yet");
/*assert(abcNormals.isConstant());
assert(abcNormals.isIndexed() == false);
auto nsp = abcNormals.getExpandedValue().getVals();
auto test = nsp->size();*/
}
auto abcUVs = abcMesh.getUVsParam();
if ( abcUVs.valid() )
{
Log::error("ExtensionAlembic", "File contains UVs, not implemented yet");
}
mesh->calculateNormals(true, true);
result = geometry;
}
else
{
auto group = scene->createGroup();
size_t const numChildren = object.getNumChildren();
for ( size_t i = 0; i < numChildren; ++i )
{
auto child = object.getChild(i);
auto childNode = importNode(coreAPI, scene, child);
group->addChild(childNode);
}
result = group;
}
result->setName(object.getName());
if ( Alembic::AbcGeom::IXform::matches(object.getMetaData()) )
{
Alembic::AbcGeom::XformSample xs;
Alembic::AbcGeom::IXform(object, Alembic::Abc::kWrapExisting).getSchema().get(xs);
auto matrix = xs.getMatrix();
result->setTransform(Core::SceneGraph::Transformation(Math::Matrix4(matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0],
matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1],
matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2],
matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3])));
}
return result;
}
std::shared_ptr<Core::SceneGraph::Scene> ExtensionAlembic::readFile(std::string filePath, Tools::ProgressReporter & progress)
{
progress.setMessage("Reading file");
if ( progress.update(0) == false )
{
return nullptr;
}
Alembic::AbcCoreFactory::IFactory factory;
Alembic::AbcCoreFactory::IFactory::CoreType coreType;
Alembic::Abc::IArchive archive = factory.getArchive(filePath, coreType);
if (!archive.valid())
{
Log::error("ExtensionAlembic", "Archive invalid");
return nullptr;
}
Alembic::Abc::IObject inTop = archive.getTop();
std::shared_ptr<SceneGraph::Scene> scene = _coreAPI->createScene();
std::shared_ptr<SceneGraph::Node> root = importNode(_coreAPI, scene.get(), inTop);
scene->setRootNode(root);
if ( progress.update(100) == false )
{
return nullptr;
}
return scene;
}
Example Files
Seriously, there seem to be none on the net! Okay, except for that one octupus model on the alembic website… if you know any public sources, please drop a comment below.
Leave a Reply