Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Thursday, March 1, 2012

Perforce Python API Basics

Many Python users working with Perforce believe that calling out to "p4.exe" with subprocess is the only method available. Perforce actually maintains free, native API packages for several languages, including Python. The Perforce Python API is fast, fully-featured and easy to work with. It lets you interact with Perforce in a familiar Python manner, without having to capture and parse command-line output. Parsing output is one of my least favorite things to do, and I doubt I'm alone there.

Here is a dead-simple example, showing how to use the Perforce Python API to sync all files in a certain depot folder.

# Sync contents of a folder
import P4

p4_api = P4.P4( )
p4_api.connect( )

results = p4_api.run_sync( '//project_x/...' )

p4_api.disconnect( )
Some notes on connections... You'll notice above I first "connected" before issuing any commands with the API. Typically you do this once in your tool/script, run any Perforce commands you need, then disconnect when you're finished or the tool closes. It will also disconnect when the API object falls out of scope and gets destroyed. There's no need to open and close the connection all the time.

You can also use the "with" statement to easily manage the connection, automatically disconnecting when that block of code is completed:
with p4_api.connect( ):
   # connected here
   results = p4_api.run_sync( '//project_x/...' )
# disconnected here
Going back to the top example... as written it will simply use the default Perforce port, client and user. If you want to explicitly set this and not use the default, call the "set_env" function prior to your connect call (new in version 2011.1):
p4_api.set_env = ( 'P4CLIENT', 'my_workspace' )
Next, take a look at the "run_sync" command we issued. One cool thing about the Perforce Python API is that the general syntax for everything is "<api_object>.run_<command>( args )", where "command" is literally the command string you would pass to p4.exe when using the command-line interface. Examples: "run_sync", "run_edit", "run_add", "run_fstat", etc. If you know how to use Python from the command-line you already know how to use the Python API.

Above you'll see I captured the return value of our sync as "results". Calls like this all return a single list of dictionaries, one dict for each file the operation was run on. In the sync example above, it only has to update two files in my workspace, so the results object returned looks like this:
[
   {
      'totalFileSize': '5299712',
      'rev': '319',
      'totalFileCount': '2',
      'clientFile': 'D:\\projects\\project_x\\stuff.dll',
      'fileSize': '4865024',
      'action': 'updated',
      'depotFile': '//project_x/stuff.dll',
      'change': '969310'
   },

   {
      'action': 'updated',
      'clientFile': 'D:\\projects\\project_x\\foo.txt',
      'rev': '134',
      'depotFile': '//project_x/foo.txt',
      'fileSize': '434688'
   }
]
Looking at the second dictionary at the bottom, you'll notice several keys indicating data from the sync operation for that file, including the action ("updated", "added", etc.), both the client and depot paths to the file, and its new revision number.

For some operations the first dictionary returned contains some extra keys related to the overall operation, such as the total number of files acted on, and their total sizes on disk.

These returned results are full of any data you need to present friendly messages to your users. Being in simple dictionary form means they're flexible and easy to work with.

Sunday, January 29, 2012

Old is the new new

If you're seeing old posts of mine on the RSS feed, I apologize. The code syntax highlighter I use stopped working and I had to modify several posts to get it working again. For unknown reasons that causes the feed to treat them as new.

If anyone else uses Blogger and knows how to avoid that, please enlighten me.

Monday, January 9, 2012

GDC 2012 Tech Artist Boot Camp Announcement

I'm organizing and MCing the Tech Artist Boot Camp at GDC 2012 in March. The TABC is an all-day Tutorial-format session on Tuesday, March 6, from 10 AM-6 PM.

Below is the session description and list of speakers & topics. We also plan to do a group panel-style Q&A session at the end of the day.

I spoke at the TABC last year, and it was an excellent way to reach out to and share with other industry TAs.  I hope to see you there!

Description
Technical Art is evolving rapidly. In many studios TAs play key roles in developing efficient tools pipelines and ensuring art content is visually striking and optimized for performance. TAs bridge content and engineering helping make both more successful. However, many studios have still not fully embraced the TA role. Their TAs are smart and eager to make an impact, but are not sure how to best prove their value, and be given key roles in development.

A group of experienced, respected technical artists from across the industry would like to invite you to sit with them for a day and learn how to be a more effective TA. Focus on the tools and skills TAs can use to demonstrate their value, and further integrate technical art into their studios' pipelines and cultures. Find the worst development problems at your studio and show them what a TA can do!

Intended Audience
This all-day tutorial is for technical artists and other developers of any experience level. A light focus will be placed on techniques and skills useful to TAs at studios with little-to-no tech art integration and culture.

Takeaway
At the end of this all-day event, attendees will understand key techniques to help them take technical art to the next level at their studios. Learn how to effectively work within constraints, integrate into your teams, communicate with other disciplines, design better code and pipelines, and master new shader techniques.

Speakers & Topics ---

Welcome, Introduction
Adam Pletcher, Technical Art Director, Volition, Inc.

You Have to Start Somewhere... Defining the Tech Art Role and Building Their Team
Arthur Shek, Technical Art Director, Microsoft Studios (Turn 10)
This session will go over the trials of moving from a job in film/animation to a studio with a minimal Tech Art presence and the ensuing panic of change. The Tech Art role has a soft definition and differs at every studio – our common quality is that we are problem solvers, and to problem solve, you must have experience, wide knowledge and the ability to scramble on your feet. At times, what we may feel pressure to know can be overwhelming. Relax - you have to start somewhere.

