Saturday, April 12, 2014

Reaping the fruits of "Alembic" proportions

Many have heard about Alembic. It is now becoming the #1 used format for geometry data interchange in the VFX/Animation/Games communities. Created by the joined effort of Sony Imageworks and Industrial Light and Magic, all of us in the industry got a fantastic tool.

In my recent endeavors I had to work on modifying Maya's Alembic Exporter to support multiple UV sets exporting. I will try to document my whole experience here so not to loose the info I learned, and also, perhaps, assist someone later in achieving the same thing easier, and perhaps better then I did.

In essence, the actual theory behind the idea is not overly difficult, and Alembic supports such a possibility pretty straight forward. So, theoretically, I knew exactly what I was to do, or specifically:

  1. Parse if the object contains more then one UV set.
  2. Get those UVs
  3. Convert the data to a format that alembic can read and store.
  4. Store it.
  5. DONE!
Looking around the net for information, I stumbled onto a pretty large topic on the google alembic-discussion board:
From there I gathered some more detailed info on what needed to happen such as:
  • UVs are stored as V2fGeomParam with a scope of varying or facevarying.
  • The original UVs, the default ones, are stored within the PolyMeshSchema of the mesh we are exporting through it's setUVs function, which is able to store only a single V2fGeomParamSample.
  • Any additional Params need to be stored within the arbGeomParams of the PolyMeshSchema.
The biggest challenge I was going to face, as I learned quite quickly, was dealing with Alembic API. For such a popular tool, I have to say, the documentation of it is superbly limited. Having gone through the official documentation on http://code.google.com/p/alembic/, I realized that it was pretty much all the information they had... The rest had to be derived almost entirely from their example code (that had nearly no doc strings) and the actual header files (yea, I built the Doxygen, which, without docstrings, is also not the most useful tool...) .

Having been coding Python almost exclusively, digging in head-first into such a project was a bit of a shock... One really starts to look at code documentation with greater respect... So for the first 2 days I felt very much like this:

Well, I am glad to say that I did finally finish the implementation, and I will share it here. So if anyone is interested in adding the code to their own exporter, they will be able to export multiple UVs themselves. I still need to write the modifications to the AbcImporter for Maya so that the multi UVs will be read and added to the imported meshes, but for the time being this works for things like Maya->Katana pipelines.

So the main change I did was to modify the creator function of the MayaMeshWriter. There are two parts in that creator function that run for a SubD mesh export and a PolyMesh export. I will post the poly mesh one, and will specify what needs to be changed for the SubD stuff.

So you should replace this:
...
Alembic::AbcGeom::OPolyMesh obj(iParent, name.asChar(), iTimeIndex);
mPolySchema = obj.getSchema();

Alembic::AbcGeom::OV2fGeomParam::Sample uvSamp;

