Script Distribution using Maya Modules

 

 

 

Modules_Demo3.gif

Hi everyone!

As a TD, part of your job is to be able to create and distribute tools to the artists on your team. Sometimes, those artists may not be as tech-savvy as your average TD, and so part of the challenge is coming up with an easy way to distribute scripts and keep those scripts updated as you make changes to them.

Enter the magical ancient world of Maya’s module workflow.

In short, Maya’s module system allows scripts to be held in folders outside of the \user\maya\scripts folder. The magic of that is that you can thus put those scripts in a folder that’s shared over a network or cloud drive, which would allow for everyone on the team to be on the same page.

Here’s what you need to do:

Say you have a python script in the directory D:\sharedFolder\myScript.py

What you need to do is create a folder above that that will hold the entire module.

D:\sharedFolder\myModule\myScript.py

Then, you need to create a folder in there called “scripts” just like maya’s “scripts” folder and put in it all of the scripts you want to have shared. You could also do the same with plug-ins, icons, prefs… It’s essentially mirrors the \user\maya directory structure.

So! now you have D:\sharedFolder\myModule\scripts\myScript.py

And you’re almost set! Now what you need to do is in your \user\maya\modules folder, you need to create a type of file called a .mod file. This file will be used by maya to link the folder on our desktop to your maya’s path.

Just to get you started, the most basic syntax for a mod file is:

+ [module name] [version number] [full path to module folder]

for example:

+ myModule 1.0 D:\sharedFolder\myModule

Now, if you restart Maya and try to import myScript, everything should work as expected.

This kind of workflow, however, isn’t very animator friendly. That’s why I created a much quicker install method for installing a module.

  1. Download the module or add the shared folder to you shared drive.
  2. Drag a script called “drag_into_maya.py” into Maya and simply browse to confirm the module file’s location.
  3. Restart Maya and you’re done.

All I’m doing in that “drag_into_maya” python file is opening up a file dialog at the location of that python file, which should already be in the module package. I then write a .mod file into the “userAppDir” path ‘s module folder.

That’s it! Once you start looking into this, you’ll realize tools like the BCS and SHAPES are already being distributed via modules. I never really thought twice about modules before but this is honestly a game changer for my workflow. Thanks Rik, my coworker on a freelance project I’m on right now, for introducing me to this 🙂

Ben

More resources

Image

Vector-Based Wheel Rigging

Hey everyone!

First of all, I’d like to start with announcing I’ll be a Character TD intern this summer at Insomniac Games! I’m so excited for this opportunity!

Anyways, I’ve been working on a lot lately, partly for the Puppeteer Lounge and partly for just regular university stuff, but in my free time I’ve been chipping away at a car solution in Maya.

car1

Wheel Rotation

As a high school geometry refresher, in order to do a full 360 degree rotation, a wheel must travel a distance equal to Pi times its diameter, or C = Pi*D.  Traditional Maya wheel setups will use a simple set-driven-key approach to get things done. For example, for a wheel with diameter 2, you’d go Tau units (6.283) forward on the axis and have that drive a 360 degree rotation on the wheels. That can work for a basic setup, but you’ll still get a lot of wheel sliding since it’s only a one-dimensional driver.

A better way, I found, is to use a vector-based approach. I started with this youtube video as a starting point, but I found some problems with it, including confusing equations and variable names and also the fact that his technique for dot product doesn’t account for the wheel’s individual ry control. Either way, I knew I had to figure this out on my own to figure this out.

Vector Magnitude

The magnitude of a vector is equal to the square root of the sum its squared contents. In simpler words, it’s like the quadratic formula but for n dimensions. So magnitude is a scalar (number) that comes from a vector (“array”) and can be used as distance.

Therefore, at each frame, I knew I’d need to know the previous location and the current location as a vector. This was done using empty group nodes that I parented to a top level, “static” node (could be an XTR group in a rig). This transform node’s job is simply to store the previous frame’s location.

vector $wheel_old_vector = `xform -q -t -ws “wheel_cc_old_vector”`;

I then get the wheel’s current location:

vector $wheel_vector = `xform -q -t -ws “wheel_cc_pos_vector”`;

The current vector – previous vector gives us the movement vector from the previous to the current location, (I remember this as A-B gives B to A). The magnitude of this is the distance traveled.

vector $translate_vector = ($wheel_vector – $wheel_old_vector);

float $distance = mag($translate_vector);

At this point, we know the distance traveled and can calculate from that the rotation that we need to do ( 360 * distance / (Tau*radius) ). However, this doesn’t account for forward and backwards movement and for the lessened rotation that occurs when the direction of motion is oblique to the angle of the wheels.

Wheel Direction

I therefore added two other empty transform nodes to calculate the direction of the wheel. The first was just parented below the “move” control for the wheel and projected onto the zx plane, and the other is set as follows:

matchTransform wheel_cc_forward_vector wheel_cc_pos_vector;
xform -os -r -t 1 0 0 wheel_cc_forward_vector; 

So I’m matching the rotation and translation of the forward vector to the wheel vector and then going forward in object space from there. (I’m determining +as the forward direction here). From that, we get the vector:

vector $wheel_dir_vector = ($forward_vector – $wheel_vector);

This vector “points” in the direction of the wheel.

Vector dot products

Okay, I have to be honest. I had a pretty traditional Linear Algebra professor and I never really understood how dot products were related to cosine. I understood the dot product could give orthogonality and also if two vectors are pointing in similar directions, but cosine? What?

Well, you just have to normalize them, and it all makes sense.

v1 = [1,0,0]     v2 = [0,1,0]        << two normalized (magnitude 1) vectors

v1 dot v2 = 1*0 + 0*1 + 0*0 = 0 (these vectors are orthogonal)

Oh yeah, and acos(0) is pi/2 or 90 degrees!

Another example that made me get this:

v1 = [1,0,0]     v2 = [1, 1, 0]        << v2 is not normalized. Let’s normalize it!

v2 = (1 / √2 ) * v2 = [√2 / 2 , √2 / 2, 0]

v1 dot v2 = 1*√2 / 2 + 0*√2 / 2 n + 0*0 = √2 / 2

acos(√2 / 2) is pi/4 or 45 degrees!

Okay, so how are we going to use this for our wheels? Well, all we need to do is get the dot product of the normalized wheel and wheel direction vectors and we’ll know the cosine of the angle between them. If it’s negative, we know we need to rotate backwards. If it’s 0, we shouldn’t rotate at all because the car is skidding sideways. And then everything in between.

float $dot = dotProduct($translate_vector, $wheel_dir_vector,1);

We then use that in the final equation to determine if we need to rotate forwards or backwards.

