Using the nuke.math Python module to do Vector and Matrix operations
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 | |
1 |
v1 = nuke.math.Vector3() |
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 | |
1 |
v1 = nuke.math.Vector3() |
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 | |
1 |
v1 = nuke.math.Vector3(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 | |
1 |
v1 = nuke.math.Vector3(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:
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)
Modifying a list:
|
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:
1 |
v1 = nuke.math.Vector3(0,0,0) |
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 | |
1 |
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] |
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 | |
1 |
pointA = nuke.math.Vector2(50,100) |
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 | |
1 |
import math # we'll need this for the arc cosine function, and to convert radians to degrees |
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 | |
1 |
vectorA = nuke.math.Vector2(15,46) |
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 | |
1 |
vectorA = nuke.math.Vector2(15,46) |
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 | |
1 |
vectorA = nuke.math.Vector3(1,0,0) # Unit vector along the X axis |
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 | |
1 |
#Assuming we already know the A,B,C and D values that define a plane |
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 | |
1 |
vector = nuke.math.Vector3(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 | |
1 |
m = nuke.math.Matrix3() |
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 |
matrix = nuke.math.Matrix3() |
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 |
matrix = nuke.math.Matrix3() |
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 |
v = nuke.math.Vector3(0,1,0) # Unit vector looking up towards +Y |
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 |
m = nuke.math.Matrix3() |
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 |
# Projection variables |
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 |
matrix = nuke.math.Matrix4() |
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 |
matrix = nuke.math.Matrix4() |
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 |
cornerPinMatrix = nuke.math.Matrix4() |
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 | |
1 |
def getMatrixFromNode(node): |
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 | |
1 |
import math |
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
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.
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
RSS feed for comments to this post