<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" gd:etag="W/&quot;CEcFRHo6fyp7ImA9WxNUFkg.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490</id><updated>2009-11-07T21:46:55.417-06:00</updated><title>Tech Art Tiki</title><subtitle type="html">The eternal quest to improve videogame development, one stupid script at a time.</subtitle><link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/posts/default" /><link rel="alternate" type="text/html" href="http://techarttiki.blogspot.com/" /><link rel="hub" href="http://pubsubhubbub.appspot.com/" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email></author><generator version="7.00" uri="http://www.blogger.com">Blogger</generator><openSearch:totalResults>19</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><link rel="self" href="http://feeds.feedburner.com/TechArtTiki" type="application/atom+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><entry gd:etag="W/&quot;CEABR3w_eCp7ImA9WxJaFUs.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-8907637594085867188</id><published>2009-08-06T06:57:00.010-05:00</published><updated>2009-08-06T07:45:56.240-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-06T07:45:56.240-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="regex" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><title>Regular Expressions Coaching</title><content type="html">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.&lt;br /&gt;&lt;br /&gt;Awhile back a co-worker introduced me to &lt;a href="http://weitz.de/regex-coach/"&gt;The Regex Coach&lt;/a&gt;, 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.&lt;br /&gt;&lt;br /&gt;&lt;img src="http://adam.pletcher.googlepages.com/regexcoach.png" /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;A bonus tip for the regex-challenged like myself...&lt;br /&gt;If you memorize only one thing: &lt;span style="font-family:courier new;"&gt;&lt;strong&gt;(.*)&lt;/strong&gt;&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-8907637594085867188?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/oddJQaTT6gk" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/8907637594085867188/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=8907637594085867188" title="9 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8907637594085867188?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8907637594085867188?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/oddJQaTT6gk/regular-expressions-coaching.html" title="Regular Expressions Coaching" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">9</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2009/08/regular-expressions-coaching.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DU4GQH0-eCp7ImA9WxJVEU0.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-3195991674779725908</id><published>2009-06-27T07:34:00.007-05:00</published><updated>2009-06-27T08:05:21.350-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-06-27T08:05:21.350-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="maxscript" /><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><title>Hidden HiddenDOSCommand details</title><content type="html">Twice in recent months I've been bitten by MaxScript's HiddenDOSCommand.&lt;br /&gt;&lt;br /&gt;It was added in 3ds Max 2008 as a way to issue DOS commands without bringing up an ugly command prompt.  Sounds great but what the docs don't tell you is that the optional "startpath:&lt;path&gt;" argument is actually not optional at all. If you leave it out you'll receive a cryptic error like this:.&lt;pre class="maxscript:nogutter" name="code"&gt;HiddenDOSCommand "notepad %temp%\\cmdout.tmp" prompt:"Waiting..."&lt;br /&gt;-- Error! CreateProcess(cmd /e:on /d /c "notepad %temp%\hiddencmdout.tmp") failed!&lt;/pre&gt;Note, that command was pasted from Example Usage in the MaxScript docs for HiddenDOSCommand. It will not work, nor will the other examples listed there &lt;em&gt;unless&lt;/em&gt; you include "startpath"....&lt;pre class="maxscript:nogutter" name="code"&gt;HiddenDOSCommand "notepad %temp%\\cmdout.tmp" prompt:"Waiting..." startpath:"C:\\"&lt;br /&gt;true&lt;/pre&gt;This may have been addressed in the helpfile for 3ds Max 2010, I haven't checked.  This can be Google fodder in the meantime.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-3195991674779725908?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/xZef9P0zsRE" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/3195991674779725908/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=3195991674779725908" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3195991674779725908?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3195991674779725908?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/xZef9P0zsRE/hidden-hiddendoscommand-details.html" title="Hidden HiddenDOSCommand details" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2009/06/hidden-hiddendoscommand-details.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUcCSXw9fip7ImA9WxJSGUs.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-1909198647220480157</id><published>2009-05-10T09:14:00.005-05:00</published><updated>2009-05-10T09:51:08.266-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-05-10T09:51:08.266-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="maxscript" /><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="com" /><title>What we do with Python</title><content type="html">There's a great thread going at &lt;a href="http://www.tech-artists.org/"&gt;tech-artists.org&lt;/a&gt; called &lt;a href="http://tech-artists.org/forum/showthread.php?t=359"&gt;What do you do with Python?&lt;/a&gt; The other day I posted a few of the things our studio has done with Python in the past year or two...&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;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. &lt;/li&gt;&lt;li&gt;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). &lt;/li&gt;&lt;li&gt;A non-linear GUI editor for an otherwise complex/table-driven cutscene pipeline.&lt;/li&gt;&lt;li&gt;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. &lt;/li&gt;&lt;li&gt;Tool that communicates with game C code (via socket) running on consoles to do in-game realtime lighting. &lt;/li&gt;&lt;li&gt;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. &lt;/li&gt;&lt;li&gt;One Exporter that writes out various data files from 3ds Max, Photoshop, and imports/categorizes them in our asset system. &lt;/li&gt;&lt;li&gt;Logs me into Outlook's webmail without manually entering my creds every time. I guess that was a home project. :)&lt;/li&gt;&lt;li&gt;At 3ds Max startup, scan folders for MaxScripts, building a MacroScript .mcr file for all of them. &lt;/li&gt;&lt;li&gt;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. &lt;/li&gt;&lt;li&gt;Profile rendering performance of art assets recently submitted to Perforce, recording data to SQL database.&lt;/li&gt;&lt;li&gt;Searches web-based bug tracker database for entries assigned to you and displays data in a Vista Sidebar gadget.&lt;/li&gt;&lt;li&gt;Creates makefiles with dependencies, for distributed build processes in Incredibuild/XGE.&lt;/li&gt;&lt;li&gt;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.&lt;/li&gt;&lt;li&gt;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.&lt;/li&gt;&lt;li&gt;Tons of data mining uses. Like searching various exported XML files for instances of X material, mesh, etc. in game world.&lt;/li&gt;&lt;li&gt;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. &lt;/li&gt;&lt;li&gt;Custom scripts for integrating our tools/processes into Wing (the Python IDE we use).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;P.S. Call your mom today. &lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-1909198647220480157?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/HlA5DIXDqts" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/1909198647220480157/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=1909198647220480157" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1909198647220480157?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1909198647220480157?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/HlA5DIXDqts/what-weve-done-with-python.html" title="What we do with Python" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2009/05/what-weve-done-with-python.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CkAMSXw8cCp7ImA9WxVUGUg.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-6390402288200276837</id><published>2009-03-24T21:49:00.006-05:00</published><updated>2009-03-24T22:13:08.278-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-03-24T22:13:08.278-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="gdc" /><title>Volition at GDC 2009</title><content type="html">Hey if you're at this year's &lt;a href="http://www.gdconf.com/"&gt;Game Developers Conference&lt;/a&gt;, be sure to check out the talks from my co-workers. &lt;a href="http://www.volition-inc.com/"&gt;Volition&lt;/a&gt; has a strong showing this year, with plenty of great tech artist material.&lt;br /&gt;&lt;br /&gt;&lt;a href="https://www.cmpevents.com/GD09/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=8541"&gt;&lt;strong&gt;Blowing Up the Outside World: Destruction Done the Next Gen Way&lt;/strong&gt;&lt;/a&gt;&lt;br /&gt;&lt;strong&gt;by Eric Arnold and Jeff Hanna&lt;/strong&gt;&lt;br /&gt;This session presents an in-depth look at the tools and technologies used to make a truly destructible world for &lt;a href="http://www.redfaction.com/"&gt;RED FACTION: GUERRILLA&lt;/a&gt;. The presenters will share the lessons they learned and the problems they had to overcome in order to have destruction in their game.&lt;br /&gt;&lt;br /&gt;&lt;a href="https://www.cmpevents.com/GD09/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=8574"&gt;&lt;strong&gt;Technical Artist Roundtable&lt;/strong&gt;&lt;/a&gt;&lt;br /&gt;&lt;strong&gt;by Jeff Hanna&lt;/strong&gt;&lt;br /&gt;This roundtable will be an animated group discussion about being an effective technical artist. Topics of discussion will include what skills a technical artist should possess, how the role differs from company to company, scripting content creation applications, shader development, asset management, and improving production pipelines.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;&lt;a href="https://www.cmpevents.com/GD09/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=8694"&gt;Technical Art Techniques Panel: Tools and Pipeline&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;Robert Galanakis, Jeff Hanna, Seth Gibson, Christopher Evans and Ross Patel&lt;/strong&gt;&lt;br /&gt;As game pipelines, their tools, and content become more complex, technical artists have become the developers of choice for much of the planning, overseeing, and implementation of pipelines. Technical artists from BioWare, Bungie, Microsoft, and Volition discuss their solutions and practices for tools and pipeline.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;&lt;a href="https://www.cmpevents.com/GD09/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=8677"&gt;Breathing LIFE into an Open World&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;by Scott Phillips&lt;/strong&gt;&lt;br /&gt;Examine the history of populating open worlds and a detailed description and post-mortem of the LIFE system developed by Volition for SAINTS ROW 2. Attendees will learn about the inspiration, organization, methodology, successes and failures of the LIFE system used to add life to the open world city of Stilwater.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;&lt;a href="https://www.cmpevents.com/GD09/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=8567"&gt;Universal Character System in SAINTS ROW 2&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;by Chris Fortier&lt;/strong&gt;&lt;br /&gt;Attendees will learn how SAINTS ROW 2's character customization and random NPC generator work. Some things that will be discussed include the universal body mesh, character morphing, normal map blending, layered clothing, shader-based customization features, how we assemble NPCs and how all this character variation affects animation.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-6390402288200276837?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/OxzlcOKg0n8" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/6390402288200276837/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=6390402288200276837" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/6390402288200276837?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/6390402288200276837?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/OxzlcOKg0n8/volition-at-gdc-2009.html" title="Volition at GDC 2009" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2009/03/volition-at-gdc-2009.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A0QFQXc6eip7ImA9WxVUGUk.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-1331331430492624053</id><published>2009-03-17T21:29:00.011-05:00</published><updated>2009-03-24T21:48:30.912-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-03-24T21:48:30.912-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><title>Python cheat sheets</title><content type="html">I'm out of hibernation, time I posted something.&lt;br /&gt;&lt;br /&gt;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 &lt;strong&gt;list comprehensions&lt;/strong&gt;, &lt;strong&gt;filter&lt;/strong&gt;, and &lt;strong&gt;map&lt;/strong&gt;.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;List Comprehensions&lt;/strong&gt;&lt;br /&gt;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 &lt;span style="font-family:courier new;"&gt;for&lt;/span&gt; clause.&lt;br /&gt;&lt;br /&gt;The below example takes an existing list, &lt;span style="font-family:courier new;"&gt;my_list&lt;/span&gt; and builds a new list with only the elements that are greater than 2. In this case the result is assigned right back to &lt;span style="font-family:courier new;"&gt;my_list.&lt;/span&gt;&lt;pre class="python:nogutter" name="code"&gt;my_list = [x for x in my_list if x &gt; 2]&lt;/pre&gt;&lt;strong&gt;Filter&lt;/strong&gt;&lt;br /&gt;Using &lt;span style="font-family:courier new;"&gt;filter&lt;/span&gt; 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.&lt;br /&gt;&lt;br /&gt;Filtering is often done with a &lt;span style="font-family:courier new;"&gt;lambda&lt;/span&gt; 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 &lt;span style="font-family:courier new;"&gt;filter&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;lambda&lt;/span&gt; together, the fact they were seperate didn't occur to me when I was learning the language.&lt;br /&gt;&lt;br /&gt;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'.&lt;pre class="python:nogutter" name="code"&gt;my_files = filter(lambda f: f.endswith('.py'), my_files)&lt;/pre&gt;That is the shorter equivalent of:&lt;br /&gt;&lt;br /&gt;&lt;pre class="python:nogutter" name="code"&gt;def is_py_filename(filename):&lt;br /&gt;   return filename.endswith('.py')&lt;br /&gt;&lt;br /&gt;my_files = filter(is_py_filename, my_files)&lt;/pre&gt;With &lt;span style="font-family:courier new;"&gt;filter&lt;/span&gt;, 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 &lt;span style="font-family:courier new;"&gt;False&lt;/span&gt; or &lt;span style="font-family:courier new;"&gt;None&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;&lt;pre class="python:nogutter" name="code"&gt;my_files = filter(None, my_files)&lt;/pre&gt;&lt;strong&gt;Map&lt;/strong&gt;&lt;br /&gt;Mapped functions let you apply a function to every element in a sequence.&lt;br /&gt;&lt;pre class="python:nogutter" name="code"&gt;def add_ten(x):&lt;br /&gt;   return x+10&lt;br /&gt;&lt;br /&gt;result = map(add_ten, [1,2,3,4,5])&lt;/pre&gt;The value of &lt;span style="font-family:courier new;"&gt;result&lt;/span&gt; would be &lt;span style="font-family:courier new;"&gt;[11, 12, 13, 14, 15]&lt;/span&gt;. Of course you could also use a lambda here, too:&lt;br /&gt;&lt;pre class="python:nogutter" name="code"&gt;result = map(lambda x: x+10, [1,2,3,4,5])&lt;/pre&gt;So what's on your cheat sheet?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-1331331430492624053?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/3xdAj24ePnE" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/1331331430492624053/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=1331331430492624053" title="5 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1331331430492624053?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1331331430492624053?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/3xdAj24ePnE/python-cheat-sheets.html" title="Python cheat sheets" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">5</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2009/03/python-cheat-sheets.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUYEQXw_cCp7ImA9WxRTEUU.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-9177472460559986469</id><published>2008-08-29T08:29:00.011-05:00</published><updated>2008-08-31T07:51:40.248-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-08-31T07:51:40.248-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><title>Read-only Windows files with Python</title><content type="html">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.&lt;pre class="python:nogutter" name="code"&gt;import os, stat&lt;br /&gt;myFile = r'C:\stuff\grail.txt'&lt;br /&gt;&lt;br /&gt;fileAtt = os.stat(myFile)[0]&lt;br /&gt;if (not fileAtt &amp; stat.S_IWRITE):&lt;br /&gt;   # File is read-only, so make it writeable&lt;br /&gt;   os.chmod(myFile, stat.S_IWRITE)&lt;br /&gt;else:&lt;br /&gt;   # File is writeable, so make it read-only&lt;br /&gt;   os.chmod(myFile, stat.S_IREAD)&lt;/pre&gt;You may prefer the &lt;a href="http://python.net/crew/mhammond/win32/"&gt;pywin32 extensions&lt;/a&gt; for this sort of thing...&lt;pre class="python:nogutter" name="code"&gt;import win32api, win32con&lt;br /&gt;myFile = r'C:\stuff\grail.txt'&lt;br /&gt;&lt;br /&gt;fileAtt = win32api.GetFileAttributes(myFile)&lt;br /&gt;if (fileAtt &amp; win32con.FILE_ATTRIBUTE_READONLY):&lt;br /&gt;   # File is read-only, so make it writeable&lt;br /&gt;   win32api.SetFileAttributes(myFile, ~win32con.FILE_ATTRIBUTE_READONLY)&lt;br /&gt;else:&lt;br /&gt;   # File is writeable, so make it read-only&lt;br /&gt;   win32api.SetFileAttributes(myFile, win32con.FILE_ATTRIBUTE_READONLY)&lt;/pre&gt;Or, more concisely with win32:&lt;pre class="python:nogutter" name="code"&gt;roAtt = win32api.GetFileAttributes(myFile) &amp; win32con.FILE_ATTRIBUTE_READONLY&lt;br /&gt;win32api.SetFileAttributes(myFile, ~roAtt)&lt;/pre&gt;Using win32 you can also set other Windows file attributes (unlike &lt;span style="font-family:courier new;"&gt;os.chmod&lt;/span&gt;), but read/write is usually all I care about.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-9177472460559986469?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/zEIp-t9FCVk" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/9177472460559986469/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=9177472460559986469" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/9177472460559986469?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/9177472460559986469?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/zEIp-t9FCVk/read-only-windows-files-with-python.html" title="Read-only Windows files with Python" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/08/read-only-windows-files-with-python.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C0IFQnc6fSp7ImA9WxVQFko.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-3465164648304574101</id><published>2008-08-22T14:43:00.007-05:00</published><updated>2009-02-03T08:31:53.915-06:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-02-03T08:31:53.915-06:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><title>ColladaMax for 3ds Max 2009 64-bit</title><content type="html">I was unable to find the &lt;a href="http://sourceforge.net/projects/colladamaya/"&gt;ColladaMax&lt;/a&gt; importer/exporter plugin for 3ds Max 2009 64-bit, so I built one. It's from the 3.05b source.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Update 02/03/09&lt;/strong&gt;&lt;em&gt; - I made a new build that depends on an older version of the DirectX SDK.  It should fix the "failed to initialize" error some of you were getting, without the need to install anything else.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;Feel free to grab it if you like:&lt;br /&gt;&lt;a href="http://adam.pletcher.googlepages.com/ColladaMax_2009x64.rar"&gt;ColladaMax_2009x64.rar&lt;/a&gt; (709 KB)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-3465164648304574101?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/1iyiYoYasbk" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/3465164648304574101/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=3465164648304574101" title="19 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3465164648304574101?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3465164648304574101?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/1iyiYoYasbk/colladamax-for-3ds-max-2009-64-bit.html" title="ColladaMax for 3ds Max 2009 64-bit" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">19</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/08/colladamax-for-3ds-max-2009-64-bit.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEcESX0_eyp7ImA9WxdUGUQ.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-8591884194828575886</id><published>2008-08-05T22:30:00.007-05:00</published><updated>2008-08-05T23:13:28.343-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-08-05T23:13:28.343-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="com" /><title>Photoshop scripting with Python</title><content type="html">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.&lt;br /&gt;&lt;br /&gt;The methods here are similar to those used in my &lt;a href="http://www.volition-inc.com/gdc"&gt;GDC 2008 Python lecture&lt;/a&gt;, about driving 3ds Max via Python.  You start by dispatching the Photoshop COM server, using Python as the client:&lt;pre class="python:nogutter" name="code"&gt;import win32com.client&lt;br /&gt;psApp = win32com.client.Dispatch("Photoshop.Application")&lt;/pre&gt;This connects to your already-opened Photoshop session, or opens one if none are running.  The root COM object is then assigned to &lt;span style="font-family:courier new;"&gt;psApp&lt;/span&gt;, and you're ready to do some cool stuff.  Here's a quick example:&lt;pre class="python:nogutter" name="code"&gt;psApp.Open(r"D:\temp\blah.psd")         # Opens a PSD file&lt;br /&gt;doc = psApp.Application.ActiveDocument  # Get active document object&lt;br /&gt;layer = doc.ArtLayers[0]                # Get the bottom-most layer&lt;br /&gt;layer.AdjustBrightnessContrast(20,-15)  # Bright +20, Contrast -15&lt;br /&gt;doc.Save()                              # Save the modified PSD&lt;/pre&gt;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.&lt;br /&gt;&lt;br /&gt;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 &lt;span style="font-family:courier new;"&gt;exportType&lt;/span&gt; dictionary determines the name/suffix pairs.&lt;pre class="python:nogutter" name="code"&gt;# Recursively scans a folder (psdRoot) for Photoshop PSD files.&lt;br /&gt;# For each, exports various 24-bit PNG textures based on layer&lt;br /&gt;# groups found in the PSD.&lt;br /&gt;# Requires the Win32 Extensions:&lt;br /&gt;# http://python.net/crew/mhammond/win32/&lt;br /&gt;&lt;br /&gt;import win32com.client&lt;br /&gt;import os&lt;br /&gt;&lt;br /&gt;# Change to match your root folder&lt;br /&gt;psdRoot = r'C:\ArtFiles\PSD'&lt;br /&gt;&lt;br /&gt;# Map of layer group names and the suffixes to use when exporting&lt;br /&gt;exportTypes = {'diffuse':'_D', 'normal':'_N', 'specular':'_S'}&lt;br /&gt;&lt;br /&gt;if (__name__ == '__main__'):&lt;br /&gt;   # COM dispatch for Photoshop&lt;br /&gt;   psApp = win32com.client.Dispatch('Photoshop.Application')&lt;br /&gt;&lt;br /&gt;   # Photoshop actually exposes several different COM interfaces,&lt;br /&gt;   # including one specifically for classes defining export options.&lt;br /&gt;   options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb')&lt;br /&gt;   options.Format = 13   # PNG&lt;br /&gt;   options.PNG8 = False  # Sets it to PNG-24 bit&lt;br /&gt;&lt;br /&gt;   # Get all PSDs under root dir&lt;br /&gt;   psdFiles = []&lt;br /&gt;&lt;br /&gt;   for root, dir, files in os.walk(psdRoot):&lt;br /&gt;      for thisFile in files:&lt;br /&gt;         if (thisFile.lower().endswith('.psd')):&lt;br /&gt;            fullFilename = os.path.join(root, thisFile)&lt;br /&gt;            psdFiles.append(fullFilename)&lt;br /&gt;&lt;br /&gt;   # Loop through PSDs we found&lt;br /&gt;   for psdFile in psdFiles:&lt;br /&gt;      doc = psApp.Open(psdFile)&lt;br /&gt;      layerSets = doc.LayerSets&lt;br /&gt;&lt;br /&gt;      if (len(layerSets) &gt; 0):&lt;br /&gt;         # First hide all root-level layers&lt;br /&gt;         for layer in doc.Layers:&lt;br /&gt;            layer.Visible = False&lt;br /&gt;         # ... and layerSets&lt;br /&gt;         for layerSet in layerSets:&lt;br /&gt;            layerSet.Visible = False&lt;br /&gt;            &lt;br /&gt;         # Loop through each LayerSet (aka Group)&lt;br /&gt;         for layerSet in layerSets:&lt;br /&gt;            lsName = layerSet.Name.lower()&lt;br /&gt;&lt;br /&gt;            if (lsName in exportTypes):&lt;br /&gt;               layerSet.Visible = True  # make visible again&lt;br /&gt;&lt;br /&gt;               # Make our export filename&lt;br /&gt;               pngFile = os.path.splitext(psdFile)[0] + exportTypes[lsName] + '.png'&lt;br /&gt;&lt;br /&gt;               # If PNG exists but older than PSD, delete it.&lt;br /&gt;               if (os.path.exists(pngFile)):&lt;br /&gt;                  psdTime = os.stat(psdFile)[8]&lt;br /&gt;                  pngTime = os.stat(pngFile)[8]&lt;br /&gt;         &lt;br /&gt;                  if (psdTime &gt; pngTime):&lt;br /&gt;                     os.remove(pngFile)&lt;br /&gt;&lt;br /&gt;               # Export PNG for this layer Group&lt;br /&gt;               if (not os.path.exists(pngFile)):&lt;br /&gt;                  doc = psApp.Open(psdFile)&lt;br /&gt;                  doc.Export(ExportIn=pngFile, ExportAs=2, Options=options)&lt;br /&gt;                  print 'exporting:', pngFile&lt;br /&gt;               else:&lt;br /&gt;                  print 'skipping newer file:', psdFile&lt;br /&gt;                  &lt;br /&gt;               # Make LayerSet invisible again&lt;br /&gt;               layerSet.Visible = False&lt;br /&gt;&lt;br /&gt;         # Close PSD without saving&lt;br /&gt;         doc.Close(2)&lt;/pre&gt;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.&lt;br /&gt;&lt;br /&gt;Here is a ZIP containing the above script and a sample PSD file to try it on: &lt;a href="http://adam.pletcher.googlepages.com/exportTextureLayers.zip"&gt;exportTextureLayers.zip&lt;/a&gt; (143 KB)&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;If you dig this, I'd recommend reading the &lt;a href="http://www.adobe.com/devnet/photoshop/pdfs/PhotoshopScriptingGuide.pdf"&gt;Scripting Guide&lt;/a&gt; and &lt;a href="http://www.adobe.com/devnet/photoshop/pdfs/photoshop_cs3_vbscript_ref.pdf"&gt;CS3 VBScript Reference&lt;/a&gt; found in the &lt;a href="http://www.adobe.com/devnet/photoshop/scripting/"&gt;Adobe Photoshop Developer Center&lt;/a&gt;.  While the above wasn't VBScript, the COM interface we used is nearly identical.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-8591884194828575886?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/UGSpYO39ZQA" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/8591884194828575886/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=8591884194828575886" title="27 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8591884194828575886?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8591884194828575886?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/UGSpYO39ZQA/photoshop-scripting-with-python.html" title="Photoshop scripting with Python" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">27</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/08/photoshop-scripting-with-python.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUECRXk5cSp7ImA9WxdaFU0.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-2050900471838074697</id><published>2008-07-16T18:50:00.011-05:00</published><updated>2008-08-23T10:01:04.729-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-08-23T10:01:04.729-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="maxscript" /><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="com" /><title>Checksums in 3ds Max (part 2 of 2)</title><content type="html">In &lt;a href="http://techarttiki.blogspot.com/2008/03/checksums.html"&gt;Part 1&lt;/a&gt; I showed how to calculate checksums inside 3ds Max. Here's how to do something useful with them.&lt;br /&gt;&lt;br /&gt;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).&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;The script is divided into three functions and a short bit of main code.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-family:courier new;"&gt;getChecksum()&lt;/span&gt;&lt;/b&gt; is the first function, taken from my &lt;a href="http://techarttiki.blogspot.com/2008/03/checksums.html"&gt;previous post&lt;/a&gt;. 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.&lt;br /&gt;&lt;br /&gt;Next is the &lt;b&gt;&lt;span style="font-family:courier new;"&gt;getPropsString()&lt;/span&gt;&lt;/b&gt; and &lt;b&gt;&lt;span style="font-family:courier new;"&gt;getMaterialChecksum()&lt;/span&gt;&lt;/b&gt; functions:&lt;pre class="maxscript:nogutter" name="code"&gt;------------------------------------------------------------&lt;br /&gt;-- (str)getPropsString (material)mat&lt;br /&gt;--&lt;br /&gt;-- Description:&lt;br /&gt;-- Builds a string representing the property names/values&lt;br /&gt;-- of the supplied Max material.&lt;br /&gt;------------------------------------------------------------&lt;br /&gt;fn getPropsString mat = (&lt;br /&gt;   myStr = "" as stringStream&lt;br /&gt;   if (mat == undefined) then (&lt;br /&gt;      format "undefined" to:myStr&lt;br /&gt;   ) else (&lt;br /&gt;      -- Start our string w/the classname&lt;br /&gt;      format (classOf mat as string) to:myStr&lt;br /&gt;      if (classof mat == ArrayParameter) then (&lt;br /&gt;         -- Array, so recursively add strings for each element&lt;br /&gt;         for element in mat do (&lt;br /&gt;            format (getPropsString element) to:myStr&lt;br /&gt;         )&lt;br /&gt;      ) else (&lt;br /&gt;         -- Not an array, so see if it has properties&lt;br /&gt;         propNames = undefined&lt;br /&gt;         try (&lt;br /&gt;            propNames = getPropNames mat&lt;br /&gt;         ) catch ()&lt;br /&gt;         if (classOf mat == BitMap) then (&lt;br /&gt;            try (  -- Add bitmap's filename&lt;br /&gt;               format mat.filename to:myStr&lt;br /&gt;            ) catch ()&lt;br /&gt;         ) else if (propNames == undefined) then (&lt;br /&gt;            format (mat as string) to:myStr&lt;br /&gt;         ) else (&lt;br /&gt;            format (propNames as string) to:myStr&lt;br /&gt;            -- Loop through properties, adding their names&lt;br /&gt;            -- and values to our string to be checksummed&lt;br /&gt;            for i in 1 to propNames.count do (&lt;br /&gt;               format (i as string) to:myStr&lt;br /&gt;               p = propNames[i]&lt;br /&gt;               val = getProperty mat p&lt;br /&gt;               format (i as string) to:myStr&lt;br /&gt;               format (getPropsString val) to:myStr&lt;br /&gt;            )&lt;br /&gt;         )&lt;br /&gt;      )&lt;br /&gt;   )&lt;br /&gt;   (myStr as string)&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;------------------------------------------------------------&lt;br /&gt;-- (str)getMaterialChecksum (material)mat&lt;br /&gt;--&lt;br /&gt;-- Description:&lt;br /&gt;-- Takes a Max material (or multi-sub material) and&lt;br /&gt;-- calculates a checksum value from it, for use as a&lt;br /&gt;-- hashtable key, or whatever you like.&lt;br /&gt;------------------------------------------------------------&lt;br /&gt;fn getMaterialChecksum mat = (&lt;br /&gt;   str = ""&lt;br /&gt;   if (classof mat == Multimaterial) then (&lt;br /&gt;      for id in mat.materialIDList do (&lt;br /&gt;         -- Add material IDs as factors&lt;br /&gt;         str += id as string&lt;br /&gt;      )&lt;br /&gt;      for subMat in mat.materialList do (&lt;br /&gt;         -- Get string representing each submaterial&lt;br /&gt;         str += (getPropsString subMat)&lt;br /&gt;      )&lt;br /&gt;      ) else (&lt;br /&gt;         -- Get string representing this material&lt;br /&gt;         str += getPropsString mat&lt;br /&gt;      )&lt;br /&gt;   -- Add string length as a factor&lt;br /&gt;   str += str.count as string&lt;br /&gt;&lt;br /&gt;   -- Get checksum from our base string&lt;br /&gt;   -- 99991 = largest prime number under 10k&lt;br /&gt;   (getChecksum str)&lt;br /&gt;)&lt;/pre&gt;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 &lt;b&gt;&lt;span style="font-family:courier new;"&gt;getChecksum()&lt;/span&gt;&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;The main code block loops through the entire 3ds Max scene, doing the above for every material found on geometry objects:&lt;br /&gt;&lt;pre class="maxscript:nogutter" name="code"&gt;----------&lt;br /&gt;-- MAIN&lt;br /&gt;----------&lt;br /&gt;-- Set up a few things first.&lt;br /&gt;&lt;br /&gt;timeStart = timestamp()  -- Time we started process&lt;br /&gt;removedCount = 0  -- Counters for printing info below&lt;br /&gt;uniqueCount = 0&lt;br /&gt;&lt;br /&gt;-- Array of two synced arrays, first with the material&lt;br /&gt;-- checksums, second with materials themselves.&lt;br /&gt;-- Basically a poor-man's hashtable.&lt;br /&gt;csMatArr = #(#(), #())&lt;br /&gt;&lt;br /&gt;format "Scanning scene materials...\n"&lt;br /&gt;&lt;br /&gt;-- Loop through all geometry&lt;br /&gt;for obj in geometry do (&lt;br /&gt;   mat = obj.material&lt;br /&gt;&lt;br /&gt;   alreadyDone = (findItem csMatArr[2] mat) != 0&lt;br /&gt;&lt;br /&gt;   if (not alreadyDone) and (mat != undefined) then (&lt;br /&gt;      -- First get this material's checksum&lt;br /&gt;      csum = getMaterialChecksum mat&lt;br /&gt;&lt;br /&gt;      idx = findItem csMatArr[1] csum&lt;br /&gt;&lt;br /&gt;      if (idx != 0) then (&lt;br /&gt;         -- Dupe material found, so remove it by&lt;br /&gt;         -- assigning the first mat to this object&lt;br /&gt;         format "Replacing material '%' with '%'\n" mat.name csMatArr[2][idx].name&lt;br /&gt;         obj.material = csMatArr[2][idx]&lt;br /&gt;         removedCount += 1&lt;br /&gt;      ) else (&lt;br /&gt;         -- New checksum, so add it to our table,&lt;br /&gt;         -- along with the material itself.&lt;br /&gt;         append csMatArr[1] csum&lt;br /&gt;         append csMatArr[2] mat&lt;br /&gt;         uniqueCount += 1&lt;br /&gt;      )&lt;br /&gt;   )&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;gc()  -- Remind Max to take out the trash&lt;br /&gt;&lt;br /&gt;-- Done, print some results&lt;br /&gt;format "-- DONE in % secs --\n" ((timestamp() - timeStart) / 1000.0)&lt;br /&gt;format "Old material count = %\n" (uniqueCount + removedCount)&lt;br /&gt;format "New material count = %\n" uniqueCount&lt;br /&gt;format "Duplicates removed = %\n" removedCount&lt;/pre&gt;That's it. At the end a summary is printed to the MaxScript Listener.&lt;br /&gt;&lt;br /&gt;In the Max scene I was working with today this script cut the root material count from 533 to 261. That's &lt;strong&gt;51%&lt;/strong&gt; fewer materials! It also reduced the file load time from 136 seconds to 102 seconds.&lt;br /&gt;&lt;br /&gt;You can download the complete script above here:  &lt;a href="http://adam.pletcher.googlepages.com/RemoveDupeMaterials.zip"&gt;RemoveDupeMaterials.zip&lt;/a&gt; (4 KB)&lt;br /&gt;It includes the Python script to register the COM server, and the alternate MaxScript checksum method.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Update 7/25/08:&lt;/b&gt; I modified &lt;b&gt;&lt;span style="font-family:courier new;"&gt;getPropsString()&lt;/span&gt;&lt;/b&gt; to better handle bitmap values, and generally run faster.  The ZIP file above is updated as well.  Thanks to MoonDoggie/Colin on &lt;a href="http://forums.cgsociety.org/showthread.php?f=98&amp;t=656126"&gt;CGTalk&lt;/a&gt; for the feedback!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-2050900471838074697?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/cx3PKfa637M" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/2050900471838074697/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=2050900471838074697" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/2050900471838074697?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/2050900471838074697?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/cx3PKfa637M/checksums2.html" title="Checksums in 3ds Max (part 2 of 2)" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/07/checksums2.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUECRXk4eCp7ImA9WxdaFU0.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-683076651941569877</id><published>2008-06-18T22:28:00.002-05:00</published><updated>2008-08-23T10:01:04.730-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-08-23T10:01:04.730-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="maxscript" /><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="com" /><title>Checksums in 3ds Max (part 1 of 2)</title><content type="html">In my &lt;a href="http://techarttiki.blogspot.com/2008/03/calling-python-from-maxscript.html"&gt;Calling Python from MaxScript post&lt;/a&gt; I mentioned the usefulness of checksums in Tech Art work. I was hoping to elaborate on that a bit.&lt;br /&gt;&lt;br /&gt;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: &lt;span style="font-family:courier new;"&gt;"Tech Art is A-#1 Supar"&lt;/span&gt;, and the checksum you've computed is &lt;span style="font-family:courier new;"&gt;"30532"&lt;/span&gt;. If any character in that string changes, the computed checksum for it will be different, like &lt;span style="font-family:courier new;"&gt;"18835"&lt;/span&gt; or &lt;span style="font-family:courier new;"&gt;"1335"&lt;/span&gt;... basically anything other than &lt;span style="font-family:courier new;"&gt;"30532"&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Checksums are most useful in cases where you need to compare two sets of data to see if they differ, but don't care &lt;em&gt;where&lt;/em&gt; or &lt;em&gt;how&lt;/em&gt; 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.&lt;br /&gt;&lt;br /&gt;Here's a MaxScript function that takes a string of any length and returns a checksum for it.&lt;pre class="maxscript:nogutter" name="code"&gt;&lt;br /&gt;------------------------------------------------------------&lt;br /&gt;-- (str)getChecksum (string)val (int)size:256&lt;br /&gt;--&lt;br /&gt;-- Description:&lt;br /&gt;-- Calculates simple checksum value from supplied string (or&lt;br /&gt;-- any value convertable to a string).  Default size is 256,&lt;br /&gt;-- but can be changed with the optional "size" parameter.&lt;br /&gt;------------------------------------------------------------&lt;br /&gt;fn getChecksum val size:256 = (&lt;br /&gt;   if (classof val != String) then (&lt;br /&gt;      try (&lt;br /&gt;         val = val as String&lt;br /&gt;      ) catch (&lt;br /&gt;         return false&lt;br /&gt;      )&lt;br /&gt;   )&lt;br /&gt;   alphaKey = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 !@#$%^&amp;amp;*()[]\\{};':\",./&lt;&gt;?"&lt;br /&gt;   total = 0&lt;br /&gt;   for i in 1 to val.count do (&lt;br /&gt;      thisVal = findString alphaKey val[i]&lt;br /&gt;      if (thisVal == undefined) then (&lt;br /&gt;         thisVal = 0&lt;br /&gt;      )&lt;br /&gt;      -- Multiply the alphanumeric value by its position in&lt;br /&gt;      -- the input string, add to running total&lt;br /&gt;      total += (thisVal * i)&lt;br /&gt;   )&lt;br /&gt;      -- make sure divisor is smaller than dividend&lt;br /&gt;   while (total &lt; size) do (&lt;br /&gt;      total = total * 2&lt;br /&gt;   )&lt;br /&gt;&lt;br /&gt;   -- Return final checksum value&lt;br /&gt;   checksum = mod total size&lt;br /&gt;   return (checksum as string)&lt;br /&gt;)&lt;/pre&gt;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) **.&lt;br /&gt;&lt;br /&gt;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, &lt;a href="http://techarttiki.blogspot.com/2008/03/calling-python-from-maxscript.html"&gt;Calling Python from MaxScript&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;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:&lt;br /&gt;&lt;br /&gt;&lt;pre class="maxscript:nogutter" name="code"&gt;fn getChecksum val = (&lt;br /&gt;   comObj = createOLEObject "PythonCom.Utilities" &lt;br /&gt;   checksum = comObj.checksumMD5 val&lt;br /&gt;   return checksum&lt;br /&gt;)&lt;/pre&gt;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.&lt;br /&gt;&lt;br /&gt;Now you know more about checksums, and how to generate them in 3ds Max. Next time (in &lt;a href="http://techarttiki.blogspot.com/2008/07/checksums2.html"&gt;Part 2&lt;/a&gt;) I'll discuss how you can use them to save memory in both Max and your game engine.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-683076651941569877?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/9FkE2VV_eAc" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/683076651941569877/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=683076651941569877" title="8 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/683076651941569877?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/683076651941569877?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/9FkE2VV_eAc/checksums.html" title="Checksums in 3ds Max (part 1 of 2)" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">8</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/checksums.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkABSX4-fip7ImA9WxdWFU8.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-8898785537843528785</id><published>2008-05-26T06:09:00.001-05:00</published><updated>2008-07-08T08:45:58.056-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-08T08:45:58.056-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><title>Python String Templates</title><content type="html">The Template class, found in the standard Python &lt;span style="font-family:courier new;"&gt;string&lt;/span&gt; module, is extremely useful. Start with a template string containing keys you want to replace. By default keys start with "$".&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="python:nogutter"&gt;&gt;&gt;&gt; import string&lt;br /&gt;&gt;&gt;&gt; thisTmp = string.Template("The $speed $color $thing1")&lt;br /&gt;&gt;&gt;&gt; thisTmp.substitute(speed='quick', color='brown', thing1='fox')&lt;br /&gt;'The quick brown fox'&lt;/pre&gt;You can also pass the &lt;span style="font-family:courier new;"&gt;substitute&lt;/span&gt; method a dictionary with key/value pairs for your template:&lt;pre class="python:nogutter" name="code"&gt;&gt;&gt;&gt; strDict = {'speed':'slow', 'color':'toupe', 'thing1':'mango'}&lt;br /&gt;&gt;&gt;&gt; thisTmp.substitute(strDict)&lt;br /&gt;'The slow toupe mango'&lt;/pre&gt;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.&lt;br /&gt;&lt;br /&gt;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.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-8898785537843528785?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/Ss7WzBBeAtU" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/8898785537843528785/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=8898785537843528785" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8898785537843528785?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8898785537843528785?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/Ss7WzBBeAtU/python-string-templates.html" title="Python String Templates" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/07/python-string-templates.html</feedburner:origLink></entry><entry gd:etag="W/&quot;D0EBRHw7eCp7ImA9WxJVFk8.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-1320934547341084259</id><published>2008-05-06T22:39:00.007-05:00</published><updated>2009-07-03T07:54:15.200-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-03T07:54:15.200-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="maxscript" /><category scheme="http://www.blogger.com/atom/ns#" term="3ds max" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="com" /><title>Calling Python from MaxScript</title><content type="html">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.&lt;br /&gt;&lt;br /&gt;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 &lt;a href="http://en.wikipedia.org/wiki/Component_Object_Model"&gt;gory details on COM&lt;/a&gt; if you're curious.&lt;br /&gt;&lt;br /&gt;Here's a simple COM server using Python:&lt;br /&gt;&lt;i&gt;Requires the &lt;a href="http://python.net/crew/mhammond/win32/"&gt;Python Win32 Extensions&lt;/a&gt; (which no TA should be without)&lt;/i&gt;&lt;br /&gt;&lt;pre name="code" class="python"&gt;# A simple Python COM server.&lt;br /&gt;class PythonComUtilities:&lt;br /&gt;   # These tell win32 what/how to register with COM&lt;br /&gt;   _public_methods_ = ['checksumMD5']&lt;br /&gt;   _reg_progid_ = 'PythonCom.Utilities'&lt;br /&gt;   # Class ID must be new/unique for every server you create&lt;br /&gt;   _reg_clsid_ = '{48dd4b8f-f35e-11dc-a4fd-0013029ef248}'&lt;br /&gt;&lt;br /&gt;   def checksumMD5(self, string):&lt;br /&gt;      """Creates MD5 checksum from string"""&lt;br /&gt;      import md5&lt;br /&gt;      m = md5.new()&lt;br /&gt;      m.update(str(string))&lt;br /&gt;      return m.hexdigest()&lt;br /&gt;&lt;br /&gt;if (__name__ == '__main__'):&lt;br /&gt;   print 'Registering COM server...'&lt;br /&gt;   import win32com.server.register as comReg&lt;br /&gt;   comReg.UseCommandLine(PythonComUtilities)&lt;/pre&gt;This defines a function, &lt;code&gt;checksumMD5&lt;/code&gt; that takes a string as input, and returns the MD5 checksum for that string.&lt;br /&gt;&lt;br /&gt;To register the COM server on a PC, simply run the Python script.  Windows records it in registry, noting which script/application it uses.&lt;br /&gt;&lt;br /&gt;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:&lt;br /&gt;&lt;pre name="code" class="maxscript"&gt;&lt;br /&gt;-- Connect to the COM server by name&lt;br /&gt;comObj = createOLEObject "PythonCom.Utilities"&lt;br /&gt;-- Call the function it exposes, with a sample string&lt;br /&gt;checksum = comObj.checksumMD5 "The quick brown fox."&lt;br /&gt;&lt;/pre&gt;It's that simple.  The checkum value returned for our sample string is &lt;code&gt;"2e87284d245c2aae1c74fa4c50a74c77"&lt;/code&gt;.&lt;br /&gt;&lt;br /&gt;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 &lt;a href="http://techarttiki.blogspot.com/2008/03/checksums.html"&gt;Checksums in 3ds Max, Part 1&lt;/a&gt; and &lt;a href="http://techarttiki.blogspot.com/2008/07/checksums2.html"&gt;Part 2&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;i&gt;Python COM server example adapted from code appearing in &lt;/i&gt;&lt;a href="http://www.oreilly.com/catalog/pythonwin32/"&gt;Python Programming in Win32&lt;/a&gt;&lt;i&gt; by Mark Hammond and Andy Robinson... a great book for getting more out of Windows with Python.&lt;/i&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-1320934547341084259?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/55h7Gey9Tu8" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/1320934547341084259/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=1320934547341084259" title="4 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1320934547341084259?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1320934547341084259?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/55h7Gey9Tu8/calling-python-from-maxscript.html" title="Calling Python from MaxScript" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">4</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/calling-python-from-maxscript.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUcBQHs7cSp7ImA9WxdWEkg.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-1245404644776776591</id><published>2008-04-05T23:18:00.000-05:00</published><updated>2008-07-05T06:24:11.509-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-05T06:24:11.509-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><title>Logging (I'm no lumberjack, but I'm okay)</title><content type="html">Exploring the Python standard library is fun.&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;pre name="code" class="python"&gt;&lt;br /&gt;import logging&lt;br /&gt;import logging.handlers&lt;br /&gt;&lt;br /&gt;# Sets up a basic textfile log, with formatting&lt;br /&gt;logging.basicConfig(level=logging.DEBUG,&lt;br /&gt;   format='%(asctime)s %(levelname)-8s %(message)s',&lt;br /&gt;   datefmt='%m/%d/%y %H:%M:%S',&lt;br /&gt;   filename=r'C:\temp\mylog.log',&lt;br /&gt;   filemode='a')&lt;br /&gt;&lt;br /&gt;# Log a few different events&lt;br /&gt;logging.info('Just testing the water.')&lt;br /&gt;logging.warning('Hmm, something is not right here')&lt;br /&gt;logging.error("Oh no, now you're in for it")&lt;br /&gt;&lt;/pre&gt;The resulting text log:&lt;br /&gt;&lt;pre&gt;02/14/08 22:19:03 INFO     Just testing the water.&lt;br /&gt;02/14/08 22:19:03 WARNING  Hmm, something is not right here&lt;br /&gt;02/14/08 22:19:03 ERROR    Oh no, now you're in for it&lt;/pre&gt;Add a few more lines and it sends you an email for any logs that are level "ERROR" or above:&lt;br /&gt;&lt;pre name="code" class="python:firstline[15]"&gt;&lt;br /&gt;email = logging.handlers.SMTPHandler('smtp.foo.com',&lt;br /&gt;   'script@foo.com',('techart@bar.com'),'Error Report')&lt;br /&gt;email.setLevel(logging.ERROR)&lt;br /&gt;&lt;br /&gt;logging.getLogger('').addHandler(email)&lt;br /&gt;&lt;/pre&gt;There's several other handler types as well. Very useful!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-1245404644776776591?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/SyhFuB6tJ4s" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/1245404644776776591/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=1245404644776776591" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1245404644776776591?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1245404644776776591?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/SyhFuB6tJ4s/this-is-test-of-some-code.html" title="Logging (I'm no lumberjack, but I'm okay)" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/this-is-test-of-some-code.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEAERHw5cCp7ImA9WxdWEkg.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-8345776354536344338</id><published>2008-03-15T20:20:00.000-05:00</published><updated>2008-07-05T06:18:25.228-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-05T06:18:25.228-05:00</app:edited><title>Excel as a Database?</title><content type="html">Normally I don't pass stuff like this on, but this one still kills me every time.  And if you're reading my blog you've probably heard at least one conversation like this.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.neopoleon.com/home/blogs/neo/archive/2003/09/29/5458.aspx"&gt;Rory - Neopoleon: Excel as a Database&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-8345776354536344338?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/0MWKloE9Pew" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/8345776354536344338/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=8345776354536344338" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8345776354536344338?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/8345776354536344338?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/0MWKloE9Pew/excel-as-database.html" title="Excel as a Database?" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/07/excel-as-database.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CE4MQ3Y5fCp7ImA9WxdWEks.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-1206528237456450348</id><published>2008-03-01T21:02:00.001-06:00</published><updated>2008-07-05T08:03:02.824-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-05T08:03:02.824-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="gdc" /><title>On Farming</title><content type="html">About a year ago I took a look at &lt;a href="http://pyro.sourceforge.net/"&gt;Pyro&lt;/a&gt;, 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.&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;So one of the many GDC 2008 lectures I wasn't able to attend was &lt;a href="https://www.cmpevents.com/GD08/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=6455"&gt;Life on the Bungie Farm: Fun Things to Do with 180 Servers&lt;/a&gt; by Luis Villegas and Sean Shypula (&lt;a href="http://www.bungie.net/images/Inside/publications/presentations/Life_on_the_Bungie_Farm.pptx"&gt;lecture slides&lt;/a&gt;). 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.&lt;br /&gt;&lt;br /&gt;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.&lt;br /&gt;&lt;br /&gt;And to Luis and Sean, thanks for sharing!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-1206528237456450348?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/Ed-YgGgEHpI" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/1206528237456450348/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=1206528237456450348" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1206528237456450348?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/1206528237456450348?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/Ed-YgGgEHpI/farm-idea.html" title="On Farming" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/farm-idea.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkEHQXs4fyp7ImA9WxRbGEU.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-3364985545438333191</id><published>2008-02-24T23:23:00.007-06:00</published><updated>2008-12-09T22:50:30.537-06:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-12-09T22:50:30.537-06:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="gdc" /><title>GDC wrap-up</title><content type="html">&lt;a href="http://www.gdconf.com/"&gt;GDC 2008&lt;/a&gt; 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!&lt;br /&gt;&lt;br /&gt;I've posted our slides on the &lt;a href="http://www.volition-inc.com/gdc"&gt;Volition GDC Archive&lt;/a&gt;, along with the promised Python Example Files (&lt;a href="http://www.volition-inc.com/gdc/GDC2008_AdamPletcher_PythonSamples.zip"&gt;zipfile&lt;/a&gt;) 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.&lt;br /&gt;&lt;br /&gt;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.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-3364985545438333191?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/zQKPb_0HY-M" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/3364985545438333191/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=3364985545438333191" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3364985545438333191?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/3364985545438333191?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/zQKPb_0HY-M/gdc-wrap-up.html" title="GDC wrap-up" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/gdc-wrap-up.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEIESHozeip7ImA9WxdXFEw.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-6087462681789490905</id><published>2008-02-12T22:36:00.007-06:00</published><updated>2008-06-25T12:55:09.482-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-06-25T12:55:09.482-05:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="outsourcing" /><category scheme="http://www.blogger.com/atom/ns#" term="python" /><category scheme="http://www.blogger.com/atom/ns#" term="gdc" /><title>GDC 2008 coming</title><content type="html">I'm in a mad rush to complete my two lectures for the &lt;a href="http://www.gdconf.com/"&gt;2008 Game Developers Conference&lt;/a&gt;. Every three years or so I forget how hard these are and submit another proposal. What possessed me to do two this year I'll never know.&lt;br /&gt;&lt;br /&gt;This year I'm doing one called &lt;a href="https://www.cmpevents.com/GD08/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=6459"&gt;Python for Technical Artists&lt;/a&gt; and another, &lt;a href="https://www.cmpevents.com/GD08/a.asp?option=C&amp;amp;V=11&amp;amp;SessID=6460"&gt;Internal and Outsourcer Management of Tools and Pipelines&lt;/a&gt;. Yeah, that one's a mouthful. Fortunately I'm splitting the second one with TA co-worker of mine, so I can blame him.&lt;br /&gt;&lt;br /&gt;I actually feel fairly well-prepared this year. The material is also a lot of fun, which makes all the difference, I think.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-6087462681789490905?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/ZigDwk_o_iY" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/6087462681789490905/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=6087462681789490905" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/6087462681789490905?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/6087462681789490905?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/ZigDwk_o_iY/gdc-2008-this-way-comes.html" title="GDC 2008 coming" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/03/gdc-2008-this-way-comes.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEUCQ38-fip7ImA9WxdWGUw.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-874491044031943610</id><published>2008-02-10T08:21:00.005-06:00</published><updated>2008-07-12T21:31:02.156-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-12T21:31:02.156-05:00</app:edited><title>First</title><content type="html">After a few seconds of intense analysis, I concluded the world needs exactly one more blog. I doubt I'll post to this much, but I hope what I do post will be useful or interesting to some.

I'll try not to impress you with details on the great burrito place I found during lunch, why I think X or Y sucks, or post mildly amusing YouTube links.

This blog is focused on technical art for videogame development. Hope you dig it.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-874491044031943610?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/AlaJrcythH4" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://techarttiki.blogspot.com/feeds/874491044031943610/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=5843977256413530490&amp;postID=874491044031943610" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/874491044031943610?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/874491044031943610?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/AlaJrcythH4/first.html" title="First" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://techarttiki.blogspot.com/2008/02/first.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkMGSHw6fyp7ImA9WxJUGE4.&quot;"><id>tag:blogger.com,1999:blog-5843977256413530490.post-783182076444234537</id><published>2008-02-09T12:47:00.011-06:00</published><updated>2009-07-17T07:40:29.217-05:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-17T07:40:29.217-05:00</app:edited><title>Tech Art Tiki</title><content type="html">Tech Art Tiki is Adam Pletcher's blog for Technical Artists in Videogame Development.&lt;br /&gt;&lt;br /&gt;In his 15 years at &lt;a href="http://www.volition-inc.com/"&gt;Volition, Inc.&lt;/a&gt; Adam has filled a wide variety of roles at including Studio Art Director, Lead Game Designer, Lead Artist, effects and cinematics art, and, most of all, every corner of technical art.&lt;br /&gt;&lt;br /&gt;His industry credits include the DESCENT and FREESPACE series, SUMMONER, RED FACTION and SAINTS ROW. He's currently the Technical Art Director of Volition's core tools and technology team.&lt;br /&gt;&lt;br /&gt;When he's not fixing stuff for kids at work, he's fixing stuff for kids at home.&lt;br /&gt;&lt;br /&gt;Contact Adam: &lt;a href="mailto:adam.pletcher@gmail.com"&gt;adam.pletcher@gmail.com&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5843977256413530490-783182076444234537?l=techarttiki.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TechArtTiki/~4/A6RObZ6gKy0" height="1" width="1"/&gt;</content><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/783182076444234537?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/5843977256413530490/posts/default/783182076444234537?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TechArtTiki/~3/A6RObZ6gKy0/tech-art-tiki.html" title="Tech Art Tiki" /><author><name>Adam Pletcher</name><email>adam.pletcher@gmail.com</email><gd:extendedProperty name="OpenSocialUserId" value="01317330807829023136" /></author><feedburner:origLink>http://techarttiki.blogspot.com/2008/02/tech-art-tiki.html</feedburner:origLink></entry></feed>
