A blog about technical art, particularly Maya, Python, and Unity. With lots of obscurantist references
We've Moved
The blog has been retired - it's up for legacy reasons, but these days I'm blogging at blog.theodox.com. All of the content from this site has been replicated there, and that's where all of the new content will be posted. The new feed is here . I'm experimenting with crossposting from the live site, but if you want to keep up to date use blog.theodox.com or just theodox.com
Sunday, June 12, 2016
The New Hotness
I've finally completed rolling over to to a new, self-hosted blog platform!
The process took a bit longer than I wanted, mostly because it web development remains a messy, iterative process - at least for me. Since I ended up unifying both the blog and the old Character Rigger's Cookbook as well as on old markdown wiki, I had to do a lot of little scripts to groom the old content into a consistent format and linking strategy. Add in more than a modicum of CSS noodling and whatnot and my 'couple of weekends' project turned into a couple of months.
However all that is behind me now, and all my future updates are going to be coming up at theodox.github.io (you can also use www.theodox.com). If you're subscribed to the current feed, you should switch over to either http://theodox.github.io/feeds/atom.xml or http://theodox.github.io/feeds/rss.xml, depending on your reader; one of the nice side effects of the switch is that the new feeds are much cleaner than Blogger's -- no more CSS gibberish instead of article summaries, thank you very much!)
I'm going to leave this site intact, and I'll try to keep monitoring comments and so on here, but the new site is going to be where all the news stuff comes out. I'm also going to change the blog.theodox.com redirect so it goes to the new site, so if you're using that in a blogroll you won't have to update it.
PS. I had to touch a lot of content during the migration: there are about 150 blog posts, several dozen wiki pages, and a bunch of articles that all had to be repointed and I'd be pretty surprised if nothing odd slipped through. Please let me know using the comments at the new site so I can fix up anything that's confusing or misleading.
So, here one more link to the new site, just in case. Hope to see you there!
Thursday, May 12, 2016
Shamless Job Plug
We're looking for one TA with good python chops to ride herd on the python/maya toolkit, and another TA with good shading and GPU skills to help us wring the most out of the Unreal Engine. Details of both positions are in the link above.
Contact me directly or apply via the Undead Labs website!
Thursday, April 28, 2016
minor minq manifestations
I've added a couple of minor features as well. There's a bunch of operators for counting things -- for example you can get the vertex count of meshes with something like
Meshes().get(VertCounts)
. On a somewhat related note all streams have a count()
function which will return the length of the stream, and a first()
method which will pull the head item from a stream -- which is handy if you expect to narrow down to a single item and don't want a single-item iterable.Last but not least I'd love to hear from the community about good minq hacks -- I'll be happy to add cool ones to the examples file. I'd also appreciate any bugs you find going into the issues page!
Sunday, April 3, 2016
Blogger blues
Wherein our author uses blogger to post a blog post blogging about how much he dislikes blogger.It's late on a Sunday night and I need to get this off my chest.
I really have come to loathe Blogger. The sluggish, overly complicated, JS heavy theme, the sluggish, too-complex-for-speed-but-too-simple-for-interesting-stuff editor, and the way it stuffs stylesheet info into the RSS feed come to mind. but overall... it's just gotten on my nerves.
So, I'm probably going to transition the blog over to something else. My current leading candidate for a site generator is Pelican, a Python based static html site generator which seems to be powerful enough for my not-too-complex needs. Jekyll is another candidate but all things being equal I'd rather stick with a Python-based setup and the final output will be pretty much the same.
I'm a tad nervous about what happens to old links and traffic so I assume that I'll probably transition over gradually with duplicate postings for a while. If any of you have done something similar in the past I'd be curious to hear about how it went.
In the meantime, I'll just add that I've been dealing with the transition in typical TA fashion. I hacked up a script to download all of the existing blog posts as XML, then used the html2text module on the cheese shop to convert the HTML from the posts into markdown text. I'm still going to have to hand-finish every pieces, cleaning up dead links and missing images and so on: I'm sure it'll be a TA-style spit'n'bailing-wire party for a while yet.
In the meantime I'm all ears if anybody has more suggestions for site generators, or a reason to go with something other than a static site on github.io, please let me know in the comments!
update: the new site is here
Saturday, March 26, 2016
mGui updates in the offing...
For those of you who’ve been using mGui to speed up and simplify your Maya gui coding, there are some interesting changes on the horizon. Although I’m not entirely ready to release the changes I have in mind they are mostly sitting in their own branch in the Github repo.
The upcoming version introduces some new idioms - in particular, it gets rid of the need for explicitly setting keys on new controls to get access to nested properties. In the first version of mGui you’d write something like this:
with gui.Window('window', title = 'fred') as example_window: with VerticalForm('main') as main: Text(None, label = "Items without vertex colors") lists.VerticalList('lister' ).Collection < bind() < bound with HorizontalStretchForm('buttons'): Button('refresh', l='Refresh') Button('close', l='Close')
With the new refactor that looks like this:
with gui.Window('window', title = 'fred') as example_window: with VerticalForm() as main: Text(label = "Items without vertex colors") lister = lists.VerticalList() lister.collection < bind() < bound with HorizontalStretchForm() as button_row: refresh = Button( label='Refresh') close = Button(label='Close')
The big advantage here is that those local variables are not scoped exclusively to the layout context managers where they live, which makes it easy to control when and where you hook up your event handlers: In the above example you could defer all the bindings and event handlers to the end of the script like this:
with gui.Window('window', title = 'fred') as example_window: with VerticalForm() as main: Text(label = "Items without vertex colors") lister = lists.VerticalList() with HorizontalStretchForm() as button_row: refresh = Button( label='Refresh') close = Button(label='Close') lister.collection < bind() < bound refresh.command += refresh_def close.command += close_def
So far I’m really liking the new idiom, particularly eliminating the extra quotes and redundant None
keys. However this is a minorly breaking change: in some cases, old code which relied on the key value to name and also label a control at the same time will when the keys become redundant. Moreover I bit the bullet and started to refactor the entire mGui module to use correct pep-8 naming conventions – in particular, member variables are no longer capitalized. So if you have code outside of mGui this will introduce some issues. When I converted my own code, most of the changes could be done with a regular expression but there were a few danglers.
I think the changes are worth the effort, but I’d be really interested in hearing from users before trying to bring the new mGui branch back into the main line. It should actually be possible to write a script that fixes most existing code automatically, that’s something we could refine collaboratively.Please let me know in the comments or by opening an issue on the GitHub site if you have comments or plans. As always, bug fixes and pull requests always entertained!
Sunday, March 20, 2016
Another Year, Another GDC...
It is, however, interesting to note how cynical we’ve become as an industry about tech bubbles: from MMOs to Facebook games to In-app purchases and 3-D TVs, we’ve all lived through so many Next Big Things that we habitually reserve judgement – even on things like the latest crop of VR gear which sets our little nerdly hearts a-fluttering. I had a lot of conversations with people on the general theme of “wow, that’s cool. In three years most of them will be out of business, though.” – even with people in the VR business itself.
Personally, I think VR is going to survive but I don’t think it’ll be the kind of world-changing, ubiquitous tech that the broadband internet and handhelds have turned out to be. It’s an awesome geek toy and the gateway to many interesting and novel experiences. I see it sort of like that kayak in the garage: a cool thing that gets dusted off a few times a year but not a regular part of daily life, and never going to buffed up to the high consumerist gloss of an iPhone. Maybe in another decade, but for now it’s a really cool niche product. I hope all that crazy money sloshing around in VR land fuels some general purpose innovation : in particular, I hope that VR’s need for screaming framerates in two renders at once may make hardware that is more performant overall and also for split-screen friendly deferred rendering.
In the actual, as opposed to the virtual, world the highlight of the show for me is always the TA roundtables. It’s such a great resource for the community, and a chance for folks who often live a bit outside the main stream of their development teams to get together with people who share their unique and esoteric pain. Those three roundtables are the only room full of people who will chuckle at jokes about MaxScript and who truly appreciate the hopeless pathos of sending emails about proper check-in procedures to a team of artists. As always, hats off to +Jeff Hanna for running the roundtables and also the TA Bootcamp, which I had to miss this year but sounded really good. I’m anxiously awaiting the release of the talks I missed on the GDC Vault.
In these annual roundups I usually try to note the size and intensity of the job market. This year was kind of difficult to gauge. The big mega-booths from mega-teams were largely absent this year: I didn’t see many of the long lines of students queuing up to show their portfolios to Blizzard, Bungie, or 343. On the other hand the general commercial tempo seemed pretty up beat. I think this reflects the general trend away from big, highly institutionalized teams and toward smaller, more agile (but also more fragile) groupings: the job market is more fragmented but not necessarily smaller than in years gone by. I could be wrong about that one, though – this might just be a mid-console-cycle lull.
To all the folks I saw down at the show – it was awesome! To those who didn’t make it this year – don’t miss it next time!
PS - one important thing that came out of the round table is that more and more people are signing up to the Tech artists slack channel. If you're not already a member, follow that link to sign up.
Sunday, March 6, 2016
A touch of minq
So, it’s with a little bit of trepidation that I’m sharing my latest library. Minq bills itself as ‘a query language for Maya scenes.’ The goal is to simplify a very common task for Maya coders: finding things in a scene.
Now, that isn’t a particularly interesting job most of the time, but it’s one we do a lot: a quick grep of my own codebase shows over 600 calls to
cmds.ls()
, cmds.listRelatives()
, cmds.listHistory
and cmds.nodeType()
in various combinations: as far as I can tell, ls()
is actually the single most common call I make. Moreover, I’m reasonably certain (though I didn’t do the grepping to bear this out) that those hundreds of
ls()
calls are accompanied by hundreds of little snippets of code to deal with Maya’s quirks. How often have you run into little gems like this?stuff = ['top', 'something_thats_not_transform'] print cmds.ls(*stuff, type='transform') # [u'top'] stuff = [] print cmds.ls(*stuff, type='transform') # [u'front', u'persp', u'side', u'top']
for item in cmds.ls(my_meshes, type='mesh'): print item # Error: 'NoneType' object is not iterable # Traceback (most recent call last): # File "<maya console>", line 1, in <module> # TypeError: 'NoneType' object is not iterable #
ls()
calls alone will produce at least 5 bugs.More importantly – and, frankly, the whole reason for this project – dealing with these little gotchas is not an interesting job. Finding, filter and sorting stuff in your Maya scene is not am opportunity for you to display your brilliant algorithms or clever strategies for bending Maya to your will: it’s just a bunch of stuff you have to on your way to fixing the problems your users really want fixed.
Minq in action
Hence, minq.The goal of minq is to provide a more concise and more readable way to find things in your maya scenes. Here’s an example to give you the idea of how the project is supposed to work.
Suppose you need to find all of your character skeletons and distinguish them from other things lying around in the scene. The easy way to do that is usually to look for assemblies (top level nodes) which have children who drive skinClusters. Here’s an example of how you could find all the root nodes in the scene which drive skins using conventional means:
def find_assemblies_that_drive_skins(): skinned = [] for asm in cmds.ls(assemblies=True) or []: children = cmds.listRelatives(asm, ad=True) or [] history = cmds.listHistory(children, future=True) if history and cmds.ls(history, type='skinCluster'): skinned.append(asm) return skinned
or []
to make sure we don’t get errors for failed queries. We have to create two temporary variables (childen
and history
) in order to store the intermediate results. And, obviously, we’re 3 layers deep when we get to the actual work item. Above all, though, you need to remember two little bits of Maya trivia to make sense of this code: that
cmds.ls(asm=True)
means ‘give me the assemblies’ and that listRelatives(ad=True)
gives you the children of an object. These are, of course, very clear to Maya vets – but there are over 50 flags in ls()
and more than a dozen in listRelatives()
. I’ve been working in Maya for 20 years and I still need to look up most of them. You pass those flags to Maya as strings which won’t get evaluated until runtime – and it’s possible to mistype them and not even know because ls()
, in particular, makes wierd tweaky decisions about how to interpret conflicting flags.Here’s the minq equivalent to the previous function:
def drives_skin(some_object): children = using(some_object).get(AllChildren) skin_clusters = children.get(Future).only(SkinClusters) return any(skin_clusters) unskinned_assemblies = Assemblies().where(drives_skin)
1.
drives_skin()
takes a maya object2. It gets all of that object’s children
3. It gets all of the future history of those children
4. It it filters down to only the skin clusters in that future history
5. it returns true if any skin clusters are present
The rest of it pretty self evident:
unskinned_assemblies
just collects all of the assemblies which pass drives_skin()
. The algorithm is exactly the same as the first version – but, at least to me, that algorithm is actually expressed much more clearly in the minq version. As for concision, I deliberately broke the query into two lines to make it easier to read -- otherwise it could all be done in a single expression.A purist will probably point out that there are important under-the-hood details in the first one that are hidden in the second, and s/he’d be right. However after doing a lot of this kind of code down the years I’m fairly certain that those important details have almost always been important because screwing them up causes problems – not because they provide an opportunity for a wizardly optimization or better approach to the problem. I’m interested in finding unskinned meshes, not in remembering to pass the correct flags to
ls
and listRelatives
.Here’s a couple of other examples to give you the flavor of what a minq query looks like:
# get all mesh transforms in a scene mesh_transforms = Meshes().get(Parents) # find stub joints def is_stub(obj): return not any (using(obj).get(Children).only(Transforms)) stubs = Joints().where(is_stub) # filtering by type, by name, and with functions cube_creator_nodes = PolyCreators().only('polyCube') used_to_be_cubes = cube_creator_nodes.get(Future).only(Meshes) has_8_verts = lambda p: cmds.polyEvaluate(p, v=True) == 8 still_are_cubes = used_to_be_cubes.where(has_8_verts) # adding, subtracting or intersecting queries too_high = Transforms().where(item.ty > 100) too_low = Transforms().where(item.ty < -100) middle_xforms = Transforms() - (too_high + too_low)
Saturday, March 5, 2016
Technical Art for Art Directors
Thursday, February 18, 2016
The little things
If you’ve ever spent any time wrestling with Maya distribution, you’ve probably noticed that
userSetup.py
executes in an odd fashion: it’s not a module that gets imported, it’s basically a series of statements that get executed when Maya fires up. Unfortunately that also means that most of the usual strategies you’d use in python to find out where, exactly, you are running from is problematic. The usual python tricks like __file__
don’t work; and most of the time asking for os.getcwd()
will point at your Maya program directory. Usually you end up running around looking at all the directories where Maya might be stashing a userSetup
and trying to figure out which one is the one you are in`. It’s ugly.However today, I actually found one which works. At least, I haven’t figured out how to break it yet.
import os, inspect USER_SETUP_PATH = os.path.dirname(inspect.currentframe().f_code.co_filename)
PS, if you’re wondering why I care: this makes it really easy to do a simple install/uninstall of a
userSetup.py
/ userSetup.zip
combo with no environment variables or special rules. PPS: Take that, Maya!
Saturday, February 6, 2016
The Memory Hole
Along the way, that attention to detail and mastery of nuance tends to make us think we know it all. But – a shock, I know – we don’t. More to the point, we might now it all for the moment. But we’ll dump that knowledge to make sure we have room for our encyclopedic knowledge of tomorrow’s problem. And next week’s. And next months.
In short, we’re constantly flushing our caches. Unless you’re stuck in a rut, doing the same thing every day, you’re constantly learning new little things for your current problem and silently shelving the knowledge you aquired for your last.
This is one reason why good code comments are so important. Sure, comments rot just like code. But a couple of well-placed notes about how and why the code looks the way it does can save future you a lot of time when many layers of memory recycling have left you completeley oblivious about what the hell past you was up to. I can easily think of a couple of embarrassing occasions where I’ve literally chased my own tail – done something non-obvious because of a wierd maya bug, then come back six month later to ‘clean up’ my ‘ugly code’ and of course hit the exact same bug again.
Of course, good comments don’t have to have high literary quality, they don’t need to cover every variable and for loop, and they certainly don’t need to be overwhelming: what they should be is notes to future-self that will help him or her revive all the fading memories which seem so obvious today but which will be utterly erased before the next season of Silicon Valley is released.
Which brings me, by a very roundabout route, to what I actually set out to talk about: a perfect case in point. I was noodling around with a system that needed to fire events for maya attribute changes: basically, a way to make
attributeChanged
scriptJobs that were easy to start, stop and restart. So I did a little googling and… Yep. I’d already written it. I’d even put it up on Github.
In my defense, I realized in retrospect that I had cancelled the project at work that made it necessary the first time: I did the work on the system, got it ready to go, and then decided that there was a simpler way to solve the problem without all those attribute-change scripts anyway. Nonetheless it’s a perfect illustration of how thoroughly one’s short-term memory cache gets flushed – and of the importance of leaving good comments. At least when I found the damn thing the readme that Github makes you put up reminded me how it was supposed to work (as an aside, it’s a great reason for putting your stuff up on GitHub or similar forums: knowing that other people will be looking at it forces you to clean up and document more than you would if you just decided to shelve a project).
So there you have it: an object lesson in the importance of clarity in tools development and a free module for messing around with AttributeChange scriptJobs!
Monday, February 1, 2016
Friday, January 29, 2016
Some fixups for sfx
Luckily +Kees Rijnen, the main author of shaderfx, noticed the blog post and helpfully pointed me at the source of the problem which I've included in a fix.
If you are using the original version of the code this may be a breaking change. To unify the way that individual nodes and groups are connected, I changed the connect() and disconnect() methods to take only two arguments where they previously took 4. In the first pass you would write
network.connect( node1, node1.outputs.xyz, node2, node2.inputs.rgb)which was needlessly wordy. So connect() and disconnect() now sport the cleaner, simpler syntax
network.connect(node1.outputs.xyz, node2.inputs.rgb)As always, comments and pulls are encouraged!
Wednesday, January 13, 2016
First module of the year!
All of shaderfx in maya is organized by a single, undocumented command. Which is pretty lame.
However, it’s not as bad as it seems once you figure out the standard command form, which is always some variant of this form:
shaderfx -sfxnode <shader node> -command <command> <node id>
sfxnode
argument tells maya which sfx shader to work on. The command
flag indiciates an action and the node id
specifies an node in the network. Nodes are assigned an id in order of creation, with the firstnode after the root ordinarily being number 2 and so on – however the ids are not recycled so a network which has been edited extensively can have what look like random ids and there is no guarantee that the nodes will form a neat, continuous order. Many commands take additional arguments as well. Those extra always follow the main command; thus
shaderfx -n "StingrayPBS1" -edit_int 19 "uiorder" 1;
uiorder
field on node 19 to a value of 1. The
shaderfx
command can also return a value: to query the uiorder
field in the example above you’d issue shaderfx -n "StingrayPBS1" -getPropertyValue 19 "uiorder"; // Result: 1 //
So, the good news is that the
shaderfx
command is actually pretty capable: so far, at least, I have not found anything I really needed to do that the command did not support. For some reason the help documentation on the mel command is pretty sparse but the python version of the help text is actually quite verbose and useful.Still, it’s kind of a wonky API: a single command for everything, and no way to really reason over a network as a whole. Worse, the different types of nodes are identified only by cryptic (and undocumented) numeric codes: for example a
Cosine
node is 20205 – but the only way to find that out is to use the getNodeTypeByClassName
command (and, by the way, the node type names are case and space sensitive).Cleanup crew
With all that baggage I was pretty discouraged about actually getting any work done using shaderfx programmatically. However a little poking around produced what I hope is a somewhat more logical API, which I’m sharing on github.The
sfx
module is a plain python module - you can drop it into whatever location you use to story your Maya python scripts. It exposes two main classes:SFXNetwork represents a single shader network – it is a wrapper around the Maya shader ball. The
SFXNetwork
contains an indexed list of all the nodes in the network and also exposes methods for adding, deleting, finding and connecting the nodes in the network.SFXNode represets a single node inside the network. It exposes the properties of the node so they can be accessed and edited using python dot-style syntax.
The module also includes to submodules,
sfxnodes
and pbsnodes
. These make it easier to work with the zillions of custom node ids: Instead of remembering that a Cosine
node is type 20205, you reference sfxnodes.Cosine
. I’ll be using the StingrayPBSNetwork
class and the pbsnodes
submodule in my examples, since most of my actual use-case involves the Stingray PBS shader. The syntax and usage, however, are the same for the vanilla SFXNetwork
and sfxnodes
– only the array of node types and their properties.Here’s a bit of the basic network functionality.
Create a network
To create a new shaderfx network, use thecreate
classmethod:from sfx import StingrayPBSNetwork import sfx.pbsnodes as pbsnodes network = StingrayPBSNetwork.create('new_shader')
Listing nodes
An SFXNetwork contains a dictionary of id and nodes in the fieldnodes
. This represents all of the graph nodes in the network. Note I’ve used a different shader than the default one in this example to make things easier to read.print network.nodes # { 1 : <sfxNode UnlitBase (1)>, 2: <sfxNode 'MaterialVariable' (2)> } print network.nodes[2]: # <sfxNode 'MaterialVariable' (2)>
show node IDs
toggle in the ShaderFX window.The values of the node dictionary are
SFXNode
objects.Adding new nodes
To add a node to the network use itsadd()
method and pass a class from either the sfxnodes
or pbsnodes
submodule to indicate the type. if_node = network.add(pbsnodes.If) # creates an If node and adds it to the network var_node = network.add(pbsnodes.MaterialVariable) # creates a MaterialVariable node and adds it to the network
Connecting nodes
Connecting nodes in shaderfx requires specifying the source node the source plug, the target node and the target plug. Unforunately the plugs are indentifited by zero-based index numbers: the only way to know them by default is to count the slots in the actual shaderfx UI. Output plugs are usually (not always) going to be index zero but the target plugs can be all over the map.To make this cleaner, each
SFXNode
object exposes two fields called inputs
and outputs
, which have named members for the available plugs. So to connect the ‘result’ output of the var_node
object to the input named ‘B’ on the if_node
:network.connect(var_node.outputs.result, if_node.inputs.b)
MayaCommandError
will be raised.It’s common to have to ‘swizzle’ the connections: to connect the x and z channels of a 3-pronged output to channels of an input, for example. Mismatched swizzles are a common cause of those
MayaCommandErrores
. You can set the swizzle along with the connection by passing the swizzle you need as a stringnetwork.connect(var_node.outputs.result, if_node.inputs.b, 'z') # connects the 'x' output of var_node to the b channel of the input
Setting node properties
Nodes often have editable properties. There are a lot of different ones so it is often necessary to inspect a node and find out what properties it has and what type of values those properties accept. EverySFXNode
object has a read-only member properties
, which is a dictionary of names and property types. Using the same example objects as above:print if_node.properties ### BLah blah example here
node = network.properties[5] # get the node at index 5 in this network print node.properties: # { 'min': 'float', 'max': 'float', 'method': 'stringlist' } print node.min # 1.0 # getting a named property returns its value. node.min = 2.0 # sets the node value print node.min # 2.0
print node.i_dont_exist # AttributeError: no attribute named i_dont_exist node.i_dont_exist = 99 # MayaCommandError