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

Written by Ivan Busquets on .

Nuke has its own python math module (nuke.math) that provides support for Matrix, Vector, and Quaternion classes, and useful functions to operate with them.  If you need to do any maths involving vector operations in Nuke (like adding or substracting vectors, transforming vectors with transformation matrices, calculating vector reflections, inverse-projecting a 3d vector into 2d screen coordinates, etc), then the nuke.math module can be of great help.

Unfortunately, at this time there is no way to write out pixel data using python in Nuke, so the use of this module will be rather limited. However, it can still prove very useful to do any kind of vector and matrix operations between knobs, 3d objects, and even on sampled pixels (just not writing them out, unfortunately)

The documentation is a bit scarce on the nuke.math module at the moment, and the little there is is mostly a direct port from the same classes in C++. When first facing this module, I found myself having to learn how to use it the hard way, so here's some notes, examples, tips and extended documentation on the nuke.math module, hoping it will make it a bit more accessible to anyone trying to use it for the first time.

Note: Part of the descriptions of some classes and methods is taken from the C++ Plugin development documentation, but I've tried to expand a little more to give a better insight on what the different methods can be used for.

# 1. Creating Vector and Matrix objects:

Vector objects in the nuke.math module are convenience classes that store a set of attributes (x,y for a Vector2; x,y,z for a Vector3; x,y,z,w for a Vector4) and provide specific methods for each class to perform common vector operations (vector length, normalize, distance between vectors, dot product...) Matrix objects can be either of the Matrix3 or Matrix4 kind. Matrix3 defines a 3x3 transformation matrix, which can describe a translation, rotation and scale transforms. Matrix4 holds a 4x4 transformation matrix, most commonly used to represent perspective transforms. First, let's have a look at how to create new objects/instances of those classes. It's a good habit to check the documentation of any new Python class/method we want to use. So, let's pull the help for the Vector3 Class, for example:

help(nuke.math.Vector3)

And look under its constructor method, to see how an instance of the Vector2 class can be initialized (the constructor is the protected method called __init__):

| __init__(...)
|····· __init__( (object)arg1) -> None :
|····· ...
|····· __init__( (object)arg1, (float)arg2, (float)arg3, (float)arg4) -> None :
|····· ...
|····· __init__( (object)arg1, (Vector3)arg2) -> None :
|····· ...

This is telling us there's 3 ways a Vector3 object can be constructed. If you ignore the first argument (object)arg1, which is just the object we're constructing or 'self', we are left with the following three possible syntaxes to create a new a Vector3. I'll use the Vector3 Class as an example to explain the different ways to create one, but the same methods apply to Vector2 and Vector4 classes. They also apply to Matrix classes, with a small exception that will be covered later.

## Creating a new Vector3 instance with no arguments:

##### Syntax: v = nuke.math.Vector3()

This will create a Vector3 object with NON INITIALIZED values. The values/attributes of a Vector3 object are the x,y,z coordinates. Constructing a Vector 3 this way does NOT set x,y and z to 0. The short explanation is that the whole nuke.math is just a hook to the C++ Vector and Matrix Classes, and in C++, a variable that's not been initialized will be assigned a space in memory, but that space will not be set to any given value until the variable is initialized. So, until then, the value will be unpredictable (it will be the value previously stored at that memory position).
Let's see this in a practical example:

 Vector3 with non-initialized values 12345 v1 = nuke.math.Vector3()v2 = nuke.math.Vector3()print v1, v2 # Result: {1.79998e-37, 1.78e-37, 2.73861e-26} {0, 3.7807e-11, 0}

See how both new Vector3 instances have different values assigned to their x,y and z attributes. This is because they were not initialized in the constructor. You could easily fix that by using the 'set' method available for all Vector instances.
Like this:

 Setting values of a Vector3 after creation 12345 v1 = nuke.math.Vector3()v1.set(1,2,3)print v1 # Result: {1,2,3}

So, generally, make sure you only use this method when you know you're going to need a Vector3, don't know what x, y and z values it needs to have yet, but you will set them yourself later. Otherwise, if you want to avoid unexpected behaviour from non-initialized attributes, give the initial values yourself when creating a new Vector instance, as explained in the following method.

## Creating a new Vector3 instance directly setting its arguments:

##### Syntax: v = nuke.math.Vector3(float, float, float)

This will create a Vector3 object with its 3 attributes (x, y and z) initialized to the value of our choice.
Example:

 New Vector3 with initialised values 1234 v1 = nuke.math.Vector3(1, 2, 3)print v1 # Result: {1, 2, 3}

## Creating a new Vector3 instance from an existing Vector3:

##### Syntax: v = nuke.math.Vector3(nuke.math.Vector3)

The following code will create a new Vector3 instance (v2) taking the values from an existing instance of the same class (v1)

 New Vector3 copying values from another Vector3 12345 v1 = nuke.math.Vector3(0,0,0)v2 = nuke.math.Vector2(v1)print v1, v2 #Result: {0, 0, 0} {0, 0, 0}

Why would we want to do this, and not just do v2 = v1?

Well, we could, but then we would have two variables referencing the same Vector3 instance. This is because instances (objects) of the Classes defined in the nuke.math module are mutable objects, and therefore work in a similar way to python lists (which are also a mutable type, unlike numbers, strings or tuples) in that they can be modified 'in place'. Another important implication of this is the way we can copy a Vector or Matrix instance into a new variable/instance. Because this will become important later on when explaining some of the Vector and Matrix methods, let me open a parenthesis and show some of the differences between mutable and immutable types using some of the Python built-in types as an example. If you are already familiar with these concepts, feel free to skip the following framed section:

Immutable vs Mutable types in Python

Most python guides make a distinction between mutable and immutable types. Numbers, strings and tuples are immutable, meaning they cannot be modified (or at least not directly, in-place), whereas lists and dictionaries are mutable types, meaning they can be modified in-place. Here's a couple of practical examples to show the difference:

##### "Modifying" an int (which is actually doing an assignment to a new int)
 123456 a = 3b = aa += 1print a, b #Result: 4, 3

What happens when you do "a += 4" is a new assignment. We are not modifying the original number, because number types are immutable. Instead, python creates a new instance of type 'int' and assigns its value to the variable, effectively loosing the previous reference 'a' was pointing to. So, by the time we print "a" and "b", "a" and "b" already point to two different objects.

##### Modifying a list:
 123456 a = [1, 2, 3]b = aa.append(4)print a, b # Result: [1, 2, 3, 4] [1, 2, 3, 4] # both a and b are still pointing to the same list

Here, because lists are mutable, the result is different. Like in the previous example, an instance is created (of the type 'list') and assigned to a variable called "a". Then The same list is assigned to variable "b". Both a and b point to the same instance. Then a.append(4) tries to modify the list instance referenced by variable a. Because lists are mutable and can be modified in place, this is a valid operation and the original instance is modified (a new item '4' is appended to it). "a" and "b" are still pointing to the same list, therefore printing their content gets us the same list for both "a" and "b".

So, the method described above allows us to make a fully independent copy of an existing Vector3. If we had attempted to create v2 with just a simple assignment, here's what would have happened:

 123456 v1 = nuke.math.Vector3(0,0,0)v2 = v1v2.set(1,2,3)print v1, v2 # Result: {1,2,3} {1,2,3} (v1 and v2 are actually pointing to the same Vector3 instance)

Keep this in mind every time you want to have a copy of your Vector or Matrix objects without altering the original.

## Creating Matrix3 and Matrix4 objects:

