Here’s a small tip on where to quickly put files (ini or xml for example) that you can easily fetch from both Max:
import os os.getenv('LOCALAPPDATA')
Both will return “C:\Users\USER\AppData\Local”. And if you ever wondered what’s the difference between /AppData/Local and AppData/Roaming this might shed some light on it: http://superuser.com/questions/150012/what-is-the-difference-between-local-and-roaming-folders
Here’s a nice trick my mate at Funcom, Endre Eikrem, told me. You can scale everything, scene, mesh, skin, rig, animation, with this simple code:
units.SystemScale = 0.5 holdMaxFile() units.SystemScale = 1.0 fetchMaxFile useFileUnits:false quiet:true
You just need to have “Respect System Unit in Files” activated, inside Customize/Unit Setup/System Unit Setup. That’s it!
Except! There’s always some exception. So far we found a couple of stuff that are not scaled with this trick:
- Wire Parameters. They are strings, so it’s expected that Max doesn’t scale them.
- Key Tangents. If tangents were manually edited they are not going to be scaled. On the other hand, animation keys are scaled correctly.
- Float Limit Controller. It’s value is not scaled.
There might be other exceptions, but so far these are quite easy to workaround with Maxscript. Specially if your rig is simple. On Funcom rigs, we just needed to parse Orientation_Constraint controllers and edit their wire parameters to the FK/IK handle. Float_Limit controller is just a matter of finding it and setting a new value based on the units.SystemScale used. And key tangents are not to worry if you first bake/plot the animation on the handles. Keyframes being scaled, it works flawlessly.
If anyone trying this out find any more exceptions please let me know.
Paul Neale has a very good tutorial about dotnet and maxscript, specially reading and writing xml, but I decided to add some more information from a slightly different approach. First, let’s translate an XML to dotnetesque:
<rootElement> <element1 /> <element2 attributeName="attribute content"> <element2.1>innerXML of this element</element2.1> </element2> </element>
And the entire document is a XmlDocument (instance of dotNetObject “System.Xml.XmlDocument“)
There are many ways to read and write XML with dotnet. If you need to read the whole document first, probably the most optimized is by using XmlParser, but it can be a bit of a pain. If you are using Maxscript you are not doing it in realtime, so a couple miliseconds won’t make that much of a difference.
Anyway, if you are looking for specific information inside tags you can easily do it using .GetElementsByTagName:
XmlDoc = dotNetObject "System.Xml.XmlDocument" XmlDoc.load "D:\\Characters\\Rider\\exportEMFX.xml" -- I know <baseCharacter> has only one entry, so I get the first one tagBaseChar = XmlDoc.GetElementsByTagName "baseCharacter" baseCharacter = (tagBaseChar.item 0).InnerXML -- I don't know how many <file>s there is, so I get them into an array tagFiles = XmlDoc.GetElementsByTagName "file" fileList = (for i=0 to (tagFiles.count-1) collect (tagFiles.item i).InnerXML)
To read something like:
<mlExportEMFX> <baseCharacter>D:\\Characters\\Rider\\rider.max</baseCharacter> <filesToLoad> <file>D:\\Characters\\Rider\\FBX\\stopNeutral_L.fbx</file> <file>D:\\Characters\\Rider\\FBX\\stopNeutral_R.fbx</file> </filesToLoad> </mlExportEMFX>
Note that GetElementsByTagName doesn’t care about hierarchy, it just finds the element you are looking for, no matter where in the tree it is or how many times it’s repeated. If you have a “file” under “filesToLoad” and also under “filesToSave” it won’t differentiate between them.
If you have a tree/graph structure and you don’t know each tag name one solution is to recourse through it all loading them on a dictionary or array.
Say you have an hierarchy structure you want to save, together with their layers, into an XML. You can store the data in two arrays with:
/* this two are recursive functions to return the hierarchy in a single array */ fn GetHierarchyTree lParent = ( toRet = #(lParent.name) for o in lParent.children do append toRet (GetHierarchyTree o) toRet ) fn GetLayerTree lParent = ( toRet = #(lParent.layer.name) for o in lParent.children do append toRet (GetLayerTree o) toRet ) /* this is just an example, you shouldn't use globals like this */ global parentTree = #() global layerTree = #() /* gets the data into parentTree and layerTree */ fn getData = ( for o in objects where o.parent == undefined do ( append parentTree (GetHierarchyTree o) append layerTree (GetLayerTree o) ) )
To save this into an XML you also go with recursion:
fn recurseTreeToXML baseNode xmlDocNew pTree lTree = ( for i=1 to pTree.count do ( -- if it's a leaf, there's data, so we append if (isKindOf pTree[i] String) then ( curNode = xmlDocNew.CreateElement pTree[i] curNode.SetAttribute "layer" lTree[i] baseNode.appendChild curNode baseNode = curNode ) else -- it's a branch with more data underneath ( recurseTreeToXML baseNode xmlDocNew pTree[i] lTree[i] ) ) ) fn saveDataOnXML = ( -- you cannot append elements to an xmldoc -- you have to append elements to a single root element, child of xmldoc xmlDocNew = dotNetObject "System.Xml.XmlDocument" xmlRoot = xmlDocNew.CreateElement (getFilenameFile maxfilename) xmlDocNew.appendChild xmlRoot recurseTreeToXML xmlRoot xmlDocNew parentTree layerTree xmlDocNew.save ("C:\\temp\\test.xml") )
And also to read an XML:
fn recurseXML docElement = ( tempPTree = #(docElement.name) tempLayerTree = #(docElement.getAttribute "layer") for i = 0 to (docElement.childNodes.count - 1) do ( tmp = (recurseXML docElement.childNodes.itemOf[i]) append tempPTree tmp append tempLayerTree tmp ) return #(tempPTree, tempLayerTree) ) global parentTreeXML = #() global layerTreeXML = #() fn readXML = ( xmlDoc = dotNetObject "System.Xml.XmlDocument" xmlDoc.load ("C:\\temp\\test.xml") docElement = XmlDoc.documentElement -- if we don't do this here we end up with the root node on parentTreeXML -- which can be worked out in anoter way also, of course. for i = 0 to (docElement.childNodes.count - 1) do ( tmp = (recurseXML docElement.childNodes.itemOf[i]) append parentTreeXML tmp append layerTreeXML tmp ) )
Remember, besides dotnet help website, showproperties and showmethods are your best friends. You can also copy this code and paste on maxscript editor or jedit or somewhere with a syntax highlighter to better read it.
If you know where to find the information it becomes quite straight forward if you are not doing anything complex. With python you can use xml.dom.minidom, while with maxscript you can use either .NET Object “System.Xml.XmlDocument” or Class “System.Xml.XmlReader“.
With python you create a root element and append child elements from there. A very good sample can be found on: http://www.postneo.com/projects/pyxml/ To read you have getElementsByTagName on both python and dotnet XmlDocument. XmlReader might be a bit faster, but you have to parse elements by yourself.
There’s an issue with minidom’s toprettyprintxml, it adds whitespace and tabs between tags, around textNodes. And they are obviously read afterwards. A couple of different solutions are discussed on Ron Rothman’s blog, the easiest one using xml.dom.ext.PrettyPrint.
It’s quite easy. Basically you need to:
- plot to the skeleton;
- save one animation;
- turn on Mirror Animation for the character;
- plot back to the control rig, which then mirrors the animation;
- rotate the Character Reference model 180 degrees;
- plot back to the skeleton;
- save mirrored animation.
The tricky part is just number 5 where you need to do some matrix rotation. Python for this would be something like:
from pyfbsdk import * app = FBApplication() char = app.CurrentCharacter savePath = r"C:\" filename = app.FBXFileName skeleton = FBCharacterPlotWhere.kFBCharacterPlotOnSkeleton ctrlrig = FBCharacterPlotWhere.kFBCharacterPlotOnControlRig # plot to skeleton, see bellow plotAnim(char, skeleton) # save left animation sOptions = FBFbxOptions(False) # false = save options sOptions.SaveCharacter = True sOptions.SaveControlSet = False sOptions.SaveCharacterExtension = False sOptions.ShowFileDialog = False sOptions.ShowOptionsDialog = False app.SaveCharacterRigAndAnimation(savePath + "\\" + filename + "_L", char, sOptions) # activate mirror and plot char.MirrorMode = True plotAnim(char, ctrlrig) # get reference model refModel = FBFindModelByName("Character_Ctrl:Reference") # rotating 180, the tricky part # http://www.j3d.org/matrix_faq/matrfaq_latest.html#Q28 rotateY180 = FBMatrix() rotateY180 = math.cos((180*0.017453292519943295769236907684886)) rotateY180 = math.sin((180*0.017453292519943295769236907684886)) rotateY180 = -math.sin((180*0.017453292519943295769236907684886)) rotateY180 = math.cos((180*0.017453292519943295769236907684886)) refMT = FBMatrix() refModel.GetMatrix(refMT) refModel.SetMatrix( MatrixMult(rotateY180, refMT) ) scene.Evaluate() # plot back to skeleton plotAnim(char, skeleton) # save again app.SaveCharacterRigAndAnimation(savePath + "\\" + filename + "_R", char, sOptions)
The plot and multiplication functions are:
# This is from Neil3d: http://neill3d.com/mobi-skript-raschet-additivnoj-animacii?langswitch_lang=en def MatrixMult(Ma, Mb): res = FBMatrix() for i in range(0,4): for j in range(0,4): sum=0 for k in range(0,4): sum += Ma[i*4+k] * Mb[k*4+j] res[i*4+j] = sum return res def plotAnim(char, where): if char.GetCharacterize: switchOn = char.SetCharacterizeOn(True) plotoBla = FBPlotOptions() plotoBla.ConstantKeyReducerKeepOneKey = True plotoBla.PlotAllTakes = True plotoBla.PlotOnFrame = True plotoBla.PlotPeriod = FBTime( 0, 0, 0, 1 ) #plotoBla.PlotTranslationOnRootOnly = True plotoBla.PreciseTimeDiscontinuities = True #plotoBla.RotationFilterToApply = FBRotationFilter.kFBRotationFilterGimbleKiller plotoBla.UseConstantKeyReducer = False plotoBla.ConstantKeyReducerKeepOneKey = True if (not char.PlotAnimation(where, plotoBla)): FBMessageBox( "Something went wrong", "Plot animation returned false, cannot continue", "OK", None, None ) return False return char
If you are exporting the character to a game, pay attention to the root node rotation. If you used it in the characterization, chances are it might also be rotated, and you might not want that to happen. You can also establish a convention on the name of the files, and easily detect if the animation currently open ends with _L or _R, and adapt the filename correctly.
FBApplication().SaveCharacterRigAndAnimation() is the equivalent of the Save Character Animation on the Character Controls window, which saves only the animation, without mesh. I prefer to use this when exporting only the animation from Motionbuilder to some other software, since it’s cleaner, faster and file sizes are smaller, but you could use any other function also.