3
$\begingroup$

I've been trying different examples to parent an object to a bone without the object moving (aka keep_transform), but no matter what I try it always offsets:

object.parent = armature
object.parent_bone = bone #object now in weird position
object.parent_type = 'BONE'

I also tried this as suggested here:

object.parent = armature
object.parent_bone = bone
bpy.context.active_pose_bone
bpy.ops.object.parent_set(type='BONE', keep_transform=True)

But get error Error: No active bone

I notice the object I am parenting to the bone in the armature is always moving to the bone's tail when I need it to be positioned at the head.

Is there anything I can do to programmatically affix the object to the bone's head? Any help her would be most encouraging, I just keep hitting walls with this.

One idea I had was to just offset the object by the head/tail delta, but because blender allows parenting-in-place I figured I should stick to convention.

$\endgroup$

3 Answers 3

3
$\begingroup$

I have just tested batFINGER's code in Blender 2.79b, there is a bug in the formula.

When you multiply armature's world matrix with pose bone's object space matrix, you actually reached the bone head, not the bone tip, so you need to walk a bone's length to the bone tip, which is the origin of the pose bone.

Pose bone has a 'head' property and a 'tail' property, so we can calculate the bone vector, which equals to bone.tail minus bone.head.

I have verified the code in my project, it works correctly.

Here is the bug fixed version:

import bpy
context = bpy.context
from mathutils import Matrix
scene = context.scene
armature = scene.objects.get("Armature")
cube = scene.objects.get("Cube")

if cube and armature:
    cube.parent = armature
    bone = armature.pose.bones.get("Bone")
    cube.parent_bone = bone.name
    cube.parent_type = 'BONE'
    # keep current location
    cube.matrix_parent_inverse = (armature.matrix_world * Matrix.Translation(bone.tail - bone.head) * bone.matrix).inverted()

$\endgroup$
2
  • $\begingroup$ Thank you for posting this, it has been so helpful and works flawlessly. $\endgroup$
    – Logic1
    Nov 1, 2019 at 8:27
  • 1
    $\begingroup$ Hey, trying to use this in Blender 3.2 and only getting this error: "Matrix.invert(ed): matrix does not have an inverse". Any idea how to fix it? $\endgroup$ Sep 5, 2022 at 14:04
3
$\begingroup$

When using bpy.ops.parent_set you want the type to be 'BONE_RELATIVE'.

Setting the active pose bone is a little un-intuitive, you need to set it in the armature object, not in the context.

import bpy

scn = bpy.context.scene
object = scn.objects['Cube']
armature = scn.objects['Armature']
arm_bones = bpy.data.armatures[armature.name].bones

object.select = True
armature.select = True
scn.objects.active = armature

arm_bones.active = arm_bones['Bone']

bpy.ops.object.parent_set(type='BONE_RELATIVE')
$\endgroup$
2
  • $\begingroup$ @sambler This worked very well! Thank you so very much!! $\endgroup$
    – Logic1
    Jul 14, 2018 at 2:02
  • $\begingroup$ Also thank you @batFINGER for the suggestion, my armature object and data name were in fact different and this also worked very well!! $\endgroup$
    – Logic1
    Jul 14, 2018 at 2:03
2
$\begingroup$

API method (no operators)

EDIT

Would like to thank @Art Golf for pointing out the error in my ways, have corrected, and updated for 2.9. At the risk of sounding "touche" there is an issue with setting the object space parent inverse matrix using an inverted global space matrix. If an object already has a parent will not work as expected

Since the fix has moved parenting from tail to tip added the option to parent to either end by setting an offset factor 0 for tip, 1 for tail.

Added some different ways to get to the same result of either snapping to bone or keeping the current global transform.

If you know the object and the bone, then often it's easier to parent directly using API methods, setting ob.parentto armature object, ob.parent_type to 'BONE' and ob.parent_bone to the name of the bone.

Some basics

Does a child object inherit the matrix from the parent?

The transform properties panel, location, rotation and scale is where you set the matrix basis of an object.

When an object is parented its local coordinates are calculated

ob.matrix_local = ob.matrix_parent_inverse @ ob.matrix_basis

any global point can be put into the local (or object) coordinates of the parent

local_coord = op.parent.matrix_world.inverted() @ global_coord

allowing us to arbitrarily set the origin of our basis coordinate system. The most basic would be setting parent inverse to identity, ensuring the origin of our basis space matches that of the parent.

From the equations above, knowing both the global matrices of parent and child, can set the parent inverse accordingly.

Blenders default parenting would be akin to 'KEEP_WORLD_AND_BASIS' below, in that the object is parented maintaining both the local and world transform.

Test script, the 'KEEP' options remain in same location, the 'SNAP' option snaps cube to the bone.

import bpy
context = bpy.context
from mathutils import Matrix
scene = context.scene
armature = scene.objects.get("Armature")
cube = scene.objects.get("Cube")
bone = armature.pose.bones.get("Bone")
# parent from tip to head
offset = 0 # 0 tip, 1 head
op = 'KEEP_WORLD_AND_BASIS'

if cube and armature and bone:

    # 4x4 Identity
    I = Matrix()
    # tip matrix world
    T = Matrix.Translation(
        (1 - offset) * (bone.tail - bone.head)
        )
    tmw = armature.matrix_world @ T @ bone.matrix
    amwi = armature.matrix_world.inverted() 
    cmw = cube.matrix_world.copy()
    cmb = cube.matrix_basis.copy()      
    if op == 'KEEP_WORLD_AND_BASIS':

        # keep  cmw and cmb constant, set parent inverse

        cml = tmw.inverted() @ cmw
        cube.matrix_parent_inverse = cml @ cube.matrix_basis.inverted()
        
    elif op == 'SNAP_IDENTITY':
        # snap to bone make local and pinv identity
        cube.matrix_local = I
        cube.matrix_parent_inverse = I
    elif op == 'SNAP_NO_INVERSE':
        # snap with identity inverse (same as above?)
        cube.matrix_parent_inverse = I 
        cube.matrix_world = tmw         
        
    
    cube.parent = armature
    
    cube.parent_bone = bone.name
    cube.parent_type = 'BONE' 
    
    # quick n dirty, set the global matrix and 
    # let blender deal with local and parent inverse
    
    if op == 'KEEP':
        cube.matrix_world = cmw
    elif op == 'SNAP':
        cube.matrix_world = tmw
$\endgroup$

You must log in to answer this question.

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