Browsing"pipeline"

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…