Articles by " LoneRobot"

SliderMan – Does everything that a slider can

Mar 8, 2015 by     3 Comments    Posted under: 3dsMax, Characters, Maxscript, pipeline, Rigging, User Controls

After many years of rigging in 3dsMax I have to admit I’ve not been a fan of viewport sliders and joysticks. Coming from animation,  I always felt that too much clutter from rigs gets in the way of the character silhouette that you’re supposed to be honing. So I’ve always been a custom attribute squirrel, hiding stuff away in the command panel.

However I was recently rigging some organic tree-like structures, and the rig and the the mesh were such that I felt it was needed to have some form of feedback about the state of the controls twinned with the animation mesh. My rigging sense was tingling – It made me realise that I didn’t have a toolset for making these myself. That’s fine, its not hard to make them, but I thought I’d give myself a couple of train rides to and from work to block something out quickly that I could give back to the community.

So SliderMan is the outcome.

UI

There’s a few options, horizontal, vertical with flipped and centre positioned slider layouts, as well as the traditional joystick setup.

They are resizable (If the option is selected) and there’s some rudimentary alignment tools and a group option to draw a border around them. To resize, just change the width and length properties on the slider border. A joystick has to be square on creation, as the script makes a decision on whether it is a horizontal, vertical or joystick slider on the rectangle dimensions. But the joystick can be changed after creation to a rectangle if needed.

bevel

There’s also a parameter block so you can wire anything you like to them quickly and easily. The values are normalized between 0 and 1 for the default slider, or -1 and 1 for a centered slider.

SliderWire

 

A joystick has two parameters for each direction

JoyWire

That’s pretty much it!

To Use – Just make a rectangle shape that you want to use, or let the script create one for you. There’s a couple of point helpers to handle the resizing and limits, so you can  pop them on a hidden layer in your rig.

Enjoy!

download script

 

 

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

Simples!

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
{
[DllImport(\"user32.dll\")]
public static extern bool GetWindowRect(System.IntPtr hWnd, out RECT lpRect);
[DllImport(\"user32.dll\")]
public static extern bool GetClientRect(System.IntPtr hWnd, out RECT lpRect);
[DllImport(\"user32.dll\")]
public static extern bool PrintWindow(System.IntPtr hWnd, System.IntPtr hdcBlt, int nFlags);
[DllImport(\"user32.dll\")]
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);

[DllImport(\"user32.dll\")]
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);
System.Threading.Thread.Sleep(100);
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);

gfxBmp.ReleaseHdc(hdcBitmap);
gfxBmp.Dispose();

return bmp;
}

[StructLayout(LayoutKind.Sequential)]
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"))
cBmp.save 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 "PrintWindowOpsFN.ms"
clearlistener()	
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"))	
	cBmp.save bmpOutput

	
	gc()
		
	--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"
	true
	)
	else
	(
		--format "Bypass Handle: %\n" WindowHandle
		false
	)	
) -- 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
			--DialogMonitorOPS.ShowNotification()
			
			-- push any updates (to update UI on batch capture operations)
			windows.processPostedMessages()	
			
			-- 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")

matPrevME

-- 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

captureCurrentMeditMaterial()

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

matPrevThumbs

The Path of the Righteous

Oct 31, 2014 by     4 Comments    Posted under: 3dsMax, Maxscript, pipeline

This month’s tutorial is designed for beginner scripters. Anybody who has worked in an environment where the folder structure isn’t locked down will tell you that a Wild West approach always ends badly. What happens if somebody leaves? or is sick and has to pass their working shot over to somebody else? It’s something we have to be aware of and build systems to handle. Conceiving a default set of folders for the type of work you do is a useful starting point to this process, and relevant even if there is only one of you. Most people will want to customise their folder structure, so if you start to design a base folder there’s a couple of approaches.

  • You can make an empty structure and manually copy this when you start a new project
  • You can make a tool that builds the structure automatically

Either approach is fine, but a tool is much COOLER.

 

Take The Right Path

wizard of oz

What might be important is how you construct the paths to your file structure in projects. Here’s the string concatenation way:

"\\\\server\\projectname\\sequence" + "\\shot" + "\\animation" + "\\maxfiles"

Remember that you have the option of adding a forward slash instead of using “\\” but the result for me is the same, it’s a file path that you have to be careful to format properly in order for it to parse correctly. Any portion of the code that misses the path separator could mean an unusable path. You could use stringstream and format, but it’s still a similar sort of concatenation operation.

Pathconfig Structure

pcap

The Pathconfig struct has a few useful methods. It’s likely that (like me) you looked at the first few methods and assumed it was something to do with the built-in max project structure. However, keep going, it has some very useful functions lower down. The single most useful function which I tend to use all the time is appendPath. This is great because you don’t have to use string concatenation, it automatically adds the correct separators for you:

pathconfig.appendPath "\\\\server\\projectname\\bad" "dates"

So this is fine when you’re adding one folder name onto another. You could reason that this is an unnecessary step to something simple. You might be right, but for me it is to do with code clarity. I deal with 1000s of lines of code that has references to a tightly bound folder structure. So when I see appendPath highlighted in a code block, I know exactly what is happening at that point when overviewing code. It’s a subtle thing but allows me to quickly revisit code and see exactly what is happening.

While this might be all fine with a single concatenation, what if you want to build a path from a number of variables? Here is were we can learn from other programming languages. Both dotnet and python have path modules that allow us to construct paths easilly.

--Dotnet Join Path
(dotnetclass "system.io.path").combine #("C:/the", "of", "monarch", "the", "I_am")

# Python Join Path 
from os import path
os.path.join ( "C:/the", "of", "monarch", "the", "I_am" )

so to create something like this in MXS, you can easily use something like:

-- Like the join function in dotnet or python
fn joinPath paths =
(
local fullPath = ""
for i in paths do fullPath = pathconfig.appendPath fullPath (i as string)
fullPath
)

joinPath #("C:/Snakes/Why","did", "it", "have", "to", "be", "snakes")

Some other useful functions in pathconfig

pathConfig.convertPathToRelativeTo <path1> <path2>

I use this to truncate paths if it is too long to display on something like a messagebox. Often, the user knows the root of the project so only really needs to see the important information. Remember, the longer path should be the first argument

fullPath = "\\Server\\Share\\Project\\Scene01\\Shot06\\Animation\\XAF"
rootPath = "\\Server\\Share\\Project"

trPath = pathConfig.convertPathToRelativeTo rootPath fullPath
messagebox ("The file has been written to\n\n" + trPath) title:"PathConfig" beep:false

This is useful to extract the last folder in a filepath

trPath2 = pathConfig.stripPathToLeaf fullPath
messagebox ("The file has been written to the " + trPath2 + " directory") title:"PathConfig" beep:false

The main reason is I know exactly what is happening when I see the stripPathToLeaf function. You can do the same with filterstring, but I think pathconfig is a clearer approach than this:

pathEle = filterstring "\\Server\\Share\\Project\\Scene01\\Shot06\\Animation\\XAF" "\\"
if pathEle.count > 0 then
trPath3 = pathEle[pathEle.Count]
pathConfig.isPathRootedAtBackslash fullPath

This is useful to decide if you are working locally or not, sometimes if an artist drags a file into the viewport they can end up working outside pipeline and I need a way of detecting if this happens.

Building a simple project configuration tool

You can discuss for more hours in a day the nuances of folder structure, but ultimately, any structure is better than no structure. However, you don’t have to create a complex script to do something like this. 3dsMax has a built in project configuration that you can use if you want to setup a basic project structure, but can also create your own from the existing configuration if you need.

pathConfig.doProjectSetupStepsUsingDirectory <ProjectPath>

The useful thing about this call is that it will make an additional file with an .mxp extension. This looks like this:

[Directories]
Animations=.\sceneassets\animations
Archives=.\archives
AutoBackup=.\autoback
BitmapProxies=.\proxies
Downloads=.\downloads
Export=.\export
Expressions=.\express
Images=.\sceneassets\images
Import=.\import
Materials=.\materiallibraries
MaxStart=.\scenes
Photometric=.\sceneassets\photometric
Previews=.\previews
ProjectFolder=C:\Users\LoneRobot\Desktop\meh
RenderAssets=.\sceneassets\renderassets
RenderOutput=.\renderoutput
RenderPresets=.\renderpresets
Sounds=.\sceneassets\sounds
VideoPost=.\vpost
[XReferenceDirs]
Dir1=.\scenes
[BitmapDirs]
Dir1=C:\Program Files\Autodesk\3ds Max 2014\Maps
Dir2=C:\Program Files\Autodesk\3ds Max 2014\Maps\glare
Dir3=C:\Program Files\Autodesk\3ds Max 2014\Maps\adskMtl
Dir4=C:\Program Files\Autodesk\3ds Max 2014\Maps\Noise
Dir5=C:\Program Files\Autodesk\3ds Max 2014\Maps\Substance\noises
Dir6=C:\Program Files\Autodesk\3ds Max 2014\Maps\Substance\textures
Dir7=C:\Program Files\Autodesk\3ds Max 2014\Maps\mental_mill
Dir8=C:\Program Files\Autodesk\3ds Max 2014\Maps\fx
Dir9=.\downloads

You realise that this is just an ini file, which means we can use this, edit it to add new folders and structures to suit our production, or completely remove the default max project structure.

-- add some custom paths
Scenes=.\scenes
Scenes1=.\scenes\shots\animation
Scenes2=.\scenes\shots\maxfiles

-- add some asset folders
Assets1=.\assets\characters
Assets2=.\assets\sets
Assets3=.\assets\props

If we want to parse this mxp file and create our own pipeline structure building function, we can write a simple function like this:

fn createProjectfromMXP root mxpFile =
(
if doesfileexist root and doesfileexist mxpFile then
(
--grab the directory keys from the mxp file as an array
folderKeys = getINISetting projectPreset "directories"

-- loop over the keys and get the value
for key in folderKeys do
(
-- construct the new folder path
newFolder = (pathconfig.appendPath newProjectRoot (getINISetting projectPreset "directories" key))
makeDir newFolder
format "Created new folder % in Project %\n" newFolder root
)
)
)

createProjectfromMXP "D:\TheLastCrusade" "C:\Users\LoneRobot\Desktop\ProjectDefault.mxp"

It is project creation at it’s simplest. I’ve extended this idea to having XML folder presets for each discipline, having a default preset but also allowing each project to customise the folder structure to their needs. It becomes useful with the different requirements of jobs and means the same tool can be used each time to generate the structure.

I hope this has made you think about how you handle files and pathing in all of your scripts. Please note, I don’t think concatenation is wrong, I am simply proposing a consistent approach in the way other programming languages are able to. When you start to build a larger codebase for your pipelines, it is something that I find much easier to visualise when revisiting scripts.

Appendix

One consideration when writing a tool to abstract folder structures into XML format was the nature of what to store. It’s a great example of something that would appear to non-programmers as a pointless endeavour but a perfect challenge to a programmer. The question I asked myself was:

How do you make sure that the list of folders you are about to write is as economical as possible?

Lets take something like a folder such as the Python install on my computer. Imagine this was the structure I wanted to write to file. Id grab the folders using something like this

pyDirs = (dotnetclass "system.IO.Directory").GetDirectories @"C:/Python27" \
 "*" (dotnetclass "System.IO.SearchOption").AllDirectories
pyDirs.count 
--632

So out of that list, there are quite a few nested subfolders.

C:\Python27
C:\Python27\Tools
C:\Python27\Tools\Scripts

Logically, you don’t need to store the first two entries here – they are made automatically in creating the last one. So you need to check the array to see what the longest elements are and cross reference them with the shortest.  Whilst I’m certain my approach can be optimised massively, it takes a few seconds to analyse the most efficient folder list to write to file.

fn ProcessUniqueDirectoryArray root returnSortedbyName:true debug:false = 
( 
 if doesfileexist root then
 (
 -- compare path length 
 -- Used by the qsort method to sort an array according to longest>shortest path length
 fn comparePathLength v1 v2 =
 (
 local pathLen = v1.count<v2.count
 case pathLen of
 (
 false: -1
 true: 1
 default:0
 )
 ) 
 clearlistener()
 start = timeStamp()
 --Use dotnet to get All Folders in a directory (EnumerateFiles is faster but only in later frameworks)
 DirStructure = (dotnetclass "system.IO.Directory").GetDirectories root "*" (dotnetclass "System.IO.SearchOption").AllDirectories
 -- Strip the pathroot to provide a neutral path root location
 -- This is so we can combine the neutral path with a new root location later
relPaths = (for dir in DirStructure collect substituteString dir (root + "\\") "")
-- sort the path array by longest to shortest path order
 qsort relPaths comparePathLength
 if Debug then format "Path length Counts %\n" (for i in relpaths collect i.count)
 -- Grab the folder count before we do anything 
 PreProcessCount = relPaths.count
-- Loop through each folder from the start (the longest path) 
 for i in relpaths do
 ( 
 if debug then format "path loop %\n" i
 -- For each path in the array, loop through the paths backwards and check if the text is contained within the longer path
 -- This is the core to calculating the most optimized path list
 -- i.e if you have a directory "C:\Python27\Lib\logging", you dont need to store "C:\Python27\Lib\" or "C:\Python27"
 -- as they will be created by storing the longest unique path
 for p = relpaths.count to 1 by -1 do
 (
 -- Check that the path we are checking in our p loop is not the i loop path (otherwise it will be removed)
 if not relpaths[p] == i then
 (
 --if debug then format "checking % is contained in %\n" relpaths[p] i
 if matchpattern i pattern:(relpaths[p]+"*") then 
 (
 -- If the shorter path is found in the longer path, we get the location of the shorter path 
 --if debug then format "Path similarity found % % \n" relpaths[p] i
 --dont need to use find item as the p loop is the same
 -- as we are going backwards we can safely delete items
 --it = finditem relpaths relpaths[p]
 if debug then format "p-loop Integer % | FindItem Integer % \n" p it 
 if debug then format "************************************* Deleting path from array : % *************************************\n" relpaths[p]
 deleteitem relpaths p 
 
 )
 --else if debug then format "% is a unique path\n" relpaths[p]
 )
 ) 
 )
if debug then format "Post Path Analysis Count : %\n" relPaths.count
 qsort relPaths comparePathLength
 if debug then format "re-sorted by length\n" 
 if debug then for i in relpaths do print i
 if returnSortedbyName then sort relpaths
 if debug then format "re-sorted by name\n" 
 if debug then for i in relpaths do print i
 
 if debug then format "PreProcess Count : % | PostProcess Count %\n" PreProcessCount relPaths.count
 if debug then format "Processed in % seconds\n" ((timeStamp() - start) / 1000.0)
 relPaths 
 )
)
ProcessUniqueDirectoryArray @"C:\Python27" debug:true

Running this yields the following:

PreProcess Count : 632 | PostProcess Count 391

So a little bit of thought can never hurt…

I Don’t Need Regular Expressions

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

spag

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"
true

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

/d

[0-9]

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

[0-9]{2}

[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"
"\*_v[0-9]{2}_[0-9]{2}.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
--false
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.

SE_2

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])

pots_ahoy

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

SE_1

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:

Teapot[2-3][0-9]{3}_

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:

Teapot([2-3][0-9]{3}|[6-7][0-9]{3})_

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

se_3

The crowd says teapot selecta!

boselecta

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

*.[0-9]*[02468]_

or

*.[0-9]*[13579]_

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:

http://djv.sourceforge.net/

 

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 :

Filename_<FirstFrame><Hyphen><LastFrame>.<Extension>

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) )
else
(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"
proc.Start()
-- make sure we don't freeze
windows.processPostedMessages()
reader = proc.StandardOutput
while (l = reader.readline()) != undefined do
(
format "%\n" l
windows.processPostedMessages()
)
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
djv.input=@"C:\Users\LoneRobot\Documents\3dsMax\renderoutput\sq_PassTest\T_0000.exr"
djv.createQuickTimeFromEXR()

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.

Kinetic Energy

May 2, 2012 by     7 Comments    Posted under: 3dsMax

To commemorate the launch of 3ds max 2013, there’s no better fanfare than a wink to the past. I was searching my office in my usual dash to get my accounts in on time and found some old max install disks.This was in the days when 3ds max was owned by kinetix and users (i.e. me) spent most of their time resolving dongle authorisation issues.

MAXBLAST

That’s not to say I am trying to make the point that everything was better in the past,  but it does take you back to a halcyon age when 3d was just emerging.

I thought I’d share some screenshots and photos in the spirit of nostalgia.

3dsMax R1 has a 16 bit installer – So you can’t even run the setup in compatibility mode on a 64bit machine. I managed to copy the disc contents to my NAS and run the installer on my wife’s Dell Mini 9, which has x86 XP installed.

There’s a host of images from the disc, including some screenshots of the pre-release version of Character Studio and some early scanline renders.

enjoy!

I also have disks and boxes for Max r3, r6, r7, r8 and r9. I’m gutted I didn’t rescue r2 from the skip when we had a studio clear out years ago. I’ll go through these in due course and see what treasures I can find on them.