Sunday, August 11, 2019

Animation Control Orient Tool

USES
When the rotation axes you need to rotate around are not aligned with the world axes,  an "orient group" is needed (also called "offset group"or "reset group") in order to keep the animation control with zero values. Simply aligning the animation control and freezing transforms won't work because freezing transforms will reset the rotation axes to world. This script finds the rotation values necessary to properly align an animation control to the correct rotation axes, creates a group above the control and assigns the rotation values to it, so that the control can have zero transforms, but still be aligned as needed. 




HOW TO USE

First create your animation control and place it so that its pivot point is where you need it. Then, open the GUI and assign one or two planes, by selecting either a face or 3 vertices and clicking assign, and the animation control. If one of your rotation axes is aligned with one of the world axes, like in the figure A below, you will only need to assign one plane, else you'll need two. You can use the radio buttons next to each plane assignment to select which axis will aligned with that plane. Finally, click "Orient control". 
The script will create a new group named "your_control_name"_orientGrp, and will parent the animation control to it. You can now parent this orient group to your hierarchy and lock all channels, since this group should not be animated.
If the control is not visually facing the way you want, you can rotate it and then freeze transforms, that won't affect the rotation axes since those are coming from the values in the parent group. 


Fig A - Y axis is aligned with the world
Fig B - No axis are aligned with the world
















Choosing the 3 vertices to select can be confusing depending on the shape of your mesh,  the vertices don't need to be defining a face. You might be selecting vertices on the flat side of a hinge, or on the edge loop of the elbow. Watch the video below to see a practical example.





INSTALLATION

To get the Animation Control Orient Tool, download the script file and save it in your "maya/version/scripts" folder:
Download lr_ctrlOrientTool_UI.py

Then run the following code in the script editor (make sure to be in a python tab):
import lr_ctrlOrientTool_UI
reload(lr_ctrlOrientTool_UI)

You can save these lines in a shelf button for easy access.

If you don't want to download the file you can just copy the code below to the script editor and drag it onto your custom shelves so you can access it at any time (remember to save your shelves before quitting Maya)


