HitchHiker – A dynamic thumbnail UI control for 3dsMax

Oct 24, 2008 by     2 Comments    Posted under: 3dsMax, DotNet, User Controls

HitchHiker is a custom UI control written in It could be happily used in a windows forms project, but specifically I am writing all my stuff with the intention of integrating them into my animation workflow in 3dsMax.

Hitchhiker is a dynamic rollout creator – it has one method – populate(path) and it searches the location for a particular filetype. Depending on the display method, it will then dynamically cache thumbnails of many image filetypes and 3dsmax thumbnails and display them as a button within the control. The button has a custom event attached to it, and passes the file location so that you can do with it as you wish. However, you can also drag and drop from it into max should you wish.

Image Limitations in Windows

If you’ve ever done any windows programming, you will be aware of the native image filetypes that windows recognizes. The most notable omission is TARGA, a much used format in the 3d industry. Hitchhiker employs a custom image library to bridge the gap and provide access to many image filetypes.

these include BMP,TIF,PNG,ICO,PSD,JPG, and TGA. however, it is not just limited to image files. it will also search for .max, .wav, .avi and .xml. This makes it extremely flexible and could be used in just about any situation.

Why is this useful?

I hate building UI’s. Funny i guess considering i’m mostly programming UI controls but that’s the point. If i can put something on a maxscript rollout that handles all of the code generation of file handling, it means I can build a script quickly. Hitchhiker has multiple properties to control how the buttons appear in the control. Here’s a few examples. – click the image to see it full size.

Flatbot is a utility that allow me to take a folder of images and generate plane geometry, opacity mapped for a recent production. It’s a simple idea but it really saves time on setup.

Hitchhiker is on the left in ImageText display mode. You can also see that it rescales the thumbnail and keeps the aspect ratio correct. There is another custom class in this utility. The transparent picturebox control is an inherited user control, it simply paints the photoshop-esque checkerboard background for viewing images.

I wanted to be able to easily display the alpha channel of a Targa, but was frustrated on how to do it. I was able to build a method into HitchHiker that converts the targa into a temporary PNG and passes this to the custom picturebox control. This way, it displays the alpha correctly. This conversion is swift, and displays almost immediately.

You can probably see with this control you could build a CG asset browser in about 10 minutes.

other uses are : a sound loading utility –

Above, HitchHiker is in IconText display mode. All formats have a custom icon that will display if you don’t want image thumbnails.

When specifying avi as a search type, it will extract the first frame and do a GDI+ overlay of the icon type in the corner so you know it is a video file. –

HitchHiker also has a tooltip that displays after a moment on the selected button. (This can be disabled) Depending on the filetype, it will extract information pertaining to that format.

Future Plans

i’m most excited about integrating HitchHiker into a character pose system. Storing the poses in XML format for a character, it allows you to dynamically retrieve facial expressions, lipsync, rig poses, anything!

In the XML search function i have a routine that checks for a custom image. if you store a snapshot image titled the same name as the XML file, it will use this as an image on the button.

I’m hoping to release a free version of this control with reduced functionality soon.

thanks for reading!

Retrieving the 3DSMax File Thumbnail with DotNet

Oct 5, 2008 by     12 Comments    Posted under: 3dsMax, DotNet, Maxscript, Technical Research

Firstly, a small disclaimer – If you are unsure about any of the information contained on this page, Please do not try to implement it.

Before Max opted for DotNet integration, You could use the Windows ActiveX thumbnail component to view the 3dsMax file thumbnail. I have always found this small image to be very useful. However, when looking for a method to retrieve this via DotNet, I found it much more difficult. There is very little written about this, and I was getting nowhere until and I was fortunate enough to have Cuney Tozdas of Splutterfish point me towards two C++ files in the SDK examples. Please note that I am not a programmer, so apologies if there are any inaccuracies in my text.

How Max Stores it

The notes in the SDK pointed me towards the notion of IPropertyStorage and Structured Storage. Basically, the summary info of a Max file is set using this method. So not only is the Thumbnail stored in this part of the file, there are options to store many other pieces of information pertaining to the maxfile. You can see this by picking File>Summary Info in 3dsMax. The bad news is that novices like myself are not going to get near the level of programming needed to access this part of a file without some C++ knowledge.

Fortunately, help is at hand because it just so happened that while researching IPropertyStorage I stumbled across something on from Microsoft. MS Office files also use the same method as 3dsMax to store information in office files, using OLE Structured Storage, and Microsoft have provided a class for dealing with the lower level stuff for you called DSOFile.

DsoFile is a COM class and can be freely downloaded from this link

And the tricky bit….

Com classes need to be registered, which means that you have to use regsrv32 in order to do this. If you use the installer from the Microsoft website it will perform this step for you. However, should you ever move the dll or are interested here are the steps for performing this manually. If anyone knows about building setup projects in Visual Studio to automate this i’d be grateful if they could let me know, as i would much prefer to give an installer for this and save everyone the trouble.

  1. Once your Window’s operating system has loaded completely, click on Start and then click on Run.
  2. Input in the Run field a command that tells your computer to register the DLL file. You will need to input specific information including the path and the file name. The following is a template for the command: regsvr32 “FileName.dll”
    It is important to note that path is the actually location or directory of where the file is located.

  1. Once the command is input into the Run field correctly, press Enter.
  2. Once the DLL has been registered, you should receive a confirmation in the form of a pop up box.

This message will list your newly registered DLL file and confirm that is was successfully registered into the registry.

Once registered, you will now be able to use this class in Visual Studio. I’m not going to go into how to use DSOfile for this, as you can figure this out from the source provided from microsoft. When you add a reference to a COM class in Visual Studio, it creates an interop class that allows the .net framework and the COM class to talk to each other. So as well as registering the DSOfile dll, you will need to load the InterOp class as an assembly in any MaxScript code to use it.

The MaxFileInfo Class

I have written a basic dotnet class with a few properties and an overloaded method to retrieve the Max Thumbnail. Note that when instantiated, the MaxFileInfo class creates a MaxFileinfo dotnetobject. This means that once created, it will automatically reference the information properties. However, the Thumbnail access is fast so it is generated on demand. In my HitchHiker utility at the top It propagates the control almost immediately.

to create a MaxFileInfo object, you provide a string path variable

MaxFileInfo “<Maxfile path and filename>”

I have provided two options – calling Thumbnail on the file will return a DotNet image, useful if you are building a dotNet rollout in max, or by specifying Thumbnail True will copy this to the clipboard and allow you to use getclipboardBitmap() to put onto a MXS button. I have included a small example of the two methods in the download.

Still to do…

This class will only currently work in 32 bit versions of Max and Windows. Someone very kindly recompiled DSOFile for the X64 platform for me and i am working around a couple of issues at the moment, so I will hope to release a working version for the X64 platform very soon. I am slightly behind on this because my Laptop runs 32bit vista and my work machine XPx64, so i don’t get an awful lot of time to test in this environment. I will be upgrading my laptop shortly in order to develop this further, as without 64bit compatibility it is about as much use as a chocolate teapot. Indeed, i wish it was more straightforward.

I will be the first to say that this implementation is far from perfect. There are a couple of other references that need to be addressed depending on the platform you are running. On XP, I sometimes had to provide the stdole.dll also, as well as visualbasic.compatibility.dll as this is used by the class to convert the picture thumbnail to a dotnet image.

If you have any questions or need help with this class, feel free to email me (pete at lonerobot dot com) or PM me via CGTalk. If you want to use this in any scripts, you are freely welcome to do so, although I’d appreciate a mention!

I have also looked into extending this class with a few GDI+ methods, embedding the file information into the image before it returns it with some layout control, as well as a crude resampling routine to resize the thumbnail to something larger.

Included in the download is a very basic utility written in Visual Studio that allows you to browse files via a treeview. It has drag and drop functionality too. Its just provided as is, in the hope it would be useful to someone.

The other thing to add to this class library would be the comprehensive ability to specify the summary properties of a maxfile via dotnet. If you think this would be of particular use to you, please let me know, and I might be able to find the time to write it for you.


MultiThreading in 3D Studio Max using the BackgroundWorker class

Aug 29, 2008 by     10 Comments    Posted under: 3dsMax, DotNet, Maxscript

You will all be aware that when you perform any intensive calculation process within MaxScript that it pretty much ties up that session of max. The solution to this would be to run this process in a separate thread, much like 3DS Max does with all other operations. However, the system.threading class can be tricky to configure. Luckily the DotNet chaps have provided an alternative that does all the hard work for you – The BackgroundWorker.