if ( mWriteUVs )
{
    getUVs(uvs, indices);
    if (!uvs.empty())
    {

 uvSamp.setScope( Alembic::AbcGeom::kFacevaryingScope );
 uvSamp.setVals(Alembic::AbcGeom::V2fArraySample(
     (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
 if (!indices.empty())
 {
     uvSamp.setIndices(Alembic::Abc::UInt32ArraySample(
  &indices.front(), indices.size()));
 }
    }
}

Alembic::Abc::OCompoundProperty cp;
Alembic::Abc::OCompoundProperty up;
if (AttributesWriter::hasAnyAttr(lMesh, iArgs))
{
    cp = mPolySchema.getArbGeomParams();
    up = mPolySchema.getUserProperties();
}

// set the rest of the props and write to the writer node
mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj, lMesh,
    iTimeIndex, iArgs));

writePoly(uvSamp);
...
With this:
...
Alembic::AbcGeom::OPolyMesh obj(iParent, name.asChar(), iTimeIndex);
mPolySchema = obj.getSchema();
 
std::vector<Alembic::AbcGeom::OV2fGeomParam::Sample> uvSamps;
Alembic::AbcGeom::OV2fGeomParam::Sample defaultUV;
 
std::vector<float> uvs;
std::vector<Alembic::Util::uint32_t> indices; 
MStringArray uvSetNames;
lMesh.getUVSetNames(uvSetNames);
if ( mWriteUVs )
{
    MGlobal::displayInfo("Writing the default UVs");
    getUVs(uvs, indices);
 
    if (!uvs.empty())
    {
         defaultUV.setScope( Alembic::AbcGeom::kFacevaryingScope );
         defaultUV.setVals(Alembic::AbcGeom::V2fArraySample(
              (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
         if (!indices.empty())
         {
              defaultUV.setIndices(Alembic::Abc::UInt32ArraySample(
                   &indices.front(), indices.size()));
         }
    } 
}

Alembic::Abc::OCompoundProperty cp;
Alembic::Abc::OCompoundProperty up;
if (AttributesWriter::hasAnyAttr(lMesh, iArgs))
{
        cp = mPolySchema.getArbGeomParams();
        up = mPolySchema.getUserProperties();
}

// set the rest of the props and write to the writer node
mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj, lMesh,
            iTimeIndex, iArgs));

writePoly(defaultUV);
        
//Write multiple UVs if present
if (uvSetNames.length() > 1)
{
 MGlobal::displayInfo("Will be exporting multiple UVs now!");
 Alembic::Abc::OCompoundProperty arbParams;
 arbParams =  mPolySchema.getArbGeomParams();
 
 MString lastUVSetName = lMesh.currentUVSetName();
 for (int i = 0; i < uvSetNames.length(); i++)
 {
  MString uvSetName = uvSetNames[i];
  
  if (uvSetName == lastUVSetName)
  {
   continue; 
  }
   
  std::string uvSetPropName = uvSetName.asChar();
 
  Alembic::AbcCoreAbstract::MetaData md;

  Alembic::AbcGeom::OV2fGeomParam uvProp(arbParams, uvSetPropName, true,
   Alembic::AbcGeom::kFacevaryingScope, 1, iTimeIndex, md);
  mUVParams.push_back(uvProp);
 }
 std::vector <Alembic::AbcGeom::OV2fGeomParam>::iterator uvIt;
 std::vector <Alembic::AbcGeom::OV2fGeomParam>::iterator uvItEnd;
      
 uvIt = mUVParams.begin();
 uvItEnd = mUVParams.end();

 for(; uvIt != uvItEnd; ++uvIt)
 {
  MString uvSetName(uvIt->getName().c_str());
  lMesh.setCurrentUVSetName(uvSetName);
  Alembic::AbcGeom::OV2fGeomParam::Sample uvSamp;    

  getUVs(uvs, indices);

  if (!uvs.empty())
  {
   uvSamp.setScope( Alembic::AbcGeom::kFacevaryingScope );
   uvSamp.setVals(Alembic::AbcGeom::V2fArraySample(
       (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
   if (!indices.empty())
   {
    Alembic::Abc::UInt32ArraySample vals(&indices.front(), indices.size());
    uvSamp.setIndices(vals);
   }
  }
  uvIt->set(uvSamp);
  uvSamps.push_back(uvSamp);
 }
 lMesh.setCurrentUVSetName(lastUVSetName);
}
...
I am not sure how optimized that is, but it works. I more then welcome comments on how to improve this, btw! Now a few things to add. You need to include a definition of the mUVParams to the MayaMeshWriter.h file:
...
std::vector&ltAlembic::AbcGeom::OV2fGeomParam&gt mUVParams;
...
Now, I made a mistake here, which I learned is a big one. When you define the entry in the header, you will need to recompile most of the plugin to make sure your symbol table matches (I still need to read up more on that to know exactly what the meaning is, unfortunately, but as far as I understand, the mapping of the variables to the memory addresses gets messed up if you don't do the re-compile...). When I forgot to do that as I was testing the implementations, I got very weird memory crashes, until it hit me that perhaps my headers information wasn't being updated with the rest of the plugin.

And finally, the only thing needed to change for that code to suit it for the SubDiv part of MayaMeshWriter::MayaMeshWriter, is to replace all of the mPolySchema variables with mSubDSchema. Note the way OSubD stuff is defined in the code originally, and try to maintain that so that you don't screw up the schema writing process.

Hope that helps!

Cheers,

DK

2 comments:

  1. Hey Dimitry,

    This is great. We have been looking for something like this for a while. Would be it be possible for you to write the importer as well. We could really use your help here.

    Thanks

    Rachit.

    ReplyDelete
    Replies
    1. Hey Rachit,
      Yea, an importer is something that I've been meaning to write for a while, but just need some time to actually do it. As we use Katana for lighting, the attributes are visible thanks to the universal importer in Katana. Maya, obviously, would need a modification to the Alembic reader to recognize those additional attributes. Once I get a chance to do that, I'll definitely post the solve here.

      Best,

      DK

      Delete