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

Written by Ivan Busquets on .

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
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 
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
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
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 edge
inRadius = 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 nodes
m.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 3689 guests and 87 members online