Cubic root solving

Update: Autodesk released their interpolation code for Maya animation curves, weighted tangents on animation curves do exactly this.

Refer to (and use!) https://github.com/Autodesk/animx instead of the code below. I know it’s not python, but it does work where I found bugs in my version below.

I really need to get back to this, but I mocked up this bit of code and finally got it to work.
It is about animation curve evaluation, due to the parametric nature of curves it is very complicated to get the value of a curve on at an arbitrary time, and it involves finding the parameter T for a value X.

This demo shows how the parameter (left to right) relates to the time of the animcurve (top to bottom), so the red curve shows linear parameter increasing results in non linear time samples.
It then computes backwards from the found value to the parameter again, showing we can successfully reconstruct the linear input given the time output.

I intend to clean up this code and make a full 2D example, rendering a 2D cubic spline segment as normal and then overlaying an evaluation based on the X coordinate, but wanted to dump the result nonetheless. Knowing how bad I am at getting back to things…
Using QT purely for demonstration, code itself is pure python…

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


def cubicArgs(x0, x1, x2, x3):
    a = x3 + (x1 - x2) * 3.0 - x0
    b = 3.0 * (x2 - 2.0 * x1 + x0)
    c = 3.0 * (x1 - x0)
    d = x0
    return a, b, c, d


def cubicEvaluate(x0, x1, x2, x3, p):
    # convert points to a cubic function & evaluate at p
    a, b, c, d = cubicArgs(x0, x1, x2, x3)
    return a * p * p * p + b * p * p + c * p + d


class CurveDebug(QWidget):
    def __init__(self):
        super(CurveDebug, self).__init__()
        self.t = QTimer()
        self.t.timeout.connect(self.update)
        self.t.start(16)
        self.ot = time.time()

    def paintEvent(self, event):
        painter = QPainter(self)

        life = time.time() - self.ot

        padding = 100
        w = self.width() - padding * 2
        h = self.height() - padding * 2
        painter.translate(padding, padding)

        # zig zag 2D bezier
        x0, y0 = 0, 0
        x1, y1 = (sin(life) * 0.5 + 0.5) * w, 0
        x2, y2 = 0, h
        x3, y3 = w, h

        # draw hull
        # painter.setPen(QColor(100, 220, 220))
        # painter.drawLine(x0, y0, x1, y1)
        # painter.drawLine(x1, y1, x2, y2)
        # painter.drawLine(x2, y2, x3, y3)

        for i in xrange(w):
            p = i / float(w - 1)

            # draw curve
            # painter.setPen(QColor(220, 100, 220))
            # x = cubicEvaluate(x0, x1, x2, x3, p)
            # y = cubicEvaluate(y0, y1, y2, y3, p)
            # painter.drawPoint(x, y)

            # draw X as function of P
            painter.setPen(QColor(220, 100, 100))
            x = cubicEvaluate(x0, x1, x2, x3, p)
            painter.drawPoint(i, x)

            # now let's evaluate the curve at x and see if we can get the original p back
            # make cubic with offset
            a, b, c, d = cubicArgs(x0 - x, x1 - x, x2 - x, x3 - x)

            # find roots
            # http://www.1728.org/cubic2.htm
            f = ((3.0 * c / a) - ((b * b) / (a * a))) / 3.0
            g = (((2.0 * b * b * b) / (a * a * a)) - ((9.0 * b * c) / (a * a)) + ((27.0 * d) / a)) / 27.0
            _h = ((g * g) / 4.0) + ((f * f * f) / 27.0)
            root0, root1, root2 = None, None, None
            if _h <= 0.0:
                # we have 3 real roots
                if f == 0 and g == 0:
                    # all roots are real & equal
                    _i = d / a
                    root0 = -copysign(pow(abs(_i), 0.3333), _i)
                else:
                    _i = sqrt((g * g / 4.0) - _h)
                    j = pow(_i, 0.3333333)
                    k = acos(-(g / (2.0 * _i)))
                    m = cos(k / 3.0)
                    n = sqrt(3.0) * sin(k / 3.0)
                    _p = b / (3.0 * a)
                    root0 = 2.0 * j * m - _p
                    root1 = -j * (m + n) - _p
                    root2 = -j * (m - n) - _p
            else:
                # we have only 1 real root
                R = -(g / 2.0) + sqrt(_h)
                S = copysign(pow(abs(R), 0.3333333), R)
                T = -(g / 2.0) - sqrt(_h)
                U = copysign(pow(abs(T), 0.3333333), T)
                root0 = (S + U) - (b / (3.0 * a))

            painter.setPen(QColor(100, 100, 220))
            painter.drawPoint(i, root0 * h)
            if root1:
                painter.drawPoint(i, root1 * h)
                painter.drawPoint(i, root2 * h)


app = QApplication([])
cvd = CurveDebug()
cvd.show()
cvd.resize(300, 300)
app.exec_()

Lattice -> Joints

It’s not perfect, but here’s a small script that samples a lattice and tries to set joint weights based on the influence of each lattice point.

Given a set of lattice vertices and a model influenced by these vertices it will create joints at every lattice point, bind a skin and set the weights.

Usage: just edit the variables at the top & run the script. It’s slapped together really quickly.

It moves every lattice point one by one & stores the amount of movement that occured per vertex, which is basically the weight of this point for that vertex.

Issues: Small weights are completely vanishing, you could try dividing the sampled movement by the amout of movement to get a 0-1 weight, then apply an inverse s-curve or pow / sqrt to that value and use it as weight instead.

Requirements: to set all weights really fast I use a custom “skinWeightsHandler” command, you can write your own ‘set all weights for all joints and then normalize’ routine or get the plugin by installing Perry Leijten’s skinning tools for which I originally made this plugin.

model = r'polySurface1'
influences = (r'ffd1Lattice.pt[0][0][0]',
r'ffd1Lattice.pt[0][0][1]',
r'ffd1Lattice.pt[0][1][0]',
r'ffd1Lattice.pt[0][1][1]',
r'ffd1Lattice.pt[1][0][0]',
r'ffd1Lattice.pt[1][0][1]',
r'ffd1Lattice.pt[1][1][0]',
r'ffd1Lattice.pt[1][1][1]')

def sample(model):
    return cmds.xform(model + '.vtx[*]', q=True, ws=True, t=True)[1::3]

def difference(list, list2):
    stack = [0] * len(list)
    for i in range(len(list)):
        stack[i] = abs(list2[i] - list[i])
    return stack

def gather(model, influences):
    original = sample(model)
    weights = {}
    for influence in influences:
        cmds.undoInfo(ock=True)
        cmds.xform(influence, ws=True, r=True, t=[0, 1000, 0])
        weights[influence] = difference(sample(model), original)
        cmds.undoInfo(cck=True)
        cmds.undo()
    return weights

weights = gather(model, influences)
# generate joints
joints = []
for influence in influences:
    pos = cmds.xform(influence, q=True, ws=True, t=True)
    cmds.select(cl=True)
    joints.append(cmds.joint())
    cmds.xform(joints[-1], ws=True, t=pos)
# concatenate weights in the right way
vertexCount = len(weights.values()[0])
influenceCount = len(influences)
vertexWeights = [0] * (vertexCount * influenceCount)
for i in xrange(vertexCount):
    tw = 0
    for j, influence in enumerate(influences):
        vertexWeights[i * influenceCount + j] = weights[influence][i]
        tw += weights[influence][i]
    if not tw:
        # weight is 0
        continue
    for j in xrange(influenceCount):
        vertexWeights[i * influenceCount + j] /= tw
# expand to shape
if not cmds.ls(model, type='mesh'):
    model = cmds.listRelatives(model, c=True, type='mesh')[0]
# bind skin
cmds.select(model, joints)
skinCluster = cmds.skinCluster()
# set weights
cmds.SkinWeights([model, skinCluster],  nwt=vertexWeights)

Maya discovery of the day

if you’re looking for all objects with a specific attribute, it is nice to know that ls and it’s wildcards also work on attributes! It even does not care whether you supply the long or the short name. To get all objects with a translateX attribute you can simply use:

cmds.ls('*.tx')

Wildcards do not work with some other modifiers however, so you can not do this:

cmds.ls('*.myMetaData', l=True, type='mesh', sl=True)

because the returned type is not a mesh, but an attribute; but you can of course do this (notice the o=True to return object names not attributes):

cmds.ls(cmds.ls('*.myMetaData', o=True), l=True, type='mesh', sl=True)

Just wanted to share that bit of information! And while we’re at it, python supports ‘or’ in arbitrary expressions, so if you wish to find all transforms that contain a mesh (or get the transforms of selected meshes at the same time), you’ll often find yourself doing this:

selected_transforms = cmds.ls(type='transform', sl=True, l=True)
selected_meshes = cmds.ls(type='mesh', sl=True, l=True)
if selected_transforms is not None:
    meshes = cmds.listRelatives(selected_transforms, c=True, type='mesh', f=True)
    if meshes is not None:
        if selected_meshes is not None:
            selected_meshes += meshes
        else:
            selected_meshes = meshes
selected_mesh_transforms = []
if selected_meshes is not None:
    selected_mesh_transforms = cmds.listRelatives(selected_meshes, p=True)

just because ls and listRelatives return None instead of an empty list this code is super complicated. With ‘or’ we can simply do this:

meshes = (cmds.ls(type='mesh', sl=True, l=True) or []) + (cmds.listRelatives(cmds.ls(type='transform', sl=True, l=True), c=True, type='mesh', f=True) or [])
selected_mesh_transforms = cmds.listRelatives(meshes, p=True, f=True) or []

Admittedly a bit less readable, but make a utility function or name variables appropriately is my advice!

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")')

Computing 3D polygon volume

I wanted to compute the volume of a mesh in Maya. It was surprisingly simple and elegant to do as well! Using the Divergent Theorem (which is unreadable to me when written mathematically) the only constraints are: the mesh must be closed (no holes, borders or tears; Maya’s fill-hole can help), the mesh must be triangulated (using the Maya API you can already query triangles so no need to manually triangulate in this case).

Now imagine to compute the volume of a prism. All you need is the area of the base triangle * the height. To compute the base I use Heron’s formula as described here.

def distance(a, b):
    return sqrt((b[0]-a[0])*(b[0]-a[0])+
      (b[1]-a[1])*(b[1]-a[1]))

def getTriangleArea(pt0, p1, p2):
    a = distance(pt1, pt0)
    b = distance(pt2, pt0)
    c = distance(pt2, pt1)
    s = (a+b+c) * 0.5
    return sqrt(s * (s-a) * (s-b) * (s-c))

Now notice how this only computes the triangle area in the XY plane. This works simply because the 2D projection of the triangle area is all we need. The height is then defined by the triangle’s center Z.

def getTriangleHeight(pt0, pt1, pt2):
    return (pt0[2] + pt1[2] + pt2[2]) * 0.33333333

Consider any triangle, extrude it down to the floor, and see that this works for any prism defined along the Z axis this way.

A rotated triangle’s area in the XY plane is smaller than the actual area, but by using the face-center the volume will remain accurate.

prisms

Now these prisms have the same volume. The trick is to consider every triangle as such a prism, so call getTriangleVolume on each triangle. The last problem is negative space, for this we compute the normal. I use maya’s normals, so the volume is negative if all normals are inversed, but you can compute them all the same.

def getTriangleVolume(pt0, pt1, pt2):
    area = getTriangleArea(pt0, pt1, pt2) * getTriangleHeight(pt0, pt1, pt2)
    # this is an optimized 2D cross product
    sign = (pt1[0]-pt0[0]) * (pt2[1]-pt0[1]) - (pt1[1]-pt0[1]) * (pt2[0]-pt0[0])
    if not sign:
        return 0
    if sign < 0:
        return -area
    return area

prisms2

The selected wireframe shows the prism defined by the bottom triangle, because the normal.z points downwards it will become negative volume. So adding the initial prism volume and this prism volume will give the accurate volume of this cut off prism. Now consider this:

prisms3

To avoid confusion I placed the object above the grid; but below the grid a negative normal * a negative height will still add volumes appropriately.

So that's it.

from math import sqrt
from maya.OpenMaya import MItMeshPolygon, MDagPath, MSelectionList, MPointArray, MIntArray


def distance(a, b):  
    return sqrt((b[0]-a[0])*(b[0]-a[0]) +   
      (b[1]-a[1])*(b[1]-a[1]))  
      
def getTriangleArea(pt0, pt1, pt2):  
    a = distance(pt1, pt0)  
    b = distance(pt2, pt0)  
    c = distance(pt2, pt1)  
    s = (a+b+c) * 0.5  
    return sqrt(s * (s-a) * (s-b) * (s-c))  
    
def getTriangleHeight(pt0, pt1, pt2):  
    return (pt0[2] + pt1[2] + pt2[2]) * 0.33333333  

def getTriangleVolume(pt0, pt1, pt2):  
    area = getTriangleArea(pt0, pt1, pt2) * getTriangleHeight(pt0, pt1, pt2)  
    # this is an optimized 2D cross product  
    sign = (pt1[0]-pt0[0]) * (pt2[1]-pt0[1]) - (pt1[1]-pt0[1]) * (pt2[0]-pt0[0])  
    if not sign:  
        return 0  
    if sign < 0:  
        return -area  
    return area

def getPolygonVolume(shapePathName):
	volume = 0
	li = MSelectionList()
	li.add(shapePathName)
	path = MDagPath()
	li.getDagPath(0, path)
	iter = MItMeshPolygon(path)
	while not iter.isDone():
		points = MPointArray()
		iter.getTriangles(points, MIntArray())
		for i in range(0, points.length(), 3):
			volume += getTriangleVolume(points[i], points[i+1], points[i+2])
		iter.next()
	return volume

Natural IK Chain

So RiggingDojo.com shared this video series from Yutaca Sawai:

I decided to test it, and quickly made a script to generate a chain of n-segments
Essentially the left chain is the important one (bold in the video) and the rest are just a construct to propagate a single rotation to a full fletched motion.

Open Maya, run this Python script, see for yourself how one rotation and a bunch of parented joints & ikhandles can generate complex motion!

def joint(x,y,z):
    jt = cmds.joint()
    cmds.xform(jt, t=[x,y,z], ws=True)
    return cmds.ls(jt, l=True)[0]
    
def ikHandle(start, end):
    sl = cmds.ls(sl=True)
    cmds.select(start, end)
    ikh = cmds.ikHandle()[0]
    cmds.select(sl)
    return ikh

def constructBase(cycles = 10):
    cmds.select(cl=True)
    rotator = joint(1,0,0)
    
    #demonstrative animation
    cmds.currentTime(0)
    cmds.setKeyframe('%s.rz'%rotator)
    cmds.currentTime(60)
    cmds.setAttr('%s.rz'%rotator, 360)
    cmds.setKeyframe('%s.rz'%rotator)
    cmds.playbackOptions(min=0, max=60)
    
    root = joint(0,1,0)
    chain2root = joint(-2,-1,0)
    cmds.select(root)
    joint(-2,-1,0)
    anchor = joint(0,-3,0)
    cmds.group(ikHandle(root, anchor)) #group to make the ik handle fixed in place
    
    #chain 1
    cmds.select(anchor)
    ikGroups1 = []
    parents1 = []
    for i in range(cycles):
        ikGroups1.append([joint(2,-1 - i * 8,0)])
        joint(2,-5 - i * 8,0)
        ikGroups1[-1].append(joint(-2,-5 - i * 8,0))
        parents1.append(joint(-2,-9 - i * 8,0))

    #chain 2
    cmds.select(chain2root)
    ikGroups2 = []
    parents2 = []
    for i in range(cycles):
        parents2.append(joint(-2,-5 - i * 8,0))
        ikGroups2.append([joint(2,-5 - i * 8,0)])
        joint(2,-9 - i * 8,0)
        ikGroups2[-1].append(joint(-2,-9 - i * 8,0))
    for i in range(len(ikGroups2)):
        cmds.parent(ikHandle(*ikGroups2[i]), parents1[i])
        
    for i in range(len(ikGroups1)):
        cmds.parent(ikHandle(*ikGroups1[i]), parents2[i])


constructBase()

Advanced locator

This is another take on the locator. It supports multiple shapes and can have a unique color instead of only Maya’s built-in colors.

locator

It does not:
> Actually draw curves (I just called it that because I usually use degree-1 curves as controls, it just uses GL_LINES).
> Support separate colors per shape. It is in the end one shape node.

It does:
> Save you from the hassle of parenting curve shapes manually and having other scripts break because you suddenly have too many (shape) children.
> Support any color!

Scripts to convert selected curves to a CurveLocator (it samples smooth curves to have enough points so the look is the same):

Plugin:
> Compiled against Maya 2014
MLL file

Source:
> Solution is Visual Studio 2013
Source

The code that made the preview image:

from maya import cmds
cmds.loadPlugin("CurveLocator.mll", qt=True)

#david star
l = cmds.createNode("CurveLocator", n='CurveLocatorShape')
cmds.setAttr('%s.shapes[0].closed'%l, True)
cmds.setAttr('%s.shapes[0].point[0]'%l, 1.8, -1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[1]'%l, 0.6, -1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[2]'%l, 0, -2, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[3]'%l, -0.6, -1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[4]'%l, -1.8, -1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[5]'%l, -1.2, 0, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[6]'%l, -1.8, 1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[7]'%l, -0.6, 1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[8]'%l, 0, 2, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[9]'%l, 0.6, 1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[10]'%l, 1.8, 1, 0, type='double3')
cmds.setAttr('%s.shapes[0].point[11]'%l, 1.2, 0, 0, type='double3')
cmds.setAttr('%s.color'%l, 0.8, 0.5, 0.1, type='float3')

#joint like shape
from math import sin, cos, pi
l = cmds.createNode("CurveLocator", n='CurveLocatorShape')
cmds.setAttr('%s.shapes[0].closed'%l, True)
cmds.setAttr('%s.shapes[1].closed'%l, True)
cmds.setAttr('%s.shapes[2].closed'%l, True)
for i in range(36):
    cmds.setAttr('%s.shapes[0].point[%s]'%(l, i), cos(i / 18.0 * pi), -sin(i / 18.0 * pi), 0, type='double3')
    cmds.setAttr('%s.shapes[1].point[%s]'%(l, i), cos(i / 18.0 * pi), 0, -sin(i / 18.0 * pi), type='double3')
    cmds.setAttr('%s.shapes[2].point[%s]'%(l, i), 0, cos(i / 18.0 * pi), -sin(i / 18.0 * pi), type='double3')
cmds.setAttr('%s.color'%l, 0.0, 0.87, 0.3, type='float3')

#circle
l = cmds.createNode("CurveLocator", n='CurveLocatorShape')
for i in range(36):
    cmds.setAttr('%s.shapes[0].point[%s]'%(l, i), cos(i / 18.0 * pi), -sin(i / 18.0 * pi), 0, type='double3')
cmds.setAttr('%s.color'%l, 1.0, 0.1, 0.8, type='float3')

#jagged circle
l = cmds.createNode("CurveLocator", n='CurveLocatorShape')
for i in range(36):
    cmds.setAttr('%s.shapes[0].point[%s]'%(l, i), cos(i), -sin(i), 0, type='double3')
cmds.setAttr('%s.color'%l, 1.0, 0.1, 0.8, type='float3')

Maya UI (ELF) wrapper

This UI wrapper was originally created to avoid PyQt installation (and instability) and I recently had the chance to do some bug-fixes and port it to Maya 2010.

What it does is use simple maya UI elements (from the cmds module) but wraps them in a more user friendly and editable way. This is not much to see but it’s to use! Using form layouts in an automated way saves the headache of making things work and align neatly and comes with a nice perk: you can have row-layouts with a dynamic number of columns (because they are actually form layouts).

Alltogether this makes interface code less long and more logical wrapping the static native Maya UI system (to be fair: this system is old and pretty decent, but the amount of exposed API is extremely limited. So for a third party, like me and probably you, it is very hard to use.

Click to get a zip:

PythonUI

Run the install to add the extracted folder to the pythonpath, go into ElfUI/icons/ to find another useful BAT example: drag a PNG on it to get an XPM out of it – > you do need to open it and edit it to point to the right path though (this assumes x64 maya 2010, default location, as you will notice once you open it).

Last but not least, have a little example script that inherits a window and adds some elements into it:

import ElfUI


class UI( ElfUI.Window ):
    def __init__(self):
        super(UI, self).__init__('Easy interface.')
        self.size = [200,300]
        
        self.collapsable = ElfUI.FrameLayout(self, self.layout, 'File list')
        ElfUI.Label(self, self.collapsable, 'Label 1')
        ElfUI.Label(self, self.collapsable, 'Label 2')
        
        self.header = ElfUI.RowLayout(self, self.layout)

        btnA = ElfUI.Button(self, self.header, 'A', None, [16, 32], 'Prints the letter a!')
        btnA.AppendClicked(self._PrintA)
        self.header.AddChild(btnA)
        
        btnB = ElfUI.Button(self, self.header, 'B', None, [32, 16], 'Prints the letter b!')
        btnB.AppendClicked(self._PrintB)
        self.header.AddChild(btnB)
        
    def _PrintA(self):
        print('a')
        
    def _PrintB(self):
        print('b')


UI().show()

Maya Scene Assembly Wrapper

My ex-classmate Freek Hoekstra was asking about scripting with scene assembly nodes, as it appeared to be lacking documentation and generally didn’t work.

So I felt up to the challenge and with some trial and error was able to create an assemblyDefinition with working representations (the trick is to set all attributes or it will disable the entry).

The difficult part was the assemblyReference. It appeared to import the files rather than referencing them. As I finally found in the assemblyReference.cpp source code this is in fact what should happen.

The assemblyReference imports the file you want to reference, looks up the assemblyDefinition node it just imported and then copies it’s attributes and deletes any new nodes it found. Problem is: it can’t find the assemblyDefinition and doesn’t clean up after itself. So that bit I did manually in python by essentially tracking the difference in ALL scene nodes before and after referencing the file. If there are any new nodes the referencing went wrong and I attempt to do it manually. At the very least my wrapper DOES clean up the file and print some more errors if no assemblyDefinition was found.

This code has no interface to go with it yet, I first want to make up some more features before doing so (such as screenshots!). What the wrapper code does support however is single line exporting of multiple selected groups to each be another representation of one object (imagine a file containing all LODs of an asset). It also supports exporting in different types (maya scene, alembic, gpucache).

Then there’s a single line to save an assetDefinition as a separate file (containing just the one assetDefinition node) which is then ready for referencing; creating an assetReference from a file path is another one-liner.

Please look and try out the examples at the bottom, you could create a sphere and a cube, select them both and run all code at once. This should leave you with a folder & 3 files exported next to the current scene as well as an assetDefinition and assetReference node.

To be complete frank there’s also one thing seriously lacking: changing the definition file of the created assemblyReference node from the attribute editor does not work as it results in errors identical to the ones this wrapper fixes. AssetReferences created by the maya ‘create->scene assembly->assembly reference’ button don’t suffer this problem but I don’t know the code that lies behind it.

#
# Resources used:
# cmds.listAttr('dagAsset1.representations', multi=True)
# C:\Program Files\Autodesk\Maya2014\devkit\plug-ins\sceneAssembly
# C:\Program Files\Autodesk\Maya2014\Python\Lib\site-packages\maya\app\sceneAssembly
# http://docs.autodesk.com/MAYAUL/2013/ENU/Maya-API-Documentation/index.html?url=cpp_ref/hierarchy.html,topicNumber=cpp_ref_hierarchy_html
#

                
import os
import os.path
from maya import cmds
from maya.OpenMaya import *
from maya.OpenMayaMPx import *


cmds.loadPlugin('AbcExport.mll', qt=True)
cmds.loadPlugin('AbcImport.mll', qt=True)
cmds.loadPlugin('sceneAssembly.mll', qt=True)


class Enum():
    '''Bare bones Enum implementation for python 2'''
    def __init__(self, *args):
        
        self.reverse_mapping = {}
        self.__dict = {}
        
        for i in range(len(args)):
            self.reverse_mapping[i] = args[i]
            self.__dict[args[i]] = i
    
    def keys(self):
        return self.__dict.keys()
    
    def __getattr__(self, sAttribname):
        try:
            return self.__dict[sAttribname]
        except:
            raise AttributeError



#Export types, determine what function to use (abcExport, gpuCache, file)
SAExportType = Enum( 'Alembic', 'GpuCache', 'Scene' )

#Reference types, defined by the plugin
SAAssetType = Enum( *cmds.adskRepresentation(q=True, lrt=True) )


class SABase(object):
    '''
    Scene assembly base class, shared functionality
    between reference and definition node wrappers
    '''
    _nodename = None
    _node = None
    
    @property
    def nodename(self):
        return self._nodename
        
    @nodename.setter
    def nodename(self, sNewNodePath):
        if cmds.objExists(sNewNodePath):
            sFullPath = cmds.ls(sNewNodePath, l=True, type=self._wrappedType)[0]
            if not sFullPath:
                cmds.error('Attempting to swap scene assembly node %s with %s, but new node is not of type %s, ignored'%(self._nodename, sNewNodePath, self.__wrappedType))
                return
            self._nodename = sFullPath
            li = MSelectionList()
            MGlobal.getSelectionListByName(self._nodename, li)
            obj = MObject()
            li.getDependNode(0, obj)
            if obj.isNull():
                cmds.error('Attempting to swap scene assembly node %s with %s, but MObject could not be found, ignored'%(self._nodename, sNewNodePath))
                return
            self._node = MFnAssembly( obj )
        else:
            cmds.error('Attempting to swap scene assembly node %s with non existing node %s, ignored'%(self._nodename, sNewNodePath))
            return

    @property
    def activeRepresentationName(self):
        return self._node.getActive()
    
    @activeRepresentationName.setter
    def activeRepresentationName(self, sNewName):
        bValidName = False
        
        #validate name
        iaValidIndices = cmds.getAttr('%s.representations'%self.nodename, multiIndices=True)
        if not iaValidIndices:
            iaValidIndices = []
        for iValidIndex in iaValidIndices:
            if sNewName == cmds.getAttr('%s.representations[%s].repName'%(self.nodename, iValidIndex)):
                bValidName = True
        
        if not bValidName:
            cmds.error('Attempting to activate representation %s on assembly node, but %s has no representation with that name, ignored.'%(sNewName, self._nodename))
            return
        
        #set name
        self._node.activate(sNewName)
    
    
    def __init__(self):
        '''
        ABSTRACT CLASS, do not initialize
        '''
        cmds.error('Initializing SABase, but this is an abstract class. You probably intend to use SAReference or SADefinition.')
        return


class SAReference(SABase):
    '''
    Scene assembly helper class to represent a referenced
    asset in code
    
    NOTE: It should be possible to bind class to existing
    node when working from existing scenes / data so when
    extending take this class implement this funcitonality!
    '''
    
    
    #set the node type for this class, important for error handling
    _wrappedType = 'assemblyReference'
    
    
    def __init__(self, sNodeFullPath=None):
        '''
        @param sNodeFullPath: string, full path name of the
        existing assemblyReference node to bind this object
        instance to.
        '''
        
        #if no argument is given, initialize a blank class
        if sNodeFullPath == None:
            self.nodename = cmds.createNode('assemblyReference')
            return
        
        #else wrap the node given
        if not type(sNodeFullPath) in (unicode, str):
            cmds.error('Trying to initialize SAAsset from %s but argument is not a string'%sNodeFullPath)
            return
        sPath = cmds.ls(sNodeFullPath, type='assemblyReference', l=True)
        if not sPath:
            cmds.error('Trying to initialize SAAsset from %s but argument is not a valid assemblyReference node'%sNodeFullPath)
            return
        self.nodename = sPath[0] 
    
    
    @classmethod
    def CreateFromFile(cls, sFilePath):
        '''
        Given a filepath this creates a reference nodes and connects the path
        It is not capable of reading information of the file beforehand, so just
        like maya's builtin create assembly reference menu it gives errors upon
        importing a file without a reference node and does not obey the LOD saved
        inside the referenced file.
        
        @TODO:
        This function does not work! It just appears to use do regular import...
        '''
        if not os.path.exists(sFilePath):
            cmds.error('Attempting to create scene assembly reference to %s, but file does not exist, ignored.'%sFilePath)
            return
        outInstance = SAReference()
        
        #rename the node
        sFileName = sFilePath.replace('\\','/').rsplit('/',1)[-1].rsplit('.',1)[0]
        outInstance.nodename = cmds.rename(outInstance.nodename, sFileName)
        
        #set the file path
        #POSTLOAD fails and leaves us with a bunch of nodes so
        #let's search for the assemblyDefinition ourselves and
        #keep things clean eh!
        allNodes = cmds.ls(l=True)
        
        #this should work and newNodes should be empty, but it does not work and leaves a mess
        cmds.setAttr('%s.definition'%outInstance.nodename, sFilePath, type='string')
        
        #get file changes
        newNodes = list( set(cmds.ls(l=True))-set(allNodes) )
        if not newNodes:
            cmds.warning('SAReference.CreateFromFile: Reference definition either worked or file was empty. Returning outInstance assuming it is valid.')
            return outInstance
        
        #get assembly definition
        saValidNodes = cmds.ls(newNodes, type='assemblyDefinition', l=True)
        if not saValidNodes or len(saValidNodes) != 1:
            #too many or too few definitions, clean the file
            cmds.delete(newNodes)
            cmds.delete(outInstance)
            cmds.error('Attempting to set assembly reference file to %s but 0 or more than 1 definition nodes were found. File could not be referenced, assemblyReference node removed.'%sFilePath)
            return
        
        iaValidInidices = cmds.getAttr('%s.representations'%saValidNodes[0], multiIndices=True)
        if iaValidInidices:
            #copy all representations
            for i in iaValidInidices:
                #get
                sRepName = cmds.getAttr('%s.representations[%s].repName'%(saValidNodes[0], i))
                sRepLabel = cmds.getAttr('%s.representations[%s].repLabel'%(saValidNodes[0], i))
                sRepType = cmds.getAttr('%s.representations[%s].repType'%(saValidNodes[0], i))
                sRepData = cmds.getAttr('%s.representations[%s].repData'%(saValidNodes[0], i))
                #set
                cmds.setAttr('%s.representations[%s].repName'%(outInstance.nodename, i), sRepName, type='string')
                cmds.setAttr('%s.representations[%s].repLabel'%(outInstance.nodename, i), sRepLabel, type='string')
                cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), sRepType, type='string')
                cmds.setAttr('%s.representations[%s].repData'%(outInstance.nodename, i), sRepData, type='string')
                
            #apply last representation as default
            if len(iaValidIndices) != 0:
                iFurthest = iaValidIndices[len(iaValidIndices)-1]
                sRepName = cmds.getAttr('%s.representations[%s].repName'%(saValidNodes[0], iFurthest))
                outInstance.activeRepresentationName = sRepName

        cmds.delete(newNodes)
        return outInstance


class SAAsset(SABase):
    '''
    Scene assembly helper class to create and represent an
    asset in code
    
    NOTE: It should be possible to bind class to existing
    node when working from existing scenes / data so when
    extending take this class implement this funcitonality!
    '''
    
    
    #set the node type for this class, important for error handling
    _wrappedType = 'assemblyDefinition'
    
    
    def __init__(self, sNodeFullPath=None):
        '''
        @param sNodeFullPath: string, full path name of the
        existing assemblyDefinition node to bind this object
        instance to.
        '''
        
        #if no argument is given, initialize a blank class
        if sNodeFullPath == None:
            self.nodename = cmds.createNode('assemblyDefinition')
            return
        
        #else wrap the noe given
        if not type(sNodeFullPath) in (unicode, str):
            cmds.error('Trying to initialize SAAsset from %s but argument is not a string'%sNodeFullPath)
            return
        sPath = cmds.ls(sNodeFullPath, type='assemblyDefinition', l=True)
        if not sPath:
            cmds.error('Trying to initialize SAAsset from %s but argument is not a valid assemblyDefinition node'%sNodeFullPath)
            return
        self.nodename = sPath[0]
    
    def SaveAsAssembly(self):
        '''
        Exports this asset to a file using currentSceneName_alembic_assembly
        
        @returns: string, the new file path

        @TODO:
        support suffixing (don't assume alembic),
        support multiple assets exported from one file (so not based on scene name)
        '''
        sCurrentFile = cmds.file(q=True, sn=True)
        if not sCurrentFile:
            cmds.error('Scene needs to be saved first, subfolder and LOD files will be created next to it')
            return
        sSceneType = cmds.file(q=True, type=True)[0]
        sCurrentDirectory, sCurrentFileName = sCurrentFile.replace('\\','/').rsplit('/', 1)
        sCurrentFileName, sCurrentExtension = sCurrentFileName.rsplit('.',1)
        
        cmds.select(self.nodename)
        sAssemblyFilePath = '%s/%s_alembic_assembly.%s'%(sCurrentDirectory, sCurrentFileName, sCurrentExtension)
        cmds.file(sAssemblyFilePath, force=True, type=sSceneType, pr=True, es=True);
        
        return sAssemblyFilePath
    
    
    @classmethod
    def CreateFromGroups(cls, saLodGroups, iExportType):
        '''
        @param saLodGroups: string array, full path names of
        each group starting from most detailed to least detailed
        
        @param exportType: SAExportType, defines the export function to use
        
        This function exports each group to a separete file and
        creates a sceneassembly node pointing to each file as
        next lod level
        '''
        #get selection
        sSelection = cmds.ls(sl=True, l=True)
        
        #grab info from current scene
        sCurrentFile = cmds.file(q=True, sn=True)
        if not sCurrentFile:
            cmds.error('Scene needs to be saved first, subfolder and LOD files will be created next to it')
            return
        sSceneType = cmds.file(q=True, type=True)[0]
        sCurrentDirectory, sCurrentFileName = sCurrentFile.replace('\\','/').rsplit('/', 1)
        sCurrentFileName = sCurrentFileName.rsplit('.',1)[0]
        
        #generate directory to store lods ins
        sLodDir = '%s/%s_LODs'%(sCurrentDirectory, sCurrentFileName)
        if not os.path.exists(sLodDir):
            os.makedirs(sLodDir)
        
        #create dag asset to put lods into
        outInstance = SAAsset()

        if True: #try:
            #export lods
            for i in range(len(saLodGroups)):
                #get file name
                sOutFileName = '%s_lod%s'%(sCurrentFileName, i)
                #get file full path
                sLodFilePath = '%s/%s'%(sLodDir, sOutFileName)
                
                if SAExportType.GpuCache:
                    iCurrentFrame = cmds.currentTime(q=True)
                    sLodFilePath = '%s.abc'%sLodFilePath
                    sDir, sName = sLodFilePath.replace('\\','/').rsplit('/',1)
                    sGpuCacheFile = cmds.gpuCache(saLodGroups[i], startTime=iCurrentFrame, endTime=iCurrentFrame, directory=sDir, fileName=sName)
                    
                    #append extension to show the file type in the representation name
                    sOutFileName = '%s.abc'%sOutFileName
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Cache', type='string')
                    
                elif SAExportType.Alembic:
                    iCurrentFrame = cmds.currentTime(q=True)
                    sLodFilePath = '%s.abc'%sLodFilePath
                    cmds.AbcExport(j='-frameRange %s %s -root %s -file %s'%(iCurrentFrame, iCurrentFrame, saLodGroups[i], sLodFilePath))
                    
                    #append extension to show the file type in the representation name
                    sOutFileName = '%s.abc'%sOutFileName
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Cache', type='string')
                    
                else:
                    cmds.select(saLodGroups[i])
                    cmds.file(sLodFilePath, force=True, type=sSceneType, pr=True, es=True);
                    
                    #append extension
                    sOutFileName = '%s.%s'%(sOutFileName, sCurrentFile.rsplit('.',1)[-1])
                    sLodFilePath = '%s.%s'%(sLodFilePath, sCurrentFile.rsplit('.',1)[-1])
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Scene', type='string')
                    
                #set node attributes
                cmds.setAttr('%s.representations[%s].repName'%(outInstance.nodename, i), sOutFileName, type='string')
                cmds.setAttr('%s.representations[%s].repLabel'%(outInstance.nodename, i), sOutFileName, type='string')
                cmds.setAttr('%s.representations[%s].repData'%(outInstance.nodename, i), sLodFilePath, type='string')
                
                #default to furthest lod
                if i == len(saLodGroups)-1:
                    outInstance._node.activate(sOutFileName)
        else: #except:
            cmds.delete(outInstance)
            cmds.select(sSelection)
            cmds.error('LOD exporting and linking failed, no scene assembly definition created.')
            return
        
        #restore selection, make redo easier & avoid confusion
        if sSelection:
            cmds.select(sSelection)
        
        return outInstance

'''
#Usage examples

#Create an assemblyDefinition and for each selected transform: export and add as representation
dagAsset1 = SAAsset.CreateFromGroups( cmds.ls(sl=True, l=True, type='transform'), SAExportType.GpuCache)

#Wrap an existing assemblyDefinition
dagAsset1 = SAAsset( 'dagAsset1' )

#Set the currently visible definition (default is last)
dagAsset1.activeRepresentationName = 'crystal_pylon_lod0.abc'

#Export the wrapped assemblyDefinition to a separate file for referencing
sExportedPath = dagAsset1.SaveAsAssembly()

#Reference a filePath, assuming it contains exactly one assemblyDefinition node (other nodes are discarded)
reference1 = SAReference.CreateFromFile( sExportedPath )

#Set the currently visible definition in the reference (default is last)
reference1.activeRepresentationName = 'crystal_pylon_lod2.abc'
'''

Maya scatter node

Just enjoying exploring the API…

I made a node that scatters a bunch of points and then a script that puts locators on those positions; not useful yet, but this attribute can be pushed to another node to say.. instance a mesh around or as source to spawn something from or whatever you like.

cmds.file(new=True, f=True)
cmds.unloadPlugin('MayaAPI.mll')

cmds.loadPlugin('MayaAPI.mll')
scatterNode = cmds.createNode('scatterPointsOnMesh')
inMesh = cmds.polyCylinder()[0]
cmds.connectAttr( '%s.outMesh'%inMesh, '%s.inMesh'%scatterNode )
cmds.dgeval( '%s.outPoints'%scatterNode )

for point in cmds.getAttr( '%s.outPoints'%scatterNode ):
    cmds.spaceLocator(p=point[0:3])

It left me with the knowledge that compiling for 32bit really won’t work if you only have 64bit Maya; I was using VS2008 which didn’t let me compile to 64bit profile so after installing VS2012 I had more success.

Additionally if you create numeric attributes, their data type in the compute method is going to be MFnData::kNumeric and not a more detailed MFnNumericType.

scatter

So what it basically does is use stdlib’s rand to get random UV coordinates (it returns an int so use (float)rand() / (float)RAND_MAX to get desirable results). Then it iterates over the mesh polygon Ids and uses MFnMesh.getPointAtUv to see whether the UV coord lies in that polygon and where the 3D point is.

I can see the flaw of non 0-1 space UVs or no UVs here, so make sure to use automatic mapping, auto layout and set the UV set before implementing this somewhere useful.

It’s something really basic, but brief nodes are the most reusable ones. Download solution here…

Is it purple?

Or.. is it affected by a selected object?

I asked this question on Creative Crash and this reply was definately helpful in finding out how to deal with this; but don’t use affectsNet, it creates tons of nodes which contain info you could also come up with or print out. The golden tip was simply to look at the matrix and shape attributes of geometry and transform nodes.

So to do this we use the cmds.affects function, which tells what inputs of a node have influence (affect) the given output (it can do the other way around with the by flag, I don’t use that here however).

So whether a transform node is affected seems reasonably simple, the shape turns purple when one of it’s parents’ matrices is affected by a selected node.

>We use affects to find out what inputs affect the matrix attribute on a transform node
>Then we list the incoming connection of every parent
>We filter the incoming connections to only those that connect to the attributes returned by affects
>We check if one of these nodes is selected

But then it’s not that simple. The matrix attribute is affected by several attributes, these attributes are affected by more attributes, hence we need to iterate over all affecting attributes to see what affects them, until we had all the attributes and have a clear map of what nodes – directly or indirectly – affect the matrix attribute.

When none of the nodes are selected, we’re not there yet – what if a parent of an incomoing connection is selected? That is solved with the isParentSelected function posted here.

Now what if the node (and its parents) isn’t selected, maybe the output attributes that drive the inputs that affect the matrix attribute on the done we wish to know more about are affected by inputs which are connected as well. On second thought, look at this image instead of attempting to grasp that scentence.

affects

So we wish to know whether the rightmost node is affected, we map the matrix attribute and see that the red attributes affect eachother. Now the other node connected to it isn’t selected, but we must map the incoming connections’ affection to see that the blue attributes also affect eachother, because there are more affecting attributes we need to map inputs on the input nodes’ affected attributes as well, giving us the leftmost node which IS selected and therefore the rightmost node IS affected!

So the first trick is to get the true affects result by iterating of the the initial result until no more attributes affect the affects set.

def affectsAll( attr, type ):
    #these lists can in theory be precalculated constants
    attrs = cmds.affects(attr, t=type)
    if not attrs:
        return []
    i = 0
    while i < len(attrs):
        tmp = cmds.affects(attrs[i], t=type)
        if tmp:
            attrs.extend(tmp)
        attrs = list(set(attrs))
        i += 1
    return attrs

The next step is to find the full network of affected attributes by listing inputs, filtering by affected attributes, and iterating again as if we wanted to know whether that input node was affected. This can be done by going over all known nodes, starting with the given node, then appending all valid input nodes to the target list and repeating the iteration:

def affectedNet( inAttr, inNode ):
    nodes = [inNode]
    attributes = [[inAttr]]
    
    #iterate until affection found or entire network traversed
    i = 0
    while i < len(nodes):
        #find internel affection net
        attributes[i].extend( affectsAll(attributes[i][0], cmds.nodeType(nodes[i])) )
        
        #find nodes that are connected to plugs in the affected net
        inputs = cmds.listConnections(nodes[i], s=True, d=False, c=True, p=True)
        if inputs:
            for j in range(0,len(inputs),2):
                #attribute name in affectednet
                if inputs[j].rsplit('.',1)[-1] in attributes[i]:
                    #get node attribute pair
                    nodeattr = inputs[j+1].split('.',1)
                    nodeattr[0] = cmds.ls(nodeattr[0], l=True)[0]
                    if nodeattr[0] not in nodes:
                        #append new nodes
                        nodes.append(nodeattr[0])
                        attributes.append([nodeattr[1]])
                    else:
                        #append new plugs on known nodes
                        attributes[ nodes.index(nodeattr[0]) ].append( nodeattr[1] )
        
        #if no incoming node was selected, continue iterating
        i += 1
    return nodes, attributes

The next step is to provide input for these functions. If we wish to check whether a shape node is affected this is most cumbersome, as every shape node's out geometry attribute has a different name. So we assume the node to be a transfomr node with the attribute that determines affected color being 'matrix'. Then we check whether the object is a shape and change the attribute name before finally grabbing the affectedNet for that node/attr combination and checking whether any node in it, or one if that node's, parents is selected.

def isAffected(inPathStr):
    #assume node is a transform by default
    attrib = 'matrix'
    
    #get the output attribute if node is a shape
    if cmds.ls(inPathStr, type='shape'):
        #detect the attribute name to get the affectedNet for
        nodetype = cmds.nodeType( inPathStr )
        if nodetype == 'mesh':
            attrib = 'outMesh'
        elif nodetype == 'subdiv':
            attrib = 'outSubdiv'
        elif nodetype in ('nurbsCurve','nurbsSurface'):
            attrib = 'local'
        else:
            raise ValueError('Nodetype %s of node %s not supported in isAffected'%(nodetype, inPathStr))
    elif not cmds.ls(inPathStr, type='dagNode'):
        raise ValueError('Given node path %s is not a Dag node in isAffected'%inPathStr)

    
    for node in affectedNet(attrib, inPathStr)[0]:
        if isParentSelected(node):
            return True
    return False

Then the very last thing we need to do is to not check only the given node, but all it's parent nodes as well. Because if not the shape, then perhaps a parent is affected and the shape still needs to appear affected.

def isAffectedRecursively(inPathStr):
    obj = cmds.ls(inPathStr, l=True)
    if not obj:
        return False
    obj = obj[0]
    while obj and len(obj) > 1:
        if isAffected(obj):
            return True
        obj = obj.rsplit('|',1)[0]
    return False

By merging the affectedNet with the isAffected function I managed to get a 15% speed increase as this function is reasonably slow, but it cancels simply as soon as a found node is selected. What may be better is to cache all the affected networks once we need them (put them in a dict, key is the full dagpath string) and then use that. Just the merged code in case you disagree:

def isAffected( inNode ):
    nodes = [inNode]
    attributes = [['matrix']]
    
    
    #get the output attribute if node is a shape
    if cmds.ls(inNode, type='shape'):
        #detect the attribute name to get the affectedNet for
        nodetype = cmds.nodeType( inNode )
        if nodetype == 'mesh':
            attributes[0][0] = 'outMesh'
        elif nodetype == 'subdiv':
            attributes[0][0] = 'outSubdiv'
        elif nodetype in ('nurbsCurve','nurbsSurface'):
            attributes[0][0] = 'local'
        else:
            raise ValueError('Nodetype %s of node %s not supported in isAffected'%(nodetype, inNode))
    elif not cmds.ls(inNode, type='dagNode'):
        raise ValueError('Given node path %s is not a Dag node in isAffected'%inNode)


    #iterate until affection found or entire network traversed
    i = 0
    while i < len(nodes):
        #find internel affection net
        attributes[i].extend( affectsAll(attributes[i][0], cmds.nodeType(nodes[i])) )
        
        #find nodes that are connected to plugs in the affected net
        inputs = cmds.listConnections(nodes[i], s=True, d=False, c=True, p=True)
        if inputs:
            for j in range(0,len(inputs),2):
                #attribute name in affectednet
                if inputs[j].rsplit('.',1)[-1] in attributes[i]:
                    #get node attribute pair
                    nodeattr = inputs[j+1].split('.',1)
                    nodeattr[0] = cmds.ls(nodeattr[0], l=True)[0]
                    if nodeattr[0] not in nodes:
                        #bail as soon as node is affected
                        if isParentSelected(nodeattr[0]):
                            return True
                        #append new nodes
                        nodes.append(nodeattr[0])
                        attributes.append([nodeattr[1]])
                    else:
                        #append new plugs on known nodes
                        attributes[ nodes.index(nodeattr[0]) ].append( nodeattr[1] )
    
        #if no incoming node was selected, continue iterating
        i += 1
    return False

Remove the affectedNet and replace the isAffected function with the above and run this testing code to see the printed time drop as well as to see the function's cases functional (note I didn't use isAffectedRecursively here):

#test code
c = cmds.polyCube()[0]
s = cmds.polySphere()[0]
cmds.xform(s,t=[0,0,2])
cn = cmds.orientConstraint(c, s)
import time
t = time.time()
print 'direct input selection'
cmds.select(cn)
print isAffected( s )
print 
print 'direct input selection that does not drive an affecting attribute'
n = cmds.group(em=True)
cmds.connectAttr('%s.visibility'%n, '%s.visibility'%s)
print isAffected( s )
print 
print 'secondary input selection'
cmds.select(c)
print isAffected( s )
print 
print 'input parent selection'
cmds.group(c)
print isAffected( s )
print 
print 'irrelevant selection'
cmds.select(cmds.listRelatives(c,c=True,f=True))
print isAffected( s )
print 
print 'no selection'
cmds.select(cl=True)
print isAffected( s )
print 
print 'beware: the object affects itself because it contains the constraint'
cmds.select(s)
print isAffected( s )
print 
print 'as you can see it\'s shape does not'
cmds.select(cmds.listRelatives(s,c=True,f=True,type='shape'))
print isAffected( s )
print 
print 'and with the constraint deleted neither does the object any longer'
cmds.delete(cn)
cmds.select(s)
print isAffected( s )
print 
print time.time()-t

Detecting wire color in Maya II

Continueing from here I am going to look at override attributes. An object’s display color is affected by overrides of itself, or of it’s parents. For this I wrote a function that checks whether overrides are enabled, if not I check it for the parent, its parent, and so on. When an object has overrides enabled, I wish to get the color and later the displayType (template/reference). After having written the code I decided to create a simpler function that can get an override attribute by name instead of having multiple functions doing the same thing.

def overrideAttr(inObj, inAttr):
    target = inObj
    while target:
        if not cmds.getAttr('%s.overrideEnabled'%target):
            target = cmds.listRelatives(target, p=True, f=True)[0]
        return cmds.getAttr('%s.%s'%(target,inAttr))

The neat thing about this is that if the overrideDisplayType is set back to normal while the parent is templated, it will return 0 and display the object as normal, which it should, automatically. Then to apply this I only need to insert this code right before final line in drawColor:

    #override color
    overridecolor = overrideAttr(shape, 'overrideColor')
    if overridecolor: #not None and not 0
        return cmds.colorIndex( overridecolor, q=True )

But now we can easily expand this to templating and referencing as well by getting the overrideDisplayType. Then if the object is selected we need to return activeTemplate color, otherwise simply the templateColor or referenceColor will suffice. Now here’s a confusing bit: the displayColor is referenceLayer so that it won’t be confused with file referencing and for the template we use displayRGBColor because in the preferences this does not have a simple index, but a free RGB selection unlimited to the palette of other colors. This goes for a select list of colors, which you can also read by using

for i in cmds.displayRGBColor(list=True): print i

Now for ease of use I added a display layer, added the testing cube in it, and printed the color in every state: selected templated (orange), selected referenced (which is just the lead green), templated (gray), referenced (black), colorized layer and normal (in my case blue) and it all works as you may see by trying!

So here’s the drawColor function in full again:

def drawColor(inObj):
    shapes = maya.utils.executeInMainThreadWithResult( 'cmds.listRelatives(\'%s\', ad=True, type=\'shape\', f=True)'%inObj )
    if not shapes:
        if cmds.nodeType(inObj) != 'transform':
            shape = inObj
        else: #transform node without shapes has no color
            return None
    else:
        shape = shapes[0]

    nodetype = displayColorType( shape )
    selected = isParentSelected( shape )
    displaytype = overrideAttr(shape, 'overrideDisplayType')
    
    if selected:
        #templated
        if displaytype == 1:
            return cmds.colorIndex( cmds.displayColor('activeTemplate', q=True, active=True), q=True )
        #lead
        if selected == cmds.ls(os=True, l=True)[-1]:
            return cmds.colorIndex( cmds.displayColor('lead', q=True, active=True), q=True )
            
        #active
        return cmds.colorIndex( cmds.displayColor(nodetype, q=True, active=True), q=True )
        
    #referenced
    if displaytype == 2:
        return cmds.colorIndex( cmds.displayColor('referenceLayer', q=True), q=True )
        
    #templated
    if displaytype == 1:
        return cmds.displayRGBColor('template', q=True)
    
    #override color
    overridecolor = overrideAttr(shape, 'overrideColor')
    if overridecolor: #not None and not 0
        return cmds.colorIndex( overridecolor, q=True )

    #dormant
    return cmds.colorIndex( cmds.displayColor(nodetype, q=True, dormant=True), q=True )

Now the last thing to do is find out if an object is affected by another, selected, object. I will implement this by inserting, above the lines for referenced objects, directly after the indent for selected objects, the following:

    #affected
    if cmds.displayPref( q=True, displayAffected=True ) and isAffectedRecursively( shape ):
        #if obj is affected by something that is selected
        return cmds.colorIndex( cmds.displayColor('activeAffected', q=True, active=True), q=True )

Now the displayPrefe is a maya command and is necessary to disable the returning of this color if the user disabled it in their preferences. The isAffectedRecursively function is a long awnser to a simple question ‘is it purple?’ which I have described in detail (with code) here.

Detecting wire color in Maya

When creating a shape node with Maya’s API in the draw event you simply get the state of the object. Sadly, this can never be retrieved anywhere else (unless we’d override all Maya nodes to have them store the value somewhere). After a long search I found no way of replicating what Maya does before drawing a node, so I had to come up with a different method.

When determining the color of an object’s wireframe there’s all kind of inÎfluences. Is it:
>selected
>a lead selection
>templated
>referenced
>in a layer which is templated or referenced
>does it have an override color set
>does it have a layer with a color
>does it have a parent with an override color
and most hated of all:
>is it purple? (affected by a selected object)

Now luckily layers drive the overridesEnabled, overrideColor and overrideDisplayType attributes, so we don’t really have to worry about those.

An important part is determining in which order of importance these colors are determined. Essentially templating is most important

>objects turn orange when selected and templated simultanously
>green when selected as last (lead)
>white when selected
>purple when influenced by other selected objects (affected)
>gray when templated
>black when referenced
>overrideColor when enableOverrides is True
>blue otherwise

All these properties are inherited from parents as well. So when a referenced object has a drawingOverride, it is still black, when an object is affected by another selected object but also selected it will still be green (or white). When an object’s parent is templated, the object itself appears templated, etcetera.

Do realize this only applies to shapes, as they are the only objects actually being drawn!

The next problem once we know all this information, is determining what colour links to that info. There’s the displayColor and displayRGBColor commands for that, and lucky for use, they have a list feature. So printing each entry and then reading for quite a while we find out the names of the attributes (which mostly match those in the Window -> Settings/preferences -> Color Settings but not always).

So some colors can be set freely, such as the template color. Other colors can only be set to certain indices, displayColor returns a number and we’ll have to use the colorIndex command to get to the actual color. We could hardcore the colors, but then the result is not matching the display if the user changes his settings.

So let’s start with the most basic scenario, a given node’s child shape’s dormant color. Here we run immediately into the next issue, every shape type can have it’s own deselected and selected color and most of the names do not match the nodeType name. For example a measure node is of type distanceDimShape and it’s color needs to be retrieved as ‘dimension’. So here’s a partial list of name conversion:

def displayColorType(inObj):
    objtype = cmds.nodeType(inObj)
    if objtype == 'nurbsSurface':
        trims = cmds.listConnections(shape, s=True, d=False, type='planarTrimSurface')
        if trims:
            obtype = 'trimmedSurface'
        else:
            objtype = 'surface'
    if objtype == 'nurbsCurve':
        projectCurves = cmds.listConnections(shape, s=True, d=False. type='projectCurve')
        if projectCurves:
            objtype = 'curveOnSurface'
        else:
            objtype = 'curve'
    if objtype == 'mesh':
        objtype = 'polymesh'
    if objtype == 'joint' and cmds.listRelatives(shape, ad=True, type='effector'):
        objtype = 'segment'
    if objtype == 'cluster':
        objtype = 'locator'
    if objtype == 'distanceDimShape':
        objtype = 'dimension'
    return objtype

This function is not limited to shapes, but it will result in errors when you attempt to use

cmds.displayColor(displayColorType(),q=True)

on a transform node.

So I assume this to be all I need but the list may get longer when it turns out certain objects try to get their color by the wrong name. Now let’s check whether the object is selected or not and return either the dormant or the active color related to its type:

def drawColor(inObj):
    shapes = cmds.listRelatives(inObj, ad=True, type='shape', f=True)
    if not shapes:
        if cmds.nodeType(inObj) != 'transform':
            shape = inObj
        else: #transform node without shapes has no color
            return None
    else:
        shape = shapes[0]

    nodetype = displayColorType( shape )
    if shape in cmds.ls(sl=True,l=True):
        return cmds.colorIndex( cmds.displayColor(nodetype, q=True, active=True), q=True )
    return cmds.colorIndex( cmds.displayColor(nodetype, q=True, dormant=True), q=True )

print( drawColor( cmds.polyCube()[0] ) )

Now this will immediately print the wrong color, as the parent is selected and not the shape.

#result: [1.0, 1.0, 1.0]

So let’s solve that bit with the following function:

def isParentSelected(inObj):
    selection = cmds.ls(sl=True, l=True)
    target = cmds.ls(inObj, l=True)[0] #ensure full path
    while target:
        if target in selection:
            return True
        target = cmds.listRelatives(target, p=True, f=True)[0]
    return False

Now in drawColor on line 11 instead of using

if shape in cmds.ls(sl=True, l=True)

I will use

if isParentSelected(shape):

Now it returns the the selected color, which is white.

#result: [1.0, 1.0, 1.0]

But our object is in the lead, so it is green. So with some modifications this isn’t too difficult. Let the isParentSelected return the parent (or None) instead of True. I also fixed the part where I forgot to check if we had a selection, ls and listRelatives return None instead of an empty list if there is no result, so we get problems at ‘target in selection’ if there is no selection.

def isParentSelected(inObj, ignoreSelf=False):
    selection = cmds.ls(sl=True, l=True)
    if not selection: #no selection, no result
        return
    if not ignoreSelf:
        if inObj in selection:
            return inObj
    targets = cmds.listRelatives(inObj, ap=True, f=True)
    if not targets:
        return
    for target in targets:
        if target in selection:
            return target
    return

Then the last bit of drawColor becomes this:

    nodetype = displayColorType( shape )
    selected = isParentSelected( shape )
    if selected:
        if selected == cmds.ls(os=True, l=True)[-1]:
            return cmds.colorIndex( cmds.displayColor('lead', q=True, active=True), q=True )
        return cmds.colorIndex( cmds.displayColor(nodetype, q=True, active=True), q=True )
    return cmds.colorIndex( cmds.displayColor(nodetype, q=True, dormant=True), q=True )

The ls function returns the ordered selection, ensuring that the last entry is indeed the lead entry and we use the active lead displayColor isntead of the active nodetype displayColor. The lead color can also be customised, but not per object type.

#result: [0.2630000114440918, 1.0, 0.63899999856948853]

Now this post is getting rather long, so more on this later, where I’ll have a look into overrides.

Poly ribbon painting

I’m working at this with Perry Leijten

It’s inspired by 3pToolsLite, recently released for 3DsMax

We’re trying to get as far as possible in this week, about 4 days left, I hope to go into all the steps in more detail for anyone interesting in how this functions and all the things we learned about Maya while creating it that can serve many other applications.

3D to pixels in Maya

When looking to find the 2D bounding rect of an object I came across this:
http://www.185vfx.com/2003/03/convert-a-3d-point-to-2d-screen-space-in-maya/
Sadly, it is the only tagged tech, maya or mel, so no more goodies there.

Here’s a python version. I intended to do it with more API, but the heighFieldOfView of the MFnCamera does not return the same value as the cmds.camera(hfv=True), not even when you convert it to radians, or when you put it through a tan operation. Hence I have no idea how that value is formatted and can’t continue with it.

Updated the math library for this (somewhere in python, located all the way at the bottom), to include a boundingbox class and fixed matrix to vector multiplication (I mixed up rows and columns before according to how Maya formats matrices, which is how openGL formats them as well).

It returns a BoundingBox with two 2D vectors (min and max) that represent screenspace coordinates. Multiply it with the width of the image to get actual pixels. The aspect ratio is squared so the top of the image may not be 1 if the height is not equal to the width. If you multiply the outy with the image width, you’ll still get the proper pixels though.

import math

from maya.OpenMaya import *
from maya import cmds
from Vmath.boundingbox import BoundingBox

import Vmath.matrix
import Vmath.vec

PI = 3.14159
DEGTORAD = (PI/180)

def getScreenspaceBoundingRect(in_objstr, in_camstr):
    hfv = math.tan(cmds.camera(in_camstr, hfv=True, q=True)*0.5*DEGTORAD)
    vfv = math.tan(cmds.camera(in_camstr, vfv=True, q=True)*0.5*DEGTORAD)
    worldInverseMatrix = Matrix( 4, 4, cmds.getAttr('%s.worldInverseMatrix'%in_camstr) )
    ar = cmds.camera(in_camstr, q=True, ar=True)
    '''
    handle meshes
    '''
    path = getDagPathFromName(in_objstr)
    iter = MItMeshVertex( path )    
    bb = None
    while not iter.isDone():
        pt = iter.position(MSpace.kWorld)
        pt = worldInverseMatrix*Vec(pt.x,pt.y,pt.z)
        if pt[2]:
            ptx,pty = 0,0
            ptx = ((pt[0]/-pt[2])/hfv)*0.5+0.5
            pty = ((pt[1]/-pt[2])/vfv)*0.5+ar*0.5
            pty *= (1.0/ar)
            if not bb:
                bb = BoundingBox([ptx,pty],[ptx,pty])
            else:
                bb.extend([ptx,pty])
        iter.next()
    return bb


def getDagPathFromName(in_name):
    selector = MSelectionList()
    MGlobal.getSelectionListByName(in_name,selector)
    path = MDagPath()
    selector.getDagPath(0, path)
    return path

Geodesic Sphere with UI

You can go and put this in your self as a python button straight away.

The geo sphere contains a perfect flat horizontal edge loop on the XZ plane, this means that you can take only half the sphere as a nice dome with a flat border edge.

from maya import mel
from maya.OpenMaya import *
from maya import cmds


def makegeosphere(in_divisions, in_radius):
    if in_divisions == 1 :
        return cmds.polyPlatonicSolid(r=in_radius,st=2,cuv=4,ch=False)
    elif in_divisions >= 2:
        out_obj = cmds.polyPlatonicSolid(r=in_radius,st=1,cuv=4,ch=False)
        out_obj = cmds.ls(out_obj,l=True)[0]
        cmds.xform(out_obj,ro=[0,0,31.717])
        cmds.makeIdentity(out_obj,apply=True)
        if in_divisions > 2:
            #linear smooth gives instant right topology, but deforms
            #the lines to be curved so we can't have nice domes
            #AND is actually (although by 5 to -1 milisecond on 6 divisions) slower than this method
            for i in range(3,in_divisions,1):
                nf = cmds.polyEvaluate(out_obj,face=True)
                cmds.polySmooth(out_obj, mth=0, dv=1, c=0, ch=False)
                nvtx = cmds.polyEvaluate(out_obj,vertex=True)
                cmds.select('%s.vtx[%s:%s]'%(out_obj, nvtx-nf, nvtx))
                mel.eval('DeleteVertex;')
                cmds.polyTriangulate(out_obj)
        li = MSelectionList()
        MGlobal.getSelectionListByName(out_obj, li)
        path = MDagPath()
        li.getDagPath(0, path)
        iter = MItMeshVertex(path)
        mesh = MFnMesh(path)
        while not iter.isDone():            
            #defaults to object space
            point = iter.position()
            mesh.setPoint( iter.index(), MPoint( MVector(point).normal()*in_radius ) )
            iter.next()
        cmds.select(out_obj)
        return out_obj

w = cmds.window(title='GeoSphere Creator')
cmds.columnLayout()
cmds.rowLayout(nc=2)
ds = cmds.intSliderGrp(label='Divisions', field=True, fieldMinValue=1, minValue=1, maxValue=10, value=4, cw3=[42,42,136], width=220)
rs = cmds.floatSliderGrp(label='Radius', field=True, fieldMinValue=0.001, minValue=0.001, value=1.0, cw3=[42,42,136], width=220)
cmds.setParent('..')
cmds.rowLayout(nc=2)
cmds.button('Confirm', w=220, c='makegeosphere( cmds.intSliderGrp("%s",q=True,v=True), cmds.floatSliderGrp("%s",q=True,v=True) ); cmds.deleteUI("%s")'%(ds, rs, w))
cmds.button('Cancel', w=220, c='cmds.deleteUI("%s")'%w)
cmds.showWindow(w)

It basically creates a platonic solid, smoothes it and then sets every vertex at the given radius from the origin again.

It uses exponential smooth, which can be set to actually only subdivide but it also adds points in the center of every face. Hence we have to delete this points and triangulate the mesh making it more difficult than linear smooth, but that just messes things up and adds minor deformation to the end result.

Maya API in Visual Studio 2010

Maya API doesn’t compile with dot net framework 4.0, as it defines MStatus as MS, which is a namespace in .NET 4.0

Hence create a new project, select Visual C++, at the top select .Net Framework3.5 and make an Empty Project.

Then per super quick test you can add the following files:

stdafx.h
#include
#include <maya/MString.h>
#include <maya/MArgList.h>
#include <maya/MFnPlugin.h>
#include <maya/MPxCommand.h>
#include <maya/MIOStream.h>

main.h

#include "stdafx.h"


class hello : public MPxCommand
{
public:
	hello(){ }
	virtual ~hello(){ }
	static void* creator(){ return new hello; }
	bool isUndoable() const{ return false; }

	MStatus doIt( const MArgList& args );
};

main.cpp

#include "stdafx.h"
#include "main.h"


MStatus hello::doIt( const MArgList& args )
{
	displayInfo("Hello "+args.asString(0)+"\n");
	return MS::kSuccess;
}


MStatus initializePlugin( MObject obj ) {
	MFnPlugin plugin( obj, "Autodesk", "1.0", "Any" );
	plugin.registerCommand( "hello", hello::creator );
	cout << "initializePlugin()\n";
	return MS::kSuccess;
}


MStatus uninitializePlugin( MObject obj ) {
	MFnPlugin plugin( obj );
	plugin.deregisterCommand( "hello" );
	cout << "uninitializePlugin()\n";
	return MS::kSuccess;
}

I use precompiled headers for all external stuff and all finished parts (so I can have multiple nodes and commands in one plugin and work on them one by one), you don't have to and in that case can dump the contents of stdafx.h into main.h and leave it out.

