Tagged with " featured"

What we are today comes from our thoughts of yesterday

Apr 11, 2012 by     5 Comments    Posted under: DotNet

Apologies for a slightly existential tone to my latest blog post – I was reading the excellent blog by Maya TD Hamish Mackenzie, and it did get me thinking. It’s a strange conclusion to come to when you realise that the thing that you were learning to make your life as an animator easier ends up taking over and stopping you from doing something that you love.

But that’s just one side of the argument.

In order to get to that stage, there is a choice that is made over which discipline to focus on. You have to be serious because there is no short learning curve in 3D. Any choice you make about career direction has to be for the same reason that you started anything in this industry to begin with – because you love doing it. Perhaps, part of the consternation is the idea that programming isn’t quite as glamorous as a more signposted artistic discipline. But there is no doubt that a TD requires a balance of logic and creativity that is difficult to find, as they are not natural bedfellows. Not only are they difficult to find, they are harder to draw the line between. Are you an artist who codes, or a coder who does art? Sometimes you are both of these. Your brain will tell you if you’re straying more towards the side you dont want to be. Hamish wrote in his post :

…doing animation is something that I think is really important for me as a technical animator.  Using the tools I’ve written, working through the workflows I’ve helped define, having to deal with all the bugs, shortcomings etc of the work environment I’ve helped create puts me in the shoes of my users and forces me to see and understand the implications of my design decisions.  And I think thats really important.

I’d have to agree – I certainly can’t think the same about animation in the way I may have once. I am too hardwired to see bottlenecks and tool potential with any process to allow me to spend the single minded application necessary these days. Hamish said it perfectly :

But the other big thing is the fact that its hard to stop thinking about the technical side of things while I’m animating.  Its been such a part of my mental process for so long that its really hard to turn it off.  And I think this is the biggest thing.  So many parts of the animating process have the potential to be improved, sped up, optimized, made easier etc…  And when I’m animating, all these thoughts are running through my head.  So just pushing those thoughts aside – or at least shelving them for later – is hard.

Many years ago I graduated with a degree in Sculpture. The notion that 10 or so years down the line I would be figuring out custom event delegates in C# wouldn’t have been in the forefront of my mind.  It’s just ironic that you’ve ended up becoming the thing that you wouldn’t actually choose to call yourself. So essentially, us lot are a strange breed. We live in code and dream of drawing. We camelCase shopping lists and hate the fact that we now spell colour, color.

I guess the important thing would be to make sure that you never lose your creative side, and channel that into your new trade.

Be Deep Bop De Doop 010101 #!-$Z#!

 

WindowBox Replanted

Jul 18, 2011 by     4 Comments    Posted under: DotNet

After sitting unloved on Scriptspot since last year, WindowBox has suddenly registered over 300 votes! Many thanks to everyone who pressed the plus button, and I’m glad you have all been finding it useful. The version on Scriptspot is now on v2.3 and this latest version has better 64bit support as well as listing the Slate Material Editor.

http://www.scriptspot.com/3ds-max/scripts/give-me-back-my-dialogs

 

 

 

 

 

Another  good window position script is also on Scriptspot too. Don’t forget to check out this script by Raymond H.Ingebretsen –

http://www.scriptspot.com/3ds-max/scripts/reset-3ds-max-windows

DotNet Demystified : Part 1

Mar 27, 2011 by     14 Comments    Posted under: DotNet

I get a fair few requests asking if I could do some beginner’s tutorials about how to use dotnet in 3dsMax. It seems that a lot of people would like to get a handle on how to use the language properly. It’s actually no surprise to me that this is the case  – I also found at the start that If approached in the wrong way, dotnet and maxscript is a confusing and verbose place,  much like IKEA on the first day of school holidays.

What I’ll try to do in these tutorials is not to give you a series of instructions on ‘what to type in max in order to use dotnet’, but give you a background into –

  • How it works
  • What you are looking for
  • Where to find answers when you are not sure of how to proceed.

There are dotnet tutorials that exist already, like on Paul Neale’s site that gives information on how to use various dotnet controls. This is all great information already, so there’s no real point to me replicating this. What my focus is in these articles is to deviate from my normal article and give you a background into the dotnet world from a beginner’s perspective.

I’ll try to keep the programming jargon to a minimum, but be warned, you will have to accept that there will be some new terms to learn. So with that in mind, lets kick off with some jargon from the word go….