Creating Matrix objects works the same way as creating Vector objects. The constructor also has the three different variants explained before, except in the case of the Matrix4 class. Matrix4 objects hold the definition of a 4x4 matrix, which is a total of 16 float attributes. Problem is, the nuke.math module was written using Boost.Python, which has a limit of 15 arguments that can be passed to a function. For that reason, you cannot create a new Matrix4 instance by dirtectly passing it all its values. However, there's ways to work around that. Here's an example on how we can create a Matrix4 with all 16 values, having them stored in a list.

 New Matrix object from values stored in a list 1234567 matrixValues = [3039.75, 176.404, 0.155272, 0.155268, -124.794, 2800.11, -0.121872, -0.121869, -547.587, -1113.77, -0.980346, -0.980326, 4755.08, 509.823, 8.00838, 8.20822]myMatrix = nuke.math.Matrix4()for val in matrixValues:myMatrix[matrixValues.index(val)] = valprint myMatrix # Result: {3039.75, 176.404, 0.155272, 0.155268, -124.794, 2800.11, -0.121872, -0.121869, -547.587, -1113.77, -0.980346, -0.980326, 4755.08, 509.823, 8.00838, 8.20822}

The code above is creating a new, empy Matrix4 instance (with no initialized values). Then we iterate over each value in our list and assign it to the same index of our Matrix4 instance. Like lists, Matrix4 objects can be treated as an array of values, and we can make assignments to single indices of that array. (ex. myMatrix[3] = 0.0).

Additionally, the nukescripts.snap3d module (Nuke 6.1 only)  offers a very convenient function to create a Matrix4 from the knob values of any camera in Nuke. It takes a camera node object as the only argument (we could use the selected node, or nuke.toNode(nodename) to access a camera by name, or any other function that returns a camera node object). The function returns a Matrix4 object. Here's an example where we can create a Matrix4 object out of a selected camera node in Nuke, and assign it to a variable called myMatrix:

myMatrix = nukescripts.snap3d.cameraProjectionMatrix(nuke.selectedNode())

# 2. Vector methods

Here's where the fun starts. I will go through some of the most useful methods available in the Vector and Matrix classes defined in nuke.math. This should give a basic idea of what can be done with them, as well as help understand how to use them. Note: for a complete list of the methods available for each class, use "dir(nuke.math.className)" and "help(nuke.math.className)".

## Vector2:

Vector2 is a Class that defines a bi-dimensional vector. That is, a vector that's defined by two coordinates, x and y. This is usually used to represent a point/vector in the plane. Here's a few useful methods available for Vector2 objects:

##### distanceBetween(...)

Syntax: Vector2.distanceBetween(Vector2) -> float :

Description: Returns the distance between two 2D points. This would also be the hypotenuse of the right triangle that can be described by those 2 points.

Example:

 Finding the distance between two 2D points 123456 pointA = nuke.math.Vector2(50,100)pointB = nuke.math.Vector2(150, 175)distance = pointA.distanceBetween(pointB)print distance # Result: 125.0
##### distanceSquared(...)

Syntax: Vector2.distanceSquared(Vector2) -> float :

Description: Returns the suare of the distance between points. If the equation for the distance between points is sqrt( (A.x-B.x)^2 + (A.y-B.y)^2), this function just avoids the square root, meaning it's faster to compute. This won't make any difference for a small number of operations, but this method can be faster if you're looping through a large number of vectors and don't need the exact distance. For example, when you only want to 'compare' the distance between many points and another point, to find out which one is the closest.

##### dot(...)

Syntax: Vector2.dot(Vector2) -> float :

Description: Returns the dot product of two vectors. This is a scalar value that represents how much the two vectors are looking in the same direction. This can be used for basic lighting operations (check how much a surface normal is facing a light source), or any kind of operation that involves checking the angle between two vectors.

Example:

 Find the angle between vectors A and B 123456789101112131415 import math # we'll need this for the arc cosine function, and to convert radians to degrees vectorA = nuke.math.Vector2(50,100)vectorB = nuke.math.Vector2(150, 175) # We need both vectors to be 1-unit length so that the dot product between them# equals the cosine of the angle between them, so we normalize A and B vectorA.normalize()vectorB.normalize()dotProduct = vectorA.dot(vectorB)angle = math.degrees(math.acos(dotProduct)) # 'A' dot 'B' = cos(angle), so angle = acos('A' dot 'B')print angle # Result: 14.0362473503
##### length(...)

Syntax: Vector2.length() -> float :

Description: Returns the magnitude of the Vector as a float. This is the same as the distance of the vector from the origin.

Example:

 Length of a 2D vector 1234 vectorA = nuke.math.Vector2(15,46)print vectorA.length() # Result: 48.3838806152
##### lengthSquared(...)

Syntax: Vector2.length() -> float :

Description: Returns the squared magnitude of the Vector as a float. Just like in the case of distanceSquared, this is just a faster method to compute, which may be useful if all you want to do is compare the lengths of many vectors to see which one has the biggest or smallest magnitude.

##### normalize(...)

Syntax: Vector2.normalize() -> float :

Description: Returns the magnitude of the Vector as a float, and converts the vector to a unit vector (by dividing each one of its components by the magnitude). So, this returns the same as Vector2.length(), and also divides the vector by the result. Note that this modifies the vector in-place, so running this method on a vector object actually modifies the object.

Example:

 Length vs normalize methods 12345678910111213141516 vectorA = nuke.math.Vector2(15,46)print vectorA.length()· # This should return the same as vectorA.normalize() # Result: 48.3838806152 print vectorA/vectorA.length()· # This should the normalized version of vectorA # Result: {0.310021, 0.95073} print vectorA.normalize() # This is the same as .length(), but also modifies vectorA # Result: 48.3838806152 print vectorA # This is the already normalized vector # Result: {0.310021, 0.95073}

## Vector3:

Vector3 is a Class that defines a 3-dimensional vector, with coordinates x, y and z. This is usually used to represent a point/vector in 3d space. The Vector3 Class has all the methods available in Vector2, plus a few additional ones:

##### cross(...)

Syntax: Vector3.cross(Vector3) -> Vector3 :

Description: Returns the a Vector3 object that's the cross product between two vectors. One of the definitions possible definitions for this is that the cross productof two vectors produces a third vector which is perpendicular to the plane in which the first two lie. If you imagine the standard viewer handles of an Axis, the Y vector would be the cross product of X and Z, X would be the cross product of Y and Z, etc. One of the most common uses of the cross product (in computer graphics) is to find the normal of a plane defined by two points

Example:

 Using cross product to find the surface normal of a plane 123456 vectorA = nuke.math.Vector3(1,0,0) # Unit vector along the X axisvectorB = nuke.math.Vector3(0,1,0) # Unit vector along the Y axisvectorC = vectorA.cross(vectorB)print vectorC # Result: {0, 0, 1} # Unit vector along the Z axis -> normal vector of the XY plane

##### distanceFromPlane(...)

Syntax: Vector3.distanceFromPlane(float A, float B, float C, float D) -> Float :

