Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

New Tool- Blender A3D Quick Tools

This tool came out of some frustration with the Blender Hotkey layouts. Blender has amazing functionality, but my fingers aren't flexible enough to keep up with the hotkeys, and I was really missing some more context to the right click menus available. I wanted a filtered, simple toolkit that was relevant to what I needed at that moment.

The main goal was to avoid interrupting the flow of modelling so I could focus on the problem at hand rather than either look up the functionality or spend the next few seconds scanning a right click menu for that item.

I decided to take a stab at making a hotkeyed pie Menu. Where Blender suffers in the UI/UX department, it more than shines in how well it exposes it's UI functionality to Python (although selection and mesh manipulation is still a bit of a dark art compared to Maya's MEL and PyMel).

I also wanted to keep it pretty concise- the core idea behind the Pie menu was to keep it fast, and keep the cognitive load low. I think I may have ended up a little bit on the too many options side, but it's still usable without getting lost.

Here are some of the items I did a bit of extra legwork to get in:

Partial Edge Loop Selection:

What I wanted was a version of edge loop selection that would stop at UV seams or where the mesh poles. The UV seams were the biggest driver of this to make marking seams easier, but it's a handy modelling tool too.

I started with an example I looked up on Stack Exchange about how the edge loop lookup code someone had written actually works. Essentially it relies on walking around a faces edges using linked loops, but has a bunch of caveats- the code as written works as intended on a cube or something where the loop actually, you know, loops, but when applied to something with borders the results are a bit weird...

Nailed it.

The algorithm has two termination conditions- either return back to the loop we started with and break, or reach the maximum iterations allowed and break. 

To get to my intended result I added a couple of additional boundary termination clauses- 
  • if the radial loops to either side of the current loop are seams or borders, break. 
  • If one of the current loop's vertices aren't associated with exactly four faces, break.
This worked well, but to get the final result I was after, I also had to reverse my lookup direction if I hit one of the boundary clauses. This would give me partial and complete loops on either side of my connected edges.



Partial edge loops are useful.


Partial edge loop selection in action.
Handy tool! If you want to see more check the tool out here.


-Pete

Blender: Calling parameters with UI Operators

A quick little tip for how to call parameters via UI operators in your scripts.

This is useful if you want to call a function that has parameters that change it's behavior- for example the bpy.ops.mesh.mark_sharp(clear=True) vs bpy.ops.mesh.mark_sharp() are the same operator with a flag that inverts the method's outcome- marking or clearing a sharp edge.

Your UI operator object wraps the python operator, including it's parameters. As long as you know what the parameters are you can call them explicitly on the UI object- either inline when it is declared, or on subsequent lines if you assign it to an object.

# ... in your Draw function...
# Default function to mark a sharp edge
pie.operator("mesh.mark_sharp", text="Mark Hard Edge", icon="BLENDER")

# We add the additional parameter here to clear the edge- this is the equivalent of calling
# bpy.ops.mesh.mark_sharp(clear=True)
pie.operator("mesh.mark_sharp", text="Clear Hard Edge", icon="BLENDER").clear = True

# If we have a custom operator with multiple parameters we can assign the operator to an object
# and call as many as required.
my_op = pie.operator("foo.bar", text="FooBar")
my_op.foo = True
my_op.bar = False
# ...etc...

Photoshop Comtypes 2020 edition

Hello! I got asked a question about smart layer manipulation and I didn't know the answer off of the top of my head, so I dug out this ye ole snippet of mine from 2012 and updated it to 2020.

Some small differences are that I had to add a couple of flags to the comtypes object creation (which may have just been a quirk of my machine, see the stackoverflow link for details) and I've added some details about manipulating smart layers at the bottom of the snippet.

Happy coding!

##############################################################################
#
# Eight years later, here is my 2020 version of how to use Comtypes to drive photoshop
#
# Here is a quick code sample showing how to manage different layers and groups  
# in a photoshop document using Python. 
#
# Pete Hanshaw, 2020
# http://peterhanshawart.blogspot.com.au/
#
##############################################################################
#
# How to make a layerSet (aka, 'group') artLayer (aka 'layer'), how to make them
# active and how to move them. 
#
# These examples use the comtypes module. Grab it here:
# http://sourceforge.net/projects/comtypes/
#
##############################################################################


#Create the application reference
import comtypes.client as ct

# https://stackoverflow.com/questions/42794530/error-pointeriunknown-when-trying-to-access-com-object-properties/42823644
psApp = ct.CreateObject('Photoshop.Application', dynamic=True)
psApp.Visible = True

#Create a new document to play with
doc = psApp.Documents.Add(256, 256, 72, 'test_bed', 2, 1, 1)

#When scripting 'groups' are called 'layerSets'. 
new_layerSet = doc.LayerSets.Add()