wheel_cc_rz_expr_grp.rotateZ = wheel_cc_rz_expr_grp.rotateZ + (360 * (($distance * $dot) / (-6.283 * $radius)));

And that’s it! Or the bulk of it, at least. Here’s the rest of the mel code if you want to use this expression in your own setups.

car2

// Wheel expression
float $radius = 1.000000;
vector $wheel_old_vector = `xform -q -t -ws “wheel_cc_old_vector”`;
vector $wheel_vector = `xform -q -t -ws “wheel_cc_pos_vector”`;
matchTransform wheel_cc_forward_vector wheel_cc_pos_vector;
xform -os -r -t 1 0 0 wheel_cc_forward_vector;
vector $forward_vector = `xform -q -t -ws “wheel_cc_forward_vector”`;
vector $wheel_dir_vector = ($forward_vector – $wheel_vector);
vector $translate_vector = ($wheel_vector – $wheel_old_vector);
float $distance = mag($translate_vector);
float $dot = dotProduct($translate_vector,$wheel_dir_vector,1);
wheel_cc_rz_expr_grp.rotateZ = wheel_cc_rz_expr_grp.rotateZ + (360 * (($distance * $dot) / (-6.283 * $radius)));
xform -t ($wheel_vector.x) ($wheel_vector.y) ($wheel_vector.z) wheel_cc_old_vector;
if (frame == 1){
wheel_cc_rz_expr_grp.rotateZ = 0;
}

~ Ben

Image

Documenting Python Code with Sphinx

Well, it’s been a while.

While silent here, I’ve definitely been busy elsewhere. Follow me on github to see the work I’ve been doing with the Maya API and Py.Qt. Also find me on vimeo for demos of some of the stuff I’ve been working on.

Anyways though, I’ve been meaning to address my WordPress for some time now, so what better way than to document my newly-learned skill of automatically creating documentation from docstrings using Sphinx!

Check out the Docs I created for some of my GitHub projects as an example.

Install

Installing Sphinx is a pretty straightforward task. First of all, make sure Python is in the Path for your command prompt. If you type Python and hit enter and it gives you an error, you know it’s not. If it’s not, make sure to add the file path to the file that contains python.exe into your system’s Path environment variable.

In your favorite command prompt, navigate to the folder you have python installed on (probably something like C:\Users\Benjamin\Python\Scripts). If you open that folder in explorer, you should see an application there called easy_install. In your command prompt, while in the ~\Python\Scripts directory, run: easy_install sphinx

Python will do its thing and lots of new files will be created in the Sphinx directory (can you tell I don’t come from a computer science background yet?). You’re good to go!

Setting up a Sphinx Doc

Setting up a document in Sphinx is pretty easy too. Navigate to a folder in your command prompt and create a new folder (md folderName if you’re using the command prompt). Run the command sphinx-quickstart and it will give you lots of instructions to follow. Most of them are pretty self explanatory, but if you don’t know what some of them do just leave it at its default. You can go back and change them later.

C:\Users\Benjamin\Desktop>md sphinxTutorial

C:\Users\Benjamin\Desktop>cd sphinxTutorial

C:\Users\Benjamin\Desktop\sphinxTutorial>sphinx-quickstart
Welcome to the Sphinx 1.6.6 quickstart utility.

Some notable settings that we can set here that will save us some time later are saying y to the query:

> autodoc: automatically insert docstrings from modules (y/n) [n]: y

This will enable us to generate our documentation into html automatically using special “docstrings”.

Setting Up a Python File for Sphinx Documentation

Besides your regular #comments, docstrings are a special type of string that Sphinx can interpret and convert into professional-looking html. According to python.org, a docstring is “a string literal that occurs as the first statement in a module, function, class, or method definition.”

Here’s an example:

def pointInCone(meshDag, p0):
    “””Determines if/the factor by which a point is inside a polygon cone; i.e. a “Search Cone”

    :param meshDag: The dag path of a polyCone object
    :param p0: A world space vector representing the point to check
    :type meshDag: MDagPath
    :type p0: MVector
    :return: The factor by which the point is in the cone or False if it is not
    :rtype: float, False
    “””

# rest of function

It might look like a lot’s going on there, and there is some syntax to be aware of, so let’s break it down.

  • The triple quoted string must go directly after you declare a function. In PyCharm, if you make one it will automatically make the :param: and :return: bits for you.
  • The description for the function goes directly after the triple quote.
  • A blank line separates the description and the parameter descriptions.
  • :param param1: Description of parameter   (How to declare an argument)
  • :type param1: object type   (How to declare that argument’s type)
  • :return: What it returns 
  • :rtype: return type

Pretty self-explanatory, right? You could obviously go way more in depth with these so read up on the Sphinx docstring syntax if you want to learn more.

Besides your docstrings, you also need to make sure your Python file won’t do anything or throw any errors when it runs. Part of this (or at least my workaround) involves surrounding all your imports with Try/except ImportError statements, because many of the maya-specific modules I’m importing aren’t recognized by Sphinx when it goes to parse the file.

Here’s what the end result of the above code gives us in the end:

point in cone result

Setting Things up for Documentation

Okay! Now to actually make our documents. In your Sphinx project folder, find the python file called conf.py   and open it up. The first thing we need to do is make sure our python package is in Sphinx’s path, so add the line sys.path.insert(0, os.path.abspath(r”fullPathToPythonPackage”))

  • notice the raw string formatting we used with the r”” in front of the string

Next, if you forgot to do this in the setup, make sure you have “sphinx.ext.autodoc” in the extensions array in the conf.py file.

Automating the Documentation

Okay! So now we’re ready to do some documentation!  Create a new rst file called something like code.rst  … In it, give it a heading. This is the name that it will be given in our contents tree later.

example:

Code Docs
—————-

Notice the underline under the words. Think of it like an html heading. Different symbols like === or surrounding it on both the top and bottom with symbols will give it different weights in the end.

Finally, we’re going to do what Sphinx was meant to do: autodocs!

*A simple search cone algorithm using vector angles*

.. automodule:: bm_pointInCone
   :members:

 

  • In rst format, surrounding text with * with make it italics, and ** will make it bold.
  • After a blank line, we’re saying here that we want Sphinx to run the automodule script on the bm_pointInCone module. Remember when we gave it the full path to its package? That’s why.
  • Using the :members: tag will make documentation for any method in the module that contains docstrings.
  • rst file formats use 3 spaces for tabs instead of 4

 

Classes are pretty much the same; here’s a basic example:

.. autoclass:: bm_spaceSwitcher_ui.SpaceSwitcherUI
   :members:

   .. automethod:: __init__

 

  1. Running autoclass on a class on a module we imported
  2. Making docs for every method that has docstrings on it
  3. Forcing Sphinx to make a doc for the __init__ method using the .. automethod:: function.