####################################
## CTRL ORIENT UI ##
## lr_ctrlOrientTool_UI.py ##
## Created by Lorena Rother ##
## Updated: 26 Sep 2019 ##
####################################
import maya.cmds as mc
import math
import maya.OpenMaya as om
from functools import partial
#check if UI is already open
if mc.window("ctrlOrientUI", query=True, exists=True):
mc.deleteUI("ctrlOrientUI", window=True)
#turn on track selection order
if ( not mc.selectPref(trackSelectionOrder=1, q=1)):
mc.selectPref(trackSelectionOrder=True)
plane01 = None
plane02 = None
control = None
class DummyPlane():
def __init__(self, sel):
#check that sel is 3 vertices or 1 face
if len(sel)==3:
if (".vtx[" in sel[0] and ".vtx[" in sel[1] and ".vtx[" in sel[2]):
self.components = sel
self.components_string = ', '.join(self.components)
else:
raise Exception("Selection must be 3 vertices or 1 face")
elif len(sel)==1:
if (".f[" in sel[0]):
self.components = sel
self.components_string = sel[0]
else:
raise Exception("Selection must be 3 vertices or 1 face")
else:
raise Exception("Selection must be 3 vertices or 1 face")
def get_normal(self):
dplane=None
if (".f[" in self.components[0]):
face_verts = mc.polyListComponentConversion( self.components, ff=True, tv=True )
verts = mc.ls(face_verts, fl=1)
dplane= mc.polyCreateFacet( p=[mc.pointPosition(verts[0]), mc.pointPosition(verts[1]), mc.pointPosition(verts[2])] )
else:
dplane= mc.polyCreateFacet( p=[mc.pointPosition(self.components[0]), mc.pointPosition(self.components[1]), mc.pointPosition(self.components[2])] )
str_normal = mc.polyInfo(dplane, fn=1)[0].split()
mc.delete(dplane)
return om.MVector(float(str_normal[2]), float(str_normal[3]), float(str_normal[4]))
def warning_msg(msg, arg=None):
mc.confirmDialog(title='Warning', message=msg, button='ok')
#save and display selections
def assignSelection(number, *args):
global plane01
global plane02
global control
sel = mc.ls(os=1, sn=1)
if sel:
if number ==1:
plane01= DummyPlane(sel)
mc.text(plane01_list, e=True, l= plane01.components_string)
elif number==2:
plane02= DummyPlane(sel)
mc.text(plane02_list, e=True, l= plane02.components_string)
else:
control = sel[0]
mc.text(anm_control, e=True, l= str(control))
#if nothing selected, empty field
else:
if number == 1:
mc.text(plane01_list, e=True, l='')
plane01=None
elif number==2:
mc.text(plane02_list, e=True, l='')
plane02=None
else:
mc.text(anm_control, e=True, l='')
control=None
def create_orient_group(*args):
plane01_normal=None
plane02_normal=None
if(plane01 and control and not plane02):
print("no plane02")
#create helper plane. Find normals
plane01_normal = plane01.get_normal()
# if only one plane is provided, find which one of the world axes is orthogonal to the normal
if (plane01_normal*om.MVector(1,0,0) == 0):
plane02_normal = om.MVector(1,0,0)
elif (plane01_normal*om.MVector(0,1,0) == 0):
plane02_normal = om.MVector(0,1,0)
elif (plane01_normal*om.MVector(0,0,1) == 0):
plane02_normal = om.MVector(0,0,1)
else:
#if none of the axes are aligned with a world axis, ask for more verts to define a 2nd plane
warning_msg("No axes are aligned with a world axis. You'll need to select 6 vertices and then ctrl.")
return
elif(plane01 and control and plane02):
#create helper plane/s. Find normals
plane01_normal = plane01.get_normal()
plane02_normal = plane02.get_normal()
if plane01_normal== plane02_normal:
warning_msg("1st plane and 2nd plane are the same.Please select 2 different planes.")
return
else:
warning_msg("Please assign at least 1 plane and a control.")
return
#find 3rd vector
plane03_normal = plane01_normal^plane02_normal
#create orient grp with same pivot as ctrl then parent ctrl
orientGrp = mc.group(n=str(control).replace('_Ctrl', '_')+'orientGrp', em=1)
constraint = mc.parentConstraint( control, orientGrp)
mc.delete(constraint)
mc.parent(control, orientGrp)
#make rotation matrix from vectors
#query matrix to use position row
orientGrp_matrix = mc.xform(orientGrp,q=1, m=1)
#query axes selection
plane01_axis = mc.radioCollection(xyz_collection01, q=1, sl=1)
plane02_axis = mc.radioCollection(xyz_collection02, q=1, sl=1)
#assign vector order
vectorX = None
vectorY = None
vectorZ = None
if plane01_axis == xrb01.split("|")[-1]:
vectorX = plane01_normal
if plane02_axis == yrb02.split("|")[-1]:
vectorY = plane02_normal
vectorZ = plane03_normal
if plane02_axis == zrb02.split("|")[-1]:
vectorZ = plane02_normal
vectorY = plane03_normal
if plane01_axis == yrb01.split("|")[-1]:
vectorY = plane01_normal
if plane02_axis == xrb02.split("|")[-1]:
vectorX = plane02_normal
vectorZ = plane03_normal
if plane02_axis == zrb02.split("|")[-1]:
vectorZ = plane02_normal
vectorX = plane03_normal
if plane01_axis == zrb01.split("|")[-1]:
vectorZ = plane01_normal
if plane02_axis == xrb02.split("|")[-1]:
vectorX = plane02_normal
vectorY = plane03_normal
if plane02_axis == yrb02.split("|")[-1]:
vectorY = plane02_normal
vectorX = plane03_normal
#assign rotation matrix to orientGrp
mc.xform(orientGrp, m=(vectorX.x, vectorX.y, vectorX.z, 0, vectorY.x, vectorY.y, vectorY.z, 0, vectorZ.x, vectorZ.y, vectorZ.z, 0,orientGrp_matrix[12],orientGrp_matrix[13],orientGrp_matrix[14],orientGrp_matrix[15] ))
#freeze transforms
mc.makeIdentity(orientGrp, apply=1, t=1, n=0)
def radioSwitch (rb, *args):
state = mc.radioButton( rb, q=1, en=1)
alt_sel =None
if rb==xrb02:
alt_sel=yrb02
if rb==yrb02:
alt_sel=zrb02
if rb==zrb02:
alt_sel=xrb02
if state:
mc.radioButton(rb, e=1, enable=0 )
mc.radioButton(alt_sel, e=1, sl=1 )
else:
mc.radioButton(rb, e=1, enable=1)
##### GUI ---------------------------------------------------------
win = mc.window('ctrlOrientUI', title='Control orient Tool', rtf=1)
mc.window(win, e=1, widthHeight=(580, 100))
mc.columnLayout( cw=500 )
#Select
mc.rowColumnLayout(numberOfColumns=6, cs=[(1, 0), (2, 5), (3, 0), (4, 10), (5, 5), (6, 5)], columnWidth=[(1, 80), (2, 300), (3, 80), (4, 30), (5, 30), (6, 30)])
mc.text(label='1st Plane:', align='left', bgc = [0.5, 0.5, 0.5])
plane01_list = mc.text(label='Select a face or 3 vertices',font='obliqueLabelFont', align='left')
mc.button(label=' assign', command= partial(assignSelection,1))
xyz_collection01 = mc.radioCollection()
xrb01 = mc.radioButton( label='x', sl=1)
yrb01 = mc.radioButton( label='y' )
zrb01 = mc.radioButton( label='z' )
mc.text(label='2nd Plane:', align='left', bgc = [0.5, 0.5, 0.5])
plane02_list = mc.text(label='Select a face or 3 vertices',font='obliqueLabelFont', align='left')
mc.button(label='assign', command= partial(assignSelection, 2))
xyz_collection02 = mc.radioCollection()
xrb02 = mc.radioButton( label='x' , en=0)
yrb02 = mc.radioButton( label='y' , sl=1)
zrb02 = mc.radioButton( label='z' )
mc.text(label='Control:', align='left', bgc = [0.5, 0.5, 0.5])
anm_control = mc.text(label='Select animation control',font='obliqueLabelFont', align='left')
mc.button(label=' assign', command= partial(assignSelection, 3))
mc.setParent( '..' )
mc.setParent( '..' )
mc.radioButton( xrb01, e=1, cc = partial(radioSwitch, xrb02 ))
mc.radioButton( yrb01, e=1, cc = partial(radioSwitch, yrb02 ))
mc.radioButton( zrb01, e=1, cc = partial(radioSwitch, zrb02 ))
#Process
mc.columnLayout(cw=500)
mc. button(label='Orient control', w=575, command= partial(create_orient_group))
#Show
mc.showWindow('ctrlOrientUI')
####################################
## CTRL ORIENT UI ##
## lr_ctrlOrientTool_UI.py ##
## Created by Lorena Rother ##
## Updated: 26 Sep 2019 ##
####################################

