tech art

Unity Demo01

This video is a WIP of a demo I’ve been throwing together in Unity to explore and better understand its capabilities and use of c# scripting and shaders.

This has been my first real foray into Unity “hands on” and I estimate I’ve spent between 60-80 hours over the course of about a month working on it so far, having started during the Winter holiday.

It has been a blast and time permitting I will get back to the missing aspects and polish soon.

MotionBuilder get final camera rotations

GOURCE visualization of ‘vmobu’

For fun, I used the open source gource app to generate this video.


To create it I had to pull changelist data from our perforce server and conform it to a log format that gource would use. To accomplish this, I made this quick little python script, intended to be run from a console.

python "depot_path" "output.gource"
gource output.gource -o gource.ppm

With the “” being this script:

import P4
import os
import sys
from optparse import OptionParser

	"add"				: "A" ,
	"branch"			: "A" ,
	"edit"			: "M" ,
	"move/add"		: "M" ,
	"integrate"		: "M" ,
	"delete"			: "D" ,
	"move/delete"	: "D",
	"purge"			: "D"

class P4_Connect( object ):
	Context manager that connects a p4 instance on entry if it is not connected and disconnects on exit.

	Also, p4 exception level is raised to Errors.

		:``None``: ``

		:``p4_instance``: `P4.P4` Optional P4 instance rather than creating one on the fly.

		:``None``: ``

	**Examples:** ::
		>>> with P4_Connect() as p4:

		* kelly.snapka,, 6/25/2016 9:05 PM
	def __init__( self, p4_instance=None ):
		self.p4_instance						= p4_instance or P4.P4()
		self.exception_level					= self.p4_instance.exception_level
		self.p4_instance.exception_level = P4.P4.RAISE_ERROR

	def __enter__( self ):
		self.isConnected = self.p4_instance.connected()
		if not self.isConnected:

		return self.p4_instance

	def __exit__( self, type, value, tb ):
		self.p4_instance.exception_level = self.exception_level
		if not self.isConnected:
			# Only disconnect if a connection was made in the __enter__ method

def printProgress (iteration, total, prefix = '', suffix = '', decimals = 1, barLength = 100):
	Call in a loop to create terminal progress bar
		iteration   - Required  : current iteration (Int)
		total       - Required  : total iterations (Int)
		prefix      - Optional  : prefix string (Str)
		suffix      - Optional  : suffix string (Str)
		decimals    - Optional  : positive number of decimals in percent complete (Int)
		barLength   - Optional  : character length of bar (Int)
	formatStr       = "{0:." + str(decimals) + "f}"
	percents        = formatStr.format(100 * (iteration / float(total)))
	filledLength    = int(round(barLength * iteration / float(total)))
	bar             = '*' * filledLength + '-' * (barLength - filledLength)
	sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)),
	if iteration == total:

def get_options():
	usage		= "Usage: %prog [options] DEPOT_PATH OUTPUT_FILE"
	parser 	= OptionParser( usage )
	return parser.parse_args()

def build_log( depot_path, *args ):
	log = ''
	with P4_Connect() as p4:
		changes = p4.run_changes( depot_path, *args )
		changelist_numbers = [ d['change'] for d in changes ]

		_depot_path = depot_path.split("...")[0]
		num_changelists = len( changelist_numbers )
		for n in range( num_changelists ):
			changelist_number = changelist_numbers[ n ]
			printProgress( n+1, num_changelists, prefix = 'Progress:', suffix = 'Complete', barLength = 50)
			descriptions = p4.run_describe( '-s', changelist_number )

			for d in descriptions:
				timestamp	= d.get( 'time' )
				author		= d.get( 'user' )

				num_files = len( d.get( 'depotFile' ) )
				for i in range( num_files ):
					action		= P4_ACTION_TO_GOURCE[ d.get( 'action' )[i] ]
					depot_file	= d.get( 'depotFile' )[i]

					if _depot_path in depot_file:
						log += '{0}|{1}|{2}|{3}\n'.format( timestamp, author, action, depot_file )

	return log

def write_log_to_disk( p4_log, gource_file ):
	with open( gource_file, 'w' ) as file:
		file.write( p4_log )

