PacktPublishing / Python-Scripting-in-Blender

Python Scripting in Blender, published by Packt
MIT License
38 stars 16 forks source link

`obj.matrix_local` != (`child_of_parent.matrix_world` @ `obj.matrix_world`) ? #7

Closed Andrej730 closed 10 months ago

Andrej730 commented 11 months ago

Hi! In the book it's mentioned that obj.matrix_local contains the local location, rotation, and scale of an object, omitting the transformation inherited by the parent object but not the one resulting from constraints. Haven't used this matrix before and end up putting some experiments to understand it.

Doesn't it mean that matrix_local should should be equal to matrix_basis (contains the local location, rotation, and scale of an object before it is transformed by object constraints) multiplied by constraints matrix? Assuming matrix_world = parent @ matrix_local and matrix_local = constraints @ matrix_basis.

Here's the short example to reproduce this. With my understanding I expected it to print True at the end, perhaps I'm missing something in understanding matrix_local.

import bpy
import numpy as np

same = lambda x, y: np.allclose(x, y, atol=10e-5)

bpy.ops.mesh.primitive_cube_add(location=(13, 0, 0))
bpy.ops.mesh.primitive_cube_add(location=(23, 14, 0))

obj = bpy.data.objects["Cube"]
obj.location = (10, 0, 0)
obj.rotation_euler = (0.5,0.5,0.5)

parent = bpy.data.objects["Cube.001"]
parent.name = "Parent"
parent.rotation_euler = (1,1,1)

child_of_parent = bpy.data.objects["Cube.002"]
child_of_parent.name = "Child Of Parent"
parent.rotation_euler = (1.33,1.33,1.33)

obj.parent = parent
obj.matrix_parent_inverse.identity()
constraint = obj.constraints.new(type="CHILD_OF")
constraint.target = child_of_parent
constraint.inverse_matrix.identity()
bpy.context.view_layer.update()

print(same(obj.matrix_local, child_of_parent.matrix_world @ obj.matrix_basis)) # False
pKrime commented 10 months ago

Hi,

this is an interesting topic, I hope that elaborating on your example can help a bit. There are two tricky points to keep in mind:

print(same(obj.matrix_world, parent.matrix_world @ obj.matrix_local)) # True

matrix_local is changed so that the result of parent.matrix_world @ obj.matrix_local satisfies the condition of the constraint.

Child of is designed so that obj is subject to both the transformations coming from its hierarchy (parent) and from its target (child_of_parent). On top of that, obj can be moved, rotated and scaled locally: these channels are stored in matrix_basis.

This is a World-Space constraint, it's goal is for obj.matrix_world to satisfy:

print(same(obj.matrix_world, child_of_parent.matrix_world @ parent.matrix_world @ obj.matrix_basis)) # True

In other words

parent.matrix_world @ obj.matrix_local = child_of_parent.matrix_world @ parent.matrix_world @ obj.matrix_basis

If those were scalars we could just divide by parent.matrix_world and find obj.matrix_local, but matrices are different and instead we premultiply by parent.matrix_world.inverted() on both sides.

It's tricky if one thinks of numbers, but keeping in mind that

parent.matrix_world.inverted() @ parent.matrix_world = identity

makes it more intuitive how come the transformation

parent.matrix_world.inverted() @ parent.matrix_world @ obj.matrix_local = parent.matrix_world.inverted() @ child_of_parent.matrix_world @ parent.matrix_world @ obj.matrix_basis

is the formula for computing the matrix_local enforced by Child of: obj.matrix_local is the only term remaining on the left. You can see how adding this line to your script prints "True"

print(same(obj.matrix_local, parent.matrix_world.inverted() @ child_of_parent.matrix_world @ parent.matrix_world @ obj.matrix_basis)) # True

Please let me know if this reply answers to your question, congratulations on your progress!

p.

Andrej730 commented 10 months ago

Thank you, appreciate the detailed explanation!

The main takeaways for me: