Tuesday, February 25, 2014

SIGGRAPH University - "The Digital Production Pipeline"

An excellent talk on pipelines across VFX from Sigrgaph 2013.

Debugging Models and Views using ModelTest

If you are like me, and spend quite a bit of time making new tools that always seems to require more and more advanced ways of working with QModels and QViews, you might have ran into issues with Segmentation Faults and general problems where it is very hard to tell where an how you've gone wrong.
QT has a very neat little module in it's C++ library called ModelTest. This is basically a little virtual tester that keeps a watch on your model's activity, and as certain aspects of the model or it's data change, it analyzes the changes, and tests if all the changes go through the expected cycle.
If you are not using QT, but are rather on the side of using PySide or PyQt, there is a Python Version of ModelTest. It saved me a ton of time just recently where I had some very hard-to-track issues that would pop up without recognizable patterns.
Using ModelTest is super simple:
...
from ModelTest import ModelTest
model = MyModel()
parent.setModel(model)
ModelTest(model, self) # Second argument simply needs to be a parent to attach ModelTest to.
...

Happy Coding!

Friday, February 21, 2014

Getting mouse position within QAbstractItemView

Spent a few hours trying to make a presenter for a context menu that I am making within my UI. What I wanted to do was to allow for the context menu to be dynamic to what it appears above, regardless of what is currently selected. The problem I faced was that I needed to get the position of the mouse, and then map that global position to the view that I am using, and get the index of the model that is at the position I am looking at.

In order, here is how I proceeded.

When I create the context menu for a view, there is signal triggered, that emits a QPoint of where the mouse has been clicked:
...
# Connect the signal and the slot
self.view_context_menu = QtGui.QMenu()
self.view.customContextMenuRequested.connect(self.show_view_context_menu)

# The function to show the menu
def show_view_context_menu(self, pos):
    # We need to take the point we got from the signal, and map it to the world
    mapped_point = self.view.viewport().mapToGlobal(pos)
    # Now show the menu in the global space
    self.view_context_menu.exec_(mapped_point)
All seems well and simple, we got our menu, and we displayed it. But now, I want to get the index that might be at that point, and change the context menu accordingly. Lets say for now, I just want to see if the index is a valid one or not.
...
# Connect this to run right before the menu appears
self.view_context_menu.aboutToShow.connect(self.set_menu_action_states)
...

def set_menu_action_states(self):
    point = QtGui.QCursor.pos()
    index = self.get_item_at_point(point)
    print index.isValid()

def get_item_at_point(self, point):
    return self.view.indexAt(point)
Seems straightforward. However, what happens here, the point we get with QtGui.QCursor.pos() is in the world space of our monitor(s). So, whenever you will be trying to run this code, you will pretty much never get a valid index, because, as far as the view is concerned, you are not within it. So to solve this issue one has to transform the cursor position back into the view's coordinate system. But, you have to also remember something - all views in QT inherit the QAbstractItemView class, which in turn inherits the QAbstractScrollArea. What this means, is, the coordinate system of the actual view widget is a bit different then what we get when we are displaying the menu with customContextMenuRequested. The scroll area creates little widgets to drive the area display if some items in your view are hidden, allowing, well, for scrolling. Additionally, there are headers that the view posses, and those are different from what we see within the rest of the view. To be exact, everything we see withing the view, such as the view's items, are all sitting within the view's viewport coordinate system. If one has paid attention when we were showing the menu, we use the self.view.viewport().mapToGlobal() function to determine where the menu should appear. We have to do the same, but in reverse, when we are trying to find the indices under our mouse. So the corrected function looks like this:
def get_item_at_point(self, point):
    mapped_point = self.view.viewport().mapFromGlobal(point)
    return self.view.indexAt(mapped_point)
And that's it!

Tuesday, February 18, 2014

Alembic Maya - Edge Crease Export