Better, Faster Stronger: Teaching Tech Artists to Build Technology
Rob Galanakis, Lead Technical Artist, CCP Games
The success of Tech Art has caused a complexity of projects and tools for which our traditional skill set is under-equipped. Tech Artists are now building technology, not just scripts, and our essential growth must be as a cohesive team, not just trained individuals. In this session, attendees will learn how to apply a few key practices of professional software development, such as code review, support processes, and collaborative coding, to the unique environment of Tech Art.

Build it on Stone: Best Practices for Developing A Tech Art Infrastructure
Seth Gibson, Senior Technical Artist, Crystal Dynamics
In this session we present a set of best practices for building Tech Art tools and pipelines in a stable, maintainable, and scalable fashion through the establishment of a solid tools development infrastructure geared toward the specific needs of Technical Artists.

Joining the Dark Side: How Embedded Tech Artists Can Unite Artists and Programmers
Ben Cloward, Senior Technical Artist, Bioware Austin
Technical Artists can be a powerful force to unify teams and ensure that productions run smoothly. In this case study, I’ll show how the simple act of moving two technical artists into the programmers’ working area helped to improve the relationship between art and programming and resulted in a better-looking, more efficient game.

Lessons in Tool Development
Jason Hayes, Technical Art Director, Volition, Inc.
All too often, the importance of planning the architecture of tools and pipelines in game development is overlooked. In most cases, project pressures often give us the false impression that we don’t have time to plan, or worse, we actually save time by “just getting it done”. Nothing could be further from the truth. This session explains why up front planning is important, when to recognize over-engineering and offers architectural design principles for effective tools development-- such as program organization, data design, scalability and user interface design. Internal tools developed at Volition will be used to demonstrate these topics.

Shady Situations: Real-time Rendering Tips & Techniques
Wes Grandmont III, Senior Technical Art Director, Microsoft Studios (343 Industries)
This tutorial session will cover a variety of techniques that can be used individually or combined to solve a variety of game related real-time shading problems. It will begin with a brief overview of the current generation GPU pipeline, followed by some HLSL basics. The rest of the talk will dive into a range of techniques with a complete overview of how each one is implemented.

Unusual UVs: Illuminating Night Windows in Saints Row The Third
Will Smith, Technical Artist, Volition, Inc.
This session presents a holistic case study involving HLSL shader development. Included is not only the problem and its resolution, but perhaps more importantly, an insight into the Technical Artist’s problem-solving mindset throughout its resolution.

Group Q&A, Conclusion

Monday, December 5, 2011

py2exe, Windows 7 & Vista

I don't use py2exe very often, but it can be a useful tool for environments that may not have an existing Python installation.

