[PySide Qt] :: Builder Window Story #1

Modular Rig Build

Building tools is always fun because of how much over-lap of tools are needed to support the main tool. I figured that it’s time to redesign my current builder with a new one hence this project is born.

When I design a complex window, I find that it’s always a good practice to break up the widgets into separate functions. In this case, my window only contains two widgets, ModuleForm and InformationForm, and PySide makes things easier in combining modules:

class MainWindow(QtWidgets.QMainWindow):
    HEIGHT = 400
    WIDTH = 400
    INFORMATION = {}

    module_form = None
    information_form = None
    # the main build blue-print to construct
    # every time a module is added to module form, this updates the blueprint dictionary

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        # add the widgets to the layouts.
        self.main_widget = QtWidgets.QWidget(self)
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        self.main_layout = QtWidgets.QHBoxLayout(self)

        # add the two widgets to the main layout
        horizontal_split = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
        self.main_layout.addWidget(horizontal_split)

        self.module_form = ModuleForm(parent=self)
        self.information_form = InformationForm(parent=self)

        horizontal_split.addWidget(self.module_form)
        horizontal_split.addWidget(self.information_form)

As you can guess the ModuleForm are where each rig module class is stored; and when that is selected, it triggers an InformationForm refresh, showing the necessary information for that module, and it works like so:

PySide Widgets' Relationship
PySide Relationships

This project is still in its infancy, I still need to add a blueprint file feature that saves and loads the module configuration as per rig specifications. My main purpose of this project is so that I can build creature rigs cleanly — including creature face work, I find that upkeep for creating faces is high, so having a modular based builder keeps file scenes nice and tidy.

I am not worried about the aesthetics of the tool for now, just its modular utility:

Modular Rig Build
Each module is responsible for each piece of rig.

As I can see, PySide offers much flexibility with UI tool design, for example, I found out that you can add separate widgets to a QListWidgetItem like so:

    @add_module_decorator
    def add_module(self, *args):
        """
        adds the module
        :param args:
        :return:
        """
        module_name = args[0]
        item = QtWidgets.QListWidgetItem()
        widget = ModuleWidget(module_name=module_name, list_widget=self.module_form.list, item=item, parent=self)
        item.setSizeHint(widget.sizeHint())

        # add a widget to the list
        self.module_form.list.addItem(item)
        self.module_form.list.setItemWidget(item, widget)
        return widget

And that QListWidgetItem contains a Widget with QLabel attached with a colored QtGui.QPixmap which can be changed just by re-assigning a different QtGui.QPixmap by using these two lines of code:

    def change_status(self, color="green"):
        """
        Change the status of the widget to "built"
        """
        self.q_pix = QtGui.QPixmap(buttons[color])
        self.icon.setPixmap(self.q_pix)

[Maya Rigging] :: Blend-Shape Based Face Rig.

Today I will explain how a blend-shape based face rig works for autodesk Maya. Understand that blendShapes is an additive mesh shape deformer, that one after another shape gets activated and can be driven by a single controller with values from 0.0 to 1.0 to drive shapes: shape0 + shape0_5 + shape1_0.

I had trouble finding a face mesh to work with, so I headed over to AnimSchool and downloaded their Malcom Rig and extracted the head mesh for me to work on:

https://www.animschool.com/DownloadOffer.aspx

While the set-up of the controllers are rather simple, each controller had to have a maximum value of 1. In addition to that, here is work needs to be done in creating the shapes themselves. For this reason, I chose to play with OpenMaya::MFnBlendShape class to add, and remove shape targets.

To initialize a blend-shape node without targets (important step):

def create_blendshape(mesh_objects, name=""):
    """
    creates a new blendShape from the array of mesh objects provided
    :param mesh_objects: <tuple> array of mesh shapes.
    :param name: <str> name of the blendshape.
    :return: <OpenMayaAnim.MFnBlendShapeDeformer>
    """
    blend_fn = OpenMayaAnim.MFnBlendShapeDeformer()

    if isinstance(mesh_objects, (str, unicode)):
        mesh_obj = object_utils.get_m_obj(mesh_objects)
        blend_fn.create(mesh_obj, origin, normal_chain)

    elif len(mesh_objects) > 1 and isinstance(mesh_objects, (tuple, list)):
        mesh_obj_array = object_utils.get_m_obj_array(mesh_objects)
        blend_fn.create(mesh_obj_array, origin, normal_chain)
    else:
        raise ValueError("Could not create blendshape.")

    if name:
        object_utils.rename_node(blend_fn.object(), name)
    return blend_fn

Each blend-shape index starts from 5000 and ends at 6000, so to get indices we need to use OpenMaya.MIntArray(), please understand that we need to use MIntArray and not a list of integers because otherwise Maya will not accept those integers:

def get_weight_indices(blend_name=""):
    """
    get the weight indices from the blendShape name provided.
    :param blend_name: <str> the name of the blendShape node.
    :return: <OpenMaya.MIntArray>
    """
    blend_fn = get_deformer_fn(blend_name)
    int_array = OpenMaya.MIntArray()
    blend_fn.weightIndexList(int_array)
    return int_array

Now we can add shape targets like by using the following code, the objects are accepted from targets_array and Maya’s specified index:

def add_target(targets_array, blend_name="", weight=1.0, index=0):
    """
    adds a new target with the weight to this blend shape.
    Maya has a fail-safe to get the inputTargetItem from 6000-5000
    :param targets_array: <tuple> array of mesh shapes designated as targets.
    :param blend_name: <str> the blendShape node to add targets to.
    :param weight: <float> append this weight value to the target.
    :param index: <int> specify the index in which to add a target to the blend node.
    :return:
    """
    blend_fn = get_deformer_fn(blend_name)
    base_obj = get_base_object(blend_name)[0]
    if isinstance(targets_array, (str, unicode)):
        targets_array = targets_array,
    targets_array = object_utils.get_m_shape_obj_array(targets_array)
    length = targets_array.length()
    if not index:
        index = get_weight_indices(blend_fn.name()).length() + 1
    # step = 1.0 / length - 1
    for i in xrange(0, length):
        # weight_idx = (i * step) * 1000/1000.0
        blend_fn.addTarget(base_obj, index, targets_array[i], weight)
    return True

One after another we an add all the shapes by code in whatever order of targets we want, adding in-betweens from 0.0 -> 1.0. I always choose to go in steps of “5”-ves. (0.0, 0.25, 0.5, 0.75, 1.0). This is because don’t really need to go any more complicated that that.

Over the course of constructing the blend-shape based rig, you need to have two base meshes: one for deformation and the other for duplicating mesh objects for sculpting. I used abSymMesh for mesh mirroring because the tool is already there and I did not need to re-invent another one. All in all I think I’ve done a good job with my face:

Face rig video of my repurposes Malcom face rig.

The complete module I used in this construction can be found at my GitHub page: