Tuesday, January 29, 2013

Building a Scripted Plugin Modifier for Game Characters

In response to  +David Moulder I have detailed a little more of our character "referencing" and scene building process, starting with our custom character modifier. I hope this is helpful.

All of our characters and animated objects have a custom scripted plugin modifier that sits on top of the character mesh modifier stack. It stores references to the skeleton and helper objects in the parameter blocks of the modifier. To store these object references you will see below the parameter type should probably be a #maxObjectTab type.  If you are looking for something similar to the message attributes in Maya the #maxObject parameter type, or what 3dsmax calls weak referencing, is as close as you are going to get. Paul Neale has a pretty good tutorial for weak referencing.

Weak References

I went ahead and wrote up some functional code below to help provide a basic understanding of scripted plugin modifiers and weak referencing. If you evaluate this code a new modifier will appear in the modifier list named "AnimExportTest".  It does not modify your mesh/object it is only a container for storing references and variables relative to your character.
plugin Modifier AnimExportTest
name:"AnimExportTest"
extends:EmptyModifier
 
--- The id here must be unique run genclassid() to generate a unique id for your plugin
classId:#(0x2c608fe8, 0x14804a99)
 
version:1
(
 -- set the file path parameter
 fn set_path &path =
 (
  local file_path = getSaveFilename filename:"C:" types:"Meshx(*.meshx)|*.meshx|ALL|*.*" 
  if file_path != undefined do (
   path = file_path
  )
 )
  
 -- add object weak references to the parameters
 fn add_bones objs =
 (
  for obj in objs do (
   -- this is how you create a weak reference to an object/node
   add_obj = (nodeTransformMonitor node:obj forwardTransformChangeMsgs:false)
   append this.p_bones add_obj
  )
 )
  
 -- object selection filter
 -- boolean check to see if the incoming object has what you are looking for
 fn bone_filter obj =
 (
  is_valid = True

  -- make sure the object is our type of bone
  if ( not superclassof obj == helper ) do ( 
   is_valid = False 
  )

  -- make sure object name has namespace identifier ":"
  if ( not matchpattern obj.name pattern:"*:*") do ( 
   is_valid = False 
  )

  -- make sure the object or object name doesnt already exist in our list
  for bone_obj in this.p_bones do (
   -- since this is a weak reference get the .node to query the actual object
   if ( bone_obj.node == obj ) do ( 
    is_valid = False
    exit    
   )
  )

  -- return bool
  is_valid
 )
  
 --- Store any variable,object references, export paths, values necessary to this modifier
 Parameters export_params
 (
  p_export_mesh type:#string   default:""
  p_export_rig type:#string   default:""

  p_bones   type:#maxObjectTab  tabSize:0  tabSizeVariable:true
  p_bone_names type:#stringTab     tabSize:0  tabSizeVariable:true
 )
 
 -- rollout ui creation
 rollout ro_export "Export Paths"
 (
  Group "Export Paths" (
   Button bn_export_mesh "Export Mesh" width:100
   Button bn_export_rig  "Export Rig"  width:100
  )

  --- Functions to set paths
  On bn_export_mesh pressed do (
   set_path &this.p_export_mesh
  ) 

  --- Functions to set paths
  On bn_export_rig pressed do (
   set_path &this.p_export_rig
  ) 

 )
  
 -- export objects menu
 rollout ro_export_bones "Export Objects"
 (
  Group "Export Bones" (
   MultiListBox lbox_bones "" width:140 height:20
   Button bn_add_bones "+" across:2
   Button bn_remove_bones "-"
  )  
   
  -- Update the rollout interface
  fn update_ui =
  (
   local bone_names = #()
   for obj in this.p_bones do (
    -- since this is a weak reference get the .node to query the actual object name
    append bone_names obj.node.name
   )

   lbox_bones.items = bone_names
  )
    
  --- Functions to add and remove bones
  on bn_add_bones pressed do (
   
   --- object picker dialog
   new_bones = selectByName filter:bone_filter
   if ( new_bones != undefined ) do (
    add_bones new_bones
   )
   
   -- update the interface
   update_ui()
  ) 
  
  -- Update the interface when the rollout is opened
  on ro_export_bones open do (
   update_ui()
  )
 )  
)

Custom Test Modifier
Custom Character Modifier
by  +Nathaniel Albright 
Our version of this modifier has many more methods, attributes and options for storing animation and other references. This is more or less the core interface for evaluating incoming animatable assets to make sure they are properly setup for animation and exporting when building an animation scene, as noted in my previous post.

You may chose to build a similar interface but rather than a modifier it can be a custom shape object that lives in your scene. It's pretty much the same thing just where it lives is different.


Custom Attributes & adding Assets
When adding an asset I will merge the entire scene and inspect this custom modifier to make sure it has everything I need for animation. If it passes the validation process I will collect a list of only the objects necessary for animation and delete the remaining unnecessary objects that came into the scene. Once I have identified the valid objects I will apply a "namespace" prefix to them based on the supplied asset name. I will then stamp additional attributes onto all of the valid objects to sort them and identify them as a unique asset within the scene. You can see my post on Tech-Artists.Org for more on custom attributes. Below is an example of setting the custom asset attributes on the incoming objects.

-- Asset Attributes
-- This attribute block should probably live in your library code
--- so that it is always accessible.
my_asset_data = attributes asset_data
attribid:#(0x6664296c, 0x5dec7a0)
version:1
(
 parameters asset_data
 (
  asset_name  type:#string  default:"none" 
      asset_id  type:#integer  default:0
      asset_type type:#string  default:"Character"
 )
)

-- Loop through all the incoming objects and apply the custom attribute

-- Apply attribute class to an object
add_attribute = custAttributes.add obj my_asset_data baseobject:True

-- update attribute data with your unique asset name, asset Id
-- and anything additional you need to track
obj.asset_name = obj.supplied_asset_name
obj.asset_id = unique_asset_id


A Bulk of the Work
Now that you have brought in a character asset into your scene in a clean and managed fashion the heavy lifting is done by your scene manager. Adding and Deleting should then become fairly trivial as your asset objects should be clearly identified. However, the swapping of your animated characters requires your tool to do much more in the way of saving animation, link constraints and export data defined in your scene. This of course relies heavily on your control rig system be it CAT, custom, or god forbid Biped +Brad Clark.  Once you have a proper save and load, swapping assets should become a "breeze". I will note that 80% of my Asset Manager code is handling the saving and loading of animation. You may find many cases where you have to do a lot of code to preserve the state of your animation in relation to other objects or characters in your scene.

Feel free to ask any additional questions. I will try to follow up.
Until next time.

UPDATE: Fixed some script errors in the plugin modifier. It will now refresh the ui when you reopen it.

1 comment:

  1. Good read...thanks for sharing...weak reference always comes handy... addition to that tab never shrink in size when actual max object get deleted. If use NodeTransformMonitor then that instance (which is stored in tab) will point to undefined / null node...it could lead to system exception (...as forwardTransformChangeMsgs is false... deletion of maxobject will create problem if not clean it properly). Increased tab size also can give performance trouble during looping when dealing with many objects...could validate and remove those deleted ones from tab during new addition. Also try to use more mapped fn where possible and avoid "exit".

    ReplyDelete