Showing posts with label 3ds max. Show all posts
Showing posts with label 3ds max. Show all posts

Wednesday, February 20, 2013

Why retarget missing bitmaps in 3ds Max?

I was just reading a blog post by Nick Covington about retargeting missing bitmaps in 3ds Max scenes. He has a perfectly clever idea for doing so, but I wondered why I've never needed to write something similar for my users. Or rather, why so many people feel that bitmap retargeting is necessary?

3ds Max has an "External File Paths" feature found under Customize/Configure User Paths. It allows you to list folders in which bitmaps can be found on your system, regardless of where they were when first assigned. For instance, if you load a Max scene containing a reference to "C:\my_killer_art\foo.tga", but that file or folder doesn't exist on your PC, it will automatically look for "foo.tga" in each of the External File folders until it's found.

It also works for shader files. As long as you have paths in that list that contain all bitmaps your scenes could possibly use, you'll never see another "missing files" error again.

So am I missing something? Do people just prefer to retarget in the scenes themselves, rather than maintain that list of external file folders? I don't, but mileage might vary.

Admittedly, one limitation of that external file paths is that they don't automatically recurse into subdirectories. If you have a folder below one listed there, it will not look in that folder, it needs to be listed explicitly. Rather dumb.

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

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.

Saturday, June 27, 2009

Hidden HiddenDOSCommand details

Twice in recent months I've been bitten by MaxScript's HiddenDOSCommand.

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:" argument is actually not optional at all. If you leave it out you'll receive a cryptic error like this:.

HiddenDOSCommand "notepad %temp%\\cmdout.tmp" prompt:"Waiting..."
-- Error! CreateProcess(cmd /e:on /d /c "notepad %temp%\hiddencmdout.tmp") failed!
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 unless you include "startpath"....
HiddenDOSCommand "notepad %temp%\\cmdout.tmp" prompt:"Waiting..." startpath:"C:\\"
true
This may have been addressed in the helpfile for 3ds Max 2010, I haven't checked. This can be Google fodder in the meantime.

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.

Friday, August 22, 2008

ColladaMax for 3ds Max 2009 64-bit

I was unable to find the ColladaMax importer/exporter plugin for 3ds Max 2009 64-bit, so I built one. It's from the 3.05b source.

Update 02/03/09 - 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.

Feel free to grab it if you like:
ColladaMax_2009x64.rar (709 KB)

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.

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.