Skip to content

Instantly share code, notes, and snippets.

@cttlfsh
Last active September 11, 2024 12:48
Show Gist options
  • Save cttlfsh/9cd15235a1bca9856d7cb3f9203826fc to your computer and use it in GitHub Desktop.
Save cttlfsh/9cd15235a1bca9856d7cb3f9203826fc to your computer and use it in GitHub Desktop.
[Autodesk Maya] Matrix Based Object Orientation

A Different Approach to Object Orientation

Orienting objects in a 3D software can be a task that sometimes takes more time than predicted. In Autodesk Maya it can be obtained in several different ways - constraints, direct connections or even the OrientJoint tool if the object is type is a joint.

Nevertheless, these methods are far from the optimal solution in terms of performance and are sometimes tricky to setup - with a special regard to the aimConstraint.

Here it is explained how to obtain the same result, orienting ANY kind of of object towards another, without relying on pre-made tools (constraints) or spending time in the NodeEditor setting up nodes. With a little bit of linear algebra, we can compute a transformation matrix that encodes the correct values to orient the object in the desired way.

Some basics of Linear Algebra

Before diving into the code, it's important to first clarify some basic concepts of Linear Algebra to ensure the material is accessible to everyone.

Matrices

  • Identity Matrix: It is a square matrix with all the diagonal-elements equal to 1, 0 in all the others and it is generally identified by the symbol I. If I is multiplied by any given matrix, it applies no information, fulfilling the multiplicative identity property. It can be seen as the matricial equivalent of the multiplication by 1.

$$ I = \left\lbrack \matrix{1 & 0 & 0 & 0 \cr 0 & 1 & 0 & 0 \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1} \right\rbrack $$

  • Affine Transformation Matrix: Any matrix that can be interpreted as a combination of translation, rotations, scalings

  • Matrix Transposition: operation which flips the matrix over its diagonal, switching rows with columns. The transpose of a matrix $\mathbf{A}$ it is denoted as $\mathbf{A^T}$,

  • A vector can be represented in two different forms: row or column. This distinction affects how vectors are displayed:

    • Row-Vector: $\vec{v} = \left\lbrack \matrix{1 & 0 & 0 & 0} \right\rbrack$ ,
    • Column-Vector: $\vec{v} = \left\lbrack \matrix{1 \cr 0 \cr 0 \cr 0} \right\rbrack $.

    To switch between these representations, a transpose operation is required. In computer science are called Row-Major and Column-Major representation and, since vectors and matrices are stored in arrays, it's crucial to know which representation is being used, as it impacts the order in which values are stored.

From now on, the Column-Vector form will be used to display all vectors and matrices.

Dot product

Also known as scalar product, it is a vector operation that produces a scalar value. Given two vectors a, b, the dot product between them is defined as:

$$ \mathbf{\vec{a}} \cdot \mathbf{\vec{b}} = \Vert\mathbf{\vec{a}}\Vert * \Vert\mathbf{\vec{b}}\Vert * \cos(\theta) $$

where $\Vert\mathbf{\vec{x}}\Vert$ is the lenght (norm) of the vector x, and $\theta$ is the angle between a and b.

Properties

This operation fulfills several properties, among which:

  • Commutative: $\mathbf{a} \cdot \mathbf{b} = \mathbf{b} \cdot \mathbf{a}$,
  • Distributive over vector addition: $\mathbf{\vec{a}} \cdot (\mathbf{\vec{b}} + \mathbf{\vec{c}}) = (\mathbf{\vec{a}} \cdot \mathbf{\vec{b}}) + (\mathbf{\vec{a}} \cdot \mathbf{\vec{c}})$,
  • Orthogonal: Two not null vectors are orthogonal iff $\mathbf{\vec{a}} \cdot \mathbf{\vec{b}} = 0$.

Cross product

Also known as vector product, it is a vector operation that produces a vector perpendicular to the plane formed by the original vectors. Giving two vectors a, b, the cross product between them can be defined as:

