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.parent
to 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