Using the nuke.math Python module to do Vector and Matrix operations

Written by Ivan Busquets on .

3. Matrix methods


Matrix3:


The Matrix3 class defines a 3x3 transformation matrix and methods to operate with that data, as well as methods to transform vectors
You multiply a Vector by one of these to go from one 3D space to another. This cannot represent 3D translations or perspective.· A Matrix4 should be used for that.


makeIdentity(...)

Syntax: Matrix3.makeIdentity() -> none :

Description: Replaces the contents of the matrix with the identity. The identity is the no-transformation matrix

Example:

 

Using makeIdentity() to initialize a matrix to the identity
1
2
3
4
5
6
7
8
9
10
11
m = nuke.math.Matrix3()
print m
 
# Result: {7.82714e-25, 2.68728e-26, 7.81665e-25, 2.68732e-26, 2.36558e+20, 0, 0, 1.74133e-37, 0}
# This is a matrix with non-initialized values
 
m.makeIdentity()
print m
 
# Result: {1, 0, 0, 0, 1, 0, 0, 0, 1}
# Now we have the identity matrix

 


transform(...)

Syntax: Matrix3.transform(Vector3) -> Vector3 :

Description: Transforms a Vector3 object using this matrix. This is the same as using the multiply operator (matrix * vector). The method returns a new Vector3 object.

Example:

 

Applying a Matrix to a Vector3 using transform()
1
2
3
4
5
6
7
matrix = nuke.math.Matrix3()
matrix.rotationX(math.pi/2) # new matrix with a 90 degrees rotation about the X axis
vector = nuke.math.Vector3(0,1,0) # new vector looking up towards +Y
transformedVector = matrix.transform(vector)
print transformedVector
 
# Result: {0, -4.37114e-08, 1} # Transformed vector (looking towards +Z)

 


determinant(...)

Syntax: Matrix3.determinant() -> float :

Description: Returns the determinant of the matrix. The determinant is a real number (not another matrix) that can be used for characterizing the matrix. A matrix is said to be singular when its determinant is 0. In other words, if the determinant is 0, a matrix cannot be inverted.


inverse(...)

Syntax: Matrix3.inverse() -> Matrix3 :
 

Description: Returns inverse of the incoming matrix. Note that, unlike other Vector and Matrix methods, this doesn't modify the matrix object· in-place, but returns a new matrix object. The returned matrix will be such that, when multiplied with the original matrix, will equal to the identity matrix (no transformation). This can be useful to jump between different space coordinates (world space, object space, view space...)

Example:

 

Inverting a matrix
1
2
3
4
5
6
7
8
matrix = nuke.math.Matrix3()
m.rotationX(math.pi/2) # rotate matrix 90 degrees about the X axis
inv = m.inverse()
 
print m * inv
 
# Result: {1, 0, 0, 0, 1, 0, 0, 0, 1}
# Multiplying a matrix by its inverse gives the identity matrix back

 


scaling(...), rotation(...), rotationX(...), rotationY(...), rotationZ(...)

Syntax: Multiple signatures for each method. Check the help of the nuk.math.Matrix3 Class for more details.

Description: These are all methods that REPLACE the contents of a matrix object with a scaling or a rotation matrix. These are useful if you just want to quickly set up a matrix that does a certain scale or rotation transformation. However, if you need to build up your matrix by concatenating multiple transformations, you'll need to use the next set of methods, which modify the contents of the matrix object instead of replacing them.

Example:

 

Setting a rotation / scaling Matrix3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
v = nuke.math.Vector3(0,1,0) # Unit vector looking up towards +Y
m = nuke.math.Matrix3()
 
m.scaling(15) # Replaced matrix with a scaling of x15
 
print m * v # transform vector with matrix (matrix * vector)
 
# Result: {0, 15, 0} # Original vector scaled by 15
 
m.rotationX(math.radians(90)) # Replaced matrix with a rotation of 90 degrees about the X axis
 
print m * v
 
# Result: {0, -4.37114e-08, 1} # Original vector rotated. Now looking towards +Z

 

scale(...), rotate(...), rotateX(...), rotateY(...), rotateZ(...), skew(...)

Syntax: Multiple signatures for each method. Check the help of the nuk.math.Matrix3 Class for more details.
 

Description: These methods take the existing matrix and modify it by adding a scale, rotate, or skew transformation to it.

Example:

 

Adding scale / rotation transformations to a Matrix3
1
2
3
4
5
6
7
8
9
10
11
m = nuke.math.Matrix3()
m.makeIdentity()
m.scale(15)
m.rotateX(math.radians(90))
 
