How To ... Enhance the Morpher Modifier With Floating Controls

The Morpher modifier in 3ds Max provides up to 100 morph channels, but only 10 of them are visible at a time and can be controlled only using a value spinner. Using MAXScript, we will add a very basic floating UI extension to the Morpher modifier representing and controlling the Morpher channels using progress bars.

The script will also demonstrate the dynamic creation of UI elements by a script and the usage of time and change callbacks to react to time and properties changes.

Related Topics:

Defining MacroScripts

Morpher : Modifier

Morpher_Channel_Access

Time Change Callback Mechanism

Change Handlers and When Constructs

NATURAL LANGUAGE

Package the code as macroScript to be able to use as a button, menu item or shortcut.

Make sure an object with a morpher modifier is selected

Get the morpher modifier and collect the used channels

Build a string describing a dialog with controls for each used channel.

Create a function to update all controls’a values

Register a callback to call the update function when scene time changes

Register a callback to call the update function when a parameter of the morpher changes

MAXSCRIPT

   macroscript MorpherFloater category: "HowTo"
   (
   global mf_float, mf_morpher_mod
   on isEnabled return
    selection.count == 1 and (try($.morpher)catch(undefined)) != undefined
   on execute do
   (
    mf_morpher_mod = $.modifiers[#morpher]
    used_channels = #()
    txt ="rollout mf_main \"Morpher Floater\" (\n"
    for i = 1 to 100 do
    (
     if WM3_MC_HasData mf_morpher_mod i then
     (
      append used_channels i
      txt +="progressbar mf_slider_"+ i as string
      txt +=" value:"+ (WM3_MC_GetValue mf_morpher_mod i) as string
      txt +=" width:150 height:18 across:2 align:#left\n"
      txt +="edittext mf_label_"+i as string
      txt +=" align:#right text:\""+i as string+": "
      txt +=(WM3_MC_GetName mf_morpher_mod i) +"\"\n"
      txt +="on mf_slider_"+i as string+" clicked val do (\n"
      txt +="WM3_MC_SetValue mf_morpher_mod "
      txt += i as string+" (val as float) \n"
      txt +="SliderTime +=0)\n"
     )
    )--end i loop
   txt +=")\n"

   createDialog (execute txt) 340 (used_channels.count*24)
   txt ="fn mf_update_slider = (\n"
   for i in used_channels do
   (
    txt +="mf_main.mf_slider_"+i as string
    txt +=".value = WM3_MC_GetValue mf_morpher_mod "+i as string+" \n"
   )--end i loop
   txt +=")\n"

   global mf_update_slider = execute txt
   registertimecallback mf_update_slider
   deleteAllChangeHandlers id:#morpher_floater

   when parameters mf_morpher_mod changes \
    HandleAt:#RedrawViews \
    id:#morpher_floater do mf_update_slider()
   )--end execute
   )--end script

Step-By-Step

macroscript MorpherFloater category:"HowTo"
(

The macroScript will be called MorpherFloater . To use the script, you can go to Customize... and drag the script from the category "HowTo" to a toolbar, a menu, a quad menu or assign to a keyboard shortcut.

Defining Macro Scripts

global mf_float, mf_morpher_mod

We will need a couple of global variables to store the rollout used to display the Morpher controls, and the currently selected Morpher modifier to control.

Scope of Variables

on isEnabled return
selection.count == 1 and (try($.morpher)catch(undefined)) != undefined

The script will become active only when a single objects is selected in the scene and it has a Morpher modifier on its stack.

The isEnabled handler evaluates the expression after the return statement and enables the script’s button, menu item or shortcut when the expression yields true. Otherwise, the script is disabled.

In the first statement, we compare the number of selected objects in the scene with 1. If there is only one selected object, this expression returns true.

Then we also ask whether the selection has a Morpher modifier applied. We do this inside a try()catch() context – if there is a Morpher modifier, it will be returned as the result of this expression. If there is no Morpher, an error would normally occur. We catch this error and return undefined instead. The result is then checked whether it is NOT undefined. This gives us the result of true when a Morpher was encountered, and false when not.

Only when the first AND the second part of the test return true will the isEnabled handler also return true and activate the script!

Macroscript_Body_Event_Handlers

on execute do
(

The execute handler contains the body of the script and is executed whenever the user clicks the button, selects the menu item or presses the keyboard shortcut the script is assigned to. This can only happen when isEnabed has returned true!

Macroscript_Body_Event_Handlers

mf_morpher_mod = $.morpher

First we store the Morpher modifier in the global variable defined at the beginning. We have made sure that there is only one selected object, so we use $ instead of selection\[1\]. Also, we don’t need error catching anymore as we already know there is a Morpher modifier in the selected object thanks to the isEnabled check...

used_channels = #()

We will need a user-defined array to store the indices of used channels in the Morpher.

Array Values

txt ="rollout mf_main \"Morpher Floater\" (\n"

Here we start a quite strange operation – we are going to build a whole new rollout dynamically by first putting all necessary definitions in a long string variable and the executing as if it were a regular script file. Note that we will need a backslash \ everywhere quotation marks appear inside of the string! Also, we will have to denote each new line using the \n sequence.

String Values

Rollout Clauses

for i = 1 to 100 do
(

A Morpher modifier can have up to 100 channels. Using a for loop, we will go through all of them. The variable i will count from 1 to 100 representing the current morph target channel.

For Loop

if WM3_MC_HasData mf_morpher_mod i then
(

In 3ds Max 5, a few functions for accessing morpher channels have been implemented. By calling the WM3_MC_HasData function and passing the morpher and the channel index to it, we can check whether a channel has morpher data or not. If it does, we go on inside the if statement’s context, if not, we just skip this channel...

If Expression

Morpher_Channel_Access

append used_channels i

We append the channel index stored in the for loop’s variable i to the array of used channels. When we are ready with the for loop, the array will contain all used channels!

Array Values

txt +="progressbar mf_slider_"+ i as string
txt +=" value:"+ (WM3_MC_GetValue mf_morpher_mod i) as string
txt +=" width:150 height:18 across:2 align:#left\n"

Now we describe the creation of a new progressbar UI element inside the rollout string. Note that we ADD the new partial strings to the current string using +=. We append the channel number as string to get unique names for all UI elements. We set the current value of the progressbar to the current value of the Morpher modifier’s channel. Using the across:2 keyword, we tell the UI to place two UI elements in the same row (we will add a text label with the name of the morph target later)

ProgressBar

txt +="on mf_slider_"+i as string+" clicked val do (\n"
txt +="WM3_MC_SetValue mf_morpher_mod "
txt += i as string+" (val as float) \n"
txt +="SliderTime +=0)\n"

A progressbar UI element is normally used to display the progress of processes, but it can also be used in place of a slider because it provides a change handler that can return the value changed with the mouse. The on ... clicked val do... handler is executed each time the user clicks with the mouse on the progressbar. The variable var will contain the current value between 0 and 100. We will assign it as a floating point number between 0.0 and 100.0 to the respective channel with index i .

SliderTime += 0 is a small trick to fool the time callback we will define a bit later. It does not actually change the slider time as we add zero, but it causes the system to evaluate the change and execute the time callback.

Time Control

txt +="edittext mf_label_"+i as string
txt +=" align:#right text:\""+i as string+": "
txt +=(WM3_MC_GetName mf_morpher_mod i) +"\"\n"

We also describe the creation of an edit text UI element. Its text will contain the channel index as string and the name of the i-th morpher channel. This label will appear in the same line as the progressbar and will be aligned to the right.

Edittext

)--end if
)--end i loop
txt +=")\n"

At this point, the i loop is over. It has added controls for all non-empty morpher channels to the string describing the UI. We just have to add the rollout’s closing bracket.

createDialog (execute txt) 340 (used_channels.count*24)

Now we create a dialog using the string we just put together as the source of the new rollout. The execute function evaluates the string just like any external source code file. The result of the evaluation is a rollout as you would type in using a Script Editor. The dialog will be 340 pixels wide and 24 times the total number of collected channels / number of controls in the rollout. This means that the UI will change its height if there are more or less morpher channels in the modifier every time the script is started again.

CreateDialog

String Values

txt ="fn mf_update_slider = (\n"

Once again, we start a new string defining the function to be called each time the morpher has changed.

Defining Custom Functions

for i in used_channels do
(

We will need to update every single slider corresponding to a used morpher channel. The variable i will contain the index of the respective channel in each loop repetition.

For Loop

txt +="mf_main.mf_slider_"+i as string
txt +=".value = WM3_MC_GetValue mf_morpher_mod "+i as string+" \n"

Here we tell the progressbar of channel i in the globally defined rollout mf_main to change its value to the value of the i-th channel in the Morpher modifier stored in the global variable mf_morpher_mod .

Morpher_Channel_Access

)--end i loop
txt +=")\n"

At the end, we close the function’s bracket.

global mf_update_slider = execute txt

Then we execute the function’s definition text which gives us an evaluated function we can use in our callbacks. We store it in a global variable to make it accessible to all the callbacks we intend to define...

Scope of Variables

String Values

registertimecallback mf_update_slider

The first callback reacts to time changes. Every time the scene time changes (by moving the time slider with the mouse, playing the animation etc.), the time callback will call the supplied function, in our case the UI update function we just defined! Remember the SliderTime += 0 part some paragraphs higher? It causes this callback to be called, too.

The result of this callback is that if you animate the morpher channel values and then play the animation, the progress bars will update dynamically in realtime WHILE the animation is playing!

Time Change Callback Mechanism

deleteAllChangeHandlers id:#morpher_floater

Before registering our other callback, we first make sure any older callbacks registered by the same script have been removed from memory. Our callback has a unique ID called #morpher_floater (this name is user-defined and can be anything as long as it is unique). So we tell MAXScript to delete any existing callbacks with that ID.

Change Handlers and Callbacks

when parameters mf_morpher_mod changes \
HandleAt:#RedrawViews \
id:#morpher_floater do mf_update_slider()

And finally, we register a parameter change callback. The when construct defines a callback which is executed whenever a monitored property type of a specified set of objects (in our case any parameter of the Morpher modifier) has changed its value.

We tell the callback to update only when the viewports are being redrawn. This reduces the actual number of internal calls and speeds the script up without any noticeable drawbacks for the user.

We provide out unique ID to be able to delete the callback later.

The only thing the callback will do is call the update function. The effect is that when you change the value of a morpher channel, the floating UI we created will automatically reflect the changes!

Change Handlers and Callbacks

)--end execute
)--end script

Using the Script

Evaluate the script. To use it, you can use Customize... to drag the script from the category "HowTo" to a toolbar, a menu, a quad menu or to assign to a keyboard shortcut.

Select a single object with a Morpher modifier and some active channels in the scene and execute the script. A new floating dialog should appear showing a progress bar and a text field displaying the channel number and the target name.

Where to go from here

This is a very simplified example. You could go on and expand the floater by adding things like additional spinners, buttons for selecting the single target objects in the scene (as long as they exist) etc. You could also change the progress bar elements and use regular sliders instead.

The above script will not detect changes in the used morpher channels (adding/deleting morph targets). Currently, the script would have to be restarted to reflect the changes. You could try to add handling for these cases, too.

Back to

"How To" Tutorials Index Page