#Once you create a layerSet object reference, you can access it's
#'name' attribute. The same goes for other objects you can normally
#name within Photoshop.
new_layerSet.name = "I'm a layerSet"

#regular, paintable layers are called 'ArtLayers'
new_art_layer = doc.ArtLayers.Add()
new_art_layer.name = "I'm an ArtLayer"

#To add a nested art layer into a LayerSet, use our layerSet object as a reference
nested_art_layer = new_layerSet.ArtLayers.Add()
nested_art_layer.name = "I'm a nested ArtLayer"

#The same goes for adding a nested LayerSet!
nested_layerSet = new_layerSet.LayerSets.Add()
nested_layerSet.name = "I'm a nested LayerSet"

#and so on!
deep_art = nested_layerSet.ArtLayers.Add()
deep_art.name = "Deep man, deep."

#Every time a new object is made, it will become the active layer. 
#To make other layers active, you can refer to them either by their name, or 
#their index location. 

#For example:

#Making an art layer active using the layer's name:
doc.activeLayer = (doc.artLayers["I'm an ArtLayer"])

#Making an art layer active using the layer's index location:
doc.activeLayer = (doc.artLayers[-1]) #This will select the background!

#Selecting a nested art layer is a little more difficult, as you have to
#'drill down' through the hierachy in order to select it. 
doc.activeLayer = (doc.layerSets["I'm a layerSet"].
    layerSets["I'm a nested LayerSet"].
    artLayers["Deep man, deep."])

#Moving a layer in the hierachy is done using the move command.
#The arguments specify which hierachy to move in, and where to put it. 

#For example, this will move the first layerSet we made just above the background
#layer.

#Make a new layer set
mobile_layerSet = doc.LayerSets.Add()
mobile_layerSet.name = "move me"

#Move the 'mobile' layerSet to just after the 'background' layer
mobile_layerSet.Move(doc, 2)

# Smart Layer Manipulation Examples adapted from:
# https://www.photopea.com/tuts/edit-smart-objects-with-a-script/
# Create a smart object layer.

# CREATING A SMART LAYER
# select a layer that you want to work with
smart_layer = doc.ArtLayers.Add()
smart_layer.name = "Smarty pants Layer."

doc.activeLayer = smart_layer

# Convert the active layer into a smart object
psApp.executeAction(psApp.stringIDToTypeID("newPlacedLayer"));

# EDITING A SMART LAYER
# Now we can edit the smart object
psApp.executeAction(psApp.stringIDToTypeID("placedLayerEditContents"))

# now, the Smart Object is an active document, we can work with it. Rename the layer...
smartDoc = psApp.activeDocument
super_smart_layer = smartDoc.ArtLayers[0]
super_smart_layer.name = "Amazingly smart layer."

# save the smart object and close it
psApp.activeDocument.save()
psApp.activeDocument.close()

# We are now back in the root object

# QUERYING A SMART LAYER
# If we want to check if a layer is smart, we can query it...
doc = psApp.activeDocument

for layer in doc.ArtLayers:
    # 17 is a psSmartObjectLayer - see the Adobe Scripting API 'PsLayerKind' to see what each value means.
    if layer.Kind == 17:
        print "We found a smart layer named {}".format(layer.name)

Calling Python from Substance Painter

Here is a quick code snippet for calling a Python script from Substance Painter and parsing the results.

The in/out is very simple, but serves as an example of using the alg.subprocess.check_output function to bridge the JS api and your own Python scripts.


// This can be called from the QML UI, or elsewhere in the plugin code. 
function GetAllFilesInDirectory(root)
{
  if (root == undefined)
  return;

  // I put my scripts in a relative path to keep my plugin tidy. 
  var script_path = "Scripts/FileUtils.py";

  // Gathering files
  var ret = "NOSTRING";
  try
  {
      // The arguments are used as parameter inputs to the Python script. This requires some planning
      // and well communicated conventions, but works well enough. 
      ret = alg.subprocess.check_output(
          [
          pypath, // Absolute path to interpreter.
          script_path, // Relative path to the py script.
          "log_files_of_type", // sys.argv[1], in this case the python method I'm calling.
          root, // sys.argv[2], used as a parameter for my method, in this case, the root directory to find files in.
          "spp" // sys.argv[3], used as a parameter for my method, in this case the file type extension to look for.
          ]
      );
  }
  catch(err)
  {
      alg.log.exception(err);
      return;
  }

  // Iterating the returned string 
  var file_urls = [];
  var all_files = ret.split(/\r?\n/);
  for (x = 0; x < all_files.length; x++) 
  { 
      var local_file = all_files[x];
      if(local_file.length == 0)
          continue;

      // Convert the file names to the native substance path URL format. 
      var project_url = alg.fileIO.localFileToUrl(local_file);
      file_url.push(project_url);
  }
  return file_urls;
}


The Python script is also quite simple:

function GetAllFilesInDirectory(root)
#!/usr/bin/env python3

import glob
import sys

def log_files_of_type(root_dir, ext):
    """
    Returns a list of all the files with a given extension in the named directory. 
    @param root_dir : the directory to parse. 
    @param ext : the extension to match. 
    """
    for filename in glob.iglob(root_dir + '**/*.{0}'.format(ext), recursive=True):
        print(filename)

if __name__ == "__main__":
    # Select the method to run based off of the arguments.
    # args are [0] script name, [1] method name. Subsequent args are arbitrary based on method called. 
    method_name = str(sys.argv[1])

    if method_name == "log_files_of_type":
        log_files_of_type(sys.argv[2], sys.argv[3])


I find that any script I write that passes information back and forth between two different platforms/interpreters usually comes with it's own set of headaches.

This kind of string parsing should look familiar to anyone who has done work with Photoshop bridging tools, or who has chosen to wrap the P4 command line interface themselves.

(PS... syntax highlighting borked... should fix that one of these days...)


Do you want to play a game?

My Chrome browser split in half last night and asked me if I wanted to play a game. Saying yes turned my browser into a pseudo python console. Apparently Google has been tracking my search history and seen that, yeah, I like Python. Ok, having the browser window suddenly split open because someone is watching your search history is vaguely creepy, but what the hey, I do like Python!

Why do they want to play a game with me? I don't know, its Google. Why did they make Google maps into Pac Man? Its Google. They do stuff like that.

The first challenge was to take an equation as a string and parse it into reverse polish notation. After thinking about it today I was able to write a passable parser that will probably make any real mathematician twitch- but it works and the code doesn't make me twitch.

The challenge was, well, challenging, but fun! It reminds me a bit of the Project Euler challenges, except this one gives you a hopping bunny when you succeed. An animated ASCII hopping bunny.

Why?

Pac Man

Hop little bunny. Hop for joy!

Check in- Pete's Been busy...

Hello World,

I've been busy, so busy that I've been neglecting to blog. Luckily, some of what I've been up to is worth blogging about, so it works out.

At work, I've started a new position as a Senior Technical Environment Artist. If that seems like a long title, its because it is. I'm straddling a strange gap between grumbling about crappy tools and actually writing better ones. It's a great place to be, because I get to save myself tonnes of time and pat myself on the back about it at the same time.

Outside of work it was my birthday and something strange and computer-ey fell into my lap in the shape of a Raspberry Pi. We quickly became friends and I decided to make it a home.

Some Stuff about 3d Printing:

My Pi's new case.
Its my first attempt at industrial design! I created the case in Maya, based off of a CAD model of the Pi itself. I'm pretty surprised happy that the Raspberry Pi actually fits inside the case! Not only that, but it fits pretty damn well! I paid careful attention to how the case would open/close (its a two part slide) and how to access the GPIO pins.

Although this case works, its not quite where I want it to be. On the next version:
-I need to slightly adjust the positioning of the HDMI access point, and the Ethernet port which line up well enough to work, but could look better.
- The most disappointing part of the prototype is the complete failure of my light transmitting rods in the bottom right of the case... due to inaccuracies in printing these were not only very difficult to install, but also constantly mis-aligned due to the inherit flexibility of the transparent material used. These will be redesigned. Lame.
- Additional room will be added for the composite video output's metal sleeve. Not that anyone really uses them, but yeah. Its there. Why not make it work?
- Structural support will be given to the GPIO access point. Its kinda floppy at the moment, which makes the case feel unstable.
- The USB outputs will be flush with the case in the next version. This will increase the price of the model a little, but it will look waaay cooler, and also allow more air circulation inside the case.
- The text next to the LED status lights will be made bigger. Right now it prints more like Braille than text...
- Also... the next version will have bevels on the micro USB power port. Bevels are in right? Trendy.

The original design, soon to be updated!


Some stuff about code!

I've been continuing with writing tools in both CSharp and Python, and I've found as I learn and write more CSharp it's been making my Python code, for want of a better word, better. I'm a big fan of structure and readability, and while I've always been a fan of Python's flexibility, I feel it is a language that really gives you enough rope to hang yourself with.

Working with a language like CSharp that forces you to define the return type as part of the inherit properties of a method (even if its just void...) has really forced me to think about the way I approach my classes and methods in Python, and has resulted in my newer tools being leaner, with more modular logic and much more robust structure than my earlier ones. Yeah, I still love writing in Python, but now I feel a bit more confident that I'm not placing as many traps for myself in the future!

More stuff about 3d Printing:

Finally, my first color print!

