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:
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
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.
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.
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.
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.
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.
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...
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!
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)
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.
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.
)--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.
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.
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.
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
.
)--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...
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.
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!
)--end execute
)--end 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.
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