1
2
3 import math
4 import nuke, nuke.geo, _nukemath
5
6
7
8
9
10
13
16
19
20
21
22
24 '''
25 Translate the specified node to the average position of the current vertex selection in the active viewer.
26 The nodeToSnap must contain a 'translate' knob and the transform order must be 'SRT'.
27
28 @type nodeToSnap: nuke.Node
29 @param nodeToSnap: Node to translate
30 '''
31 return translateSelectionToPoints(nodeToSnap, getSelection())
32
34 '''
35 Translate the specified node to the average position of the current vertex selection in the active viewer
36 and rotate to the orientation of the (mean squares) best fit plane for the selection.
37 The nodeToSnap must contain 'translate' and 'rotate' knobs, the transform order must be 'SRT' and the
38 rotation order must be 'ZXY'.
39
40 @type nodeToSnap: nuke.Node
41 @param nodeToSnap: Node to translate and rotate
42 '''
43 return translateRotateSelectionToPoints(nodeToSnap, getSelection())
44
46 '''
47 Translate the specified node to the average position of the current vertex selection in the active viewer,
48 rotate to the orientation of the (mean squares) best fit plane for the selection
49 and scale to the extents of the selection.
50 The nodeToSnap must contain 'translate', 'rotate' and 'scale' knobs, the transform order must be 'SRT' and
51 the rotation order must be 'ZXY'.
52
53 @type nodeToSnap: nuke.Node
54 @param nodeToSnap: Node to translate, rotate and scale
55 '''
56 return translateRotateScaleSelectionToPoints(nodeToSnap, getSelection())
57
58
66
74
82
84 if len(vertexSelection) < minLen:
85 raise ValueError('Selection must be at least %d points' % minLen)
86
87
89
90 nodeKnobs = nodeToSnap.knobs()
91 for knob in knobList:
92 if knob not in nodeKnobs:
93 raise ValueError('Snap requires "%s" knob' % knob)
94
95 verifyNodeOrder(nodeToSnap, "xform_order", "SRT")
96
97 if "rotate" in knobList:
98 verifyNodeOrder(nodeToSnap, "rot_order", "ZXY")
99
101 orderKnob = node.knob(knobName)
102
103 order = orderKnob.enumName( int(orderKnob.getValue()) )
104 if orderName != order:
105 raise ValueError('Snap requires "%s" %s' % (orderName, knobName))
106
107
109 - def __init__(self, objnum, index, value, position):
114
115
118 self.vertexInfoSet = set()
119 self.length = 0
120
123
124
126
127
128 for info in self.vertexInfoSet:
129 yield info
130
131 - def add(self, vertexInfo):
132 self.vertexInfoSet.add(vertexInfo)
133 self.length += 1
134
141
150
154
185
186 - def scale(self, vector):
191
192
194 t = m[0+3*1]
195 m[0+3*1] = m[1+3*0]
196 m[1+3*0] = t
197
198 t = m[0+3*2]
199 m[0+3*2] = m[2+3*0]
200 m[2+3*0] = t
201
202 t = m[1+3*2]
203 m[1+3*2] = m[2+3*1]
204 m[2+3*1] = t
205
206 return
207
208
210
211 centre = calcAveragePosition(vertexSelection)
212
213 nodeToSnap['translate'].setValue(centre)
214
215 inverseTranslation = -centre
216 vertexSelection.translate(inverseTranslation)
217
225
227
228 norm = averageNormal(vertexSelection)
229
230 rotationVec = calcRotationVector(vertexSelection, norm)
231
232 rotationDegrees = _nukemath.Vector3( math.degrees(rotationVec.x), math.degrees(rotationVec.y), math.degrees(rotationVec.z) )
233
234 nodeToSnap['rotate'].setValue(rotationDegrees)
235
236 vertexSelection.inverseRotate(rotationVec, "YXZ")
237
242
247
248
250
251 points = vertexSelection.points()
252
253
254 if len(vertexSelection) >= 3:
255 planeTri = nuke.geo.bestFitPlane(*points)
256
257 rotationVec = planeRotation(planeTri, norm)
258 elif len(vertexSelection) == 2:
259
260 u = points[1] - points[0]
261 u.normalize()
262
263 w = norm
264 v = w.cross(u)
265 v.normalize()
266
267 w = v.cross(u)
268
269
270 planeTri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
271
272 rotationVec = planeRotation(planeTri, norm)
273 elif len(vertexSelection) == 1:
274
275 w = norm
276 w.normalize()
277
278 if abs(w.x) < abs(w.y):
279 v = _nukemath.Vector3(0.0, 1.0, 0.0)
280 u = w.cross(v)
281 u.normalize()
282
283 v = w.cross(u)
284 else:
285 u = _nukemath.Vector3(1.0, 0.0, 0.0)
286 v = w.cross(u)
287 v.normalize()
288
289 u = w.cross(v)
290
291
292 planeTri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
293
294 rotationVec = planeRotation(planeTri, norm)
295
296
297 rotationVec.z = 0
298
299 return rotationVec
300
301
302
303
304
306 '''
307 Calculate the average position of all points.
308
309 @param points: An iterable sequence of _nukemath.Vector3 objects.
310 @return: A _nukemath.Vector3 containing the average of all the points.
311 '''
312 pos = _nukemath.Vector3(0, 0, 0)
313 count = 0
314 for info in vertexSelection:
315 point = info.position
316 pos += point
317 count += 1
318 if count == 0:
319 return
320 pos /= count
321 return pos
322
324
325
326 high = _nukemath.Vector3(1e-20, 1e-20, 1e-20)
327 low = _nukemath.Vector3(-1e-20, -1e-20, -1e-20)
328 for info in vertexSelection:
329 pos = info.position
330 for i in xrange(len(pos)):
331 if pos[i] < low[i]:
332 low[i] = pos[i]
333 elif pos[i] > high[i]:
334 high[i] = pos[i]
335
336 bounds = high - low
337 return bounds
338
340 '''
341 Calculate the rotations around the X, Y and Z axes that will align a plane
342 perpendicular to the Z axis with the given triangle.
343
344 @param tri: A list or tuple of 3 _nukemath.Vector3 objects. The 3 points must
345 describe the plane (i.e. they must not be collinear).
346 @return: A _nukemath.Vector3 object where the x coordinate is the angle of
347 rotation around the x axis and so on.
348 @raise ValueError: if the three points are collinear.
349 '''
350
351 a = tri[1] - tri[0]
352 b = tri[2] - tri[0]
353
354
355
356
357
358 u = _nukemath.Vector3(a)
359 u.normalize()
360 w = a.cross(b)
361 w.normalize()
362 v = w.cross(u)
363
364
365
366 if norm != None:
367 if w.dot(norm) < 0.0:
368 w.x = -w.x
369 w.y = -w.y
370 w.z = -w.z
371
372 v.x = -v.x
373 v.y = -v.y
374 v.z = -v.z
375
376
377 m = ( (u[0], v[0], w[0]),
378 (u[1], v[1], w[1]),
379 (u[2], v[2], w[2]) )
380 if abs(m[1][2]) == 1.0:
381 ry = 0.0
382 rx = (math.pi / 2.0) * -m[1][2]
383 rz = math.atan2(-m[0][1], m[0][0])
384 else:
385 cosx = math.sqrt(m[0][2] ** 2 + m[2][2] ** 2)
386 if cosx == 0:
387 cosx = 1.0
388 rx = math.atan2(-m[1][2], cosx)
389 rz = math.atan2(m[1][0] / cosx, m[1][1] / cosx)
390 ry = math.atan2(m[0][2] / cosx, m[2][2] / cosx)
391
392 return _nukemath.Vector3(rx, ry, rz)
393
394
395
396
398 '''
399 averageNormal(selectionThreshold -> _nukemath.Vector3
400 Return a _nukemath.Vector3 which is the average of the normals of all selected points
401 '''
402
403 if not nuke.activeViewer():
404 return None
405
406
407 fastIndices = vertexSelection.indices()
408
409 found = False
410 norm = _nukemath.Vector3(0.0, 0.0, 0.0)
411
412 for theNode in allNodesWithGeoSelectKnob():
413 geoSelectKnob = theNode['geo_select']
414 sel = geoSelectKnob.getSelection()
415 objs = geoSelectKnob.getGeometry()
416
417 for o in xrange(len(sel)):
418 objPrimitives = objs[o].primitives()
419 objTransform = objs[o].transform()
420
421
422 visitedPrimitives = {}
423 for prim in objPrimitives:
424
425 if prim not in visitedPrimitives:
426 for pt in prim.points():
427
428 if pt in fastIndices:
429 found = True
430 n = prim.normal()
431 n = _nukemath.Vector3(n[0], n[1], n[2])
432 n = objTransform.vtransform(n)
433 norm += n
434 visitedPrimitives[prim] = pt
435 break
436
437 if found == False:
438 return None
439
440 norm.normalize()
441 return norm
442
443
445 '''
446 anySelectedPoint(selectionThreshold) -> _nukemath.Vector3
447 Return a selected point from the active viewer or the first viewer with a selection.
448 The selectionThreshold parameter is used when working with a soft selection.
449 Only points with a selection level >= the selection threshold will be returned by this function.
450 '''
451 if not nuke.activeViewer():
452 return None
453
454 for n in allNodesWithGeoSelectKnob():
455 geoSelectKnob = n['geo_select']
456 sel = geoSelectKnob.getSelection()
457 objs = geoSelectKnob.getGeometry()
458 for o in xrange(len(sel)):
459 objSelection = sel[o]
460 for p in xrange(len(objSelection)):
461 if objSelection[p] >= selectionThreshold:
462 pos = objs[o].points()[p]
463 tPos = objs[o].transform() * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
464 return _nukemath.Vector3(tPos.x, tPos.y, tPos.z)
465 return None
466
467
469 '''
470 selectedPoints(selectionThreshold) -> iterator
471
472 Return an iterator which yields the position of every point currently
473 selected in the Viewer in turn.
474
475 The selectionThreshold parameter is used when working with a soft selection.
476 Only points with a selection level >= the selection threshold will be
477 returned by this function.
478 '''
479 if not nuke.activeViewer():
480 return
481
482 for info in selectedVertexInfos(selectionThreshold):
483 yield info.position
484
485
492
493
495 '''
496 selectedVertexInfos(selectionThreshold) -> iterator
497
498 Return an iterator which yields a tuple of the index and position of each
499 point currently selected in the Viewer in turn.
500
501 The selectionThreshold parameter is used when working with a soft selection.
502 Only points with a selection level >= the selection threshold will be
503 returned by this function.
504 '''
505 if not nuke.activeViewer():
506 return
507
508 for n in allNodesWithGeoSelectKnob():
509 geoSelectKnob = n['geo_select']
510 sel = geoSelectKnob.getSelection()
511 objs = geoSelectKnob.getGeometry()
512 for o in xrange(len(sel)):
513 objSelection = sel[o]
514 objPoints = objs[o].points()
515 objTransform = objs[o].transform()
516 for p in xrange(len(objSelection)):
517 value = objSelection[p]
518 if value >= selectionThreshold:
519 pos = objPoints[p]
520 tPos = objTransform * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
521 yield VertexInfo(o, p, value, _nukemath.Vector3(tPos.x, tPos.y, tPos.z))
522
523
525 '''
526 anySelectedVertexInfo(selectionThreshold) -> VertexInfo
527
528 Returns a single VertexInfo for a selected point. If more than one point is
529 selected, one of them will be chosen arbitrarily.
530
531 The selectionThreshold parameter is used when working with a soft selection.
532 Only points with a selection level >= the selection threshold will be
533 returned by this function.
534 '''
535 if not nuke.activeViewer():
536 return None
537
538 for n in allNodesWithGeoSelectKnob():
539 geoSelectKnob = n['geo_select']
540 sel = geoSelectKnob.getSelection()
541 objs = geoSelectKnob.getGeometry()
542 for o in xrange(len(sel)):
543 objSelection = sel[o]
544 objPoints = objs[o].points()
545 objTransform = objs[o].transform()
546 for p in xrange(len(objSelection)):
547 value = objSelection[p]
548 if value >= selectionThreshold:
549 pos = objPoints[p]
550 tPos = objTransform * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
551 return VertexInfo(o, p, value, _nukemath.Vector3(tPos.x, tPos.y, tPos.z))
552 return None
553
554
556 '''
557 allNodes() -> iterator
558 Return an iterator which yields all nodes in the current script.
559
560 This includes nodes inside groups. They will be returned in top-down,
561 depth-first order.
562 '''
563 yield node
564 if hasattr(node, "nodes"):
565 for child in node.nodes():
566 for n in allNodes(child):
567 yield n
568
569
577
578
580 'Calculate the projection matrix for the camera based on its knob values.'
581
582
583 camTransform = cameraNode['transform'].value().inverse()
584
585
586 roll = float(cameraNode['winroll'].getValue())
587 scale_x, scale_y = [float(v) for v in cameraNode['win_scale'].getValue()]
588 translate_x, translate_y = [float(v) for v in cameraNode['win_translate'].getValue()]
589 m = _nukemath.Matrix4()
590 m.makeIdentity()
591 m.rotateZ(math.radians(roll))
592 m.scale(1.0 / scale_x, 1.0 / scale_y, 1.0)
593 m.translate(-translate_x, -translate_y, 0.0)
594
595
596 focal_length = float(cameraNode['focal'].getValue())
597 h_aperture = float(cameraNode['haperture'].getValue())
598 near = float(cameraNode['near'].getValue())
599 far = float(cameraNode['far'].getValue())
600 projection_mode = int(cameraNode['projection_mode'].getValue())
601 p = _nukemath.Matrix4()
602 p.projection(focal_length / h_aperture, near, far, projection_mode == 0)
603
604
605 format = nuke.root()['format'].value()
606 imageAspect = float(format.height()) / float(format.width())
607 t = _nukemath.Matrix4()
608 t.makeIdentity()
609 t.translate( 1.0, 1.0 - (1.0 - imageAspect / float(format.pixelAspect())), 0.0 )
610
611
612 x_scale = float(format.width()) / 2.0
613 y_scale = x_scale * format.pixelAspect()
614 s = _nukemath.Matrix4()
615 s.makeIdentity()
616 s.scale(x_scale, y_scale, 1.0)
617
618
619
620
621
622 return s * t * p * m * camTransform
623
624
626 '''
627 projectPoint(camera, points) -> list of nuke.math.Vector2
628
629 Project the given 3D point through the camera to get 2D pixel coordinates.
630
631 @param camera: The Camera node or name of the Camera node to use for projecting
632 the point.
633 @param points: A list or tuple of either nuke.math.Vector3 or of list/tuples of
634 three float values representing the 3D points.
635 @raise ValueError: If camera or point is invalid.
636 '''
637
638 camNode = None
639 if isinstance(camera, nuke.Node):
640 camNode = camera
641 elif isinstance(camera, str):
642 camNode = nuke.toNode(camera)
643 else:
644 raise ValueError, "Argument camera must be a node or the name of a node."
645
646 camMatrix = cameraProjectionMatrix(camNode)
647 if camMatrix == None:
648 raise RuntimeError, "snap3d.cameraProjectionMatrix() returned None for camera."
649
650 if not ( isinstance(points, list) or isinstance(points, tuple) ):
651 raise ValueError, "Argument points must be a list or tuple."
652
653 for point in points:
654
655
656 if isinstance(point, nuke.math.Vector3):
657 pt = point
658 elif isinstance(point, list) or isinstance(point, tuple):
659 pt = nuke.math.Vector3(point[0], point[1], point[2])
660 else:
661 raise ValueError, "All items in points must be nuke.math.Vector3 or list/tuple of 3 floats."
662
663 tPos = camMatrix * nuke.math.Vector4(pt.x, pt.y, pt.z, 1.0)
664 yield nuke.math.Vector2(tPos.x / tPos.w, tPos.y / tPos.w)
665
666
667
669 '''
670 projectPoint(camera, point) -> nuke.math.Vector2
671
672 Project the given 3D point through the camera to get 2D pixel coordinates.
673
674 @param camera: The Camera node or name of the Camera node to use for projecting
675 the point.
676 @param point: A nuke.math.Vector3 or of list/tuple of three float values
677 representing the 3D point.
678 @raise ValueError: If camera or point is invalid.
679 '''
680
681 return projectPoints( camera, (point,) ).next()
682
683
684
686 '''
687 projectSelectedPoints(cameraName='Camera1') -> iterator yielding nuke.math.Vector2
688
689 Using the specified camera, project all of the selected points into 2D pixel
690 coordinates and return their locations.
691
692 @param cameraName: Optional name of the Camera node to use for projecting the
693 points. If omitted, will look for a node called Camera1.
694 '''
695 camNode = nuke.toNode(cameraName)
696 camMatrix = cameraProjectionMatrix(camNode)
697 for pt in selectedPoints():
698 tPos = camMatrix * nuke.math.Vector4(pt.x, pt.y, pt.z, 1.0)
699 yield nuke.math.Vector2(tPos.x / tPos.w, tPos.y / tPos.w)
700
701
702
703
704
705
706
707
708 snapFuncs = []
709
710
712 '''
713 addSnapFunc(label, func) -> None
714 Add a new snapping function to the list.
715
716 The label parameter is the text that will appear in (eg.) an Enumeration_Knob
717 for the function. It cannot be the same as any existing snap function label
718 (if it is, the function will abort without changing anything).
719
720 The func parameter is the snapping function. It must be a callable object
721 taking a single parameter: the node to perform the snapping on.
722 '''
723 if label in dict(snapFuncs):
724 return
725 snapFuncs.append( (label, func) )
726
727
729 '''
730 callSnapFunc(nodeToSnap) -> None
731 Call the snapping function on a node.
732
733 The nodeToSnap parameter is optional. If it's not specified, or is None, we
734 use the result of nuke.thisNode() instead.
735
736 The node must have an Enumeration_Knob called "snapFunc" which selects the
737 snapping function to call.
738 '''
739 if nodeToSnap is None:
740 nodeToSnap = nuke.thisNode()
741
742
743 if "snapFunc" not in nodeToSnap.knobs():
744
745 return
746
747 snapFunc = dict(snapFuncs)[nodeToSnap['snapFunc'].value()]
748 snapFunc(nodeToSnap)
749