Getting to grips with what ‘Object Oriented Programming’ actually means.

I say this, because not everyone approaches this from a programming background. Let’s assume that, like me, you were an artist originally and got into scripting as a means to write tools to aid you in your creative process. Now the first thing that most people will say is ‘Yeah, I know what Object Oriented (OO) is, move onto the next lesson already…” But let’s just pause there and consider what it actually is. Remember, Maxscript has some level of OO but it isn’t a true OO language.

‘Object Oriented’ will obviously mean that it uses objects, but let’s clarify what an object is. In a 3d program, an object can be many things – a shape, a primitive, a light, a camera. Its easy to relate to this as an object because you can see it, and set various visual aspects through the modifier stack.

A teapot, for example is a programmatical description of everything the computer needs in order to display a teapot in the viewport. In programming terms, this description is known as a class, and it’s a blueprint of how the particular object needs to be made. In order to make a teapot in 3dsMax, it normally looks for something known as a constructor. You might have read this term before – you’ll see it in the maxscript help at the start of any listing. It basically means the command that you have to call in order to create the particular object.

teapot

In the teapot’s case, it is the word ‘teapot’, so that’s about as straight forward as it gets. However, a constructor is actually a method, and methods need to be executed by adding a set of parentheses at the end, exactly like a function.

When you call the teapot constructor you make an instance of the teapot class. This means that it creates a copy of the teapot from the instructions in the class. This is a process known as instantiation. Until the class is instantiated, there is no object. You can see this by typing teapot into the max listener –

  teapot
  Teapot
  teapot()
  $Teapot:Teapot01 @ [0.000000,0.000000,0.000000]

When you type teapot it just returns the class name, verifying that it exists. It is only when you call the constructor (by adding the parentheses) that the class is instantiated. In simple terms, you have made a teapot.

Okay, so where does this fit in with dotnet? You might be getting annoyed that I haven’t even mentioned anything about using dotnet yet. But this is the key to using it – Once you have embraced the Idea of what object oriented programming actually is, you will be able to take greater steps with dotnet. So the parallels between the teapot example in maxscript is very similar to how you should approach programming with dotnet.

So let’s recap so far –

blueprint

With a teapot, the constructor is very simple – the default constructor creates a teapot with default parameters, radius and segments. We can set these after the teapot is made, by using a property call. So if we set a value to the radius, the teapot changes size. We might want that from the start, so the teapot constructor also accepts multiple arguments.

But let’s also imagine that the appearance of the teapot is something that we want to organise for ourselves. In dotnet, we might decide that the way a teapot looks would merit having an object to specifically organise this information. So in order to acheive this, we might write a class to handle this. Lets call this the geometry_appearance class.  In this class, there would be the properties we needed like radius and segments. So in this example, the teapot would require a geometry_appearance class in order to create one. So how do we do this? well, it’s a class so you see we treat it in exactly the same way – by instantiating it.

So we have an instance of the geometry_appearance class which can be passed to the teapot constructor. No big deal, we still just get a teapot. But hang on, a sphere has properties like radius and segments. We could in theory use the instance of geometry_appearance and pass it to a sphere to create a sphere with the same number of segments and radius. So the power of the geometry_appearance class is that it can be used to create many different objects, depending on it’s design.

pseudo-class

This is of course hypothetical, these objects are not created like this in maxscript, but when you start looking into dotnet classes, you will start to find that there are properties that you can set to numerical values and boolean values (true and false) but there are just as many properties that can only be set by supplying other classes.

Take the dotnet label for example. We know the difference between a class and an object now, so we know if we called the command dotnetclass on label, we would just get the properties of the class, not the object. When you ask a dotnetclass information about itself, it will display the class name in between brackets like these –  <>.

lbl = dotnetclass "label"
dotNetClass:System.Windows.Forms.Label
  show lbl
  .CheckForIllegalCrossThreadCalls : <System.Boolean>, static
  .DefaultBackColor : <System.Drawing.Color>, read-only, static
  .DefaultFont : <System.Drawing.Font>, read-only, static
  .DefaultForeColor : <System.Drawing.Color>, read-only, static
  .ModifierKeys : <System.Windows.Forms.Keys>, read-only, static   .MouseButtons : <System.Windows.Forms.MouseButtons>, read-only, static   .MousePosition : <System.Drawing.Point>, read-only, static
true

By calling the object method, we actually create a label object. Look at the amount of properties now!