$$ \mathbf{\vec{a}} \times \mathbf{\vec{b}} = \Vert \mathbf{\vec{a}} \Vert \Vert \mathbf{\vec{b}} \Vert \sin(\theta) \mathbf{\vec{n}} $$

where $\theta$ is the angle between the two vectors on the plane they form, $\Vert \mathbf{\vec{a}} \Vert$, $\Vert \mathbf{\vec{b}} \Vert$ are the lenghts (norms) of the vectors a and b and n is the unit vector perpendicular to the plane formed by a and b.

Properties

This operation fulfills several properties, among which:

  • Anticommutative: $\mathbf{\vec{a}} \times \mathbf{\vec{b}} = -(\mathbf{\vec{b}} \times \mathbf{\vec{a}})$,
  • Distributive over vector addition: $\mathbf{\vec{a}} \times (\mathbf{\vec{b}} + \mathbf{\vec{c}}) = (\mathbf{\vec{a}} \times \mathbf{\vec{b}}) + (\mathbf{\vec{b}} \times \mathbf{\vec{c}})$.

Affine Transformation Matrix

As stated before, an Affine Transformation Matrix can be interpreted as a combination of three (four if we consider shear) different operations and in a 3D environment it is represented as a 4x4 matrix.

$$ A = \left\lbrack \matrix{a_{11} & a_{12} & a_{13} & a_{14} \cr a_{21} & a_{22} & a_{23} & a_{24} \cr a_{31} & a_{32} & a_{33} & a_{34} \cr a_{41} & a_{42} & a_{43} & a_{44}} \right\rbrack $$

where the $a_{ij}$ parameters are the combination of the desired operations (translate, rotate, scale and shear). Let's now give definitions of some basics linear transformations.

Translation

Translations is the easiest of the operations, and its 4x4 representation is defined as:

$$ T = \left\lbrack \matrix{1 & 0 & 0 & t_x \cr 0 & 1 & 0 & t_y \cr 0 & 0 & 1 & t_z \cr 0 & 0 & 0 & 1} \right\rbrack $$

If applied to an object, it moves (translate) it alone one or more axis. In particular, it adds the translation vector (tx, ty, tz) to the object, shifting its position.

$$ T * \left\lbrack \matrix{ p_x \cr p_y \cr p_z \cr 1} \right\rbrack = \left\lbrack \matrix{p_x + t_x \cr p_y + t_y \cr p_z + t_z \cr 1} \right\rbrack $$

Scale

A Scaling Matrix changes the object's size along one or more axis. Its 4x4 representation is the following:

$$ S = \left\lbrack \matrix{ s_x & 0 & 0 & 0 \cr 0 & s_y & 0 & 0 \cr 0 & 0 & s_z & 0 \cr 0 & 0 & 0 & 1 } \right\rbrack $$

Applying a Scaling Matrix to an object, means multiplying the object's coordinates by the scalar values of the corresponding axis:

$$ S * \left\lbrack \matrix{ p_x \cr p_y \cr p_z \cr 1} \right\rbrack = \left\lbrack \matrix{s_x * p_x \cr s_y * p_y \cr s_z * p_z \cr 1} \right\rbrack $$

Rotation

The Rotation Matrix is the more complicated, as its definition is a little less intuitive than the previous matrices. It has a different definition depending on the rotation axis and the 4x4 representation of the three rotation matrices, for each rotation axis (x, y, z), by an angle 𝜃 is:

$$ \begin{align*} R_x = \left\lbrack \matrix{ 1 & 0 & 0 & 0 \cr 0 & \cos\theta_x & -\sin\theta_x & 0 \cr 0 & \sin\theta_x & \cos\theta_x & 0 \cr 0 & 0 & 0 & 1 } \right\rbrack \\ R_y = \left\lbrack \matrix{ \cos\theta_y & 0 & \sin\theta_y & 0 \cr 0 & 1 & 0 & 0 \cr -\sin\theta_y & 0 & \cos\theta_y & 0 \cr 0 & 0 & 0 & 1 } \right\rbrack \\ R_z = \left\lbrack \matrix{ \cos\theta_z & -\sin\theta_z & 0 & 0 \cr \sin\theta_z & \cos\theta_z & 0 & 0 \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1 } \right\rbrack \end{align*} $$

When working with rotations, angles are usually assigned a sign (+/-) to indicate the direction of rotation—either clockwise or counterclockwise. In mathematical conventions, positive angles ($\theta$=+90°) typically represent counterclockwise rotations, while negative angles denote clockwise rotations. In the specific case of Autodesk Maya, positive rotations are counter-clockwise as viewed downward from the axis end position.

Combination

Once it is clear how the basic operations (translation, scale and rotation) can be represented in a 4x4 matrix form, it is now time to combine them. This procedure simply involves a sequence of matrix multiplication operations. The final matrix can be represented as follows:

$$ A = \left\lbrack \matrix{1 & 0 & 0 & t_x \cr 0 & 1 & 0 & t_y \cr 0 & 0 & 1 & t_z \cr 0 & 0 & 0 & 1} \right\rbrack \cdot \left\lbrack \matrix{\cos\theta_z & -\sin\theta_z & 0 & 0 \cr \sin\theta_z & \cos\theta_z & 0 & 0 \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1} \right\rbrack \cdot \left\lbrack \matrix{\cos\theta_y & 0 & \sin\theta_y & 0 \cr 0 & 1 & 0 & 0 \cr -\sin\theta_y & 0 & \cos\theta_y & 0 \cr 0 & 0 & 0 & 1} \right\rbrack \cdot \left\lbrack \matrix{1 & 0 & 0 & 0 \cr 0 & \cos\theta_x & -\sin\theta_x & 0 \cr 0 & \sin\theta_x & \cos\theta_x & 0 \cr 0 & 0 & 0 & 1} \right\rbrack \cdot \left\lbrack \matrix{s_x & 0 & 0 & 0 \cr 0 & s_y & 0 & 0 \cr 0 & 0 & s_z & 0 \cr 0 & 0 & 0 & 1} \right\rbrack $$

In this specific case, if we apply A to an object, the operation that are performe, in order are:

  1. Scaling it by ($s_x$, $s_y$, $s_z$),
  2. Rotating it along the X axis by $\theta_x$,
  3. Rotating it along the Y axis by $\theta_y$,
  4. Rotating it along the Z axis by $\theta_z$,
  5. Translating it with the vector ($t_x$, $y_y$, $t_z$)

The power and efficiency of using matrices is that it is not necessary to store intermediate values after each multiplication. It is possible - and usually preferable - to perform all the sequence of multiplications and then use that final matrix - called the Affine Transformation Matrix - and use it to transform the object accordingly.

Some notes have to be made, though. Even if the intermediate steps are not necessary, the order of multiplications strongly matters. Indeed, givn three matrices A, B, C, that can be any of the previously explained operations:

  • Matrix multiplication is associative: it is true that ABC = (AB)C = A(BC),
  • Matrix multiplication is not commutative in general: it is generally false that AB = BA.

Orient in a 3D environment

Now that some basics of linear algebra have been explained, it is time to explore a little deeper what orienting means in a 3D environment. Orienting refers to setting the rotation and/or alignment of an object relative to a given reference frame or to another object's coordinate system. This means determining how the object is angled or turned along the three axes — X, Y, and Z — and it is obtained by applying one or more transformation matrices with matrix multiplications to change the object's coordinates to the desired values.

There are different details to keep track of:

  1. The aiming axis: local axis that will point to the target object,
  2. The up axis: local axis that has to be considered as "up",
  3. The sign of these axes, that can either be positive or negative - i.e. if it follows or not the right-hand rule,
  4. The world up vector: that defines the "up" direction relative to the world coordinate system or a specified up object.
  5. Source: position of the object to orient,
  6. Target: position of the object to aim at.