You create an instance of this class in MXS via the following method –

MainThread = dotnetobject "System.ComponentModel.BackGroundWorker"

This is a small test that demonstrates use of this class. You will notice that when you run the example via MXS, the 3Dsmax UI will be completely tied up. Via the Dotnet method, you can still use the max interface and do other tasks within a single copy of max. It also supports cancellation so you can abort an intensive task should you need to.

The main thread is called via the DoWork() event – specifying this in MaxScript as a dotnethandler allows you to pass a function to operate as the work thread. So you can pretty much put what you like in here and it will run in a thread separate to the UI. This is obviously better suited to work that isn’t viewport related. It is perfect if you want to perform an intensive calculation without starting a separate instance of 3DS Max.

Updating the UI thread from the Worker Thread

Something to understand in VB/C# with this class is that like using other threading methods you have to use delegates if you want to update a UI that was started in a thread different to the one perform the work. If you attempt this in VB/C# it will throw an exception. The backgroundworker class has this delegate functionality built in, providing an easy way to pass progress (and other) information back to the UI thread. I haven’t implemented it here as it didn’t seem to be necessary for the script to work, although i have included it commented out in the code. In VB/C#, The backgroundworker class allows this to happen via the ProgressChanged handler. If you look in the script this can only report back if you set the object’s workerreportsprogress property to true., without this property set will also result in an exception.

Once started with the runworkerasync() function, The mainthread performs the work function. You choose when to update back to the UI thread via the reportprogress method. This takes two arguments, a percentage integer and a userstate object. So, you can put pretty much anything in this object – i have placed an array with various properties in so i can can the userstate[integer]method to retrieve these in the updatethread function. The ProgressChangedEventArgs therefore contains a progresspercentage property to update a progressbar or similar and the userstate property for anything else. For example I have used this to pass the e.progresspercentage argument to the painthandler of a control and use GDI+ to draw a custom progressbar over a bitmap that is loading. You don’t have to provide a userstate property, the eventarg is overloaded (meaning you can choose which arguments to supply) so that you can just return the progress percentage should you wish.

Insert Update Thread pun here…

Whilst the background worker seems to perform well in a VS environment, I had notived a couple of intermittent synchronisation errors when using a this class within 3dsMax. Following a discussion on CGTalk, the dotnet SDK mentions this issue. Here is what it says –

SynchronizingBackgroundWorker Class

Replaces the standard BackgroundWorker provided by the .NET Framework to fix what seems to be a bug causing the BackgroundWorker to fire events in the wrong thread.We have encountered cases where the BackgroundWorker seems to lose track of the main thread or just arbitrarily decide to fire ProgressChanged and RunWorkerCompleted in a new thread rather than in the main thread. This causes synchronization errors.

This replacement class allows the client to specify a ISynchronizeInvoke synchronizer object through which the events will be fired. All Controls implement ISynchronizeInvoke, so any Control should be adequate to act as a synchronizer to invoke the events in the main thread.

I have updated these classes to run with this component by adding a reference to CsharpUtilities in VS. In max, you can just reference it normally like so –

Worker = DotNetObject "CSharpUtilities.SynchronizingBackgroundWorker"

You don’t need to load the assembly as MaxDoes this automatically at startup.

download script

Joystick Control – A custom user control project

Aug 28, 2008 by     1 Comment     Posted under: 3dsMax, DotNet, User Controls

Integrating a custom UI control into 3dsMax

A while ago I decided to research making your own UI controls in Visual Studio. Why? Well, as a character animator by day I am always looking for better ways of providing input for an animator to set various character attributes. I always liked the old joystick manipulator by Borislav Petrov but wanted a way of integrating this into the UI rather than in the viewport. Until Dot net was integrated with max, this would have been impossible without writing one yourself in C++.

This page details the results so far. As a project it was very much in the deep end, and the creation of the control was limited to my research into programming VB at the time. I have been coding maxscript in some shape or form since R3 but this was my first foray into this type of coding.

A brief Recap – Why arguments are not always bad…