if __name__ == "__main__":
	options, args	= get_options()
	depot_path		= args[0]
	gource_file		= args[1]

	p4_log = build_log( depot_path )
	write_log_to_disk( p4_log, gource_file )

Locate pydevd at runtime

One of the quirks in remote debugging an application with PyCharm is that you need to be able to import the proper pydevd package.  This tends to change location with every PyCharm installation and update.  Rather than hardcode these paths and check against the installed version of PyCharm to locate them, I’ve found it convenient to simply use a running instance of PyCharm to locate the location of the pydevd that I need to import into the client app.

Once you have the path, you can append it to you sys.path or import it directly with a module like “imp”.

Eitherway, here is the snippet that locates pydevd for the running instance of PyCharm…

import os
import sys
import win32com.client

def get_pydevd_path():
	Gets the pydevd path based on locating the executable path of a running PyCharm process.


	**Keyword Arguments:**

		:``pydevd_path``: `<str>` E.G. "C:/Program Files (x86)/JetBrains/PyCharm 4.0.1/pycharm-debug.egg"

	pydev_path = None

	WMI = win32com.client.GetObject( 'winmgmts:' )
	processes = WMI.InstancesOf('Win32_Process')

	for process in processes:
		if 'pycharm' in process.Properties_('Name').Value:
			path = process.Properties_('ExecutablePath').Value
			p = os.path.abspath(os.path.join( os.path.dirname(str(path)), '../debug-eggs/pycharm-debug.egg'))
			if os.path.exists( p ):
				pydev_path = p

	return pydev_path

Debugging Motion Builder Scripts in PyCharm (3.4)

So, you want debug python in MoBu with PyCharm but either aren’t sure how to set it up or missed some detail along the way.  Well, this post should help.

Note: Much of this information holds true for Maya and other applications with integrated python. More >

Python FBX SDK, brief intro…

I had the opportunity to crack open the Python FBX SDK the other day.  Doing so happily validated many assumptions I had made confirming that I can read\modify\write .fbx files without going through a DCC.   While not hard to overcome, I thought it might be nice to have some of the initial items I stumbled on collected in a single post.

First, get the Python FBX SDK  here:

Second, be aware that due to its c++ underpinnings it relies heavily on iterators to build up collections.  This was a little foreign to me as I’m used to looping through existing loops.  For example, to build a list of properties on an object one needs to enter a while loop until an invalid property is returned…

props = []
prop = obj.GetFirstProperty()
    while prop.IsValid():
        if prop.GetFlag(FbxPropertyAttr.eUser):
            props.append( prop )

        prop == obj.GetNextProperty( prop )

Third, the python exposure of FBXProperties has no “get()” method. So, you have to cast it into a appropriate property type that does.  I found a post on Autodesk’s Area that covers this well.

Fourth, Python FBX SDK is highly enumerated.  An example of this is in the code above where I inspect a property to see if it is a custom, user defined property.

if prop.GetFlag(FbxPropertyAttr.eUser):

Well, that’s about it for now.  I’m pretty excited about the doors opened by being able to leverage this library in standalone python scripts.

Exporting to FBX from MAX (Morpher Issues)

I’ve been looking into porting assets to Maya, MotionBuilder, Softimage via FBX. One problematic area I ran into early on was that of the ‘Morpher’ modifier…

To maintain quads, be sure to do the following:

  1. All mesh objects should be of class ‘Editable_Poly’
  2. In the exporter options, “Preserve Triangle Orientation” should not be checked.

Morpher compatibility…

  1. It seems that only the top most MORPHER modifier is exported with FBX. The others are ignored.Furthermore,it seems that morpher modifiers are limited to 100 channels. While you can set channels above this in mxs, the results are not as intended and I’ve only seen one channel above 100 work and it seems to be any index over 100.
  2. All morph targets need to exist in the scene as objects and targeted via the MORPHER ui. Failing to do this will result in the channel names being truncated to the first letter of their name. IE “Jaw_open” and “Jaw_back” both become “J” and “J” respectively. A script below helps address this by extracting and re-targeting morph target objects embedded in the MORPHER modifier.

More >

Facial Animation R&D