def createAnimation(): """ Creates the node that houses a rig. Used to export animation. Returns: (pm.nodetypes.piperAnimation): Animation node created. """ scene_name = pm.sceneName().namebase name = scene_name if scene_name else 'piperAnimation' piper_animation = create('piperAnimation', 'dark green', name=pcfg.animation_prefix + name) attribute.lockAndHideCompound(piper_animation) rigs = get('piperRig', ignore='piperAnimation') pm.parent(rigs[0], piper_animation) if len(rigs) == 1 else pm.warning( '{} rigs found!'.format(str(len(rigs)))) return piper_animation
def createRig(name=''): """ Creates the node that houses all rig nodes. Args: name (string): If given, will use the given name as the name for the rig node. Returns: (pm.nodetypes.piperRig): Rig node created. """ name = name if name else 'piperRig' piper_rig = create('piperRig', 'burnt orange', name=name) piper_rig._.lock() attribute.nonKeyable(piper_rig.highPolyVisibility) attribute.lockAndHideCompound(piper_rig) attribute.addSeparator(piper_rig) return piper_rig
def createHigh(rigs=None): """ References in the high poly version of the mesh. Naming convention is important! See piper configs "high_poly" to correctly suffix high poly file and high poly geometry. Args: rigs (list): Rigs to create high-poly version of mesh. Returns: (list): Absolute path of references made. """ warnings = [] piper_rigs = [] references = {} high_references = [] skeleton_meshes = rig.getSkeletonMeshes(rigs=rigs) # get all the data more organized based on what file reference each mesh belongs to for mesh, data in skeleton_meshes.items(): reference = pm.FileReference(namespace=mesh.namespace()) # no need to operate on meshes that are already high poly if reference.namespace.startswith(pcfg.high_poly_namespace): continue if reference in references: references[reference].update({mesh: data}) else: references[reference] = {mesh: data} for reference, meshes in references.items(): deformers = [] skinned_mesh = None should_continue = False path, extension = os.path.splitext(reference.path) high_absolute = path + pcfg.high_poly_file_suffix + extension high_references.append(high_absolute) # cannot reference in path that does not exist if not os.path.exists(high_absolute): text = 'High poly path does not exist! ' + high_absolute warnings.append(text) pm.warning(text) continue for i, mesh in enumerate(meshes): if i == 0: # all meshes in reference should share same skinned mesh skinned_mesh = meshes[mesh]['skinned_mesh'] should_continue = skinned_mesh.wraps.get( ) # if there is data in wraps attribute, then high_poly exists if should_continue: text = skinned_mesh.name() + ' already has high poly!' warnings.append(text) pm.warning(text) break # getting relative path root_path = reference.path.split(reference.unresolvedPath())[0] high_relative = high_absolute.split(root_path)[-1] # reference in high poly, filter by transform, create group, group deformers, and parent group nodes = pm.createReference(high_relative, namespace=pcfg.high_poly_namespace, returnNewNodes=True) nodes = pm.ls(nodes, type='transform') group_name = os.path.basename( path) + pcfg.high_poly_file_suffix + pcfg.group_suffix group = pm.PyNode(group_name) if pm.objExists( group_name) else pm.group(name=group_name, em=True) deformers.append( group.name() ) # deformers will be serialized with json so using group name pm.parent(nodes, group) pm.parent(group, skinned_mesh) attribute.lockAndHideCompound(group) # finding the accompanying high poly mesh with a suffix swap high_name = pcfg.high_poly_namespace + ':' high_name += mesh.name(stripNamespace=True).replace( pcfg.low_poly_suffix, pcfg.high_poly_suffix) if not pm.objExists(high_name): text = mesh.name( ) + ' has no high poly equivalent! ' + high_name warnings.append(text) pm.warning(text) continue # create proximity wrap piper_rig = meshes[mesh]['rig'] piper_rigs.append(piper_rig) high_poly = pm.PyNode(high_name) deformer = skin.createProximityWrap(mesh, high_poly) deformers.append(deformer) piper_rig.highPolyVisibility >> high_poly.visibility if should_continue: continue # write the deformers and group onto the skinned mesh wraps attribute deformers_data = json.dumps(deformers) skinned_mesh.wraps.set(deformers_data) pm.select(piper_rigs) # finished referencing high poly, display warnings if any found warning_length = len(warnings) if warning_length == 0: pm.displayInfo( 'Finished referencing and proximity wrapping high-poly geometry') elif warning_length == 1: pm.warning('Finished with ONE warning: ' + warnings[0]) else: pm.warning( 'Finished with MULTIPLE warnings. Please see Script Editor for details.' ) return high_references
def reverse(driver, target, driven_negate=None, transform=None, switcher_ctrl=None, shape=curve.square, axis=None): """ Creates a control that offsets the given target through rotation (usually foot roll reverse rig). Args: driver (pm.nodetypes.Transform): The transform that drives the whole chain. Usually the IK handle. target (pm.nodetypes.Transform): Transform that will have control rotations added to. Usually end joint. driven_negate (pm.nodetypes.Transform): Transform that will have control rotations subtracted from. Usually any controls further down the chain/hierarchy of the given target. transform (pm.nodetypes.Transform): Transform for making the control. Useful for figuring out control size too. If None given, will try to use given driven_negate, if no driven_negate, will try to use given target. switcher_ctrl (pm.nodetypes.Transform): Transform that handles switching between FK and IK chains. shape (method): Creates the shape control that will drive reverse rig system. axis (string): Direction control will be facing when created. Returns: (pm.nodetypes.Transform): Control created. """ if not transform: transform = driven_negate if driven_negate else target # attempt to deduce axis if transform only has one child and axis is not given if not axis and transform.getChildren() and len(transform.getChildren()) == 1: axis_vector = pipermath.getOrientAxis(transform, transform.getChildren()[0]) axis = convert.axisToString(axis_vector) axis = convert.axisToTriAxis(axis)[1] # create control name = transform.name() + '_reverse' driver_parent = driver.getParent() ctrl = control.create(transform, shape, name, axis, 'burnt orange', 0.5, True, parent=driver_parent) name = ctrl.name() pm.parent(driver, ctrl) attribute.lockAndHideCompound(ctrl, ['t', 's']) target_source = target.rotate.connections(scn=True, plugs=True, destination=False) # add control's rotation to whatever is connected to target's rotate. if target_source: target_source = target_source[0] plus = pm.createNode('plusMinusAverage', n='_'.join([target.name(), 'plus', ctrl.name()])) target_source >> plus.input3D[0] ctrl.rotate >> plus.input3D[1] plus.output3D >> target.rotate else: ctrl.rotate >> target.rotate # if no driven negate given or driven negate is not being offset by the offsetParentMatrix, we are finished here if not driven_negate or not driven_negate.offsetParentMatrix.connections(scn=True, plugs=True, destination=False): return ctrl # decompose and compose matrices to get rotation value subtracted with control's rotation source_matrix = driven_negate.offsetParentMatrix.connections(scn=True, plugs=True, destination=False)[0] source_name = source_matrix.node().name() decomp_matrix = pm.createNode('decomposeMatrix', n=source_name + '_DM') compose_matrix = pm.createNode('composeMatrix', n=source_name + '_CM') source_matrix >> decomp_matrix.inputMatrix decomp_matrix.outputTranslate >> compose_matrix.inputTranslate decomp_matrix.outputScale >> compose_matrix.inputScale minus = pm.createNode('plusMinusAverage', n='_'.join([source_name, 'minus', name])) minus.operation.set(2) decomp_matrix.outputRotate >> minus.input3D[0] ctrl.rotate >> minus.input3D[1] minus.output3D >> compose_matrix.inputRotate compose_matrix.outputMatrix >> driven_negate.offsetParentMatrix attribute.addReverseMessage(ctrl, driven_negate) if switcher_ctrl: # add reverse control to switcher data and connect ik visibility onto reverse control switcher.addData(switcher_ctrl.attr(pcfg.switcher_reverses), [name]) switcher_attribute = switcher_ctrl.attr(pcfg.fk_ik_attribute) switcher_attribute >> ctrl.lodVisibility # add proxy fk_ik attribute to ctrl attribute.addSeparator(ctrl) ctrl.addAttr(pcfg.proxy_fk_ik, proxy=switcher_attribute, k=True, dv=0, hsx=True, hsn=True, smn=0, smx=1) # only make driven_negate by affected if IK is set to True blend = pm.createNode('blendMatrix', n=source_name + '_negateBlend') source_matrix >> blend.inputMatrix compose_matrix.outputMatrix >> blend.target[0].targetMatrix switcher_attribute >> blend.target[0].weight blend.outputMatrix >> driven_negate.offsetParentMatrix return ctrl
def banker(joint, ik_control, pivot_track=None, side='', use_track_shape=True): """ Creates a reverse foot control that changes pivot based on curve shape and control rotation input. Useful for banking. Args: joint (pm.nodetypes.Joint): Joint that will be driven by the reverse module and IK handle. ik_control (pm.nodetypes.Transform): Control that drives the IK Handle. pivot_track (pm.nodetypes.Transform): With a NurbsCurve shape as child that will act as the track for the pivot. side (string or None): Side to generate cross section use_track_shape (boolean): If True, will use the pivot track shape as the control shape Returns: (pm.nodetypes.Transform): Control that moves the reverse foot pivot. """ joint_name = joint.name() prefix = joint_name + '_' axis = pipermath.getOrientAxis(joint.getParent(), joint) axes = convert.axisToTriAxis(axis) if not side: side = pcu.getSide(joint_name) # get the IK handle and validate there is only one ik_handle = list(set(ik_control.connections(skipConversionNodes=True, type='ikHandle'))) if len(ik_handle) != 1: pm.error('Needed only ONE ik_handle. {} found.'.format(str(len(ik_handle)))) ik_handle = ik_handle[0] # create a pivot track (cross section curve) if no pivot track (curve) is given if not pivot_track: # if IK joint given, get the name of the regular joint by stripping the ik prefix if joint_name.startswith(pcfg.ik_prefix): stripped_name = pcu.removePrefixes(joint_name, pcfg.ik_prefix) namespace_name = pcfg.skeleton_namespace + ':' + stripped_name search_joint = pm.PyNode(stripped_name) if pm.objExists(stripped_name) else pm.PyNode(namespace_name) else: search_joint = joint # tries to get the meshes influenced by the skin cluster connected to the joint skins = search_joint.future(type='skinCluster') meshes = {mesh for skin in skins for mesh in pm.skinCluster(skin, q=True, g=True)} if skins else None # create the pivot track curve pm.select(cl=True) pivot_track = curve.originCrossSection(meshes, side=side, name=prefix + 'pivotTrack') # validate that only one is made if len(pivot_track) != 1: pm.error('Needed only ONE curve! {} curves made. Try to specify a side.'.format(str(len(pivot_track)))) pivot_track = pivot_track[0] # create the pivot and the normalized pivot, move the norm pivot to joint and then to floor pivot = pm.group(em=True, name=prefix + 'Pivot') normalized_pivot = pm.group(em=True, name=prefix + 'normalizedPivot') pm.matchTransform(normalized_pivot, joint, pos=True, rot=False, scale=False) normalized_pivot.ty.set(0) xform.toOffsetMatrix(normalized_pivot) # figure out control size, create control, lock and hide axis, translate, and scale if ik_control.hasAttr(pcfg.proxy_fk_ik): switcher_control = switcher.get(ik_control) transforms = switcher.getData(switcher_control.attr(pcfg.switcher_transforms), cast=True) size = control.calculateSize(transforms[-1]) else: size = None if use_track_shape: ctrl = pm.duplicate(pivot_track, n=prefix + 'bank')[0] curve.color(ctrl, 'burnt orange') else: ctrl = control.create(joint, shape=curve.plus, name=prefix + 'bank', axis=axes[0], color='burnt orange', matrix_offset=True, size=size, inner=.125, outer=1.25) attribute.lockAndHide(ctrl.attr('r' + axes[0])) attribute.lockAndHideCompound(ctrl, ['t', 's']) # node to add small number small_add = pm.createNode('plusMinusAverage', n=prefix + 'plusSmallNumber') small_add.input1D[0].set(0.001) normalize_node = pm.createNode('vectorProduct', n=prefix + 'pivotNormal') normalize_node.operation.set(0) normalize_node.normalizeOutput.set(True) # adding a small amount to avoid division by zero ctrl.attr('r' + axes[1]) >> small_add.input1D[1] small_add.output1D >> normalize_node.attr('input1' + axes[2].upper()) # need to multiply the rotation by -1 negative_mult = pm.createNode('multDoubleLinear', n=prefix + 'negative') ctrl.attr('r' + axes[2]) >> negative_mult.input1 negative_mult.input2.set(-1) normalize_input_attribute = negative_mult.output normalize_input_attribute >> normalize_node.attr('input1' + axes[1].upper()) normalize_node.output >> normalized_pivot.translate # creating the normalized (circle) version of the cross section positions = [] duplicate_curve = pm.duplicate(pivot_track)[0] pm.move(0, 0, 0, duplicate_curve, rpr=True) cvs = duplicate_curve.numCVs() for cv in range(0, cvs): position = duplicate_curve.cv[cv].getPosition(space='world') position.normalize() positions.append(position) # delete the duplicate and finally make the normalize track. Make sure to close the curve and center pivots pm.delete(duplicate_curve) normalized_track = pm.curve(d=1, p=positions, k=range(len(positions)), ws=True, n=prefix + 'normalizedTrack') normalized_track = pm.closeCurve(normalized_track, replaceOriginal=True)[0] pm.xform(normalized_track, centerPivots=True) # move normalized track to joint, then to floor, and freeze transforms pm.matchTransform(normalized_track, joint, pos=True, rot=False, scale=False) normalized_track.ty.set(0) myu.freezeTransformations(normalized_track) decomposed_matrix = pm.createNode('decomposeMatrix', n=normalize_node + '_decompose') normalized_pivot.worldMatrix >> decomposed_matrix.inputMatrix nearest_point = pm.createNode('nearestPointOnCurve', n=prefix + 'nearestPoint') decomposed_matrix.outputTranslate >> nearest_point.inPosition normalized_track.getShape().worldSpace >> nearest_point.inputCurve curve_info = pm.createNode('pointOnCurveInfo', n=prefix + 'curveInfo') nearest_point.parameter >> curve_info.parameter pivot_track.getShape().worldSpace >> curve_info.inputCurve reverse_group = pm.group(em=True, n=prefix + 'reverse_grp') xform.parentMatrixConstraint(ik_control, reverse_group, offset=True) pm.parent([pivot, ctrl], reverse_group) # curve_info position is where the pivot goes! Connect something to it if you want to visualize it ctrl.r >> pivot.r curve_info.result.position >> pivot.rotatePivot # connect ik handle by letting the pivot drive it pm.parent(ik_handle, pivot) # make the pivot drive the joint's rotations joint.r.disconnect() xform.parentMatrixConstraint(pivot, joint, t=False, r=True, s=False, offset=True) # clean up by hiding curves pivot_track.visibility.set(False) normalized_track.visibility.set(False) ik_control.addAttr(pcfg.banker_attribute, dt='string', k=False, h=True, s=True) ik_control.attr(pcfg.banker_attribute).set(ctrl.name()) # hook up pivot control with fk_ik attribute if ik has an fk-ik proxy if ik_control.hasAttr(pcfg.proxy_fk_ik): switcher_control = switcher.get(ik_control) switcher_attribute = switcher_control.attr(pcfg.fk_ik_attribute) switcher_attribute >> ctrl.lodVisibility attribute.addSeparator(ctrl) ctrl.addAttr(pcfg.proxy_fk_ik, proxy=switcher_attribute, k=True, dv=0, hsx=True, hsn=True, smn=0, smx=1) return ctrl
def IK(start, end, parent=None, shape=curve.ring, sizes=None, connect=True): """ Creates IK controls and IK RP solver and for the given start and end joints. Args: start (pm.nodetypes.Joint): Start of the joint chain. end (pm.nodetypes.Joint): End of the joint chain. parent (pm.nodetypes.Transform): Parent of start control. shape (method): Creates the shape control that will drive joints. sizes (list): Sizes to use for each control. connect (bool): If True, connects the duplicate FK chain to the given start/end transforms to be driven. Returns: (list): Controls created in order from start to end. """ axis = None mid_ctrl = None start_ctrl = None controls = [] transforms = xform.getChain(start, end) duplicates = xform.duplicateChain(transforms, prefix=pcfg.ik_prefix, color='purple', scale=0.5) mid = pcu.getMedian(transforms) mid_duplicate = pcu.getMedian(duplicates) if mid == start or mid == end: pm.error('Not enough joints given! {} is the mid joint?'.format(mid.name())) for i, (transform, duplicate) in enumerate(zip(transforms, duplicates)): name = duplicate.name(stripNamespace=True) size = sizes[i] if sizes else control.calculateSize(transform) ctrl_parent = parent if transform == transforms[0] else None if transform != transforms[-1]: next_transform = transforms[i + 1] axis_vector = pipermath.getOrientAxis(transform, next_transform) axis = convert.axisToString(axis_vector) # start if transform == transforms[0]: ctrl = control.create(duplicate, name=name, axis=axis, shape=shape, size=size) attribute.bindConnect(transform, ctrl, ctrl_parent) start_ctrl = ctrl # mid elif transform == mid: ctrl = control.create(duplicate, curve.orb, name, axis, scale=0.01, matrix_offset=False, size=size) translation, rotate, scale, _ = xform.calculatePoleVector(start, mid, end) pm.xform(ctrl, t=translation, ro=rotate, s=scale) mid_ctrl = ctrl # end elif transform == transforms[-1]: ctrl = control.create(duplicate, name=name, axis=axis, shape=pipernode.createIK, control_shape=shape, size=size) attribute.bindConnect(transform, ctrl) attribute.uniformScale(ctrl, axis) else: ctrl = control.create(duplicate, name=name, axis=axis, shape=shape, size=size) if connect: xform.parentMatrixConstraint(duplicate, transform) controls.append(ctrl) piper_ik = controls[-1] mid.attr(pcfg.length_attribute) >> piper_ik.startInitialLength transforms[-1].attr(pcfg.length_attribute) >> piper_ik.endInitialLength if axis.startswith('n'): piper_ik.direction.set(-1) axis = axis.lstrip('n') # connect controls to joints, and make ik handle decompose = xform.parentMatrixConstraint(start_ctrl, duplicates[0], t=True, r=False, s=True) xform.parentMatrixConstraint(piper_ik, duplicates[-1], t=False) ik_handle_name = duplicates[-1].name(stripNamespace=True) + '_handle' ik_handle, _ = pm.ikHandle(sj=duplicates[0], ee=duplicates[-1], sol='ikRPsolver', n=ik_handle_name, pw=1, w=1) pm.parent(ik_handle, piper_ik) pipermath.zeroOut(ik_handle) ik_handle.translate >> piper_ik.handleTranslate ik_handle.parentMatrix >> piper_ik.handleParentMatrix # xform.poleVectorMatrixConstraint(ik_handle, mid_ctrl) attribute.addSeparator(mid_ctrl) mid_ctrl.addAttr('poleVectorWeight', k=True, dv=1, min=0, max=1) constraint = pm.poleVectorConstraint(mid_ctrl, ik_handle) mid_ctrl.poleVectorWeight >> constraint.attr(mid_ctrl.name() + 'W0') # connect the rest start_ctrl.worldMatrix >> piper_ik.startMatrix mid_ctrl.worldMatrix >> piper_ik.poleVectorMatrix piper_ik.startOutput >> mid_duplicate.attr('t' + axis) piper_ik.endOutput >> duplicates[-1].attr('t' + axis) piper_ik.twist >> ik_handle.twist piper_ik._.lock() # scale ctrl connect pipernode.multiply(duplicates[0], decompose.attr('outputScale' + axis.upper()), inputs=[piper_ik.startOutputScale]) pipernode.multiply(mid_duplicate, mid_ctrl.attr('s' + axis), inputs=[piper_ik.endOutputScale]) attribute.uniformScale(mid_ctrl, axis) # parent pole vector to end control and create pm.parent(mid_ctrl, piper_ik) xform.toOffsetMatrix(mid_ctrl) space.create([start_ctrl], mid_ctrl) mid_ctrl.useScale.set(False) attribute.lockAndHideCompound(mid_ctrl, ['r']) # preferred angle connection mid_bind = convert.toBind(mid, pm.warning) if mid_bind: mid_bind.preferredAngle >> piper_ik.preferredAngleInput piper_ik.preferredAngleOutput >> mid_duplicate.preferredAngle return duplicates, controls