Before I talk in more depth about the control itself, i thought i’d mention the basics of how events are handled within Visual Basic so that you can understand how to set these up and use them correctly within the MaxScript/DotNet bridge. I’m sure the procedure is similar in C# but I will use examples in VB as this is what i use. (My programmer friend Chris said to me once that VB and C# are identical, except C# is spoken with an American accent)

There are many handlers aside from the standard mouse input handlers, but for a custom control, these (along with GDI+ paint handlers) are the important ones. In VB, when you specify a mousedown handler it passes two arguments to the handling subroutine.

Private Sub Button1_MouseDown (ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
  Handles Button1.MouseDown

This is useful since even when using a dotnetcontrol, you can pass the sender and event arguments and use them in a similar way. These arguments are important in regard to a control that uses the mouseposition to set positions of UI elements.

Writing a custom event class

With a custom control, you might not want to just return the mouse position in the event handler. Due to the nature of the control you might need another property that you can do something with. This is where writing your own event class comes in. You can specify what the handler uses as its ‘e’ argument. This is the basics of the new event class. It inherits the eventargs class so that it can use all of the properties of this class but augment them with others.

Public Class JoystickEventArgs

Inherits EventArgs

Private _location As Size
Private _X As Integer
Private _Y As Integer
Private _MorphValue As MorphValue

Public Property Location() As Size
Return _location
End Get
Set(ByVal value As Size)
_location = value
End Set
End Property

Public Property X() As Integer
Return _X
End Get
Set(ByVal value As Integer)
_X = value
End Set
End Property
Public Property Y() As Integer
Return _Y
End Get
Set(ByVal value As Integer)
_Y = value
End Set
End Property

Public Property MorphValue() As MorphValue
Return _MorphValue
End Get
Set(ByVal value As MorphValue)
_MorphValue = value
End Set
End Property

Public Sub New(ByVal Joystick As Joystick)
Location = Joystick.MovableLabel.Location
X = Me.Location.Width
Y = Me.Location.Height
MorphValue = GetMorphValuesLinear(Me.Location) End Sub
End Class

However, this class is not complete without another class. I created one called morphvalue, which is simply a container object to store four integer values for up,down,left and right. I also created an enumeration that you can retrieve in the handler to show which quadrant of the control you are in. This means you can use a case expression to control different objects on a per quadrant basis. The joystickeventarg creates an instance of this class each time it is called (within the new sub) and calls the getmorphvalueslinear() function. this is not listed but returns a value related to the mouse position.

Public Class MorphValue

Enum Quad
End Enum

Private _Left As Integer
Private _Right As Integer
Private _Up As Integer
Private _Down As Integer
Private _Area As Quad

Public Property Area() As Quad
Return _Area
End Get
Set(ByVal value As Quad)
_Area = value
End Set
End Property

Public Property Left() As Integer
Return _Left
End Get
Set(ByVal value As Integer)
_Left = value
End Set
End Property

Public Property Right() As Integer
Return _Right
End Get
Set(ByVal value As Integer)
_Right = value
End Set
End Property

Public Property Up() As Integer
Return _Up
End Get
Set(ByVal value As Integer)
_Up = value
End Set
End Property

Public Property Down() As Integer
Return _Down
End Get
Set(ByVal value As Integer)
_Down = value
End Set
End Property

Public Sub New(ByVal Area As Quad, ByVal Value1 As Integer, ByVal Value2 As Integer, ByVal Value3 As Integer, ByVal Value4 As Integer)

Me.Up = Value1
Me.Down = Value2
Me.Left = Value3
Me.Right = Value4
Me.Area = Area
End Sub

End Class

so, within Visual studio and bound to the joystickmove handler on the joystick object, the new joystickeventargs class returns the following output –

Private Sub Joystick1_JoystickMove(ByVal sender As System.Object, ByVal e As LoneRobot.JoystickEventArgs) Handles Joystick1.JoystickMove

Dim Mretval As Size = Joystick1.MorphtoValue(e.MorphValue.Up, e.MorphValue.Down, e.MorphValue.Left, e.MorphValue.Right)

Label1.Text = e.X.ToString & "/" & e.Y.ToString & " || " & Mretval.Width.ToString & "/" & Mretval.Height.ToString
LabelUP.Text = e.MorphValue.Up.ToString
LabelDown.Text = e.MorphValue.Down.ToString
LabelLeft.Text = e.MorphValue.Left.ToString
LabelRight.Text = e.MorphValue.Right.ToString

LabelUP.Height = e.MorphValue.Up
LabelDown.Height = e.MorphValue.Down
LabelLeft.Height = e.MorphValue.Left
LabelRight.Height = e.MorphValue.Right
Label2.Text = e.MorphValue.Area
End Sub

you will see that the handler is now specifying "e As LoneRobot.JoystickEventArgs" meaning that it is returning this class.

If you are binding this into a max dotnetcontrol event, you just need to set the morph channels to the morphvalue.<direction> property of  the eventarg.


3ds Max File Drop via DotNet

Aug 26, 2008 by     3 Comments    Posted under: 3dsMax, DotNet, Tips and Tricks

If you need to mimic the way that max presents you with merge/open/xref options when you drag into a viewport, you can perform this yourself with DotNet. Max expects a dataobject containing a string array. Depending on the filetype that this string member points to will decide whether max will prompt you with a file open menu or a texture map as viewport background. The other option is you can also pass a map path to drag onto slots on the material editor.

Below is a snippet of the code for this operation. It is entirely handled within the mousedown handler of the control. It makes sense in this case as you wouldn’t put it in the mousemove handler for example as this would mean the code would be needlessly called multiple times.

on btndragdrop mouseDown sender args do 
theIniFile = getdir #maxData + "3dsmax.ini" 
theKeys = getIniSetting theIniFile "FileList" 
maxfilearray = for o in theKeys where o != "MaxFiles" collect (getIniSetting theIniFIle "FileList" o)
intnum = dotnetobject "System.Int32" 0 
filenamestring= dotnetobject "System.string" maxfilearray[1] 
dropfile = dotnetobject "System.String[]" 1 
dropfile.setvalue filenamestring intnum 
DataObj = dotnetobject "DataObject" ((dotnetclass "DataFormats").filedrop) dropfile 
sender.dodragdrop Dataobj ((dotnetclass "DragDropEffects").Copy) 

The only thing to note is that the Dataobject takes an object as it’s first parameter. This can basically be anything you like, except this in this case. The SDK notes that it needs an array in this object. The square brackets after the dropfile variable denotes that it is an array of strings with 1 member, not a string. Passing a string object therefore has no effect and does not present you with the menu seen below, which is the result of the correct format of the DataObject.

Not the end of the story…

I noticed an odd behavior using drag and drop in max. When employed on a dotnetcontrol in a maxscript window, it classes it as being ‘inside’ max even though it is a dotnet object. Therefore you could in theory initialize the drag and drop onto the label control to perform a legitimate drop. This behavior is possibly not wanted, if you needed to perform other handlers – the drag drop might get triggered accidentally. The way around this is to use the mouseeventargs to check if the mouse has moved outside the client area of the control.

on DragDropOps open do 
btndragdrop.allowdrop = false
btndragdrop.text = "Hooray! A Drag/Drop Enabled Label!!!\n\nTo drop a Texturemap, just pass the map path string in the dataobject instead of a max file path. This will also work if draging a map to the material editor" btndragdrop.borderstyle = (dotNetClass "System.Windows.Forms.BorderStyle").fixedsingle 
btndragdrop.backcolor = (dotnetclass "System.Drawing.Color").orangered 
btndragdrop.forecolor = (dotnetclass "System.Drawing.Color").yellow 

on btndragdrop mousemove sender args do 
if (sender.clientrectangle.contains args.x args.y) then 
setsyscur #arrow 
 setSysCur #move

on btndragdrop mouseup sender args do 
if (sender.clientrectangle.contains args.x args.y) then 
( ) 
theIniFile = getdir #maxData + "3dsmax.ini" 
theKeys = getIniSetting theIniFile "FileList" m
axfilearray = for o in theKeys where o != "MaxFiles" collect getIniSetting theIniFIle "FileList" o 
filenameString = maxfilearray[1] 
dropfile = dotnetobject "System.String[]" 1 
dropfile.setvalue filenameString 0 
DataObj = dotnetobject "DataObject" ((dotnetclass "DataFormats").filedrop) dropfile 
sender.dodragdrop Dataobj ((dotnetclass "DragDropEffects").Copy) ) )