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.

404
electrician