Browsing"DotNet"

Automating Photoshop with a DotNet Class Library and COM interop

Recently, there was a thread on CGTalk about whether it was possible to save out multiple render passes into a layered photoshop file from 3dsMax itself. This question had been sitting in the back of my mind ever since and I decided to look into whether it was possible. My first thought was that there was an external image library out there that could do the job. However, finding a good solution meant I didn’t want to have to purchase software for something that was purely research. I already own a great library – AIL, which is based on the freeimage library. Alas, while this can open Photoshop documents, it can not create, structure and save them. However, after a bit of research on the Adobe site, I found that it is indeed possible to do all these things using DotNet.

With Photoshop, Adobe has included an object library assembly. This is a clever assembly that allows you to write custom scripts and automate the Photoshop UI. “Hang on Shirley”, I hear you cry, “We don’t need that – I own a Mac and we’ve got actions for that, I can do everything I need from there! It’s fun, too”.

Well, yes and no. This programmed method still could be the ‘droids you are looking for’.

Actions mean that you obviously need to have it recorded at some point, and be able to trigger the start of the action from within the photoshop application. However, when using maps in 3dsMax there are many occasions when you need to perform a tweak here and there to a map, and the workflow is broken as you have to open it in another program and perform the tasks.

Some other benefits (from the Adobe scripting guide) are –

  • You can add conditional logic, so that the script automatically makes “decisions” based on the currentsituation. For example, you could write a script that decides which color border to add depending onthe size of the selected area in an image: “If the selected area is smaller than 2 x 4 inches, add a greenborder; otherwise add a red border.”
  • A single script can perform actions that involve multiple applications. For example, depending on thescripting language you are using, you could target both Photoshop CS3 and another Adobe CreativeSuite 3 Application, such as Illustrator® CS3, in the same script.
  • You can open, save, and rename files using scripts.
  • You can copy scripts from one computer to another. If you were using an Action and then switchedcomputers, you’d have to recreate the Action.
  • Scripts provide more versatility for automatically opening files. When opening a file in an action, youmust hard code the file location. In a script, you can use variables for file paths.

With this in mind, you are presented with the opportunity that you can automate Photoshop from within the 3dsMax interface. For example, you could have a maxfile that renders multiple passes and calls a post-render script when it finishes, triggering the creation of a psd composite of all the images. You could use it to call a function to overlay an Ambient Occlusion pass on notification of a completed render. You could select an object in the 3dsMax viewport and have Photoshop apply a Gaussian Blur or an artistic effect to the Diffusemap without leaving the 3dsMax Interface. The applications can be as specialised as you want but completely customisable in the same breath.

Creating a Photoshop Assembly for 3dsMax

Because the access to Photoshop is via COM, you need to use an interop assembly in order to use it within a DotNet context. With my fuzzy logic, The interop assembly basically handles the calls between the DotNet assembly and the COM object. I have demonstrated this before, using the COM assembly DSOFile in order to extract the 3DSMax file thumbnail, and there seems to be a small snag that I haven’t been able to resolve as of yet.

Trying to directly use COM interop via the DotNet bridge in 3dsMax can only get you so far. At some point you will (via interop) be accessing a COM object itself. This part I don’t explain fully myself, but as Max uses reflection it reaches a point at which certain objects are created and viewed explicitly as the system.__COMobject type. This is a pain, – one because you can’t cast it to the correct object type in 3dsMax as this is a core function, and two because as a system.__COMobject, you can only access the properties and methods of that type, not the underlying class that the interop assembly is giving you access to. Here’s an example of my failed attempt using just the interop assembly and attempting to set this up via MaxScript – (Listener output is in blue)