lbl = dotnetobject "label"
dotNetObject:System.Windows.Forms.Label
show lbl
  .AccessibilityObject : , read-only
  .AccessibleDefaultActionDescription :
  .AccessibleDescription :
  .AccessibleName :
  .AccessibleRole :
  .AllowDrop :
  .Anchor :
  .AutoEllipsis :
  .AutoScrollOffset :
  .AutoSize :
  .BackColor :
  .BackgroundImage :
  .BackgroundImageLayout :
  .BindingContext :
  .BorderStyle :
  .Bottom : , read-only
  .Bounds :
  .CanFocus : , read-only
  .CanSelect : , read-only
  .Capture :
  .CausesValidation :
  .CheckForIllegalCrossThreadCalls : , static
  .ClientRectangle : , read-only
  .ClientSize :
  .CompanyName : , read-only
  .Container : , read-only
  .ContainsFocus : , read-only
  .ContextMenu :
  .ContextMenuStrip :
  .Controls : , read-only
  .Created : , read-only
  .Cursor :
  .DataBindings : , read-only
  .DefaultBackColor : , read-only, static
  .DefaultFont : , read-only, static
  .DefaultForeColor : , read-only, static
  .DisplayRectangle : , read-only
  .Disposing : , read-only
  .Dock :
  .Enabled :
  .FlatStyle :
  .Focused : , read-only
  .Font :
  .ForeColor :
  .Handle : , read-only
  .HasChildren : , read-only
  .Height :
  .Image :
  .ImageAlign :
  .ImageIndex :
  .ImageKey :
  .ImageList :
  .ImeMode :
  .InvokeRequired : , read-only
  .IsAccessible :
  .IsDisposed : , read-only
  .IsHandleCreated : , read-only
  .IsMirrored : , read-only
  .LayoutEngine : , read-only
  .Left :
  .Location :
  .Margin :
  .MaximumSize :
  .MinimumSize :
  .ModifierKeys : , read-only, static
  .MouseButtons : , read-only, static
  .MousePosition : , read-only, static
  .Name :
  .Padding :
  .Parent :
  .PreferredHeight : , read-only
  .PreferredSize : , read-only
  .PreferredWidth : , read-only
  .ProductName : , read-only
  .ProductVersion : , read-only
  .RecreatingHandle : , read-only
  .Region :
  .Right : , read-only
  .RightToLeft :
  .Site :
  .Size :
  .TabIndex :
  .TabStop :
  .Tag :
  .Text :
  .TextAlign :
  .Top :
  .TopLevelControl : , read-only
  .UseCompatibleTextRendering :
  .UseMnemonic :
  .UseWaitCursor :
  .Visible :
  .Width :
  .WindowTarget :

When 3dsmax uses a dotnetclass, it can perform certain things without having to change how you would do them is maxscript. So for example, the text property on a label is listed as

.Text : <System.String>

So you can see that text takes a string as a property. You should notice that it is a system.string, which means it’s actually a dotnet string, not a maxscript string. These are both strings but they are different data types. Good news is that maxscript automatically converts the maxscript string into a dotnet one, so you dont have to do anything special. This is the same for booleans and values like integers and floats. (a float in dotnet is known as a single) In programming, this is known as casting.

blueprint-cast

However, while maxscript can cast a maxscript string into a dotnet string, it can’t do something like this –

.Size : <System.Drawing.Size>

Rather than have a width and height property, a dotnet label has a size. Like the hypothetical teapot example, it has a class that decides how it is supposed to be.  So to set the size of the label, we need to instantiate the size class. So the create this class into an object, surely we need to call the same dotnetobject method in max?

sz = dotnetobject "System.Drawing.Size"
  -- Runtime error: No constructor found which matched argument list: System.Drawing.Size

in this case, no. The size class’s constructor needs some other parameters in order to instantiate it. So how do we know what these are? In maxscript you can use the command :

dotnet.showconstructors dotnetclass

So remember, we are not operating on the object yet, because we haven’t made it – a class has the constructor, not the object so we need to pass that –

dotnet.showconstructors (dotnetclass "System.Drawing.Size")
System.Drawing.Size <System.Drawing.Point>pt
System.Drawing.Size <System.Int32>width <System.Int32>height

Bingo! here’s the information that you need. This means that the size class actually has two possible constructors. The first is actually saying that you could pass it an instance of the System.drawing.point class to call the constructor. The second is saying that it needs two properties – width and height. As you can see, these are instances of the integer class. (System.Int32) Fortunately, maxscript casts integers into dotnet integers without a problem so you can instantiate the size class with two numbers.

lbl = dotnetobject "label"
  dotNetObject:System.Windows.Forms.Label

sz = dotnetobject "System.Drawing.Size" 20 20
dotNetObject:System.Drawing.Size
lbl.size = sz
dotNetObject:System.Drawing.Size

So there you have it, Some properties will need classes to specify what happens to them, and those classes might even have their own classes for construction. But just remember where you are in terms of creating the object and read what the listener is telling you. It really does give you most of the information that you need.

So where can this fall down? Let’s look at another example and explain what is going on. Say we wanted to set the label’s backcolor property –

.BackColor : <System.Drawing.Color>

Lets try making an instance of the color class.

dotnetobject "system.drawing.color"
  -- Runtime error: No constructor found which matched argument list: system.drawing.color

Ok, so it’s saying it needs a constructor that we havent supplied.

dotnet.showconstructors (dotnetclass "system.drawing.color")
  false

That’s not helping us like the last time.

columbo-1

Let’s try a showproperties call on the class itself.

show (dotnetclass "system.drawing.color")
  .AliceBlue : , read-only, static
  .AntiqueWhite : , read-only, static
  .Aqua : , read-only, static
  .Aquamarine : , read-only, static
  .Azure : , read-only, static
  .Beige : , read-only, static
  .Bisque : , read-only, static
  .Black : , read-only, static
  .BlanchedAlmond : , read-only, static
  .Blue : , read-only, static
  .BlueViolet : , read-only, static
  .Brown : , read-only, static
  .BurlyWood : , read-only, static
  .CadetBlue : , read-only, static
  .Chartreuse : , read-only, static
  .Chocolate : , read-only, static
  .Coral : , read-only, static
  .CornflowerBlue : , read-only, static
  .Cornsilk : , read-only, static
  .Crimson : , read-only, static
  .Cyan : , read-only, static
  .DarkBlue : , read-only, static
  .DarkCyan : , read-only, static
  .DarkGoldenrod : , read-only, static
  .DarkGray : , read-only, static
  .DarkGreen : , read-only, static
  .DarkKhaki : , read-only, static
  .DarkMagenta : , read-only, static
  .DarkOliveGreen : , read-only, static
  .DarkOrange : , read-only, static
  .DarkOrchid : , read-only, static
  .DarkRed : , read-only, static
  .DarkSalmon : , read-only, static
  .DarkSeaGreen : , read-only, static
  .DarkSlateBlue : , read-only, static
  .DarkSlateGray : , read-only, static
  .DarkTurquoise : , read-only, static
  .DarkViolet : , read-only, static
  .DeepPink : , read-only, static
  .DeepSkyBlue : , read-only, static
  .DimGray : , read-only, static
  .DodgerBlue : , read-only, static
  .Firebrick : , read-only, static
  .FloralWhite : , read-only, static
  .ForestGreen : , read-only, static
  .Fuchsia : , read-only, static
  .Gainsboro : , read-only, static
  .GhostWhite : , read-only, static
  .Gold : , read-only, static
  .Goldenrod : , read-only, static
  .Gray : , read-only, static
  .Green : , read-only, static
  .GreenYellow : , read-only, static
  .Honeydew : , read-only, static
  .HotPink : , read-only, static
  .IndianRed : , read-only, static
  .Indigo : , read-only, static
  .Ivory : , read-only, static
  .Khaki : , read-only, static
  .Lavender : , read-only, static
  .LavenderBlush : , read-only, static
  .LawnGreen : , read-only, static
  .LemonChiffon : , read-only, static
  .LightBlue : , read-only, static
  .LightCoral : , read-only, static
  .LightCyan : , read-only, static
  .LightGoldenrodYellow : , read-only, static
  .LightGray : , read-only, static
  .LightGreen : , read-only, static
  .LightPink : , read-only, static
  .LightSalmon : , read-only, static
  .LightSeaGreen : , read-only, static
  .LightSkyBlue : , read-only, static
  .LightSlateGray : , read-only, static
  .LightSteelBlue : , read-only, static
  .LightYellow : , read-only, static
  .Lime : , read-only, static
  .LimeGreen : , read-only, static
  .Linen : , read-only, static
  .Magenta : , read-only, static
  .Maroon : , read-only, static
  .MediumAquamarine : , read-only, static
  .MediumBlue : , read-only, static
  .MediumOrchid : , read-only, static
  .MediumPurple : , read-only, static
  .MediumSeaGreen : , read-only, static
  .MediumSlateBlue : , read-only, static
  .MediumSpringGreen : , read-only, static
  .MediumTurquoise : , read-only, static
  .MediumVioletRed : , read-only, static
  .MidnightBlue : , read-only, static
  .MintCream : , read-only, static
  .MistyRose : , read-only, static
  .Moccasin : , read-only, static
  .NavajoWhite : , read-only, static
  .Navy : , read-only, static
  .OldLace : , read-only, static
  .Olive : , read-only, static
  .OliveDrab : , read-only, static
  .Orange : , read-only, static
  .OrangeRed : , read-only, static
  .Orchid : , read-only, static
  .PaleGoldenrod : , read-only, static
  .PaleGreen : , read-only, static
  .PaleTurquoise : , read-only, static
  .PaleVioletRed : , read-only, static
  .PapayaWhip : , read-only, static
  .PeachPuff : , read-only, static
  .Peru : , read-only, static
  .Pink : , read-only, static
  .Plum : , read-only, static
  .PowderBlue : , read-only, static
  .Purple : , read-only, static
  .Red : , read-only, static
  .RosyBrown : , read-only, static
  .RoyalBlue : , read-only, static
  .SaddleBrown : , read-only, static
  .Salmon : , read-only, static
  .SandyBrown : , read-only, static
  .SeaGreen : , read-only, static
  .SeaShell : , read-only, static
  .Sienna : , read-only, static
  .Silver : , read-only, static
  .SkyBlue : , read-only, static
  .SlateBlue : , read-only, static
  .SlateGray : , read-only, static
  .Snow : , read-only, static
  .SpringGreen : , read-only, static
  .SteelBlue : , read-only, static
  .Tan : , read-only, static
  .Teal : , read-only, static
  .Thistle : , read-only, static
  .Tomato : , read-only, static
  .Transparent : , read-only, static
  .Turquoise : , read-only, static
  .Violet : , read-only, static
  .Wheat : , read-only, static
  .White : , read-only, static
  .WhiteSmoke : , read-only, static
  .Yellow : , read-only, static
  .YellowGreen : , read-only, static
  .Empty : , read-only, static

Whoa, result. So why is this showing all of the color properties without instantiating the class first?

This is because the color class is a static class. A static class means that the properties and functions can be utilised without having to make an instance of the class. If we wanted a color that was predefined in the class – like YellowGreen, we could use one of these properties.

However, remember these are just the properties of the class which in this case are a load of color presets. We might also need to call showmethods too.

showmethods (dotnetclass "system.drawing.color")
  .[static]Equals objA objB
  .[static]FromArgb argb
  .[static]FromArgb alpha baseColor
  .[static]FromArgb red green blue
  .[static]FromArgb alpha red green blue
  .[static]FromKnownColor color
  .[static]FromName name
  .[static]ReferenceEquals objA objB
true

You can see that these are static methods, so do not need a class instance. From this, we can deduce that the color class can be used with the .fromARGB method which has numerous methods. We can use this to specify an RGB value –

.[static]<System.Drawing.Color>FromArgb
System.Int32>red
System.Int32>green
System.Int32>blue

So we can use this in max in the following syntax –

(dotnetclass "system.drawing.color").fromARGB 255 0 0
  dotNetObject:System.Drawing.Color

You will encounter static classes from time to time. Most notably with the image class. This is used in the same way. You’ll see it is static as soon as you dig in to find information about what you’re doing with it. As soon as you see the static keyword, it means you’ll be using the class itself and not the instantiated object.

showmethods (dotnetclass "system.drawing.image")
  .[static]Equals objA objB
  .[static]FromFile filename
  .[static]FromFile filename useEmbeddedColorManagement
  .[static]FromHbitmap hbitmap
  .[static]FromHbitmap hbitmap hpalette
  .[static]FromStream stream
  .[static]FromStream stream useEmbeddedColorManagement
  .[static]FromStream stream useEmbeddedColorManagement validateImageData
  .[static]GetPixelFormatSize pixfmt
  .[static]IsAlphaPixelFormat pixfmt
  .[static]IsCanonicalPixelFormat pixfmt
  .[static]IsExtendedPixelFormat pixfmt
  .[static]ReferenceEquals objA objB

