Browsing"Tips and Tricks"

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.

Two Clicks From Amsterdam

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

End User Event Roundup

 

http://www.enduserevent.com/

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
category:"LoneRobot"
toolTip:"Show Hide Layers"
buttontext:"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
			)
			else
				(
				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
			)
			else
			(
				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.

Conclusion

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

 

Something sparkling for Christmas…

Dec 18, 2010 by     No Comments    Posted under: 3dsMax, Tips and Tricks

Jamie’s Jewels is a blog is created by Jamie Gwilliam of Autodesk. Jamie has been a very useful contact at Autodesk for me on the last project. He arranged a meeting for us with the max design team and we had a great chat about the software and the direction it was heading in the future.

As an application specialist for Autodesk he really knows his stuff. He helped us with his imaging expertise and contacts to get our monitors properly calibrated.

The blog concentrates on 3ds Max/Design and Design Visualisation. This covers past email newsletters as an archive and also includes articles he has produced on image creation.

Check it out here –

http://bit.ly/3jamiesjewels

Floating Dialog Amnesty

Mar 16, 2010 by     9 Comments    Posted under: 3dsMax, DotNet, Maxscript, Tips and Tricks

WindowBoxUI.png

Ack, not another one of those posts where the only picture is a p*ss poor dialog with some stolen twinkly twink icons. My old art teacher would be despairing right now, and perhaps comforting himself by grinding a yellow ochre paste. Oh, how he loved that yellow ochre.

A situation existed recently where between working on the train and at work, I’d lost some dialogs on the screen space outside the resolution of my laptop monitor. A few of 3ds max’s window locations are stored in the ini file.

inipath = getMAXIniFile()
setINISetting inipath “MtlEditorPosition”  “MainWindow” “0 0 375 734”
setINISetting inipath “RenderVFBPosition”  “Position” “0 0”
setINISetting inipath “MaterialBrowserDialogPosition” “Dimension” “0 0 340 600”
setINISetting inipath “EnvironmentDialogPosition” “Dimension” “0 0 350 580”
setINISetting inipath “RenderDialogPosition” “Dimension” “0 0 360 750”
setINISetting inipath “mentalrayMessagesPosition” “Dimension” “0 0 600 400”
setINISetting inipath “RenderPresetsCategoryDialogPosition” “RenderPresetsCategoryDialogDimension” “0 0 210 280”

But most of the useful ones are not – like the layer manager and curve editor.

Unfortunately, you cant set their position like you can a rollout floater – it meant it was time for the programmatic equivalent of James Herriot putting on a large rubber glove.

Windows has a wealth of useful functions in the API that aren’t yet exposed to the dotnet framework – well not out the box, there is a managed API but I haven’t looked into this yet . However this doesn’t mean you cant use native API calls in a managed code environment. You can declare an API call in a dotnet assembly, and with a little coercion, get it working as you want.

pinvokelogoThe place to look for Win32 and API related stuff is pinvoke.net. This has loads of info about how to call these windows functions via the managed dotnet framework

From the site itself-

“The term PInvoke is derived from the phrase “Platform Invoke”. PInvoke signatures are native method signatures, also known as Declare statements in VB”

After looking on this site, I found the methods i was looking for.

  • GetWindowRect – Gets the size of the window from the window handle
  • GetWindowPlacement – Gets the current window location from the window handle
  • MoveWindow – Moves the window to a screen location

All that was need now was to convert these signatures into a format string ready to compile directly from 3dsmax, so that I could call these win32 functions from a dotnetobject within 3dsmax, and hopefully retrieve my missing windows.

When you compile a dll directly in 3dsmax, you are taking the code that you would write within the Visual Studio window and sending it to the compiler manually. So the flavour of managed language you are using dictates the compiler type.

The key to this class is to set up some custom properties so that the data coming out when used in 3dsmax is not too weird –  it is logical to return a drawing.point for a location, and a drawing.size for a window size for instance.

Getwindowrect uses a RECT structure (a structure is similar to a class) but creating one in max can be tricky. Mike Biddlecombe on CG Talk had posted a method to instantiate a structure in 3dsmax by adding it into an array and retrieving the first member. That was brilliant stuff and typical of the sort of stuff that Mike is figuring out the whole time. I decided in this case, that since I was compiling an assembly anyway, i’d just handle all of the structure business there and avoid any potential banana skins. So I added a width and height property to the RECT structure definition, meaning after it was instantiated, you can call the method from PInvoke and return a structure that has standard properties. Planning return values in your classes is something that helps in the long run – It makes deployment much easier in Max. I have found that since the VS interface is easier to debug, it’s a good place to do as much as you can there.

fn DialogWindowOpsClass =
(
source = “”
source += “Imports System.Runtime.InteropServicesn”
source += “Imports System.Drawingn”
source += “Public Class DialogWindowOpsn”
source += “Public Structure RECTn”
source += “Public left As Integern”
source += “Public top As Integern”
source += “Public right As Integern”
source += “Public bottom As Integern”
source += “Public ReadOnly Property Width() As Integern”
source += “Getn”
source += “Return right – leftn”
source += “End Getn”
source += “End Propertyn”
source += “Public ReadOnly Property Height() As Integern”
source += “Getn”
source += “Return bottom – topn”
source += “End Getn”
source += “End Propertyn”
source += “End Structuren”
source += “Public Structure POINTAPIn”
source += “Public x As Integern”
source += “Public y As Integern”
source += “End Structuren”
source += “Public Structure WINDOWPLACEMENTn”
source += “Public Length As Integern”
source += “Public flags As Integern”
source += “Public showCmd As Integern”
source += “Public ptMinPosition As POINTAPIn”
source += “Public ptMaxPosition As POINTAPIn”
source += “Public rcNormalPosition As RECTn”
source += “End Structuren”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function MoveWindow(ByVal hWnd As System.IntPtr, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal bRepaint As Boolean) As Booleann”
source += “End Functionn”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function GetWindowRect(ByVal hWnd As System.IntPtr, ByRef lpRect As RECT) As Booleann”
source += “End Functionn”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function GetWindowPlacement(ByVal hWnd As System.IntPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Booleann”
source += “End Functionn”
source += “Public Function WindowSize(ByVal Hwnd As System.IntPtr) As System.Drawing.Sizen”
source += “Dim LPRECT As RECTn”
source += “GetWindowRect(Hwnd, LPRECT)n”
source += “Dim WinSize As System.drawing.size = New System.drawing.size(LPRECT.Width, LPRECT.Height)n”
source += “Return WinSizen”
source += “End Functionn”
source += “Public Function WindowPosition(ByVal Hwnd As System.IntPtr) As System.Drawing.Pointn”
source += “Dim intRet As Integern”
source += “Dim wpTemp As WINDOWPLACEMENT = New WINDOWPLACEMENT()n”
source += “wpTemp.Length = System.Runtime.InteropServices.Marshal.SizeOf(wpTemp)n”
source += “intRet = GetWindowPlacement(Hwnd, wpTemp)n”
source += “Dim WinPoint As System.drawing.point = New System.drawing.point(wpTemp.rcNormalPosition.left, wpTemp.rcNormalPosition.top)n”
source += “Return WinPointn”
source += “End Functionn”
source += “End Class”

VBProvider = dotnetobject “Microsoft.VisualBasic.VBCodeProvider”
compilerParams = dotnetobject “System.CodeDom.Compiler.CompilerParameters”
compilerParams.ReferencedAssemblies.add “C:WindowsMicrosoft.NETFrameworkv2.0.50727System.drawing.dll”
compilerParams.GenerateInMemory = on
compilerResults = VBProvider.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 VB code”
return undefined
)
return compilerResults.CompiledAssembly.CreateInstance “DialogWindowOps”
)

What this code does is make a class that can be instantiated in max and used to control the positions of the dialogs in the 3dsmax user interface, whether we can see them or not.

In order to call them though, we need an array of handles for the active max dialogs. Fortunately, Maxscript gives us a way of doing this –

UIAccessor.GetPopupDialogs()

The rest, was building an interface. I went for the datagridview – Its a complex user control and I am probably only scratching the surface of the sort of things you can do. Its designed for handling masses of complex data from data sources like sql databases but you can just use it like a listview. Its just got a few more options.

Maxscript also has methods for interacting with win32 functions, so it’s entirely possible that this could be achieved using that, but then that’d just be cheating, wouldn’t it?

download

We have all the time in the world

Mar 16, 2010 by     2 Comments    Posted under: Tips and Tricks

Time is commonly understood as something rather more than a linear pattern of execution. Many theoretical physicists believe in Einstein’s relativity theory which states that we are all experiencing a personal time relative to our current position in space. This intrapersonal time structure can curve and run parallel to others in space-time, potentially creating an indefinite number of alternate realities.

einstein

Einstein was obviously not a commuter, because he might have been able to spend a bit of this ‘relative’ time working out why I haven’t got any. So I’m well aware that my postings on lonerobot.net have been a little lapse as of late.

I will be adding as often as I can some maxscript hors d’oeuvres that will hopefully help with a few of the day to day things about using maxscript.

Perhaps it will grow into something sprawling and unstoppable, rather like an enormous platter of Ferrero Rocher at the Ambassador’s reception. And you will cry, “Oh Lonerobot, weev zis small snippets posting you are spoiling us no?”

That, or you’ll realise that the time you spent reading this can never be returned to you, in this reality or any number of alternate ones.