Parenting¶
An object's parent can be queried or set simply through its parent
attribute, which needs to reference another Object (or None
).
But when parenting is involved the use of transformation matrices becomes somewhat more complex. Suppose we have two cubes above each other, the top cube transformed to Z=5 and the bottom cube to Z=2:
Using the 3D viewport we'll now parent the bottom cube to the top cube (LMB
click bottom cube, Shift-LMB
click top cube, Ctrl-P
, select Object
) and inspect the values in Python:
>>> bpy.data.objects['Bottom cube'].parent
bpy.data.objects['Top cube']
# The bottom cube is still located in the scene at Z=2
# in world space, even after parenting, as is expected
>>> bpy.data.objects['Bottom cube'].matrix_world
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 2.0),
(0.0, 0.0, 0.0, 1.0)))
If an object has a parent its matrix_local
attribute will contain the transformation relative to its parent, while matrix_world
will contain the resulting net object-to-world transformation. If no parent is set then matrix_local
is equal to matrix_world
.
Let's check the bottom cube's local matrix value:
# Correct, it is indeed -3 in Z relative to its parent
>>> bpy.data.objects['Bottom cube'].matrix_local
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, -3.0),
(0.0, 0.0, 0.0, 1.0)))
As already shown above the parent
attribute can be used to inspect and control the parenting relationship:
>>> bpy.data.objects['Top cube'].parent
# None
>>> bpy.data.objects['Bottom cube'].parent
bpy.data.objects['Top cube']
# Remove parent
>>> bpy.data.objects['Bottom cube'].parent = None
At this point the two cubes are no longer parented and are at Z=2 (Bottom cube) and Z=5 (Top cube) in the scene. But when we restore the parenting relationship from Python something funny happens 1:
# Set parent back to the top cube, as before
>>> bpy.data.objects['Bottom cube'].parent = bpy.data.objects['Top cube']
The "Bottom cube" actually jumps in +Z direction as ends up on top of "Top cube". The reason for this behaviour is that when using the UI to set up a parenting relationship as earlier does more than just setting the parent
attribute of the child object. There's also something called the parent-inverse matrix. Let's inspect it and the other matrix transforms we've already seen for the current (unexpected) scene:
# Identity matrix, i.e. no transform
>>> bpy.data.objects['Bottom cube'].matrix_parent_inverse
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0)))
# Hmmm, this places the "Bottom cube" 2 in Z *above* its parent at Z=5...
>>> bpy.data.objects['Bottom cube'].matrix_local
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 2.0),
(0.0, 0.0, 0.0, 1.0)))
# ... so it indeed ends up at Z=7 as we saw (above "Top cube")
>>> bpy.data.objects['Bottom cube'].matrix_world
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 7.0),
(0.0, 0.0, 0.0, 1.0)))
So what happened here? Apparently the matrix_local
matrix changed from its value of Z=-3 as we saw earlier. The answer is that when you set up a parenting relationship using the UI the parent-inverse matrix is set to the inverse of the current parent transformation (as the name suggests), while matrix_local
is updated to inverse(parent.matrix_world) @ to_become_child.matrix_world
.
If we clear the parent
value from Python and redo the parenting in the UI we can see this in the resulting transform matrices:
>>> bpy.data.objects['Bottom cube'].parent = None
# <parent "Bottom cube" to "Top cube" in the UI>
# Was identity, is now indeed the inverse of transforming +5 in Z
>>> bpy.data.objects['Bottom cube'].matrix_parent_inverse
Matrix(((1.0, -0.0, 0.0, -0.0),
(-0.0, 1.0, -0.0, 0.0),
(0.0, -0.0, 1.0, -5.0),
(-0.0, 0.0, -0.0, 1.0)))
# Was Z=2, is now Z=2-5=-3
>>> bpy.data.objects['Bottom cube'].matrix_local
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, -3.0),
(0.0, 0.0, 0.0, 1.0)))
# Was Z=7
>>> bpy.data.objects['Bottom cube'].matrix_world
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 2.0),
(0.0, 0.0, 0.0, 1.0)))
The reason for this behaviour is that when doing parenting in the 3D viewport you usually do not want the object that you are setting as the child to move, which would happen if the child's existing transform is suddenly interpreted as being a transform relative to its parent. To overcome this the parent-inverse matrix set during the parenting steps is used to compensate, as it sits between the parent and child transforms. But when we simply set parent
from Python, the matrix_local
value is used as is, causing our bottom cube to suddenly move up, as it is used as the transform relative to its parent, while it actually would need a different value to stay in place.
There's actually a worse side-effect, in that the location values shown in the Transform UI no longer seem to make sense:
Yes, that's right, the cube that's lower in Z (left image) shows a higher Z location value than the cube that's actually higher in Z (right image). Again, this is caused by the inconsistent parent-inverse matrix from doing "manual" parenting through Python.
So when setting up parenting relations through Python it's best to be careful and keep the above in mind. There's actually quite a bit more going on with all the different parenting options available from the UI. See this page for more details.
Children¶
To retrieve an object's children (i.e. the objects it is the parent of) one can use its children
property. This only returns the direct children of that object, and so not children of its children, etc. Getting to the set of all children of an object (direct and indirect) was made slightly easier in Blender 3.1 with the addition of the children_recursive
attribute.
For example, given a Cube, Suzanne and Torus object, where Suzanne is parented to Cube, and the Torus is parented to Suzanne:
>>> list(bpy.data.objects)
[bpy.data.objects['Cube'], bpy.data.objects['Suzanne'], bpy.data.objects['Torus']]
>>> bpy.data.objects['Suzanne'].parent
bpy.data.objects['Cube']
>>> bpy.data.objects['Torus'].parent
bpy.data.objects['Suzanne']
>>> bpy.data.objects['Cube'].children
(bpy.data.objects['Suzanne'],)
>>> bpy.data.objects['Suzanne'].children
(bpy.data.objects['Torus'],)
>>> bpy.data.objects['Cube'].children_recursive
[bpy.data.objects['Suzanne'], bpy.data.objects['Torus']]
These attributes are also available for collections.
-
The same thing happens when setting the parent in the UI using
Object Properties > Relations > Parent
↩