Saturday, August 10, 2019

Vray Z-depth Tool

USES
This script connects the Depth White and Depth Black attributes of the Vray Z-depth render element to two locators that follow along with the animation of your camera and object of interest. It gives visual feedback so it's easy to set up and see what area is being covered. The object of interest will always render the same value of gray and the white and black values will remain at a constant distant (set by the user) at all times.

HOW TO USE

First, make sure you have a Vray Z-depth render element in your scene and that it's named exactly "Z_depth". Then select the camera, shift select the object that needs to stay in focus and run the script. The script will create a new group called zDepth_grp which will have two attributes that you can use to control where the whitest and blackest planes will be in the render. When you adjust the value of these attributes you will see in the viewport two locators that slide on the line that connects the camera to the object of interest.






INSTALLATION

To get the Vray Z-depth Tool, download the script file and save it in your "maya/version/scripts" folder:
Download lr_zDepth_tool.py

Then run the following code in the script editor (make sure to be in a python tab):
import lr_zDepth_tool
reload(lr_zDepth_tool)

You can save these lines in a shelf button for easy access.

If you don't want to download the file you can just copy the code below to the script editor and drag it onto your custom shelves so you can access it at any time (remember to save your shelves before quitting Maya)


import maya.cmds as mc
if (mc.pluginInfo('vrayformaya.mll', q=1, l=1) == 0):
mc.loadPlugin ('vrayformaya.mll')
#check for zDepth render element
re = mc.ls(typ='VRayRenderElement')
if (u'Z_depth' not in re):
mc.confirmDialog(title='Warning', message='Cannot find Z_depth render element. \n This tool is intended to be used in a Light Rig file that has a Z_depth render element.', button='ok')
else:
#save camera and focus object selections
sel = mc.ls(sl=1)
if (len(sel) == 2):
cam = sel[0]
obj = sel[1]
#create camera locator
camLoc = mc.spaceLocator(name= cam+'_loc')
constraint1 = mc.pointConstraint( cam, camLoc)
#create obj, white depth and black depth locators
objLoc = mc.spaceLocator(name= str(obj)+'_loc')
constraint2 = mc.pointConstraint( obj, objLoc)
whiteLoc = mc.spaceLocator(name= 'whiteDepth_loc')
constraint3 = mc.pointConstraint( obj, whiteLoc)
mc.delete(constraint3)
mc.parent(whiteLoc, objLoc)
blackLoc = mc.duplicate(name= 'blackDepth_loc')
#create white depth distance measurement
wd_Distance = mc.distanceDimension(camLoc, whiteLoc)
#create black depth distance measurement
bd_Distance = mc.distanceDimension(camLoc, blackLoc)
#conect distance to zDepth
mc.connectAttr(str(wd_Distance)+'.distance', 'Z_depth.vray_depthWhite')
mc.connectAttr(str(bd_Distance)+'.distance', 'Z_depth.vray_depthBlack')
#group distance measurements and locators
zDepth_grp = mc.group(wd_Distance, bd_Distance, camLoc, objLoc, n='zDepth_grp')
#add black depth and white depth attributes
mc.addAttr(zDepth_grp, ln='blackDepth', at='float', k=True)
mc.addAttr(zDepth_grp, ln='whiteDepth', at='float', k=True)
#lock and hide attrs not needed
grp_attrs = ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz','visibility']
for att in grp_attrs:
mc.setAttr(str(zDepth_grp)+'.'+att, k=0, l=1)
#get object boundingbox
bbox = mc.exactWorldBoundingBox(obj)
xLength = bbox[3] - bbox[0]
yLength = bbox[4] - bbox[1]
zLength = bbox[5] - bbox[2]
locDistance = max(xLength,yLength,zLength)/2
#set white/black depth attrs as locDistance
mc.setAttr(str(zDepth_grp)+'.blackDepth', locDistance)
mc.setAttr(str(zDepth_grp)+'.whiteDepth', -locDistance)
#create object-cam vector and normalize
mc.createNode('plusMinusAverage', name='cam_'+str(obj)+'_Vector')
mc.setAttr('cam_'+str(obj)+'_Vector.operation', 2)
mc.connectAttr(str(camLoc[0])+'Shape.worldPosition','cam_'+str(obj)+'_Vector.input3D[0]')
mc.connectAttr(str(objLoc[0])+'Shape.worldPosition','cam_'+str(obj)+'_Vector.input3D[1]')
mc.createNode('vectorProduct', name='cam_'+str(obj)+'_VectorNormalized')
mc.setAttr('cam_'+str(obj)+'_VectorNormalized.operation', 0)
mc.setAttr('cam_'+str(obj)+'_VectorNormalized.normalizeOutput', 1)
mc.connectAttr('cam_'+str(obj)+'_Vector.output3D','cam_'+str(obj)+'_VectorNormalized.input1')
#create locators' move vectors
mc.createNode('multiplyDivide', name='whiteLoc_Distance')
mc.connectAttr(str(zDepth_grp)+'.whiteDepth','whiteLoc_Distance.input2X')
mc.connectAttr(str(zDepth_grp)+'.whiteDepth','whiteLoc_Distance.input2Y')
mc.connectAttr(str(zDepth_grp)+'.whiteDepth','whiteLoc_Distance.input2Z')
mc.connectAttr('cam_'+str(obj)+'_VectorNormalized.output','whiteLoc_Distance.input1')
mc.createNode('multiplyDivide', name='blackLoc_Distance')
mc.connectAttr(str(zDepth_grp)+'.blackDepth','blackLoc_Distance.input2X')
mc.connectAttr(str(zDepth_grp)+'.blackDepth','blackLoc_Distance.input2Y')
mc.connectAttr(str(zDepth_grp)+'.blackDepth','blackLoc_Distance.input2Z')
mc.connectAttr('cam_'+str(obj)+'_VectorNormalized.output','blackLoc_Distance.input1')
#connect locator vectors to locators translates
mc.connectAttr('blackLoc_Distance.output', str(blackLoc[0])+'.translate')
mc.connectAttr('whiteLoc_Distance.output', str(whiteLoc[0])+'.translate')
else:
mc.confirmDialog(title='Warning', message='Please select one camera and one focus object', button='ok')
mc.select(cl=1)

