Writing and Reading 3ds Max Scene “Sidecar” Data in Python

By
-
Login to Follow
-
Industry
  • Film & VFX
Products
  • 3ds Max
Skill Level
  • Advanced
Duration
15 min

In 3ds Max 2019 custom file streams were exposed in the C++ API and MAXScript. This feature was a direct response to customer feedback (see https://forums.autodesk.com/t5/3ds-max-ideas/maxscript-python-access-to-a-max-file-be-able-to-read-object/idi-p/6787124). This functionality allows you to store arbitrary string data in the 3ds Max scene file itself, which is very handy for adding metadata you might want for handling scene files in your pipeline. The great thing about this feature is that it uses the standard Microsoft Structured Storage format, so you can read and write to these streams outside of 3ds Max. (See https://docs.microsoft.com/en-us/windows/desktop/stg/structured-storage-start-page)

The 3ds Max SDK has some great samples illustrating how to use this feature in C++ (see maxsdk/samples/utilities/CustomFileStream – there are two projects there, one that reads and writes to storage as a Max plug-in, the other that accesses these streams as a stand-alone application, which also provides a C++ API for doing so efficiently). This post will explore how we might use this feature purely from Python. Luckily, Custom File Streams are exposed in MAXScript, which means we can also use them in Python, in the pymxs module.

There are two scripts in this example. The first is run in 3ds Max, and saves layer and geometry data to the current 3ds Max scene file. This is fairly simple, it leverages pymxs and the new customFileStream interface to gather some data about the scene and write it to the scene stream. Using pymxs, we can use the MAXScript interface customFileStream, which implements a method to write string data (writeStream), and another to write an array of string data (writeStreamArray). The interface implements several other methods for manipulating streams, you can see the documentation here. We also register ourselves as a callback, so the file stream data gets updated every time the scene is saved.

The second script can be run outside of Max, and has no dependencies on the MaxPlus or pymxs Python modules. It does, however, depend on an OLE structured storage utility library for Python called olefile (see https://olefile.readthedocs.io/en/latest/Install.html).

Script #1:

# custom_stream_scene_data.py
import MaxPlus
import pymxs
rt = pymxs.runtime

# create coerce as string fn
MaxPlus.Core.EvalMAXScript("fn asStr s = (return s as String)")

def getLayers():
layers = []
for idx in range (rt.layermanager.count):
layer= rt.layermanager.getLayer(idx)
l = '{}:{}'.format(layer.name, idx)

layers.append(l)

return layers

def getGeometry():
geom = []
for obj in rt.geometry:
geom.append(rt.asStr(obj))
return geom

def saveSceneStream():
# current scene file name:
sceneName = rt.maxFilePath + rt.maxFileName

geom = getGeometry()
layers = getLayers()

# write some data:
rt.customFileStream.writeStream( sceneName, "MyString", "This is some string data" )
rt.customFileStream.writeStreamArray( sceneName, "MyGeometry", geom )
rt.customFileStream.writeStreamArray( sceneName, "MyLayers", layers )

print 'Scene Data Saved'

# register ourselves as a callback to run whenever the scene is saved
rt.Execute ('pyCallback = undefined')
rt.pyCallback = saveSceneStream
rt.callbacks.addScript(rt.Name('filePostSave'), 'pyCallback()')
 

This is pretty straight forward. We create a function to write all the geometry and layers to arrays in the current file’s custom file stream. It uses a trick to get around some MAXScript syntax that can’t be replicated in Python by wrapping the “as String” coercion in a function on the MAXscript layer, and then calling that from the pymxs.runtime. To register as a callback, we first create an undefined variable in the MAXScript layer, and then connect it to our Python function.

Script #2:

# Read custom file stream data from a Max file in Python

import olefile

# set this to your file
f = r'D:\My Documents\3ds Max\scenes\filestream_test.max'

def cleanString(data,isArray=False):
# remove first 6 bytes + last byte
data = data[6:]
if isArray:
data = data[:-1]
return data

with olefile.OleFileIO(f) as ole:
ole.listdir()
stream = ole.openstream('CustomFileStreamDataStorage/MyString')
myString = stream.read().decode('utf-16')
myString = cleanString(myString)

stream = ole.openstream('CustomFileStreamDataStorage/MyGeometry')
myGeometry = stream.read().decode('utf-16')
myGeometry = cleanString(myGeometry, isArray=True)
myGeometry = myGeometry.split('\x00')

stream = ole.openstream('CustomFileStreamDataStorage/MyLayers')
myLayers = stream.read().decode('utf-16')
myLayers = cleanString(myLayers, isArray=True)
myLayers = myLayers.split('\x00')
print ("My String: {}\nMy Geometry: {}\nMy Layers: {}".format (myString, myGeometry, myLayers))

Here we open the Max scene file and read our three streams (always located under ‘CustomFileStreamDataStorage’). The first 6 bytes of the stream are used by Max to set flags, such as persistence (see the SDK documentation for Custom File Streams for more information). We’re throwing those away, but you will want to understand them if you want to write data to these streams outside of Max. We detect the null Unicode character \x00 as a delimiter for our arrays.

For my test scene I get output that looks like this:

My String: This is some string data

My Geometry: ['$Teapot:Teapot001 @ [4.661083,23.481279,0.000000]', '$Sphere:Sphere001 @ [49.097572,-28.429771,31.312767]', '$Box:Box001 @ [-72.083115,-20.333820,0.000000]', '$Plane:Plane001 @ [-53.787693,33.141487,0.000000]', '$Torus:Torus001 @ [-37.467735,-64.871323,0.000000]']

My Layers: ['0:0', 'Layer001:1', 'MyAwesomeLayer:2', 'Layer002:3']

I hope this example gives you some ideas on how you can leverage custom file streams in 3ds Max for your own workflows.

Posted By
Tags
  • 3ds Max
0 Comments
To post a comment please login or register
*Save $66 per month on Autodesk's Suggested Retail Price (SRP) when purchasing 1 year term 3ds Max or Maya subscription.