I realized after the fact that the chicken leg in his knapsack does not present too well...
A speedy speed sculpt. With color!


Maya: Animation Event Definition Tool

I've been working on a new set of tools to define animation event data in Maya and export it into Unity for consumption by the AssetPostProcessor. This has been kind of a cool learning experience, because before now I really didn't have much to do with the Animation pipe, which has been an entirely manual process for the animators.

Previously clips were cut up and events were defined by hand in Unity once the .fbx files had been exported from Maya. This wasn't such a bad thing, except that the tools in Unity for defining events were very un-intuitive. The aim of doing all this was to allow the animators the ability to work in a tool they were familiar with, and get the AssetPostProcessor to do most of the boring legwork on the Unity side.

The Maya interface. 
The Maya tool is pretty simple right now, and responsive enough that the animators don't hate me. Navigating to different clips or events will move to that time range on the slider. The event list is directly correlated to events called by the engineers in engine, so they are only available as pre-set items in a drop down.

The data is stored in the scene as attributes on empty nodes. On export time, the data is compiled into dictionaries that are then written into a .JSON file named after the .fbx.

The end result is something like this:

Data for an 'Attack' animation. The first clip is the entire duration, the next three are the intro, loop and outro. In the anim events the unit fires it's weapons three times, once on frame 5, then on 9 and 13. 'Index' is used for the muzzle position on a unit or turret, as those are the only entities in game making use of the system. I would prefer to be using named bones for more flexibility. 
Once this animation is out there, the engineering team pull it into the AssetPostProcessor and generate the required clips and event metadata from the contents. 

All in all, the Maya side is pretty basic at the moment, and implemented on a project that currently has pretty basic animation requirements. Even so, the idea of feeding data into the AssetPostProcessor is something that really appeals to me, and the .JSON format has been pretty easy to write to from Python. 

Eventually I would like to be able to define custom VFX events on named bones, and mark up non-event related data like whether you want an asset to automatically generate lightmap UV's on an imported model or not. There is a lot of potential there. 

Perforce: Delete empty change lists using Python

Some of the tools I've written allow a user to check out chains of files- like Maya files as well as any exported FBX files and their unity metadata etc, all in one nice pass. This is great, but it has one annoying side effect. If the user presses the button twice the files are added to another change list, but the one they were just in stays there, empty. Press it a number of times and suddenly you have an army of zombie phantom changelists whose only purpose is to clutter up your pending list and annoy you. 

I have been using the P4Python API and found it to be... well... its kinda crap. It would be great if it had a 'delete all empty pending changelists' function, but it doesn't seem to. So I wrote one of my own. 

Beware! If you don't like hacking output out of strings the following code will make you cringe... 

There's my target...
import subprocess

"""Get a list of changelists from the command line. Try to delete them (will delete if empty)"""
# sInfo= subprocess.STARTUPINFO() # Use this instead of the code below to hide the output window. Kinda handy if you don't like annoying artists.  
# sInfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # Hide the cmd window
# p = subprocess.Popen(command, stdout=subprocess.PIPE, startupinfo=sInfo) # Same as below, but no cmd window. 

workspace = 'YourWorkspace'
# Get the pending changes on the local client using the P4 console commands and reading the output.
command = "p4 changes -s pending -c %s" % workspace 
p = subprocess.Popen(command, stdout=subprocess.PIPE) 
temp = p.stdout.read()

# Split the block of text at the line endings. This should give you one changelist per line. 
raw_data = temp.split('\n') 

print raw_data

# Now go through each line and split the text at the spaces. 
# This should end up with something that reads like ['Change', '99999', 'on' etc etc...
for line in raw_data: 
    target = line.rstrip().split(" ")
    if len(target) > 1: # Skip any list with single elements. That aint got what we want. 
        changeNum = target[1] # The changeNum should be the second element
        command = 'p4 change -d %s' % (changeNum) # Attempt to delete the changelist. P4 refuses to delete changelists with items.  
        p = subprocess.Popen(command, stdout=subprocess.PIPE) # Lets get some info back again. 
        temp = p.stdout.read() # Tell us about what you just did. 
        print temp # Good boy. 

The Result... poor 76610 got what was coming to him...

Output will be something like this:
["Change 76610 on 2014/02/06 by ****@**** *pending* '[EmptyChangeListToNuke] '\r", 
"Change 76607 on 2014/02/06 by ****@**** *pending* '[ToolDev] Stuff '\r", '']

Change 76610 deleted.
Change 76607 has 35 open file(s) associated with it and can't be deleted.




Use Python to use JavaScript to get Photoshop to do stuff... and tell you about it!

Its been pretty well established that you can send a .jsx file to Photoshop using a subprocess call in Python. The tricky part is then getting Photoshop to send some information back.