That’s about all for part one of the tutorial. I hope it’s explained things in a concise way, I’ll be adding to these over the next few weeks and introducing other aspects of dotnet so that you can start to use it more efficiently. If you have any aspects of DotNet that you cant get a handle on, contact me through the form on this site and let me know – I can add them to a DotNet FAQ.

Stuff the revolution, I’m thinking about a Character Selection Framework

Jun 1, 2010 by     10 Comments    Posted under: 3dsMax, Characters, DotNet, Rigging, Technical Research, User Controls

 

One of the most time consuming things always seems to be building pesky UI’s in Maxscript. The ever reliable Visual Maxscript editor, whilst having the reputation of Marmite, has been compromised by a spectacular bug in the last few releases that prints the last few characters of the previous line onto the next one, thus scrambling your code.

rollout MXSMashup "Untitled" width:169 height:171
     (
         button btn1 "Button" pos:[7,8] width:70 height:39
     39
         bitmap bmp1 "Bitmap" pos:[97,25] width:52 height:84
    84
        checkbutton ckb1 "CheckButton" pos:[28,138] width:132 height:20
    20
        combobox cbx1 "ComboBox" pos:[16,91] width:55 height:1
    1
        edittext edt1 "" pos:[14,60] width:55 height:20
    20
    )

All this means you end up manually  tweaking positions and sizes till the end of time, for a script  that is essentially doing something very simple. It’s always faster to have a small dialog with a picture of the rig with fast access to individual bones to remove the necessity of picking them in the viewport. However, generating these things up can usually take more time than we have to give in production. I thought it would be an interesting focus to my research to see how easy it would be to implement a system that allowed even a non-scripter to build specialised dialogs for selection, ultimately a task that you end up doing a billion times with a rig.

The IDE driven usercontrol approach

Now Visual Studio has a pretty nifty IDE, and you can layout windows forms pretty darned fast with it. It would be rather handy to have something similar but centred around 3dsMax, albeit with a basic layout toolset. The obvious approach (in terms of linking the dotnet framework to 3dsMax objects) was binding controls to the names of scene nodes. Okay, maybe scene node names might not be the most robust of solutions but it is fine for this sort of thing – but if someone starts renaming rigs just for fun, you’re going to be screwed on a bigger level than just the selection tools.

Look at this lot –

all dialogs

With Rig Studio, you can do this without any coding in no time at all. Here’s how –

A Runtime Design Surface

Have a look at the video below to see an example of how RigStudio sets up a basic control dialog and then stores it to be read by a user end interface.

You build up an interface by drag and drop, and there are preset sizes and shapes defined for speed. You can size these up to whatever you like. There are options to set the forecolor and background color, and to position text blocks to illustrate the different areas of the dialog.

You might recognise the control on the right of the UI – It’s a propertygrid and is a great little control for exactly this type of thing, as it takes a dotnetobject and displays an area so that you can adjust the properties easily. You can use this out of the box, but I have customised it to only display the properties that you need to adjust. I have also written a custom UITypeEditor class to handle the control list drop down method for choosing hierarchies.

TypeEditor

This is not a default behaviour of the propertygrid control. the Child property on a rigcontrol object is a string denoting the name of the next node in the hierarchy. In order to give better design-time support, I am asking the UITypeEditor  to take an array of controls from the design surface and build a sorted listbox from the results. It needs to be sorted as the controls could have been created in any order.

Custom Toolstrip Renderers

Alignment-Options_Dotnet Alignment-Options

Keen-eyed UI aficionados will notice the similarity of the drop down menu on the left to 3ds Max’s menu system, and not the default dotnet toolstrip renderer (which is on the right). Its a subtle difference, but this is due to the use of a custom toolstrip renderer. Autodesk has provided this as part of the MaxCustomControls namespace.

MaxToolStripSystemRenderer is a custom renderer and is assigned by simply giving an instance of it to the renderer property. So you can implement this into Maxscript pretty easilly too, if you want everything to look like it is part of the same application.

-- VB
Me.CtrlMenu.Renderer = New MaxCustomControls.MaxToolStripSystemRenderer()
-- MXS
CtrlMenu.Renderer = dotnetobject "MaxCustomControls.MaxToolStripSystemRenderer"

What might annoy you about the toolstrip, especially if you run the UI from the dark side of the force, is the border that windows paints around the toolstrip menu. This is just plain annoying if you are trying to keep a nice minimal looking UI.