dotnet.loadassembly ((getdir#scripts)+"LoneRobotClassLibInterop.Photoshop")
dotNetObject:System.Reflection.Assembly
ps = dotnetobject "Photoshop.ApplicationClass"
dotNetObject:Photoshop.ApplicationClass
showproperties ps
.ActiveDocument : <Photoshop.Document>
.Application : <Photoshop.Application>, read-only
.BackgroundColor : <Photoshop.SolidColor>
.ColorSettings : <System.String>, read-only
.DisplayDialogs : <Photoshop.PsDialogModes>
.Documents : <Photoshop.Documents>, read-only
.Fonts : <Photoshop.TextFonts>, read-only
.ForegroundColor : <Photoshop.SolidColor>
.FreeMemory : <System.Double>, read-only
.Locale : <System.String>, read-only
.MacintoshFileTypes : <System.Object>, read-only
.MeasurementLog : <Photoshop.MeasurementLog>, read-only
.Name : <System.String>, read-only
.Notifiers : <Photoshop.Notifiers>, read-only
.NotifiersEnabled : <System.Boolean>
.Path : <System.String>, read-only
.Preferences : <Photoshop.Preferences>, read-only
.PreferencesFolder : <System.String>, read-only
.RecentFiles : <System.Object>, read-only
.ScriptingBuildDate : <System.String>, read-only
.ScriptingVersion : <System.String>, read-only
.Version : <System.String>, read-only
.Visible : <System.Boolean>
.WinColorSettings : <System.String>, read-only
.WindowsFileTypes : <System.Object>, read-only
ps.documents
dotNetObject:System.__ComObject
-- this should be a Photoshop.Documents class??
show ps.documents
false
showmethods ps.documents
.<System.Runtime.Remoting.ObjRef>CreateObjRef <System.Type>requestedType
.<System.Boolean>Equals <System.Object>obj
.[static]<System.Boolean>Equals <System.Object>objA <System.Object>objB
.<System.Int32>GetHashCode()
.<System.Object>GetLifetimeService()
.<System.Type>GetType()
.<System.Object>InitializeLifetimeService()
.[static]<System.Boolean>ReferenceEquals <System.Object>objA <System.Object>objB
.<System.String>ToString()
-- ie showing the methods of the COM object, not the documents class.

I cant say exactly what is going on, except that Max is returning the COM object’s methods rather than the methods the interop assembly is allowing from the Adobe Object library. Maybe this is something to do with Reflection or the way 3dsMax sees DotNet, I really don’t know. If anyone reading this has any ideas, feel free to correct me. Just using the application class methods, like load() will work, so you can open a photoshop file via max, it’s just that you can’t do an awful lot after that. 🙁

It’s not all bad, as there is no such limitation in the Visual Studio IDE. Which means you can write a wrapper to expose the functions you want to call within max. This is actually a nice way of testing these things, as the VS IDE is a far superior environment to test and debug your classes, and you also get help from intellisense and prompting you when you make all sorts of errors.

Coding a COM Wrapper

The next bit assumes the following criteria, and was done in this environment –

  • A Visual Studio 2008 VB project on Vista x86
  • PhotoShop CS3 (this is the newest and only version I have)
  • 3dsMax 2009

For different versions of Photoshop, there may be slight differences but the principles should essentially be the same. Also note that the code examples are in Visual Basic, but would be almost identical if done in C#. Once the photoshop object is created, the methods and property access would be the same. A wrapper is a class library that provides you with convenient access to the photoshop application, so that you can make general calls from max like photoshop.blur() or photoshop.strokeselection red. It means you can write an assembly with whatever functions you want and deploy them wherever you like.

Creating the Interop Assembly

Open the Visual Studio IDE. Choose File>New Project>Class Library (VB or C#)

Name it something suitable. Not PatSharpesHairPiece or MrCommotasticsPhotoShopofHorrors, something meaningful. Actually that last one isn’t that bad.

Click the show all files button on the Solution Explorer

Right Click the References branch and choose Add Reference (Or from the menu Project>Add Reference)

When the dialog loads, instead of .NET, pick the COM tab. Choose Adobe Photoshop Object Library from the list.

Bingo, you’ve created a reference to photoshop –

Save, and Build your project. Depending on the compile parameters, you will have created an interop assembly in one of the folders. Usually this is the projectfolder>bin>debug folder. If you check in this folder in the solution explorer you will see listed Interop.Photoshop.dll

That’s the first part, Visual Studio automatically creates the COM interop assembly for you. Thanks Visual Studio, and much obliged I am too.

Bring on the Shop

Now a drop of the good stuff. Take a look at how the photoshop object is constructed. I have highlighted the three main aspects to show what we will be using in this tutorial –

Most important is the Application class. This creates an instance of the photoshop application. If photoshop is not open, Instantiating this object will open Photoshop for you. Don’t worry, you can make the UI a background process after this if you don’t want to (or need) see what is going on. All operations on individual documents are through the Documents class, strangely, and individual layers are accessed via the ArtLayers class. Collections index members are from 1 rather than 0 like in a VB array. The first document
created has an index of 1.

If you look up in the Document object in the
Visual Basic Object Browser, you will see that there is no Add() method for the object. However, the Add()
method is available for the Documents object. Similarly, the ArtLayer object does not have an Add()
method; the ArtLayers object does. You have to add these via these collections than the individual object class. Not only that, you need to refer to them via the Application class. So to adjust the brightness and contrast of layer 4 you would have to do it with the following code –

<ApplicationObject>.ActiveDocument.ArtLayers(4).AdjustBrightnessContrast()

In your class library, place an import (or using in C#) statement at the top of the class so you don’t have to keep writing the full address. I have called my class PhotoShopBot, surprisingly.

Imports Photoshop
Public Class PhotoshopBot
End Class

In order for the class to perform the automation you are going to program, It will need an instance of the Photoshop application object. It makes sense to declare this as a private member variable at the start, and instantiate it in the new sub. That way, you only create one photoshop application object, which can then be used if you extend the class library in the future and add more methods. You don’t want to be creating it each time you call a function, that would be wasteful.

Imports Photoshop
Private PSDapp As Application
Public Class PhotoshopBot
Public Sub New()
PSDapp = New Application()
End Sub
End Class

Currently, all this class is doing is instantiating the application object, so in theory creating a dotnetobject in max from the resulting assembly would actually open up the photoshop interface!

Now, define a sub for the actual work you want the class to do. Since the post on CGTalk was discussing whether it was possible to create a PSD file with layers from multiple render passes, so this is what I will setup in this tutorial.

Imports Photoshop
Private PSDapp As Application
Public Class PhotoshopBot
Public Sub New()
PSDapp = New Application()
End Sub
Public Sub BasicImageCompositor(ByVal Images() As String, _
ByVal OutputFileName As String, _
ByVal HideApplication As Boolean, _
ByVal OpenOnCompletion As Boolean)
End Sub
End Class

BasicImageCompositor is going to work by providing an array of filenames. This could be passed from a maxscript using the GetFiles function to create the array, so could be generated dynamically from a folder’s contents. For the purposes of this basic subroutine, the layer order will be done bottom up by the order of the array elements, so make sure the background layer is first and each subsequent layer next. You could give your passes names so that this would automatically arrange them into the correct order after calling sort on the array. The reason it is an array of strings is that the Application object calls load on a string filename, don’t forget windows only opens a small selection of filetypes natively.

The thing to remember is that you need to keep track of the active document before you do anything. This is a property that is set via the Documents collection of the Application object. Let’s look at the entire class –

Imports Photoshop

Public Class PhotoshopBot

Private PSDapp As Application

Public Sub New()
PSDapp = New Application()
End Sub

Public Sub BasicImageCompositor(ByVal Images() As String, _
ByVal OutputFileName As String, _
ByVal HideApplication As Boolean, _
ByVal OpenOnCompletion As Boolean)

' get a fileinfo object to check the folder validity
Dim OutFile As New IO.FileInfo(OutputFileName)

If OutFile.Directory.Exists Then
' hide the photoshop UI
If HideApplication Then PSDapp.Visible = False
' Suppress the Photoshop dialogs (if any)
PSDapp.DisplayDialogs = 3
'   enum 3 =
'   PsDialogModes.psDisplayNoDialogs()
'load the first image and duplicate it into a new image document

If Images.Length > 0 Then
Dim BackGroundLayer As String = Images(0)
PSDapp.Load(BackGroundLayer)
PSDapp.ActiveDocument = PSDapp.Documents.Item(1)
PSDapp.ActiveDocument.Duplicate(OutFile.Name)
'close the original
'close(1) bypasses the save dialog
PSDapp.Documents.Item(1).Close(1)
'set the active document to the duplicate
PSDapp.ActiveDocument = PSDapp.Documents.Item(1)
'loop through the rest of the array, copying and pasting it into the
'duplicated document
For imageindex = LBound(Images) + 1 To UBound(Images)
PSDapp.Load(Images(imageindex))
PSDapp.ActiveDocument = PSDapp.Documents.Item(2)
PSDapp.ActiveDocument.Selection.SelectAll()
PSDapp.ActiveDocument.ArtLayers(1).Copy()
PSDapp.Documents.Item(2).Close(1)
PSDapp.ActiveDocument = PSDapp.Documents.Item(1)
PSDapp.ActiveDocument.Paste()
Next

'save it
PSDapp.ActiveDocument.SaveAs(OutputFileName)
PSDapp.ActiveDocument.Close(1)
' show the ui and load the image, if desired
If HideApplication Then PSDapp.Visible = True
If OpenOnCompletion Then PSDapp.Load(OutputFileName)
End If
Else
' if the folder doesn't exist it will throw this exception
Throw New IO.DirectoryNotFoundException("Directory does not exist")
End If

End Sub

End Class

That’s it, compile the project and copy the two dlls it makes to your max directory or wherever you load your dotnet stuff from. Depending on your project and namespaces used this will be different but mine is photoshop.dll and photoshop.interop.dll

Using this assembly in 3DSMax

Now there is the simple issue of creating an instance of this class and calling the only method it has. I am going to composite the following images –

Background – (Click to save if you want to try this example)

Other layers

As for 3dsMax, here is the script to automate the creation of the PSD file. Don’t forget your paths will be different to mine!

dotnet.loadassembly ((getdir#scripts)+"LoneRobotClassLibInterop.Photoshop")

dotnet.loadassembly ((getdir#scripts)+"LoneRobotClassLibPhotoshop")

oPhotoShop = dotnetobject "LoneRobot.Imaging.PhotoshopBot"

showmethods PshopApp

imagepath = @"C:LoneRobot Script DevelopmentPhotoShopInteroptestimages"

imagearray = #((imagepath + "bg.jpg"),(imagepath + "layer1.png"),(imagepath + "layer2.png"),(imagepath + "layer3.png"),(imagepath + "layer4.png"))

oPhotoShop.BasicImageCompositor imagearray (imagepath +@"testoutput.psd") false true

All things being fair and just, you should have this in Photoshop – Ah crap!

Problem with this is that the copy/paste automation has placed the layers bang in the middle of the composition. This is how photoshop does this, regardless of image, if there is a transparent area around the image it takes the bounding box of the layer and pastes it smack in the middle of the comp. What it needs is a method for placing the layers when they are put in the composition. Looks like we will have to return to our class library and implement this.

Adding layer placement to the class

In a somewhat hacky way, photoshop does allow you to paste a layer into a certain position – If you select a marquee somewhere on the composition, it will use that as a location point to place the pasted layer. It takes the center of the layer and aligns it to the center of the selection marquee. Because of this, we can try to mimic this behavior in our loop.

So firstly, we need to work out how to automate the selection procedure in Photoshop. The Photoshop application object wants a mildly ambiguous Region object to be passed to the select function. This is listed in the Adobe help as

Array(Array(0, 0), Array(0, 100), Array(100,100), Array(100,0))

Now this is VBScript so my best guess is that in VB it is an array of Integer Arrays, with each array being the coordinate of the drawing surface. The other thing to note is if you traced the draw order, it means that it is drawing the selection marquee point-by-point in an anti-clockwise direction. This is a little confusing
to say the least, but since drawing selection marquees are pretty important,
It would be worth finding a way of calling this method with a simpler structure. To create a Photoshop selection region via VB the syntax is slightly different –

 Dim psdArray1() as Object = {x, y}

Dim psdArray2() as Object = {w, y}

Dim psdArray3() as Object = {x, h}

Dim psdArray4() as Object = {x, h}

Dim psdArray() as Object = {psdArray1, psdArray2, psdArray3, psdArray4}

PSDapp.Selection.Select(psdArray)

It seems that casting to anything else than a generic object causes an exception with the COM assembly. The best thing, in order to keep this DotNetty, would be to write a function to return the selection region by providing a Drawing.Rectangle object as an argument. That way it’s much easier to visualise the area that you want to select. Here’s what i came up with –

Public Function SelectionMarquee(ByVal Area As Drawing.Rectangle) As Object

Dim psArray1() As Object = {Area.X, Area.Y}

Dim psArray2() As Object = {Area.X, Area.Y + Area.Width}

Dim psArray3() As Object = {Area.X + Area.Height, Area.Y + Area.Width}

Dim psArray4() As Object = {Area.X + Area.Height, Area.Y}

Dim psArray() As Object = {psArray1, psArray2, psArray3, psArray4}

Return psArray

End Function

My idea is to pass a drawing.point array from max, since you are simply saying “paste this layer into position X,Y”. The function will then construct a rectangle with a height and width of 1 pixel. Then, when pasting the layer, it will be at the XY coordinates specified. Let’s see how that looks –

Public Sub BasicImageCompositor(ByVal Images() As String, _

ByVal Placement() As Drawing.Point, _

ByVal OutputFileName As String, _

ByVal HideApplication As Boolean, _

ByVal OpenOnCompletion As Boolean)

' get a fileinfo object to check the folder validity

Dim OutFile As New IO.FileInfo(OutputFileName)

If OutFile.Directory.Exists Then

' hide the photoshop UI

If HideApplication Then PSDapp.Visible = False

' Suppress the Photoshop dialogs (if any)

PSDapp.DisplayDialogs = 3

'   enum 3 =

'   PsDialogModes.psDisplayNoDialogs()

'load the first image and duplicate it into a new image document

If Images.Length > 0 Then

Dim BackGroundLayer As String = Images(0)

PSDapp.Load(BackGroundLayer)

PSDapp.ActiveDocument = PSDapp.Documents.Item(1)

PSDapp.ActiveDocument.Duplicate(OutFile.Name)

'close the original

'close(1) bypasses the save dialog

PSDapp.Documents.Item(1).Close(1)

'set the active document to the duplicate

PSDapp.ActiveDocument = PSDapp.Documents.Item(1)

'loop through the rest of the array, copying and pasting it into the

'duplicated document

For imageindex = LBound(Images) + 1 To UBound(Images)

PSDapp.Load(Images(imageindex))

PSDapp.ActiveDocument = PSDapp.Documents.Item(2)

PSDapp.ActiveDocument.Selection.SelectAll()

PSDapp.ActiveDocument.ArtLayers(1).Copy()

PSDapp.Documents.Item(2).Close(1)

PSDapp.ActiveDocument = PSDapp.Documents.Item(1)

'Select the point at which to paste the layer

Try

Dim Location As Drawing.Point = CType(Placement(imageindex), Drawing.Point)

Dim Marquee As Object = SelectionMarquee(New Drawing.Rectangle(Location.X, Location.Y, 1, 1))

PSDapp.ActiveDocument.Selection.Select(Marquee)

Catch ex As Exception

End Try

PSDapp.ActiveDocument.Paste()

Next

'save it

PSDapp.ActiveDocument.SaveAs(OutputFileName)

PSDapp.ActiveDocument.Close(1)

' show the ui and load the image, if desired

If HideApplication Then PSDapp.Visible = True

If OpenOnCompletion Then PSDapp.Load(OutputFileName)

End If

Else

' if the folder doesn't exist it will throw this exception

Throw New IO.DirectoryNotFoundException("Directory does not exist")

End If

End Sub

So now you need to change the code you call from 3dsMax to include a placement argument. This is just an array of dotnet point objects.

oPhotoShop = dotnetobject "LoneRobot.Imaging.PhotoshopBot"

imagepath = @"C:LoneRobot Script DevelopmentPhotoShopInteroptestimages"

imagearray = #((imagepath + "bg.jpg"),(imagepath + "layer1.png"),(imagepath + "layer2.png"),(imagepath + "layer3.png"),(imagepath + "layer4.png"))

LocationArray = #((DotNetObject "System.Drawing.Point" 0 0),(DotNetObject "System.Drawing.Point" 411 513),(DotNetObject "System.Drawing.Point" 109 531),(DotNetObject "System.Drawing.Point" 193 444),(DotNetObject "System.Drawing.Point" 314 421))

oPhotoShop.BasicImageCompositor imagearray LocationArray (imagepath +@"testoutput.psd") false true

The first point in the array is (0,0) – This is because it represents the Background layer position, so is disregarded by the loop in the class.

What do we get now?

An automated composition with layer placement, all from within 3dsMax!

Huzzah!

I hope this tutorial has been helpful and you can see the potential of being able to use this within 3DSmax. You really can automate just about everything, without even going near the photoshop interface. I’m quite excited about the possibilities of this, so I will perhaps write more in the future.

Further reading –

http://www.adobe.com/devnet/photoshop/scripting/

The download package has the assemblies and the VB version of the class itself. If you want the test images, just leech them from here. Cheers!

Update!

I have updated the wrapper with some more methods listed below. There are a few things you can now change with this class. A simple showmethods from 3ds max will give you all the info you need to use these functions. The enums are used within a few of the functions, like setting the blend modes. It’s just an easier way to specify these.

I would advise always using the function setactivelayer before any code, as it is a boolean function that returns true on setting the correct layer, and false if it doesn’t exist.

If you’ve got this far then you know what you are doing anyway! Enjoy!

Lastest Update!

I’ve added a whole load of classes exposing the save options for various other photoshop image formats. You can now save out to the following image types with this assembly –

  • .bmp
  • .jpg
  • .tga
  • .png
  • .eps
  • .psd
  • .gif
  • .tiff

Download at the bottom as usual!

New Update!

Just managed to get a handle on programming the Action Descriptor using VB. This is reasonably straight forward, you can use the Adobe Scripting Listener to record the actions. It’s amost the same as VB, as it is VBscript. This has basically allowed me to now integrate pretty much any action or command into the assembly. You could make a highly complex series of commands and wrap it into a fuction and call it straight from 3dsMax. Take a look at the new methods – there are many not exposed by the Adobe Object Library. If you think of a useful action or operation you’d like to see in this assembly, let me know below and i’ll try to integrate it for you.

download script

Generating PDF Documents via MXS and .NET

Apr 14, 2009 by     No Comments    Posted under: 3dsMax, DotNet, Imaging, Maxscript, Program Automation, Technical Research

Recently, I discovered an open source library written in C# called PDFSharp. This came about because I was working on a script that generated a text file with information in at the end. Unfortunately, when it came to refining the presentation, my ASCI skills are seriously limited. For example, my attempt at the LoneRobot logo looked like this –

............o
............|
......... ___
........ /../
......8=/- /=8  - BeepBeepDoWopDoDoopDeep!
....... ---
........| |
........o.o

In the magazine “ASCI Art Lovers Monthly” , this attempt gained a four asterisk rating, although others have subsequently informed me that the **** peppered over the artistic comments page are not related to this.

Because this generated document was to be printed and used as reference, It got me thinking about whether I could generate better printable documents directly from 3dsMax without having to use OpenOffice, Acrobat or any of my presentation skills. Fortunately, PDFSharp is an assembly that allows you to create PDF documents. The way it does this is to mirror a whole set of GDI+/WPF drawing classes and allow you to construct the page programmatically via DotNet. (If you want to look at a few DotNet GDI+ drawing methods for 3dsmax, check out this page)

Regardless of it’s limited appeal within Max, it is an amazing piece of open source code that should most certainly be looked at.

In order to integrate this, I wrote a small MXS Struct to wrap a few of the functions of PDFSharp’s GDI+ drawing methods, since this was all I was interested in using for my purposes. There are many, many more drawing and formatting options available through this assembly, but in the struct the following methods are available –

  • DrawImage (With optional scale multiplier)
  • DrawRectangle (Specify a corner radius to draw a rounded rectangle)
  • DrawEllipse
  • DrawLine (With different line styles)
  • DrawString (With various paragraph alignment options – Left,Right,Justify,Center)

The PDF is drawn on a GDI+ drawing surface, in this case a GDI bitmap the size of an A4 document. There is a basic example within the test code that creates the following PDF file –

Don’t Forget, you’ll need to replace the line where it loads in and draws the image, or the TestDocument function will fail.

You can get PDFSharp from here, and of course download the example below as normal. It’s well commented so you should be able to get an idea of how to use it. The source code of PDFSharp has a pretty comprehensive array of test projects to pick at.

I’m still waiting for our subscription copy of max 2010, so I’ll probably add a version later using the private/public declarations available in this release.

If anyone is interested, the text on the PDF is from the rather beautiful short story ‘The Man Who Planted Trees‘ by Jean Giono.

download script

XML LayoutPanel – Another Custom control for 3dsMax

Mar 24, 2009 by     No Comments    Posted under: DotNet, Technical Research, User Controls

Recently, I was using a FlowLayoutPanel to provide a UI to load XML presets on a Character Pose system. The FlowlayoutPanel is a really useful UI component available in the DotNet toolbox. It is basically a container for you to place other controls into, and as a result, is perfect for dynamic interfaces. The problem in Max is that DotNet event handlers need to be specified in GLOBAL scope in order for them to register. This creates issues when using them in as part of a custom attribute or modifier plugin, the very thing (regrettably) that I wanted to do.

The way around this to build your own class that inherits the FlowLayoutPanel and adds the handlers outside of Max. Since I was going to the trouble of doing this, I thought I’d add a couple of methods I’d previously put into my HitchHiker control. Here is the control in the interface –

The control to the right is a node storage system for all of the facial bones, and the left hand interface is the custom FlowLayoutPanel. Clicking a button gives access to the XML file where the stored transforms for various facial expressions are stored.

The Panel has a single method, populate <path string> ,which searches the folder and adds a button to the layout pane for each file it finds. It will also look for a related image. This is how I wanted to show the presets. If there is a png file with the same name as the file in the folder, it will put that on the button. If that isn’t present, it checks if there is a file called “Default.png”. This was so I could put a rough icon for the storage type if I wanted. If not, the default XML icon is used. There is a compact (pictured) and larger icon and it depends on the button size if the larger one gets used. Take a look at the class diagram below for more information on other properties you can set –

Setting up the control properties in 3dsMax

Here are some examples of setting properties using this class within 3dsMax – Note the namespace of the control –

dotNetControl XMLPanel “LoneRobot.UI.Character.XMLLayoutPanel” pos:[0,186] width:161 height:336

Also note the use of the + sign when using a custom enumeration in 3dsMax –

XMLPanel.thumbsize = dotNetObject “system.drawing.size” 140 30
XMLPanel.selectedborderwidth = 2
XMLPanel.border = true
XMLPanel.textalign=(dotnetclass “System.Drawing.ContentAlignment”).middleright
XMLPanel.imagealign=(dotnetclass “System.Drawing.ContentAlignment”).middleleft
XMLPanel.sortby=(dotnetclass “LoneRobot.UI.Character.XMLLayoutPanel+SortByOptions”).filename
XMLPanel.sortby=(dotnetclass “LoneRobot.UI.Character.XMLLayoutPanel+SortByOptions”).length
XMLPanel.sortby=(dotnetclass”LoneRobot.UI.Character.XMLLayoutPanel+SortByOptions”).lastwritetime
XMLPanel.populate DirectoryPath

Top of the Class

The more observant may have noticed that in the event section of the class diagram was an event called Picked. If you did, then award yourself a biscuit. Picked isn’t a standard dotnet event for the FlowLayoutPanel. It is in XMLLayoutPanel though, so what is this and how does it work?

You may already be familiar with the concept of Object Orientated Programming, or OOP. It’s kind of fundamental to working with DotNet, and getting a handle on it will really improve how you approach a task. If you’re not 100% straight on what all this OOP malarkey is, I’ll try to give a brief explanation. The DotNet framework is a big orchard of classes. Almost everything (with some exceptions naturally!) is a class in dotnet. One way to look at it is that if a class was a banana, the dotnetclass would be a blueprint for what a banana is. A dotnetobject is the actual banana. And there are multiple bananas.

Buttons are classes, Forms are classes, BackgroundWorkers are classes. Each time you use a dotnetobject, you are using an instance of that class.

Now this is all very straightforward so far, and is the sort of thing that really impresses girls when you talk to them about it.

Let’s go on by looking at what happens when you press a button in dotnet. In order to handle the event, you have an eventarg attached to it. This has relevant information pertaining to the button being pressed, like the x and y position of the mouse etc. However, on an XMLLayoutPanel, that sort of information isn’t really useful. What you need is something that will convey the data you need after the event is fired.

You’ll remember at the start that I talked about inheriting classes like the FlowLayoutPanel to make the XMLLayoutPanel. Where it gets more interesting is when you realize that the eventarg is also a class. This means you can inherit them also, and change the functionality to something you want to use.

For the XMLLayoutPanel, all that you really need is the path and filename to the XML file you have picked. Since the system.eventarg tied to a button press doesn’t have that kind of information in it, you can write a custom eventarg to provide it. Once you have defined the event name in Visual Studio, you will need to write the event handler class. Here is the setup of this in VB – don’t worry if it makes no sense, i’ll try to explain the process of what is going on.

Public Class XMLPickedEventArgs
Inherits EventArgs
Private _fileName As String
Public Property FileName() As String
Get
Return _fileName
End Get
Set(ByVal value As String)
_fileName = value
End Set
End Property
Public Class XMLPickedEventArgs
Public Sub New(ByRef control As XMLLayoutPanel, ByVal fileName As String)
control.CurrentItem = fileName
Me.FileName = fileName
End Sub
End Class

XMLPickedEventArgs is created with two constructors, a reference to me ,the base control itself (not the sender in this case, which would be the button) and the Tag property of the button, which is the string path of the XML file. Now that this object has been instantiated, it means the XML filename has been stored in the eventargs filename property, and it can now be passed to raise the event.

If you look at this class in Visual studio, you will see that the control has a new custom registered event in the properties window that isn’t part of the FlowlayoutPanel –

Even better, Intellisense tells us the arguments in our custom class. As the class has just one property, that is all we see. However, this is the only one we need! So from this, we know that if we access the filename property of the eventarg, we will get the path to the XML file we just clicked. This load string can then be handled in max and passed to whatever XML function you wish.

You can do the same when using the class in max by calling showproperties on the eventarg in max – Notice the structure is the same, with sender being the custom control itself, and args being the eventarg. Depending on what you pass from the assembly you could do other things, for example , In the sender variable you could have passed a reference to the button from the event, meaning that you could use the event to change the state of it (i.e. backcolor, border etc), or you could keep the sender as the XMLPanel and add a property to the eventarg to refer to the button. It’s really up to you.

on XMLPanel picked sender args do
(
showproperties args
listener output –
.FileName : <System.String>
.Empty : <System.EventArgs>, read-only, static

In short, with Visual Studio you can build a control as broad or as specialized as you like. In this case, I really wanted something for this particular task, and was able to build it. The main advantage is it bypasses the event handler issues of the 3dsMax command pane. Of course this approach could be done for a variety of filetypes

I hope this has given you a background as to why you would write your own event handler, and how to use it in Max. Feel free to download the control and use it in your projects. I’m using this control more and more in the utilities I write; I think that it gives a good visual feel to the UI and allows for dynamic layouts to be made where the layout logic is completely handled by the control, not by you.

Which is nice.

download script

Basic Color Adjustment in 3ds Max without Photoshop

Mar 17, 2009 by     No Comments    Posted under: DotNet, Imaging, Maxscript

A fair bit of my MXS/DotNet tinkering leads me on tangents that previously haven’t been within the remit of what I started. Normally this means I start writing a script which is half finished before I have an idea that seems far more interesting than the thing I actually started, by which time it’s too late and I’ve disappeared into a swirling abyss of ignorance and subterfuge.

Recently I was adding to another script for a 3D/Sculpture crossover project, when I realized that it would be helpful if I could perform some kind of image tweak in the script in order to adjust the results of what I was generating, without having to load it into Photoshop. Also, I was wondering why people use those white earbuds that came with their swooshy new ipod/phone when they are clearly crap. The other day I clearly identified that someone was listening to Glen Campbell, and that wasn’t good for me on two levels. One, that I could hear it, and two, that I knew it was Glen Campbell. Do you see what I mean about tangents?

Much of this article is the result of a post by Ofer Zelichover on CGTalk – When thinking about this I remembered a colormatrix method he posted a little while back. So thanks Ofer, you did much of the hard work already, I’ve just added a few different matrices to the mix.

The ColorMatrix Class

If you are familiar with how 3dsMax performs translation,rotation and scaling in the application, you will be familiar with the Transform Matrix. The Color Matrix is a similar principle, except with a 5×5 matrix with each row containing information about the RGB channels of an image, (with an extra column for the alpha channel). Without having to get into exactly what goes on, (there is plenty of information about this explained by far better qualified people) you can pass different color matrices via DotNet to an image in order to manipulate the pixel colour like the way a Transform matrix manipulates vertices or nodes.

Some of the Matrices are absolute values. I found two examples of a grayscale matrix. The one below seemed to be the most popular, taken from the NTSC guidelines on conversion of a color TV image to a black and white one. However, I also found an alternate grayscale matrix that accounts for linear color space. This seems to keep the highlights of the the original image a little better. I’ve included both methods in the struct code for comparison.

Some of the adjustment matrices need the current pixel values in order to base their adjustments, so these are implemented via some functions that pass back the corrected color matrix object. There is one function that handles all of the work, and the methods are commented within the download.

It is performed drawing the adjusted image onto a GDI bitmap. This is just about fast enough to perform the color adjustments within the utility. I had experimented with the lockbits method which is using unmanaged code but 3dsMax seems to have problems with this. It is important to specify the bitmap in the utility outside this function, since you don’t want to be continually creating bitmap objects with each slider event, as you would create a big memory problem. (There is a setimage function that creates it when the image is specified)

Filters Featured (Starting From Top Left)

  • Adjust Red Balance
  • Adjust Green Balance
  • Adjust Blue Balance
  • Saturation
  • Contrast
  • Brightness
  • Invert
  • Grayscale
  • Sepia
  • Red Channel Only
  • Green Channel Only
  • Blue Channel Only

At the moment, the utility passes the image back to a max display bitmap for save, this was to allow for a save in a non-windows image format. This is possible with an external image assembly, but not necessary for this.

The only other thing to note is the UI has a couple of custom controls – most notably the slider component that you can download with the code. This needs to be put in your scripts directory. These are some things i had developed for use in character setups, but were a bit more compact and had some functionality that could be set when the utility opens, rather than hardcoding it all into the script. I like this control because, when focused, it will allow you to scroll the middle mouse button to move the slider position.

If possible, I’d like to add a multiplication function to pass multiple matrices as dotnet doesn’t allow for this in the bitmapdata class. One for the future, and another half finished script. Apologies to any Glen Campbell fans, he’s not that bad, my Dad used to listen to him when I was young. Before his breakdown.

download.png

Using Base64 encoding in 3dsMax

Dec 17, 2008 by     8 Comments    Posted under: 3dsMax, DotNet, Imaging, Maxscript

If you have ever received an email and instead of your normal information for pharmaceutical-related special offers and personal member enhancement, you get a jumble of nonsense, you’re probably already aware of what a Base64 encoded string looks like. Email clients use MIME to transfer messages and attachments, and one way to break up things like images so that it can be sent is Base64 encoding.

aWNlLCBpY2UgYmFieSwgZHVtIGR1bSBkdW0gZHVtIGR1bSBkdW0g
ZHVtIGR1bSBkdW0sIHZhbmlsbGEgaWNlLCBpY2UsIGJhYnk=

Despite looking like the sound you make when trapping your plums in the fridge door, Base64 encoded strings can be used to represent images and sounds and deployed with scripts to avoid the need for external linked dependencies.

DotNet Provides some easy methods to do this within the framework, so here are some functions for converting images to Base64 within 3dsmax below.

fn ConvertImageToBase64String filename =
(
if (doesfileexist filename) do
(
memstream = dotnetobject "System.IO.MemoryStream"
ImgLoaded = ImageClass.fromfile filename
ImgLoaded.save memstream ImgLoaded.rawformat
Base64string = ConvertClass.ToBase64String (memstream.ToArray())
memstream.close()
return Base64String
)
)
fn ConvertBase64StringToImage string =
(
bytearr = convertclass.FromBase64String string
memstream = dotnetobject "System.IO.MemoryStream" bytearr
DecodedImg = ImageClass.fromstream memstream
memstream.close()
return DecodedImg
)

I’ve added these into a utility with a few extras (namely functions to convert and play Wav files using Base64 encoding) You can download this script at the bottom of the page.

Grimlock says “SSdtIGdvbm5hIG9wZW4gYSBjYW4gb2YgV0hPT1BBU1Mgb24geW91IQ==”

If if you have ever read the “Bitmap Values” topic in the MXSHelp, you will be aware of this script –

b=selectbitmap() -- open image file browser
bname="bitmap_"+(getfilenamefile b.filename) -- build name from filename
w=b.width -- get properties of bitmap
h=b.height
format "----------nfn load_% = (n" bname -- start defining function
format "local %=bitmap % %n" bname w h -- create bitmap in function
-- write out a function that unpacks an integer into a pixel color
format "fn unpack val = for p in val collect (r=p/256^2; g=p/256-r*256; b=mod p 256; color r g b)n"
for r=0 to h-1 do -- for each row in the bitmap
-- have function write the column of pixels to the bitmap
( format "setpixels % [0,%] (unpack #(" bname r
pixels=getpixels b [0,r] w -- read in the column of pixels
for c=1 to w do -- loop through each pixel
( p=pixels[c] -- get the pixel
-- pack the pixel into an integer and write it out
format "%" (((p.r as integer)*256+(p.g as integer))*256+(p.b as integer))
if c != w then -- if not at end of data
format ", " -- write a comma
else
format "))n" -- else close out the line
)
)
format "return %n" bname -- function returns the bitmap
format ")n----------n" -- finish off function definition
)

Base64 is a DotNet method of performing the same thing, and instead of returning a max bitmap, it returns a Dotnet image.

This could probably benefit from being moved into a dedicated dotnet assembly, as I found with the color control a few weeks back, similar functions are much slower within max. Therefore if you are converting large images you might find the UI snagged up for a while.

Finally, to convert text to and from Base64, here’s a great site I found that will do it for you! –

http://textop.us/Encryption/Base64

download script