Recently I encountered a request from our Rigging TD to see how we may be able to export meshes in Alembic format with edge crease information from Maya. Unfortunately, Alembic is still quite poorly documented in one single place, so it takes a bit of time to put all the information together to figure out how certain things are done. One of the things I ended up having to do was to actually traverse the AlembicExporter for Maya to learn how certain export processes work. The main Object that we need to look at to see how Alembic finds crease data is MayaMeshWrite.cpp. In this Object, if we look at lines 288-324, the constructor of the MayaMeshWriter object, we can see this chunk of code:
// check to see if this poly has been tagged as a SubD
MPlug plug = lMesh.findPlug("SubDivisionMesh");
if ( !plug.isNull() && plug.asBool() )
{
    Alembic::AbcGeom::OSubD obj(iParent, name.asChar(), iTimeIndex);
    mSubDSchema = 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 = mSubDSchema.getArbGeomParams();
        up = mSubDSchema.getUserProperties();
    }
    mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj,
        lMesh, iTimeIndex, iArgs));

    writeSubD(uvSamp);
}
In the code, you may notice that the exporter is looking for the plug "SubDivisionMesh" in the mesh that is being parsed. Now, when I looked at this the first time, I thought that the tag might be added when you use an actual SubD meshes created in Maya. But, alas, in Maya 2014, the support for the SubD meshes has been changed, and you no longer can even create Subdivision primitives...

After a few attempts I realized that the SubD meshes that I got converted from PolyMeshes were not containing the "SubDivisonMesh" plug that the exporter was looking for.

Over the weekend the Rigging TD that asked me to look into this, had a look as well, and saw what I was missing this whole time - the flag "SubDivisionMesh" was a bool tag that the user was supposed to add him/herself! And sure enough, as soon as you add the tag to the mesh (NOTE: Add the tag to the Shape node of the mesh, NOT the transform node. But export the Transform node. Makes sense, right??? If so, lucky you, cause I don't get the point...:( ) and export it, the mesh is exported using Alembic's OSubD and OSubDSchema, which is capable of storing the edge creases and corners, as per Alembic docs. The creases are exported in MayaMeshWriter::writeSubD function, in lines 834-862:
std::vector <Alembic::Util::int32_t> creaseIndices;
std::vector <Alembic::Util::int32_t> creaseLengths;
std::vector <float> creaseSharpness;

std::vector <Alembic::Util::int32_t> cornerIndices;
std::vector <float> cornerSharpness;

MUintArray edgeIds;
MDoubleArray creaseData;
if (lMesh.getCreaseEdges(edgeIds, creaseData) == MS::kSuccess)
{
    unsigned int numCreases = creaseData.length();
    creaseIndices.resize(numCreases * 2);
    creaseLengths.resize(numCreases, 2);
    creaseSharpness.resize(numCreases);
    for (unsigned int i = 0; i < numCreases; ++i)
    {
        int verts[2];
        lMesh.getEdgeVertices(edgeIds[i], verts);
        creaseIndices[2 * i] = verts[0];
        creaseIndices[2 * i + 1] = verts[1];
        creaseSharpness[i] = static_cast<float>(creaseData[i]);
    }

    samp.setCreaseIndices(Alembic::Abc::Int32ArraySample(creaseIndices));
    samp.setCreaseLengths(Alembic::Abc::Int32ArraySample(creaseLengths));
    samp.setCreaseSharpnesses(
        Alembic::Abc::FloatArraySample(creaseSharpness));
}
Another thing to note: the "SubDivisionMesh" plug does not need to be added to the export Attributes list in the Alembic Exporter or the script function AbcExport. The plug is searched for in the MayaMeshWriter constructor, and so any mesh object being exported will be checked for the presence of this flag.

Hopefully this saves someone out there the time it took me and my colleague to look all this up!

I'll keep writing more as I dig deeper into Alembic, and Alembics exporter for maya, so perhaps this blog will live again! Yay!