Finally, if you want to include a block of code in your rst file (which will eventually become a webpage), you can use:

.. code-block:: python

   import maya.cmds as cmds
   import maya.OpenMayaUI as omui

Notice the blank line after declaring the code block.

 

Adding Docs to our Webpage

Now that we’ve made our rst file, go to your index.py file. You’ll find code that says

.. toctree::
   :maxdepth: 2
   :caption: Contents:

  • toctree stands for Table of Contents Tree
  • With a max depth higher than one, your contents will expand if it has anything underneath it.

Skip a line and add the name of the rst file we made with all our automodule:: stuff in it.

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   code

 

Creating the html webpage

It’s as simple as opening up your command prompt again while in the sphinx project directory (C:\Users\Benjamin\Desktop\sphinxTutorial) and running make html 

This will put your web files in the _build folder Sphinx has. Under _build/html you’ll find your index.html file. That’s the “homepage” file. Double click it and see what it looks like 🙂

Don’t like the look? Want it to be mobile-ready? Download and install the readTheDocs theme.

 

Publishing on GitHub

Okay, so now you want people to actually  be able to see your docs. GitHub has a nice Pages feature that allows you to create webpages for either your entire profile or for a single repo.

If you’re doing it for a single repo:

  1. Create a /docs folder
  2. Put the contents of the  _build/html folder in there.
  3. ***index.html must be in the root of the /docs folder for GitHub to build the site. ***
  4. Create a blank .nojekyll file in the /docs folder so that GitHub won’t ignore the folders that Sphinx made that start with an _
  5. Publish and wait! It sometimes takes around half an hour or so to see the result published. Go for a walk. You’ve been sitting for too long!

 

If you’re doing it for your username.github.io page:

  1. Put the contents of the _build/html folder in the root of your project repo. For user pages, GitHub requires the index.html file to be in the root of the repo.
  2. Create a blank .nojekyll file in the root of the repo so that GitHub won’t ignore the folders that Sphinx made that start with an _
  3. Publish and wait! It sometimes takes around half an hour or so to see the result published. Go for a walk. You’ve been sitting for too long!

 

That’s it! Again, check out my (work in progress) GitHub Page if you want to see what you can do with Sphinx.

Take care!

Ben

 

Image

Making a Picker GUI for Maya with PySide and QtDesigner

hoppsGui.JPG
The Hopps GUI in Maya 2016.5

The past few weeks have gone into the development of a new picker tool for a character rig I’m finishing up. The UI was designed in the amazing QtDesigner and all the Maya integration was done using PySide (PySide 1 that is).

QT Designer stuff:

The whole design is a QMainWindow widget, and the tabs come from the behavior of two QTabWidget objects.

  1. Do a png playblast of your character in the A or T pose you want them in. You want a nice obvious silhouette to make it easier to place the controls.
  2. Take that png into your ui file in QtDesigner and add it as the pixmap to a QLabel widget. This QLabel should be the backmost object in your design.
  3. In your python code (we’ll get to that later) you’ll have to re-connect the pixmap with the correct file path, but we will do this in QtDesigner so that we have a reference for placing our controls.
  4. Make sure you have “scaledContents” enabled.

Now it was just a matter of adding buttons.

  1. Add a button widget
  2. Give it an original objectName
  3. You can color it if you wish with the styleSheet property.
    1. background-color: rgb(80, 255, 73);\ncolor: rgb(0, 0, 0);
      1. ^^ that’s an example of the style sheet my green buttons shared. The background-color flag controls the color of the button and the color flag controls the color of the text.

The other widgets were pretty straightforward if you’ve ever done any PySide or PyQt coding. Besides QPushButton I used QLineEdit for the Namespace input and QComboBox for the dropdown menu.

qtDesigner

Integrating with Maya:

This was one of the hardest parts of the project to figure out. A whole lot of props goes to internet heroes like Brian Kortbus who are kind enough to post information and code relating to QtDesigner and PySide integration. Here’s the problem:

QtDesigner gives you a .ui file which Maya doesn’t know how to deal with. Maya DOES however have integration with PySide, which can take ui files and convert them into objects that Python can deal with. Basically, if you’ve ever hand-coded a UI in PySide, the following code allows you to take your nice ui you made in QtDesigner and make it into something that can be easily integrated with Maya (2016.5 or below):

import sys
import os
from PySide import QtCore, QtGui, QtUiTools
import shiboken
import maya.cmds as cmds
import maya.OpenMayaUI as MayaUI
"""
Code template for loading .ui files into Maya courtesy of Brian Kortbus
http://www.briankortbus.com/single-post/2016/11/09/UI-Files-in-Maya-2016-PySide—Part-2
"""
# Where is this script?
SCRIPT_LOC = os.path.split(__file__)[0]
def loadUiWidget(uifilename, parent=None):
"""Properly Loads and returns UI files – by BarryPye on stackOverflow"""
loader = QtUiTools.QUiLoader()
uifile = QtCore.QFile(uifilename)
uifile.open(QtCore.QFile.ReadOnly)
ui = loader.load(uifile, parent)
uifile.close()
return ui
# Call this function inside of Maya to run the script:
def runMayaTemplateUi():
"""Command within Maya to run this script"""
if not (cmds.window("templateUi", exists=True)):
TemplateUi()
else:
sys.stdout.write("Tool is already open!\n")
class TemplateUi(QtGui.QMainWindow):
"""A bare minimum UI class – showing a .ui file inside Maya 2016"""
def __init__(self):
mainUI = SCRIPT_LOC + "\\qtDesignerDoc.ui"
MayaMain = shiboken.wrapInstance(long(MayaUI.MQtUtil.mainWindow()), QtGui.QWidget)
super(TemplateUi, self).__init__(MayaMain)
# main window load / settings
self.MainWindowUI = loadUiWidget(mainUI, MayaMain)
self.MainWindowUI.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
self.MainWindowUI.destroyed.connect(self.onExitCode)
self.MainWindowUI.show()
self.makeConnections()
def makeConnections(self):
# the lambda: is (in my words) a handy way to pass an argument to our function without python running it.
self.MainWindowUI.addCube_btn.clicked.connect(lambda: self.someFunction("I'm a function!"))
def onExitCode(self):
"""Do this when the script is closed"""
sys.stdout.write("UI successfully closed\n")
def someFunction(self, someArg):
print someArg

So for example, if we had a QPushButton object in QtDesigner named addCube_btn, line 56 would then connect its “clicked” slot to the function “someFunction”. It’s really that easy for every other button, it just takes a lot of manual connection. The nice thing is that the names for my controls won’t change much between characters so the UI will be pretty re-usable.

I said earlier we had to do something with the pixmap on our QLabel widget. This is what I did:

self.MainWindowUI.bgdImage_label.setPixmap(QtGui.QPixmap(SCRIPT_LOC + "\\hoppsGuiBody.png"))

If I didn’t do this then the background image wouldn’t show up since the folder that QtDesigner makes the image point to by default isn’t at all what you want. I know there’s a whole resource system and all in QtDesigner but considering this project needed only one pixmap I didn’t really find it necessary to get that deep into learning how to do that. Maybe later.

Connecting Widgets:

Now, there are a few things to note with selecting controls. First of all, you can’t just pass it a control like “L_shoulderFK_CTRL” because you don’t know if the rig was referenced or not. That’s where the Namespace comes in. Instead of just directly selecting my control, I made the buttons call a function that concatenates the defined namespace onto the control. In other words: cmds.select(self.namespace + ctrl, add=1) where self.namespace is defined as self.MainWindowUI.namespaceTxt.text() in another function.

So that takes care of the selection issue. With that same code, we’re able to do things like select the entire body, reset the selection, key selection…

I think I posted this before but the code I use for a smart reset of an object’s attributes is:

sel = cmds.ls(sl=1)
for obj in sel:
    keyable = cmds.listAttr(obj, keyable=1, unlocked=1, settable=1)
    for attr in keyable:
        default = cmds.attributeQuery(attr, node=obj, listDefault=1)
        if cmds.getAttr(obj + "." + attr, settable=1) != 0:
            cmds.setAttr(obj + "." + attr, default[0])

That’s it for this post. I’ll make another post where I talk about how I tackled the fun problems of pose flipping/mirroring and IK-FK and FK-IK snapping.

A thing to note is that this is all written in PySide1, and Maya 2017 and up uses the newer version PySide2. Unfortunately, just changing the input line doesn’t fix the integration (yes I tried it) but I have seen some posts about the problem that try to fix it. So yes, this rig is for Maya 2016.5.

~ Ben Morgan

 

Image

Geometry Controls using Nodes

Hi everyone!

This discovery comes directly from a Jason Schleifer video  and I really just wanted to pass it on. It’s a very simple method for creating controls that stick to geometry.

Screenshot (37)
The node layout for this setup

How it works:

  • We get our out mesh from the skin cluster and plug that into a mesh that’s the duplicate of that skinned mesh
  • However, doing that will give us double transforms. That’s where the Transform Geometry node will come in
  • We plug in the World Inverse Matrix to a Transform Geometry node, and get the skinned mesh’s out mesh from our skinned geo’s Shape
  • We then plug in the result of that Transform Geometry node into our new control’s shape.

Overall, it’s a pretty simple process that, so far, seems easy to implement. I made a quick script that does this for you. All you do is select the geometry on the skinned mesh you want to become the control and then give it a child.

import maya.cmds as cmds
import maya.mel as mel
def geoCtrl(name="geoCtrl_1", geo=[], child=None):
"""
:param name: The name for our geometry control.
:param geo: The list of our face selection. You can just pass it cmds.ls(sl=1)
:param child: The child that our control will manipulate.
:return: [ctrl, ctrlShape]
"""
if len(geo) == 0:
return
if not child:
return
# get the object's name
object = geo[0][0:geo[0].index(".")]
objectShape = cmds.pickWalk(object, d="down")[0]
cmds.select(cl=1)
# first duplicate our object
ctrl = cmds.duplicate(object, name=name)[0]
ctrlShape = cmds.pickWalk(ctrl, d="down")[0]
# now create a "transformGeometry" node. This will allow us to get rid of our double transforms
tGeo = cmds.createNode("transformGeometry", n=name + "_transformGEO_#")
# connect our objectShape's outMesh to the inputMesh of the transformGeometry node
cmds.connectAttr(objectShape + ".outMesh", tGeo + ".inputGeometry")
# connect our control's worldInverseMatrix to the transformGeometry's transform input
cmds.connectAttr(ctrl + ".worldInverseMatrix[0]", tGeo + ".transform")
# now connect the transformGeometry's outputGeometry to our ctrlShape's inputGeometry
cmds.connectAttr(tGeo + ".outputGeometry", ctrlShape + ".inMesh")
# select the original selection again but this time on our new mesh. Invert the selection and delete it
cmds.select(cl=1)
for sel in geo:
cmds.select(ctrl + sel[sel.index("."):], add=1)
mel.eval("invertSelection;")
cmds.delete()
cmds.select(cl=1)
# center our control's pivot based on it's new geometry
cmds.xform(ctrl, cp=1)
# unlock all of the keyable transform attributes in our new control
for attr in cmds.listAttr(ctrl, k=1):
cmds.setAttr(ctrl + ".%s" % attr, l=0)
# finally parent our control to the child we wanted
cmds.parentConstraint(ctrl, child, mo=1)
return [ctrl, ctrlShape]

view raw
bm_geoCtrl.py
hosted with ❤ by GitHub

Image

RigAssembly Script/UI Test

Hi all!

It’s been a while, but I’ve been very hard at work.

This UI and script is the culmination of a few months of work. It’s a UI for an autoRig script I’ve written.

Features:

  • Add any number of arms, legs, etc and connect them to a spine
  • Stretchy ribbon limbs
  • Elbow pinning
  • Smooth twist forearms
  • Noodle arms
  • Hybrid IK/FK ribbon spine
  • Reverse foot setup
  • IK/FK blend
  • FK arm/leg stretching
  • Ability to turn off stretching on limbs

Plus:

  • Automatic rig coloring
  • Ability to change rig control colors after creation
  • Auto-scaling of rig controls based on character’s height
  • Ability to minimize the amount of ribbons created (for game engine rigs)

Things I’m planning to add:

  • A functionality that will set up your rig’s hierarchy in such a way that unreal will accept it (i.e. all the joints are one hierarchy, the controls are in a totally separate group from the joints, etc. )This should be added very soon.
  • Facial rigging setup. I already have the script written for this, so I just have to implement it into the rig.
  • Ribbon tail module
Image

Simple PySide Maya Scene Cleanup Tool

 

cleanup ool

 

Hi all!

Today I’m going to be showing a new tool I wrote using the GUI API PySide for Maya. It’s a little confusing at first, but after a while I enjoyed the level of flexibility and design I was able to get out of it compared to Maya’s simple UI tools. The goal of this project was to learn how different widgets work in PySide and to get comfortable with the process of integrating it within Maya. I learned a lot! I think I’ll be writing all my future GUIs in PySide/QT.

The tool has options for deleting history, non-deformer history, and unused nodes. It also has a re-label tool that has a dropdown menu for different labels to choose from. I chose this method because it allows me to easily predict what the pre/suffixes are that I need to replace out of the string before I append the new suffix on.

The renamer tool is pretty basic but it works. You can even press enter to run its command.

The next project I’d want to do is to figure out how to do some sort of Picker Window-type tool in PySide that’s similar to the AnimSchool picker. After some research, it seems like it’s going to be pretty complex, so I’ll need to start small.

-Ben

# our standard imports when using pySide
import maya.OpenMayaUI as omui
import maya.cmds as cmds
import maya.mel as mel
from PySide import QtCore
from PySide import QtGui
from shiboken import wrapInstance
# convenience function to get the main window. Part of the basic pySide Gui framework.
def maya_main_window():
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(main_window_ptr), QtGui.QWidget)
# Cleanup toolbox that includes renaming functions, relabeling functions, and History/Node Cleanup
class CleanupToolsUI(QtGui.QDialog):
def __init__(self, parent=maya_main_window()):
# init the QDialog class to that the child inherits its properties
super(CleanupToolsUI, self).__init__(parent)
# name the window
self.setWindowTitle("bm_cleanupTools")
# make the window a "tool" in Maya's eyes so that it stays on top when you click off
self.setWindowFlags(QtCore.Qt.Tool)
# Makes the object get deleted from memory, not just hidden, when it is closed.
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
# call our create_layout function here in the init
self.create_layout()
# establish our connections
self.create_connections()
def create_layout(self):
# place to put layout code
# formatting for labels
self.labelFont = QtGui.QFont()
self.labelFont.setBold(1)
self.labelFont.setPixelSize(12)
# History Deleting tools
self.delLbl = QtGui.QLabel()
self.delLbl.setFont(self.labelFont)
self.delLbl.setAlignment(QtCore.Qt.AlignVCenter)
self.delLbl.setText(" Delete:")
self.delLbl.setStyleSheet("QLabel {color: #b7b7b7;}")
self.delHistBtn = QtGui.QPushButton("History")
# change our button's text and background color
self.delHistBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
self.delHistBtn.setToolTip("Delete All By Type: History")
self.delNonDefHistBtn = QtGui.QPushButton("Non-Deformer History")
self.delNonDefHistBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
self.delNonDefHistBtn.setToolTip("Delete All By Type: Non-Deformer History")
self.delUnusedNodesBtn = QtGui.QPushButton("Unused Nodes")
self.delUnusedNodesBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
self.delUnusedNodesBtn.setToolTip("Delete Unused Nodes")
# relabel tools
self.relabelLbl = QtGui.QLabel()
self.relabelLbl.setFont(self.labelFont)
self.relabelLbl.setText("Relabel Tools: ")
self.relabelLbl.setStyleSheet("QLabel {color: #b7b7b7;}")
self.relabelBtn = QtGui.QPushButton("Relabel")
self.relabelBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
# radio buttons to determine whether the label is a prefix or suffix. It seems like they automatically
# act as a group.
self.suffixRadio = QtGui.QRadioButton("Suffix")
self.prefixRadio = QtGui.QRadioButton("Prefix")
self.prefixRadio.setChecked(1)
# a dropdown menu with common prefix/suffix choices
self.labelBox = QtGui.QComboBox()
self.labelBox.addItem("BIND")
self.labelBox.addItem("DRIVE")
self.labelBox.addItem("FK")
self.labelBox.addItem("GRP")
self.labelBox.addItem("GEO")
self.labelBox.addItem("IK")
self.labelBox.addItem("JNT")
self.labelBox.addItem("R")
self.labelBox.addItem("L")
# add a check box that determines if the label is replaced or appended
self.labelCheckBox = QtGui.QCheckBox("Append Label")
self.labelCheckBox.setChecked(0)
self.labelCheckBox.setToolTip("Tip: Append the new label instead of replacing the old one.\n"
" Ex: L_head_GEO")
# renamer tools
self.renamerLbl = QtGui.QLabel()
self.renamerLbl.setFont(self.labelFont)
self.renamerLbl.setText("Renamer Tools: ")
self.renamerLbl.setStyleSheet("QLabel {color: #b7b7b7;}")
# QLineEdit is a good object type for standard text input
self.renamerTxt = QtGui.QLineEdit()
self.renamerTxt.setPlaceholderText("Rename")
self.renamerTxt.setToolTip("Tip: Use # to insert a number inside the string.\n"
" – Supports up to ####")
self.renamerBtn = QtGui.QPushButton("Rename")
self.renamerBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
# selection tools
self.selectionTxt = QtGui.QLineEdit()
self.selectionTxt.setToolTip("Tip: Use * to select all objects containing that string.\n"
" Ex: *_GEO will select all nodes ending in _GEO")
self.selectionTxt.setPlaceholderText("Select")
self.selectionBtn = QtGui.QPushButton("Select")
self.selectionBtn.setStyleSheet("QPushButton {background-color: #828282; color: black;}")
# create a layout
main_layout = QtGui.QGridLayout()
# change our margins
main_layout.setContentsMargins(2, 2, 2, 2)
# fix margin spacing
main_layout.setSpacing(5)
main_layout.setColumnMinimumWidth(0, 50)
# add our items to our layout
# delete history tools
main_layout.addWidget(self.delLbl, 0, 0)
main_layout.addWidget(self.delHistBtn, 0, 1)
main_layout.addWidget(self.delNonDefHistBtn, 0, 2)
main_layout.addWidget(self.delUnusedNodesBtn, 0, 3)
# relabel tools
main_layout.addWidget(self.relabelBtn, 2, 3, 1, 1)
main_layout.addWidget(self.suffixRadio, 2, 1)
main_layout.addWidget(self.labelBox, 2, 2)
main_layout.addWidget(self.labelCheckBox, 3, 3)
# renamer tools
# have our text widget span across two columns (row,column,rowSpan,columnSpan)
main_layout.addWidget(self.renamerTxt, 4, 0, 1, 3)
main_layout.addWidget(self.renamerBtn, 4, 3)
main_layout.addWidget(self.selectionBtn, 5, 3, 1, 1)
main_layout.addWidget(self.selectionTxt, 5, 0, 1, 3)
# set column stretching
main_layout.setColumnStretch(0, 1)
main_layout.setColumnStretch(1, 1)
main_layout.setColumnStretch(2, 1)
main_layout.setColumnStretch(3, 1)
main_layout.setColumnStretch(4, 1)
# fit window to contents
self.resize(self.minimumSizeHint())
# set our layout
self.setLayout(main_layout)
def create_connections(self):
# establish our button connections
self.delHistBtn.clicked.connect(self.delHist)
self.delNonDefHistBtn.clicked.connect(self.delNonDefHist)
self.delUnusedNodesBtn.clicked.connect(self.delUnusedNodes)
# we can use the returnPressed attribute on our text to run a function
self.renamerTxt.returnPressed.connect(self.renamerTxtFctn)
self.renamerBtn.clicked.connect(self.renamerTxtFctn)
self.relabelBtn.clicked.connect(self.relabelFctn)
self.selectionBtn.clicked.connect(self.selectionTxtFctn)
self.selectionTxt.returnPressed.connect(self.selectionTxtFctn)
# our functions for the button conections. If we just put the cmd in the
# clicked.connect function, it would call it when we open the Ui
def delHist(self):
mel.eval("DeleteAllHistory")
print "Cleanup Helper — Delete All History"
def delNonDefHist(self):
mel.eval("BakeAllNonDefHistory")
print "Cleanup Helper — Delete All Non-Deformer History"
def delUnusedNodes(self):
mel.eval('hyperShadePanelMenuCommand("hyperShadePanel1","deleteUnusedNodes")')
print "Cleanup Helper — Delete Unused Nodes"
def renamerTxtFctn(self):
sel = cmds.ls(sl=1)
newName = self.renamerTxt.text().replace(" ", "_")
newName = newName.replace("'", "")
if "#" in newName:
j = newName.index("#")
k = newName.count("#")
newName = newName.split("#")
i = 1
if len(sel) > 0 and len(newName) > 0:
for obj in sel:
if j == 0:
# maya doesn't allow you to lead an object name with a number
cmds.rename(obj, newName[1:])
else:
if k == 1:
cmds.rename(obj, newName[0] + "%01d" % i + newName[1])
if k == 2:
cmds.rename(obj, newName[0] + "%02d" % i + newName[1])
if k == 3:
cmds.rename(obj, newName[0] + "%03d" % i + newName[1])
if k == 4:
cmds.rename(obj, newName[0] + "%04d" % i + newName[1])
i += 1
print "Cleanup Helper — Rename Object"
def relabelFctn(self):
sel = cmds.ls(sl=1)
newLabel = self.labelBox.currentText()
if self.suffixRadio.isChecked():
position = "suffix"
else:
position = "prefix"
# returns a list containing all of the labels in the labelBox
labels = [self.labelBox.itemText(i) for i in range(self.labelBox.count())]
if len(sel) > 0:
for obj in cmds.ls(sl=1):
objName = obj
# strip off any old labels
if self.labelCheckBox.isChecked() == 0:
for label in labels:
objName = objName.replace("_" + label, "")
objName = objName.replace(label + "_", "")
if position == "prefix":
cmds.rename(obj, newLabel + "_" + objName)
else:
cmds.rename(obj, objName + "_" + newLabel)
print "Cleanup Helper — Relabel Object"
def selectionTxtFctn(self):
sceneObjects = cmds.ls(type="transform")
selectionQuery = self.selectionTxt.text()
if "*" in selectionQuery:
if len(selectionQuery) == 1:
# if all the user entered was *, return
return
i = selectionQuery.index("*")
if i == 0:
for obj in sceneObjects:
if selectionQuery[1:1] in obj:
cmds.select(obj, add=1)
else:
for obj in sceneObjects:
if selectionQuery[0:i] in obj and selectionQuery[i + 1:2] in obj:
cmds.select(obj, add=1)
else:
cmds.select(selectionQuery, add=1)
def selectionTxtToolTipFctn(self):
QtGui.QToolTip.showText(self.selectionTxt.mapToGlobal(QtCore.QPoint()), "Test Tool Tip")
def showUI():
global ui
try:
ui.close()
except:
pass
ui = CleanupToolsUI()
# useful line of code that gets rid of lots of errors during testing
ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
ui.show()

 

Image

Nurbs Ribbon Script

ribbonScriptWeb

Hi everyone, hope you’re doing well.

I finished a new script that creates a ribbon using a Nurbs plane. It supports creation with any number of drive and bind joints. Very useful for rigging faces!

Also, now that I have this scripted in an object-oriented way, I can now use ribbons in my auto-rig scripts! What held me back for so long was the follicle creation part, which I found a method to do so here in MEL. The trick is to manually create Follicle nodes and make the connections yourself instead of relying on Mel.Eval for making hair systems (which doesn’t return anything to you).

– Ben

# bm_nurbsRibbon
import maya.cmds as cmds
# sets up a ribbon with j drive joints and k bind joints
class NurbsRibbon(object):
def __init__(self, numFollicles=6, numDrivers=3, name="nurbsRibbon"):
if numDrivers <= 1:
return
# Create our nurbs plane which will be the basis for our ribbon. We're using a nurbs plane and not a
# polyPlane because it has cleaner UV space and gives better normal results for the follicles.
self.ribbon = cmds.nurbsPlane(w=6, ax=(0, 1, 0), ch=0, u=6, v=1, lr=.1666666667, n=name + "_Ribbon")
self.ribbonShape = cmds.pickWalk(self.ribbon, d="down")
self.ribbon = [self.ribbon[0], self.ribbonShape[0]]
cmds.makeIdentity(self.ribbon, s=1)
cmds.delete(self.ribbon, constructionHistory=1)
cmds.select(self.ribbon[0])
# Now we manually setup a "hair" system by making follicle nodes and making the correct attachments.
self.ribbonFollicles = []
i = 0
while i < numFollicles:
follicle = cmds.createNode("follicle")
# a weird thing we have to do when making follicle nodes..
follicle = cmds.pickWalk(follicle, d="up")[0]
follicle = cmds.rename(follicle, name + "_follicle#")
follicleShape = cmds.pickWalk(follicle, d="down")[0]
self.ribbonFollicles.append([follicle, follicleShape])
cmds.connectAttr(self.ribbon[1] + ".local", follicleShape + ".inputSurface")
cmds.connectAttr(self.ribbon[1] + ".worldMatrix[0]", follicleShape + ".inputWorldMatrix")
cmds.connectAttr(follicleShape + ".outRotate", follicle + ".rotate")
cmds.connectAttr(follicleShape + ".outTranslate", follicle + ".translate")
# U value is between 0 and 1. Here we calculate its value based on the number of follicles the user wants
U = (1 / float(2 * numFollicles)) + (2 * (1 / float(2 * numFollicles)) * float(i))
cmds.setAttr(follicleShape + ".parameterU", U)
cmds.setAttr(follicleShape + ".parameterV", .5)
i += 1
# make the bind joints
self.bindJnts = []
i = 0
while i < numFollicles:
cmds.select(cl=1)
# same deal so that the Bind joints are in the same location as the follicles they're going to follow
U = (1 / float(2 * numFollicles)) + (2 * (1 / float(2 * numFollicles)) * float(i))
xLoc = 3 2 * (3 * float(U))
self.bindJnts.append(cmds.joint(p=[xLoc, 0, 0], n=name + "_" + "follicleJnt" + str(i + 1) + "_BIND"))
cmds.parent(self.bindJnts[i], self.ribbonFollicles[i][0])
i += 1
# make the drive joints
self.driveJnts = []
i = 0
while i < numDrivers:
cmds.select(cl=1)
self.driveJnts.append(cmds.joint(p=(0, 0, 0), radius=1.5, n=name + "_DriveJnt#"))
i += 1
# orient our joints and give them some SDK groups
i = 0
k = 6.0 / float(numDrivers 1)
self.driverOrientGrps = []
self.driverSDK1Grps = []
self.driverSDK2Grps = []
for jnt in self.driveJnts:
xLoc = 3 + i * k
sdk2 = cmds.group(jnt, n=jnt + "_SDK2")
sdk1 = cmds.group(sdk2, n=jnt + "_SDK1")
orient = cmds.group(sdk1, n=jnt + "_ORIENT")
self.driverSDK1Grps.append(sdk1)
self.driverSDK2Grps.append(sdk2)
cmds.xform(orient, t=(xLoc, 0, 0))
self.driverOrientGrps.append(orient)
i += 1
cmds.select(cl=1)
for obj in self.driveJnts: cmds.select(obj, add=1)
cmds.select(self.ribbon, add=1)
# bind the drive joints to the ribbon
ribbonCluster = cmds.skinCluster(dropoffRate=1.55, n=name + "_" + "ribbonCluster#")[0]
cmds.select(cl=1)
# edit our weights so the end drive joints stick to the end of the self.ribbon 1:1
cmds.skinPercent(ribbonCluster, self.ribbon[0] + ".cv[8][0:3]", transformValue=(self.driveJnts[1], 1))
cmds.skinPercent(ribbonCluster, self.ribbon[0] + ".cv[0][0:3]", transformValue=(self.driveJnts[0], 1))
# organize our ribbon
self.driveGrp = cmds.group(self.driverOrientGrps, n=name + "_" + "ribbonJnts_DRIVE")
cmds.select(cl=1)
for obj in self.ribbonFollicles: cmds.select(obj[0], add=1)
self.follicleGrp = cmds.group(n=name + "_" + "ribbon_FOLLICLES")
self.allGrp = cmds.group(self.driveGrp, self.follicleGrp, self.ribbon, n=name + "_ALL")
# our driveJnts group will be used to orient the ribbon as a whole
i = 0
while i < numFollicles:
cmds.scaleConstraint(self.driveJnts, self.ribbonFollicles[i][0], mo=1)
i += 1
# make a set for our BIND joints. This is really only to benefit the rigger, as the animator has no use for them
# Therefore, be sure to delete all these sets once you're ready to send the rig out.
cmds.select(cl=1)
for obj in self.bindJnts: cmds.select(obj, add=1)
cmds.sets(n=name + "_BIND_SET")
cmds.select(cl=1)
# finally, make our ribbon invisible when rendered
cmds.setAttr(self.ribbon[1] + ".primaryVisibility", 0)
cmds.setAttr(self.ribbon[1] + ".castsShadows", 0)
cmds.setAttr(self.ribbon[1] + ".receiveShadows", 0)
cmds.setAttr(self.ribbon[1] + ".visibleInReflections", 0)
cmds.setAttr(self.ribbon[1] + ".visibleInRefractions", 0)

