Jump to content

Extract rotation from packed geometry to a bone (Python)


Alain2131

Recommended Posts

Hello !

I'm trying to get an animation transfer from packed geometry to bone to work.

It works when transferring the animation to a geometry (there is no visible problem in the viewport), but in the Animation Editor, the keys are not very clean. See below

Here is the nice and tidy one, the source :

Clean keys

Here is the result from the script, the baked one :

Messy keys
As I said, in the viewport it works, but these keys are not very clean. Why do they look like that ? How to make them cleaner ? (See script and scene below)

BUT ! What I really want is to bake the animation on a bone, not on a box. The previous problem still apply to this situation.
The position works, but the rotation just fails miserably (with the same code !). The very same rotation is applied to the bone as the previous one that was applied to the test box, but the result in the viewport is not the same at all.
The red is the source and the blue is the skinned box (to the visible bone). 

rotation_problem.PNG.bff73ab3e8a5fa4e405f822d8fa2da53.PNG

Why is that ? What am I missing ?

Here's the code :

node = hou.pwd()
obj = hou.node("/obj")

def extractEulerRotates(self, rotate_order="xyz"): # Thanks to the Houdini help page for that. But there is a problem here though
    return hou.Matrix4(self.extractRotationMatrix3()).explode(rotate_order=rotate_order)["rotate"]
# The extracted rotation from this function is incorrect.

def bakePackedAnim():
    # Saving out some time-related variables
    intialFrame = hou.intFrame()
    startFrame = int(hou.hscriptExpression("$RFSTART")) # Don't know how to do it in Python
    endFrame = int(hou.hscriptExpression("$RFEND")) # Don't know how to do it in Python
    
    hou.setFrame(startFrame)
    
    # '''
    # Initial setup : Creates a bone and a box, and then skins the box to the bone with a Capture Proximity.
    
    theBone = obj.createNode("bone", "tranformed_bone") # Create only one bone. Would put it in a loop to create multiple.
    # theBone.moveToGoodPosition() # Easier to work without it. Will uncomment in the end
    
    
    # TO REMOVE... But weirdly with -not a bone- it's working. Hmmm.
    testGeo = obj.createNode("geo", "test_geo")
    fileNode = testGeo.allSubChildren()[0]
    
    testTransform = testGeo.createNode("xform")
    testTransform.setFirstInput(fileNode)
    testTransform.moveToGoodPosition()
    testTransform.setDisplayFlag(True)
    testTransform.setRenderFlag(True)
    # Remove up to here
    
    
    skinnedGeo = obj.createNode("geo", "skinned_geo")
    # skinnedGeo.moveToGoodPosition() # Easier to work without it. Will uncomment in the end
    
    skinnedGeo.deleteItems(skinnedGeo.allSubChildren()) # Removes the file node
    boxNode = skinnedGeo.createNode("box")
    
    captureProximNode = skinnedGeo.createNode("captureproximity")
    captureProximNode.setFirstInput(boxNode)
    captureProximNode.moveToGoodPosition()
    captureProximNode.parm("rootpath").set(str(theBone.path()))
    
    deformNode = skinnedGeo.createNode("deform")
    deformNode.setFirstInput(captureProximNode)
    deformNode.moveToGoodPosition()
    #deformNode.setDisplayFlag(True)
    #deformNode.setRenderFlag(True)
    
    # Applying some color to the skinned box
    attribWrangle = skinnedGeo.createNode("attribwrangle", "color")
    attribWrangle.setFirstInput(deformNode)
    attribWrangle.parm("snippet").set("@Cd = {0,0,1};")
    attribWrangle.moveToGoodPosition()
    attribWrangle.setDisplayFlag(True)
    attribWrangle.setRenderFlag(True)
    
    # '''
    
    # Transfers the animation from the specified geometry to the bone
    
    workingNode = hou.node("/obj/animated_box/OUT_script").geometry() # Gets the geometry of my test scenario
    
    for i in xrange(startFrame, endFrame+1): # For some reasons, xrange goes from the correct start value to the end value, minus 1. Strange.
        hou.setFrame(i)
        
        theFullTransform = workingNode.prims()[0].fullTransform()
        thePosition = workingNode.points()[0].attribValue("P") # This code works only for one object. Would do a loop here through all the pack geo.
        theRotation = extractEulerRotates(theFullTransform) # Got a problem with how this function extracts the rotation
        
        
        # Position
        key = hou.Keyframe(thePosition[0])
        theBone.parm("tx").setKeyframe(key)
        testTransform.parm("tx").setKeyframe(key)
        
        key = hou.Keyframe(thePosition[1])
        theBone.parm("ty").setKeyframe(key)
        testTransform.parm("ty").setKeyframe(key)
        
        key = hou.Keyframe(thePosition[2])
        theBone.parm("tz").setKeyframe(key)
        testTransform.parm("tz").setKeyframe(key)
        
        # Rotation
        key = hou.Keyframe(theRotation[0])
        theBone.parm("rx").setKeyframe(key)
        testTransform.parm("rx").setKeyframe(key)
        
        key = hou.Keyframe(theRotation[1])
        theBone.parm("ry").setKeyframe(key)
        testTransform.parm("ry").setKeyframe(key)
        
        key = hou.Keyframe(theRotation[2])
        theBone.parm("rz").setKeyframe(key)
        testTransform.parm("rz").setKeyframe(key)
        
    hou.setFrame(intialFrame)

bakePackedAnim() # Would need to create a UI or a button for convenience, calling this out.

And the scene : packed_anim_baker.hip

Thanks !

EDIT :
P.S. If I copy and paste relative reference from the source's rotation to the bone's rotation, the result is the same : obviously wrong rotation.

Edited by Alain2131
Link to comment
Share on other sites

  • Alain2131 changed the title to Extract rotation from packed geometry to a bone (Python)

Well, I've found the reason for why the bone have an incorrect interpretation of the rotation information inputted to it.
Answer : Rotation Order !

The bones has a rotation order fixed at ZYX. The animation being made in XYZ order, the bone said noppe, I do as I please.
For the test case, if I change the rotation order of the keyframe-animated source to match the bone's rotation order, the two animations match. Yay !

Will have to do some test with actual from-dop sim geometry instead of my keyframe-animated test case.

EDIT : Well, re-reading my code, I've got a pretty easy fix : it's at the fourth line, it's called "rotate_order" and it's currently set to "xyz". Congrats to me for taking this long to figure this out. >.< Setting this to "zyx" pretty much solves the problem.
Will have to do some more test, and I'll post a working solution.

Edited by Alain2131
Link to comment
Share on other sites

Phew, that was harder than expected !
A few problems arose, that for now I kinda dodged, or more accurately diverted onto the user to fix. Easy fix, do not worry (see file)

Here is a working code, but not usable. Working because it actually does the job, not usable for the time it takes to do so.
For 104 pieces, 96 frames, it took 50 seconds.
If I recall correctly, for the whole 240 frames, it was something like 6 minutes. It works, but it's too long. Wayyy too long.

EDIT : I must say I haven't tried sending the result to Maya or other packages yet. It's supposed to work, but eh, not tested.
EDIT2 : At the current moment, importing in Maya with FBX works properly. Only drawback is that it floods the Outliner with a bunch of skinCluster and tweakSet. I'm not very used to the Maya-way, so if someone could tell me if there is a way to either get rid of them in Maya or at the export from Houdini, or tell me that it's normal, I'd be grateful :)
P.S. Manually deleting the tweakSets doesn't seem to do anything.

The next step is taking the problem from before, the one I diverted on the user, and try to correct it automatically. See code comments and file for reference.

node = hou.pwd()
obj = hou.node("/obj")

def extractEulerRotates(self, rotate_order="zyx", thePivot=(0,0,0)): # Thanks to the Houdini help page for that.
    return hou.Matrix4(self.extractRotationMatrix3()).explode(rotate_order=rotate_order, pivot=thePivot)["rotate"]
# Currently not used, as there is a line that does exactly that (a shorter line, as this is still only one line... whatever xD)

def createGeomNodes(pieceName, masterSubnet, boneParent, workingNode, geoSubnet):
    # Creates a bone and a geometry node fetching the simulation's geometry, and then skins the geometry to the bone with a Capture Proximity.
    
    currentBone = masterSubnet.createNode("bone", str("bone_"+pieceName))
    currentBone.setFirstInput(boneParent)
    currentBone.moveToGoodPosition()
    
    #initialPosition = workingNode.points()[loopValue].attribValue("P") # Keeping that for reference.
    #theFullTransform = workingNode.prims()[loopValue].fullTransform()
    #initialRotation = extractEulerRotates(theFullTransform)
    
    skinnedGeo = geoSubnet.createNode("geo", pieceName)
    skinnedGeo.moveToGoodPosition()
    
    skinnedGeo.deleteItems(skinnedGeo.allSubChildren()) # Removes the file node
    
    objectMergeNode = skinnedGeo.createNode("object_merge")
    objectMergeNode.parm("objpath1").set(str(workingNode.sopNode().path()))
    
    deleteNode = skinnedGeo.createNode("delete")
    deleteNode.setFirstInput(objectMergeNode)
    deleteNode.moveToGoodPosition()
    deleteNode.parm("group").set("@name="+pieceName) # Putting the piece's name in the group to keep only this one
    deleteNode.parm("negate").set(1) # Set to Delete Non-Selected
    deleteNode.parm("entity").set(1) # Set to Points
    
    
    timeShiftNode = skinnedGeo.createNode("timeshift")
    timeShiftNode.setFirstInput(deleteNode)
    timeShiftNode.moveToGoodPosition()
    timeShiftNode.parm("frame").deleteAllKeyframes() # Remove the expression already present
    timeShiftNode.parm("frame").set(1) # Just to be sure, manually set the frame parameter to 1. Could be useless though
    
    unpackNode = skinnedGeo.createNode("unpack")
    unpackNode.setFirstInput(timeShiftNode)
    unpackNode.moveToGoodPosition()
    
    captureProximNode = skinnedGeo.createNode("captureproximity")
    captureProximNode.setFirstInput(unpackNode)
    captureProximNode.moveToGoodPosition()
    captureProximNode.parm("rootpath").set(str(currentBone.path())) # Set the rootpath to only one bone and not the hierarchy. Easier skinning.
    
    deformNode = skinnedGeo.createNode("deform")
    deformNode.setFirstInput(captureProximNode)
    deformNode.moveToGoodPosition()
    deformNode.setDisplayFlag(True)
    deformNode.setRenderFlag(True)
    
    '''
    # Applying some color to the skinned geometry. Used for debug
    attribWrangle = skinnedGeo.createNode("attribwrangle", "color")
    attribWrangle.setFirstInput(deformNode)
    attribWrangle.parm("snippet").set("@Cd = {0,0,1};")
    attribWrangle.moveToGoodPosition()
    attribWrangle.setDisplayFlag(True)
    attribWrangle.setRenderFlag(True)
    # '''
    
    return currentBone # I need the created bone for later reference


