Grabbing The Material Preview

Dec 9, 2014 by     No Comments    Posted under: 3dsMax, DotNet, Maxscript, pipeline
That's not going to work...

That’s not going to work…

I’ve recently started to figure out a shader database for our lighting and rendering pipeline, so needed to think about how to implement this as a complete system. The core would be to provide an easy way of storing materials and associated metadata to enable easy searching for shaders based on certain keywords, like type or speed rating. With C#, its comparatively easy to construct such a system as you can serialise the data to XML and use some great search methods via Language Integrated Query, or LINQ. If you aren’t sure about what LINQ is capable of, the webulator is awash with great information about it, and it’s featured in a couple of my previous posts up until now.

So in planning a task like this, I would guess that secret of any good shader library interface would be the ability to provide a snapshot of the material thumbnail from the material editor. So if you’ve tried to do this in the past it is at best a hacky ball-ache, and at worst just not possible. The common approach, passed down from the wisdom of the elders of Maxscript is to render sphere in your current scene, setting up a camera and lights then restoring the scene back after this has completed. But with renderers like vray being used more and more, it’s a painful process. Not to mention it feels so tantalisingly close. I mean, its SITTING THERE IN FRONT OF YOU! JUST GIVE IT… ahem.

Given a fresh set of eyes with the benefit of distance from my previous attempts I think I’ve come up with a method that is intimately nothing more than a hacky ball-ache, but concludes that it is in fact possible. So what is it that is so unreachable about the material editor preview window? In past times,  you would have to see if there is maxscript access to a particular part of a UI. Now, with dotnet in maxscript we can pimp this procedure with some window hooks to redress the lack of maxscript access. My approach to a lot of automation in max is to break down and understand the procedure, so there are a few things that we need to be able to do with the material editor in order for this to work.

Switch to the compact (or old school) material editor and display it
Highlight to material we want
Double click the swatch to present the preview window
Get the window handle
Somehow capture the bitmap of the preview window
Save it


So to do this, we’re going to need some lower level automation classes. This is where our old friend pInvoke comes in handy. There are some useful methods available to us if we can create a nice dotnet assembly of the fly with the methods that we want to use.

fn CreatePrintWindowOps forceRecompile:on = if forceRecompile do
source = "using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Diagnostics;

class PrintWindowOps
public static extern bool GetWindowRect(System.IntPtr hWnd, out RECT lpRect);
public static extern bool GetClientRect(System.IntPtr hWnd, out RECT lpRect);
public static extern bool PrintWindow(System.IntPtr hWnd, System.IntPtr hdcBlt, int nFlags);
public static extern int SetCursorPos(Int32 x, Int32 y);

private const int MOUSEEVENT_LEFTDOWN = 0x0002;
private const int MOUSEEVENT_LEFTUP = 0x0004;

[DllImport(\"user32.dll\", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern void mouse_event(int dwFlags , int dx , int dy, int cButtons, int dwExtraInfo);

public static extern void MoveWindow(System.IntPtr hWnd, int x, int y, int w, int h, Boolean bRepaint);

public static void DoubleClick(int x, int y)
SetCursorPos(x, y);
mouse_event(MOUSEEVENT_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENT_LEFTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENT_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENT_LEFTUP, 0, 0, 0, 0);

public static Rectangle GetWindowRectangle(System.IntPtr hwnd)
RECT rc;
GetWindowRect(hwnd, out rc);
return new Rectangle(rc.Location.X, rc.Location.Y, rc.Width, rc.Height);

public static Rectangle GetClientRectangle(System.IntPtr hwnd)
RECT rc;
GetClientRect(hwnd, out rc);
return new Rectangle(rc.Location.X, rc.Location.Y, rc.Width, rc.Height);

public static Bitmap PrintWindow(System.IntPtr hwnd)
RECT rc;
GetWindowRect(hwnd, out rc);

Bitmap bmp = new Bitmap(rc.Width, rc.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Graphics gfxBmp = Graphics.FromImage(bmp);
System.IntPtr hdcBitmap = gfxBmp.GetHdc();

PrintWindow(hwnd, hdcBitmap, 0);


return bmp;

public struct RECT
private int _Left;
private int _Top;
private int _Right;
private int _Bottom;

public RECT(RECT Rectangle) : this(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom)


public RECT(int Left, int Top, int Right, int Bottom)
_Left = Left;
_Top = Top;
_Right = Right;
_Bottom = Bottom;

public int X {
get { return _Left; }
set { _Left = value; }
public int Y {
get { return _Top; }
set { _Top = value; }
public int Left {
get { return _Left; }
set { _Left = value; }
public int Top {
get { return _Top; }
set { _Top = value; }
public int Right {
get { return _Right; }
set { _Right = value; }
public int Bottom {
get { return _Bottom; }
set { _Bottom = value; }
public int Height {
get { return _Bottom - _Top; }
set { _Bottom = value + _Top; }
public int Width {
get { return _Right - _Left; }
set { _Right = value + _Left; }
public Point Location {
get { return new Point(Left, Top); }
set {
_Left = value.X;
_Top = value.Y;
public Size Size {
get { return new Size(Width, Height); }
set {
_Right = value.Width + _Left;
_Bottom = value.Height + _Top;

public static implicit operator Rectangle(RECT Rectangle)
return new Rectangle(Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height);
public static implicit operator RECT(Rectangle Rectangle)
return new RECT(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom);
public static bool operator ==(RECT Rectangle1, RECT Rectangle2)
return Rectangle1.Equals(Rectangle2);
public static bool operator !=(RECT Rectangle1, RECT Rectangle2)
return !Rectangle1.Equals(Rectangle2);

public override string ToString()
return \"{Left: \" + _Left + \"; \" + \"Top: \" + _Top + \"; Right: \" + _Right + \"; Bottom: \" + _Bottom + \"}\";

public override int GetHashCode()
return ToString().GetHashCode();

public bool Equals(RECT Rectangle)
return Rectangle.Left == _Left && Rectangle.Top == _Top && Rectangle.Right == _Right && Rectangle.Bottom == _Bottom;

public override bool Equals(object Object)
if (Object is RECT) {
return Equals((RECT)Object);
} else if (Object is Rectangle) {
return Equals(new RECT((Rectangle)Object));

return false;


csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
compilerParams.ReferencedAssemblies.Add "System.dll"
compilerParams.ReferencedAssemblies.Add "System.Drawing.dll"
--compilerParams.ReferencedAssemblies.Add "System.Diagnostics.dll"
compilerParams.ReferencedAssemblies.Add "System.Runtime.InteropServices.dll"

compilerParams.GenerateInMemory = true
compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)

-- this is very useful to debug your source code and check for referencing errors
if (compilerResults.Errors.Count > 0 ) then
errs = stringstream ""
for i = 0 to (compilerResults.Errors.Count-1) do
err = compilerResults.Errors.Item[i]
format "Error:% Line:% Column:% %\n" err.ErrorNumber err.Line \
err.Column err.ErrorText to:errs
MessageBox (errs as string) title: "Errors encountered while compiling C# code"
return undefined

(compilerResults.CompiledAssembly).createInstance "PrintWindowOps"

The class we are going to use is mainly constructed of the following methods

GetWindowRect – Gets the boundaries of the current window from the supplied handle
GetClientRect – Gets the boundaries of the parent window from the supplied handle
MoveWindow – Reposition the window on screen
PrintWindow – Captures a bitmap of the current window from the supplied handle
SetCursorPos – Moves the mouse via code
DoubleClick – Clicks the mouse via code

See, I told you it was hacky. We’re going to do everything to bring up the material preview window via code, including moving the mouse and double clicking it. Its worth noting that in 3dsMax 2014, a few methods were added to the windows structure, namely get/setwidowpos. This would make some of the code from the custom assembly redundant, but since people might not be up to that version yet, I’ve kept it as broad as possible. The code is heavilly commented so you can see where you can replace any calls if you needed.

So once the Material Editor is open, we’ll use DialogMonitorOPs to register the window opening, return the correct handle and capture the bitmap. Why do we need DialogMonitorOPs? Oddly, the material editor is a strange beast. The preview panel inside the editor window appears to be parented to a different window dll than the 3dsmax.dll that all other Ui elements link to. Using sometihng like spy++ to look into the window handles yielded nothing as far as the material editor was concerned. But DialogMonitorOps did, and told us that the material editor is triggered by the res1.dll. I have found precisely ZERO information about this on any site, including the SDK docs so this is one of those things that I just had to wear rather than understand.

Although, since DialogMonitorOps reports some useful extra data, we can sandwich the actual code to discover the preview into the callback that we register. So the process is now…

Switch to the compact (or old school) material editor and display it
Highlight to material we want
Register a function via DialogMonitorOps
Double click the swatch to present the preview window
Get the window handle from the data provided via DialogMonitorOps
Capture the thumbnail
Unregister DialogMonitorOps Callback

There’s a perfect example of how to use DialogMonitorOps in the MXS Help. I used this to troubleshoot my method in order to find a solution.

Quirks of using this procedure

DialogMonitorOps gives us a window handle of the panel that contains the preview. The classname of the preview window is DragDropWindow, and we know that it is the correct object as we have only opened the preview window by code. However, calling PrintWindow on this handle gives an empty, black bitmap. I again wasn’t sure how to deal with this. However, calling PrintWindow on the client window gives us a capture of the bitmap. So all we need to do is work out the crop window of the child control and draw this region into a new bitmap to complete the preview.

dropPanelHwnd = for i in (UIAccessor.GetChildWindows WindowHandle) where (UIAccessor.GetWindowClassName i) == "DragDropWindow" do exit with i
dnDPH = (dotnetobject "System.IntPtr" dropPanelHwnd)

rect = cpo.GetWindowRectangle dnDPH
loc = cpo.GetClientRectangle dnDPH
-- get location data from the main preview panel
wRect = cpo.GetWindowRectangle wPtr
clientCropCoords = dotnetobject "system.drawing.rectangle" (rect.x-wrect.x) (rect.y-wrect.y) loc.width loc.height
mbmp = cpo.PrintWindow wPtr
cBmp = mbmp.clone clientCropCoords (dotnetclass "System.Drawing.Imaging.PixelFormat").DontCare

if not doesFileExist outputDir then makeDir outputDir
-- save a png file of the preview
bmpOutput = (pathconfig.appendPath outputDir (matName + ".png")) bmpOutput

The full method

Here’s the full code to capture the thumbnail. The really useful thing is as we have the window handle, we can resize it off-screen to any size we like. Which means we can have high-res previews up to any size. Obviously, they are not perfect quality at higher resolutions, but it gives us a better size than the default material thumbnail.

filein ""
DialogMonitorOPS.unRegisterNotification id:#matPreviewOps  
Max2014 has new windows structure functions that could replace printwindowops.GetWindowRectangle
I'm keeping it in my assembly though to keep a little backwards compatibility with earlier versions of max, but I've 
included the code for 2014+ for reference

-- change here to alter the thumbnail size
local thumbsize = 300
local cpo = CreatePrintWindowOps()

fn grabMaterialPreview debug:false outputDir:@"C:/MatPreview/" =
	-- if we are at this point, it means that we know the material editor is open
	-- this has been called via DialogMonitorOPS
	-- we are pretty sure this will be the material preview window as we've just clicked in the drop panel of the material editor
	WindowHandle = DialogMonitorOPS.GetWindowHandle()	
	matName = meditMaterials[activeMeditSlot].name
	dllName = UIAccessor.GetWindowDllFileName WindowHandle

	-- check the dll name. Material Editor doesnt use #max as the window parent
	-- but a dll called res1.dll
	if dllName != undefined and filenamefromPath dllName == "res1.dll" then
	--this is the entire material window (including the update button etc..)
	wPtr = dotnetobject "System.IntPtr" WindowHandle			
	if debug then format "Dialog Window Handle: %\n" WindowHandle
	-- get the window positon data 
	cMEPpos = cpo.GetWindowRectangle wPtr	
	--get the current position of the dialog in max2014
	--cMEPpos = windows.getWindowPos WindowHandle	
	-- set the thumbnail size by code (calculated by size wanted plus 56)
	-- this also forces the window offscreen 
	format "ThumbSize %\n" thumbsize
	cpo.MoveWindow wPtr	-1000 -1000 (thumbsize+56) (thumbsize+56) true
	-- Max2014
	-- windows.setWindowPos WindowHandle -1000 -1000 (thumbsize+56) (thumbsize+56) true	
	-- get the preview window handle now we have the hwnd of the material preview	
	dropPanelHwnd =  for i in (UIAccessor.GetChildWindows WindowHandle) where (UIAccessor.GetWindowClassName i) == "DragDropWindow" do exit with i
	-- this is the material preview handle- 
	-- if you try to pass this handle to printWindow, it will be BLANK 
	-- so we work out the client are and crop the bitmap from the capture of the main window
	dnDPH = (dotnetobject "System.IntPtr" dropPanelHwnd)

	-- universal method via Pinvoke
	-- get location data from the material preview window
	rect = cpo.GetWindowRectangle dnDPH
	loc = cpo.GetClientRectangle dnDPH
	-- get location data from the main preview panel
	wRect = cpo.GetWindowRectangle wPtr
	--max2014 onwards
	--mxsWP_prev = windows.getWindowPos dropPanelHwnd
	--mxsWP_me = windows.getWindowPos WindowHandle
	--format "mxsWP_prev %\n" mxsWP_prev
	--format "mxsWP_me %\n" mxsWP_me
	format "rect % % % % %\n" rect rect.x rect.y rect.width rect.height
	format "wrect % % % % %\n" wrect wrect.x wrect.y rect.width rect.height
	format "loc % % % % %\n" loc loc.x loc.y loc.Width loc.height

	clientCropCoords = dotnetobject "system.drawing.rectangle" (rect.x-wrect.x) (rect.y-wrect.y) loc.width loc.height
	--windows.getWindowPos 2014+ method
	--clientCropCoords = dotnetobject "system.drawing.rectangle" (mxsWP_prev.x-mxsWP_me.x) (mxsWP_prev.y-mxsWP_me.y) mxsWP_prev.w mxsWP_prev.h

	mbmp = cpo.PrintWindow wPtr	
	cBmp = mbmp.clone clientCropCoords (dotnetclass "System.Drawing.Imaging.PixelFormat").DontCare

	if not doesFileExist outputDir then makeDir outputDir
	-- save a png file of the preview
	bmpOutput = (pathconfig.appendPath outputDir (matName + ".png")) bmpOutput

	--put the preview dialog back on screen	
	--windows.setWindowPos WindowHandle cMEPpos.x cMEPpos.y cMEPpos.w cMEPpos.h true	
	UIAccessor.CloseDialog WindowHandle
	DialogMonitorOPS.Enabled = false
	DialogMonitorOPS.unRegisterNotification id:#matPreviewOps
	-- if you want a post preview, uncomment this line
	--display (openBitmap bmpOutput)
	-- or a confirmation message
	--messagebox "Material Preview captured sucessfully!" title:"Material Capture"
		--format "Bypass Handle: %\n" WindowHandle
) -- end grabMaterialPreview

fn captureCurrentMeditMaterial mtlSlot:activeMeditSlot = 
	wasSlate = false
	goCapture = true
	if MatEditor.mode == #advanced then
		if querybox "Material Capture currently works on the compact material editor. Do you want to switch?" title:"Material Capture" then
			wasSlate = true
			MatEditor.mode = #basic		
		else goCapture = false

	if goCapture then
	-- open the material editor and get the window handle
	if not (MatEditor.isOpen()) then ( MatEditor.Open() )  
	matEditHwnd  = for w in (UIAccessor.GetPopupDialogs()) where matchpattern (UIAccessor.GetWindowText w) pattern:"Material Editor*" collect w 
	if matEditHwnd.count == 1 then 
		if mtlSlot != undefined then
			if mtlslot>=1 and mtlSlot<= 24 then activeMeditSlot = mtlSlot
		dropPanel = for m in (windows.getChildrenHWND matEditHwnd[1]) where (matchpattern m[4] pattern:"DragDropWindow") do exit with m
		if isKindof dropPanel array then
			-- need to cast the mxs hwnd pointer into a dotnet IntPtr,
			-- otherwise max internally casts to the wrong type
			dpHwnd = dotnetobject "System.IntPtr" dropPanel[1]
			dpp = cpo.GetWindowRectangle dpHwnd
			-- form max2014 onwards, you could use this to replace cpo.GetWindowRectangle
			-- dppMXS = windows.getWindowPos dropPanel[1]
			-- set the thumbsize (this is used in the grabMaterialPreview function)
			-- I'd pass it in the function call itself, but im not sure how to do this in DialogMonitorOPS.RegisterNotification
			-- thumbsize = 700
			-- enable DialogMonitorOPS to register the capture function
			DialogMonitorOPS.RegisterNotification grabMaterialPreview id:#matPreviewOps
			DialogMonitorOPS.Enabled = true
			-- push any updates (to update UI on batch capture operations)
			-- Hackylicious! double click the panel to force the preview window to open
			-- we trigger the click in the center of the MEdit dialog is case the user moves the mouse during capture
			cpo.doubleclick (dpp.x + (dpp.width/2)) (dpp.y + (dpp.height/2))
			-- the same call in max2014 onwards	using the mxs hwnd		
			-- cpo.doubleclick (dppMXS.x + (dppMXS.w/2)) (dppMXS.y + (dppMXS.h/2))
	-- if you want to revert the editor state back to slate, uncomment this line
	--if wasSlate then MatEditor.mode = #advanced 
) -- end captureCurrentMeditMaterial


I have the C# assembly code in a separate file, and call it with a filein at the start of the main function.

Some testing code


-- create some random materials in the material editor...
for i = 1 to meditmaterials.count do meditmaterials[i] = standard diffuse:(color (random 0 255) (random 0 255) (random 0 255)) name:("Standard_" + formattedprint i format:"03i")


-- grab the entire material editor in one go...
for i = 1 to meditmaterials.count do captureCurrentMeditMaterial mtlSlot:i

To capture the current material in the material editor, just call


Result! A folder full of material previews to be used in any number of ways!


I Don’t Need Regular Expressions

Sep 1, 2014 by     11 Comments    Posted under: 3dsMax, DotNet, Maxscript, Tips and Tricks


For a long time there were two things in life that truly scared me and I avoided at all costs. One was the soundtrack from Frozen, the other was Regular Expressions. I came to realise that my avoidance techniques were simply a defence mechanism towards something I didn’t understand.  After trying to understand both of my fears, I have come to the conclusion that I’m right to avoid the soundtrack from Frozen, but Regex is my best friend.

In this article I’ll be discussing Regex and try to de-mystify their usage enough for you to use them in basic scripts in 3dsMax. It took me a long time to realise just how useful they can be, but it is sometimes difficult to get past their verbosity as a programming concept. It is not going to be a tutorial of the more complex aspects of Regex, nor do I consider myself and expert, so I’ll just be looking at some usable examples based on 3 occasions that they have really helped me so that you can use them in your 3D pipeline scripts.

What are they used for?

Put simply, Regular Expressions are a way of searching and manipulating strings. But wait – Maxscript has a more than a few ways of handling strings, so let’s consider first what we have already in order to work out if we actually need another way. Here are a list of the most common string manipulation methods available to maxscript.

MatchPattern --A method to decide if a string is similar to a particular pattern
substituteString -- Swaps one string for another
filterString -- Splits a string into pieces
findString -- Locates the index of a particular string
subString -- Creates a new string based on a portion of another
stricmp -- another method to compare strings, very useful in qSort operations

These are very useful, and I use them all the time. They can pretty much cover most scenarios and they are well documented in the help. So why use a different method? Most occasions of pattern matching in a a string can be handled by matchpattern, but what if you are looking for a particular selection of characters, or capitals, or numeric digits? If you need a little more control over this, then unless you want to use filterstring to split and analyse each part of the string, regex is easier to use.

Let’s consider 3 examples of instances where it might be necessary to manipulate strings in our day to day work.

  1. Validating a filename to see if it conforms to a pipeline structure
  2. Selecting elements from hierarchies with the scene explorer
  3. Parsing a filename argument for a commandline script

Validating a Filename

One thing that needs to happen a lot is checking if a filename conforms to a particular pattern. We have a saving routine that automatically backs up and versions our older files into a separate location. This means there is always one ‘current’ version of a file like a rig or model that anyone can work on.  Let’s say that there are a few pieces of information that we want to be able to check for, like the name of the asset, the type, the artists’s intials who last worked on the file, the major file version, and the minor increment.

So we could have a filename like this:

filename = "Rabbit_RIG_LR_v01_07.max"

If we wanted to ask the question “Is this file in the correct format?” we could try to use matchpattern…

matchpattern filename pattern:"*_*_??_v??_??.max"

It gets us pretty close. Note that you can use the question mark in matchpattern to indicate that there are a certain number of characters, rather than the wildcard pattern * which allows any number of characters.  The problem with this is that we are looking for a specific configuration of numerals and letters, so this would also be a match:

filename = "Rabbit_RIG_lr_vEr_LR.max"
matchpattern filename pattern:"*_*_??_v??_??.max"

Ironically, the pattern we have constructed for matchpattern is about as complex as the regex string needed. There’s no regular expressions available to maxscript, so we can use dotnet instead. Here’s how you do it..

rx = dotnetobject "System.Text.RegularExpressions.Regex" <<RegexString>>

If you want to use lots of different regex patterns, you could just use the class and construct them on the fly, but when you have a single pattern that you want to validate, it’s easier to set this up at the start.  The method that regex has to determine that it is a match is the crazily named .isMatch()

So regex is pretty similar, but you can differentiate between letters and numbers. To check for a number, you can use the following



and if you need to check for a certain number of digits, you can use the curly braces


[0-9]{2,4} — Any number of digits between 2 and 4

This is very useful. Checking for words and characters is easy, broadly speaking you can use the fullstop, asterisk or /w. If you wanted to check a render was part of a 4 numeral padded sequence for example, you could do this:

rx = dotnetobject "System.Text.RegularExpressions.Regex" "\w_[0-9]{4}.*"
filename = "Characters_Main_Velocity_v01_0634.jpg"
rx.isMatch filename

So going back to our regex, we need to check for the version, two digits, an underscore, and another two digits. So we are adding this square brace syntax into the string that we need to check for…

-- remember - a matchpattern version of this would be "*_v??_??.max"

So the regex to check just the version structure is correct, is this:

filename = "Rabbit_RIG_LR_v01_08.max"
rx = dotnetobject "System.Text.RegularExpressions.Regex" "\*_v[0-9]{2}_[0-9]{2}.max"
rx.isMatch filename

We can extend this to incorporate the two digit intials string too. Using the same square bracket syntax, we can specify that we can two letter digits, but they should also be capitals.

-- [A-Z]{number of letters}
rx = dotnetobject "System.Text.RegularExpressions.Regex" "\*_[A-Z]{2}_v[0-9]{2}_[0-9]{2}.max"
rx.isMatch filename

So this is almost there. We now just need to check there is a name, and the type of asset. We could check for specific instances of words depending on what part of the pipeline we want to identify, but we just need to add the existence of the underscore between them. We also don’t want numbers in the asset type. We can use the [a-zA-Z]+ to indicate that we want any number of letters.

filename = "Rabbit_RIG01_LR_v01_08.max" 
rx = dotnetobject "System.Text.RegularExpressions.Regex" ".*_[a-zA-Z]+_[A-Z]{2}_v[0-9]{2}_[0-9]{2}.max"
rx.isMatch filename
filename = "Rabbit_RIG_LR_v01_08.max"
rx = dotnetobject "System.Text.RegularExpressions.Regex" ".*_[a-zA-Z]+_[A-Z]{2}_v[0-9]{2}_[0-9]{2}.max"
rx.isMatch filename
-- true

So there it is – it looks a little more daunting when you look at it in the complete form, but when you break it down into the component parts, it is actually not as bad. For sure, there will probably be more efficient regex strings to do this kind of thing. In the example for matching only letters, we could have said [^0-9] which, means anything but numbers. Many times you’ll look for examples and people have tried to make the truncation of regex strings an art form within itself. However, In this slightly longer form it means it is easy to understand.

Selecting elements with the scene explorer

One fact about the scene explorer is that you can use regex to apply a custom selection filter. Let’s use what we learned in the previous lesson to apply this into the scene explorer. Firstly, we will need to set the scene explorer to use regex as the search parameter. Press H and select this option.


In my test scene, I have run the following script to create 1000 teapots at random locations.

for i = 1 to 1000 do
             teapot name:("Teapot" + (formattedprint ((random 0 9999) as integer) format:"04i") + "_Mesh__MF") pos:(random [0,0,0] [1000,1000,1000])


This script also gives them a random index name between zero and 9999.


So lets assume a hypothetical situation where we wanted to select all teapots with indices between 2000 and 3999, and 6000 and 7999. How would we do this without manually performing an arduous selection across much of the dialog? Based on what we did before, we can format a regex pattern like this:


However, we need to specify the other range. We can use the (|) notation for this.  So to combine the ranges [2-3][0-9]{3} and [6-7][0-9]{3} we end up with:


Pasting this string into the Find box automatically selects any object named Teapot within the range we specified before.


The crowd says teapot selecta!


If you want to try something cool based on this, try entering




into the find field. It will select all even or odd numbered teapots in the scene. From what we have learned so far, you can see the pattern of how I’m differentiating between the two types. The uses for this are not just applicable to teapots, I’ve used it for more complex selections in rig hierarchies. But it is good to know it is there.

Parsing a commandline argument

Using commandline applications is going to be covered in another post as it’s too useful to skirt over in just this small section. But I wanted to share an example of a script I use for converting EXR files into half-res Quicktimes for fast and easy file checking. If you have something like Deadline, it is easy to use Draft or a custom python script to do something like this. You can even start Nuke in terminal mode to convert but it might be a little overkill to use a Nuke license for something like this. There is an amazing open source program called DJV that has a commandline option that can be used to do this exact thing. You can get DJV from here:


The great thing is that it supports Open EXR without having to compile the damn thing yourself. This is very useful for pipeline imaging automation.  What DJV needs is a very specific filename argument. If we want to transcode a file sequence, we have to format the filename into the following format :


So when writing a routine to automate this, I decided that as Regex and Me were like Riggs and Murtaugh, I’d use it for creating the commandline argument. The full code is below. Take a look and see how I’m using regex to split and concatenate the file array into a single line ready to process. I’ll step through this script in a later post if you can’t get the gist of it, but its not really important if you just want to have an open source and free method of converting renders. The cool thing is you can use a post render script or deadline task to automatically generate this, meaning you can arrive in the morning and make a fast preliminary check of renders without having to load large EXR sequences, or needing a 3rd party player like PD or RV.

global djv

Struct djvOps
-- i've declared these in the struct, you could do it at instantiation also.
x86Path = @"C:\CMD_useful\DJV\x86\djv_convert.exe",
x64Path = @"C:\CMD_useful\DJV\x64\djv_convert.exe",
input = undefined,
resolution = 0.5,
isExr = false,
openOutput = true,

-- includes gamma correction in the argument string for OpenEXR
fn cmdArgs inStr outStr res =
if djv.isExr then
(inStr + " " + outStr + @" -load OpenEXR Gamma 1.0 -scale " + (res as string) )
(inStr + " " + outStr + @" -scale " + (res as string) )

-- function djv_parseImageSequenceToInputString
-- We Need a string to pass to the DJV commandline
-- this is a combination of the first and last frames, with a hyphen between them
-- an example of how DJV needs a string output is "C:\Users\LoneRobot\Documents\3dsMax\renderoutput\sq_PassTest\T_0000-0100.exr"
-- This works only with padded sequences between 4 and 5 digits
fn parseImageSequenceToInputString =
if djv.input != undefined and doesfileexist djv.input then
local seq
if classof djv.input == string then
-- Regex replace the filename padding and change for wildcard to get all of the image sequence
rxPadding = dotnetobject "System.Text.RegularExpressions.Regex" "[0-9]{4,5}"
wcPattern = rxPadding.replace djv.input "*" 1
--format "wcPattern - %\n" wcPattern
if wcPattern as name != djv.input as Name then seq = sort ( getfiles wcPattern )
else seq = image

if seq.count > 0 then
djv.isExr = getFilenameType (amin seq) == ".exr"
head = getfilenameFile (amin seq)
tail = getfilenameFile (amax seq)
-- you can reuse the same regex pattern, this time extracting the padding
h1 = (rxPadding.match head).groups.item[0].value
t1 = (rxPadding.match tail).groups.item[0].value

if h1 as integer != undefined and t1 as integer != undefined then
rxPadding.replace djv.input (h1 + "-" + t1) 1
else djv.input
-- default resolution parameter is half res
fn createQuickTimeFromEXR res:djv.resolution =
-- Add the path to the DJV executable
-- Transcoding to quicktime would need the x86 version,
-- Image Transcoding can be used with either the x64 or x86 versions
-- These paths need to reflect where you installed DJV to
if doesfileexist djv.X86Path then
-- pass the first frame in the sequence to the regex function
-- this is a 100 frame sequence
theFile = parseImageSequenceToInputString()
--theFile = "C:\Users\LoneRobot\Documents\3dsMax\renderoutput\sq_PassTest\T_0000-0100.exr"
-- insert theFile into the CMD argument stream
if theFile != undefined then
--setup the file output
filePath = getfilenamepath theFile
fileName = getfilenamefile theFile
outDir = pathconfig.appendpath filePath "_preview"
if not doesfileexist outDir then makedir outDir
theOutPut = pathconfig.appendpath outDir (fileName + ".mov")
-- we are going to create a half res quicktime
args = djv.cmdArgs thefile theOutput res
--theFile + " " + theOutPut + @" -load OpenEXR Gamma 1.0 -scale " + (res as string)
-- create the commandline process
proc = dotnetobject "system.diagnostics.process"
-- make sure this is the executable you want to use
proc.StartInfo.FileName = djvX86Path
proc.StartInfo.Arguments = args
proc.StartInfo.RedirectStandardOutput = true
-- needs UseShellExecute set to false in order to redirect IO streams
proc.StartInfo.UseShellExecute = false
proc.StartInfo.CreateNoWindow = true
format "**************************\nCreating Quicktime from EXR sequence\n"
-- make sure we don't freeze
reader = proc.StandardOutput
while (l = reader.readline()) != undefined do
format "%\n" l
if djv.openOutput then shelllaunch "explorer.exe" ("/e," + outDir)
else messagebox ("There was a problem with the image you supplied.\n\n" + djv.input + "\nFile Exists? : " + (if doesfileexist djv.input then "Yes" else "No")) title:"ooops" beep:false
else messagebox ("You don't appear to have DJV installed in the location specified.\n\n" + djvX86Path) title:"ooops" beep:false
)-- end struct

-- example usage
djv = djvOps()
-- set the first frame of the exr sequence to the struct input

So that concludes things for now. Please check back soon for new posts. Yes I know I’ve been rubbish. Thanks to Rotem Schiffman for his maxscript syntax brush, it makes these posts much more readable.

What we are today comes from our thoughts of yesterday

Apr 11, 2012 by     5 Comments    Posted under: DotNet

Apologies for a slightly existential tone to my latest blog post – I was reading the excellent blog by Maya TD Hamish Mackenzie, and it did get me thinking. It’s a strange conclusion to come to when you realise that the thing that you were learning to make your life as an animator easier ends up taking over and stopping you from doing something that you love.

But that’s just one side of the argument.

In order to get to that stage, there is a choice that is made over which discipline to focus on. You have to be serious because there is no short learning curve in 3D. Any choice you make about career direction has to be for the same reason that you started anything in this industry to begin with – because you love doing it. Perhaps, part of the consternation is the idea that programming isn’t quite as glamorous as a more signposted artistic discipline. But there is no doubt that a TD requires a balance of logic and creativity that is difficult to find, as they are not natural bedfellows. Not only are they difficult to find, they are harder to draw the line between. Are you an artist who codes, or a coder who does art? Sometimes you are both of these. Your brain will tell you if you’re straying more towards the side you dont want to be. Hamish wrote in his post :

…doing animation is something that I think is really important for me as a technical animator.  Using the tools I’ve written, working through the workflows I’ve helped define, having to deal with all the bugs, shortcomings etc of the work environment I’ve helped create puts me in the shoes of my users and forces me to see and understand the implications of my design decisions.  And I think thats really important.

I’d have to agree – I certainly can’t think the same about animation in the way I may have once. I am too hardwired to see bottlenecks and tool potential with any process to allow me to spend the single minded application necessary these days. Hamish said it perfectly :

But the other big thing is the fact that its hard to stop thinking about the technical side of things while I’m animating.  Its been such a part of my mental process for so long that its really hard to turn it off.  And I think this is the biggest thing.  So many parts of the animating process have the potential to be improved, sped up, optimized, made easier etc…  And when I’m animating, all these thoughts are running through my head.  So just pushing those thoughts aside – or at least shelving them for later – is hard.

Many years ago I graduated with a degree in Sculpture. The notion that 10 or so years down the line I would be figuring out custom event delegates in C# wouldn’t have been in the forefront of my mind.  It’s just ironic that you’ve ended up becoming the thing that you wouldn’t actually choose to call yourself. So essentially, us lot are a strange breed. We live in code and dream of drawing. We camelCase shopping lists and hate the fact that we now spell colour, color.

I guess the important thing would be to make sure that you never lose your creative side, and channel that into your new trade.

Be Deep Bop De Doop 010101 #!-$Z#!


Autodesk Webinar

Apr 10, 2012 by     No Comments    Posted under: DotNet

Despite a nagging suspicion that “Webinar” is a made up word, I will be speaking with my colleague Ben Robins about some of our work at Nexus and how 3dsmax features in our animation pipeline.

Details can be found here :

Jamies Jewels

Autodesk site

Two Clicks From Amsterdam

Oct 2, 2011 by     2 Comments    Posted under: 3dsMax, Characters, DotNet, Maxscript, Tips and Tricks

End User Event Roundup

Yes it’s a little on the late side. Due to me starting a new job on the following Monday at Nexus in London my EUE write up has been a little delayed.

So what is EUE? If you don’t already know, it’s a 3d event that has been going for a few years now that allows like-minded 3d enthusiasts and proffesionals to all get together and attend talks. This is all great, but the cherry on the top is when you realise that it’s hosted in a PUB in Utrech.


It was an absolute honour to be asked to speak there this year. There were too many great people that I met to thank them all, but a special mention goes to Bobo, Rune Spaans and Ted Boardman (Both just about the nicest people you’d ever meet in the 3d industry) and to the people I hung out with over the weekend – Johan Boekoven, Yoni Cohen and Gonzalo Rueda. Also, massive thanks extend to Joep Van Den Steen and Michiel Quist who organised the whole thing, and of course to Jamie and Shane from Autodesk who were all great guys.

My talk at EUE was to not just showcase a few of the animation systems that I have written over the last few years, but to get across  the many potential stages of development for tools. Whether you are writing a line of code to make your life easier or trying to establish a pipeline, good tools will persist and ill-thought out ones will not last even one production. There is no correlation between the complexity or the number of lines of code in the success of a tool. Your animators will decide!

MaxScript Lesson Ahoy!

One of the tools that I have tended to re-use over many productions is the LayerControl script. It allows animators to bypass the layer manager and hide layers according to object type. While there are a few ways of achieving this on a node level using AppData or UserProps, you can quickly setup a system like this with a methodical and consistent naming convention. When working, hiding and showing rig controls and meshes on masse become a simpler and quicker affair. This might not sound much, but over a long animation process tools like this save time.

Lets look at how to set this system up.

Firstly, at the rigging stage, you will want to make sure that all of your characters have consistent layer names.

It is the suffixes of these layers that the script will be using to control whether a layer is hidden or not.

Script Stage 1

-- Basic Layer Visibility Control

( local str = “MESH” local ishidden = true for i= 1 to (LayerManager.count-1) where ( (dotnetobject “system.string” (LayerManager.getLayer i).name).endswith str ) do (LayerManager.getLayer i).ishidden = ishidden )

This is a basic piece of code that will hide any layer with the suffix “MESH”. As it stands, its not really useful for anything except to illustrate the code of the script. Note that this has been formatted this way in order to make it clearer, you could add it all into one line. Using where in a loop is a useful trick. In this case, it allows us to perform the hiding of the layer without having to collect the Mesh layers into an array and iterating that. You’ll notice I use a dotnet string method in this. You could just as easily use :

matchpattern (LayerManager.getLayer i).name pattern:(“*”+str)

The dotnet method might be fractionally slower to execute, but since we’re talking a few milliseconds, it’s not going to make a whole lot of difference. I included it to illustrate the dotnet string obejct. While maxscript string methods are great, there are even more methods available to you via the dotnet methods should you need it. Use whichever one you like, it’s not going to make the final script any better or worse. If you’re just getting into scripting and programming, you’ll find a lot of the time you’ll want to make sure the core of the script is working before fiddling around with the UI.

Script Stage 2

The first script was just to establish how the method for hiding and unhiding will work.  The next version builds a basic UI and starts to add the functionality we want. It now works by calling a function that takes two arguments. For novice scripters, a function is something used widely in programming to represent and operation that you will want to re-use multiple times. As a form of shorthand, you just call the function name and pass the values it requires rather than typing the same code out repeatedly. In function calls the extra values are known as arguments. In this case, the suffix string that we want to hide/unhide is the first argument. The second argument is to decide whether the function will hide or unhide the layer. Since this is an either/or type, we use a boolean argument of true or false. So in this code we use a button click to hide the layer (passing the string and the value true) and the button’s right click handler to pass the same string and false to unhide it.

try(destroydialog HideRigs)catch()

rollout HideRigs "" width:73 height:84
	fn LayerVisibiltyBySuffix str ishidden =
	for i= 1 to (LayerManager.count-1) where ((dotnetobject "system.string" (LayerManager.getLayer i).name).endswith str) do ((LayerManager.getLayer i).ishidden = ishidden)

	button btnRIG "Rig" pos:[2,56] width:67 height:23	border:false
	button btnMESH "Mesh" pos:[3,30] width:67 height:23 border:false
	button btnCTRLS "Controls" pos:[3,3] width:67 height:23 border:false

	on btnRIG pressed do LayerVisibiltyBySuffix "RIG" true
	on btnCTRLS pressed do LayerVisibiltyBySuffix "CTRLS" true
	on btnMESH pressed do LayerVisibiltyBySuffix "MESH" true

	on btnRIG rightclick do LayerVisibiltyBySuffix "RIG" false
	on btnCTRLS rightclick do LayerVisibiltyBySuffix "CTRLS" false
	on btnMESH rightclick do LayerVisibiltyBySuffix "MESH" false	

createdialog HideRigs

Script Stage 3

Stage 3 has some improvements in the form of replacing the max controls with some dotnet controls. Have read of the code and I’ll discuss what s going on afterwards.

macroScript ShowHideLayers
toolTip:"Show Hide Layers"
	try(destroydialog HideRigs)catch()

	rollout HideRigs "" width:84 height:324
		local DotNetColorMan = (dotnetclass "managedservices.cuiupdater").getinstance()
		local MlbSelection = #("Angus", "Big_Pig", "Bo", "Cow", "Cowhand_One", "Cowhand_Two", "Crow", "Farmer", "FarmGirl", "Hebaa", "Leonard", "Mini", "Piggy", "Shebaa", "Trinny", "Trotski", "Unicorn", "Winnie")

		fn LayerVisibiltyBySuffix str lbx ishidden =
		if 	lbx.selection.isEmpty then
				for i= 1 to (LayerManager.count-1) where ((dotnetobject "system.string" (LayerManager.getLayer i).name).endswith str) do ((LayerManager.getLayer i).ishidden = ishidden)
				enableaccelerators = true
				for each in lbx.selection  do
					local lay = LayerManager.getLayerfromname (lbx.items[each] +"_"+ str)
					if lay !=undefined then lay.ishidden = ishidden
					enableaccelerators = true

		fn MouseButton args = dotNet.compareEnums args.button (dotnetclass "System.Windows.Forms.MouseButtons").left

		dotNetControl btnRIG "button" pos:[1,56] width:82 height:23
		dotNetControl btnMESH "button" pos:[1,30] width:82 height:23
		dotNetControl btnCTRLS "button" pos:[1,3] width:82 height:23
		Multilistbox lbxCh "" pos:[1,82] width:82 height:18 items:MlbSelection

		on HideRigs open do
		btnRIG.flatstyle = btnMESH.flatstyle= btnCTRLS.flatstyle =(dotNetclass "System.Windows.Forms.FlatStyle").Flat
		btnRIG.backcolor = btnMESH.backcolor= btnCTRLS.backcolor = DotNetColorMan.GetControlColor()
		btnRIG.forecolor = btnMESH.forecolor= btnCTRLS.forecolor = DotNetColorMan.GetTextColor()
		btnRIG.text = "Rig"
		btnMESH.text = "Mesh"
		btnCTRLS.text = "Controls"

		on btnRIG mouseDown sender args do LayerVisibiltyBySuffix "RIG" lbxCh (MouseButton args)
		on btnMESH mouseDown sender args do LayerVisibiltyBySuffix "MESH" lbxCh (MouseButton args)
		on btnCTRLS mouseDown sender args do LayerVisibiltyBySuffix "CTRLS" lbxCh (MouseButton args)

		on lbxCh rightclick do lbxch.selection = #{}

createdialog HideRigs

You’ll see that there is only one handler for each button. So you may be wondering how it passes the required true/false argument with just one call. This is one reason why we use  dotentcontrols instead of max UI controls. When you click a dotnetcontrol you can get some additional provided about which mouse button you have used. This is contained within the args property. The mousedownevent calls a function before passing it’s final argument. This function returns the boolean argument we need. So before handling the mousedown event, it asks this function a question:

fn MouseButton args = dotNet.compareEnums args.button (dotnetclass "System.Windows.Forms.MouseButtons").left

this translates in plain speech to “is the mouse button clicked the left button?”. We use dotnet.compareenums to return either true or false in answering a comparision of the mouse button used to click each button and the enumeration of the left button. Dotnet uses enumerations to simplify data types rather than expecting the user to identify an arbitrary integer code.

The other addition to this script is to add a listbox of character names. These names are hardcoded into an array at the start of the script. You could easily scan a set of character folders and retrieve these names dynamically. You just need to make sure you are consistent with naming or the system will break down.

Script Stage 4

From now on, I wont be posting the entire code for each example, but highlighting the important parts of the new code. Don’t worry though, full code samples will be provided at the end of the post so you’ll be able to see what I mean.  Stage 4 brings in base64 encoded strings to store button bitmaps. There’s no point me going into this as i’ve posted about this before here. In normal circumstances, the base64 struct would be added to the startupscripts as a separate entity so that it executes only once on maxstart.

We have also ramped up the modifier key functionality. I’m personally a fan of having multiple functions on a single button – purely for the fact that you keep the UI as small as possible. There’s nothing worse than cramming a UI with extra buttons that could be easily passed to a shift click variation of the same button. In the case of the layer control, its critical to keep the footprint as small as possible and just add a button with If somebody doesn’t want to use a shft click or ctrl click then they won’t. If they do, you’ve already got the functionality in there. You can’t lose really. I’ve always considered that you’ve memorised a whole load of keyboard shortcuts up to this point, it wont hurt to memorise a couple more. The key is to make them match design patterns that already exist in the software. So if you have shift click to select all the objects in a particular layer, then shift + ctrl click should add these nodes to the current selection, exactly how max appends a ctrl select. It’s just a way of keeping things consistent.

So this list of modifer key functions are as follows –

Left Click – Hide Layer

Right Click – Unhide Layer

Shift Click – Select Nodes on layer

Shift Ctrl Click – Add nodes to selection

Shift Double Click – Isolate layer node selection

Alt-Left Click – Freeze layer

Alt-Right Click – Unfreeze layer

Ctrl – Alt Click – Perform an inverse action – i.e. If one character name is selected, do the hide/unhide action on all other layers EXCEPT the selected one.

Too many? my logic is if they are overkill, people won’t use them. Bear in mind all of the permutations above were as a result of animators asking for them over the course of many productions!

Script Stage 5

Okay, final polish time here. The dotnet multilistbox has been changed to ownerdraw mode. This is more advanced dotnet stuff but it means that you can take control of the appearance of a dotnetcontrol in ways where the original appearance properties do not do what you require.

on dnlbx DrawItem sender args do
			if (dotNet.compareEnums args.state (dotnetclass "DrawItemState").Selected) then
				-- draw the selected state of the listbox
				args.Graphics.FillRectangle selBrush args.bounds -- background colour
				args.Graphics.DrawString dnlbx.Items.Item[args.index] args.font brushes.purple  args.bounds.location.x args.bounds.location.y -- draw the text
				args.DrawBackground() -- this is an inbuild call to draw the default background
				args.Graphics.DrawString dnlbx.Items.Item[args.index] args.font uTextBrush args.bounds.location.x args.bounds.location.y -- draw the text string
			--	args.DrawFocusRectangle() -- draw the focus rectangle last

The comments should explain what is going on.


So that’s it for my EUE talk roundup. I hope you’ve been able to get something out of it. If any part makes someone want to start coding useful tools to help their company productions then it’s all been worthwhile. For anyone reading this that attended my talk, thanks for turning up and not throwing anything. If you have any questions, feel free to contact me.

All versions of the script are available to download below.

If you want a PDF of the visual slide material that I displayed in the background, you can find it here