Simplifications of the A matrix

Since the goal is to orient an object toward another, the formulation of the A matrix can be simplified:

  • The object remains stationary in space, so the translation components of the Translation Matrix will simply be the object's coordinates in world space.
  • Scaling is not required for this operation, so the Scaling Matrix is omitted.
  • The final rotation matrix is split into two components: the Local Rotation Matrix ($R_{lt}$) and the Change of Basis Matrix ($R_{cb}$). The Local Rotation Matrix handles rotating the object toward the target while maintaining the object's local axis orientation. The Change of Basis Matrix adjusts the coordinate system so the object's aim and up axes align with the world up vector.

Additionally, because this method is designed for Autodesk Maya, the matrices must be Row-Major, while the theory discussed is Column-Major. To maintain consistency between theory and implementation, the $R_{lt}$ and $R_{cb}$ matrices will be represented as their transposes: $R_{lt}^T$ and $R_{cb}^T$, respectively.

Compute the final matrix

The final transformation matrix will be calculated by:

$$ A = R_{cb}^T * R_{lt}^T * \left\lbrack \matrix{1 & 0 & 0 & 0 \cr 0 & 1 & 0 & 0 \cr 0 & 0 & 1 & 0 \cr t_x & t_y & t_z & 1} \right\rbrack = R_{cb}^T * T_{lt}$$

Where $T_{lt}$ is the Local Transformation Matrix, and $R_{cb}^T$ is the transpose of the Change Basis Matrix.

Local Transformation Matrix $T_{lt}$

The computation of the Local Transformation Matrix involves three vectors—an aim vector, an up vector, and a right vector—along with the source object's position. These vectors must be orthogonal to each other and are calculated as follows:

  • Aim Unit Vector ($a_i$): This is the normalized vector pointing from the source to the target, calculated as the difference between the target and the source positions.

$$ \mathbf{aim} = \frac{\mathbf{target} - \mathbf{source}}{\Vert \mathbf{target} - \mathbf{source} \Vert} $$

  • Right Unit Vector ($r_i$): The normalized cross product of the Aim Vector and the World Up Vector, which gives the right direction.

$$ \mathbf{right} = \frac{\mathbf{aim} \times \mathbf{worldup}}{\Vert \mathbf{aim} \times \mathbf{worldup} \Vert} $$

  • Up Unit Vector ($u_i$): The normalized cross product of the Right Vector and the Aim Vector, ensuring the Up Vector's orthogonality.

$$ \mathbf{up} = \frac{\mathbf{right} \times \mathbf{aim}}{\Vert \mathbf{right} \times \mathbf{aim} \Vert} $$

With these vectors computed, the Local Transformation Matrix is constructed as:

$$ T_{lt} = \left\lbrack \matrix{a_1 & a_2 & a_3 & 0 \cr u_1& u_2 & u_3 & 0 \cr r_1 & r_2 & r_3 & 0 \cr t_x & t_y & t_z & 1} \right\rbrack $$

Change Basis Matrix $R_{cb}^T$

When dealing with the Change Basis Matrix, its purpose is to apply rotations in multiples of 90° along one or more axes.This ensures that the $\sin$ and $\cos$ functions will only take values of -1, 0, 1, simplifying the calculations.

Without delving into detailed proofs, which would involve iterating through all possible combinations of aim and up axes, we can conclude that the matrix can be constructed by placing the following vectors into the upper-left 3x3 portion of the matrix:

  • $[\pm1, 0, 0]$ in the row corresponding to the aim axis.
  • $[0, \pm1, 0]$ in the row corresponding to the up axis.
  • $[0, 0, \pm1]$ in the row corresponding to the right axis.