def bakePackedAnim():
    # Saving out some time-related variables
    intialFrame = hou.intFrame()
    startFrame = int(hou.hscriptExpression("$RFSTART")) # Don't know how to do it in Python
    endFrame = int(hou.hscriptExpression("$RFEND")) # Don't know how to do it in Python
    
    hou.setFrame(startFrame)
    
    workingNode = hou.node("/obj/simulated_geo/OUT_script").geometry() # Gets the geometry. Change this to point to your geometry. <----
    
    
    masterSubnet = obj.createNode("subnet", "baked_animation") # Will contain all the bones and geometry
    masterSubnet.moveToGoodPosition()
    
    boneParent = masterSubnet.createNode("null", "Parent") # All bones will be parented to this null
    boneParent.moveToGoodPosition()
    
    geoSubnet = masterSubnet.createNode("subnet", "geometry") # Will contain all the geometry
    geoSubnet.moveToGoodPosition() # I create it here for the moveToGoodPosition to place it where I want
    geoSubnet.setPosition((0,7)) # And I move it slightly up. This is just to have it at a nice place in the node editor :)
    # Another nicety would be to set the visible bounds for the view in the node editor. But I don't know how to, and it's not very important.
    
    boneList = []
    for fragments in workingNode.points():
        boneList.append( createGeomNodes(fragments.attribValue("name"), masterSubnet, boneParent, workingNode, geoSubnet) )
        # There is no clever thinking into the order of the arguments.
        # I just made a function and passed the arguments as the errors showed up. =P
    
    # Transfers the animation from the specified geometry to the bone
    # I plan to make this next part into a VEX wrangler instead. The previous part is easier done in Python and is acceptably fast,
    # but for the next part, Python is very shitty, speed-wise. Will see.
    for i in xrange(startFrame, endFrame+1): # For some reasons, xrange goes from the correct start value to the end value, minus 1. Strange.
        for j in xrange(0, len(boneList)): # But xrange works here. Hmmmm.
        #for j in xrange(9, 10): # Used for debug
            currentBone = boneList[j]
            
            hou.setFrame(i)
            
            initialPosition = hou.Vector3(hou.node(geoSubnet.path() + "/piece" + str(j) + "/timeshift1").geometry().points()[0].attribValue("P"))
            
            theFullTransform = workingNode.prims()[j].fullTransform()
            
            #thePosition = workingNode.points()[j].attribValue("P")
            
            #initialPosition = hou.Vector3(workingNode.points()[j].attribValue("P"))
            #theRotation = extractEulerRotates(theFullTransform, initialPosition)
            
            #thePivot = hou.Vector3(workingNode.prims()[j].intrinsicValue("pivot"))
            
            
            thePosition = theFullTransform.extractTranslates("srt") # Get the transform information from the Identity Matrix
            # thePosition += initialPosition # Move to the right position
            # The previous line is required if the identity matrix hasn't been correctly set. See wall of text below.
            
            
            # As of now, the position is correct.
            # BUT THE ROTATION ISN'T
            # The problem is that it still rotates around the old pivot point, the one before the "thePosition += initialPosition" line.
            # I've got to figure out how to make the rotation rotate around another center
            # For now, I'll keep the fix as is and go on with the second option
            
            # The fix is in simulate_geo, in the input 0 of the switch
            # All it does is correctly populate the identity matrix before the simulation (see comment just before this wall of text)
            
            # The second option is doing this after the simulation, in a for-each loop nested into the HDA that this will become
            # I'm letting all the commented tests here for future reference if needed.
            # The problem is in Python, this code is utterly slow
            # For 104, 96 frames, it took 50 seconds. That's wayyy too much for wayyy too few pieces.
            
            # All the commented code is some tests to manually rebuild the identity matrix to be able to have to right rotation.
            # Without success.
            # Next step is described above.
            
            #print theFullTransform
            #theFullTransform.setAt(3,0,thePosition[0])
            #theFullTransform.setAt(3,1,thePosition[1])
            #theFullTransform.setAt(3,2,thePosition[2])
            #print theFullTransform
            #print "\n"
            
            #initialPosition = hou.Vector3(workingNode.points()[j].attribValue("P"))
            #initialPosition = hou.Vector3(thePosition)
            #print initialPosition
            #theRotation = theFullTransform.extractRotates(transform_order="srt", rotate_order="zyx", pivot=initialPosition)
            
            #initialPosMatrix = hou.hmath.buildTranslate(initialPosition)
            
            #modifiedMatrix = theFullTransform + initialPosMatrix
            #print modifiedMatrix
            theRotation = theFullTransform.extractRotates(transform_order="srt", rotate_order="zyx")
            
            
            # Position
            key = hou.Keyframe(thePosition[0])
            currentBone.parm("tx").setKeyframe(key)
            
            key = hou.Keyframe(thePosition[1])
            currentBone.parm("ty").setKeyframe(key)
            
            key = hou.Keyframe(thePosition[2])
            currentBone.parm("tz").setKeyframe(key)
            
            # Rotation
            key = hou.Keyframe(theRotation[0])
            currentBone.parm("rx").setKeyframe(key)
            
            key = hou.Keyframe(theRotation[1])
            currentBone.parm("ry").setKeyframe(key)
            
            key = hou.Keyframe(theRotation[2])
            currentBone.parm("rz").setKeyframe(key)
        
    hou.setFrame(intialFrame)

bakePackedAnim() # Would need to create a UI or a button for convenience, calling this out.

 

packed_anim_baker.hip

EDIT :
For the ones that doesn't have access to Houdini FX, I'll explain the "diverted-on-the-user" fix.
Just before sending the geometry to the simulation, drop down an attribute wrangle, store the current position in a variable, and set the position to the origin.

v@oldP = @P;
@P = (0,0,0);

Then unpack, then pack (make sure to have the same amount of pieces after repacking). This little ping-pong game is absolutely necessary.
I suggest using the probably already-present name attribute in the pack node, and to transfer it too, so that you have a name primitive attribute.
Then, place an attribute promote to move the name attribute from primitive to the points.
Then, place an attribute copy with the attribpromote wired into its left input, and the AttributeWrangle wired to its right input. Use "name" in Attribute to Match, and "oldP" in Attribute Name.
It's then just a matter of placing an Attribute Wrangle under all that, like so

v@P = v@oldP;

Send that to the simulation, and the code will be all too happy to spit out correct position and rotation when you run it later on !

fix_setup.thumb.PNG.fae434dddb02a2d7d9587c248cfaf4d8.PNG

All it does really, is correctly populate the Identity Matrix (see in the Geometry Spreadsheet, Intrinsic "PackedFullTransform"). Without the fix, the position is all zeros, but with it, it has the correct initial positions and will be correctly updated. Yay !

Edited by Alain2131
  • Like 1
  • Thanks 1
Link to comment
Share on other sites

I'm not done yet !
Always to make the user's life easier, I'm trying to make the fix automated !
Easier said than done...

As of now, I've got a much faster result (15 seconds for 103 pieces for 240 frames), and the user just need to connect the input to the HDA (or input the path to the object) and press Bake !
And it's working perfectly !... For the translation.

For the rotation... It's, er, an approximate. The term isn't correct, as I don't throw a guess and have a rotation as a result, it's just the rotation before the needed computation. I just have no idea how to do the said calculation. Yet.
If you don't have constraints, and it's just your object blowing up or getting smashed up, you probably won't notice the problem. But if you do have constraints, you'll end up with something like that ↓
image.thumb.png.b86e5715cdabc63519133818b8c833bd.png

Not wildly incorrect, just... Okay, that's incorrect. I'm working on that.

EDIT3 : If you've scaled your packed objects before using the tool, it won't work. So for now, forget the scale. Will try to fix that eventually.
EDIT4 : With further testing, I noticed that the tool gets all mixed up in certain situations. This has to do with the name attribute and how it's been created. Will fix that eventually.

The scene is included below (the cylinder example from above)
If you try the classic pighead falling on the ground, you won't notice the problem.

I've made an HDA out of it, it's easier to use now.

Let me know if it works for you !

P.S. If anybody could here me figure out that rotation problem, I'd be super grateful, it's been a few hours trying to decipher inverse transforms (which I never used and am not sure when to use them), multiplying matrices or making them and whatnot.

Cheers !

packed_anim_baker_1.hip

Packed_Anim_Baker.hda

EDIT :
P.P.S. If you want me to make an apprentice version, just ask for it here :)
Although it would not be very useful, as this would mainly be used to be exported over FBX, which is a FX feature... Eh

P.P.P.S. Oh ! Also, if someone could give me pointers as to how to have correct exported gizmos so that in Maya, the gizmos are following the objects, that'd be nice too :) Alright, well, thinking about it, that's not possible, because that would mean that there is some kind of animation on the geometry, with a kind of inverse animation on the gizmo to make the geometry static while the gizmo moves. That's, as far as I know, not possible. And the whole idea is not really useful anyway.

EDIT 2 :
P.P.P.P.S (this is getting ridiculous)
I must say that with the manual fix, the current version of the tool doesn't work properly. It still have some weird rotation. I can fix that (it adds 2 seconds to the whole process though. Not very bad, just slightly annoying.)
And, on the down side, the fix actually make the simulation look a bit different. That's weird, and kinda bad. So if you were planning on using the tool with the manual fix, expect the simulation to look a bit different. But if you still want that, you can ask for a version of the tool that works properly with the fix, if you can live with the slightly changed simulation.

Edited by Alain2131
Link to comment
Share on other sites

  • 6 months later...

6 months has passed. And so have I, to other matters.
But then, while I was watching a SideFX's masterclass, a certain handy dandy little node called Extract Transform was briefly shown. And I thought - hey, that might be it ! What's been missing to complete the tool of this thread !

And it is ! It works ! Yay !

Works on packed geo only.

Let me know if this tool is of use to you.

Cheers !

RigidBody_Baker.hda

rigidBody_baker_tests.hip

Edited by Alain2131
  • Like 3
Link to comment
Share on other sites

  • 1 year later...
  • 3 weeks later...

Hey guys !

Just thought I'd come back to this for a bit, adding some functionality.
I had completely forgotten about this until I received some notifications of activity on the thread. I'm glad it's useful to some people, at least !

1) There is now a toggle leaving you the choice between separating each packed pieces into its own geometry node, or to have only one geo with all of the skinning.
The previous default was separate pieces for each packed geo, which is heavier in general.

2) I added some UI to be able to name the subnet and the geometry nodes, for convenience.

3) There is also a progress bar showing the progress of the baking process.

4) And finally, I applied a Euler Filter to the rotation.

I did some other inconsequential small tweaks.

I checked, and having scaled packed pieces works perfectly, no matter if it is before the simulation, after it, or both. Same for pre/post translation/rotation. The extracttransform node really work like a charm.

rigidBody_baker_tests.hip

RigidBody_Baker.hda

P.S. This have now turned more into an Tools (HDA) topic than scripting. If an admin would prefer that I create a new topic in the Tools section, do tell me.

Edited by Alain2131
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...