This is my awesome hacky method of passing information from Photoshop to Python. It relies on being able to write to a temporary text file from Photoshop and then reading that information back into Python. This method relies on actually being able to write data to disk... if that's a problem I suspect you could do the same to the console standard output and get the same results... somehow? Maybe?

Because writing to the disk was not a problem in this situation, I went with a temp file solution. The only kinda tricky part was making my program wait for the return value to be passed. Subproces.call() returns a value of 1 or 0 from the shell, but this only indicates that the program successfully (or not) opened.

Its highly likely that whatever script you passed to Photoshop as part of the Subprocess call will still be executing by the time your Python comes to the section where you want to read the return data. In this case, your Python code will likely be reading old data from the temp file, or, no data at all.

In this case, I was fine with having my program wait until the data it needed was available. I did this by doing a check to see if the temporary output text file had been modified. Once this condition was met, the file was opened in Python and the contents were pulled back into the main program.

I've seen some people recommending using a JSON file to do this, which is something I might look into if I need more complex feedback than a single line.

Here is an example of a Python script which builds a .jsx file, sends it to Photoshop, waits for a return value and then prints the return value out to the console.

"""
Example which builds a .jsx file, sends it to photoshop and then waits for data to be returned. 
"""
import os
import subprocess
import time
import _winreg

# A Mini Python wrapper for the JS commands...
class PhotoshopJSWrapper(object):
    
    def __init__(self):
        # Get the Photoshop exe path from the registry. 
        self.PS_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 
                                      "SOFTWARE\\Adobe\\Photoshop\\12.0")
        self.PS_APP = _winreg.QueryValueEx(self.PS_key, 'ApplicationPath')[0] + 'Photoshop.exe'          

        # Get the path to the return file. Create it if it doesn't exist.
        self.return_file = 'c:\\temp\\ps_temp_ret.txt'
        if not os.path.exists('c:\\temp\\'):
            os.mkdir('c:\\temp\\')
        
        # Ensure the return file exists...
        with open(self.return_file, 'w') as f:
                f.close()  
            
        # Establish the last time the temp file was modified. We use this to listen for changes. 
        self._last_mod_time = os.path.getmtime(self.return_file)         
        
        # Temp file to store the .jsx commands. 
        self.temp_jsx_file = "c:\\temp\\ps_temp_com.jsx"
        
        # This list is used to hold all the strings which eventually become our .jsx file. 
        self._commands = []    
    
    # This group of helper functions are used to build and execute a jsx file.
    def js_new_command_group(self):
        """clean the _commands list. Called before making a new list of commands"""
        self._commands = []

    def js_execute_command(self):
        """Pass the commands to the subprocess module."""
        self._compile_commands()
        self.target = '"' + self.PS_APP +'"' + " " +  '"' + self.temp_jsx_file + '"'
        print self.target
        ret = subprocess.Popen(self.target) 
    
    def _add_command(self, command):
        """add a command to the commands list"""
        self._command_list.append(command)

    def _compile_commands(self):
        with open(self.temp_jsx_file, "wb") as f:
            for command in self._commands:
                f.write(command)
           
    # These are the strings used to build the .jsx file.  
    def js_create_document(self, varName, w, h, docName):
        """
        Javascript command to create a new document. Returns varname as 
        a reference to the jsx variable. 
        """
        self._mode = " NewDocumentMode.RGB" # Hard set, but easy to add as a python var
        self._init_fill = "DocumentFill.WHITE" # Hard set, but easy to add as a python var
        self._PaR = 1.0 # Hard set, but easy to add as a python var
        self._BpC = "BitsPerChannelType.EIGHT" # Hard set, but easy to add as a python var 
        
        self._com = (
            """
            %s = app.documents.add(%s, %s, 72, "%s", %s, %s, %s, %s);
            """ % (varName, w, h, docName, self._mode, self._init_fill, self._PaR, self._BpC)
            )
        self._commands.append(self._com)
        return varName # Return the name we used for the jsx var, we can use this later in the Python code

    
    def js_write_data_out(self, returnRequest):
        """ An example of getting a return value"""
        self._com = (
            """
            var retVal = %s; // Ask for some kind of info about something. 
            
            // Write to temp file. 
            var datFile = new File("/c/temp/ps_temp_ret.txt"); 
            datFile.open("w"); 
            datFile.writeln(String(retVal)); // return the data cast as a string.  
            datFile.close();
            """ % (returnRequest)
        )
        self._commands.append(self._com)
        
        
    def read_return(self):
        """Helper function to wait for PS to write some output for us."""
        # Give time for PS to close the file...
        time.sleep(0.1)        
        
        self._updated = False
        while not self._updated:
            self._this_mod_time = os.path.getmtime(self.return_file)
            if str(self._this_mod_time) != str(self._last_mod_time):
                self._last_mod_time = self._this_mod_time
                self._updated = True
        print "Return Detected"
        
        f = open(self.return_file, "r+")
        self._content = f.readlines()
        f.close()      
        self._ret = []
        for item in self._content:
            self._ret.append(str(item.rstrip()))
        return self._ret
    
    
# An interface to actually call those commands. 
class PhotoshopJSInterface(object):
    
    def __init__(self):
        
        self.psCom = PhotoshopJSWrapper()
    
    def create_new_document(self, x, y, docName):
        """Compile a command to create a new document"""
        self.psCom.js_new_command_group() # Clears the command list. 
        self.docRef = self.psCom.js_create_document('docRef', x, y, docName) # Adds the new document command to the list. 
        self.psCom.js_write_data_out(self.docRef + ".activeLayer.name") # Get the document's active layer name. 
        self.psCom.js_execute_command()
        
        # Now I find the return value. 
        self.layerName = self.psCom.read_return()[0]
        print "Current active layer:", self.layerName
        
        
PS = PhotoshopJSInterface()
PS.create_new_document(512, 512, 'My Amazing Document')

Now, in my mind this is pretty handy, and could be extended to a point where it could become a viable Python API for Photoshop. One thing I do want to look into is using Socket control to talk to the Photoshop application directly, replacing the use of the Subprocess module. Maybe it would be possible to then get information back without writing to a temp file. Has anyone tried this?

Texture Monkey: Starting to look like a real tool.

Stuff is happening...
After a little while in the field it came to be pretty apparent that the win32com module can't be relied upon to consistently work on everyone's machines. I spent a good amount of time trying to work out why, but came up with no solution that worked consistently on everyone's workstations. Errors like ('Member not found, None, None) would constantly pop up in their debug feedback, but not on mine. Talk about frustrating. So I got pretty annoyed and just ripped the win32com components out. 

This was a pretty big undertaking, because apart from the GUI, win32com stuff made up about 80% of the remaining code. The P4 functionality wasn't touched, because its operating through it's own native API. 

The solution I went with was to write a wrapper around the JavaScript equivalent of what I was using the win32com module for. Each of these commands would be put into a list, which is then compiled into a temporary .jsx file. Finally this file is sent to Photoshop using the subprocess module. 

To deal with getting information from Photoshop a similar process was used. A JavaScript command is called to write the requested data to a file on disk, while Python waits until it can see that the file has been modified. Once Python can see that Photoshop has completed writing to the file, the contents are read and fed back into the main program. 

Benefits/Cons
Benefits? Right off the bat, the JavaScript executes much more quickly than the win32com commands. This is pretty cool, especially when coupled with the fact that it now works on all the artists machines without mysterious bugs. Yet. Also, I still get to keep writing the tool in Python.

The biggest con is that I can't help but feel that its a mother of a hack. I mean, writing JavaScript on the fly from Python to send to a program which can only feed data back through a temporary text file? Man. Awful. But it works. But I feel dirty. But it works. But what about those ugly chunks of strings pretending to be JavaScript? But it works. Yes it works. The structure of the code behind the scenes feels like its taken a train to the chest, but it works. My next couple of days will be pulling it together and making the code a little easier on the eyes.

PSD Metadata and Real time GUI updates.

It might not be the absolute best solution, but I have been able to put into practice my idea of storing the tool settings in the PSD metadata, and it actually works pretty well.

The current setup works like this:

  • On export, go through a dictionary of controls inside the GUI and check the settings. 
  • Store each object name and it's setting as a text block with easily splittable characters. For this I chose to use ~ and | as neither of these are used for windows file or directory names. 
  • Write this text block to the PSD metadata. 
  • Meanwhile, in the export, use the GUI's current settings to determine output locations, formats and files to include.  
To reload the settings:
  • A background thread is constantly listening for any changes in the current active Photoshop document. 
  • If the name of the document changes, it automatically kicks off a function that reads the metadata out of the active PSD and splits it into objectName, setting value pairs. 
  • If no data is found, it kicks off a process to apply default settings. 
  • This data is passed to another function that finds these QT objects, and applies the relevant setting recast as the applicable object type- eg bool, int, string.
  • These settings are only saved when the document is exported. I'm expecting a little grief from the artists about this if they switch documents before exporting them at least once, so I might try to fix it. 
The Stored Metadata, ready to be read back by the tool. 
With the listener process working on a pretty short timer and a very simple function, it's possible to get what feels like real time feedback in the tool without experiencing any lag or hangs. This is the first time I've made a tool where it's a two way street- usually it's just my tool telling Photoshop what to do. It's actually pretty satisfying to click between documents in Photoshop and watch all the checkboxes of Texture Monkey light up and change. 

Main settings to be saved are the different maps and resolutions associated with the various LODs, although settings for format and destination folders are saved as well. 




Texture Monkey... version"some big number"

We are now getting Perforce. Oh how I  missed thee Perforce! And what a fantastic opportunity to revisit my favorite pet project, Texture Monkey! Be be honest, I'm kinda sick of re-writing this tool, but I've got it to a point where it meets a couple of extra prerequisites that it was sucking at before, mainly:

  • It's written at a point where I actually feel I can competently write functional and readable code.  
  • It's UI uses the QT framework. Which it MUCH nicer to work with than the previous WX. 
  • It's been built around the premise that it  should easily be portable across projects without having to change any of the source code- provided afew assumptions about project structure are met. 
  • It does more than texture exporting, and now supports Perforce integration as well. 
  • Most importantly, it is written in a modular fashion, and can be expanded upon or cut back without too much trouble. 
Now with extra stuff!
Behind the scenes:
  • It stores and loads tool settings as metadata in each PSD file, so you don't have to select those settings again when opening the file later. In addition to that, if you change the settings, then export the file, these new settings are saved to the PSD for later use. 
  • It supports exporting multiple LOD textures for different asset bundles. This is an experimental feature, and may be cut. But its fun to play around with. The idea behind this is that Unity doesn't make a distinction between different IOS devices, and instead lumps them all into one category in it's asset settings, even though there is a significant difference in specs between devices. 
  • It brings Perforce into the tool chain. When it checks out whatever active document is in Photoshop, it also brings along with it all the associated exported texture maps, basing it's search on the export locations stored in the Metadata and the source file name. 
  • I'm still thinking about adding a check in function... I kind of want to get people to check in via the P4 client, just so they have an overview of exactly what they are checking in before they actually commit it (especially if things like models are dependent on the texture changes, but fall out of the purview of this tool) For now, I have just added a function that brings the P4 client to the foreground. 
  • I have tried to add a little flexibility for other artists to be able to use this tool. It's mainly aimed at people working on textures for models, but by adding custom destinations and formats hopefully it will be of some use to export to custom locations in different formats. 
  • The configuration is all stored as an .ini file, and contains all the file naming rules, default destination folders and stuff like that. It can be hand edited, but I don't think I'm going to bother with a custom UI at this point. 
  • The Style is one billion times better, mainly due to LoneWolf's dark orange stylesheet he has provided on tech-artists.org

Writing Metadata to a PSD file using Python

I want to save some tool specific information about a PSD file, but I don't want to have another pesky metadata file floating about to bloat my source texture folder.

Luckily, the PSD file format supports writing custom MetaData within it, which is perfect for what I want to do. In this particular example, I want my tool to be able to remember which folder the flattened image associated with this PSD will eventually be put into, the format the image will be in and the resolution.

There are many fields you can write to, but I have chosen to write to the Instructions information, because that just makes the most sense. Usually in Photoshop, you can see this fields available by going to the file info panel and going to the advanced tab:

In Python, we can access and write to the PSD's metadata very easily.

import win32com.client.dynamic as w32dynamic
w32 = w32dynamic.Dispatch

psApp = w32('Photoshop.Application')
doc = psApp.activeDocument

# When I pull this info out of the Metadata I split the | into
# a list of toolObjectName~setting pairs and then
# split these pairs into a tuple of strings (toolObjectName, setting)

settings = "chkBox_res_1024~True|export_dir~c:/test/my_doc.tga|rBtn_format_tga~True"

# Now to write this to the metadata
doc.Info.Instructions = settings

# If I want to get it back out...
settings = doc.Info.Instructions
print settings


I'm still experimenting with ways of storing the settings data in a prettier way, but so far this is working well for me, although the format and information I'm saving is *very* specific to the particular tool I am writing. Still early days... but hopefully the whole Metadata thing will be handy to other people out there.



Model Monkey: Now with Configs!

I've decided to use ini files to make it easier to port my Maya exporter across different projects with as little fuss as possible, using the standard Python ConfigParser module. The last time I really played around with .ini files was to mess with Command and Conquer Red Alert, and that was some time ago. It's really awesome that the .ini file format is still worth using today.

There is a bug where you can add new file rules until the tool grows so long it is
bigger than the screen. Time to add a scroll bar...
Part of getting this to work includes creating a GUI interface to minimize the manual file editing that you might expect. Being my first major use of an .ini for a tool (I did use a teeny tiny .ini on Texture Monkey) I suddenly realized that I had to re-work a lot of my previous configuration code in order to make my tool flexible enough to be re-configurable without breaking into a million different lame parts.

BUT! It's working now, whoo! A couple of things have to be modified, but I can get info into the tool, change it, and get it back into the .ini again, rinse and repeat. In other words, I can now do what people have been doing for decades! Whoo etc. But hey, it works!


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. 


