Simple Maya mesh save/load

I recently wanted to capture some frames of an animation into a single mesh and really the easiest way to ditch any dependencies & materials was to export some OBJs, import them and then combine them! This is rather slow, especially reading gigantic models, and I did not need a lot of the data stored in an OBJ.

So here I have a small utility that stores a model’s position & triangulation and nothing else in a binary format closely resembling the Maya API, allowing for easy reading, writing and even combining during I/O.

Use write() with a mesh (full) name and use read() with a filepath to serialize
and deserialize maya meshes:

import struct
from maya.OpenMaya import MSelectionList, MDagPath, MFnMesh, MGlobal, MPointArray, MIntArray, MSpace, MPoint


def _named_mobject(path):
    li = MSelectionList()
    MGlobal.getSelectionListByName(path, li)
    p = MDagPath()
    li.getDagPath(0, p)
    return p


def writeCombined(meshes, file_path):
    # start streaming into the file
    with open(file_path, 'wb') as fh:
        # cache function sets
        fns = []
        for mesh in meshes:
            fns.append(MFnMesh(_named_mobject(mesh)))

        # get resulting mesh data sizes
        vertex_count = 0
        poly_count = 0
        index_count = 0
        meshPolygonCounts = []
        meshPolygonConnects = []
        for fn in fns:
            vertex_count += fn.numVertices()
            meshPolygonCounts.append(MIntArray())
            meshPolygonConnects.append(MIntArray())
            # we need to get these now in order to keep track of the index_count,
            # we cache them to avoid copying these arrays three times during this function.
            fn.getVertices(meshPolygonCounts[-1], meshPolygonConnects[-1])
            poly_count += meshPolygonCounts[-1].length()
            index_count += meshPolygonConnects[-1].length()

        # write num-vertices as uint32
        fh.write(struct.pack('<L', vertex_count))

        for fn in fns:
            vertices = MPointArray()
            fn.getPoints(vertices, MSpace.kWorld)

            # write all vertex positions as pairs of three float64s
            for i in xrange(vertex_count):
                fh.write(struct.pack('<d', vertices[i].x))
                fh.write(struct.pack('<d', vertices[i].y))
                fh.write(struct.pack('<d', vertices[i].z))

        # write num-polygonCounts as uint32
        fh.write(struct.pack('<L', poly_count))

        for i, fn in enumerate(fns):
            # write each polygonCounts as uint32
            for j in xrange(meshPolygonCounts[i].length()):
                fh.write(struct.pack('<L', meshPolygonCounts[i][j]))

        # write num-polygonConnects as uint32
        fh.write(struct.pack('<L', index_count))

        # keep track of how many vertices there are to offset the polygon-vertex indices
        offset = 0
        for i, fn in enumerate(fns):
            # write each polygonConnects as uint32
            for j in xrange(meshPolygonConnects[i].length()):
                fh.write(struct.pack('<L', meshPolygonConnects[i][j] + offset))
            offset += fn.numVertices()


def write(mesh, file_path):
    writeCombined([mesh], file_path)


def readCombined(file_paths):
    numVertices = 0
    numPolygons = 0
    vertices = MPointArray()
    polygonCounts = MIntArray()
    polygonConnects = MIntArray()

    for file_path in file_paths:
        with open(file_path, 'rb') as fh:
            # read all vertices
            n = struct.unpack('<L', fh.read(4))[0]
            for i in xrange(n):
                vertices.append(MPoint(*struct.unpack('<3d', fh.read(24))))

            # read all polygon counts
            n = struct.unpack('<L', fh.read(4))[0]
            numPolygons += n
            polygonCounts += struct.unpack('<%sL'%n, fh.read(n * 4))

            # read all polygon-vertex indices
            n = struct.unpack('<L', fh.read(4))[0]
            offset = polygonConnects.length()
            polygonConnects += struct.unpack('<%sL'%n, fh.read(n * 4))

            # offset the indices we just added to the match merged mesh vertex IDs
            for i in xrange(n):
                polygonConnects[offset + i] += numVertices

            numVertices += n

    new_object = MFnMesh()
    new_object.create(numVertices, numPolygons, vertices, polygonCounts, polygonConnects)
    return new_object.fullPathName()


def read(file_path):
    with open(file_path, 'rb') as fh:
        numVertices = struct.unpack('<L', fh.read(4))[0]
        vertices = MPointArray()
        for i in xrange(numVertices):
            vertices.append(MPoint(*struct.unpack('<3d', fh.read(24))))
        numPolygons = struct.unpack('<L', fh.read(4))[0]
        polygonCounts = MIntArray()
        polygonCounts += struct.unpack('<%sL'%numPolygons, fh.read(numPolygons * 4))
        n = struct.unpack('<L', fh.read(4))[0]
        polygonConnects = MIntArray()
        polygonConnects += struct.unpack('<%sL'%n, fh.read(n * 4))

    new_object = MFnMesh()
    new_object.create(numVertices, numPolygons, vertices, polygonCounts, polygonConnects)
    return new_object.fullPathName()

I basically used a snippet like this to snapshot my animation:

tempfiles = []
for f in (0,4,8,12):
    cmds.currentTime(f)
    tempfiles.append('C:/%s.mfnmesh'%f)
    writeCombined(cmds.ls(type='mesh', l=True), tempfiles[-1])
newmesh = readCombined(tempfiles)
for p in tempfiles:
    os.unlink(p)

Important notice: I have found some random crashes in using a large amount of memory (high polycount per frame) in the writeCombined function (which may be solvable when ported to C++ an receiving proper error data).

Parameter to nurbs surface node

A simple deformer that reprojects a source mesh (considered as UVW coordinates)
onto a (series of) nurbs surfaces.

Inspired by “It’s a UVN Face Rig”

It takes an array of nurbs surfaces which must be at least length 1,
a polygonal mesh where the point positions are considered parameters on the nurbs surface; Z being an offset in the normal direction (hence UVN),
and an optional int array where there can be one entry per input vertex, stating which nurbs surface this vertex should project onto.

The default surface for every vertex is 0, so for a single nurbs surface projection no array is needed and only overrides have to be specified.

This includes full source + a project compiled using VS2015 for Maya2015 x64.
Download zip

Python test code:

PLUGIN = r'UVNDeformer.mll'

cmds.loadPlugin(path)
node = cmds.createNode('UVNNurbsToPoly')
nurbs = cmds.sphere()[0]
uvn = cmds.polyPlane()[0]
cmds.select(uvn + '.vtx[*]')
cmds.rotate(90, 0, 0, r=True)
cmds.move(0.5001, 0.5001, r=True)
result = cmds.createNode('mesh')
cmds.connectAttr(nurbs + '.worldSpace[0]', node + '.ins[0]')
cmds.connectAttr(uvn + '.outMesh', node + '.iuvnm')
cmds.connectAttr(node + '.outMesh', result + '.inMesh')

cmds.select(uvn + '.vtx[*]')

Maya API Wrapping with CRTP

So I came across this: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

The amazing python ‘classmethod’.

A way to inherit static members and define static interfaces.

In python there exists the decorator “classmethod”, which is like a staticmethod that receives the type on which it is called, including inherited types.
Additionally it can be overriden and the base class function can be called separately like any ordinary member function.

Now imagine this construction:

class BaseClass(object):
	def __init__(self, name):
		self.__name = name
		print name

	@classmethod
	def getName(cls):
		raise NotImplementedError()

	@classmethod
	def creator(cls):
		return cls.creator(cls.getName())

This base class can instantiate through the creator method, that includes instantiating any subclasses because the cls argument is the owning type.
This then calls upon another classmethod, which is not implemented, effectively demanding that subclasses implement this method.
What this creates is the ability to define static interfaces, as well as share codes between those interfaces while implementation is still kept abstract.

class SubClass1(BaseClass):
	@classmethod
	def getName(cls):
		return 'SubClass1'

class SubClass2(BaseClass):
	@classmethod
	def getName(cls):
		return 'SubClass2'

instance1 = SubClass1.creator() # prints SubClass1
instance2 = SubClass2.creator() # prints SubClass2

So you see the class inherited the creator method, the creator method was aware of the sub-type and called the right getName method.

The C++ ‘Curiously recurring template pattern’ (or CRTP).

This is a terribly confusing trick that allows us to mimic the above python idiom.

So let’s take a similar scenario!

template <class T> class TemplateBase 
{
protected:
	const char* name;
	TemplateBase(const char* inName)
	{
		name = inName;
	}

public:
	static T* sCreator()
	{
		return new T(T::sName()); 
	}
}

This is functionally similar to the python example. It creates a given type and won’t compile
unless the specified type has a static sName() member returning a const char*.

Sidetracking here, that would allow us to do this:

class SubClass1
{
	const char* name;
public:
	MyName(const char* inName) { name = inName; }
	static const char* sName() { return "SubClass1"; }
}
SubClass1* instance = TemplateBase<SubClass1>::sCreator();

But (no longer sidetracking) here comes the recurring part:

class SubClass2 : public TemplateBase<SubClass2>
{
protected:
	using TemplateBase::TemplateBase;
public:
	static const char* sName() 
	{
		return "SubClass2"; 
	}
}
SubClass2* instance = SubClass2::sCreator();

This generates a template implementation for it’s own subclass, which curiously doesn’t cause
any problems, even though a base class is accessing a subclass’s (static) functions.

This also hides the (inherited) constructor so really only the sCreator function can be used
for instantiation of our class. And because the subclass doesn’t define sCreator we can very intuitively
call it on our subclass itself.

When giving arguments to a template, and then using it, the C++ compiler generates new
code for us specific to the type given to the template. So multiple subclasses currently do not
share a common base class.

Maya API utility using CRTP

The Maya API is oftem implemented in a way that demands things for the programmer, without raising
errors when the programmer does something wrong. With modern C++ we can enforce much more rules and
with the complexity of software we definately should cut the contributer some slack!

Here is a little setup for creating an MPxNode wrapper that allows the compiler to communicate what is needed.

template  class MPxNodeCRTP : public MPxNode
{
public:
	static void* sCreator() { return new T(); }
	static MStatus sInitializeWrapper() { MStatus status = T::sInitialize(); CHECK_MSTATUS_AND_RETURN_IT(status); }
}
// utility macro:
#define INHERIT_MPXNODE(CLASS) class CLASS : public MPxNodeCRTP

I’ve been taking this further to handle registration of input and output attributes,
so attributeAffects gets handled by the base class, as well as MFn::kUnknownParameter exceptions in compute().

Maya quaternion & matrix operation order

Here are some pointers I had to learn the hard way, and don’t ever want to forget.

MQuaternion(MVector a, MVector b)

constructs the rotation to go from B to A!
So if you have an arbitrary aim vector and wish to go from world space to that aim vector use something like

MQuaternion(aimVector, MVector::xAxis)

The documentation is very ambiguous about this. Or rather, it makes you think the opposite!

If you wish to combine matrices in maya think of how children and parents relate in the 3D scene to determine the order of multiplication. Childs go first, e.g.

(leafMatrix * parentMatrix) * rootMatrix

Another way to think about it is adding rotations. So if you have a rotation and you wish to add some rotation to it, you generally parent an empty group to it and rotate that, so you again get this relationship of

additionalRotation * existingRotation

A little note: not sure if adding quaternion rotations works in the same way; should check!

More conventions to come hopefully!

Python profiler output in QT GUI

I wanted to sort my profiler result (using cProfile) but found usign the stats.Stats objects rather complicated.

Profiling is easy:

import cProfile
cProfile.run('''
MY CODE AS A STRING
''')

The profiler outputs itself to the console, so to instead catch it in a file we can change python’s console output.

import cProfile
import sys
import cStringIO

backup = sys.stdout
sys.stdout = cStringIO.StringIO()

cProfile.run('''
MY CODE AS A STRING
''')

profileLog = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = backup

The original sys.stdout is also stored as sys.__stdout__
but maybe at the point you are doing this the host application already has it’s own
stdout in use, so let’s just backup and restore explicitly so we’re certainly not breaking stuff.

Now the output is a huge ascii table of stats. By converting that to a QTableWidget we can
easily sort and analyse this data. So first let’s set up the table…

from PyQt4.QtCore import *
from PyQt4.QtGui import *

widget = QTableWidget()
widget.setColumnCount(6)
widget.setHorizontalHeaderLabels(['ncalls', 'tottime', 'percall', 'cumtime', 'percall', 'filename:lineno(function)'])

I manually copied the header names from the profile log, you may make the more sensible at your leisure… The widget needs to have it’s size set up before usage, so we can estimate the number of rows beforehand instead of resizing it in every iteration:

