The Challenge >>> Biped Figure Mode


#1

Yes, I know that this site is dying as a professional resource. But according to the old tradition, I want to offer one more challenge.

Imagine I have a CheckButton “Biped In Figure Mode” in the MXS dialog (simple Rollout).

This button should always be ON if any Biped system is in figure mode, and OFF otherwise.

Obviously, we need to track events … but which ones? what is the safest way to keep track of all related events and callbacks?

How do we set up a minimal but sufficient kind of callbacks to track this task?

try(destroydialog FigureModeCheck) catch()
rollout FigureModeCheck "Figure Mode Check" width:191
(
	checkbutton figure_mode_cb "Figure Mode" width:180 align:#center	

	on FigureModeCheck open do
	(
		b = for obj in objects where iskindof obj.controller Vertical_Horizontal_Turn do exit with obj
		if isvalidnode b do figure_mode_cb.state = b.controller.figureMode 
	)
)
createdialog FigureModeCheck

this is a good test to check how well you know the event mechanism in MAX :wink:


#2

Not very elegant, but it kinda works

try(destroydialog FigureModeCheck) catch()
rollout FigureModeCheck "Figure Mode Check" width:191
(
	checkbutton figure_mode_cb "Figure Mode" width:180 align:#center
	
	local bipeds = #()
	
	fn UpdateCheckbox =
	(		
		local state = false
		bipeds = for n in bipeds where isValidNode n collect ( state = state or n.controller.figureMode; n )
		if state != figure_mode_cb.checked do figure_mode_cb.checked = state
	)
	
	fn OnEvent ev nodes =
	(
		local complete = false
		for handle in nodes while not complete where isKindOf (getAnimByHandle handle) Biped_Object do ( complete = true; UpdateCheckbox() )
	)
	
	fn OnAdded ev nodes =
	(
		local found = false
		
		for n in nodes while not found do
		(
			local new_node = getAnimByHandle n
			
			if isKindOf new_node Biped_Object do
			(
				found = true
				for n in getClassInstances Vertical_Horizontal_Turn do appendIfUnique bipeds n.rootnode
				UpdateCheckbox()
			)				
		)		
	)
	
	fn OnDeleted ev nodes =
	(
		local complete = false
		for n in bipeds while not complete where not isValidNode n do ( complete = true; UpdateCheckbox() )		
	)
	
	local handler = NodeEventCallback enabled:true controllerOtherEvent:OnEvent	added:OnAdded deleted:OnDeleted
	
	on FigureModeCheck open do
	(
		for n in getClassInstances Vertical_Horizontal_Turn do appendIfUnique bipeds n.rootnode

		UpdateCheckbox()
	)
	
	on FigureModeCheck close do
	(
		handler = undefined; gc light:true
	)
)
createdialog FigureModeCheck

#3

Not too bad, neither too good. There must be a way to minimize the calls even more.

try(destroyDialog ::RO_FIGUREMODECHECK) catch()
rollout RO_FIGUREMODECHECK "Figure Mode Check" width:200
(
	checkbutton figure_mode_cb "Figure Mode" width:180 height:32
	
	local calls = 0
	local instances = #()
	local nodesCallback = undefined
	
	fn UpdateUI =
	(
		mode = false
		for ctrl in instances do mode = mode or ctrl.figureMode
		figure_mode_cb.state = mode
		format "mode:%\tcalls:%\n" mode (calls+=1)
	)
	
	fn UpdateInstances event node =
	(
		deleteAllChangeHandlers id:#ID_0X712292CE
		instances = getClassInstances Vertical_Horizontal_Turn
		
		when parameters instances changes id:#ID_0X712292CE handleAt:#redrawViews do UpdateUI()
		UpdateUI()
	)
	
	on RO_FIGUREMODECHECK open do
	(
		UpdateInstances 0 0
		nodesCallback = NodeEventCallback added:UpdateInstances deleted:UpdateInstances
	)
	
	on RO_FIGUREMODECHECK close do
	(
		deleteAllChangeHandlers id:#ID_0X712292CE
		nodesCallback = undefined
		gc light:on
	)
)
createDialog ::RO_FIGUREMODECHECK

#4

damn :slight_smile:
So much less code and it is way simpler. I didn’t know the fact that you can use when construct with collections. :+1:


#5

we’re on the same track … use NodeEventCallback to track scene changes and when context to track Figure mode change.

now let’s get down to optimization.

getClassInstances is slow method. To find a specified class instances we enumerate ALL animatables. It’s faster to find Bip roots than all Vertical_Horizontal_Turn controllers


fn getBipBodies  = (for obj in geometry where iskindof (c = obj.controller) Vertical_Horizontal_Turn collect c)

(
	t0 = timestamp()
	h0 = heapfree

	for k=1 to 1000 do
	(
--getClassInstances Vertical_Horizontal_Turn
--getbipbodies()
	)

	format "time:% heap:%\n" (timestamp() - t0) (h0 - heapfree)
)

#6

if we call getClassInstances just once it always wins, even in situations when there’re thousands of nodes present in a scene. I don’t have a real big project to test if it is still a true in general.

Timings
calls | nodes | getBipBodies | getClassInstances
  1 |     1 |      0 |      1
  3 |     1 |      0 |      0
  5 |     1 |      0 |      1
 10 |     1 |      0 |      2

  1 |    10 |      1 |      0
  3 |    10 |      0 |      1
  5 |    10 |      0 |      1
 10 |    10 |      0 |      3

  1 |   100 |      2 |      1
  3 |   100 |      1 |      2
  5 |   100 |      2 |      4
 10 |   100 |      2 |      8

  1 |  1000 |     21 |      8
  3 |  1000 |     11 |     23
  5 |  1000 |     18 |     38
 10 |  1000 |     33 |     75

  1 |  5000 |    103 |     38
  3 |  5000 |     60 |    113
  5 |  5000 |     99 |    191
 10 |  5000 |    197 |    379

  1 |  6000 |    124 |     46
  3 |  6000 |     71 |    139
  5 |  6000 |    119 |    230
 10 |  6000 |    236 |    458

  1 |  7000 |    149 |     55
  3 |  7000 |     83 |    163
  5 |  7000 |    138 |    275
 10 |  7000 |    278 |    544

  1 |  8000 |    165 |     64
  3 |  8000 |     94 |    186
  5 |  8000 |    156 |    307
 10 |  8000 |    313 |    615

  1 |  9000 |    188 |     71
  3 |  9000 |    105 |    213
  5 |  9000 |    178 |    354
 10 |  9000 |    357 |    703

  1 | 10000 |    213 |     81
  3 | 10000 |    119 |    237
  5 | 10000 |    198 |    385
 10 | 10000 |    388 |    778
(
	fn getBipBodies  = (for obj in geometry where iskindof (c = obj.controller) Vertical_Horizontal_Turn collect c)
	format "calls | nodes | getBipBodies | getClassInstances\n" 
	for i in #( 1, 10, 100, 1000, 5000, 6000, 7000, 8000, 9000, 10000 ) do
	(		
		with undo off
		( 
			delete objects; gc() 
			for j = 1 to i do Teapot()
		)

		for max_iterations in #( 1, 3, 5, 10 ) do
		(
			result = #(max_iterations,i,0,0)
			(
				t0 = timestamp()

				for k = 1 to max_iterations do
				(
					getbipbodies()
				)

				result[3] = timestamp() - t0
			)

			(
				t0 = timestamp()

				for k = 1 to max_iterations do
				(
					getClassInstances Vertical_Horizontal_Turn
				)

				result[4] = timestamp() - t0
			)

			format "%\n" ((dotNetClass "system.string").format "{0,3} | {1,5} | {2,6} | {3,6}" (dotNet.ValueToDotNetObject result (dotnetclass "system.object[]")))
		)
		format "\n" 
	)
	
	
)

#7

check this snippet:

delete objects 
for k=1 to 1000 do point()

fn getpoints = (for obj in objects where iskindof obj point collect obj)

(
	t0 = timestamp()
	h0 = heapfree

	for k=1 to 1000 do
	(
		getclassinstances point
		--getpoints()
	)

	format "time:% heap:%\n" (timestamp() - t0) (h0 - heapfree)
)

sometimes the number of nodes in a scene is not very important. more dramatic is a number of cross-dependencies (it includes controllers, attributes, modifiers, materials, … all reference targets (anims))


#8

For a scene with 13000 bipeds, 10000 additional objects and 10000 materials I get these results:

fn getBipBodies = (for obj in geometry where iskindof (c = obj.controller) Vertical_Horizontal_Turn collect c)

time:169 heap:6980544L

getClassInstances Vertical_Horizontal_Turn processAllAnimatables:true

time:56 heap:33592L


#9

Hmm…

But what numbers do you have with my snippet above?


#10
getclassinstances point

time:3205 heap:232120L

getpoints()

time:745 heap:96120L

But, for this example (which is more “realistic”), I get these numbers:

I mean more “realisctic” because in my code (post 3), getclassinstances() is called when an object is created/deleted only, so it won’t be called too frequently, not 1000 times in less than a second for sure.

(
	with undo off
	(
		delete objects 
		for k=1 to 10000 do point()
	)
	
	gc()

	fn getpoints = (for obj in objects where iskindof obj point collect obj)
	
	t0 = timestamp()
	h0 = heapfree
	
	--getclassinstances point processAllAnimatables:true
	--getpoints()
	
	format "time:% heap:%\n" (timestamp() - t0) (h0 - heapfree)
)
getclassinstances point processAllAnimatables:true

time:50 heap:1368672L

getpoints()

time:49 heap:1368536L


#11
fn getbodycontrollers = (for obj in geometry where iskindof (c = obj.controller) Vertical_Horizontal_Turn collect c)  

(
	t0 = timestamp()
	h0 = heapfree

	for k=1 to 1 do
	(
		--getclassinstances Vertical_Horizontal_Turn processAllAnimatables:on
		getbodycontrollers()
	)

	format "time:% heap:%\n" (timestamp() - t0) (h0 - heapfree)
)

for 1000 biped default skeletons:

getclassinstances >> time:121 heap:192L
getbodycontrollers >> time:64 heap:192L

i just know the code behind getclassinstances … it cann’t be fast with that algorithm. I have my own mxs extension method that searches only TM controllers. it gives me:

getbodycontrols >> time:7 heap:192L


#12

Well, the numbers here are quite the contrary, might be due to my quantum processor.:smoking:

getclassinstances >> time:72 heap:171448L
getbodycontrollers >> time:264 heap:10725664L


#13

Oh! Why are these ‘heap’ numbers on your machine? It should not leak at all! All memory usage is about creating one mxs array…

BTW, what MAX vertion is it?


#14

2014


#15

Ha! I thought it was something new, but it’s very old. My “target” version is now 2016 :slight_smile:


#16

Max 2014

getclassinstances >> time:72 heap:171448L
getbodycontrollers >> time:264 heap:10725664L


Max 2016

getclassinstances >> time:59 heap:149264L
getbodycontrollers >> time:213 heap:8934544L


Max 2021

getclassinstances >> time:60 heap:156324L
getbodycontrollers >> time:303 heap:10168484L


#17

You are excluded from the list of testers! :point_up::sunglasses:


#18

In fact, this is what I ended up with after a series of experiments - monitor the scene with a NodeEventCallback and catch changes (notifications) of a reference target(s) (in our case this is the Body controller) with ‘when construct’ for isolated targets.

Now the next step… Can we make some sort of universal ‘monitor’ for this kind of tasks?


#19

Forget about being a little faster … let’s assume getclassinstances works well enough


#20

I guess that, with a “clear goal” and a bit of patience, it could be done, at least for some properties/classes.

Here is a little attempt that needs A LOT more work to be used in production:

try(destroyDialog ::RO_PROPERTY_MONITOR) catch()
rollout RO_PROPERTY_MONITOR "Property Monitor" width:222 height:100
(
	edittext    ed_class    "Class:"         pos:[ 25, 8] fieldwidth:156 text:"Vertical_Horizontal_Turn"
	edittext    ed_property "Property:"      pos:[  8,32] fieldwidth:156 text:"figureMode"
	checkbutton chk_state   "STATE"          pos:[  8,60] width:48 height:32 enabled:false
	checkbutton chk_monitor "Enable Monitor" pos:[ 58,60] width:156 height:32
	
	local instances     = #()
	local nodesCallback = undefined
	local theClass      = undefined
	local theProperty   = undefined
	
	fn UpdateUI =
	(
		state = false
		for ctrl in instances do state = state or (getProperty ctrl theProperty)
		chk_state.state = state
	)
	
	fn UpdateInstances event node =
	(
		deleteAllChangeHandlers id:#ID_0X712292CE
		instances = getClassInstances theClass
		
		when parameters instances changes id:#ID_0X712292CE handleAt:#redrawViews do UpdateUI()
		UpdateUI()
	)
	
	fn EnableMonitor state:false =
	(
		theClass    = execute ed_class.text		-- Needs to be validated (NOT IMPLEMENTED)
		theProperty = ed_property.text			-- Needs to be validated (NOT IMPLEMENTED) (only boolean in this example)
		
		ed_class.enabled = ed_property.enabled = not state
		
		if state then
		(
			UpdateInstances 0 0
			nodesCallback = NodeEventCallback added:UpdateInstances deleted:UpdateInstances
		)else(
			deleteAllChangeHandlers id:#ID_0X712292CE
			nodesCallback = undefined
			gc light:on
		)
	)
	
	on chk_monitor changed arg do EnableMonitor state:arg
	
	on RO_PROPERTY_MONITOR close do EnableMonitor state:false
)
createDialog ::RO_PROPERTY_MONITOR