Sunday, August 4, 2019

Multiple Reference Tool


USES:
Unlike Maya's default file reference window, this tool allows you to select more than one file at a time. Also by adding more file rows in the tool UI you can make references for files located in different folders all at once. It enables you to reference one file multiple times in one go and assign a namespace of your choice.

INTERFACE:
Use the "browse" button on the right of each row to select the files you want to reference (you can shift/ctrl select multiple files). Type the desired number of copies  in the "Qty" field and the desired namespace in the "Namespace" field.
Use the "Add file row" button towards the bottom of the UI to add as many rows as you need. You'll need different rows to browse for files that live in different folders, or if you want to assign different quantities or namespaces for different files.
When all your preferences are set click on "Create references" to reference all the files.



INSTALLATION:
To get the this tool download the script file and save it in your maya/version/scripts folder:

Then run the following code in the script editor (make sure to be in a python tab):
import lr_referenceTool
reload(lr_referenceTool)

You can save these lines in a shelf button for easy access.


If you don't want to download the file you can just copy the code below to the script editor and drag it onto your custom shelves so you can access it at any time (remember to save your shelves before quitting Maya)

import maya.cmds as mc
from functools import partial
#check if fileReferenceTool is already open
if mc.window('fileReferenceTool', query=True, exists=True):
mc.deleteUI('fileReferenceTool', window=True)
####FUNCTIONS
class FileRow():
def __init__(self, id, topLayout,fileRows, arg=None):
self.id = id
self.fileField = 'file'+str(id)+'Field'
self.qtyField = 'file'+str(id)+'QtyField'
self.nsField = 'file'+str(id)+'NSField'
self.fLayout = mc.frameLayout(p=topLayout, label='File '+str(id) )
def displayRow(self, arg=None):
mc.setParent( self.fLayout )
mc.rowColumnLayout(numberOfColumns=5, columnWidth=[(1, 40),(2, 85), (3, 450), (4, 100), (5, 30)])
mc.text(label='Qty:', align='left')
mc.text(label='Namespace:', align='left')
mc.text(label='File:', align='left')
mc.text(label='')
mc.text(label='')
mc.textField( self.qtyField , tx= '1')
NS = chr(64+self.id)
mc.textField( self.nsField , tx= NS)
mc.textField( self.fileField , tx='paste file path or browse', font= 'obliqueLabelFont')
mc. button(label='browse', command= partial(getFilepath, self.fileField))
mc. symbolButton(i='smallTrash.xpm', command= partial(delRow, self.id, fileRows))
mc.setParent( '..' )
mc.setParent( '..' )
#Choose file and add to textField
def getFilepath(fileNumber, arg=None):
mayaFileFilter = 'Maya Binary (*.mb);; Maya ASCII (*.ma)'
filePath = mc.fileDialog2(fileFilter=mayaFileFilter, dialogStyle=2, fm=4, cap= 'Choose files' )
#check if file selected
if filePath:
filesString = ''
for fileName in filePath:
filesString = filesString +',' +fileName
filesString = filesString.lstrip(',')
#display selected file path in field
mc.textField(fileNumber, e=1, tx= filesString, font='plainLabelFont')
#add new file row
def addRow( topLayout, fileRows , arg=None):
rowID = 1
while True:
try:
a = fileRows[rowID]
except (KeyError, IndexError):
break
else: rowID += 1
newInstance = FileRow(rowID, topLayout, fileRows)
#add item to dictionary
fileRows[rowID] = newInstance
fileRows[rowID].displayRow()
#print(fileRows)
#delete row
def delRow(rowID, filerows, arg=None):
#delete layout
mc.deleteUI(fileRows[rowID].fLayout)
#delete from dictionary
del fileRows[rowID]
#print(fileRows)
#create references
def createReferences(fileRows, arg=None):
for k, o in fileRows.iteritems():
qtyInput = o.qtyField
#check qty is integer
try:
qty = int(mc.textField(qtyInput, q=1, tx=1 ))
except ValueError:
mc.confirmDialog(title='Warning', message='File '+str(k)+' quantity value must be an integer', button='ok')
return
#get namespace
NSinput = o.nsField
NS = mc.textField(NSinput, q=1, tx=1 )
#check if namespace is valid
validNS = mc.namespace(vn=NS)
if (validNS == ''):
mc.confirmDialog(title='Warning', message='File '+str(k)+' namespace contains no legal characters. Please choose a different namespace', button='ok')
return
#check if namespace exists
#if (mc.namespace(ex=NS+str(k))):
#mc.confirmDialog(title='Warning', message='File '+str(k)+' namespace already exists. Please choose a different namespace', button='ok')
#return
#get file paths
fileInput = o.fileField
filePathString = mc.textField(fileInput, q=1, tx=1)
filePathList = filePathString.split(',')
j = 1
for filePath in filePathList:
#check if file exists
if (mc.file(filePath, q=1, ex=1)):
i = 1
k = j
#create references
while (i<qty+1):
mc.file( filePath, r=True, namespace= NS+str(k), shd='shadingNetworks' )
i = i + 1
k = k + 1
j = k
else:
mc.confirmDialog(title='Warning', message='File "'+str(filePath)+'" not valid', button='ok')
####GUI
win = mc.window('fileReferenceTool', title='File Reference Tool', w=705, h=250, rtf=1)
form = mc.formLayout('form', h=100)
#File fields and browse
topLayout = mc.frameLayout(label = '', w=705)
#dictionary to keep track of rows added and deleted
fileRows = {}
#add first file row
addRow(topLayout, fileRows)
mc.setParent( '..' )
mc.setParent( '..' )
#Create references and add row buttons
bottomLayout = mc.frameLayout('bottomLayout', label='' )
mc.rowColumnLayout(numberOfColumns=1, columnWidth=(1,705))
mc. button(label='Add file row', command= partial(addRow, topLayout, fileRows))
mc.button(label='Create references', command= partial(createReferences, fileRows))
mc.setParent( '..' )
mc.setParent( '..' )
mc.formLayout(form, e=1, af= (bottomLayout, 'bottom', 0) )
#Show
mc.showWindow(win)