toolstrip

It’s probably not so noticeable on a light UI scheme, but it would be great to get rid of it. With some poking around on some VB forums, I found this solution. There is a protected method called OnRenderToolstripBorder. All that you need to do is to override this and tell it to do nothing, as you don’t want the border. The great thing about class inheritance is that we can inherit the custom renderer class made by Autodesk, implement the functionality we need, and have a new class to use. By only overriding the methods that we want to change, we keep all the other existing render bits. It is actually a pretty short entry to have a version of the Autodesk Toolstrip renderer without the white border line –

Public Class BorderlessMaxToolStripRenderer
Inherits MaxCustomControls.MaxToolStripSystemRenderer
	Protected Overrides Sub OnRenderToolStripBorder _
				(ByVal e As System.Windows.Forms.ToolStripRenderEventArgs)
 	'Do nothing
 	End Sub
End Class

So now you can have the great functionality of the toolstrip without having an incongruous looking UI. It would be pretty easy to dynamically compile this class as part of a maxscript, so that you dont have to distribute an assembly too.

Toolstrip-CustomRenderer

Weirdly, you don’t get this border with a menustrip, which is fine for most things, but it’s not got the same choice of controls. Its a shame i didnt realise this until I had gone down this route!

Design-TIme Alignment Functions

Controls have a left, right, top and bottom property which returns their position relating to their parent container. This is a local position and unrelated to screen position. This makes the majority of options in the alignment menu relatively easy. Where it becomes more complicated is control spacing. Controls can be added in any order, so you need a method of deciding where a control is in dialog position terms. If you are performing a spacing arrangement, you need a method that can take an array of controls and return them sorted into order by their location property. As you can space vertically or horizionally, you’ll need a way of tells the method the direction that you want to sort them. It is precisiely this sort of logic that the Icomparer can handle, sorting abstract classes according to  defined properties.

Fortunately, the dotNet framework has some powerful sorting method called Icomparer for doing this job.

Public Enum AlignmentCompareOptions
    Vertical
    Horizontal
End Enum

Friend Class PointComparer
    Implements IComparer(Of Drawing.Point)
    Dim _CompareDirection As AlignmentCompareOptions

    Public Sub New(ByVal Direction As AlignmentCompareOptions)
        _CompareDirection = Direction
    End Sub
    Public Function Compare(ByVal x As Drawing.Point, ByVal y As Drawing.Point) As Integer Implements IComparer(Of System.Drawing.Point).Compare
        Dim pointX As Point = DirectCast(x, Point)
        Dim pointY As Point = DirectCast(y, Point)

        Select Case _CompareDirection
            Case AlignmentCompareOptions.Vertical
                Select Case True
                    Case pointX.Y > pointY.Y
                        Return 1
                    Case pointX.Y < pointY.Y
                        Return -1
                    Case pointX.Y = pointY.Y
                        Return 0
                End Select
            Case AlignmentCompareOptions.Horizontal
                Select Case True
                    Case pointX.X > pointY.X
                        Return 1
                    Case pointX.X < pointY.X
                        Return -1
                    Case pointX.X = pointY.X
                        Return 0
                End Select
        End Select

    End Function

End Class

In order to use this class, you add the controls to a SortedDictionary, one of the classes that you can supply an Icomparer to the constructor. You’ll see this SortedDictionary takes a point value, and a control value. As these are added the Sorted dictionary automatically sorts them into the order they are according to the direction (vertical or horizontal)

Dim CompareControlList As New SortedDictionary(Of Point, Control)(New PointComparer(Direction))

The Icomparer sorts it by returning an integer that relates to the evaluated expression within the IComparer, 1 if pointA is greater than pointB, –1 if B is greater than A, and 0 if they are the same.

The conclusion to all this is that you can use the results to calculate the difference between the first and last control instances, deduct the combined height or width of the controls and divide by the number of gaps to get the distance needed to space the controls evenly.

XML Control Serialization

This was a new thing for me to get my head around, and in doing so I genuinely believe I have only scratched the surface on the sort of things that you can do. In my case, XML serialization provided an elegant solution to this problem –

XMLfile.ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).InnerText

Sometimes, negotiating an XML tree manually becomes difficult to track, but in this case, you explicitly know what type of information you are dealing with. XML serialization allows you to deconstruct dotnetobjects, with the idea that you can use the serializer’s logic to re-assemble them at another time, perhaps even to send data to a compliant application on a different computer.