I recently used py2exe on my Windows 7 PC to build a small tasktray tool. The resulting executable ran fine on my PC (doesn't it always?), but threw an exception on any Vista PC it was run on.

File "win32com\__init__.pyo", line 5, in File "win32api.pyo", line 12, in 
File "win32api.pyo", line 10, in __load
ImportError: DLL load failed: The specified module could not be found.
After more online searching than I'd like to admit, I found a post that said py2exe may be including W7-specific DLLs, when instead it should be leaving those out, forcing Vista to go find its native builds of those DLLs.

I was able to fix the problem by adding two DLLs to the "dll_excludes" list in my py2exe setup script:
options = {
   "bundle_files": 3,
   "compressed": 1,
   "optimize": 1,
   "excludes": excludes,
   "packages": packages,
   'dll_excludes': [ 'mswsock.dll', 'powrprof.dll' ]
}
The tool now runs on both Vista and Windows 7.

Sunday, February 27, 2011

GDC 2011 Wrap-up, download

The Technical Artist Boot Camp went really well today. I personally learned a great deal from both the attendees and fellow presenters. Thanks to everyone that turned out, and thanks for all the great questions!

Here is the sample script file I promised in my talk on databases. It's a simple, working illustration of how to use SQLAlchemy ORM to map a Python class to a database table. Please let me know if you have any questions.

GDC2011_AdamPletcher_PythonSamples.zip (2 KB)

Sunday, February 20, 2011

GDC 2011 - Technical Artist Boot Camp


Game Developers Conference 2011 is a week away. I'm co-presenting at the Technical Artist Boot Camp, a special all-day Tutorial session on Tuesday March 1.

My portion of the Boot Camp is called "Embrace the Database." Here's my summary:

They may have a mystical aura about them, but databases are far easier to use than you may think. They can be a Technical Artist’s greatest ally in game development, powering your most important tools, gathering usage and error data you’ve never had access to, enabling new workflows and revealing hidden weaknesses (and strengths) in your content pipelines.

This session will also explore how to use Python to unlock the power of databases at your studio. We will look at what databases do best, using practical examples to get you started. We'll discover how Object-Relational Mapping lets you interact with a database in a simple manner that any Python user will already understand
.

I'm really excited to be presenting alongside so many talented TAs from our industry. It's going to be packed with great ideas and techniques. If you plan to be in SF for GDC, definitely stop by for our session.

Saturday, September 25, 2010

Using Sharepoint Lists with Python

Continuing my tradition of shoving Python into new and unusual places, I recently worked out how to use Python to post items to a Sharepoint List.

We use Sharepoint for some of our intranet needs, and I was experimenting with error-reporting workflows. While I ultimately didn't stick with Sharepoint for this purpose, the Python code worked fine and I wanted to share.

First, a few things to note. Sharepoint uses several SOAP-based webservices as a means of exposing functionality to other tools/languages. One of these is for manipulating Lists, which are Sharepoint's basic storehouse for items that hold arbitrary columns/fields of data.

I was unfamiliar with SOAP prior to this, and ended up using the "suds" extension for Python to help with the formatting. Suds is necessary to run the examples below, and can be downloaded on the Suds SourceForge page.

The "sharepoint" module I'm posting below has basic usage like this:

import sharepoint

item_data = {
   'item_id'   : 32,
   'Log Time'  : datetime.datetime.now( ),
   'Message'   : 'Sharepoint is rather obtuse',
   'User Name' : 'adam.pletcher',
}

sp_list = sharepoint.Sharepoint_List( 'http://sp_server/some_site/', 'Sharepoint List Name', 'domain_name', 'user_name', 'rot13_encoded_password' )
result = sp_list.add_item( item_data )
Walking through the above, first we create a simple dictionary of key/value pairs. The keys are the Sharepoint names for the columns in your list, and the values are the values you wish to submit for your new list item. Then, we construct a Sharepoint_List instance, passing it the site URL, List Name, domain, user and password. Finally, call our add_item method on that object, passing it the data dictionary we made.

The add_item method accepts most data types, including Python datetime objects, as shown. You can easily extend add_item to do something more elaborate.

One thing to watch out for is the list's column/field names used in your dictionary. These must be strings matching the true Sharepoint names of your fields. Even if you rename a column later, it's true name will not change once the list has been created. The only flexibility I currently provide in add_item is the ability to leave spaces in the field names.

I experimented with NTLM as a means of using the current user's Windows Authentication instead of requiring the username/pass each time. I was not successful in that, however... if anyone has success there I'd love to hear about it. As it stands, the Sharepoint_List class requires the username and a ROT-13 encoded password. If you've never encoded anything, here's how to do that from a Python prompt:
>>> 'my_password'.encode( 'rot-13' )
'zl_cnffjbeq'
The complete "sharepoint" module described here can be downloaded below. Adding an item isn't the only thing the Sharepoint lists webservice offers. You can use the framework in this module to do more including edits, deletes and other actions.

Python_Sharepoint.zip (3 KB)

Sunday, December 6, 2009

MaxScript DotNet Sockets with Python

I create and work with several Python tools that manipulate scenes in 3ds Max. These are usually floating dialogs linked to the main 3ds Max window that send MaxScript commands via COM. This works well until I need the tool's UI to update when something happens in 3ds Max. Like refresh an object list when the selection in Max changes.

You would think doing a COM connection the other way would work. However, since Python-registered COM servers run separately in their own instance of the interpreter, there's no native connection to the original tool.

Nathaniel Albright, a fellow TA at Volition, recently created a Python COM server that communicated to his Python tool via TCP/IP socket. So it went 3ds Max -> Python COM server -> TCP/IP -> Python tool. This works well, but I wondered if the DotNet facilities in MaxScript offered a direct way to use sockets.

I had yet to touch DotNet in MaxScript, so this seemed like a good opportunity to learn a few things. After a lot of searching online I only turned up a few scraps of info, no complete recipe. However, I did find enough to get MXS DotNet sockets working, and assemble a comprehensive example.

I created a little Python tool that displays the names of all selected objects in the 3ds Max scene. As the scene selection changes, the list of names automatically updates. I won't go over all the code in this post, but the full working example tool is included in the zipfile below.

There's three main points of interest in the example:

1. Using DotNet in MaxScript to communicate via TCP/IP socket
2. Listening on a socket in a background thread in Python
3. Creating and posting custom wxPython events

The MaxScript Client

I made a MaxScript struct called "mxs_socket". The code follows, and also included in the zipfile below.

struct mxs_socket (
   ip_address = "127.0.0.1", -- "localhost" also valid
   port       = 2323,        -- default port

   -- <dotnet>connect <string>ip_string <int>port_int
   --
   -- Description:
   -- Takes IP address, port and connects to socket listener at that
   -- address
   fn connect ip_string port_int = (
      socket = dotNetObject "System.Net.Sockets.Socket" ( dotnetclass "System.Net.Sockets.AddressFamily" ).InterNetwork ( dotnetclass "System.Net.Sockets.SocketType" ).Stream ( dotnetclass "System.Net.Sockets.ProtocolType" ).Tcp
      socket.Connect ip_string port_int

      socket   -- return
   ),

   -- <int>send <string>data
   --
   -- Description:
   -- Converts a string (or any object that can be converted
   -- to a string) to dotnet ASCII-encoded byte sequence and
   -- sends it via socket. Uses ip_address and port defined
   -- in struct above, or set by client.
   -- Returns integer of how many bytes were sent.
   fn send data = (
      -- Convert string to bytes
      ascii_encoder = dotNetObject "System.Text.ASCIIEncoding"
      bytes = ascii_encoder.GetBytes ( data as string )

      -- Connect, send bytes, then close
      socket = connect ip_address port
      -- result is # of bytes sent
      result = socket.Send bytes
      socket.Close()

      result  -- return # of bytes sent
   )
)
Using this, I can send bytes to any socket listener on port 5432 by doing the following:
socket = mxs_socket port:5432
socket.send "Hello, World!"
The connect method was pretty simple in the end. The only twist turned out to be converting the socket integer into a DotNet socket object.

The send method converts the string into an ASCII-encoded DotNet bytes object, connects to the listener, sends the bytes, then closes the connection. The value returned is the number of bytes sent.

The last lines of the above code sets up a MaxScript callback that fires when the object selection changes in the scene. That uses mxs_socket to send a string containing the names of all the selected objects to any tool that's listening on that port.

Now I just need to make my Python tool listen.

The Python Server

My Python server/listener (also in the zipfile below) is a typical wxPython frame, but with two added qualities... It uses a background thread to listen on a socket, and posts a custom wx.Event when data is received. I had never used either of these techniques before, but it was fun getting it working.

Since a typical wxPython app sits in its main loop waiting for user input, I created a Socket_Listen_Thread class, a subclass of threading.Thread. This does the listening in a background thread while the main UI thread waits on the user. The run method here does the real work:
def run( self ):
   self.running = True

   while ( self.running ):
      # Starting server...
      # Listen for connection.  We're in non-blocking mode so it can
      # check for the signal to shut down from the main thread.
      try:
         client_socket, clientaddr = self.socket.accept( )
         data_received = True
      except socket.error:
         data_received = False

      if ( data_received ):
         # Set new client socket to block.  Otherwise it will
         # inherit the non-blocking mode of the server socket.
         client_socket.setblocking( True )

         # Connection found, read its data then close
         data = client_socket.recv( self.buffer_size )
         client_socket.close( )

         # Create wx event and post it to our app window
         event = self.event_class( data = data )
         wx.PostEvent( self.window, event )
This listening thread runs quietly in the background until it receives data. At that point I need a way to break into the main UI thread. There's other ways to do this, but using a custom wx.Event seemed to be the best fit here.

First, when the wx.Frame is opened, I create a custom wxPython event.
(Max_Update_Event, EVT_3DS_MAX_UPDATE) = wx.lib.newevent.NewEvent()
Calling NewEvent returns both a new Event class and an object to bind the event handler to. I pass the event class to the listener thread, bind an event handler to it, and that's all.

When data comes in over that TCP/IP port from our MaxScript tool, the listening thread receives it and posts our custom event to the main wx.Frame. That in turn fires the event handler to update the UI.

My example MaxScript client and Python listener described above can be found in the following ZIP file. Drop me a line if you do something useful with them, I'd love to hear about it.

MaxScript_DotNet_Sockets_Python.zip (4 KB)

Thanks to Nate Albright and everyone contributing to the "dotNet + MXS" and "Python + MXS" threads on the CGSociety forums. Those long-running threads have been very inspiring, and contain several tips that were key in getting the MaxScript DotNet socket stuff hammered out.

Thursday, August 6, 2009

Regular Expressions Coaching

Try as I might, I've never had instant recall on the details of regular expressions. For whatever reason (infrequent use, old age) the syntax just slides out of my head and onto the floor mere minutes after using it.

Awhile back a co-worker introduced me to The Regex Coach, and I've used it regularly ever since. Paste in a snippet of text, type a regex pattern, and it highlights matches in the text as you type. Super easy way to (re)learn or explore regex.


It's targeted at Perl-style regex, which for me has proved completely compatible with Python's re module. If you like it, don't forget to donate.

A bonus tip for the regex-challenged like myself...
If you memorize only one thing: (.*)

Sunday, May 10, 2009

What we do with Python

There's a great thread going at tech-artists.org called What do you do with Python? The other day I posted a few of the things our studio has done with Python in the past year or two...

  • Measure start/stop times of various processes, logging data to SQL database. For instance, how long it takes 3ds Max to start up, so we can spot bad trends when new tools are published.
  • System for logging errors and tools usage data to central database, with optional emailing of errors/callstack. Works for Python tools as well as MaxScript (via COM).
  • A non-linear GUI editor for an otherwise complex/table-driven cutscene pipeline.
  • Build graphical user interfaces (generally with wxPython) that integrate with in-house and off-the-shelf C applications. For example, floating Python dialogs that link to app windows as children, or as docking task panes.
  • Tool that communicates with game C code (via socket) running on consoles to do in-game realtime lighting.
  • Embed Python interpreter into editor framework for next-gen development tools. This is the one I spend lots of time on these days... works like MaxScript in 3ds Max, but for our custom editors.
  • One Exporter that writes out various data files from 3ds Max, Photoshop, and imports/categorizes them in our asset system.
  • Logs me into Outlook's webmail without manually entering my creds every time. I guess that was a home project. :)
  • At 3ds Max startup, scan folders for MaxScripts, building a MacroScript .mcr file for all of them.
  • At 3ds Max startup, builds list of texture map folders for a given project, sorts them by user's discipline and adds them to Max's bitmap paths list.
  • Profile rendering performance of art assets recently submitted to Perforce, recording data to SQL database.
  • Searches web-based bug tracker database for entries assigned to you and displays data in a Vista Sidebar gadget.
  • Creates makefiles with dependencies, for distributed build processes in Incredibuild/XGE.
  • Wavelet transform calculations for content-based image comparison tools. For finding textures that are too similar, or comparing rendered output of one shader vs. another.
  • Takes zipcode or lat/long as input, gathers geo-survey data from various online sources and creates the road/terrain network inside our world editor.
  • Tons of data mining uses. Like searching various exported XML files for instances of X material, mesh, etc. in game world.
  • Tool for bridging various apps with COM interfaces in other tools. Like firing MaxScripts in 3ds Max from Ultraedit, or taking current Python script in Wing and running it in our editor's embedded interpreter.
  • Custom scripts for integrating our tools/processes into Wing (the Python IDE we use).

P.S. Call your mom today.

Tuesday, March 17, 2009

Python cheat sheets

I'm out of hibernation, time I posted something.

As much as I use Python these days, there's a few things I find myself looking up regularly. At one point I just made a small crib sheet and stuck it to my monitor. I have examples on it for list comprehensions, filter, and map.

List Comprehensions
These are useful for creating modified lists from existing data without a lot of fuss. They aren't all that hard to remember, but the syntax was a bit alien to me for awhile. They're basically an expression followed by a for clause.

The below example takes an existing list, my_list and builds a new list with only the elements that are greater than 2. In this case the result is assigned right back to my_list.

my_list = [x for x in my_list if x > 2]
Filter
Using filter is a powerful way to remove undesired elements from a list. You pass a function as the first argument, which generally returns True/False based on some criteria. The second argument is the sequence to be filtered (or any iterable object). Only the elements that return True when passed to that function will remain in the newly returned list.

Filtering is often done with a lambda as the first function argument. A lambda is a one-off function that's defined and used in the same place. Since it's only used once, it doesn't need a name. It's so common to see filter and lambda together, the fact they were seperate didn't occur to me when I was learning the language.

In this example, we have a list of filenames, my_files, and we want to remove any that aren't Python scripts, ending in '.py'.
my_files = filter(lambda f: f.endswith('.py'), my_files)
That is the shorter equivalent of:
def is_py_filename(filename):
   return filename.endswith('.py')

my_files = filter(is_py_filename, my_files)
With filter, passing None as the first argument instead of a function automatically removes any elements that don't evaluate to True. That includes integers or floats that are zero, as well as occurrences of False or None.

my_files = filter(None, my_files)
Map
Mapped functions let you apply a function to every element in a sequence.
def add_ten(x):
   return x+10

result = map(add_ten, [1,2,3,4,5])
The value of result would be [11, 12, 13, 14, 15]. Of course you could also use a lambda here, too:
result = map(lambda x: x+10, [1,2,3,4,5])
So what's on your cheat sheet?

Friday, August 29, 2008

Read-only Windows files with Python

How do you use Python to get or change read-only/writeable access on files in Windows? The Python docs don't answer this in a direct manner. Here's one option using only the standard library.

import os, stat
myFile = r'C:\stuff\grail.txt'

fileAtt = os.stat(myFile)[0]
if (not fileAtt & stat.S_IWRITE):
   # File is read-only, so make it writeable
   os.chmod(myFile, stat.S_IWRITE)
else:
   # File is writeable, so make it read-only
   os.chmod(myFile, stat.S_IREAD)
You may prefer the pywin32 extensions for this sort of thing...
import win32api, win32con
myFile = r'C:\stuff\grail.txt'

fileAtt = win32api.GetFileAttributes(myFile)
if (fileAtt & win32con.FILE_ATTRIBUTE_READONLY):
   # File is read-only, so make it writeable
   win32api.SetFileAttributes(myFile, ~win32con.FILE_ATTRIBUTE_READONLY)
else:
   # File is writeable, so make it read-only
   win32api.SetFileAttributes(myFile, win32con.FILE_ATTRIBUTE_READONLY)
Or, more concisely with win32:
roAtt = win32api.GetFileAttributes(myFile) & win32con.FILE_ATTRIBUTE_READONLY
win32api.SetFileAttributes(myFile, ~roAtt)
Using win32 you can also set other Windows file attributes (unlike os.chmod), but read/write is usually all I care about.

Tuesday, August 5, 2008

Photoshop scripting with Python

Photoshop natively supports scripting with AppleScript, JavaScript and VBScript. While Python is notably absent from that list, it can still be used to automate nearly anything in Photoshop. This is thanks to the extensive COM interface Photoshop provides.

The methods here are similar to those used in my GDC 2008 Python lecture, about driving 3ds Max via Python. You start by dispatching the Photoshop COM server, using Python as the client:

import win32com.client
psApp = win32com.client.Dispatch("Photoshop.Application")
This connects to your already-opened Photoshop session, or opens one if none are running. The root COM object is then assigned to psApp, and you're ready to do some cool stuff. Here's a quick example:
psApp.Open(r"D:\temp\blah.psd")         # Opens a PSD file
doc = psApp.Application.ActiveDocument  # Get active document object
layer = doc.ArtLayers[0]                # Get the bottom-most layer
layer.AdjustBrightnessContrast(20,-15)  # Bright +20, Contrast -15
doc.Save()                              # Save the modified PSD
Here's a more complex example. This script recursively scans a folder for PSD files, exporting various textures contained inside. One PSD can have specifically-named Layer Groups, each of which is written to a separate PNG file with a specific suffix. If a Group contains several layers, they're flattened when exported, allowing you to keep all your layered effects intact in the PSD.

In the example below, a group named "diffuse" is exported as "psdname_D.png", the "normal" group as "psdname_N.png", and so on. The exportType dictionary determines the name/suffix pairs.
# Recursively scans a folder (psdRoot) for Photoshop PSD files.
# For each, exports various 24-bit PNG textures based on layer
# groups found in the PSD.
# Requires the Win32 Extensions:
# http://python.net/crew/mhammond/win32/

import win32com.client
import os

# Change to match your root folder
psdRoot = r'C:\ArtFiles\PSD'

# Map of layer group names and the suffixes to use when exporting
exportTypes = {'diffuse':'_D', 'normal':'_N', 'specular':'_S'}

if (__name__ == '__main__'):
   # COM dispatch for Photoshop
   psApp = win32com.client.Dispatch('Photoshop.Application')

   # Photoshop actually exposes several different COM interfaces,
   # including one specifically for classes defining export options.
   options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb')
   options.Format = 13   # PNG
   options.PNG8 = False  # Sets it to PNG-24 bit

   # Get all PSDs under root dir
   psdFiles = []

   for root, dir, files in os.walk(psdRoot):
      for thisFile in files:
         if (thisFile.lower().endswith('.psd')):
            fullFilename = os.path.join(root, thisFile)
            psdFiles.append(fullFilename)

   # Loop through PSDs we found
   for psdFile in psdFiles:
      doc = psApp.Open(psdFile)
      layerSets = doc.LayerSets

      if (len(layerSets) > 0):
         # First hide all root-level layers
         for layer in doc.Layers:
            layer.Visible = False
         # ... and layerSets
         for layerSet in layerSets:
            layerSet.Visible = False
           
         # Loop through each LayerSet (aka Group)
         for layerSet in layerSets:
            lsName = layerSet.Name.lower()

            if (lsName in exportTypes):
               layerSet.Visible = True  # make visible again

               # Make our export filename
               pngFile = os.path.splitext(psdFile)[0] + exportTypes[lsName] + '.png'

               # If PNG exists but older than PSD, delete it.
               if (os.path.exists(pngFile)):
                  psdTime = os.stat(psdFile)[8]
                  pngTime = os.stat(pngFile)[8]
        
                  if (psdTime > pngTime):
                     os.remove(pngFile)

               # Export PNG for this layer Group
               if (not os.path.exists(pngFile)):
                  doc = psApp.Open(psdFile)
                  doc.Export(ExportIn=pngFile, ExportAs=2, Options=options)
                  print 'exporting:', pngFile
               else:
                  print 'skipping newer file:', psdFile
                 
               # Make LayerSet invisible again
               layerSet.Visible = False

         # Close PSD without saving
         doc.Close(2)
It only exports when the PNG is missing or older than the PSD. This makes it good for running a batch texture export on your project's entire texture tree.

Here is a ZIP containing the above script and a sample PSD file to try it on: exportTextureLayers.zip (143 KB)

I imagine you can do all of the above with the native Photoshop scripting. I just think it's cool being able to use Python instead of rooting through a language I'm less familiar with. Dinosaurs were roaming the earth the last time I tried anything in VB.

If you dig this, I'd recommend reading the Photoshop CS5 Scripting Guide and Photoshop CS5 VBScript Reference found in the Adobe Photoshop Developer Center. While the above wasn't VBScript, the COM interface we used is nearly identical.

Wednesday, July 16, 2008

Checksums in 3ds Max (part 2 of 2)

In Part 1 I showed how to calculate checksums inside 3ds Max. Here's how to do something useful with them.

Any TA that's crossed paths with 3ds Max can tell you it doesn't do the best job of managing scene materials. Due to scene object merges/imports and other typical operations, it's common for a given material to be copied several times in one Max scene. Meaning, it's not instanced across several objects, but actually copied several times in memory. This can lead to increased memory usage and potentially inefficiencies in your game engine (depending how your exporter deals with this).

What's worse, you usually can't rely on similar material names to find duplicates by hand. To do a thorough search with MaxScript, you would need to loop through every material in the scene and compare every property in it to every other material's property. This would be a slow process in C, and a complete horror-show with MaxScript.

Enough grim talk. Here's a walkthrough of a MaxScript that uses checksums to make short work of this. To summarize, the script loops through the materials in your scene, creating a checksum for each as it goes. It uses that checksum to do a quick compare on previous material checksums it found, to see if they're actually property-identical. If it finds a dupe, that object is given the original material instead, effectively deleting the duplicated material.

The script is divided into three functions and a short bit of main code.

getChecksum() is the first function, taken from my previous post. It calls the Python COM object we registered, which returns a checksum to the MaxScript. If you can't (or don't want to) set-up the COM object, you can use the MaxScript implementation I listed in that blog post instead... it's just less robust than the MD5 checksums used by the Python method.

Next is the getPropsString() and getMaterialChecksum() functions:

------------------------------------------------------------
-- (str)getPropsString (material)mat
--
-- Description:
-- Builds a string representing the property names/values
-- of the supplied Max material.
------------------------------------------------------------
fn getPropsString mat = (
   myStr = "" as stringStream
   if (mat == undefined) then (
      format "undefined" to:myStr
   ) else (
      -- Start our string w/the classname
      format (classOf mat as string) to:myStr
      if (classof mat == ArrayParameter) then (
         -- Array, so recursively add strings for each element
         for element in mat do (
            format (getPropsString element) to:myStr
         )
      ) else (
         -- Not an array, so see if it has properties
         propNames = undefined
         try (
            propNames = getPropNames mat
         ) catch ()
         if (classOf mat == BitMap) then (
            try (  -- Add bitmap's filename
               format mat.filename to:myStr
            ) catch ()
         ) else if (propNames == undefined) then (
            format (mat as string) to:myStr
         ) else (
            format (propNames as string) to:myStr
            -- Loop through properties, adding their names
            -- and values to our string to be checksummed
            for i in 1 to propNames.count do (
               format (i as string) to:myStr
               p = propNames[i]
               val = getProperty mat p
               format (i as string) to:myStr
               format (getPropsString val) to:myStr
            )
         )
      )
   )
   (myStr as string)
)

------------------------------------------------------------
-- (str)getMaterialChecksum (material)mat
--
-- Description:
-- Takes a Max material (or multi-sub material) and
-- calculates a checksum value from it, for use as a
-- hashtable key, or whatever you like.
------------------------------------------------------------
fn getMaterialChecksum mat = (
   str = ""
   if (classof mat == Multimaterial) then (
      for id in mat.materialIDList do (
         -- Add material IDs as factors
         str += id as string
      )
      for subMat in mat.materialList do (
         -- Get string representing each submaterial
         str += (getPropsString subMat)
      )
      ) else (
         -- Get string representing this material
         str += getPropsString mat
      )
   -- Add string length as a factor
   str += str.count as string

   -- Get checksum from our base string
   -- 99991 = largest prime number under 10k
   (getChecksum str)
)
The above functions work together to generate a string of data representing the supplied 3ds Max material (or Multi-Sub material). Once it has that string, it's passed to getChecksum().

The main code block loops through the entire 3ds Max scene, doing the above for every material found on geometry objects:
----------
-- MAIN
----------
-- Set up a few things first.

timeStart = timestamp()  -- Time we started process
removedCount = 0  -- Counters for printing info below
uniqueCount = 0

-- Array of two synced arrays, first with the material
-- checksums, second with materials themselves.
-- Basically a poor-man's hashtable.
csMatArr = #(#(), #())