Description: Returns the distance of a Vector3 to a plane defined by 4 float values, where each value corresponds to A, B, C and D from the plane equation Ax +By + Cz +D = 0. You can easily derive the values of that equation if you only know a point in the plane and its surface normal (like you would get from a card's translate and rotate values).

The example below assumes you already know A,B,C and D values that define the plane, but you can check one of the examples at the end of this tutorial to see how to get A, B, C and D to describe a plane knowing a point in the plane and the surface normal. Note that this function can return negative values. If you want purely the distance, you'll have to take the absolute value from this. However, it can be useful to know whether a point is facing the normal of the plane (positive result) or is on the other side of the plane (negative result).

Example:

 Distance from point to plane ABCD 123456 #Assuming we already know the A,B,C and D values that define a planeA,B,C,D = -0.146446615458, 0.499999970198, 0.853553295135, -1.7677667737P = nuke.math.Vector3(5,6,7)print P.distanceFromPlane(A,B,C,D) # Result: 6.47487258911

##### maximum(...) and minimum(...)

Syntax: Vector3.maximum(Vector3) -> Vector3  :
Vector3.minimum(Vector3) -> Vector3 :

Description: Returns a Vector3 object whose x, y and z coordinates take the maximum or minimum value out of the two input vectors. This would be the same as (using the maximum example) manually doing "x3 = max(x1,x2); y3 = max(y1,y2); z3 = max(z1,z2)".

##### reflect(...)

Syntax: Vector3.reflect(Vector3) -> Vector3 :

Description: Returns a the reflection of a Vector3 onto another Vector3 (usually a vector and a surface normal). This will result in a reflected vector

Example:

 Finding the reflection of a vector on a given surface 123456 vector = nuke.math.Vector3(10,5,2)surface = nuke.math.Vector3(0,1,0) # Flat surface pointing up, like a ground planereflection = vector.reflect(surface)print reflection # Result: {-10, 5, -2}

## Vector4:

The Vector4 Class defines a 4-component vector (x, y, z, w). This is used to represent a point in 3D homogenous (perspective) space, and is also useful to represent an rgb color with alpha. Homogeneous coordinates are commonly used in computer graphics so we can use 4x4 matrices (Matrix4) to do affine transformations and perspective projections to an arbitrary point (Vector4).
Nuke.math defines no specific methods for the Vector4 Class, but Vector4 instances can be used in some Matrix methods, as you'll see in the next section.

# 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 1234567891011 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() 1234567 matrix = nuke.math.Matrix3()matrix.rotationX(math.pi/2) # new matrix with a 90 degrees rotation about the X axisvector = nuke.math.Vector3(0,1,0) # new vector looking up towards +YtransformedVector = 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 12345678 matrix = nuke.math.Matrix3()m.rotationX(math.pi/2) # rotate matrix 90 degrees about the X axisinv = 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 1234567891011121314 v = nuke.math.Vector3(0,1,0) # Unit vector looking up towards +Ym = 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 1234567891011 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 12345678910111213141516171819202122232425262728293031323334353637 # Projection variablesfocal = 50.0h_aperture = 24.576near = 0.1far = 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 variablesprojectionMatrix = nuke.math.Matrix4()projectionMatrix.projection(focal/h_aperture, near, far, True) # Create a new Vector4 to transform through the matrixoriginalVector = nuke.math.Vector4(5, 5, -36,1) # And transform ittransformedVector = 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 outputtransformedVector /= 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 windowprint (1-(1-imageAspect/pixelAspect))transformedVector.x = (transformedVector.x + 1) / 2transformedVector.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 steptransformedVector.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 123456789101112131415161718192021 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 axismatrix.rotateY(math.pi/2) # rotate the matrix 90 degrees rotation about the Y axismatrix.rotateZ(math.pi/2) # rotate the matrix 90 degrees rotation about the Z axismatrix.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 translationprint 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 12345678910 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 123456789101112131415161718192021222324252627 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 squareimageWidth = 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() functioncornerPinMatrix.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 matrixscreenCenter = 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.

# 4. Misc Examples

Even with the limitations mentioned in the introduction of this tutorial (basically the fact that Nuke doesn't provide a way to push pixel data into an image using Python), there's still plenty of situations where the nuke.math module can come in handy.  Here's a few examples, hoping they will shed one last bit of light into the uses and possibilities of the nuke.math module.

#### Using nuke.math in a gizmo

An example of a gizmo that uses the nuke.math module can be found here:

http://www.nukepedia.com/gizmos/gizmo-downloads/metadata/prmantracker/

The gizmo generates tracking data from a world position pass and camera data stored in Renderman EXR renders.

To do so, it creates vector objects sampling the worldPosition pass for x,y,z coordinates. It then takes the projection matrix written by renderman into the exr metadata, and projects them into screen-space. It then outputs those screen coordinates to XY knobs so it can be used as tracking data.

#### Using nuke.math to create your own Python functions

You can use nuke.math inside your own functions.

Here's an example with a couple of function definitions. First one returns a Matrix4 object from any node that has a matrix knob, and the second one if a convenience function to get the distance from an Axis (or any node that has Axis-like knobs) to the plane defined by a card. This example works only for Nuke version 6.1 and above.

 Distance from an Axis to the plane defined by a card 1234567891011121314151617181920212223242526272829303132333435363738394041424344 def getMatrixFromNode(node): ''' return a nuke.math.Matrix4 object with the transformations of a node "node" is the node object of any 3D node in Nuke that has a world or local matrix knob ''' try: nodeMatrix = nuke.math.Matrix4() if 'world_matrix' in [k for k in node.knobs()]: knobname = 'world_matrix' elif 'matrix' in [k for k in node.knobs()]: knobname = 'matrix' else: raise NameError  for x in range(node[knobname].width()): for y in range(node[knobname].height()): nodeMatrix[x*4+y] = node[knobname].value(y,x) except (NameError, AttributeError): nuke.message('Node %s does not have a valid world_matrix or local_matrix knobs.\nReturning the identity matrix instead' % node.name()) nodeMatrix.makeIdentity() return nodeMatrix  def distanceAxisToCard(axisNode, cardNode): ''' return the distance from an Axis, or any node with Axis-like knobs to the plane defined by a card ''' axisMatrix = getMatrixFromNode(axisNode) cardMatrix = getMatrixFromNode(cardNode)  v = nuke.math.Vector3(0,0,0)  axisPosition = axisMatrix.transform(v) cardPosition = cardMatrix.transform(v) cardNormal = nuke.math.Vector3() cardOrientation = cardNode['orientation'].value()  cardNormal.set(int('X' not in cardOrientation), int('Y' not in cardOrientation), int('Z' not in cardOrientation)) cardNormal = cardMatrix.inverse().ntransform(cardNormal) cardNormal.normalize()  return cardNormal.dot(axisPosition-cardPosition)

#### Moving DAG art

Doubtful production value, but fun to play with, and a great way to impress your peers :)

2D art: A rotating ColorWheel

Instructions: Copy-paste the code into the script editor on a new Nuke session. Play with the rotation slider

 A rotating ColorWheel in the DAG 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 import math #### Variables #### pointSeparation = 30.0 #half the screen size of an Axis node (60)outRadius = 20.0 # number of Axis nodes from the center to the outer edgeinRadius = 0.0 # number of Axis nodes from the center to the inner edge  #### Helper function ####def HSVtoHEX(h, s, v): """Convert HSV color space to hex values to use on a node's tile_color @param h: Hue @param s: Saturation @param v: Value return hex value """  hi = math.floor(h / 60.0) % 6 f = (h / 60.0) - math.floor(h / 60.0) p = v * (1.0 - s) q = v * (1.0 - (f*s)) t = v * (1.0 - ((1.0 - f) * s))  rgb = { 0: (v, t, p), 1: (q, v, p), 2: (p, v, t), 3: (p, q, v), 4: (t, p, v), 5: (v, p, q), }[hi]  return int('%02x%02x%02x%02x' % (rgb[0]*255,rgb[1]*255,rgb[2]*255,1),16)   #### Main #### m = nuke.math.Matrix3() # Matrix that will drive the rotation of all nodesm.makeIdentity() # Initialize matrix to identity nuke.zoom(0.3,(0,0)) # Set an initial zoom level and center the DAG at 0,0 for i in range(inRadius,outRadius):  radius = pointSeparation * i long = math.pi*radius*2.0  v = nuke.math.Vector3(0,radius,0) points = math.floor(long/pointSeparation)  for x in range(points):  angle = 360*(x/float(points)) m.rotationZ(math.radians(angle)) vv = m.transform(v) d = nuke.nodes.Axis() d['hide_input'].setValue(True) d.setXYpos(vv.x, vv.y) d['translate'].setValue((vv.x,vv.y,0)) d['tile_color'].setValue(HSVtoHEX(angle, radius/(outRadius*pointSeparation),1))  ### code for knobChanged callback on controller node ### knobChangedCode = """ n = nuke.thisNode()k = nuke.thisKnob()if k.name() == 'rotate':  m = nuke.math.Matrix3() m.makeIdentity() m.rotateZ(k.value()*0.0174532925) # Rotation in radians for a in nuke.allNodes('Axis'): v = nuke.math.Vector3(a['translate'].getValue(0), a['translate'].getValue(1), 0.0) tv = m.transform(v) a.setXYpos(tv.x, tv.y) """ control = nuke.createNode('NoOp', 'name controller')control.setXYpos(outRadius * pointSeparation + 200,0)rotKnob = nuke.Double_Knob('rotate')rotKnob.setRange(-180, 180)control.addKnob(rotKnob)nukescripts.autoBackdrop()control['knobChanged'].setValue(knobChangedCode)

3D art: A Phong-shaded sphere

Instructions: Copy-paste the code into the script editor on a new Nuke session. Play with the controller node's knobs.

 A Phong-shaded Sphere in the DAG 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 # Sphere variables segments = 100.0 theta = 0.0 phi = 0.0 r = 50.0   # Vector container for each of the Sphere's vertices v = nuke.math.Vector3()   # Build the virtual Sphere using Axis2 nodes for i in range(int(segments/2)): phi += (2*nukescripts.math.pi) / segments faces = int (segments * ( 1 - (abs(i-(segments/4))) / (segments/4)))   for j in range( faces): theta += (2*nukescripts.math.pi) / faces v.x = r * nukescripts.math.sin(phi) * nukescripts.math.cos(theta) v.y = r * nukescripts.math.sin(phi) * nukescripts.math.sin(theta) v.z = r * nukescripts.math.cos(phi) d = nuke.nodes.Axis2() d['hide_input'].setValue(True) d['note_font_size'].setValue(0) d['translate'].setValue((v.x,v.y,v.z))   theta = 0.0   ##############################################################################   # create controller node control = nuke.createNode('NoOp', 'name controller', inpanel = False) control.setXYpos(500,0)   # Add all the control knobs and set some initial values lightGroup = nuke.Text_Knob("lightPosition", "lightPosition", "") latKnob = nuke.Double_Knob('latitude') latKnob.setRange(-180, 180) latKnob.setValue(200) longKnob = nuke.Double_Knob('longitude') longKnob.setRange(-180, 180) longKnob.setValue(40)   materialGroup = nuke.Text_Knob("material", "material", "") colorKnob = nuke.Color_Knob('matColor') colorKnob.setValue((0.1, 0.3, 0.3)) ambientKnob = nuke.Double_Knob('ambient') ambientKnob.setValue(0.05) diffuseKnob = nuke.Double_Knob('diffuse') diffuseKnob.setValue(0.18) specularKnob = nuke.Double_Knob('specular') specularKnob.setValue(0.8) shininessKnob = nuke.Double_Knob('shininess') shininessKnob.setRange(1, 128) shininessKnob.setValue(20)   viewGroup = nuke.Text_Knob("viewControls", "viewControls", "") translateKnob = nuke.XYZ_Knob('translate') translateKnob.setValue((0,0,300)) rotateKnob = nuke.XYZ_Knob('rotate') rotateKnob.setValue((0,180,-90)) lensKnob = nuke.Double_Knob('lens') lensKnob.setValue(80)   for k in [lightGroup, latKnob, longKnob, materialGroup, colorKnob, ambientKnob, diffuseKnob, specularKnob, shininessKnob, viewGroup, translateKnob, rotateKnob, lensKnob ]: control.addKnob(k)   ##############################################################################   knobChangedCode = """ n = nuke.thisNode() k = nuke.thisKnob()   def getCameraMatrices(n):   # initialize view matrices p = nuke.math.Matrix4() p.makeIdentity() t = nuke.math.Matrix4() t.makeIdentity() r = nuke.math.Matrix4() r.makeIdentity() o = nuke.math.Matrix4() o.makeIdentity()   p.projection(n['lens'].value()*10, 1, 1000, True)   # translation from the translate knob t.translation(n['translate'].value()[0], n['translate'].value()[1], n['translate'].value()[2])   o.rotateY(nukescripts.math.radians(180)) # make it look towards -Z   # rotations from the rotate knob r.rotateY(nukescripts.math.radians(n['rotate'].value()[1])) r.rotateX(nukescripts.math.radians(n['rotate'].value()[0])) r.rotateZ(nukescripts.math.radians(n['rotate'].value()[2]))   m = r * t   camPosition = m.transform(nuke.math.Vector3(0,0,0))   return p, t, r, camPosition, o     def transformPoints():   p, t, r, camPos, o = getCameraMatrices(n) # set view_matrix from the separate projection, translation, rotation and orientation matrices view_matrix = p * t * o * r   pos = nuke.math.Vector4(0,0,0,1)   # Loop through all points   for node in nuke.allNodes('Axis2'):   pos.x, pos.y, pos.z = node['translate'].value() # Project point onto display coordinates dpos = view_matrix.transform(pos) node.setXYpos(int(dpos.x/dpos.w), int(dpos.y/dpos.w))     def updateShading():   # get color of surface material if n['matColor'].singleValue(): matR = matG = matB = n['matColor'].value() else: matR, matG, matB = n['matColor'].value()   matColor = nuke.math.Vector3(matR, matG, matB)   # get intensities for each light component diffuseIntensity = n['diffuse'].value() specularIntensity = n['specular'].value() ambientIntensity = n['ambient'].value()   # get Light vector latitude = n['latitude'].value() longitude = n['longitude'].value() lx = 200 * nukescripts.math.cos(math.radians(longitude)) * nukescripts.math.cos(math.radians(latitude)) ly = 200 * nukescripts.math.sin(math.radians(longitude)) * nukescripts.math.cos(math.radians(latitude)) lz = 200 * nukescripts.math.sin(math.radians(latitude))   lightPos = nuke.math.Vector3(lx, ly, lz)   # get camera position vector camPos = getCameraMatrices(n)[3]   # Placeholder vector for each one of the points' position P = nuke.math.Vector3(0,0,0)   # Loop through all points for node in nuke.allNodes('Axis2'):   # P = point position vector P.set(node['translate'].value()[0], node['translate'].value()[1], node['translate'].value()[2])   # get a normalized version of P PN = nuke.math.Vector3(P) PN.fast_normalize()   # get normalized vector from light to point point2light = lightPos - P point2light.fast_normalize()   # get diffuse component   D = max(0, PN.dot(point2light))   # get specular component if PN.dot(point2light) > 0.0: point2cam = camPos - P point2cam.fast_normalize() R = point2cam.reflect(PN) rDotV = max(0, R.dot(point2light))   S = pow(rDotV, n['shininess'].value())   else: S = 0.0   specular = nuke.math.Vector3(S,S,S)   # build shader from all components shadedColor = (matColor*ambientIntensity) + (matColor * D * diffuseIntensity) + (specular * specularIntensity)   # set the node color colorR = min(255,nuke.expression('to_sRGB(%f)' % shadedColor[0])) colorG = min(255,nuke.expression('to_sRGB(%f)' % shadedColor[1])) colorB = min(255,nuke.expression('to_sRGB(%f)' % shadedColor[2])) tileColor = int('%02x%02x%02x%02x' % (colorR,colorG,colorB,1),16) node['tile_color'].setValue(tileColor)       if k.name() in ['matColor', 'ambient', 'diffuse', 'specular', 'latitude', 'longitude', 'shininess']: updateShading()   elif k.name() in ['rotate' , 'translate']: updateShading() transformPoints()   elif k.name() == 'lens': transformPoints()   elif k.name() == 'showPanel': import math updateShading() transformPoints() """   # assign knobChanged code control['knobChanged'].setValue(knobChangedCode) # open panel to trigger transformation and shade for the first time nuke.show(control) # set initial zoom level and center of the DAG nuke.zoom(0.6,(0,0))

#### 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 3418 guests and 45 members online