To use the example of, say a bog-standard button, we could serialize this but we have to address a couple of issues first. XML needs a specific data type to store the information. You couldn’t just pass it an enum style like “borderstyle.fixedsingle” as a string and expect it to know what to do with it. Also, there are a sh*t load of properties that we have on a button, many of which don’t particularly represent anything to do with the visual state. You would be serializing a lot of useless information into the XML file, making the file larger than it needs to be.

Writing a Custom Serialization class

I ended up by writing a class specifically to handle the serialization of the design surface to XML. It actually consists of three classes, the RigControlSerializer is where the action happens. It stores all the dialog information necessary to recreate the size and shape of the stored data. RigControl and RigControlText are subclasses that allow the serializer to loop through the controls on the design surface and store them into an array list. So the serializer property controlist actually has an array of the RigControl class, not the actual control itself, but is perfect as it is all the information that the deserializer needs to recreate the control faithfully on another surface.

Custom Serializer Classes

The great thing about writing a serializer class is that each property becomes a branch in the xml file automatically,  and subsequent paths are made as each property is serialized in order. So an entire dialog can be written out to XML as follows –

Private Function SerializeDesignSurfacetoXML(ByVal XMLFilename As String) As Boolean

        Try
            Dim SavedDialog As RigControlSerializer = New RigControlSerializer()

            With SavedDialog
                .DialogSize = DesignSurface.Size
                .DialogBackcolor = (CType(DesignSurface.BackColor, Color)).ToArgb
                .ControlList = CreateRigControlCopyArray()
                .TextLabelList = CreateTextLabelCopyArray()
                .BackgroundImage = CType(Me.DesignSurface.BackgroundImage, System.Drawing.Bitmap)
                .CharacterName = Me.CharacterName
            End With

            Dim writer As New XmlSerializer(GetType(RigControlSerializer))
            Dim file As New StreamWriter(XMLFilename)
            writer.Serialize(file, SavedDialog)
            file.Close()
            writer = Nothing
            Return True

        Catch ex As Exception
            Return False
            MessageBox.Show(ex.InnerException.ToString)
        End Try
    End Function

That, for me is a pretty straightforward way of converting a whole bunch of dotnet objects into an XML file!

As you control the headings of the XML branches via the class, its pretty easy to see what is going on in the XML file itself. As much as it seems like more work, I think it is a quite elegant method.

xml

One other useful thing to crop up with this is Binary Serialization. You use the same serializer class to dump all the control positions to a temporary .dat file. Why is this useful? You’ve just used the serializer to introduce an undo buffer, in case the control alignment didn’t happen as you expected, or as I do pick the wrong direction to align.

The RigSelector User Control

RigSelectorVS

The ultimate payoff to the IDE approach is providing a simple way to rebuild the selection dialog from the XML data. The RigSelector usercontrol is just a composite control that only really features a panel. The key is having a method to deserialize the XML to populate the panel with the stored node hierarchy.

We have reused the BorderlessAutodeskToolstripRenderer that we used earlier for the button.

It can resize the parent form to the dimensions of the stored dialog control.

RigSelectorClass

The rest is done by adding the handlers when deserializing the control to fire the selection within max. This perhaps the simplest part. To fire Maxscript code from within a dotnetclass you use –

>ManagedServices.MaxscriptSDK.ExecuteMaxscriptCommand(<<MXS_String>>)

Lastly, my best piece of advice about implementing ANY form of custom dotnet control class in 3dsMax.

Don’t be shy about calling enable accelerators

to pass focus back to your max shortcuts!

As much as the maxform control is supposed to handle all this stuff, using a custom class can put a spanner in the works, and there’s nothing worse than realising that you’ve just knocked out your hotkeys.

ManagedServices.AppSDK.EnableAccelerators()

I call this after every button click, which might be overkill but it’s better than losing focus to your dotnetcontrol and being powerless to get them back without a restart or frenzied maxscript call. Any one wanting to do this sort of thing should really look into ManagedServices.dll, there is some great stuff in there.

Polar-DesignSurface

I hope this article has been of interest! Comments are always welcome!

Rig Studio Update!

I recently added automatic rig generation to Rig Studio – This speeds things up considerably.

I have also added a new control – A layer control. This allows you to store a name string of a corresponding layer. This has hide and unhide functionality build in, so you can control character rig and mesh visibility without having to open the layer manager.

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