logLines = profileLog.split('\n')
widget.setRowCount(len(logLines))

Now this is a bit ugly, we essentially iterate all the lines and put their respective values into the widget. We’re splitting by whitespace with a regex.

enabled = False
y = 0
for i in range(len(logLines)):
    ln = logLines[i].strip()
    # skip empty lines
    if not ln:
        continue
    # start real iteration only after the header information
    if not enabled:
        if ln.lower() == r'ncalls  tottime  percall  cumtime  percall filename:lineno(function)'.lower():
            enabled = True
        continue
    segments = re.split('\s+', ln)
    c = len(segments)
    if c > 6:
        c = 6
        segments[5] = ' '.join(segments[5:])
    for x in range(c):
        item = QTableWidgetItem(segments[x])
        widget.setItem(y, x, item)
    y += 1

We manually increment the row count to account for the header lines and potential empty lines / otherwise ignored lines.
Last we strip off unused rows (remember we assumed line count as row count), enable sorting and show our widget.

widget.setRowCount(y)
widget.setSortingEnabled(True)
widget.show()

For convenience I wanted to make this a function that I could import and use instead of cProfile.run() at any given time. So this is my full code:

import re
import sys
import cProfile
import cStringIO
from PyQt4.QtCore import *
from PyQt4.QtGui import *


def profileToTable(code, globals=None, locals=None):
    backup = sys.stdout
    sys.stdout = cStringIO.StringIO()
    
    cProfile.run(code)
    
    profileLog = sys.stdout.getvalue()
    sys.stdout.close()
    sys.stdout = backup
    
    widget = QTableWidget()
    widget.show()
    widget.setColumnCount(6)
    widget.setHorizontalHeaderLabels(['ncalls', 'tottime', 'percall', 'cumtime', 'percall', 'filename:lineno(function)'])
    
    logLines = profileLog.split('\n')
    widget.setRowCount(len(logLines))
    
    enabled = False
    y = 0
    for i in range(len(logLines)):
        ln = logLines[i].strip()
        # skip empty lines
        if not ln:
            continue
        # start real iteration only after the header information
        if not enabled:
            if ln.lower() == r'ncalls  tottime  percall  cumtime  percall filename:lineno(function)'.lower():
                enabled = True
            continue
        segments = re.split('\s+', ln)
        c = len(segments)
        if c > 6:
            c = 6
            segments[5] = ' '.join(segments[5:])
        for x in range(c):
            item = QTableWidgetItem(segments[x])
            widget.setItem(y, x, item)
        y += 1
    return widget

We must cache the returned widget in memory for otherwise python’s garbage collection will try to delete it and then Qt will close it.

widget = profileToTable('re.compile("foo|bar")')

After that you may wish to add a search bar so you can look for specific functions that you wish to check for potential improvements or suspicious times. At least I did… simple Qt stuff! QTableWidget has a search by (partial) string utility as well as hide and show row functions, so a simple set of loops allows us to select and filter the table.

def filterTable(tableWidget):
    main = QWidget()
    layout = QVBoxLayout()
    main.setLayout(layout)
    
    search = QLineEdit()
    layout.addWidget(search)
    
    layout.addWidget(tableWidget)
    
    def filterTable(widget, text):
        # there seem to be many duplicate entries when we go from a string to an empty string
        rows = []
        if text:
            showItems = widget.findItems(text, Qt.MatchContains)
            for i in showItems:
                rows.append(i.row())
            rows.sort()
        allrows = range(widget.rowCount())
        for i in range(len(rows)-1, -1, -1):
            widget.showRow(rows[i])
            allrows.pop(rows[i])
        for i in allrows:
            widget.hideRow(i)
        
    search.textChanged.connect(functools.partial(filterTable, tableWidget))
    
    main.show()
    return main

This function takes as widget the result of the profile function so it completely appends to what’s already there. Again the returned widget must be cached. You may also make a utility function like so:

# regular usage example
widget = profileToTable('re.compile("foo|bar")')
wrapper = filterTable(widget)

def profileToFilterTable(code, globals=None, locals=None):
    return filterTable(profileToTable(code, globals, locals))

# with utlity
wrapper2 = profileToFilterTable('re.compile("foo|bar")')