v = nuke.math.Vector3(0,1,0)
 
print m * v
 
# Result: {0, -6.55671e-07, 15} # Vector is now scaled and rotated
 



Matrix4:


The Matrix4 Class defines a 4x4 transformation matrix and methods to operate with that data, as well as methods to transform vectors.
Although it can also hold the same kind of transformations as a Matrix3 , this is most commonly used to represent perspective transforms, such as the one that transforms the view of a perspective camera into screen-space coordinates.


projection(...)

Syntax: Matrix4.projection(float lens, float minz, float maxz, bool perspective) -> none :
 

Description: Replace the contents with a camera projection. This creates a matrix that matches the projection of a camera sitting at 0,0,0 and pointing along the Z axis. The 'lens' parameter should be the ratio of the camera's focal length to the horizontal aperture of the film.
The area viewed by this camera is transformed to be in a square with X and Y ranging from -1 to 1. The plane Z==minz is transformed to be at Z==-1 and the plane Z==maxz is transformed to Z==1.
The 'perspective' parameter is a boolean that defines whether this is a perspective projection (True) or an orthographic / parallel projection (False)

Example:

Building a projection matrix and transforming a 3D point to 2D coordinates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Projection variables
focal = 50.0
h_aperture = 24.576
near = 0.1
far = 10000
 
#Image format and aspect. In this example, the format is 2K Super 35 (2048x1556)
format = nuke.root()['format'].value()
imageAspect = float(format.height()) / float(format.width())
pixelAspect = float(format.pixelAspect())
 
# Build projection matrix using projection variables
projectionMatrix = nuke.math.Matrix4()
projectionMatrix.projection(focal/h_aperture, near, far, True)
 
# Create a new Vector4 to transform through the matrix
originalVector = nuke.math.Vector4(5, 5, -36,1)
 
# And transform it
transformedVector = projectionMatrix.transform(originalVector) # this is the same as projectionMatrix * originalVector
 
# Because this is a perspective projection, we'll need to divide each component of the resulting vector by its w component
# if we want to get a normalised output
transformedVector /= transformedVector.w
 
# At this point we have a transformed vector mapped to a screen window that goes from -1,-1 to 1,1
# We'll need to normalize that first so it's in a 0,0 to 1,1 window
print (1-(1-imageAspect/pixelAspect))
transformedVector.x = (transformedVector.x + 1) / 2
transformedVector.y = (transformedVector.y + (1-(1-imageAspect/pixelAspect))) / 2
 
transformedVector.x *= float(format.width()) # We multiply everything by width because the image aspect was already accounted in the previous step
transformedVector.y *= float(format.width()) * float(pixelAspect)
 
print transformedVector.x, transformedVector.y
 
# Result: 1602.70361328 1356.70361328

translate(...)

Syntax: Matrix4.translate(float x, float y, float z) -> none

 :
              Matrix4.translate(Vector3) -> none :

Description: Adds a translation in x,y and z to the transformation matrix. Modifies the matrix in-place. x,y and z can be fed to the translate method as three separate arguments, or by using a Vector3 object containing the translation vector.


translation(...)

Syntax: Matrix4.translation(float x, float y, float z) -> none

 :
             Matrix4.translation(Vector3) -> none :

Description: Same as translate(), but this fully replaces the contents of the matrix. So, translate() would add a translation to the existing matrix, keeping any other transformations in it, whereas translation() completely replaces the contents of the matrix with a new matrix holding this translation only.


transform(...), ntransform(...), vtransform(...)

Syntax: Matrix4.transform(Vector3) -> Vector3
 :
              Matrix4.transform(Vector4) -> Vector4
 :
              Matrix4.ntransform(Vector3) -> Vector3 :
             Matrix4.vtransform(Vector3) -> Vector3 :

Description: As in the Matrix3 transform method, this will apply a the transformation of the matrix to a Vector object, and return a new Vector. The special ntransform and vtransform are convenience methods aimed at transforming normals and vectors respectively.
Here's the main differences between these methods:


Matrix4.transform(Vector4) -> This is the same as multiplying the matrix with a Vector4. Applies the transformation to a point, and returns the new point as a Vector4.

Matrix4.transform(Vector3) -> Takes the Vector3 and treats is as a Vector4 with a w value of 1. Returns a new Vector3.

Matrix4.vtransform(Vector3) -> Transforms Vector3 as a direction vector, and not as a point, by treating the Vector3 as a Vector4 with a w value of 0. In practice, this means if the matrix has any translations, those are not applied. Returns a new Vector3.

Matrix4.ntransform(Vector3) -> Transforms Vector3 as a normal. This is the same as doing matrix.transpose().vtransform(Vector3). To get the correct transformation for normals, you'll need to use the inverse of your matrix too (normals transformation is done by multiplying the normal vector by the transpose of the inverse of a matrix. If this sounds confusing, read the following link to learn more about transforming normals.

http://www.unknownroad.com/rtfm/graphics/rt_normals.html


transpose(...)

Syntax: Matrix4.transpose() -> none :

Description: Replaces the contents of the Matrix4 with the transposed matrix. This is a matrix with its values reflected through the diagonal or, in other words, the matrix obtained by exchanging thw rows and columns of the original matrix. This can be used to find whether a square matrix (3x3, 4x4) is symetric, since the transpose of a symetric matrix is equal to the original matrix.


scaleOnly(...), rotationOnly(...), translationOnly(...), scaleAndRotationOnly(...)

Syntax: Matrix4.scaleOnly(), Matrix4.rotationOnly(), etc. -> none :

Description: Modifies the transformation matrix to represent only certain components (scale, rotation, translation, scale and rotation). This is useful if you only care about one set of transformations, and want to ignore all others.

Example:

 

Getting only the translation component of a Matrix4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
matrix = nuke.math.Matrix4()
matrix.translation(1,2,3) # translation replaces the contents of the matrix with a matrix
# that does a transform of x = 1, y = 2, z = 3
 
print matrix # at this stage, the matrix contains translation info only
 
# Result: {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1}
 
matrix.rotateX(math.pi/2) # rotate the matrix 90 degrees rotation about the X axis
matrix.rotateY(math.pi/2) # rotate the matrix 90 degrees rotation about the Y axis
matrix.rotateZ(math.pi/2) # rotate the matrix 90 degrees rotation about the Z axis
matrix.scale(5) # scale the matrix by 5
 
print matrix # the matrix now contains both translation, rotation, and scale
 
#Result: {9.55343e-15, -4.37114e-07, 5, 0, 2.18557e-07, -5, -4.37114e-07, 0, 5, 2.18557e-07, 9.55343e-15, 0, 1, 2, 3, 1}
 
matrix.translationOnly() # Modify the matrix so it only holds translation
print matrix # now we should have the same matrix we had in line 35
 
# Result: {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1}

 


rotationsZXY(...)

Syntax: Matrix4.rotationsZXY() -> tuple :

Description: If this is a rotation-only matrix (you might want to use the rotationOnly method first if it's not), this returns a tuple containing the equivalent rotations around the X, Y and Z axis. This is usually regarded as Matrix to Euler rotations, and becomes useful when you need to set the rotate values in a 3D node to match the rotations of a certain matrix. Note that the returned angles are in radians, so you'll need to convert them to degrees if you use them on a rotate knob. Also note that the returned rotations can be used to match the rotation of the matrix only when the rotation order of your node is set to 'ZXY'.

Example:

Finding Euler rotation angles from rotation-only Matrix4
1
2
3
4
5
6
7
8
9
10
matrix = nuke.math.Matrix4()
matrix.makeIdentity()
 
matrix.rotateY(1)
matrix.rotateX(0.75)
matrix.rotateZ(0.5)
 
print matrix.rotationsZXY()
 
# Result: (0.75, 1.0, 0.5)

 

As you can see from the example above, if you were to re-build the matrix by manually rotating it about X, Y and Z, you would need to do the rotations in the inverse order. So, if the tuple returned by rotationsZXY() is (x,y,z), to rebuild the matrix from those rotations you would need to do rotateY(y), then rotateX(x) and finally rotateZ(z) to get the matrix that does the same transformation.


mapUnitSquareToQuad(...)

Syntax: Matrix4.mapUnitSquareToQuad(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) -> none :

Description: Replaces the Matrix4 with a cornerpin-like transformation. This builds a 4x4 matrix describing a transformation such that the corners of a 1-unit square (0,0,1,1) get mapped to the 4 corners fed into the function.

Example:

Creating a matrix to match the transformation of a CornerPin2D node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cornerPinMatrix = nuke.math.Matrix4()
node = nuke.toNode('CornerPin2D2')· # Replace with the node object of a cornerPin node of your choice
 
# Get image width and height to normalize the format to a 0,0,1,1 square
imageWidth = float(node.width())
imageHeight = float(node.height())
 
# Get a list of Vector2 objects for each of the 'to' knobs. Divide by width and height to get the normalized screen coordinates (0-1)
vectors = [nuke.math.Vector2(node[f].value()[0]/imageWidth, node[f].value()[1]/imageHeight) for f in sorted(node.knobs().keys()) if f.startswith('to')]
 
# Feed all 4 coordinates into the mapUnitSquareToQuad() function
cornerPinMatrix.mapUnitSquareToQuad(vectors[0].x, vectors[0].y, vectors[1].x, vectors[1].y, vectors[2].x, vectors[2].y, vectors[3].x, vectors[3].y)
 
# Test the matrix
screenCenter = nuke.math.Vector4(0.5,0.5,0,1) # Define a point at the center of the screen
# Using Vector4 since the matrix will do
# the equivalent of a perspective projection
 
transformedPoint = cornerPinMatrix.transform(screenCenter)
 
transformedCoordinates =· ((transformedPoint.x/transformedPoint.w)*imageWidth, (transformedPoint.y/transformedPoint.w)*imageHeight)
 
print transformedCoordinates
 
# Result: (280.20587098794442, 304.4264662581848)· # The center of the original image should
# be mapped to this pixel coordinate.
# Check against your CornerPin to verify

mapQuadToUnitSquare(...)

Syntax: Matrix4.mapQuadToUnitSquare(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) -> none :

Description: Replaces the Matrix4 with a cornerpin-like transformation. This is the reverse of the method described above. It builds a 4x4 matrix describing a transformation such that the 4 corners fed into the function are mapped to the corners of a 1-unit square (0,0,1,1).


xAxis(...), yAxis(...), zAxis(...)

Syntax: Matrix4.xAxis() -> Vector3 :
              Matrix4.yAxis() -> Vector3 :
              Matrix4.zAxis() -> Vector3 :

Description: Returns a Vector3. If the Matrix4 is not a persxpective matrix (bottom row must be 0,0,0,1), the Vector3 object represents the transformation of a 1-unit vector along the respective axis (x for xAxis, y for yAxis...). This is a quick and convenient useful to check the transformation of such vectors without having to create them first and multiply them by the matrix.

 

Comments   

 
+1 # Frank Rueter 2010-09-14 15:09
awesome tutorial! Thanks so much for sharing and putting in that much effort!
 
 
0 # Carlos Trijueque 2010-09-16 00:55
Ivan, thanks for sharing this terrific piece of info.Very helpful and, as your other tutorials, very well written and explainied.
 
 
0 # Michael Garrett 2010-09-17 11:29
Brilliant! Good info on nuke.math is long overdue!
 
 
+1 # Ivan Busquets 2010-09-17 12:06
Thanks for all the comments, guys!

Yes, Michael, completely agree. It's one of those I had to learn the hard way, so I figured it would be useful to share. Plus, I learnt a bit more while writing it. Glad you liked it. ;-)
 
 
0 # Pete O'Connell 2010-09-20 19:33
Thanks Ivan, that is one of the most awesome tutorials I have ever read!
 
 
0 # Zach Lewis 2011-08-06 10:12
Wanted to chime in and say that this is awesome. A lot of care went into preparing this, and it's much appreciated.
 
 
0 # Diego Piccinato 2011-09-08 10:53
Awesome stuff, thanks Ivan!
 
 
0 # Marco Leone 2013-08-24 00:25
Hey Ivan, thanks a lot for the tutorial, it opened my mind!!!

one quick question thou, I was testing out some of those function on Matrix and I found a little problem.

What I am trying to do is to get the XYZ rotation( in degrees ) from the world matrix of a Camera.

Following you tutorial everythign works fine but the final result of the rotation is a bit different from the original rotation.

what I am doing is really simple :

########################################

import math

matrix = nuke.math.Matri x4() #creating a matrix

matrixValues = nuke.toNode('Ca mera1')['world_ matrix'].getVal ue() # getting the value from the camera world matrix

print "matrixValues :", matrixValues

### adding the value from the camera to the matrix just created ###

for VAL in matrixValues :
print "VAL",VAL
matrix[matrixVa lues.index(VAL) ] = VAL



matrix.rotationOnly() #getting sure I have only the rotation values

print math.degrees(ma trix.rotationsZ XY()[0])
print math.degrees(matrix.rotationsZXY()[1])
print math.degrees(matrix.rotationsZXY()[2])

### check if values r the same

if nuke.toNode('Ca mera1')['rotate '].value(0) == math.degrees(ma trix.rotationsZ XY()[0]) : print True
else: print False


########################################


as u may see it seems that there is an approximation and we loose the last 5 digits on every value when we are adding the worldMatrix of the camera to our Matrix4, and that may be causing the little difference in the result.

Do you know by chance if that is something I did wrong or maybe if there is a work around it ?
thanks for your time
 

You have no rights to post comments

We have 3855 guests and 102 members online