0
$\begingroup$

If you do as in the opening question, you lose your poses as soon as you remove the constraints.

One solution is to apply visual location, visual rotation and visual scale. Then you can remove constraint and bones will remain in place, at each frame.

Problem: I don't get how to apply visual anything from Python code. I'm getting error that property "visual_location" doesn't exist. And doing it in GUI for 60+ frames is unpractical.

Use case:

You want to reuse animations stored in different FBX files. Mixamo style, Unity Asset Store style, and convert each one into a different action of the same Armature.

Workflow 1: because these FBX files never have the same Rest Pose when imported, you need to match the initial pose first, for each action, either pose it yourself or use constraints+script to get a perfect match. Insert keyframe at 1 (or 0) in gui to be able to use visual_*, remove constraints, copy all frames, in graph editor, from armature 2 (the one containing the action you want to incorporate in yours) and paste into armature 1. Name your action, push down or stash.

But this workflow has a problem. Sometimes these files don't only contain keyframes in pose mode they sometimes move, and rotate, the armature origin in object mode.

So you have to copy the keyframes in graph editor in object mode too, and apply to your object. No good. My own actions never move objects origins or armature origin, only bones. And I want any imported action to integrate nicely with this, not to force me to change my way of doing things.

Workflow 2:

  • One script to recursively create constraints to all bones of Armature 1 and configure it to reference the same named bone form Armature 2.

  • Another script to recursively insert keyframes from a set minimum to a set maximum. Most actions are between 30 or 60 frames. The problem: I don't see how to apply visual transform from Python. If you apply normal transform, the final keyframed pose doesn't include influence from the constraint.

  • Another script to remove all constraints. So you clean before repeat with any other animation you want to import later.

The three scripts:

SetBoneConstraints

import bpy

arm1 = bpy.data.objects["Bip001"]
arm2 = bpy.data.objects["Bip001.001"] # Change name if need.

for bone1 in arm1.pose.bones:
    for i in range(0, 9):
        cname = "Copy Transforms.00" + str(i) # Possible duplicates from previous attempts
        if cname in bone1.constraints:
            bone1.constraints.remove(bone1.constraints[cname])
    if "Copy Transforms" in bone1.constraints:
        bone1.constraints.remove(bone1.constraints["Copy Transforms"])
    bone2 = arm2.pose.bones[bone1.name]
    crc = bone1.constraints.new("COPY_TRANSFORMS")
    crc.target = arm2
    crc.subtarget = bone2.name
    crc.target_space = "WORLD"
    crc.owner_space = "WORLD"

KeyframeAction

import bpy

arm1 = bpy.data.objects["Bip001"]

frame_start = 1
frame_end = 31 #inclusive

for f in range(frame_start, frame_end+1):
    for bone1 in arm1.pose.bones:
        bone1.keyframe_insert("location", frame=f)
        if bone1.rotation_mode == "QUATERNION":
            bone1.keyframe_insert("rotation_quaternion", frame=f)
        else:
            bone1.keyframe_insert("rotation_euler", frame=f)
        bone1.keyframe_insert("scale", frame=f)
        

RemoveBoneConstraints

import bpy

arm1 = bpy.data.objects["Bip001"]

for bone1 in arm1.pose.bones:
    if "Copy Transforms" in bone1.constraints:
        bone1.constraints.remove(bone1.constraints["Copy Transforms"])
$\endgroup$
2
  • $\begingroup$ As far as I understand the problem here, the operation you're looking for already exists. It's "bake action" operation, with clear constraints and visual keying. For each frame, register visual keyframe, then when all done, clear constraints. Can bake armature object and bones within at the same time if enabling both object and bone. For object->root bone, can copy transforms constraint from an armature object (no bone needed) onto root and bake it. $\endgroup$
    – Nathan
    Feb 24 at 4:25
  • $\begingroup$ Thanks. Looking into "bake action". $\endgroup$ Feb 24 at 4:51

1 Answer 1

0
$\begingroup$

Quick solution until a better one shows up

I tried hard to not use operators. You notice how Blender freezes around 2 seconds when you run this script (depends on armature complexity, your hardware, etc), but in the end it works.

Solution using operators, sorry.

Only second script changes.

KeyframeAction

import bpy
context = bpy.context
scene = context.scene

arm1 = bpy.data.objects["Bip001"]

frame_start = 1
frame_end = 31 #inclusive

context_o = context.copy()
context_o["selected_objects"] = list(arm1.pose.bones)
with context.temp_override(**context_o):
    for f in range(frame_start, frame_end+1):
        scene.frame_set(f)
        bpy.ops.anim.keyframe_insert_by_name(type="BUILTIN_KSI_VisualLocRotScale")

Final workflow:

You have a bunch of FBX as parts of a pack of animations. Mixamo, asset stores, etc.

First one, usually in a directory called model, contains the armature in rest pose.

  • Import that first. Note: most of the time you need to select Automatic Bone Orientation in importer file chooser window. If you don't, orientation of bone heads are super hard to work with. You want them to follow arms/leg/fingers direction. Also now is the time to apply a scale value. If you do now, animations look fine. If you do later in object properties, bone movement won't inherit scale, and movements will be super exaggerated. If you don't know ideal scale value, and need to work it visually, do a mock import, find ideal scale with objects properties, and then copy the value, delete the armature, and import again using this value in the importer file chooser dialog.

  • Do any required adjustment to match your character mesh pose, in armature edit mode, usually you have to fix arms angle and maybe legs (sometimes fingers).

  • Make parent of mesh and select Armature Deform.

  • If for some reason you want to move the armature in object mode, don't yet. First parent it to an empty. Empty must be at world origin. Then you can move the armature to correctly align with your character mesh. Move the armature, not the empty. This assumes mesh is perfectly positioned in world origin. Origin point between feet recommended. Sometimes, when parenting Armature to Empty, Mesh unparents from Armature. Watch for this and parent again. When you export to game engines, you don't need to select this empty, but it is probably harmless to keep it. You can recreate it with any node type provided by engine.

  • Now iterate your animation collection, one by one:

    • Import next FBX containing animation. Probably, for some evil reason, while part of the same package, its resting pose will not be the same of the first FBX. Because poses are relative to... (you got it) You cannot simple copy and paste pose, because it won't look right in your first Armature.

    • If you moved the first Armature in previous steps, then you need to perfectly align with the newly imported FBX, or, if the origin of this new imported FBX isn't perfectly aligned with the original rest pose (it's animated), then you have to move the empty until the Armature 1 origin point is at the original rest pose origin point. Move the Empty, not the Armature.

      You can do it visually, but a bit of arithmetic is recommended. Just take the difference between the world position of your first Armature and its original position before you moved it (do a reimport of the original to find out where it was if needed, and then delete), and apply that difference to the Empty. This will perfectly align both armatures, without modify the local position of the first Armature relative to their parent Empty. You may have to invert some signs, make some positive values negative and vice versa.

      If you see the origins not perfectly aligned between the Armature 1 and the one containing the animation, its probably because the object location of the armature is also animated. But if they are part of the same package then it should be OK. Armature 1 origin must be aligned with its original world position before you moved it, not with Armature 2 origin.

    • Run Script 1: SetBoneConstraints, this will, pain free, create constraints for every individual bone of the first Armature, and target bones with the same name from the second Armature. The names of the Armatures are hardcoded, edit them for each situation. Both set to "WORLD" is not a BUG.

    • Edit, in Script 2: KeyframeAction (the version in this answer), frame_start and frame_end of the action you want to acquire. Run script. This will create all keyframes with correct values.

    • Run Script 3, this will remove constraints from bones.

    • Give action a descriptive name. Push down or Stash.

    • Delete Armature 2. If you save and close Blender now, you save some time by not having to edit the name of the armatures in script 1, except the naming is inconsistent in original package (probably not). If you don't close Blender, the next Armature may be called "Armature.002" rather than reuse the name "Armature.001".

    • Repeat for next FBX of the package.

Note: Script 2 doesn't set interpolation to Linear yet. And it leaves Blender default (Bezier Curve). But these packages usually are keyframed every single frame and expect Linear. This can be confirmed by examining the interpolation set in the frames of each imported FBX.

You can set to Linear by selecting all keyframes in Graph Editor, then right click, then Interpolation Mode -> Linear. Due to the small distance between frames, you may very well not notice it, but it's not the correct thing to do.

Advantages of this method:

  • You get what you see.

  • If FBX imports having objects properties animated (like moving armature origin), by using constraints you get the same result but without event touching the Armature origin. Bones will just move a bit more to visually match the ones from Armature 2. That's why we used "WORLD" coordinate system in Script 1.

This probably can be integrated in a single script that also automate the import of every FBX in the collection. But I'm stopping here, as I have some collections but not so many of them as to automate more.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .