Search code examples
pythonmayamelpymelmaya-api

Duplicate a nurbs curve along a curve to perform a loft


I have put together a script that creates the following; A start and end curve shape and a linear curve between.enter image description here Now what I'm wanting to do is to duplicate and transform the starting curve shape along the path (as depicted by the image), and perform a loft (preferred as would prob give the cleanest result), or alternatively, loft between the two existing curve shapes, and then deform the loft geometry to the curve. For the latter I have tried;

pm.deformer((loftShape, path), type='curveWarp', name='curveWarp#')

without success. The locators are points calculated to generate the correct bezier curve given different distances/ starting angles. I would have thought the hard work was done, but I'm having trouble with this seemingly simple last step.

Below is a method I put together to query curve info:

def getClosestCV(x, curves, tolerance=0.0):
    '''Find the closest control vertex between the given vertices, CVs, or objects and each of the given curves.

    :Parameters:
        x (str)(obj)(list) = Polygon vertices, control vertices, objects, or points given as (x,y,z) tuples.
        curves (str)(obj)(list) = The reference object in which to find the closest CV for each vertex in the list of given vertices.
        tolerance (int)(float) = Maximum search distance. Default is 0.0, which turns off the tolerance flag.

    :Return:
        (dict) closest vertex/cv pairs (one pair for each given curve) ex. {<vertex from set1>:<vertex from set2>}.

    ex. vertices = Init.getComponents(objects, 'vertices')
        closestVerts = getClosestCV(curve0, curves)
    '''
    pm.undoInfo(openChunk=True)
    x = pm.ls(x, flatten=1) #assure x arg is a list (if given as str or single object).

    npcNode = pm.ls(pm.createNode('nearestPointOnCurve'))[0] #create a nearestPointOnCurve node.

    result={}
    for curve in pm.ls(curves):

        pm.connectAttr(curve.worldSpace, npcNode.inputCurve, force=1) #Connect the curve's worldSpace geometry to the npc node.

        for i in x:
            if not isinstance(i, (tuple, list, set)):
                pos = pm.pointPosition(i)
            else:
                pos = i
            pm.setAttr(npcNode.inPosition, pos)

            distance = Init.getDistanceBetweenTwoPoints(pos, pm.getAttr(npcNode.position))
            p = pm.getAttr(npcNode.parameter)
            if not tolerance:
                result[i] = p
            elif distance < tolerance:
                result[i] = p

    pm.delete(npcNode)

    pm.undoInfo(closeChunk=True)

    return result

def getCvInfo(c, returnType='cv', filter_=[]):
    '''Get a dict containing CV's of the given curve(s) and their corresponding point positions (based on Maya's pointOnCurve command).

    :Parameters:
        - c (str)(obj)(list) = Curves or CVs to get CV info from.
        - returnType (str) = The desired returned values. Default is 'cv'.
            valid values are: 
                'cv' = Return a list of all CV's for the given curves.
                'count' = Return an integer representing the total number of cvs for each of the curves given.
                'parameter', 'position', 'index', 'localPosition', 'tangent', 'normalizedTangent', 'normal', 'normalizedNormal', 'curvatureRadius', 'curvatureCenter'
                = Return a dict with CV's as keys and the returnType as their corresponding values.
            ex. {NurbsCurveCV(u'polyToCurveShape7.cv[5]'): [-12.186520865542082, 15.260936896515751, -369.6159740743584]}
        - filter_ (str)(obj)(list) = Value(s) to filter for in the returned results.

    :Return:
        (dict)(list)(int) dependant on returnType.

    ex. cv_tan = getCvInfo(curve.cv[0:2],'tangent') #get CV tangents for cvs 0-2.
    ex. cvParam = getCvInfo(curve, 'parameters') #get the curves CVs and their corresponding U parameter values.
    ex. filtered = getCvInfo(<curve>, 'normal', <normal>) #filter results for those that match the given value.
    '''
    result={}
    for curve in pm.ls(c):

        if '.cv' in str(curve): #if CV given.
            cvs = curve
            curve = pm.listRelatives(cvs, parent=1)
        else: #if curve(s) given
            cvs = curve.cv

        parameters = Init.getClosestCV(cvs, curve) #use getClosestCV to get the parameter location for each of the curves CVs.
        for cv, p in parameters.items():

            if returnType is 'position': # Get cv position
                v = pm.pointOnCurve(curve, parameter=p, position=True)
            elif returnType is 'localPosition':
                v = pm.getAttr(cv) # local cv position
            elif returnType is 'tangent': # Get cv tangent
                v = pm.pointOnCurve(curve, parameter=p, tangent=True)
            elif returnType is 'normalizedTangent':
                v = pm.pointOnCurve(curve, parameter=p, normalizedTangent=True)
            elif returnType is 'normal': # Get cv normal
                v = pm.pointOnCurve(curve, parameter=p, normal=True)
            elif returnType is 'normalizedNormal':
                v = pm.pointOnCurve(curve, parameter=p, normalizedNormal=True) #Returns the (x,y,z) normalized normal of curve1 at parameter 0.5.
            elif returnType is 'curvatureRadius': # Get cv curvature
                v = pm.pointOnCurve(curve, parameter=p, curvatureRadius=True) #Returns the curvature radius of curve1 at parameter 0.5.
            elif returnType is 'curvatureCenter':
                v = pm.pointOnCurve(curve, parameter=p, curvatureCenter=True)
            elif returnType is 'parameter': # Return the CVs parameter.
                v = p
            elif returnType is 'count': # total number of cv's for the curve.
                result[curve] = len(Init.getCvInfo(curve))
                break
            elif returnType is 'index': # index of the cv
                s = str(cv)
                v = int(s[s.index('[')+1:s.index(']')])
            else:
                v = None

            result[cv] = v

    if returnType is 'cv':
        result = result.keys()

    if filter_:
        if not isinstance(filter_, (tuple, set, list)):
            filter_ = list(filter_)
        try:
            result = {k:v for k,v in result.items() if any((v in filter_, v==filter_))}
        except AttributeError:
            result = [i for i in result if any((i in filter_, i==filter_))]

    if len(result) is 1:
        try:
            result = result.values()[0]
        except AttributeError, TypeError:
            result = result[0]

    return result


Solution

  • I ultimately decided to use the built-in MASH plugin for this. Perhaps this will be of help to someone in the future.

    def duplicateAlongCurve(path, start, count=6, geometry='Instancer'):
        '''Duplicate objects along a given curve using MASH.
    
        :Parameters:
            path (obj) = The curve to use as a path.
            start () = Starting object.
            count (int) = The number of duplicated objects. (point count on the MASH network)
            geometry (str) = Particle instancer or mesh instancer (Repro node). (valid: 'Mesh' (default), 'Instancer')
    
        :Return:
            (list) The duplicated objects in order of start to end.
        '''
        pm.undoInfo(openChunk=1)
    
        #create a MASH network
        import MASH.api as mapi
        mashNW = mapi.Network()
        mashNW.MTcreateNetwork(start, geometry=geometry, hideOnCreate=False) #MASH_tools module (derived from 'createNetwork')
    
        curveNode = pm.ls(mashNW.addNode('MASH_Curve').name)[0]
        pm.connectAttr(path.worldSpace[0], curveNode.inCurves[0], force=1)
    
        pm.setAttr(curveNode.stopAtEnd, 1) #0=off, 1=on
        pm.setAttr(curveNode.clipStart, 0)
        pm.setAttr(curveNode.clipEnd, 1)
        pm.setAttr(curveNode.timeStep, 1)
        pm.setAttr(curveNode.curveLengthAffectsSpeed, 1)
    
        distNode = pm.ls(mashNW.distribute)[0]
        pm.setAttr(distNode.pointCount, count)
        pm.setAttr(distNode.amplitudeX, 0)
    
        instNode = pm.ls(mashNW.instancer)[0]
        baked_curves = mashNW.MTbakeInstancer(instNode) #MASH_tools module (derived from 'MASHbakeInstancer')
    
        result=[start]
        for curve in reversed(baked_curves):
            result.append(curve)
    
        pm.delete(mashNW.waiter.name()) #delete the MASH network.
        pm.undoInfo(closeChunk=1)
    
        return result