import sys import math import random import traceback import maya.mel as mel import pymel.core as pm import maya.OpenMaya as OpenMaya import maya.OpenMayaUI as OpenMayaUI import maya.OpenMayaMPx as OpenMayaMPx import maya.OpenMayaRender as OpenMayaRender kPluginVersion = "1.1.0" kPluginCmdName = "instanceAlongCurve" kPluginCtxCmdName = "instanceAlongCurveCtx" kPluginNodeName = 'instanceAlongCurveLocator' kPluginManipNodeName = 'instanceAlongCurveLocatorManip' kPluginNodeClassify = 'utility/general' kPluginNodeId = OpenMaya.MTypeId( 0x55555 ) kPluginNodeManipId = OpenMaya.MTypeId( 0x55556 ) class instanceAlongCurveLocator(OpenMayaMPx.MPxLocatorNode): # Simple container class for compound vector attributes class Vector3CompoundAttribute(object): def __init__(self): self.compound = OpenMaya.MObject() self.x = OpenMaya.MObject() self.y = OpenMaya.MObject() self.z = OpenMaya.MObject() class CurveAxisHandleAttribute(object): def __init__(self): self.compound = OpenMaya.MObject() self.parameter = OpenMaya.MObject() self.angle = OpenMaya.MObject() # The angle over the tangent axis # Legacy attributes to support backward compatibility legacyInputTransformAttr = OpenMaya.MObject() # Input attributes inputCurveAttr = OpenMaya.MObject() inputTransformAttr = OpenMaya.MObject() inputShadingGroupAttr = OpenMaya.MObject() # Translation offsets inputLocalTranslationOffsetAttr = OpenMaya.MObject() inputGlobalTranslationOffsetAttr = OpenMaya.MObject() # Rotation offsets inputLocalRotationOffsetAttr = OpenMaya.MObject() inputGlobalRotationOffsetAttr = OpenMaya.MObject() # Scale offset inputLocalScaleOffsetAttr = OpenMaya.MObject() # Instance count related attributes instanceCountAttr = OpenMaya.MObject() instancingModeAttr = OpenMaya.MObject() instanceLengthAttr = OpenMaya.MObject() maxInstancesByLengthAttr = OpenMaya.MObject() # Curve axis data, to be manipulated by user enableManipulatorsAttr = OpenMaya.MObject() curveAxisHandleAttr = CurveAxisHandleAttribute() curveAxisHandleCountAttr = OpenMaya.MObject() displayTypeAttr = OpenMaya.MObject() bboxAttr = OpenMaya.MObject() orientationModeAttr = OpenMaya.MObject() inputLocalOrientationAxisAttr = OpenMaya.MObject() class RampAttributes(object): def __init__(self): self.ramp = OpenMaya.MObject() # normalized ramp self.rampOffset = OpenMaya.MObject() # evaluation offset for ramp self.rampAxis = OpenMaya.MObject() # ramp normalized axis self.rampAmplitude = OpenMaya.MObject() # ramp amplitude self.rampRandomAmplitude = OpenMaya.MObject() # ramp random amplitude self.rampRepeat = OpenMaya.MObject() # Simple container class for compound vector attributes class RampValueContainer(object): def __init__(self, mObject, dataBlock, rampAttr, normalize, instanceCount): self.ramp = OpenMaya.MRampAttribute(OpenMaya.MPlug(mObject, rampAttr.ramp)) self.rampOffset = dataBlock.inputValue(rampAttr.rampOffset).asFloat() self.rampRandomAmplitude = dataBlock.inputValue(rampAttr.rampRandomAmplitude).asFloat() self.rampAmplitude = dataBlock.inputValue(rampAttr.rampAmplitude).asFloat() self.rampRepeat = dataBlock.inputValue(rampAttr.rampRepeat).asFloat() if normalize: self.rampAxis = dataBlock.inputValue(rampAttr.rampAxis.compound).asVector().normal() else: self.rampAxis = dataBlock.inputValue(rampAttr.rampAxis.compound).asVector() self.useDynamicAmplitudeValues = False amplitudePlug = OpenMaya.MPlug(mObject, rampAttr.rampAmplitude) if amplitudePlug.isConnected(): # Get connected input plugs connections = OpenMaya.MPlugArray() amplitudePlug.connectedTo(connections, True, False) # Find input transform if connections.length() == 1: node = connections[0].node() nodeFn = OpenMaya.MFnDependencyNode(node) resultColors = OpenMaya.MFloatVectorArray() resultTransparencies = OpenMaya.MFloatVectorArray() uValues = OpenMaya.MFloatArray(instanceCount, 0.0) vValues = OpenMaya.MFloatArray(instanceCount, 0.0) # Sample a line, for more user flexibility for i in xrange(instanceCount): uValues.set(i / float(instanceCount), i) vValues.set(i / float(instanceCount), i) # For now... then we can just use the plug (TODO) if(node.hasFn(OpenMaya.MFn.kTexture2d)): OpenMayaRender.MRenderUtil.sampleShadingNetwork(nodeFn.name() + ".outColor", instanceCount, False, False, OpenMaya.MFloatMatrix(), None, uValues, vValues, None, None, None, None, None, resultColors, resultTransparencies) self.rampAmplitudeValues = [] self.useDynamicAmplitudeValues = True for i in xrange(resultColors.length()): self.rampAmplitudeValues.append(resultColors[i].length() / math.sqrt(3)) # Ramps base offset distOffsetAttr = OpenMaya.MObject() # Normalized thresholds for curve evaluation curveStartAttr = OpenMaya.MObject() curveEndAttr = OpenMaya.MObject() # Ramp attributes positionRampAttr = RampAttributes() rotationRampAttr = RampAttributes() scaleRampAttr = RampAttributes() # Output vectors outputTranslationAttr = Vector3CompoundAttribute() outputRotationAttr = Vector3CompoundAttribute() outputScaleAttr = Vector3CompoundAttribute() def __init__(self): OpenMayaMPx.MPxLocatorNode.__init__(self) def postConstructor(self): OpenMaya.MFnDependencyNode(self.thisMObject()).setName("instanceAlongCurveLocatorShape#") self.callbackId = OpenMaya.MNodeMessage.addAttributeChangedCallback(self.thisMObject(), self.attrChangeCallback) self.updateInstanceConnections() # Find original SG to reassign it to instance def getShadingGroup(self): inputSGPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.inputShadingGroupAttr) sgNode = getSingleSourceObjectFromPlug(inputSGPlug) if sgNode is not None and sgNode.hasFn(OpenMaya.MFn.kSet): return OpenMaya.MFnSet(sgNode) return None def assignShadingGroup(self, fnDagNode): fnSet = self.getShadingGroup() if fnSet is not None: # Easiest, cleanest way seems to be calling MEL. # sets command handles everything, even nested instanced dag paths mdgm = OpenMaya.MDGModifier() mdgm.commandToExecute("sets -e -nw -fe " + fnSet.name() + " " + fnDagNode.name()) mdgm.doIt() # Helper function to get an array of available logical indices from the sparse array # TODO: maybe it can be precalculated? def getAvailableLogicalIndices(self, plug, numIndices): # Allocate and initialize outIndices = OpenMaya.MIntArray(numIndices) indices = OpenMaya.MIntArray(plug.numElements()) plug.getExistingArrayAttributeIndices(indices) currentAvailableIndex = 0 indicesFound = 0 # Assuming indices are SORTED :) for i in indices: connectedPlug = plug.elementByLogicalIndex(i).isConnected() # Iteratively find available indices in the sparse array while i > currentAvailableIndex: outIndices[indicesFound] = currentAvailableIndex indicesFound += 1 currentAvailableIndex += 1 # Check against this index, add it if it is not connected if i == currentAvailableIndex and not connectedPlug: outIndices[indicesFound] = currentAvailableIndex indicesFound += 1 currentAvailableIndex += 1 if indicesFound == numIndices: return outIndices # Fill remaining expected indices for i in xrange(indicesFound, numIndices): outIndices[i] = currentAvailableIndex currentAvailableIndex += 1 return outIndices def getNodeTransformFn(self): dagNode = OpenMaya.MFnDagNode(self.thisMObject()) dagPath = OpenMaya.MDagPath() dagNode.getPath(dagPath) return OpenMaya.MFnDagNode(dagPath.transform()) def updateInstanceConnections(self): # If the locator is being instanced, just stop updating its children. # This is to prevent losing references to the locator instances' children # If you want to change this locator, prepare the source before instantiating if OpenMaya.MFnDagNode(self.thisMObject()).isInstanced(): return OpenMaya.kUnknownParameter # Plugs outputTranslationPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.outputTranslationAttr.compound) outputRotationPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.outputRotationAttr.compound) outputScalePlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.outputScaleAttr.compound) expectedInstanceCount = self.getInstanceCountByMode() numConnectedElements = outputTranslationPlug.numConnectedElements() # Only instance if we are missing elements # TODO: handle mismatches in translation/rotation plug connected elements (user deleted a plug? use connectionBroken method?) if numConnectedElements < expectedInstanceCount: inputTransformFn = self.getInputTransformFn() if inputTransformFn is not None: rotatePivot = inputTransformFn.rotatePivot(OpenMaya.MSpace.kTransform ) scalePivot = inputTransformFn.scalePivot(OpenMaya.MSpace.kTransform ) transformFn = self.getNodeTransformFn() newInstancesCount = expectedInstanceCount - numConnectedElements availableIndices = self.getAvailableLogicalIndices(outputTranslationPlug, newInstancesCount) displayPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.displayTypeAttr) LODPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.bboxAttr) mdgModifier = OpenMaya.MDagModifier() for i in availableIndices: # Instance transform # InstanceLeaf must be set to False to prevent crashes :) trInstance = inputTransformFn.duplicate(True, False) instanceFn = OpenMaya.MFnTransform(trInstance) # Parent new instance transformFn.addChild(trInstance) # Pivots instanceFn.setRotatePivot(rotatePivot, OpenMaya.MSpace.kTransform , False) instanceFn.setScalePivot(scalePivot, OpenMaya.MSpace.kTransform , False) instanceTranslatePlug = instanceFn.findPlug('translate', False) outputTranslationPlugElement = outputTranslationPlug.elementByLogicalIndex(i) instanceRotationPlug = instanceFn.findPlug('rotate', False) outputRotationPlugElement = outputRotationPlug.elementByLogicalIndex(i) instanceScalePlug = instanceFn.findPlug('scale', False) outputScalePlugElement = outputScalePlug.elementByLogicalIndex(i) # Make instance visible instanceFn.findPlug("visibility", False).setBool(True) # Enable drawing overrides overrideEnabledPlug = instanceFn.findPlug("overrideEnabled", False) overrideEnabledPlug.setBool(True) instanceDisplayPlug = instanceFn.findPlug("overrideDisplayType", False) instanceLODPlug = instanceFn.findPlug("overrideLevelOfDetail", False) if not outputTranslationPlugElement.isConnected(): mdgModifier.connect(outputTranslationPlugElement, instanceTranslatePlug) if not outputRotationPlugElement.isConnected(): mdgModifier.connect(outputRotationPlugElement, instanceRotationPlug) if not outputScalePlugElement.isConnected(): mdgModifier.connect(outputScalePlugElement, instanceScalePlug) if not instanceDisplayPlug.isConnected(): mdgModifier.connect(displayPlug, instanceDisplayPlug) if not instanceLODPlug.isConnected(): mdgModifier.connect(LODPlug, instanceLODPlug) mdgModifier.doIt() # Finally, assign SG to all children self.assignShadingGroup(transformFn) # Remove instances if necessary elif numConnectedElements > expectedInstanceCount: connections = OpenMaya.MPlugArray() toRemove = numConnectedElements - expectedInstanceCount mdgModifier = OpenMaya.MDGModifier() for i in xrange(toRemove): outputTranslationPlugElement = outputTranslationPlug.connectionByPhysicalIndex(numConnectedElements - 1 - i) outputTranslationPlugElement.connectedTo(connections, False, True) for c in xrange(connections.length()): mdgModifier.deleteNode(connections[c].node()) mdgModifier.doIt() def attrChangeCallback(self, msg, plug, otherPlug, clientData): incomingDirection = (OpenMaya.MNodeMessage.kIncomingDirection & msg) == OpenMaya.MNodeMessage.kIncomingDirection attributeSet = (OpenMaya.MNodeMessage.kAttributeSet & msg) == OpenMaya.MNodeMessage.kAttributeSet isCorrectAttribute = (plug.attribute() == instanceAlongCurveLocator.instanceCountAttr) isCorrectAttribute = isCorrectAttribute or (plug.attribute() == instanceAlongCurveLocator.instancingModeAttr) isCorrectAttribute = isCorrectAttribute or (plug.attribute() == instanceAlongCurveLocator.instanceLengthAttr) isCorrectAttribute = isCorrectAttribute or (plug.attribute() == instanceAlongCurveLocator.maxInstancesByLengthAttr) isCorrectAttribute = isCorrectAttribute or (plug.attribute() == instanceAlongCurveLocator.curveStartAttr) isCorrectAttribute = isCorrectAttribute or (plug.attribute() == instanceAlongCurveLocator.curveEndAttr) isCorrectNode = OpenMaya.MFnDependencyNode(plug.node()).typeName() == kPluginNodeName try: if isCorrectNode and isCorrectAttribute and attributeSet and incomingDirection: self.updateInstanceConnections() except: sys.stderr.write('Failed trying to update instances. stack trace: \n') sys.stderr.write(traceback.format_exc()) def getInputTransformPlug(self): # Backward compatibility inputTransformPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.inputTransformAttr) legacyInputTransformPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.legacyInputTransformAttr) if(legacyInputTransformPlug.isConnected()): inputTransformPlug = legacyInputTransformPlug return inputTransformPlug def getInputTransformFn(self): inputTransformPlug = self.getInputTransformPlug() transform = getSingleSourceObjectFromPlug(inputTransformPlug) # Get Fn from a DAG path to get the world transformations correctly if transform is not None and transform.hasFn(OpenMaya.MFn.kTransform): path = OpenMaya.MDagPath() trFn = OpenMaya.MFnDagNode(transform) trFn.getPath(path) return OpenMaya.MFnTransform(path) return None def getCurveFn(self): inputCurvePlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.inputCurveAttr) curve = getSingleSourceObjectFromPlug(inputCurvePlug) # Get Fn from a DAG path to get the world transformations correctly if curve is not None: path = OpenMaya.MDagPath() trFn = OpenMaya.MFnDagNode(curve) trFn.getPath(path) path.extendToShape() if path.node().hasFn(OpenMaya.MFn.kNurbsCurve): return OpenMaya.MFnNurbsCurve(path) return None # Calculate expected instances by the instancing mode def getInstanceCountByMode(self): instancingModePlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.instancingModeAttr) inputCurvePlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.inputCurveAttr) if inputCurvePlug.isConnected() and instancingModePlug.asInt() == 1: instanceLengthPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.instanceLengthAttr) maxInstancesByLengthPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.maxInstancesByLengthAttr) curveFn = self.getCurveFn() # Known issue: even if the curve fn is dag path constructed, its length is not worldspace... # If you want perfect distance-based instancing, freeze the transformations of the curve curveLength = curveFn.length() curveStart = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.curveStartAttr).asFloat() * curveLength curveEnd = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.curveEndAttr).asFloat() * curveLength effectiveCurveLength = min(max(curveEnd - curveStart, 0.001), curveLength) return min(maxInstancesByLengthPlug.asInt(), int(math.ceil(effectiveCurveLength / instanceLengthPlug.asFloat()))) instanceCountPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.instanceCountAttr) return instanceCountPlug.asInt() def getRandomizedValueUnified(self, randomValue, randomAmplitude, value): return (randomValue * 2.0 - 1.0) * randomAmplitude + value def getRandomizedValue(self, random, randomAmplitude, value): return (random.random() * 2.0 - 1.0) * randomAmplitude + value # Calculate expected instances by the instancing mode def getIncrementByMode(self, count, effectiveCurveLength): instancingModePlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.instancingModeAttr) # Distance defined manually if instancingModePlug.asInt() == 1: instanceLengthPlug = OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.instanceLengthAttr) return instanceLengthPlug.asFloat() # Distance driven by count return effectiveCurveLength / float(count) def updateInstancePositions(self, curveFn, dataBlock, count, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement, inputTransformPlug, inputTransformFn, axisHandlesSorted): # Common data translateArrayHandle = dataBlock.outputArrayValue(instanceAlongCurveLocator.outputTranslationAttr.compound) curveLength = curveFn.length() maxParam = curveFn.findParamFromLength(curveLength) curveForm = curveFn.form() # Important: enums are short! If not, the resulting int may be incorrect rotMode = dataBlock.inputValue(instanceAlongCurveLocator.orientationModeAttr).asShort() localRotationAxisMode = dataBlock.inputValue(instanceAlongCurveLocator.inputLocalOrientationAxisAttr).asShort() if localRotationAxisMode == 0: forward = OpenMaya.MVector.xAxis up = OpenMaya.MVector.yAxis right = OpenMaya.MVector.zAxis elif localRotationAxisMode == 1: forward = OpenMaya.MVector.yAxis up = OpenMaya.MVector.zAxis right = OpenMaya.MVector.xAxis elif localRotationAxisMode == 2: forward = OpenMaya.MVector.zAxis up = OpenMaya.MVector.yAxis right = OpenMaya.MVector.xAxis # We use Z axis as forward, and adjust locally to that axis referenceAxis = OpenMaya.MVector.zAxis referenceUp = OpenMaya.MVector.yAxis # Local offset is not considered for position localRotation = forward.rotateTo(referenceAxis) # Manipulator data enableManipulators = dataBlock.inputValue(instanceAlongCurveLocator.enableManipulatorsAttr).asBool() # Local translation offsets localTranslationOffset = dataBlock.inputValue(instanceAlongCurveLocator.inputLocalTranslationOffsetAttr.compound).asVector() globalTranslationOffset = dataBlock.inputValue(instanceAlongCurveLocator.inputGlobalTranslationOffsetAttr.compound).asVector() # Get pivot rotatePivot = OpenMaya.MVector() if inputTransformPlug.isConnected(): rotatePivot = OpenMaya.MVector(inputTransformFn.rotatePivot(OpenMaya.MSpace.kTransform )) rotatePivot += OpenMaya.MVector(inputTransformFn.rotatePivotTranslation(OpenMaya.MSpace.kTransform )) # Deterministic random random.seed(count) rampValues = instanceAlongCurveLocator.RampValueContainer(self.thisMObject(), dataBlock, instanceAlongCurveLocator.positionRampAttr, False, count) inputTransformRotation = OpenMaya.MQuaternion() # First, map parameter if inputTransformPlug.isConnected(): inputTransformFn.getRotation(inputTransformRotation, OpenMaya.MSpace.kWorld) # Make sure there are enough handles... for i in xrange(min(count, translateArrayHandle.elementCount())): dist = math.fmod(curveStart + math.fmod(lengthIncrement * i + distOffset, effectiveCurveLength), curveLength) param = max( min( curveFn.findParamFromLength( dist ), maxParam ), 0.0) # Ramps are not modified by curve start/end, so objects can "slide" normalizedDistance = dist / curveFn.length() rampValue = self.getRampValueAtNormalizedPosition(rampValues, normalizedDistance) # Get the actual point on the curve... point = OpenMaya.MPoint() curveFn.getPointAtParam(param, point) tangent = curveFn.tangent(param) rot = referenceAxis.rotateTo(tangent) # If the axis is parallel, but with inverse direction, rotate it PI over the up vector if referenceAxis.isParallel(tangent) and (referenceAxis * tangent < 0): rot = OpenMaya.MQuaternion(math.pi, referenceUp) # Transform rotation so that it is aligned with the tangent. This fixes unintentional twisting rot = localRotation * rot # Modify resulting rotation based on mode if rotMode == 0: # Identity rot = OpenMaya.MQuaternion() elif rotMode == 1: # Input rotation rot = inputTransformRotation; elif rotMode == 3 and i % 2 == 1: # Chain mode, interesting for position ;) rot *= OpenMaya.MQuaternion(math.pi * .5, tangent) # Get the angle from handles, and rotate over tangent axis if enableManipulators: angle = self.getRotationForParam(param, axisHandlesSorted, curveForm, maxParam) rot = rot * OpenMaya.MQuaternion(-angle, tangent) # The curve basis used for twisting basisForward = forward.rotateBy(rot) basisUp = up.rotateBy(rot) basisRight = right.rotateBy(rot) rampAmplitude = self.getRampAmplitudeForInstance(rampValues, i) twistNormal = basisRight * self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.x twistTangent = basisUp * self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.y twistBitangent = basisForward * self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.z twist = (twistNormal + twistTangent + twistBitangent) # Twist + global offset, without pivot point += twist + globalTranslationOffset - rotatePivot # Local offset point += basisRight * localTranslationOffset.x + basisUp * localTranslationOffset.y + basisForward * localTranslationOffset.z translateArrayHandle.jumpToArrayElement(i) translateHandle = translateArrayHandle.outputValue() translateHandle.set3Double(point.x, point.y, point.z) translateArrayHandle.setAllClean() translateArrayHandle.setClean() def getRampAmplitudeForInstance(self, rampValues, instanceIndex): if rampValues.useDynamicAmplitudeValues: if len(rampValues.rampAmplitudeValues) > instanceIndex: return rampValues.rampAmplitudeValues[instanceIndex] return rampValues.rampAmplitude def getRampValueAtNormalizedPosition(self, rampValues, v): util = OpenMaya.MScriptUtil() util.createFromDouble(0.0) valuePtr = util.asFloatPtr() position = math.fmod((v * rampValues.rampRepeat) + rampValues.rampOffset, 1.0) rampValues.ramp.getValueAtPosition(position, valuePtr) return util.getFloat(valuePtr) def updateInstanceScale(self, curveFn, dataBlock, count, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement): point = OpenMaya.MPoint() curveLength = curveFn.length() maxParam = curveFn.findParamFromLength(curveLength) scaleArrayHandle = dataBlock.outputArrayValue(instanceAlongCurveLocator.outputScaleAttr.compound) localScaleOffset = dataBlock.inputValue(instanceAlongCurveLocator.inputLocalScaleOffsetAttr.compound).asVector() # Deterministic random random.seed(count) rampValues = instanceAlongCurveLocator.RampValueContainer(self.thisMObject(), dataBlock, instanceAlongCurveLocator.scaleRampAttr, False, count) # Make sure there are enough handles... for i in xrange(min(count, scaleArrayHandle.elementCount())): dist = math.fmod(curveStart + math.fmod(lengthIncrement * i + distOffset, effectiveCurveLength), curveLength) param = max( min( curveFn.findParamFromLength( dist ), maxParam ), 0.0) # Ramps are not modified by curve start/end, so objects can "slide" normalizedDistance = dist / curveFn.length() rampValue = self.getRampValueAtNormalizedPosition(rampValues, normalizedDistance) unifiedRandom = random.random() rampAmplitude = self.getRampAmplitudeForInstance(rampValues, i) # Scales are unified... because it makes more sense point.x = localScaleOffset.x + self.getRandomizedValueUnified(unifiedRandom, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.x point.y = localScaleOffset.y + self.getRandomizedValueUnified(unifiedRandom, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.y point.z = localScaleOffset.z + self.getRandomizedValueUnified(unifiedRandom, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.z scaleArrayHandle.jumpToArrayElement(i) scaleHandle = scaleArrayHandle.outputValue() scaleHandle.set3Double(point.x, point.y, point.z) scaleArrayHandle.setAllClean() scaleArrayHandle.setClean() # TODO: cache this data to prevent recalculating when there is no manipulator being updated def getRotationForParam(self, param, axisHandlesSorted, curveForm, curveMaxParam): indexRange = (-1, -1) wrapAround = not (curveForm is OpenMaya.MFnNurbsCurve.kOpen) # Find the range of indices that make up this curve segment for i in xrange(len(axisHandlesSorted)): # TODO: could use a binary search if param < axisHandlesSorted[i][1]: if i > 0: indexRange = (i - 1, i) break elif wrapAround: indexRange = (len(axisHandlesSorted) - 1, 0) break else: indexRange = (0, 0) break # Edge case if indexRange[0] == -1 and indexRange[1] == -1 and len(axisHandlesSorted) > 0: if wrapAround: indexRange = (len(axisHandlesSorted) - 1, 0) else: indexRange = (len(axisHandlesSorted) - 1, len(axisHandlesSorted) - 1) # Now find the lerp value based on the range if indexRange[0] > -1 and indexRange[1] > -1: minParam = axisHandlesSorted[indexRange[0]][1] maxParam = axisHandlesSorted[indexRange[1]][1] minAxis = axisHandlesSorted[indexRange[0]][2] maxAxis = axisHandlesSorted[indexRange[1]][2] if(math.fabs(minParam - maxParam) > 0.001): if minParam > maxParam and wrapAround: if param < maxParam: param = param + curveMaxParam maxParam = maxParam + curveMaxParam t = min(max((param - minParam) / (maxParam - minParam), 0.0), 1.0) return minAxis + (maxAxis - minAxis) * t return minAxis return 0.0 def updateInstanceRotations(self, curveFn, dataBlock, count, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement, inputTransformPlug, inputTransformFn, axisHandlesSorted): # Common data curveLength = curveFn.length() maxParam = curveFn.findParamFromLength(curveLength) curveForm = curveFn.form() rotationArrayHandle = dataBlock.outputArrayValue(instanceAlongCurveLocator.outputRotationAttr.compound) # All offsets are in degrees localRotationOffset = dataBlock.inputValue(instanceAlongCurveLocator.inputLocalRotationOffsetAttr.compound).asVector() * math.radians(1) globalRotationOffset = dataBlock.inputValue(instanceAlongCurveLocator.inputGlobalRotationOffsetAttr.compound).asVector() * math.radians(1) localRotationOffset = OpenMaya.MEulerRotation(localRotationOffset.x, localRotationOffset.y, localRotationOffset.z).asQuaternion() globalRotationOffset = OpenMaya.MEulerRotation(globalRotationOffset.x, globalRotationOffset.y, globalRotationOffset.z).asQuaternion() # Important: enums are short! If not, the resulting int may be incorrect rotMode = dataBlock.inputValue(instanceAlongCurveLocator.orientationModeAttr).asShort() localRotationAxisMode = dataBlock.inputValue(instanceAlongCurveLocator.inputLocalOrientationAxisAttr).asShort() if localRotationAxisMode == 0: forward = OpenMaya.MVector.xAxis up = OpenMaya.MVector.yAxis right = OpenMaya.MVector.zAxis elif localRotationAxisMode == 1: forward = OpenMaya.MVector.yAxis up = OpenMaya.MVector.zAxis right = OpenMaya.MVector.xAxis elif localRotationAxisMode == 2: forward = OpenMaya.MVector.zAxis up = OpenMaya.MVector.yAxis right = OpenMaya.MVector.xAxis # We use Z axis as forward, and adjust locally to that axis referenceAxis = OpenMaya.MVector.zAxis referenceUp = OpenMaya.MVector.yAxis # Rotation to align selected (local) forward axis to the reference forward axis (which is aligned with tangent) localRotation = localRotationOffset * forward.rotateTo(referenceAxis) # Deterministic random random.seed(count) rampValues = instanceAlongCurveLocator.RampValueContainer(self.thisMObject(), dataBlock, instanceAlongCurveLocator.rotationRampAttr, True, count) # Manipulator stuff enableManipulators = dataBlock.inputValue(instanceAlongCurveLocator.enableManipulatorsAttr).asBool() # Original transform data inputTransformRotation = OpenMaya.MQuaternion() # First, map parameter if inputTransformPlug.isConnected(): inputTransformFn.getRotation(inputTransformRotation, OpenMaya.MSpace.kWorld) for i in xrange(min(count, rotationArrayHandle.elementCount())): dist = math.fmod(curveStart + math.fmod(lengthIncrement * i + distOffset, effectiveCurveLength), curveLength) param = max( min( curveFn.findParamFromLength( dist ), maxParam ), 0.0) # Ramps are not modified by curve start/end, so objects can "slide" normalizedDistance = dist / curveFn.length() rampValue = self.getRampValueAtNormalizedPosition(rampValues, normalizedDistance) tangent = curveFn.tangent(param) # Reference axis (Z) is now aligned with tangent rot = referenceAxis.rotateTo(tangent) # If the axis is parallel, but with inverse direction, rotate it PI over the up vector if referenceAxis.isParallel(tangent) and (referenceAxis * tangent < 0): rot = OpenMaya.MQuaternion(math.pi, referenceUp) # Rotate local axis to align with tangent rot = localRotation * rot # The curve basis used for twisting basisForward = forward.rotateBy(rot) basisUp = up.rotateBy(rot) basisRight = right.rotateBy(rot) rampAmplitude = self.getRampAmplitudeForInstance(rampValues, i) twistNormal = self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.x twistNormal = OpenMaya.MQuaternion(math.radians(twistNormal), basisRight) #X twistTangent = self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.y twistTangent = OpenMaya.MQuaternion(math.radians(twistTangent), basisUp) #Y twistBitangent = self.getRandomizedValue(random, rampValues.rampRandomAmplitude, rampValue * rampAmplitude) * rampValues.rampAxis.z twistBitangent = OpenMaya.MQuaternion(math.radians(twistBitangent), basisForward) #Z # Modify resulting rotation based on mode if rotMode == 0: # Identity rot = OpenMaya.MQuaternion() elif rotMode == 1: # Input rotation rot = inputTransformRotation; elif rotMode == 3 and i % 2 == 1: # Chain mode rot *= OpenMaya.MQuaternion(math.pi * .5, tangent) # Get the angle from handles, and rotate over tangent axis if enableManipulators: angle = self.getRotationForParam(param, axisHandlesSorted, curveForm, maxParam) rot = rot * OpenMaya.MQuaternion(-angle, tangent) rot = ((rot * twistNormal * twistTangent * twistBitangent) * globalRotationOffset).asEulerRotation().asVector() rotationArrayHandle.jumpToArrayElement(i) rotationHandle = rotationArrayHandle.outputValue() rotationHandle.set3Double(rot.x, rot.y, rot.z) rotationArrayHandle.setAllClean() rotationArrayHandle.setClean() def isBounded(self): return True def boundingBox(self): return OpenMaya.MBoundingBox(OpenMaya.MPoint(-1,-1,-1), OpenMaya.MPoint(1,1,1)) def compute(self, plug, dataBlock): try: curveDataHandle = dataBlock.inputValue(instanceAlongCurveLocator.inputCurveAttr) curve = curveDataHandle.asNurbsCurveTransformed() updateTranslation = (plug == instanceAlongCurveLocator.outputTranslationAttr.compound) updateRotation = (plug == instanceAlongCurveLocator.outputRotationAttr.compound) updateScale = (plug == instanceAlongCurveLocator.outputScaleAttr.compound) if not curve.isNull(): if updateTranslation or updateRotation or updateScale: curveFn = OpenMaya.MFnNurbsCurve(curve) instanceCount = self.getInstanceCountByMode() distOffset = dataBlock.inputValue(instanceAlongCurveLocator.distOffsetAttr).asFloat() curveLength = curveFn.length() # Curve thresholds curveStart = dataBlock.inputValue(instanceAlongCurveLocator.curveStartAttr).asFloat() * curveLength curveEnd = dataBlock.inputValue(instanceAlongCurveLocator.curveEndAttr).asFloat() * curveLength effectiveCurveLength = min(max(curveEnd - curveStart, 0.001), curveLength) lengthIncrement = self.getIncrementByMode(instanceCount, effectiveCurveLength) # Common data inputTransformPlug = self.getInputTransformPlug() inputTransformFn = self.getInputTransformFn() # Force update of transformation if OpenMaya.MPlug(self.thisMObject(), instanceAlongCurveLocator.inputTransformAttr).isConnected(): dataBlock.inputValue(inputTransformPlug).asMatrix() # Manipulator data curveAxisHandleArray = dataBlock.inputArrayValue(instanceAlongCurveLocator.curveAxisHandleAttr.compound) axisHandlesSorted = getSortedCurveAxisArray(self.thisMObject(), curveAxisHandleArray, instanceCount) if updateTranslation: self.updateInstancePositions(curveFn, dataBlock, instanceCount, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement, inputTransformPlug, inputTransformFn, axisHandlesSorted) if updateRotation: self.updateInstanceRotations(curveFn, dataBlock, instanceCount, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement, inputTransformPlug, inputTransformFn, axisHandlesSorted) if updateScale: self.updateInstanceScale(curveFn, dataBlock, instanceCount, distOffset, curveStart, curveEnd, effectiveCurveLength, lengthIncrement) except: sys.stderr.write('Failed trying to compute locator. stack trace: \n') sys.stderr.write(traceback.format_exc()) return OpenMaya.kUnknownParameter @staticmethod def nodeCreator(): return OpenMayaMPx.asMPxPtr( instanceAlongCurveLocator() ) @classmethod def addCompoundVector3Attribute(cls, compoundAttribute, attributeName, unitType, arrayAttr, inputAttr, defaultValue): # Schematic view of compound attribute: # compoundAttribute[?] # compoundAttributeX # compoundAttributeY # compoundAttributeZ unitAttr = OpenMaya.MFnUnitAttribute() nAttr = OpenMaya.MFnNumericAttribute() compoundAttribute.x = unitAttr.create(attributeName + "X", attributeName + "X", unitType, defaultValue.x) unitAttr.setWritable( inputAttr ) cls.addAttribute(compoundAttribute.x) compoundAttribute.y = unitAttr.create(attributeName + "Y", attributeName + "Y", unitType, defaultValue.y) unitAttr.setWritable( inputAttr ) cls.addAttribute(compoundAttribute.y) compoundAttribute.z = unitAttr.create(attributeName + "Z", attributeName + "Z", unitType, defaultValue.z) unitAttr.setWritable( inputAttr ) cls.addAttribute(compoundAttribute.z) # Output compound compoundAttribute.compound = nAttr.create(attributeName, attributeName, compoundAttribute.x, compoundAttribute.y, compoundAttribute.z) nAttr.setWritable( inputAttr ) nAttr.setArray( arrayAttr ) nAttr.setUsesArrayDataBuilder( arrayAttr ) nAttr.setDisconnectBehavior(OpenMaya.MFnAttribute.kDelete) cls.addAttribute(compoundAttribute.compound) @classmethod def addRampAttributes(cls, rampAttributes, attributeName, unitType, defaultAxisValue): # Not a compound attribute, just adds them all to the node nAttr = OpenMaya.MFnNumericAttribute() rampAttributes.ramp = OpenMaya.MRampAttribute.createCurveRamp(attributeName + "Ramp", attributeName + "Ramp") cls.addAttribute(rampAttributes.ramp) rampAttributes.rampOffset = nAttr.create(attributeName + "RampOffset", attributeName + "RampOffset", OpenMaya.MFnNumericData.kFloat, 0.0) nAttr.setKeyable( True ) cls.addAttribute( rampAttributes.rampOffset ) rampAttributes.rampAmplitude = nAttr.create(attributeName + "RampAmplitude", attributeName + "RampAmplitude", OpenMaya.MFnNumericData.kFloat, 0.0) nAttr.setKeyable( True ) cls.addAttribute( rampAttributes.rampAmplitude ) rampAttributes.rampRepeat = nAttr.create(attributeName + "RampRepeat", attributeName + "RampRepeat", OpenMaya.MFnNumericData.kFloat, 1.0) nAttr.setKeyable( True ) cls.addAttribute( rampAttributes.rampRepeat ) rampAttributes.rampRandomAmplitude = nAttr.create(attributeName + "RampRandomAmplitude", attributeName + "RampRandomAmplitude", OpenMaya.MFnNumericData.kFloat, 0.0) nAttr.setMin(0.0) nAttr.setSoftMax(1.0) nAttr.setKeyable( True ) cls.addAttribute( rampAttributes.rampRandomAmplitude ) cls.addCompoundVector3Attribute(rampAttributes.rampAxis, attributeName + "RampAxis", unitType, False, True, defaultAxisValue) @classmethod def addCurveAxisHandleAttribute(cls, curveAxisHandleAttr, attributeName, defaultAxisValue): # Schematic view of compound attribute: # curveAxisHandle[] # curveAxisHandleParameter # curveAxisHandleAngle nAttr = OpenMaya.MFnNumericAttribute() cmpAttr = OpenMaya.MFnCompoundAttribute() curveAxisHandleAttr.parameter = nAttr.create(attributeName + "Parameter", attributeName + "Parameter", OpenMaya.MFnNumericData.kDouble, 0.0) nAttr.setWritable( True ) cls.addAttribute(curveAxisHandleAttr.parameter) curveAxisHandleAttr.angle = nAttr.create(attributeName + "Angle", attributeName + "Angle", OpenMaya.MFnNumericData.kDouble, 0.0) nAttr.setWritable( True ) cls.addAttribute(curveAxisHandleAttr.angle) # cls.addCompoundVector3Attribute(curveAxisHandleAttr.axis, attributeName + "Axis", OpenMaya.MFnUnitAttribute.kAngle, False, True, defaultAxisValue) # Build compound array attribute curveAxisHandleAttr.compound = cmpAttr.create(attributeName, attributeName) cmpAttr.addChild(curveAxisHandleAttr.parameter) cmpAttr.addChild(curveAxisHandleAttr.angle) cmpAttr.setWritable( True ) cmpAttr.setArray( True ) cmpAttr.setUsesArrayDataBuilder( True ) cls.addAttribute(curveAxisHandleAttr.compound) @staticmethod def nodeInitializer(): # Associate the node with its aim manipulator OpenMayaMPx.MPxManipContainer.addToManipConnectTable(kPluginNodeId) # To make things more readable node = instanceAlongCurveLocator nAttr = OpenMaya.MFnNumericAttribute() matrixAttrFn = OpenMaya.MFnMatrixAttribute() msgAttributeFn = OpenMaya.MFnMessageAttribute() curveAttributeFn = OpenMaya.MFnTypedAttribute() enumFn = OpenMaya.MFnEnumAttribute() node.inputTransformAttr = matrixAttrFn.create("inputTransformMatrix", "inputTransformMatrix", OpenMaya.MFnMatrixAttribute.kFloat) node.addAttribute( node.inputTransformAttr ) node.legacyInputTransformAttr = msgAttributeFn.create("inputTransform", "it") node.addAttribute( node.legacyInputTransformAttr) node.inputShadingGroupAttr = msgAttributeFn.create("inputShadingGroup", "iSG") node.addAttribute( node.inputShadingGroupAttr ) # Input curve transform node.inputCurveAttr = curveAttributeFn.create( 'inputCurve', 'curve', OpenMaya.MFnData.kNurbsCurve) node.addAttribute( node.inputCurveAttr ) # Input instance count node.instanceCountAttr = nAttr.create("instanceCount", "iic", OpenMaya.MFnNumericData.kInt, 5) nAttr.setMin(1) nAttr.setSoftMax(100) nAttr.setChannelBox( False ) nAttr.setConnectable( False ) node.addAttribute( node.instanceCountAttr) node.addCompoundVector3Attribute(node.inputLocalRotationOffsetAttr, "inputLocalRotationOffset", OpenMaya.MFnUnitAttribute.kDistance, False, True, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.inputGlobalRotationOffsetAttr, "inputGlobalRotationOffset", OpenMaya.MFnUnitAttribute.kDistance, False, True, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.inputGlobalTranslationOffsetAttr, "inputGlobalTranslationOffset", OpenMaya.MFnUnitAttribute.kDistance, False, True, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.inputLocalTranslationOffsetAttr, "inputLocalTranslationOffset", OpenMaya.MFnUnitAttribute.kDistance, False, True, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.inputLocalScaleOffsetAttr, "inputLocalScaleOffset", OpenMaya.MFnUnitAttribute.kDistance, False, True, OpenMaya.MVector(1.0, 1.0, 1.0)) # Curve parameter offset node.distOffsetAttr = nAttr.create("distOffset", "pOffset", OpenMaya.MFnNumericData.kFloat, 0.0) nAttr.setMin(0.0) nAttr.setKeyable( True ) node.addAttribute( node.distOffsetAttr ) node.curveStartAttr = nAttr.create("curveStart", "cStart", OpenMaya.MFnNumericData.kFloat, 0.0) nAttr.setMin(0.0) nAttr.setSoftMax(1.0) nAttr.setKeyable( True ) node.addAttribute( node.curveStartAttr) node.curveEndAttr = nAttr.create("curveEnd", "cEnd", OpenMaya.MFnNumericData.kFloat, 1.0) nAttr.setMin(0.0) nAttr.setSoftMax(1.0) nAttr.setKeyable( True ) node.addAttribute( node.curveEndAttr) ## Max instances when defined by instance length node.maxInstancesByLengthAttr = nAttr.create("maxInstancesByLength", "mibl", OpenMaya.MFnNumericData.kInt, 50) nAttr.setMin(0) nAttr.setSoftMax(200) nAttr.setChannelBox( False ) nAttr.setConnectable( False ) node.addAttribute( node.maxInstancesByLengthAttr) # Length between instances node.instanceLengthAttr = nAttr.create("instanceLength", "ilength", OpenMaya.MFnNumericData.kFloat, 1.0) nAttr.setMin(0.01) nAttr.setSoftMax(1.0) nAttr.setChannelBox( False ) nAttr.setConnectable( False ) node.addAttribute( node.instanceLengthAttr) # Display override options node.displayTypeAttr = enumFn.create('instanceDisplayType', 'idt') enumFn.addField( "Normal", 0 ); enumFn.addField( "Template", 1 ); enumFn.addField( "Reference", 2 ); enumFn.setDefault("Reference") node.addAttribute( node.displayTypeAttr ) # Enum for selection of instancing mode node.instancingModeAttr = enumFn.create('instancingMode', 'instancingMode') enumFn.addField( "Count", 0 ); enumFn.addField( "Distance", 1 ); node.addAttribute( node.instancingModeAttr ) # Enum for selection of orientation mode node.orientationModeAttr = enumFn.create('orientationMode', 'orientationMode') enumFn.addField( "Identity", 0 ); enumFn.addField( "Copy from Source", 1 ); enumFn.addField( "Use Curve", 2 ); enumFn.addField( "Chain", 3 ); enumFn.setDefault("Use Curve") node.addAttribute( node.orientationModeAttr ) node.inputLocalOrientationAxisAttr = enumFn.create('inputLocalOrientationAxis', 'inputLocalOrientationAxis') enumFn.addField("X", 0) enumFn.addField("Y", 1) enumFn.addField("Z", 2) enumFn.setDefault("Z") node.addAttribute( node.inputLocalOrientationAxisAttr ) node.bboxAttr = nAttr.create('instanceBoundingBox', 'ibb', OpenMaya.MFnNumericData.kBoolean) node.addAttribute( node.bboxAttr ) # Default translation ramp axis is UP node.addRampAttributes(node.positionRampAttr, "position", OpenMaya.MFnUnitAttribute.kDistance, OpenMaya.MVector(0.0, 1.0, 0.0)) # Default rotation ramp axis is TANGENT node.addRampAttributes(node.rotationRampAttr, "rotation", OpenMaya.MFnUnitAttribute.kDistance, OpenMaya.MVector(0.0, 0.0, 1.0)) # Default scale axis is uniform node.addRampAttributes(node.scaleRampAttr, "scale", OpenMaya.MFnUnitAttribute.kDistance, OpenMaya.MVector(1.0, 1.0, 1.0)) # Output attributes node.addCompoundVector3Attribute(node.outputTranslationAttr, "outputTranslation", OpenMaya.MFnUnitAttribute.kDistance, True, False, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.outputRotationAttr, "outputRotation", OpenMaya.MFnUnitAttribute.kAngle, True, False, OpenMaya.MVector(0.0, 0.0, 0.0)) node.addCompoundVector3Attribute(node.outputScaleAttr, "outputScale", OpenMaya.MFnUnitAttribute.kDistance, True, False, OpenMaya.MVector(1.0, 1.0, 1.0)) ## Input instance count node.enableManipulatorsAttr = nAttr.create("enableManipulators", "enableManipulators", OpenMaya.MFnNumericData.kBoolean) node.addAttribute( node.enableManipulatorsAttr) node.addCurveAxisHandleAttribute(node.curveAxisHandleAttr, "curveAxisHandle", OpenMaya.MVector(0.0,0.0,0.0)) ## Input handle count node.curveAxisHandleCountAttr = nAttr.create("curveAxisHandleCount", "curveAxisHandleCount", OpenMaya.MFnNumericData.kInt, 5) nAttr.setMin(1) nAttr.setSoftMax(100) nAttr.setChannelBox( False ) nAttr.setConnectable( False ) node.addAttribute( node.curveAxisHandleCountAttr) def rampAttributeAffects(rampAttributes, affectedAttr): node.attributeAffects( rampAttributes.ramp, affectedAttr) node.attributeAffects( rampAttributes.rampOffset, affectedAttr) node.attributeAffects( rampAttributes.rampAmplitude, affectedAttr) node.attributeAffects( rampAttributes.rampAxis.compound, affectedAttr) node.attributeAffects( rampAttributes.rampRandomAmplitude, affectedAttr) node.attributeAffects( rampAttributes.rampRepeat, affectedAttr) # Curve Axis affects, for manipulator node.attributeAffects( node.inputCurveAttr, node.curveAxisHandleAttr.compound ) node.attributeAffects( node.curveAxisHandleCountAttr, node.curveAxisHandleAttr.compound ) # Translation affects node.attributeAffects( node.inputCurveAttr, node.outputTranslationAttr.compound ) node.attributeAffects( node.instanceCountAttr, node.outputTranslationAttr.compound) node.attributeAffects( node.instanceLengthAttr, node.outputTranslationAttr.compound) node.attributeAffects( node.instancingModeAttr, node.outputTranslationAttr.compound) node.attributeAffects( node.maxInstancesByLengthAttr, node.outputTranslationAttr.compound) node.attributeAffects( node.distOffsetAttr, node.outputTranslationAttr.compound ) node.attributeAffects( node.inputTransformAttr, node.outputTranslationAttr.compound ) node.attributeAffects( node.inputLocalOrientationAxisAttr, node.outputTranslationAttr.compound) node.attributeAffects(node.inputLocalTranslationOffsetAttr.compound, node.outputTranslationAttr.compound ) node.attributeAffects(node.inputGlobalTranslationOffsetAttr.compound, node.outputTranslationAttr.compound ) node.attributeAffects( node.enableManipulatorsAttr, node.outputTranslationAttr.compound) node.attributeAffects( node.curveAxisHandleAttr.compound, node.outputTranslationAttr.compound) node.attributeAffects( node.curveStartAttr, node.outputTranslationAttr.compound ) node.attributeAffects( node.curveEndAttr, node.outputTranslationAttr.compound ) rampAttributeAffects(node.positionRampAttr, node.outputTranslationAttr.compound) # Rotation affects node.attributeAffects( node.inputCurveAttr, node.outputRotationAttr.compound ) node.attributeAffects( node.instanceCountAttr, node.outputRotationAttr.compound) node.attributeAffects( node.instanceLengthAttr, node.outputRotationAttr.compound) node.attributeAffects( node.instancingModeAttr, node.outputRotationAttr.compound) node.attributeAffects( node.maxInstancesByLengthAttr, node.outputRotationAttr.compound) node.attributeAffects( node.orientationModeAttr, node.outputRotationAttr.compound) node.attributeAffects( node.distOffsetAttr, node.outputRotationAttr.compound ) node.attributeAffects( node.inputTransformAttr, node.outputRotationAttr.compound ) node.attributeAffects( node.inputLocalOrientationAxisAttr, node.outputRotationAttr.compound) node.attributeAffects( node.enableManipulatorsAttr, node.outputRotationAttr.compound) node.attributeAffects( node.curveAxisHandleAttr.compound, node.outputRotationAttr.compound) node.attributeAffects( node.inputGlobalRotationOffsetAttr.compound, node.outputRotationAttr.compound) node.attributeAffects( node.inputLocalRotationOffsetAttr.compound, node.outputRotationAttr.compound) rampAttributeAffects(node.rotationRampAttr, node.outputRotationAttr.compound) node.attributeAffects( node.curveStartAttr, node.outputRotationAttr.compound ) node.attributeAffects( node.curveEndAttr, node.outputRotationAttr.compound ) # Scale affects node.attributeAffects( node.inputCurveAttr, node.outputScaleAttr.compound ) node.attributeAffects( node.instanceCountAttr, node.outputScaleAttr.compound) node.attributeAffects( node.instanceLengthAttr, node.outputScaleAttr.compound) node.attributeAffects( node.instancingModeAttr, node.outputScaleAttr.compound) node.attributeAffects( node.maxInstancesByLengthAttr, node.outputScaleAttr.compound) node.attributeAffects( node.distOffsetAttr, node.outputScaleAttr.compound ) node.attributeAffects( node.inputTransformAttr, node.outputScaleAttr.compound ) node.attributeAffects( node.inputLocalOrientationAxisAttr, node.outputScaleAttr.compound) node.attributeAffects( node.enableManipulatorsAttr, node.outputScaleAttr.compound) node.attributeAffects( node.curveAxisHandleAttr.compound, node.outputScaleAttr.compound) rampAttributeAffects(node.scaleRampAttr, node.outputScaleAttr.compound) node.attributeAffects( node.curveStartAttr, node.outputScaleAttr.compound ) node.attributeAffects( node.curveEndAttr, node.outputScaleAttr.compound ) node.attributeAffects(node.inputLocalScaleOffsetAttr.compound, node.outputScaleAttr.compound ) ############### # AE TEMPLATE # ############### def loadAETemplateCallback(nodeName): AEinstanceAlongCurveLocatorTemplate(nodeName) class AEinstanceAlongCurveLocatorTemplate(pm.ui.AETemplate): def addControl(self, control, label=None, **kwargs): pm.ui.AETemplate.addControl(self, control, label=label, **kwargs) def beginLayout(self, name, collapse=True): pm.ui.AETemplate.beginLayout(self, name, collapse=collapse) def __init__(self, nodeName): pm.ui.AETemplate.__init__(self,nodeName) self.thisNode = None self.node = pm.PyNode(self.nodeName) if self.node.type() == kPluginNodeName: # Suppress all attributes, so that no extra controls are shown for attr in pm.listAttr(nodeName): self.suppress(attr) self.callCustom(lambda: self.showTitle(), lambda: None) self.beginScrollLayout() self.beginLayout("General", collapse=0) # Base controls annotation = "Defines if the amount of instances is defined manually or by a predefined distance." self.addControl("instancingMode", label="Instancing Mode", changeCommand=self.onInstanceModeChanged, annotation=annotation) annotation = "The amount of instances to distribute. These are distributed uniformly." self.addControl("instanceCount", label="Count", changeCommand=self.onInstanceModeChanged, annotation=annotation) annotation = "If the locator mode is on Distance, this length will define the spacing between each instance. <br> <br> Note that if the curve length is greater than an integer amount of distances, some space will be left unoccupied." self.addControl("instanceLength", label="Distance", changeCommand=self.onInstanceModeChanged, annotation=annotation) annotation = "A safe guard to prevent having too many instances." self.addControl("maxInstancesByLength", label="Max Instances", changeCommand=self.onInstanceModeChanged, annotation=annotation) self.addSeparator() annotation = "An offset for the evaluation of the curve position/rotation. This also modifies the ramp evaluation. " self.addControl("distOffset", label="Curve Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "distOffset"), annotation=annotation) annotation = "A cutoff value for the curve start point. This is normalized, so it should be in [0,1), but can have greater values for looping" self.addControl("curveStart", label="Curve Start", changeCommand=lambda nodeName: self.updateDimming(nodeName, "curveStart"), annotation=annotation) annotation = "A cutoff value for the curve end point. This is normalized, so it should be in (0,1], but can have greater values for looping" self.addControl("curveEnd", label="Curve End", changeCommand=lambda nodeName: self.updateDimming(nodeName, "curveEnd"), annotation=annotation) self.addSeparator() # Orientation controls annotation = "Identity: objects have no rotation. <br> <br> Copy From Source: Each object will copy the rotation transformation from the original. <br> <br> Use Curve: Objects will be aligned by the curve tangent with respect to the selected axis. <br> <br> Chain: Same as Use Curve, but with an additional 90 degree twist for odd instances." self.addControl("orientationMode", label="Orientation Mode", changeCommand=lambda nodeName: self.updateOrientationChange(nodeName), annotation=annotation) annotation = "Each instance will be rotated so that this axis is parallel to the curve tangent." self.addControl("inputLocalOrientationAxis", label="Local Axis" , changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputLocalOrientationAxis"), annotation=annotation) self.addSeparator() # Manipulator controls annotation = "When enabled, the rotations can be manually defined." self.addControl("enableManipulators", label="Enable manipulators", changeCommand=lambda nodeName: self.updateManipCountDimming(nodeName), annotation=annotation) annotation = "This number will define the number of handles to manipulate the curve orientation. For changes to take effect, you must click the Edit Manipulators button. <br> <br> When incrementing the number, new handles will be created in between existing ones, interpolating their values." self.addControl("curveAxisHandleCount", label="Manipulator count", changeCommand=lambda nodeName: self.updateManipCountDimming(nodeName), annotation=annotation) self.callCustom(lambda attr: self.buttonNew(nodeName), self.buttonUpdate, "curveAxisHandleCount") self.addSeparator() # Instance look controls annotation = "By default, objects display type is on Reference, so they cannot be selected. To change this, select Normal." self.addControl("instanceDisplayType", label="Instance Display Type", changeCommand=lambda nodeName: self.updateDimming(nodeName, "instanceDisplayType"), annotation=annotation) annotation = "When true, objects will be shown as bounding boxes only." self.addControl("instanceBoundingBox", label="Use bounding box", changeCommand=lambda nodeName: self.updateDimming(nodeName, "instanceBoundingBox"), annotation=annotation) self.addSeparator() self.endLayout() def showRampControls(rampName): self.beginLayout(rampName.capitalize() + " Control", collapse=True) mel.eval('AEaddRampControl("' + nodeName + "." + rampName + 'Ramp"); ') annotation = "An offset when evaluating the ramp. This is similar to the curve offset, but works only for the ramp." self.addControl(rampName + "RampOffset", label= rampName.capitalize() + " Ramp Offset", annotation=annotation) annotation = "A multiplier to evaluate multiple times the same ramp over the curve" self.addControl(rampName + "RampRepeat", label= rampName.capitalize() + " Ramp Repeat", annotation=annotation) annotation = "Ramp values are multiplied by this amplitude." self.addControl(rampName + "RampAmplitude", label= rampName.capitalize() + " Ramp Amplitude", annotation=annotation) annotation = "A random value for the ramp amplitude. The result is <br><br> amplitude + (random() * 2.0 - 1.0) * <b>randomAmplitude</b>" self.addControl(rampName + "RampRandomAmplitude", label= rampName.capitalize() + " Ramp Random", annotation=annotation) annotation = "The axis over which the ramp is evaluated. The result depends on the type of ramp. <br> <br> The (X,Y,Z) values are over the local space of the transformed object (right/bitangent, up/normal, forward/tangent)." self.addControl(rampName + "RampAxis", label= rampName.capitalize() + " Ramp Axis", annotation=annotation) self.endLayout() self.beginLayout("Offsets", collapse=True) annotation = "A translation offset over the curve local space." self.addControl("inputLocalTranslationOffset", label="Local Translation Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputLocalTranslationOffset"), annotation=annotation) annotation = "A translation offset in worldspace XYZ." self.addControl("inputGlobalTranslationOffset", label="Global Translation Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputGlobalTranslationOffset"), annotation=annotation) self.addSeparator() annotation = "A rotation offset over the curve local space. This offset is initialized to the original object rotation. " self.addControl("inputLocalRotationOffset", label="Local Rotation Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputLocalRotationOffset"), annotation=annotation) annotation = "A worldspace rotation offset." self.addControl("inputGlobalRotationOffset", label="Global Rotation Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputGlobalRotationOffset"), annotation=annotation) self.addSeparator() annotation = "A scale offset over the object local space. This offset is initialized to the original object scale." self.addControl("inputLocalScaleOffset", label="Local Scale Offset", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputLocalScaleOffset"), annotation=annotation) self.endLayout() showRampControls("position") showRampControls("rotation") showRampControls("scale") self.beginLayout("Extra", collapse=True) # Additional info annotation = "The input object transform. DO NOT REMOVE THIS CONNECTION, or the node will stop working correctly." self.addControl("inputTransformMatrix", label="Input object", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputTransformMatrix"), annotation=annotation) annotation = "The shading group for the instances. When instantiating, they will be assigned this SG." self.addControl("inputShadingGroup", label="Shading Group", changeCommand=lambda nodeName: self.updateDimming(nodeName, "inputShadingGroup"), annotation=annotation) self.endLayout() self.endScrollLayout() def showTitle(self): pm.text("Instance Along Curve v" + kPluginVersion, font="boldLabelFont") def buttonNew(self, nodeName): # pm.separator( height=5, style='none') pm.rowLayout(numberOfColumns=3, adjustableColumn=1, columnWidth3=(80, 100, 100)) self.updateManipButton = pm.button( label='Edit Manipulators...', command=lambda *args: self.onEditManipulators(nodeName)) self.updateManipButton.setAnnotation("When pressed, the manipulators will be selected. If the manipulator count changed, it will be updated.") self.resetPositionsButton = pm.button( label='Reset Positions', command=lambda *args: self.onResetManipPositions(nodeName)) self.resetPositionsButton.setAnnotation("When pressed, the manipulators will be uniformly distributed over the curve.") self.resetAnglesButton = pm.button( label='Reset Angles', command=lambda *args: self.onResetManipAngles(nodeName)) self.resetAnglesButton.setAnnotation("When pressed, all the manipulator angles will be reset to 0.") def buttonUpdate(self, attr): nodeName = pm.PyNode(attr).nodeName() self.updateManipButton.setCommand(lambda *args: self.onEditManipulators(nodeName)) self.resetPositionsButton.setCommand(lambda *args: self.onResetManipPositions(nodeName)) self.resetAnglesButton.setCommand(lambda *args: self.onResetManipAngles(nodeName)) def onResetManipPositions(self, nodeName): # First, show manips to update manip count self.onEditManipulators(nodeName) res = pm.confirmDialog( title='Confirm reset positions', message='Are you sure you want to reset the manipulators positions?', button=['Yes','No'], defaultButton='Yes', cancelButton='No', dismissString='No' ) if res == "Yes": pm.select( clear=True ) node = pm.PyNode(nodeName) curve = node.inputCurve handles = node.curveAxisHandle if len(curve.connections()) == 1: curveNode = curve.connections()[0] maxParam = curveNode.findParamFromLength(curveNode.length()) count = min(node.curveAxisHandleCount.get(), handles.numElements()) index = 0 for h in handles: if index < count: h.children()[0].set(index * maxParam / float(count)) index = index + 1 pm.select(nodeName) pm.runtime.ShowManipulators() def onResetManipAngles(self, nodeName): # First, show manips to update manip count self.onEditManipulators(nodeName) res = pm.confirmDialog( title='Confirm reset angles', message='Are you sure you want to reset the manipulators angles?', button=['Yes','No'], defaultButton='Yes', cancelButton='No', dismissString='No' ) if res == "Yes": pm.select( clear=True ) node = pm.PyNode(nodeName) handles = node.curveAxisHandle count = min(node.curveAxisHandleCount.get(), handles.numElements()) index = 0 for h in handles: if index < count: h.children()[1].set(0.0) index = index + 1 pm.select(nodeName) pm.runtime.ShowManipulators() def onEditManipulators(self, nodeName): # Unselect first, to trigger rebuilding of manips pm.select( clear=True ) pm.select(nodeName) pm.runtime.ShowManipulators() # When orientation changes, update related controls... def updateOrientationChange(self, nodeName): self.updateDimming(nodeName, "orientationMode") self.updateManipCountDimming(nodeName) def onRampUpdate(self, attr): pm.gradientControl(attr) def updateManipCountDimming(self, nodeName): enableManips = pm.PyNode(nodeName).enableManipulators.get() self.updateManipButton.setEnable(enableManips) self.resetAnglesButton.setEnable(enableManips) self.resetPositionsButton.setEnable(enableManips) self.updateDimming(nodeName, "curveAxisHandleCount", enableManips) def updateDimming(self, nodeName, attr, additionalCondition = True): if pm.PyNode(nodeName).type() == kPluginNodeName: node = pm.PyNode(nodeName) instanced = node.isInstanced() hasInputTransform = node.inputTransform.isConnected() or node.inputTransformMatrix.isConnected() hasInputCurve = node.inputCurve.isConnected() self.dimControl(nodeName, attr, instanced or (not hasInputCurve) or (not hasInputTransform) or (not additionalCondition)) def onInstanceModeChanged(self, nodeName): self.updateDimming(nodeName, "instancingMode") if pm.PyNode(nodeName).type() == kPluginNodeName: nodeAttr = pm.PyNode(nodeName + ".instancingMode") mode = nodeAttr.get("instancingMode") # If dimmed, do not update dimming if mode == 0: self.dimControl(nodeName, "instanceLength", True) self.dimControl(nodeName, "maxInstancesByLength", True) self.updateDimming(nodeName, "instanceCount") else: self.updateDimming(nodeName, "instanceLength") self.updateDimming(nodeName, "maxInstancesByLength") self.dimControl(nodeName, "instanceCount", True) # Command class instanceAlongCurveCommand(OpenMayaMPx.MPxCommand): def __init__(self): OpenMayaMPx.MPxCommand.__init__(self) self.mUndo = [] def isUndoable(self): return True def undoIt(self): OpenMaya.MGlobal.displayInfo( "Undo: instanceAlongCurveCommand\n" ) # Reversed for undo :) for m in reversed(self.mUndo): m.undoIt() def redoIt(self): OpenMaya.MGlobal.displayInfo( "Redo: instanceAlongCurveCommand\n" ) for m in self.mUndo: m.doIt() def hasShapeBelow(self, dagPath): sutil = OpenMaya.MScriptUtil() uintptr = sutil.asUintPtr() sutil.setUint(uintptr , 0) dagPath.numberOfShapesDirectlyBelow(uintptr) return sutil.getUint(uintptr) > 0 def findShadingGroup(self, dagPath): # Search in children first before extending to shape for child in xrange(dagPath.childCount()): childDagPath = OpenMaya.MDagPath() fnDagNode = OpenMaya.MFnDagNode(dagPath.child(child)) fnDagNode.getPath(childDagPath) fnSet = self.findShadingGroup(childDagPath) if fnSet is not None: return fnSet if self.hasShapeBelow(dagPath): dagPath.extendToShape() fnDepNode = OpenMaya.MFnDependencyNode(dagPath.node()) instPlugArray = fnDepNode.findPlug("instObjGroups") instPlugArrayElem = instPlugArray.elementByLogicalIndex(dagPath.instanceNumber()) if instPlugArrayElem.isConnected(): connectedPlugs = OpenMaya.MPlugArray() instPlugArrayElem.connectedTo(connectedPlugs, False, True) if connectedPlugs.length() == 1: sgNode = connectedPlugs[0].node() if sgNode.hasFn(OpenMaya.MFn.kSet): return OpenMaya.MFnSet(sgNode) return None def doIt(self,argList): try: list = OpenMaya.MSelectionList() OpenMaya.MGlobal.getActiveSelectionList(list) if list.length() == 2: curveDagPath = OpenMaya.MDagPath() list.getDagPath(0, curveDagPath) curveDagPath.extendToShape() shapeDagPath = OpenMaya.MDagPath() list.getDagPath(1, shapeDagPath) if(curveDagPath.node().hasFn(OpenMaya.MFn.kNurbsCurve)): # We need the curve transform curvePlug = OpenMaya.MFnDagNode(curveDagPath).findPlug("worldSpace", False).elementByLogicalIndex(0) # We need the shape's transform too transformFn = OpenMaya.MFnDagNode(shapeDagPath.transform()) transformMessagePlug = transformFn.findPlug("worldMatrix", True) transformMessagePlug = transformMessagePlug.elementByLogicalIndex(0) shadingGroupFn = self.findShadingGroup(shapeDagPath) # Create node first mdagModifier = OpenMaya.MDagModifier() self.mUndo.append(mdagModifier) newNode = mdagModifier.createNode(kPluginNodeId) mdagModifier.doIt() # Assign new correct name and select new locator newNodeFn = OpenMaya.MFnDagNode(newNode) newNodeFn.setName("instanceAlongCurveLocator#") newNodeTransformName = newNodeFn.name() # Get the node shape nodeShapeDagPath = OpenMaya.MDagPath() newNodeFn.getPath(nodeShapeDagPath) nodeShapeDagPath.extendToShape() newNodeFn = OpenMaya.MFnDagNode(nodeShapeDagPath) def setupRamp(rampAttr): # Set default ramp values defaultPositions = OpenMaya.MFloatArray(1, 0.0) defaultValues = OpenMaya.MFloatArray(1, 1.0) defaultInterpolations = OpenMaya.MIntArray(1, 3) plug = newNodeFn.findPlug(rampAttr.ramp) ramp = OpenMaya.MRampAttribute(plug) ramp.addEntries(defaultPositions, defaultValues, defaultInterpolations) setupRamp(instanceAlongCurveLocator.positionRampAttr) setupRamp(instanceAlongCurveLocator.rotationRampAttr) setupRamp(instanceAlongCurveLocator.scaleRampAttr) # Select new node shape OpenMaya.MGlobal.clearSelectionList() msel = OpenMaya.MSelectionList() msel.add(nodeShapeDagPath) OpenMaya.MGlobal.setActiveSelectionList(msel) # Connect :D mdagModifier = OpenMaya.MDagModifier() self.mUndo.append(mdagModifier) mdagModifier.connect(curvePlug, newNodeFn.findPlug(instanceAlongCurveLocator.inputCurveAttr)) mdagModifier.connect(transformMessagePlug, newNodeFn.findPlug(instanceAlongCurveLocator.inputTransformAttr)) if shadingGroupFn is not None: shadingGroupMessagePlug = shadingGroupFn.findPlug("message", True) mdagModifier.connect(shadingGroupMessagePlug, newNodeFn.findPlug(instanceAlongCurveLocator.inputShadingGroupAttr)) mdagModifier.doIt() # (pymel) create a locator and make it the parent locator = pm.createNode('locator', ss=True, p=newNodeTransformName) # Show AE mel.eval("openAEWindow") instanceCountPlug = newNodeFn.findPlug("instanceCount", False) instanceCountPlug.setInt(10) # Rotation offset initialized to original rotation rotX = transformFn.findPlug("rotateX", False).asMAngle().asDegrees() rotY = transformFn.findPlug("rotateY", False).asMAngle().asDegrees() rotZ = transformFn.findPlug("rotateZ", False).asMAngle().asDegrees() plugOffsetX = newNodeFn.findPlug("inputLocalRotationOffsetX", False) plugOffsetY = newNodeFn.findPlug("inputLocalRotationOffsetY", False) plugOffsetZ = newNodeFn.findPlug("inputLocalRotationOffsetZ", False) plugOffsetX.setDouble(rotX) plugOffsetY.setDouble(rotY) plugOffsetZ.setDouble(rotZ) # Scale offset initialized to original scale scaleX = transformFn.findPlug("scaleX", False).asFloat() scaleY = transformFn.findPlug("scaleY", False).asFloat() scaleZ = transformFn.findPlug("scaleZ", False).asFloat() plugOffsetX = newNodeFn.findPlug("inputLocalScaleOffsetX", False) plugOffsetY = newNodeFn.findPlug("inputLocalScaleOffsetY", False) plugOffsetZ = newNodeFn.findPlug("inputLocalScaleOffsetZ", False) plugOffsetX.setDouble(scaleX) plugOffsetY.setDouble(scaleY) plugOffsetZ.setDouble(scaleZ) else: sys.stderr.write("Please select a curve first") else: sys.stderr.write("Please select a curve and a shape") except: sys.stderr.write('Failed trying to create locator. stack trace: \n') sys.stderr.write(traceback.format_exc()) @staticmethod def cmdCreator(): return OpenMayaMPx.asMPxPtr( instanceAlongCurveCommand() ) class instanceAlongCurveLocatorManip(OpenMayaMPx.MPxManipContainer): def __init__(self): OpenMayaMPx.MPxManipContainer.__init__(self) self.nodeFn = OpenMaya.MFnDependencyNode() @staticmethod def nodeCreator(): return OpenMayaMPx.asMPxPtr( instanceAlongCurveLocatorManip() ) @staticmethod def nodeInitializer(): OpenMayaMPx.MPxManipContainer.initialize() def createChildren(self): # List of tuples self.manipCount = 0 self.manipHandleList = [] self.manipIndexCallbacks = {} selectedObjects = OpenMaya.MSelectionList() OpenMaya.MGlobal.getActiveSelectionList(selectedObjects) # Because we need to know the selected object to manipulate, we cannot manipulate various nodes at once... if selectedObjects.length() != 1: return None dagPath = OpenMaya.MDagPath() selectedObjects.getDagPath(0, dagPath) dagPath.extendToShape() nodeFn = OpenMaya.MFnDependencyNode(dagPath.node()) enableManipulators = nodeFn.findPlug(instanceAlongCurveLocator.enableManipulatorsAttr).asBool() # If the node is not using the custom rotation, prevent the user from breaking it ;) if not enableManipulators: return None self.manipCount = nodeFn.findPlug(instanceAlongCurveLocator.curveAxisHandleCountAttr).asInt() for i in xrange(self.manipCount): pointOnCurveManip = self.addPointOnCurveManip("pointCurveManip" + str(i), "pointCurve" + str(i)) discManip = self.addDiscManip("discManip" + str(i), "disc" + str(i)) self.manipHandleList.append((pointOnCurveManip, discManip)) def getSortedCurveAxisArrayFromPlug(self, nodeFn, count): axisHandles = [] plugArray = nodeFn.findPlug(instanceAlongCurveLocator.curveAxisHandleAttr.compound) for i in xrange(count): plug = plugArray.elementByLogicalIndex(i) parameterPlug = plug.child(instanceAlongCurveLocator.curveAxisHandleAttr.parameter) anglePlug = plug.child(instanceAlongCurveLocator.curveAxisHandleAttr.angle) axisHandles.append((i, parameterPlug.asDouble(), anglePlug.asDouble())) def getKey(item): return item[1] return sorted(axisHandles, key=getKey) def connectToDependNode(self, node): try: self.nodeFn = OpenMaya.MFnDependencyNode(node) curvePlug = self.nodeFn.findPlug(instanceAlongCurveLocator.inputCurveAttr) curveAxisHandleArrayPlug = self.nodeFn.findPlug(instanceAlongCurveLocator.curveAxisHandleAttr.compound) self.curveFn = OpenMaya.MFnNurbsCurve(getFnFromPlug(curvePlug, OpenMaya.MFn.kNurbsCurve)) maxParam = self.curveFn.findParamFromLength(self.curveFn.length()) if self.manipCount == 0: return None handleCountPlug = self.nodeFn.findPlug(instanceAlongCurveLocator.curveAxisHandleCountAttr) expectedHandleCount = handleCountPlug.asInt() actualHandleCount = curveAxisHandleArrayPlug.numElements() axisHandlesSorted = self.getSortedCurveAxisArrayFromPlug(self.nodeFn, actualHandleCount) # Amount of new handles handlesToInit = self.manipCount - actualHandleCount handlesPerSegment = 0 if actualHandleCount > 0: handlesPerSegment = max(math.ceil(handlesToInit / float(actualHandleCount)), 1) # Build and connect all plugs # Note: Previous plugs are still with remnant values (newHandleCount < oldHandleCount), # but because when interpolating we just read the handle count attr, it works. for i in xrange(self.manipCount): # Handle data curveAxisHandlePlug = curveAxisHandleArrayPlug.elementByLogicalIndex(i) curveParameterPlug = curveAxisHandlePlug.child(instanceAlongCurveLocator.curveAxisHandleAttr.parameter) curveAnglePlug = curveAxisHandlePlug.child(instanceAlongCurveLocator.curveAxisHandleAttr.angle) fnCurvePoint = OpenMayaUI.MFnPointOnCurveManip(self.manipHandleList[i][0]) fnCurvePoint.connectToCurvePlug(curvePlug) fnCurvePoint.connectToParamPlug(curveParameterPlug) # If we are adding a new handle, we should initialize this handle to some reasonable param/rotation # Otherwise, just keep the previous handle data... it seems the most usable solution if i >= actualHandleCount: if actualHandleCount > 1: # We distribute these new handles over existing segments, so try to distribute them evenly handleSegmentIndex = (i - actualHandleCount) % actualHandleCount handleEndSegmendIndex = (handleSegmentIndex + 1) % actualHandleCount handleSegmentSubIndex = (i - actualHandleCount) / actualHandleCount pT = float(handleSegmentSubIndex + 1) / float(handlesPerSegment + 1) pFrom = axisHandlesSorted[handleSegmentIndex][1] pTo = axisHandlesSorted[handleEndSegmendIndex][1] angleFrom = axisHandlesSorted[handleSegmentIndex][2] angleTo = axisHandlesSorted[handleEndSegmendIndex][2] # Wrap around in last segment if handleSegmentIndex + 1 >= actualHandleCount: pTo += maxParam # Interpolate both parameters and angle... lerpP = pFrom + (pTo - pFrom) * pT lerpAngle = angleFrom + (angleTo - angleFrom) * pT curveParameterPlug.setFloat(lerpP) curveAnglePlug.setDouble(lerpAngle) else: # Default case... just add them over the curve curveParameterPlug.setFloat(self.curveFn.findParamFromLength(self.curveFn.length() * float(i) / float(self.manipCount))) fnDisc = OpenMayaUI.MFnDiscManip(self.manipHandleList[i][1]) fnDisc.connectToAnglePlug(curveAnglePlug) discCenterIndex = fnDisc.centerIndex() discAxisIndex = fnDisc.axisIndex() self.addPlugToManipConversion(discCenterIndex) self.addPlugToManipConversion(discAxisIndex) self.manipIndexCallbacks[discCenterIndex] = (self.discCenterConversion, i) # Store index value self.manipIndexCallbacks[discAxisIndex] = (self.discAxisConversion, i) # Store index value self.finishAddingManips() OpenMayaMPx.MPxManipContainer.connectToDependNode(self, node) except: sys.stderr.write('Failed trying to connect manipulators. Stack trace: \n') sys.stderr.write(traceback.format_exc()) def discAxisConversion(self, manipTuple): fnCurvePoint = OpenMayaUI.MFnPointOnCurveManip(manipTuple[0]) param = fnCurvePoint.parameter() tangent = self.curveFn.tangent(param, OpenMaya.MSpace.kWorld) numData = OpenMaya.MFnNumericData() numDataObj = numData.create(OpenMaya.MFnNumericData.k3Double) numData.setData3Double(tangent.x, tangent.y, tangent.z) manipData = OpenMayaUI.MManipData(numDataObj) return manipData def discCenterConversion(self, manipTuple): fnCurvePoint = OpenMayaUI.MFnPointOnCurveManip(manipTuple[0]) center = fnCurvePoint.curvePoint() numData = OpenMaya.MFnNumericData() numDataObj = numData.create(OpenMaya.MFnNumericData.k3Double) numData.setData3Double(center.x, center.y, center.z) manipData = OpenMayaUI.MManipData(numDataObj) return manipData def plugToManipConversion(self, manipIndex): if manipIndex in self.manipIndexCallbacks: curveHandleIndex = self.manipIndexCallbacks[manipIndex][1] return self.manipIndexCallbacks[manipIndex][0](self.manipHandleList[curveHandleIndex]) print "Manip callback not set; returning invalid data!" numData = OpenMaya.MFnNumericData() numDataObj = numData.create(OpenMaya.MFnNumericData.k3Double) numData.setData3Double(0.0, 0.0, 0.0) manipData = OpenMayaUI.MManipData(numDataObj) return manipData def initializePlugin( mobject ): mplugin = OpenMayaMPx.MFnPlugin( mobject, "mmerchante", kPluginVersion ) try: if (OpenMaya.MGlobal.mayaState() != OpenMaya.MGlobal.kBatch) and (OpenMaya.MGlobal.mayaState() != OpenMaya.MGlobal.kLibraryApp): # Register command mplugin.registerCommand( kPluginCmdName, instanceAlongCurveCommand.cmdCreator ) mplugin.addMenuItem("Instance Along Curve", "MayaWindow|mainEditMenu", kPluginCmdName, "") # Register AE template pm.callbacks(addCallback=loadAETemplateCallback, hook='AETemplateCustomContent', owner=kPluginNodeName) # Register IAC manip node mplugin.registerNode( kPluginManipNodeName, kPluginNodeManipId, instanceAlongCurveLocatorManip.nodeCreator, instanceAlongCurveLocatorManip.nodeInitializer, OpenMayaMPx.MPxNode.kManipContainer ) # Register IAC node mplugin.registerNode( kPluginNodeName, kPluginNodeId, instanceAlongCurveLocator.nodeCreator, instanceAlongCurveLocator.nodeInitializer, OpenMayaMPx.MPxNode.kLocatorNode, kPluginNodeClassify ) except: sys.stderr.write('Failed to register plugin instanceAlongCurve. stack trace: \n') sys.stderr.write(traceback.format_exc()) raise def uninitializePlugin( mobject ): mplugin = OpenMayaMPx.MFnPlugin( mobject ) try: mplugin.deregisterNode( kPluginNodeId ) if (OpenMaya.MGlobal.mayaState() != OpenMaya.MGlobal.kBatch) and (OpenMaya.MGlobal.mayaState() != OpenMaya.MGlobal.kLibraryApp): mplugin.deregisterCommand( kPluginCmdName ) mplugin.deregisterNode( kPluginNodeManipId ) except: sys.stderr.write( 'Failed to deregister plugin instanceAlongCurve') raise ### UTILS def getSingleSourceObjectFromPlug(plug): if plug.isConnected(): # Get connected input plugs connections = OpenMaya.MPlugArray() plug.connectedTo(connections, True, False) # Find input transform if connections.length() == 1: return connections[0].node() return None def getFnFromPlug(plug, fnType): node = getSingleSourceObjectFromPlug(plug) # Get Fn from a DAG path to get the world transformations correctly if node is not None: path = OpenMaya.MDagPath() trFn = OpenMaya.MFnDagNode(node) trFn.getPath(path) path.extendToShape() if path.node().hasFn(fnType): return path return None # TODO: cache this data to prevent recalculating when there is no manipulator being updated def getSortedCurveAxisArray(mObject, curveAxisHandleArray, count): axisHandles = [] expectedHandleCount = OpenMaya.MFnDependencyNode(mObject).findPlug(instanceAlongCurveLocator.curveAxisHandleCountAttr).asInt() for i in xrange(min(expectedHandleCount, curveAxisHandleArray.elementCount())): curveAxisHandleArray.jumpToArrayElement(i) parameterHandle = curveAxisHandleArray.inputValue().child(instanceAlongCurveLocator.curveAxisHandleAttr.parameter) angleHandle = curveAxisHandleArray.inputValue().child(instanceAlongCurveLocator.curveAxisHandleAttr.angle) axisHandles.append((i, parameterHandle.asDouble(), angleHandle.asDouble())) def getKey(item): return item[1] return sorted(axisHandles, key=getKey) def printVector(v, s=None): print s + ":" + str(v.x) + ", " + str(v.y) + ", " + str(v.z)