Maya Exporter: Tabs and Tools!

I've been chipping away at my Maya Exporter and have a few Photoshop functions tied into it now, along side the original exporter functionality.

Behind the scenes the Photoshop commands are built on Standard modules, using Subprocess and some string manipulation in order to get JavaScript commands to the Photoshop application. As the tool has expanded beyond it's initial export-only functionality, there has been a decent amount of clean up and re-factoring behind the scenes to make sure that it doesn't just turn into a mega-script.

Maya-side functions and Photoshop functions have been split into separate classes and files from the GUI, which has made keeping track of the code a lot easier.

The GUI is built using the Pyside QT Libraries, which comes standard with Maya 2014.

Next on the list, adding the option to load different project environments.

Python Photoshop Automation without win32com- The Example

So here is a working example of what I was talking about in my last blog post- making Photoshop automation possible without needing to use the win32com module.

This specific example is not cross-platform compatible. It relies on getting the Photoshop application through it's registry entry, but the subprocess module is cross platform so it shouldn't be too hard to hammer this into a shape that can be used on Mac and PC. Or just Mac. Or whatever.

Essentially there is no real magic going on here, just lots of string manipulation behind the scenes. Like I said in my last post, the biggest hole in this right now is the lack of feedback from Photoshop, but that's something I'm looking into when I get a few spare minutes here and there.

Here is the example. When you run it, you should come up with something that looks like this:

What an Amazing document. 

And the code! This is a particularly long snippet. Hang in there!

Essentially when the commands are called, like add_new_layerSet(), the arguments are substituted into a string that contains the Javascript equivalent. Things like Booleans have to be modified to be lowercase before they are substituted because of the differences between the languages.

Once the string has been assembled, it is added to the _commands_list object, along with any other commands that will eventually be called.

Finally, when everything is ready, in Python execute_commands() is called, compiling our list of Javascript command strings into a .jsx file, and using Subprocess to call the Photoshop application with the .jsx as an argument. This makes Photoshop execute the actions contained in the script.
  
"""
Photoshop Python->JavaScript interface example.

An example of a Python wrapper for Photoshop's native Javascript. 

-Compiles Javascript commands into a temporary file.
-Calls the Photoshop executable with the jsx as an argument. 

Author: Pete Hanshaw 2013
"""

import subprocess  
import _winreg
import os


class PhotoshopInterface(object):
    
    def __init__(self):
        """
        Set up the application path, temp file location and our commands list. 
        """
        self._psApp = self._find_ps_app()
        self._temp_file = "c:\\temp\\ps_temp_com.jsx"

        # This command list holds all our commands until we are ready to write them
        # to the temp jsx file. 
        self._command_list = []
        
    
    # BEGIN INTERFACE FUNCTIONS
    def _find_ps_app(self):
        """
        Find and return the location of the Photoshop exe from it's registry entry. 
        I do this because its more robust than an assumed absolute path. 
        (although it does assume version...)
        """
        
        # I use Photoshop V12.0 64x. Change this to whatever version you are using. 
        self._psApp_reg_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Adobe\\Photoshop\\12.0")
        self._psApp_path = _winreg.QueryValueEx(self._psApp_reg_key, 'ApplicationPath')[0] + 'Photoshop.exe'
        return self._psApp_path
    
    
    def _add_command(self, command):
            """
            Generic method to add commands to the list. 
            """
            self._command_list.append(command) 
            
        
    def _compile_commands(self):
        """
        Write the commands to a temporary javascript file. 
        After this is done, empty the command list. 
        """
        if len(self._command_list) > 0:
            with open(self._temp_file, "wb") as f:
                for command in self._command_list:
                    f.write(command)
            f.close()
            self.clean_commands()  
            
        else:
            print "No commands to compile."
            
    
    def clean_commands(self):
        """
        Call this to make sure the command list is clean. 
        """
        self._command_list = []        
    
    
    def execute_commands(self):
        """
        - Call to Compile the commands in the command list. 
        - Call the target subprocess with the compiled javascript as
        an argument. 
        """
        self._compile_commands()
        self._target = self._psApp + " " +  self._temp_file
        
        # This is the magic part. Subprocess calls Photoshop with the jsx as an arg. 
        self._p = subprocess.Popen(self._target)  
        

    # BEGIN COMMAND FUNCTIONS
    
    # Because the wrapper is manipulating large chunks of text, the command
    # functions begin to take up a lot of lines very quickly. 
    def new_document(self, doc_x = None, doc_y = None, doc_name = None):        
            
        """
        Python wrapper for the new document command. Simplified to only accept
        name and resolution. 
        Supported Arguments are:
        doc_name : String
        doc_x : Int
        doc_y : Int
        """
        if (doc_x != None):
            self._doc_x = doc_x
        else:
            self._doc_x = 512
        
        if (doc_y != None):
            self._doc_y = doc_y
        else:
            self._doc_y = 512
        
        if (doc_name != None):
            self._doc_name = doc_name
        else:
            self._doc_name = "new_document"
        
        # Hard set values
        self._res = 72
        self._mode = " NewDocumentMode.RGB"
        self._init_fill = "DocumentFill.WHITE"
        self._PaR = 1.0
        self._BpC = "BitsPerChannelType.EIGHT"
        
        self.command = (
            """
            doc = app.documents.add(%s, %s, %s, '%s', %s, %s, %s, %s);
            """% (self._doc_x, self._doc_y,
                  self._res, self._doc_name,
                  self._mode, self._init_fill,
                  self._PaR, self._BpC
                  )
            )
        self._add_command(self.command)    
        
        
    def add_new_layerSet(self, Name = None):
        """
        Adds a new layer set to the currently active document. 
        Supported Arguments are:
        Name : String
        """
        
        if (Name != None):
            self._name = Name
        else:
            self._name = 'Group'
                
        self.command = (
            """
            var docRef = activeDocument;
            var newLayerSet = docRef.layerSets.add();
            docRef.activeLayer.name = '%s';
            """ % (
                    self._name
                    )
        )
                
        self._add_command(self.command)          
        
    
    # By default layer sets are transparent. This one looks a little more complicated
    # Because it has the option of adding a fill and a parent.
    def add_new_layer(self, Name = None, Parent=None, Fill = None):
        """
        Add a new layer. 
        Supported arguments are:
        -Name : String
        -Parent: String
        -Fill : Tuple
        """
            
        if (Name != None):
            self._name = Name
        else:
            self._name = 'New_Layer'       

        if Parent != None:
            self._parent = (
            """
            parentSet = docRef.layerSets['%s'];
            """ % (Parent)
            )
            
            self._move = (
            """
            thisLayer.move(parentSet, ElementPlacement.INSIDE);
            """)
            
        else:
            self._parent = ""
            self._move = ""

        if Fill != None:
            self._fill = (
                """
                var fillColor = new SolidColor();
                fillColor.rgb.red = %s;
                fillColor.rgb.green = %s;
                fillColor.rgb.blue = %s;
                app.activeDocument.selection.fill( fillColor, ColorBlendMode.NORMAL, 100, false );
                """ % (Fill[0], Fill[1], Fill[2]))
        else:
            self._fill = ""

        self.command = (
            """
            var parentSet;
            
            %s
            var docRef = activeDocument;
            var newLayer = docRef.artLayers.add();
            var thisLayer = newLayer;
            thisLayer.name = '%s';
            %s
            %s
            """ % (
                    self._parent,                    
                    self._name,  
                    self._move,
                    self._fill
                    )
        )
        
        self._add_command(self.command)    

# With this setup, I'm going to make a new document with a new layerSet. 

psCom = PhotoshopInterface()

# Clean the command list first. 
psCom.clean_commands()

# My commands. 
psCom.new_document(doc_x=512, doc_y=512, doc_name="Wrapped")
psCom.add_new_layerSet(Name="My Wonderful Layerset")

# Now add a layer set, making My Wonderful Layerset it's parent. 
psCom.add_new_layer(Name="My Fantastic Layer", Parent="My Wonderful Layerset", Fill=(128, 128, 255))

# Execute the lot of them by calling the Photoshop application.
psCom.execute_commands()

# Whoop whoop. 

Python Photoshop Automation without win32com.

Clunky but kinda fun, its possible to automate Photoshop using Javascript files passed as arguments to the subprocess. 

Although this essentially means you have to write your script in two languages (PS's native JS and Python for pipeline) the practical aspect of this is that it allows scripts to be written that do not require people to have the win32com/comtypes modules installed, and paves the way for cross platform scripts that aren't tied to windows only modules. 

I've had some success with creating a dynamic JavaScript builder that wraps Python functions around Photoshop's native Javascript commands. When the list of commands is executed it is compiled into a jsx file and then passed on to the Photoshop subprocess as an argument.

The only short coming right now is obtaining the output from Photoshop and making it into a two-way street. I have some ideas on how to do this, and will update with the solution I end up using. 


Zbrush Speedsculpt: Cup

I didn't choose this theme this week, but it was a great chance to abuse the radial symmetry brush for my own nefarious cup-theme related purposes. This was a true speed sculpt and was completed under the hour.

Yes. Technically a goblet is still a cup. 

Model Monkey: A Maya Exporter

When I started on the project I have been working on for the last few months, it lacked any kind of automation or pipeline tools for the Art department. There was no kind of convention in the names, and no kind of structure to the project. It was a nightmare! Did I mention the pipeline had artists working on two different platforms? Compensating within the tools for the different folder structures on Mac and PC computers also became a concern, although it did not end up being as much of a problem as I thought it would be.

From day one it was a goal of mine to introduce naming conventions, and restructure the project to be more pipeline friendly. It was a crazy-big job, but now its starting to pay off with some art tools.

To address the lack of exporting tools I wrote a Maya exporter that takes advantage of the new naming conventions, predictable folder structure and Python's awesome ability to manipulate strings. Behind the scenes it has a dictionary that matches the scene file's prefix with the Unity project directory the exported FBX is meant to go in, as well as loading in a pre-configured fbx settings file to reduce the chance of messed up exports. 

Functionally its still fairly basic, but it has been written to be expandable as required, and portable across projects, just by editing the separate configuration file. It also has the added bonus of working across both Mac platforms and PCs. 

Maya 2014 includes Pyside, which allows access to the powerful QTFramework. With just a few lines of code, any artist has access to the same tools that would have previously required a separate PyQt4 installation. 

ZBrush Speed Sculpt: "Worried"

I was meant to do this yesterday, but I have been spending most of my time writing an export script for our project. I was able to nab some time this morning before work to do a quick speed sculpt for the "Worried" theme.


ZBrush Speedsculpt: Blunderbuss!

The theme for this week was 'firepower' so I went with a Blunderbuss...

Note my hour was up before I could complete the flintlock mechanism... so this thing is pretty harmless. So much for firepower!

Need more flintlock thingy-mah-bobs!

3d Print- Wagon

Another 3d Print! This time the results not *exactly* what I was hoping for. 

This model is created using polished fine Fine Polyamide (Nylon), the same as my other prints but with an extra step- mechanical polishing. 

It was this extra step where things went wrong- the model is put into a little container with a large quantity of smaller stones, and vibrated to smooth the surface using mechanical abrasion. Unfortunately this process also takes a significant amount of surface material away, resulting in the thinner parts of the model either snapping or disintegrating all together. 

Although the quality on the majority of the model was pretty good some of the details on the front of the model were destroyed. The spokes on the wheels were also completely compromised. Apart from that, Its still an awesome looking model... might try fixing it with some putty!

Note the wearing down of the thinner elements of the model. Without the polish, these would have been pretty sturdy.