view raw
bm_nurbsRibbon.py
hosted with ❤ by GitHub

Image

Change Shape Script

vr68sdg

Hi everyone, it’s been a while.

I’ve been hard at work on a game project that I’m helping out on and with other college-related activities. Something I find myself doing a lot is I changing rig control shapes, especially since at this point the auto-rig scripts I’m using are ones I wrote months ago that use some clunky shapes.

On a side note, I’ll be re-writing those auto-rig scripts this summer with a whole lot more attributes that I learned from all the rigging I’ve done this year. I’m trying to make my life as easy as possible.

So! Back to the script. This script intelligently gets the world size and position of the shape you want to replace by accessing the its BoundingBox scale and Center. Besides that, I’m doing everything pretty standard. I’m using a polyCube for my cube shape object, so I have to turn off Shading and all the render stats associated with polygons, but it’s pretty basic. The only thing an animator might get annoyed by is just hiding Curves in the viewport won’t hide all the control on the rig. But you can’t have everything (and use the attribute I give you for that, anyways!)

Here’s the script. I’m uploading my scripts on GitHubGist now because they read better and can be downloaded easier if you’d like.

Gist Link

import maya.cmds as cmds
# replaces the selected transform's shape with a new one
def changeShape(obj=None,type=None):
shapeTypes = ["cube", "circle", "square", "triangle"]
if type not in shapeTypes:
print "Shape type not recognized"
return
if not obj:
obj = cmds.ls(sl=1)
if cmds.objectType(obj) != "transform":
print "Error: Objects must be of type \"transform\" "
return
objShape = cmds.pickWalk(obj, d="down")
# get shape's bounding box for its world scale
# bounding box is ony for the SHAPE's bounding box.
# use shape's Center attribute (bounding box center) for new obj's translate
# use shape's boundingBoxSize attr for new obj's scale.
# *** this gives some really interesting behavior that could come in handy elsewhere!
# * the boundingBoxSize seems to be incorrect, but it is actually using
# the CVs as the full size and not the final approximated curve
# make our new shape
if type == "cube":
newObj = cmds.polyCube(n=obj[0] + "_newShape", ch=0)
cmds.connectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.connectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
cmds.disconnectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.disconnectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
# for a 3d shape change like a cube, it will be flattened to 0 on the Y axis unless we do something about that..
# Here we just set it to the same value of the sX value to keep it square, but if the object we're querying
# had an irregular shape, we'd have to manually make some changes here
# Also this assumes that the object has a 0 value in the Y axis, which might not always be the case.
# If the rotations have been brought on by the objet's rotate values, it will inherit those
if cmds.getAttr(newObj[0] + ".scaleX") < .0001 and cmds.getAttr(newObj[0] + ".scaleY") > .0001:
cmds.setAttr(newObj[0] + ".scaleX", cmds.getAttr(newObj[0] + ".scaleY"))
elif cmds.getAttr(newObj[0] + ".scaleX") < .0001 and cmds.getAttr(newObj[0] + ".scaleZ") > .0001:
cmds.setAttr(newObj[0] + ".scaleX", cmds.getAttr(newObj[0] + ".scaleZ"))
elif cmds.getAttr(newObj[0] + ".scaleY") < .0001 and cmds.getAttr(newObj[0] + ".scaleZ") > .0001:
cmds.setAttr(newObj[0] + ".scaleY", cmds.getAttr(newObj[0] + ".scaleZ"))
elif cmds.getAttr(newObj[0] + ".scaleY") < .0001 and cmds.getAttr(newObj[0] + ".scaleX") > .0001:
cmds.setAttr(newObj[0] + ".scaleY", cmds.getAttr(newObj[0] + ".scaleX"))
elif cmds.getAttr(newObj[0] + ".scaleZ") < .0001 and cmds.getAttr(newObj[0] + ".scaleX") > .0001:
cmds.setAttr(newObj[0] + ".scaleZ", cmds.getAttr(newObj[0] + ".scaleX"))
elif cmds.getAttr(newObj[0] + ".scaleZ") < .0001 and cmds.getAttr(newObj[0] + ".scaleY") > .0001:
cmds.setAttr(newObj[0] + ".scaleZ", cmds.getAttr(newObj[0] + ".scaleY"))
# make the cube not renderable
newObjShape = cmds.pickWalk(newObj,d="down")[0]
cmds.setAttr(newObjShape + ".overrideEnabled",1)
cmds.setAttr(newObjShape + ".overrideShading",0)
cmds.setAttr(newObjShape + ".castsShadows",0)
cmds.setAttr(newObjShape + ".receiveShadows",0)
cmds.setAttr(newObjShape + ".primaryVisibility",0)
elif type == "circle":
newObj = cmds.circle(n=obj[0] + "_newShape", ch=0, nr=(0, 1, 0))
cmds.connectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.connectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
cmds.disconnectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.disconnectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
# it ends up doubling the radius we want so let's fix that
cmds.setAttr(newObj[0] + ".scaleX", cmds.getAttr(newObj[0] + ".scaleX") / 2)
cmds.setAttr(newObj[0] + ".scaleY", cmds.getAttr(newObj[0] + ".scaleY") / 2)
cmds.setAttr(newObj[0] + ".scaleZ", cmds.getAttr(newObj[0] + ".scaleZ") / 2)
elif type == "square":
newObj = cmds.circle(n=obj[0] + "_newShape", ch=0, nr=(0, 1, 0), degree=1, sections=4)
cmds.xform(newObj, ro=(0, 45, 0))
cmds.connectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.connectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
cmds.disconnectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.disconnectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
# it ends up doubling the radius we want so let's fix that
cmds.setAttr(newObj[0] + ".scaleX", cmds.getAttr(newObj[0] + ".scaleX") / 2)
cmds.setAttr(newObj[0] + ".scaleY", cmds.getAttr(newObj[0] + ".scaleY") / 2)
cmds.setAttr(newObj[0] + ".scaleZ", cmds.getAttr(newObj[0] + ".scaleZ") / 2)
elif type == "triangle":
newObj = cmds.circle(n=obj[0] + "_newShape", ch=0, nr=(0, 1, 0), degree=1, sections=3)
cmds.xform(newObj, ro=(0, 180, 0))
cmds.connectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.connectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
cmds.disconnectAttr(objShape[0] + ".center", newObj[0] + ".translate")
cmds.disconnectAttr(objShape[0] + ".boundingBox.boundingBoxSize", newObj[0] + ".scale")
# it ends up doubling the radius we want so let's fix that
cmds.setAttr(newObj[0] + ".scaleX", cmds.getAttr(newObj[0] + ".scaleX") / 2)
cmds.setAttr(newObj[0] + ".scaleY", cmds.getAttr(newObj[0] + ".scaleY") / 2)
cmds.setAttr(newObj[0] + ".scaleZ", cmds.getAttr(newObj[0] + ".scaleZ") / 2)
# finally, we can do the parenting
newObjShape = cmds.pickWalk(newObj, d="down")
# freeze the new object's transformations
cmds.makeIdentity(newObj, apply=1, t=1, r=1, s=1)
cmds.parent(newObjShape, obj, r=1, s=1)
# finally, delete the old shape and our newObj's transform node
cmds.delete(objShape, newObj)

view raw
bm_changeShape.py
hosted with ❤ by GitHub

https://gist.github.com/benmorgan28/2cac4f59929702286cbf043bf854016c/71920b7257a57d45faedbf5d6b5c0bcc4f9ad6d3

Image

Rendering a Head in Arnold

Responsible for all parts

As some of you may know, alongside Technical Art, I’m studying 3D modeling at San Jose State University. This double-focus has really benefited my rigging work, especially in artistic and anatomical ways. And as always, I always find my technical art knowledge coming in handy as I model.

I made a couple of discoveries in this project, most notably on how to quickly make some realistic hair. I knew about Arnold’s ability to render curves from doing rigging research, and I knew about its nice hair shader, so I set out on trying to figure out how I could make hair using that technique.

import maya.cmds as cmds
# convert hairs to renderable curves
for obj in cmds.ls(sl=1):
s = cmds.pickWalk(obj,d="down")

cmds.setAttr(s[0] + ".aiRenderCurve",1)
cmds.setAttr(s[0] + ".aiCurveWidth",.02)
cmds.connectAttr("eyebrow_shader.outColor",s[0] + ".aiCurveShader")
cmds.setAttr(s[0] + ".aiOpaque",0)

So there are a couple of attributes you have to keep in mind. Under the shape node for each curve, you have to enable it to be rendered by Arnold. You can adjust the render width of it, assign it a shader, and disable “opaque” so that it can have opacity. I wrote this quick for loop to allow me to do this to the hundreds of hairs all at once.

The only thing I don’t know how to do yet it taper and opacity fade. For now, however, this is better than anything I’ve been able to wrestle out of nHair or xGen…

The eyes have a few tricks in them too. They’re just using standard Arnold shaders, but the Cornea has Opaque disabled and its opacity (found under Refraction) turned all the way down. A high spec weight and falloff allows it to have that nice wet reflection. I drew a curve on the eyeballs (as a live surface) and used a similar technique to how I rendered the hair to give the really nice “wetness” border between the eyeball and the eyelid. I’m not sure if there’s a medical name for it, but it’s that tiny line of water… Everyone has it and it’s a really good detail to add to eyes, and with Arnold’s render curve feature it was incredibly simple to do.

The head uses a 3 layer Shallow/Mid/Deep Subsurface Scattering setup. The thing I found is the SSS will REALLY soften your features up, so if you want any hard painted attributes on your face you’re going to have to have it be really high contrast.

Besides that, good topology, lighting (I used a 3 point area light setup with an HDR sphere around it all) really helped me. I’m really excited to see where this project goes!

– Ben

Image