PySide and Maya2014

I've been working with PySide to make some tools for our Animation department. Part of that process involves a UI that is generated on the fly based on data read in from a spreadsheet.

It was important that each UI component had a unique and predictable object name so that I could access the information it contained and save it out later. In case anyone else needs to do it, or has a better way, here is an example of the process I use. 

Thanks to Nathan Horne and Chris Zurbrigg for their very useful examples!

# System
# System
import sys
import os

# GUI Modules
from PySide import QtCore, QtGui
import maya.OpenMayaUI as apiUI

# Allows converting pointers to Python objects
from shiboken import wrapInstance

def maya_main_window():
    main_win_ptr = apiUI.MQtUtil.mainWindow()
    return wrapInstance(long(main_win_ptr), QtGui.QWidget)

# Create out Dialog class, inheriting from QDialog
class Dialog(QtGui.QDialog):

    # Call the maya_main_window command to parent it
    def __init__(self, parent = maya_main_window()):

        super(Dialog, self).__init__(parent)

        # Create a list of rows and buttons
        self.rows = ["Row1", "Row2", "Row3"]
        self.buttons = ["Button1", "Button2", "Button3"]

        # And now set up the UI
        self.setup_ui()

    def setup_ui(self):

        self.main_layout = QtGui.QVBoxLayout()

        # Create a series of rows, and in each row, put our buttons
        for row in self.rows:

            self.row_Hbox = QtGui.QGroupBox()
            self.layout = QtGui.QGridLayout()

            for button in self.buttons:

                # Label the button with it's list name
                self.push_button = QtGui.QPushButton(button, self)

                # Give each button a unique object name
                self.b_name = row + "_" + button
                self.push_button.setObjectName(self.b_name)

                # Add a QLine Edit to each particular button
                self.q_line_name = self.b_name + "_TextEdit"
                self.my_line_edit = QtGui.QLineEdit()
                self.my_line_edit.setText("Hi! I'm " + self.q_line_name)
                
                # Also give it a unique name
                self.my_line_edit.setObjectName(self.q_line_name)

                # Offset each button in the layout by it's index number
                self.layout.addWidget(self.push_button, 0, self.buttons.index(button))

                # Offset each QLine Edit in the layout to be underneath each button
                self.layout.addWidget(self.my_line_edit, 1, self.buttons.index(button))                

                # Connect the button to an event
                self.push_button.clicked.connect(self.on_button_event)

            # Add the buttons to our layout
            self.row_Hbox.setLayout(self.layout)
            self.main_layout.addWidget(self.row_Hbox)

        # Set the layout and title
        self.setLayout(self.main_layout)
        self.setWindowTitle("Example Window")

    def on_button_event(self):

        sender = self.sender()
        print sender.objectName() + ' was pressed'

        # Get the text from the text line edit linked with the button
        self.line_edit_name = sender.objectName() + "_TextEdit"
        self.line_edit = self.findChild(QtGui.QLineEdit, self.line_edit_name)
        print self.line_edit
        print self.line_edit.text()

# Call our dialog   
dialog = Dialog()
dialog.show()

Tooling around- Maya Photoshop Bridge

My current little pet project to bridge Photoshop and Maya, from within the Maya environment.

The tool is in very early development stage, but currently supports project configuration and opening a selected model's source PSD from within the Maya environment.

Other neat time savers:

  • Exporting a selected model's UV map and using it to create a new PSD.
  • Calling Photoshop to export all of a model's maps from their source PSD. 
  • Renaming shaders based on their diffuse texture name.
  • linking associated known map types (eg- specular, gloss and normal) 
The tool is written entirely in Python/Pymel and uses the win32com module as a bridge. 

Various options to clean up scenes. 

The current texture tools. More to come!

Project options.


Sliced Py- Cheat sheet for Python Slicing

I'm constantly using slicing in my Python scripts, but I am also constantly scratching my head about exactly where to put the little colon to get the slice that I'm after.

After consulting the stackoverflow gurus I wrote a little cheat sheet using a string as an example.  


foo = "Monty Python's Flying Circus"

#Print the middle of foo- slice from the 7th-1 character end at the 15th-1 character
print foo[6:14]

#Use a slice to get the first six characters
print foo[:6]
#>>> Monty

#Slice after the first six characters of foo
print foo[6:]
#>>> Python's Flying Circus

#Print everthing in foo
print foo[:]
#>>> Monty Python's Flying Circus

#Every second character
print foo[0:27:2]
#>>> MnyPto' ligCru

#The last character of foo
print foo[-1]
#>>> s

#The last two characters of foo
print foo[-2:]
#>>> us

#Everything but the last seven charachers of foo
print foo[:-7]
#>>> Monty Python's Flying