Now let's go to the project settings: Select the project you just created in the solution explorer and hit ALT + F7
Note that you select the Project and not the Solution, or the wrong options will appear (being you miss about every category that I'm going to mention next).

In the options set Configuration (at the top) to All Configurations. If you are developing for Maya 64 bit click the Configuration Manager... button, then in Active solution platform select . Most likely it will already be set to x64 and to copy from Win32. Do this, otherwise type x64 in the top box and set Copy settings from to Win32.

Now we're building for x64, so we can continue. Platform could be set to either Active or, preferably, all (in case you wish to change to x86 you already have the majority of the settings in place).

Then on the left go to Configuration Properties, expand it and hit General. There in General set Target Extension to .mll and set Configuration Type to Dynamic Library (.dll). This is the maya plugin format. The rest can remain as is.

So we have at least one .cpp (main.cpp) file, otherwise we do not get the C/C++ options. So do create these test files please. Then expand C/C++ and go to General under that category.

Additional Include Directories should be something along the lines of:
C:\Program Files\Autodesk\Maya2013\include;%(AdditionalIncludeDirectories)
You can click the text field, then the arrow appearing on the right, hit , in the window click the new folder icon, click the appeared '...' button and browse to your Maya installation (and version), select include and hit ok and again ok. Or you can just copy paste the text I just gave you.

Now under C/C++ go to Preprocessor and in Preprocessor Definitions paste:
WIN32;NDEBUG;_WINDOWS;NT_PLUGIN;REQUIRE_IOSTREAM;%\(PreprocessorDefinitions)
For Debug mode you may wish to explicitly define DEBUG instead of NDEBUG. Do not get confused by the WIN32, this also goes for 64-bit systems.

Then, also in C/C++, head to Precompiled Headers. If you want those that is, set it to Create (/Yc), if you use my test files the Header File should be stdafx.h and the Output File can remain default.

Now expand the Linker category, here in General somewhere halfway down you must set the Additional Library Directories. Same as the include, except it is named lib rather than include:
C:\Program Files\Autodesk\Maya2013\lib;%(AdditionalLibraryDirectories)

In Linker -> Input go to Additional Dependencies, you can edit this manually and separte each entry by enters or semicolons, or just paste this again:
Foundation.lib;
OpenMaya.lib;
OpenMayaUI.lib;
OpenMayaAnim.lib;
OpenMayaFX.lib;
OpenMayaRender.lib;
Image.lib;
opengl32.lib;
%(AdditionalDependencies)

This opens up the full maya functionality, most of the time the UI, FX and Render libs are not necessary.

When developing a plugin that is not using the simple plugin macro you need to define your own initialize and uninitialize to register all the commands and nodes. The docs will tell you that the simplecommand can only make plugins with one single command, which is so limited that I wonder why you would do something that simple in C++ rather than Python. So in Linker -> Command Line under additional options enter:
/export:initializePlugin /export:uninitializePlugin
This allows the functions to be exposed from the DLL and to be called by maya when loading the plugin. Which is necessary for the test files I gave above.

Now build and get angry if it doesn't work. Also post your errors so I can add the solutions in the relevant places above.

OpenMaya utilities

These are my findings when working with Maya attributes through the python API.

When creating a dag node from an MObject it uses the first one it encounters, which may appear random. Therefore using an MDagPath is better.

When selecting by name (MGlobal.getSelectionListByName) it will return an error if the node is not unique, but a path may be specified. cmds.ls(node_name,l=True) may help in finding the full path.

def getNodeFromName(in_name):
    selector = MSelectionList()
    MGlobal.getSelectionListByName(in_name, selector)
    node = MObject()
    selector.getDependNode(0, node)
    return node


def getDependNodeFromName(in_name):
    return MFnDependencyNode(getNodeFromName(in_name))


def getDagPathFromName(in_name):
    selector = MSelectionList()
    MGlobal.getSelectionListByName(in_name,selector)
    path = MDagPath()
    selector.getDagPath(0, path)
    return path

Attributes contain all information except the value, for this you need the plug. The plug has methods to return the value in the right type, but the plug itself has no clue whatsoever it’s own value consists of.

Hence you need to get the plug’s attribute, from the attribute (MObject) find out the matching function set, which means try everything, to find out the actual type of data. Then with most function sets, you need to find a type, as for example MFnNumericData contains a whole range of numeric data types in its unitType. Not all of these types are covered by the plug and some (like double, float, int) are immediately readable by python and some (like MDistance, MAngle) are not usable straight away.

Also, plugs may be array or compound, containing several child plugs, meaning they can be subdivided and each entry can in theory be a different type of data. Hence a big helper:

def findMPlug(in_node, in_attribute):
    '''
    @param in_node_name: string, unique name of the node,
    meaning the full path if multiple nodes of this name exist
    @param in_attribute_name: string, attribute to find,
    should exist or you'll get errors
    '''
    node = getNodeFromName(in_node)
    return MPlug(node, MFnDependencyNode(node).attribute(in_attribute))


def getPlugValue(in_plug):
    '''
    @param in_plug: MPlug, to get value from
    '''
    plugs = []
    if in_plug.isCompound():
        for i in in_plug.numChildren():
            plugs.append( in_plug.child(i) )
    elif in_plug.isArray():
        for i in in_plug.numElements():
            plugs.append( in_plug.getElementByPhysicalIndex(i) )
    else:
        plugs.append(in_plug)
    
    out = [] #compound list of all data in the plug or its child plugs
    for plug in plugs:
        attr = plug.attribute()
        if attr.hasFn(MFn.kNumericAttribute):
            type = MFnNumericAttribute(attr).unitType()
            if type in (MFnNumericData.kBoolean, MFnNumericData.kByte):
                out.append(plug.asBool())
            elif type == MFnNumericData.kChar:
                out.append(plug.asChar())
            elif type == MFnNumericData.kShort:
                out.append(plug.asShort())
            elif type in (MFnNumericData.kInt, MFnNumericData.kLong):
                out.append(plug.asInt())
            elif type == MFnNumericData.kFloat:
                out.append(plug.asFloat())
            elif type == MFnNumericData.kDouble:
                out.append(plug.asDouble())
        elif attr.hasFn(MFn.kUnitAttribute):
            type = MFnUnitAttribute(attr).unitType()
            if type == MFnUnitAttribute.kAngle:
                out.append(plug.asMAngle())
            elif type == MFnUnitAttribute.kDistance:
                out.append(plug.asMDistance())
            elif type == MFnUnitAttribute.kTime:
                out.append(plug.asMTime())
        elif attr.hasFn(MFn.kTypedAttribute):
            type = MFnTypedAttribute(attr).attrType()
            if type == MFnData.kString:
                out.append(plug.asString())
        else:
            #last resort for unimplemented data types
            out.append(plug.asMObject())
    return out

Also without needing plugs, as I tried that first before finding out plugs contained the data not attributes; but it may be useful anyway to turn a node’s attribute into… an attribute rather than an MObject.

def getAttrFn(in_attrobj):
    '''
    @param in_attrobj: MObject that has the MFnAttribute functionset
    '''
    if in_attrobj.hasFn(MFn.kCompoundAttribute):
        return MFnCompoundAttribute
    elif in_attrobj.hasFn(MFn.kEnumAttribute):
        return MFnEnumAttribute
    elif in_attrobj.hasFn(MFn.kGenericAttribute):
        return MFnGenericAttribute
    elif in_attrobj.hasFn(MFn.kLightDataAttribute):
        return MFnLightDataAttribute
    elif in_attrobj.hasFn(MFn.kMatrixAttribute):
        return MFnMatrixAttribute
    elif in_attrobj.hasFn(MFn.kMessageAttribute):
        return MFnMessageAttribute
    elif in_attrobj.hasFn(MFn.kNumericAttribute):
        return MFnNumericAttribute
    elif in_attrobj.hasFn(MFn.kTypedAttribute):
        return MFnTypedAttribute
    elif in_attrobj.hasFn(MFn.kUnitAttribute):
        return MFnUnitAttribute
    return MFnAttribute


def assignMFnAttribute(in_node_name, in_attribute_name):
    '''
    @param in_node_name: string, unique name of the node,
    meaning the full path if multiple nodes of this name exist
    @param in_attribute_name: attribute to find, should exist
    or you'll get errors
    '''
    attr = getDependNodeFromName(in_node_name).attribute(in_attribute_name)
    return getAttrFn(attr)(attr)

Wow when describing how hard it is compared to just… cmds.getAttr it actually seems really ridiculous. I guess the API really isn’t build to be used like a user, but only to add new nodes and functions – but even then, functions that require rather than set attribute data are a hassle. Difference being that these functions know what attributes and what types of nodes they’re made for, rather than being completely independent like getAttr.

Getting enum option strings

Something rather difficult is to reverse engineer the strings of an enum, as it is impossible using normal commands. Here’s a function using the Python API that gives a list of enum string options for use in any visual representations and interface elements that allow attribute editing.

from maya.OpenMaya import *


def getEnumOptions(in_node, in_attr):
    list = MSelectionList()
    MGlobal.getSelectionListByName(in_node, list)
    node = MObject()
    list.getDependNode(0, node)
    node = MFnDependencyNode(node)
    attr = MFnEnumAttribute( node.attribute(in_attr) )
    #min/max maybe can be get using getattr
    util = MScriptUtil()
    ptr = util.asShortPtr()
    attr.getMin(ptr)
    min = util.getShort(ptr)
    attr.getMax(ptr)
    max = util.getShort(ptr)
    enumoptions = []
    for i in range(min, max, 1):
        try:
            enumoptions.append( attr.fieldName(i) )
        except:
            #enums can be all over the place
            #i.e. node behaviour having 5 options but a max of 10
            pass
    return enumoptions

Asset Manager

Although flawed I’m still posting this Asset Manager, allowing you to export selected assets and list them (with screenshots) in a file browser that also allows you to import and instance them.

Future plans are referencing and instancing by callback so that any imported item, no matter if duplicated and how, they always relate to the source file. This way updating the source file updates the asset in any other files resulting in good iterability in a pipeline without requiring custom mesh nodes or any other systems – which may also be a cool thing to write (a custom mesh node that loads and caches its info from a file source). But I’m raving here, enjoy.

AssetManager with PyQt4 installer

AssetManager only

Follow the nfo.txt to install and open the AssetManager in Maya. Also you will install PyQt4 (if you don’t have it already), with the installer kindly provided by Nathan Horne

In tools.assetmanager.main.py on line 103 you may wish to include your own import settings (in case it fails to import, or you wish to reference instead of import), ask your local tech artist to help you copying the import MEL script from your scripteditor after importing something and inserting it into the maya.mel.eval statement right here (so it can stay MEL). You do need to keep the “%s” instead of any filename at the end.

maya.mel.eval( ‘file -i -options “v=0;” -dns -pr -loadReferenceDepth “all” “%s”‘%item.data(6).toString() )