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. http://gource.io/

 

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 script.py "depot_path" "output.gource"
gource output.gource -o gource.ppm

With the “script.py” being this script:

import P4
import os
import sys
from optparse import OptionParser

P4_ACTION_TO_GOURCE = {
	"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.

	**Arguments:**
		:``None``: ``

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

	**Returns:**
		:``None``: ``

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

	**Author:**
		* kelly.snapka, kelly.snapka@volition-inc.com, 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:
			self.p4_instance.connect()

		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
			self.p4_instance.disconnect()


def printProgress (iteration, total, prefix = '', suffix = '', decimals = 1, barLength = 100):
	"""
	Call in a loop to create terminal progress bar
	@params:
		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:
		sys.stdout.write('\n')
	sys.stdout.flush()
	

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 ]
		changelist_numbers.reverse()

		_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.

	**Arguments:**
		None

	**Keyword Arguments:**
		None

	**Returns:**
		:``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
				break

	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: http://usa.autodesk.com/fbx/

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.

http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/fbxpropertyget-in-20131-python/

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

For years now, we’ve been using morph target based facial animation on our games at Volition. At the start of a new project there is a typical “bones vs. morphs” debate that occurs and we’ve always stuck to what we know. In the past, I’ve been on the recipient side of reviewing the results of debates and proposals. On a recent project, I had the opportunity to dive into this topic more hands on and I was quite surprised to hear myself recommending a ‘bones based’ approach…

You need to install or upgrade Flash Player to view this content, install or upgrade by clicking here.

NOTE: Normal map is inverted and wrinkle maps are not setup or shown here.
  • I made this simply to get an animated facial bone setup that we could use to test in our engine.  It is by no means final or indicative of quality. The animation is rough as quality of the performance was not under evaluation here.
  • This video is a screen capture from within 3ds Max of a 44 facial bone  setup (counting head, jaw, and tongue too).
  • I setup and animated the head in Softimage:Face Robot.
  • I did the eyes and head movement by capturing mouse tracking. The eye blinks were captured by setting keys during playback.
  • Lipsync was done with SI’s lip sync tool and it did a very nice job imo.

Head (work-in-progress) model courtesy of Ben Eoff.

More >

Performance Opt. for 3ds Max 2012 – Disable Hidden Skin & Morpher

It seems the 3ds Max 2012 is still processing Skin and Morpher modifiers when their geometry objects are hidden.  So, this quick snippet just toggles the modifiers onoff across the entire scene based on their objects’ visibility.

mapped fn toggleSkinMorph_basedOnVis obj = (
   modA = obj.modifiers
   for m in modA do (
      if classof m == Skin or classOf m == Morpher then (
         m.enabled = not obj.isHidden
      )
   )
)

toggleSkinMorph_basedOnVis $geometry

MXS Transform Test

Objects in 3ds Max will typically need to have “clean” transforms.  It is quite easy to invalidate them via non-uniform scales or mirroring. I wrote a simple script to identify objects submitted to us by our vendor that are in need of correction.

The script checked for offset pivots, mirrored  objects, and non-uniformly scaled objects.

Here is an example of the feedback dialog that the user would be presented with.

SR3 MXS Convert Pattern Maps

We discovered, after the majority of assets were authored, that we needed to convert our PC pattern maps to the technique we were using with NPC’s.  Pattern maps were used to customize the characters by replacing the pure RED, GREEN, or BLUE with a new color via the shader at runtime.

Rather than stick someone with the tedious task of reworking all of these bitmaps by hand, I created a maxscript that handled the heavy lifting.  Details below:

You need to install or upgrade Flash Player to view this content, install or upgrade by clicking here.

Here is an overview of how the script works.  It has  THREE main operations:

  1. Convert PC pattern maps to NPC pattern maps, as follows:
    1. Renders existing pattern maps (UV3) out to UV1…
    2. Converts the color setup from PC to NPC…
    3. Combines the Pattern & Diffuse…
  2. Renders out Decals on UV1.
  3. Ports shaders from PC to NPC materials.

Bake Multi Low Poly Meshes to Single Low Poly Mesh

We had the need on SR3 to bake game resolution mesh elements down into an even simpler version, specifically in the waist/hip area.  We were experiencing issues with the belts and accessories in this area having intersection issues with other assets and we did not want to outright hide them.  So, I threw the following tutorial together for the artists to make a pass on addressing the issue.  LINK

SR3 MXS Toggle Character Bitmap Size

This was a quick script that I threw together to help with outsourcing.  I needed to be able to quickly check for an evaluate the delivered high resolution and low resolution bitmaps authored for the character assets.

To toggle between the large and small maps, the script looks at the materials and finds the bitmaps in use. Then it looks on disk for it’s counterpart and swaps the entry in the material.  Simple stuff, but it makes the workflow of selecting a character and running the script much faster than doing it by hand.  The only requirements are that the filenames use the naming conventions already established, that is “_SM” for small and “_LG” need to appear somewhere in the filenames.

By default, the script runs on all selected objects toggling the maps highlighted in the scripts gui.  You can <CTRL> or <SHIFT> click to add or remove maps from the selection.  If a new map type needs to be added, it can easily be done in the script.

SR3 DX9 Shader to Simple Shader

This post shows a simple script & shader that I wrote to workaround an issue and better visualize the results an artist would see with their asset further down the pipeline in game.  This was by no means an ideal solution and was only a bandaid.

You need to install or upgrade Flash Player to view this content, install or upgrade by clicking here.

NOTE: The intended audience of the video was a group of outsourced artists.

The issue:

Our game materials are authored in a proprietary editor that generates multiple shaders for different targets, such as different hardware consoles or optimizations.  One such target is a DX9 shader that is viewable in 3ds Max.  Unfortunately, Max handles normal maps inversely from how our game renderer handles it and the tool did not account for this in the hlsl that it generated.  This meant that it was difficult for the artist to identify and correct normal map seams early in the process (prior to exporting and viewing in game, which many outsourcers did not have the ability to do).

This was a known, long standing problem and existed on the work backlog for one of our employees to address.  Unfortunately it was lower priority than other work yet was still deemed to be something that would get resolved properly and ‘soon’ by the individual. Rather than step on his toes and avoid jumping into, admittedly unfamiliar, c++ territory for the proprietary editor I implemented a quick solution…

The solution:

  • I authored a simple dx9 shader that would properly display our game’s normal map format in the 3ds Max viewport.  This shader was very stripped down to the basics…diffuse map, normal map, and ambient color.
  • I authored a simple maxscript gui to …
    • control toggling between the game shader and this simple diagnostic shader (non destructively using scene states)
    • toggle map and parameters on\off quickly

vMesh Projector

3dsmax maxscript that I threw together that aids in retopologizing hi-poly meshes down to lo-poly.

“vMeshProjector” is a maxscript that projects one mesh onto another. It is primarily envisioned as a tool to aide in building lo-poly meshes that conform to high poly counterparts, from which normal maps and other data may be pulled. It does however have other potential uses, particularly in the areas of kit bashing.

It was a fun project and I learned a lot.

Here are some additional samples of the tool in action:


Dog Gun Seat Skull