format "Scanning scene materials...\n"

-- Loop through all geometry
for obj in geometry do (
   mat = obj.material

   alreadyDone = (findItem csMatArr[2] mat) != 0

   if (not alreadyDone) and (mat != undefined) then (
      -- First get this material's checksum
      csum = getMaterialChecksum mat

      idx = findItem csMatArr[1] csum

      if (idx != 0) then (
         -- Dupe material found, so remove it by
         -- assigning the first mat to this object
         format "Replacing material '%' with '%'\n" mat.name csMatArr[2][idx].name
         obj.material = csMatArr[2][idx]
         removedCount += 1
      ) else (
         -- New checksum, so add it to our table,
         -- along with the material itself.
         append csMatArr[1] csum
         append csMatArr[2] mat
         uniqueCount += 1
      )
   )
)

gc()  -- Remind Max to take out the trash

-- Done, print some results
format "-- DONE in % secs --\n" ((timestamp() - timeStart) / 1000.0)
format "Old material count = %\n" (uniqueCount + removedCount)
format "New material count = %\n" uniqueCount
format "Duplicates removed = %\n" removedCount
That's it. At the end a summary is printed to the MaxScript Listener.

In the Max scene I was working with today this script cut the root material count from 533 to 261. That's 51% fewer materials! It also reduced the file load time from 136 seconds to 102 seconds.

You can download the complete script above here: RemoveDupeMaterials.zip (4 KB)
It includes the Python script to register the COM server, and the alternate MaxScript checksum method.

Update 7/25/08: I modified getPropsString() to better handle bitmap values, and generally run faster. The ZIP file above is updated as well. Thanks to MoonDoggie/Colin on CGTalk for the feedback!

Wednesday, June 18, 2008

Checksums in 3ds Max (part 1 of 2)

In my Calling Python from MaxScript post I mentioned the usefulness of checksums in Tech Art work. I was hoping to elaborate on that a bit.

In short, a checksum is a number computed from a larger piece of data. The checksum is (ideally) guaranteed to be unique for that data. Let's say that data is this string: "Tech Art is A-#1 Supar", and the checksum you've computed is "30532". If any character in that string changes, the computed checksum for it will be different, like "18835" or "1335"... basically anything other than "30532".

Checksums are most useful in cases where you need to compare two sets of data to see if they differ, but don't care where or how they differ. If you have a short number that uniquely identifies a huge piece of data, you can compare it to other data sets much faster/easier than comparing every element of the original data. If you're hip to how slow n-squared searches can be (especially in languages like MEL or MaxScript), this is a classic method for avoiding them.

Here's a MaxScript function that takes a string of any length and returns a checksum for it.

------------------------------------------------------------
-- (str)getChecksum (string)val (int)size:256
--
-- Description:
-- Calculates simple checksum value from supplied string (or
-- any value convertable to a string).  Default size is 256,
-- but can be changed with the optional "size" parameter.
------------------------------------------------------------
fn getChecksum val size:256 = (
   if (classof val != String) then (
      try (
         val = val as String
      ) catch (
         return false
      )
   )
   alphaKey = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 !@#$%^&*()[]\\{};':\",./<>?"
   total = 0
   for i in 1 to val.count do (
      thisVal = findString alphaKey val[i]
      if (thisVal == undefined) then (
         thisVal = 0
      )
      -- Multiply the alphanumeric value by its position in
      -- the input string, add to running total
      total += (thisVal * i)
   )
      -- make sure divisor is smaller than dividend
   while (total < size) do (
      total = total * 2
   )

   -- Return final checksum value
   checksum = mod total size
   return (checksum as string)
)
We used the above function in several of the Saints Row tools, primarily to help remove identical materials in 3ds Max scenes. It's very unscientific, however, and can generate collisions in rare cases (two different input strings that generate the same output checksum) **.

If you don't mind a little more setup, I would recommend an alternate checksum method. Python natively offers more robust checksum tools, and can be set up to be called directly from MaxScript. The steps for doing this, and the actual MD5 checksum function, are covered in my earlier blog, Calling Python from MaxScript.

Start with the Python script from that blog that defines and registers the COM server. Then you're able to use a far-shorter MaxScript function to get checksums:

fn getChecksum val = (
   comObj = createOLEObject "PythonCom.Utilities" 
   checksum = comObj.checksumMD5 val
   return checksum
)
That's it. The checksums you get from this function will create fewer collisions than the pure-MaxScript one above, and can be made to use any alternative method available in Python.

Now you know more about checksums, and how to generate them in 3ds Max. Next time (in Part 2) I'll discuss how you can use them to save memory in both Max and your game engine.

Monday, May 26, 2008

Python String Templates

The Template class, found in the standard Python string module, is extremely useful. Start with a template string containing keys you want to replace. By default keys start with "$".

>>> import string
>>> thisTmp = string.Template("The $speed $color $thing1")
>>> thisTmp.substitute(speed='quick', color='brown', thing1='fox')
'The quick brown fox'
You can also pass the substitute method a dictionary with key/value pairs for your template:
>>> strDict = {'speed':'slow', 'color':'toupe', 'thing1':'mango'}
>>> thisTmp.substitute(strDict)
'The slow toupe mango'
This makes it easy to insert variable parts in an otherwise fixed string or file. I use it all the time for generating table-based HTML reports.

Create a file like "report.html" with string keys like "$rowValue1" or "$user" in the appropriate places, and have your script read in the contents as a string Template and do the substitutions. This also allows the report layout/appearance to be altered later without touching the script code.

Tuesday, May 6, 2008

Calling Python from MaxScript

Unlike Maya, 3ds Max does not have internal support for Python. But that shouldn't stop you from calling useful Python code in your MaxScripts! Here's the basics of how to do that using COM.

COM is a Windows system that supports, among other arcane things, interprocess communication. You can use a language like Python, Visual Basic, or C to define a COM "server". This is a class or function, defined by a unique identifier (GUID) and a name. Here's some gory details on COM if you're curious.

Here's a simple COM server using Python:
Requires the Python Win32 Extensions (which no TA should be without)

# A simple Python COM server.
class PythonComUtilities:
   # These tell win32 what/how to register with COM
   _public_methods_ = ['checksumMD5']
   _reg_progid_ = 'PythonCom.Utilities'
   # Class ID must be new/unique for every server you create
   _reg_clsid_ = '{48dd4b8f-f35e-11dc-a4fd-0013029ef248}'

   def checksumMD5(self, string):
      """Creates MD5 checksum from string"""
      import hashlib
      m = hashlib.md5()
      m.update(str(string))
      return m.hexdigest()

if (__name__ == '__main__'):
   print 'Registering COM server...'
   import win32com.server.register as comReg
   comReg.UseCommandLine(PythonComUtilities)
This defines a function, checksumMD5 that takes a string as input, and returns the MD5 checksum for that string.

To register the COM server on a PC, simply run the Python script. Windows records it in registry, noting which script/application it uses.

Now that's done, another application (3ds Max, in this case) can connect to that COM server's interface and call it like any other function. Here's an example of doing that from MaxScript:
-- Connect to the COM server by name
comObj = createOLEObject "PythonCom.Utilities"
-- Call the function it exposes, with a sample string
checksum = comObj.checksumMD5 "The quick brown fox."
It's that simple. The checkum value returned for our sample string is "2e87284d245c2aae1c74fa4c50a74c77".

You might be wondering what a checksum is, or what it's good for. Stay tuned and I'll show you some slick stuff you can do with them in 3ds Max. See Checksums in 3ds Max, Part 1 and Part 2.

Python COM server example adapted from code appearing in Python Programming in Win32 by Mark Hammond and Andy Robinson... a great book for getting more out of Windows with Python.

Saturday, April 5, 2008

Logging (I'm no lumberjack, but I'm okay)

Exploring the Python standard library is fun.

Not long ago I found how great the logging module is. Use it to create a logging channel, attach different handlers to it (for logging events to a file, through email or HTTP, etc), then make one-line log events of different types.


import logging
import logging.handlers

# Sets up a basic textfile log, with formatting
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%m/%d/%y %H:%M:%S',
filename=r'C:\temp\mylog.log',
filemode='a')

# Log a few different events
logging.info('Just testing the water.')
logging.warning('Hmm, something is not right here')
logging.error("Oh no, now you're in for it")
The resulting text log:
02/14/08 22:19:03 INFO     Just testing the water.
02/14/08 22:19:03 WARNING Hmm, something is not right here
02/14/08 22:19:03 ERROR Oh no, now you're in for it
Add a few more lines and it sends you an email for any logs that are level "ERROR" or above:

email = logging.handlers.SMTPHandler('smtp.foo.com',
'script@foo.com',('techart@bar.com'),'Error Report')
email.setLevel(logging.ERROR)

logging.getLogger('').addHandler(email)
There's several other handler types as well. Very useful!

Saturday, March 1, 2008

On Farming

About a year ago I took a look at Pyro, aka Python Remote Objects. It's an extension package that lets you define code on one machine, and essentially run that code remotely on another machine.

The cool bit is how it hides all the socket and TCP/IP baloney under the hood. It handles connects, disconnects, name lookups, all that. Harnessing something powerful and not needing to understand the gory details is always something I appreciate daily as a Tech Artist. Part of me wants to learn all the inner workings someday. But seriously, I'll probably live a full life and never know.

Anyway, my first Pyro tool was a simple client/server batching utility. A server script would keep track of the slave machines connected and a little job queue, and assign the jobs as slaves became available. I didn't have time to make a GUI for it, but it worked. DOS commands or Python script jobs could be dished out to remote machines and it took very little code to do it. Then I got busy at work again and put the tool aside.

So one of the many GDC 2008 lectures I wasn't able to attend was Life on the Bungie Farm: Fun Things to Do with 180 Servers by Luis Villegas and Sean Shypula (lecture slides). It outlined a system used at Bungie to slave-out time-consuming or annoying processing tasks to a big set of rack-mounted PCs. It could do lighting processing for level artists, regular game builds and verification, whatever.

My first thought was "DO WANT". My next thought was, my little slave tool could make a great start for something much bigger and better. I've done nothing notable on it yet, but I've been turning over details in my head, and hope to find time to expand on it soon.

And to Luis and Sean, thanks for sharing!

Sunday, February 24, 2008

GDC wrap-up

GDC 2008 was quite a trip. The two lectures I gave appeared to go extremely well. High attendance and lots of great questions, reactions. Thanks to everyone that took the time to attend, we really appreciated it!

I've posted our slides on the Volition GDC Archive, along with the promised Python Example Files (zipfile) from my Python for Technical Artists lecture. If you're a TA and enjoy scripting, definitely download them, pick 'em apart and do something cool with them.

I wish I'd been able to attend more sessions myself, but between meetings and lecture prep I never hit as many as I'd like.