Max - loading skin weights

I’d posted this over a year ago on the autodesk forums, but never got an answer.

I’ve got a script that dumps out skin weights and then reloads them. The loading is the issue - if I run the loadEnvelope command by stepping through the script, it works. If I run it in a loop, it fails UNLESS I run the command twice in sequence.

So this fails:
skinOps.loadEnvelope obj.modifiers[#Skin] skobj

But this works:
skinOps.loadEnvelope obj.modifiers[#Skin] skobj
skinOps.loadEnvelope obj.modifiers[#Skin] skobj

I’ve tried adding sleep before and after the command, but no joy.

The script works with the double execution, but I’d rather not do this since it seems…dirty. Has anyone else done this?

Incidentally there is no way to bypass the load skin weights dialogue, but I found a usefull callback written by by BoBo for Anoon (which I’ve broken, so I’ve removed from the post. Will updates when I get that to work.)

I seem to remember running into the same problem a few years ago and eventually having to solve it with a deftly placed completeRedraw() either directly before or directly after skinOps.loadEnvelope, which is still as ugly as hell but not as bad as running it twice.

Thanks, that works a treat!

I placed it just before the load.

The callback was case sensitive!

The modified function from Bobo:

-- use a callback to press the buttons
-- I found this here: http://forums.cgsociety.org/showthread.php?t=485300
fn LSW_EnvelopeCallbackFunction =
(
	WindowHandle = DialogMonitorOPS.GetWindowHandle()
	theDialogName = UIAccessor.GetWindowText WindowHandle

	if theDialogName != undefined and matchpattern theDialogName pattern:"*Load Envelopes*" do
		UIAccessor.PressButtonByName WindowHandle "Match by Name"	
		
	if theDialogName != undefined and matchpattern theDialogName pattern:"*Load Envelopes*" do
		UIAccessor.PressButtonByName WindowHandle "OK"

	true
)


The looping skinweight loader (with some cleanup)

		-- Load the skin weights back on.
		-- Need to use completeRedraw() or the weights wont load when in a loop. Madness.
		-- Fix was from here: http://tech-artists.org/forum/showthread.php?p=4034
		-- Use a callback to press the buttons
		-- I found this here: http://forums.cgsociety.org/showthread.php?t=485300
		DialogMonitorOPS.RegisterNotification LSW_EnvelopeCallbackFunction ID:#ANoon_Envelopes
		DialogMonitorOPS.Enabled = true
		
		completeRedraw()
		skinOps.loadEnvelope obj.modifiers[#Skin] skobj
		
		DialogMonitorOPS.Enabled = false
		DialogMonitorOPS.UnRegisterNotification ID:#ANoon_Envelopes

Here’s a better version I wrote some time ago, posted at CGTalk:
[source=“mxs”]fn setUICheckboxState hwnd state =
(
/*******************************************************************************
<DOC> toggle an UI checkbox’s checked state via UI messages/notifications
Arguments:
<int> hwnd: HWND of the control
<int> state: checked state. 1 = check, 0 = uncheck
<skin modifier> curObj: The selected skin modifier (must be in modify panel)
Return:
<ok> should check/uncheck the checkbox and have its change effected
*******************************************************************************/
local BN_CLICKED = 0 – clicky message ID
local BM_SETCHECK = 241 – checkbutton toggle message ID
local WM_COMMAND = 273 – windows command message

local parent = UIAccessor.getParentWindow hwnd
local id = UIAccessor.getWindowResourceID hwnd
windows.sendMessage hwnd BM_SETCHECK state 0
windows.sendMessage parent WM_COMMAND ((bit.shift BN_CLICKED 16) + id) hwnd
OK

),

fn confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:0 =
(
/*******************************************************************************
<DOC> Manipulate the Load Envelopes dialog via the UI Accessor.
Arguments:
<int> removeIncomingPrefix: Corresponds to the “Remove Incoming Prefix” checkbox. 0 is false, 1 is true
<int> removeCurrentPrefix: Corresponds to the “Remove Current Prefix” checkbox. 0 is false, 1 is true
Return:
<bool> true (needed for DialogMonitorOps)
*******************************************************************************/
–local BM_SETCHECK = 241
local hwnd = dialogMonitorOps.getWindowHandle()
if (uiAccessor.getWindowText hwnd == “Load Envelopes”) then
(
local children = windows.getChildrenHWND hwnd
for child in children do
(
if (child[5] == “Remove Incoming Prefix”) then
(
setUICheckboxState child[1] 1
)
else if (child[5] == “Remove Current Prefix”) then
(
setUICheckboxState child[1] 1
)
)

	UIAccessor.PressButtonByName hwnd "Match by Name"
	forceCompleteRedraw()
	UIAccessor.PressButtonByName hwnd "OK"
	--UIAccessor.PressDefaultButton()
)
true

),

fn confTT = (confirmLoadEnvelopes removeIncomingPrefix:1 removeCurrentPrefix:1),
fn confTF = (confirmLoadEnvelopes removeIncomingPrefix:1 removeCurrentPrefix:0),
fn confFT = (confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:1),
fn confFF = (confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:0),

fn loadEnvelope theSkin envFile removeIncomingPrefix:false removeCurrentPrefix:false =
(
/*******************************************************************************
<DOC> Load an .env file. There is no function for silently loading an .env, so this
is a UI Accessor workaround.
Arguments:
<skin modifier> theSkin: The selected skin modifier (must be in modify panel)
<string> envFile: Filename of the .env file.
Return:
<ok>
*******************************************************************************/
–determine which confirmLoadEnvelopes to use
local confirmFn = case of
(
(removeIncomingPrefix and removeCurrentPrefix):confTT
(removeIncomingPrefix and not removeCurrentPrefix):confTF
(not removeIncomingPrefix and removeCurrentPrefix):confFT
(not removeIncomingPrefix and not removeCurrentPrefix):confFF
)

DialogMonitorOps.Enabled = true	--DialogMonitorOps.Enabled = false
DialogMonitorOps.RegisterNotification confirmFn id:#pressSkinOK
skinOps.LoadEnvelope theSkin envFile
DialogMonitorOps.unRegisterNotification id:#pressSkinOK
DialogMonitorOps.Enabled = false
ok

),[/source]

That’s the code for anyone who needs it. So you’d put that in a struct, and call:

<struct>.loadEnvelope <skin modifier> <envelope filename> removeIncomingPrefix:true removeCurrentPrefix:true

Here’s a better version I wrote some time ago, posted at CGTalk:

fn setUICheckboxState hwnd state =
(
/*******************************************************************************
	<DOC> toggle an UI checkbox's checked state via UI messages/notifications
	Arguments:
		<int> hwnd:			HWND of the control
		<int> state: 		checked state.  1 = check, 0 = uncheck
		<skin modifier> curObj:		The selected skin modifier (must be in modify panel)
	Return:
		<ok>  should check/uncheck the checkbox and have its change effected
*******************************************************************************/
	local BN_CLICKED = 0 -- clicky message ID
	local BM_SETCHECK = 241 -- checkbutton toggle message ID
	local WM_COMMAND = 273 -- windows command message

	local parent = UIAccessor.getParentWindow hwnd
	local id = UIAccessor.getWindowResourceID hwnd
	windows.sendMessage hwnd BM_SETCHECK state 0
	windows.sendMessage parent WM_COMMAND ((bit.shift BN_CLICKED 16) + id) hwnd
	OK
),


fn confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:0 =
(
/*******************************************************************************
	<DOC> Manipulate the Load Envelopes dialog via the UI Accessor.
	Arguments:
		<int> removeIncomingPrefix:		Corresponds to the "Remove Incoming Prefix" checkbox.  0 is false, 1 is true
		<int> removeCurrentPrefix:		Corresponds to the "Remove Current Prefix" checkbox.  0 is false, 1 is true
	Return:
		<bool> true (needed for DialogMonitorOps)
*******************************************************************************/
	--local BM_SETCHECK = 241
	local hwnd = dialogMonitorOps.getWindowHandle()
	if (uiAccessor.getWindowText hwnd == "Load Envelopes") then
	(
		local children = windows.getChildrenHWND hwnd
		for child in children do
		(
			if (child[5] == "Remove Incoming Prefix") then
			(
				setUICheckboxState child[1] 1
			)
			else if (child[5] == "Remove Current Prefix") then
			(
				setUICheckboxState child[1] 1
			)
		)
		
		UIAccessor.PressButtonByName hwnd "Match by Name"
		forceCompleteRedraw()
		UIAccessor.PressButtonByName hwnd "OK"
		--UIAccessor.PressDefaultButton()
	)
	true
),

fn confTT = (confirmLoadEnvelopes removeIncomingPrefix:1 removeCurrentPrefix:1),
fn confTF = (confirmLoadEnvelopes removeIncomingPrefix:1 removeCurrentPrefix:0),
fn confFT = (confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:1),
fn confFF = (confirmLoadEnvelopes removeIncomingPrefix:0 removeCurrentPrefix:0),

fn loadEnvelope theSkin envFile removeIncomingPrefix:false removeCurrentPrefix:false =
(
/*******************************************************************************
	<DOC> Load an .env file.  There is no function for silently loading an .env, so this
	is a UI Accessor workaround.
	Arguments:
		<skin modifier> theSkin:		The selected skin modifier (must be in modify panel)
		<string>	envFile:					Filename of the .env file.
	Return:
		<ok>
*******************************************************************************/
	--determine which confirmLoadEnvelopes to use
	local confirmFn = case of
	(
		(removeIncomingPrefix and removeCurrentPrefix):confTT
		(removeIncomingPrefix and not removeCurrentPrefix):confTF
		(not removeIncomingPrefix and removeCurrentPrefix):confFT
		(not removeIncomingPrefix and not removeCurrentPrefix):confFF
	)
	
	DialogMonitorOps.Enabled = true	--DialogMonitorOps.Enabled = false
	DialogMonitorOps.RegisterNotification confirmFn id:#pressSkinOK
	skinOps.LoadEnvelope theSkin envFile
	DialogMonitorOps.unRegisterNotification id:#pressSkinOK
	DialogMonitorOps.Enabled = false
	ok
),

That’s the code for anyone who needs it. So you’d put that in a struct, and call:

<struct>.loadEnvelope <skin modifier> <envelope filename> removeIncomingPrefix:true removeCurrentPrefix:true

Cheers Rob, invaluable stuff.

It seems odd to me that we have to use two workarounds - forcing a redraw AND forcing a silent mode. Has anyone looked in the SDK to see if it’s has a hook to bypass the callback button pressing?

I have worked around this same problem too. Great stuff here - mine was based on that same cgtalk thread IIRC.

Since then I’ve actually changed over to use the skinutils for this stuff as it fits our particular needs a bit better. You can dump/load weights to meshes and save those max scenes out instead of .env files. We’ve got a lod auto-weighter and general save/load envelopes scripts so the guys can failr smoothly store and reuse weighting on similar meshes on similar rigs. It’s pretty nifty even if hacked together. :slight_smile:

I’ll add to this thread as I’ve trying to automate another useful skinning feature and it seemed like a good home.

In this thread (http://tech-artists.org/forum/showthread.php?t=522) I describe a manual way of using the skin modifer to bake a pose as a new base pose, and I’m trying to automate it.

Essentially:

  1. Copy the skin modifier
  2. Paste the skin modifier
  3. Collapse the original modifier down to the mesh level
  4. Set the copied skin to be the new skin modifier.

It’s a fairly simple task, but the copy and paste is giving me some aggravation.

If I use:

	
SkinModifierHolder =  $.modifiers[1] 
Addmodifier $ SkinModifierHolder

…I get an instance of the skin modifer with all the weights copied, but when I make that unique I lose the skin weights in the copy.

If I use:

	
SkinModifierHolder =  copy $.modifiers[1] 
Addmodifier $ SkinModifierHolder

…then I get a unique copy of the skin modifier with all the bones added, the correct bone affect limit set etc, but none of the actual skinning information is carried over.

I could of course bake, save and load the skin weights, but that seems a little bit convulted - especially since the entire process works manually via the right click copy and paste in the modifier stack pane.

My other solution is to copy the skinweights to an array, then apply that to the copied modifier.

Any thoughts on this?


	fn resetbindpose _obj=
	(
		theskin = _obj.modifiers[#skin]
		format "%
" theskin_idx
		if theskin != undefined then
		(
			--setup
			if getCommandPanelTaskMode() != #modify then max modify mode
			select _obj
			modPanel.setCurrentObject theskin
			
			--store skin weights
			skinUtils.ExtractSkinData _obj
			--copy skin modifier
			newskin = copy theskin
			--add copy to stack
			modPanel.addModToSelection newskin
			--find orig skin
			theskin_idx = modPanel.getModifierIndex _obj theskin
			--turn off copyskin
			newskin.enabled = false

			--collapse to, to set mesh verts to new position
			maxOps.CollapseNodeTo _obj theskin_idx true
			
			--reload stored weights to make sure skinning quality is not lost
			theskindata = getnodebyname ("SkinData_"+_obj.name)
			
			select #(_obj,theskindata)
			
			skinUtils.ImportSkinDataNoDialog true false false false false 1.0 0
			newskin.always_deform = false
			newskin.enabled=true
			newskin.always_deform = true
		)
	)

I just needed this myself so I made it. Seems to work for my needs anyhow. :slight_smile: hope it is useful!

4 years too late to this party. Rob, your script is brilliant, thank you.

Not sure what is the cause, but it seems DialogMonitorOps doesn’t work anymore.
tested on:
Windows 7 - 3dsmax 2014
Windows 8.1 - 3dsmax 2014 / 3dsmax 2011

Anybody experiencing the same?