and leaving the other values an an Identity. Keep in mind that the index:axis mapping is: 0:X, 1:Y, 2:Z

Numeric Example

  • Source Object Position (world space): (5, 0, 0)
  • Target Object Position (world_space): (0, 0, 0)
  • World Up Vector: (0, 1, 0)
  • Aim axis: +Y
  • Up axis: +X

We can calculate that:

$$ \begin{align} aim = \frac{\mathbf{target} - \mathbf{source}}{\Vert \mathbf{target} - \mathbf{source} \Vert} = \frac{(0, 0, 0) - (5, 0, 0)}{\Vert (0, 0, 0) - (5, 0, 0) \Vert} = \frac{(-5, 0, 0)}{\Vert (-5, 0, 0) \Vert} = (-1, 0, 0) \\ right = \frac{\mathbf{aim} \times \mathbf{worldup}}{\Vert \mathbf{aim} \times \mathbf{worldup} \Vert} = \frac{(-1, 0, 0) \times (0, 1, 0)}{\Vert (-1, 0, 0) \times (0, 1, 0) \Vert} = (0, 0, -1) \\ up = \frac{\mathbf{right} \times \mathbf{aim}}{\Vert \mathbf{right} \times \mathbf{aim} \Vert} = \frac{(0, 0, -1) \times (-1, 0, 0)}{\Vert (0, 0, -1) \times (-1, 0, 0) \Vert} = (0, 1, 0) \\ \end{align} $$

With the three main vectors calculated, the Local Transformation Matrix $T_{lt}$ becomes:

$$ \begin{align*} T_{lt} = \left\lbrack \matrix{a_1 & a_2 & a_3 & 0 \cr u_1& u_2 & u_3 & 0 \cr r_1 & r_2 & r_3 & 0 \cr t_x & t_y & t_z & 1} \right\rbrack = \left\lbrack \matrix{-1 & 0 & 0 & 0 \cr 0 & 1 & 0 & 0 \cr 0 & 0 & -1 & 0 \cr 5 & 0 & 0 & 1} \right\rbrack \\ \end{align*} $$

The Change Basis Matrix will have:

  • $[1, 0, 0]$ in the second row, since the aim axis is +Y.
  • $[0, 1, 0]$ in the first row corresponding, since the up axis is +X.
  • $[0, 0, -1]$ in the third row since $aim \times up = (-1, 0, 0) \times (0, 1, 0) = (0, 0, -1) $.

$$ R_{cb}^T = \left\lbrack \matrix{0 & 1 & 0 & 0 \cr 1 & 0 & 0 & 0 \cr 0 & 0 & -1 & 0 \cr 0 & 0 & 0 & 1} \right\rbrack $$

and the final transformation matrix is:

$$ A = R_{cb}^T * T_{lt} = \left\lbrack \matrix{0 & 1 & 0 & 0 \cr 1 & 0 & 0 & 0 \cr 0 & 0 & -1 & 0 \cr 0 & 0 & 0 & 1} \right\rbrack * \left\lbrack \matrix{-1 & 0 & 0 & 0 \cr 0 & 1 & 0 & 0 \cr 0 & 0 & -1 & 0 \cr 5 & 0 & 0 & 1} \right\rbrack = \left\lbrack \matrix{0 & 1 & 0 & 0 \cr -1 & 0 & 0 & 0 \cr 0 & 0 & 1 & 0 \cr 5 & 0 & 0 & 1} \right\rbrack $$

import logging
from maya.api import OpenMaya
def create_transformation_matrix(source_position, target_position,
aim_index: int = 0, aim_negative: bool = False,
up_index: int = 1, up_negative: bool = False,
world_up_vector=None):
"""
Method which computes the transformation matrix to apply to an object in 'source position' to aim at
another object placed at target position.
The aim index identifies which axis has to 'point' to the target and the up index the axis which needs to
point to the world up. To use their negative values, use the corresponding parameter.
In case aim and up index are set as the same, the aim axis is kept, while the up index is set to the
next available axis according to the map below.
The axis can be specified with the following map:
- 0: X axis
- 1: Y axis
- 2: Z axis
@param source_position: Position of the source object.
@param target_position: Position of the target object - the one to aim.
@param aim_index: Index of the axis pointing to the target object. 0: X. 1: Y, 2: Z. Default X.
@param aim_negative: True if the aim axis is negative. Default False.
@param up_index: Index of the up axis of the source object to maintain. 0: X. 1: Y, 2: Z. . Default Y.
@param up_negative: True if the up axis is negative. Default False.
@param world_up_vector: Vector identifying the world up to consider. Default the positive Y: [0, 1, 0].
@return: The transformation matrix to orient the source object.
"""
if aim_index > 2:
logging.warning(f"Aim axis index {aim_index} is not valid. Valid values are: 0: X. 1: Y, 2: Z")
return
if up_index > 2:
logging.warning(f"Up axis index {up_index} is not valid. Valid values are: 0: X. 1: Y, 2: Z")
return
if not world_up_vector:
world_up_vector = OpenMaya.MVector([0, 1, 0])
# Direction vector
tgt_vec = OpenMaya.MVector(target_position)
src_vec = OpenMaya.MVector(source_position)
aim_vector = (tgt_vec - src_vec).normal()
# Aim cross Up
right_vector = (aim_vector ^ world_up_vector).normalize()
# Right cross Aim
up_vector = right_vector ^ aim_vector
# Transformation matrix
# Syntax: matrix.setElement(row, column, value)
transformation_matrix = OpenMaya.MMatrix()
[transformation_matrix.setElement(0, index, aim_vector[index]) for index in range(3)]
[transformation_matrix.setElement(1, index, up_vector[index]) for index in range(3)]
[transformation_matrix.setElement(2, index, right_vector[index]) for index in range(3)]
[transformation_matrix.setElement(3, index, source_position[index]) for index in range(3)]
# In case aim index and up index are the same, keep the aim and set the next up
if aim_index == up_index:
logging.warning("Aim axis and up axis are equal, choosing the next available axis for up X -> Y -> Z -> X")
up_index = (up_index + 1) % 3
# Computing the sign of the aim and up axos
aim_sign = -1 if aim_negative else 1
up_sign = -1 if up_negative else 1
# Computing the aim and the up axis
aim_axis = [0, 0, 0]
up_axis = [0, 0, 0]
aim_axis[aim_index] = 1 * aim_sign
up_axis[up_index] = 1 * up_sign
# Computing the right axis and the entry index in the change basis matrix
right_axis = [0, 0, 1 * sum((OpenMaya.MVector(aim_axis)) ^ (OpenMaya.MVector(up_axis)))]
right_index = next(row for row in [0, 1, 2] if row != aim_index and row != up_index)
# Change basis matrix
change_basis_matrix = OpenMaya.MMatrix()
[change_basis_matrix.setElement(aim_index, index, value) for index, value in enumerate([1 * aim_sign, 0, 0])]
[change_basis_matrix.setElement(up_index, index, value) for index, value in enumerate([0, 1 * up_sign, 0])]
[change_basis_matrix.setElement(right_index, index, right_axis[index]) for index in range(3)]
# Final transformation matrix
final_transformation_matrix = change_basis_matrix * transformation_matrix
return final_transformation_matrix
source_position = cmds.xform(source_obj, query=True, translation=True, worldSpace=True)
target_position = cmds.xform(target_obj, query=True, translation=True, worldSpace=True)
aim_index = 1
up_index = 0
aim_negative = False
up_negative = True
# Compute and apply the transformation matrix
translation_matrix = math3d.create_transformation_matrix(
source_position, target_position, aim_index, aim_negative, up_index, up_negative
)
cmds.xform(source_obj, matrix=list(translation_matrix), worldSpace=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment