Dynamically add/remove obj under Python Generator



  • On 28/04/2016 at 09:18, xxxxxxxx wrote:

    Hi all,

    I think I am most likely just misusing the Python Generator due to my own misunderstanding, but I find it odd that it actually does work... just inconsistently.

    I have a simple linkage setup that consists of an IK-chain under one "pivot" null, and the IK-goal under a second "pivot" null.  I need multiple copies of this "linkage" to be aligned to a spline, yet retain the individual IK-chain calculations.  Since I can't get that with a cloner, I've gone the route of multiple copies with Align-To-Spline tags, one on each pivot point under the "linkage" parent null.  With the second pivot offset a fixed small %, all sets of linkages can then be driven around the spline with iterative offset/modulo calculations.

    However, I want to be able to dynamically input the number of linkages (i.e. sets of two pivots) that are on a given spline.  My first attempt has been to use a Python Generator object with user data (links for the spline, linkage parent null, number of linkages, offset %, and phasing %).  The PyGen object would look at the desired number of links, and create copies equal to that number, as children of itself.  This involves deleting the current children, and creating new copies as needed.

    Using primitive objects as my pivot points under a parent null, the following works just as I had hoped--I can swap out geometry, change the link size, number of links, and offset values;  with no problems whatsoever:

    import c4d
    from c4d.utils import SplineHelp
      
    def main() :
        
        spline = op[c4d.ID_USERDATA,1]     #USER INPUT LINK FIELD FOR THE SPLINE
        master = op[c4d.ID_USERDATA,2]     #USER INPUT LINK FIELD FOR THE LINKAGE GEOMETRY
                                           #(Single parent null, with some # of pivot nulls as children)
      
        numLink = op[c4d.ID_USERDATA,3]     #USER INPUT INTEGER FOR TOTAL NUMBER OF LINKAGES
        currentLinks = op.GetChildren()     #COUNT UP THE CURRENT NUMBER OF LINKS
                                            #THAT ARE UNDER THE PYGEN OBEJCT
        
        offset  = op[c4d.ID_USERDATA,4]	     #User input FLOAT (%) for driving links around spline
        phase   = op[c4d.ID_USERDATA,5]     #Addn'l input FLOAT (%) to shift the links +/- if necc
        linkLen = op[c4d.ID_USERDATA,7]     #User inpu FLOAT (inches) to space the pivot points
        
        sh = SplineHelp()
        sh.InitSpline(spline)
        slen = sh.GetSplineLength()          #Measure the input spline length
        sh.FreeSpline()
        
        if len(currentLinks) != numLink:     #Check if the user changed the number of links
      
            for link in currentLinks:				
                link.Remove()     #<<============= Here's where I think I'm running into trouble
      
            if master != None:                 #If a master linkage GEO is provided...
                for i in range(0,numLink) :     #create copies of the master link, insert under pyGen obj
                    copy = master.GetClone()
                    copyName = copy.GetName()
                    name =  copyName + "_%i"%(i+1)
                    copy.SetName(name)
                    copy.InsertUnderLast(op)  
        
        i = 0
        cnt = float(len(currentLinks))
        ind = linkLen/slen              #Calculate small offset % based on space between pivots
      
        for link in currentLinks:
            pivots = link.GetChildren()
            j=0
            for pivot in pivots:
                atsTag = pivot.GetTag(c4d.Taligntospline)          #Check for an ATS tag
                if atsTag:
                    off = (((i/cnt+ind*j)%1+offset)%1+phase)%1     #Calculate offset for each ATS tag	
                    atsTag[c4d.ALIGNTOSPLINETAG_LINK] = spline
                    atsTag[c4d.ALIGNTOSPLINETAG_POSITION] = off
                    j+=1
            i+=1
    

    When I test it on my two-pivot linkage that involves an IK chain, it copies/distributes the objects just fine, and they all animate around the spline just fine.... But as soon as I try to change the numLink  value [which would invoke the .remove() command], C4D throws a bug-report dialog and the whole works freezes.

    So... am I to understand this is exactly what is described in the SDK as what NOT to do?  I thought if the code in the pyGen object qualified as an expression in a node , then this should've crashed when I used it with primitives.  Do I need to do some kind of c4d.StopAllThreads() first?  (I'm assuming that would NOT work since it would stop my PyGen object too, correct?)I am not a programmer, so it's a shot in the dark for me at this point.

    If anyone has thoughts as to why this works in one case, and not the other, and advice as to what would be the RIGHT way, I would greatly appreciate the input!

    --Marcus



  • On 29/04/2016 at 01:06, xxxxxxxx wrote:

    Hi Marcus,

    More than misusing you're abusing the Python Generator.

    I'm confused by the workflow of your generator: Do you know any object in Cinema that adds/removes children?

    An object can't make any change to the document, for instance removing or adding objects.
    If such changes are made then Cinema crashes.
    Of course calling c4d.StopAllThreads() in a generator will screw up completely Cinema 4D scene evaluation (which is multi-threaded).

    Is the code snippet you posted the complete generator's code?
    A Python Generator is supposed to return an object (that can have a children hierarchy) representing it.
    Note the returned object and its hierarchy are virtual until they're converted with a Current State to Object.

    Basically what you're trying to achieve in this Python Generator should be done in a script or command plugin.



  • On 29/04/2016 at 05:27, xxxxxxxx wrote:

    Hi Yannick,

    Thanks for the response.  <sigh> I thought it was probably the wrong approach--I know just enough to be dangerous (Python and C4D both), but not enough to be effective I guess!

    As far as the workflow of the PyGen (as I intended it) was to function like a cloner--at least in appearance--insofar as it would allow me to change the number of copies (clones) with a real-time update via an integer slider.  The only way that had initially thought to do that was to add/remove objects as needed if the current number didn't match the user data number.

    The remaining part of the code was simply to disperse these copies over a spline.  I want an IK-chain of 3-Joints, with the root and a null containing the goal both aligned to a spline using two separate, respective, ATS tags.  By driving the tags simultaneously (but slightly different offset values), I can get an effect similar to the Spline-Effector in conjunction with a Cloner, but I also get the correct IK motion of my linkage as it navigates around the curves of the spline.  Putting the IK group under an actual cloner object results in a static linkage, locked to the spline at only one pivot point.

    We currently do this by hand (creating as many copies of the linkage as we need), and then drive the ATS tags using the Hierarchy node in Xpresso.  I was just trying to create a quicker way to streamline this copying/linking/hierarchical driving process.  I hope that clarifies the workflow, or at least what I was trying to achieve in all of this!

    You're right--I don't know of any object that adds/removes other objects.  I guess I was thinking in terms of a cloner--in which case, it seemed to me that a cloner "adds/removes" its clones as I adjust its count.  How does the cloner object actually achieve this effect without adding/removing objects, and is it possible for me to emulate that in some way?

    A script isn't quite what I'm after, since that's a one-time deal, but I'd like this to be dynamically update-able.  I haven't yet successfully made a plugin...  but wouldn't that amount to the same limitation in being only a one-time command?  Based on your comment of not knowing "any object in Cinema that adds/removes children" I'm guessing I'm asking for the impossible--which was my fear when I asked the question.

    Thanks again,
    Marcus



  • On 29/04/2016 at 06:20, xxxxxxxx wrote:

    ... One thought that had occurred to me before was somehow manipulating the objects in memory rather than in the scene.  Would that be what you are referring to, Yannick, when you said, "the returned object and its hierarchy are virtual."

    With that idea, could I do my add/removing of copies virtually without actually inserting them into the scene and thereby use the PyGen object correctly?  Or am I still missing the mark as far as adding/removing is concerned?  What are the limitations of the "virtual hierarchy" (i.e. can I still utilize tags)?

    Thanks,
    Marcus



  • On 29/04/2016 at 06:49, xxxxxxxx wrote:

    The Cloner returns such a virtual hierarchy i.e. the clones aren't added to the document. Only the input objects of a Cloner are visible in the Attribute Manager.
    A Current State to Object conversion adds the actual clones to the document but then the Cloner no longer affects them.

    To return a virtual hierarchy from a Python Generator, create a Null object and insert the copies under. Finally return the Null object.
    Children won't be removed as the virtual hierarchy is rebuilt each time it's needed.



  • On 29/04/2016 at 10:33, xxxxxxxx wrote:

    Yannick,

    Thanks for that info--until now, I had just thought of the Python Generator as a convenient container for python code... nothing more.  The whole concept of a virtual hierarchy was not part of the equation.

    So with that, I've been tinkering a bit, and have successfully done as you said:  create multiple copies under a parent null, and returned the null.  However, I have a couple problems:

    1. the object being copied within the virtual hierarchy doesn't include the children
    2. the ATS tags that I created on these "cloned" objects aren't linked to the spline correctly
    3. the Python Generator "blinks" (i.e. disappears/reappears) when I change the number of desired copies via the user data

    So when I make the PyGen object editable, I only get copied of the top level-parent null of my "linkage," without the children (actual geometry).  Additionally, I've gotten ATS Tags to appear on these parent nulls, and have even been able to update their offset parameter with the correct amount... but for whatever reason my attempt to assign a spline doesn't want to take effect. Are either of these possible?

    Finally, I'd hoped in an ideal world to be able to animate the user data parameters on a python generator smoothly, but am now curious if that is not a viable option due to the required rebuild of the virtual hierarchy.  Is the blinking just a necessary side-effect that can't be eliminated?

    import c4d
      
    def main() :
        
        spline  = op[c4d.ID_USERDATA,1]
        linkObj = op[c4d.ID_USERDATA,2]
        numLink = op[c4d.ID_USERDATA,3]
        offset  = op[c4d.ID_USERDATA,4]
            
        linkGRP = c4d.BaseObject(c4d.Onull)
        
        for i in range(0,numLink) :
            linkCopy = linkObj.GetClone()
            linkCopy.MakeTag(c4d.Taligntospline)      
            ATStag = linkCopy.GetTag(c4d.Taligntospline)
            ATStag[c4d.ALIGNTOSPLINETAG_LINK] = spline
            ATStag[c4d.ALIGNTOSPLINETAG_POSITION] = (offset+i/float(numLink))%1
      
            linkCopy.InsertUnder(linkGRP)
                    
        return linkGRP
    

    Thanks again!
    Marcus



  • On 04/05/2016 at 05:29, xxxxxxxx wrote:

    With regards to Question 2 , I found this post where it was stated:

    Originally posted by xxxxxxxx

    The align to spline tag is an expression tag, meaning that it's executed every frame. But what you're doing is creating a virtual tag each frame which only exists at that point, so the tag is never executed. The moment you make it editable the tag can be executed and it works as expected.

    So that leads me to the conclusion that using an align to spline tag is not going to work for me.

    But does anyone have suggestions/thoughts regarding questions 1 or 3? I find it odd that the generator "blinks" as it updates if I'm changing the position of the object, but it animates smoothly if I'm simply  re-sizing it.

    Thanks,
    --Marcus



  • On 05/05/2016 at 02:38, xxxxxxxx wrote:

    Hi Marcus,

    In what way do you want the generator to be dynamically update-able?

    I still think that what you're trying to achieve in the Generator has to be done in a script or Command plugin.
    As you wrote it before, you basically want to automatize a repetitive workflow.
    By hand you are creating objects and tags, linking them etc. in the scene.
    And in a script or Command plugin you can automatize these tasks and modify the document.



  • On 05/05/2016 at 06:56, xxxxxxxx wrote:

    Thanks Yannick,

    By dynamically update-able, I'm thinking of how the cloner operates, i.e. I can increase/decrease the number of clones with an immediate response in the viewport, without having to try and manage a massive hierarchy.  I have in the past created a "macro" script that simply did some copy/paste operations based on a user specified input number of copies--I think would be similar to what you are suggesting.  But updating the number of linkages was clunky since it involved deleting the current sets, selecting the master linkage group, and then re-running the script any time you needed to change the number of linkages.  It would just be much more slick if I could simply input the number (up or down) and immediately see the results updated in the view port.

    What are the advantages, exactly, of a Command Plugin over a script?  Can you have a Command Plugin that recursively handles live-updates (like the cloner object), or is it the same as a script that it runs only once when selected?

    This may still be abusing the PyGen object, so I apologize for my ignorance:  my latest attempt was to align the objects in my PyGen's virtual hierarchy to a spline, similar to a Spline Effector--but the objects only appear when the animation is stopped or a change was made to the script.  When I uncheck the "Optimize Cache," to try and get a live update, only a dot appears on the spline and nothing actually gets rendered.   (On the bright side of things, the dots are aligned and animate just as expected!)

    import c4d
    from c4d.utils import SplineHelp
      
    def main() :
        
        spline  = op[c4d.ID_USERDATA,1]        #Spline to which objects are aligned
        linkObj = op[c4d.ID_USERDATA,2]        #Link-field for user defined object to be aligned
        numLink = op[c4d.ID_USERDATA,3]        #User defined Integer # of objects to be aligned
        offset  = op[c4d.ID_USERDATA,4]        #User defined offset (0-100)% to drive objects around spline
            
        linkGRP = c4d.BaseObject(c4d.Onull)    #Top-level parent null to be returned by the PyGen object
            
        sh = SplineHelp()                      #Initiate the SplineHelp class
        sh.InitSpline(spline.GetRealSpline())  #Use spline help to get the positions for alignment  
        spMg = spline.GetMg()                  #Grab the splines global matrix
        
        for i in range(0,numLink) :
            linkCopy = linkObj.GetClone()        #Create copy of the object to be aligned
            off = (offset+i/float(numLink))%1    #Set the offset % based on user input, and equal spacing
            pos = sh.GetMatrix(off,0,True,True)  #Retrieve the corresponding position on the spline  
            linkCopy.SetMg(spMg*pos)             #Set the object's position on the spline, in its global matrix
            linkCopy.InsertUnderLast(linkGRP)    #Insert the object under the Top-level parent null
        sh.FreeSpline()                          #Clear the SplineHelp
        
        return linkGRP
    

    Is this yet again an abuse of the PyGen object?  Why do the objects stay visible if I am only modifying their primitive parameters (i.e. sizeX, sizeY, sizeZ, etc.) but modifying the position causes them to disappear?

    Thank you for your help and patience, Yannick!
    --Marcus



  • On 06/05/2016 at 02:46, xxxxxxxx wrote:

    Hi Marcus,

    Yes a Command plugin just runs once and is a convenient way to execute tasks. Nothing dynamic can be done in it.

    Originally posted by xxxxxxxx

    The objects only appear when the animation is stopped or a change was made to the script. When I uncheck the "Optimize Cache," to try and get a live update, only a dot appears on the spline and nothing actually gets rendered.  (On the bright side of things, the dots are aligned and animate just as expected!)

    I've tried your generator's code in Cinema R17 but surely not the same scene setup as you. 
    Which dot(s) do only appear? The Generator's Null dot?

    Originally posted by xxxxxxxx

    Why do the objects stay visible if I am only modifying their primitive parameters (i.e. sizeX, sizeY, sizeZ, etc.) but modifying the position causes them to disappear?

    Do you mean modifying the primitive parameters of the cloned object? Then, do you mean changing the position of the Generator?



  • On 06/05/2016 at 05:28, xxxxxxxx wrote:

    Oh, odd... I was using R16 when I mentioned the dots.  I had a cube under a parent null, and dropped the parent null into the Object-Link field of the Python Generator.  In that case, what I'm seeing looks the same if you cloned a null with no child object around a spline.  However, if I put the cube directly in the Link-Field of the Python Generator, the dots are no longer there.

    When I tried it in R17, the dots didn't show up in either case (putting the parent null, or the cube directly into the Link-Field), which is what you were seeing, I guess.  Blast.

    Originally posted by xxxxxxxx

    Do you mean modifying the primitive parameters of the cloned object? Then, do you mean changing the position of the Generator?

    Yes, to changing primitive parameters--No, to changing the position of the Generator; I'd be wanting to change the clones positions.  Originally I thought that if I modified the primitive parameters, it animated just fine regardless.  But I just tried the following code to compare making changes to a linked object versus making changes to a generated object:

    import c4d
      
    def main() :
        
        linkobj = op[c4d.ID_USERDATA,1]    #User data link field for object
        size = op[c4d.ID_USERDATA,2]       #User data Vector field for size
        
        topNull = c4d.BaseObject(c4d.Onull) #Top parent null to return
            
        if linkobj and linkobj.GetType() == c4d.Ocube:  #Check if there is a linked object, and if it's a cube
            objLink = linkobj.GetClone()                #If so, get a copy...
            objLink[c4d.PRIM_CUBE_LEN] = size           #Set it's size according to the user data
            objLink.SetRelPos(c4d.Vector(size.x,0,0))   #Offset it's position so we can compare
            objLink.InsertUnder(topNull)                #Insert it under the parent null
        else: return None
        
        objPrim = c4d.BaseObject(c4d.Ocube)             #Create a primitive cube object for comparision
        objPrim[c4d.PRIM_CUBE_LEN] = size               #Set the size according to the user data
        objPrim.SetRelPos(c4d.Vector(-size.x,0,0))      #Offset its position for comparision
        objPrim.InsertUnder(topNull)                    #Inser it undeer the parent null
        
        return topNull
    

    In this case, I found that the objLink  would only appear if "optimize cache" was selected, and the user data PyGen object hadn't changed--but it disappears as soon as I scrub through the animation with the size vector animated.  The objPrim worked just fine, however--its size animated smoothly in either case, without disappearing.  I hadn't realized there was a difference in working with a linked object vs. a generated object, so that might be the answer to my question #3  from above--you can't animate parameters of a linked object .

    I think this may also answer my question #1 from before, wondering why the child objects weren't appearing.  In the case of the code I just posted, if you make the PyGen object editable, only the generated object appears as a child under the returned parent null.

    So I think my only remaining question would be:  is there method of retrieving linked objects (other than the GetClone() method that I'm using) that would be equivalent to the generating process, such that you could get the same feedback/updates with a linked object as you do with a linked object?

    Thanks again!
    --Marcus



  • On 13/05/2016 at 10:26, xxxxxxxx wrote:

    For anyone who is interested:

    I found that when I was trying to align copies of an object through a link-field in the user data of my Python Generator object, it would not display when animating. (The "blinking" behavior I described earlier)   However , it worked as I hoped it would if I made the object to be copied a child of the Python Generator object.

    If someone has the technical explanation as to why this is the case, I would certainly appreciate knowing the difference in a link vs. child object in the context of creating a virtual hierarchy in a Python Generator--is it somehow tied to priority calculations, i.e. the child of the PyGen is calculated simultaneously vs. a linked object is still a separate entity?

    Anyway, the closest I got to my desired goal ended up being the equivalent of a Cloner/Spline-Effector in one object.  I successfully created a virtual hierarchy of clones (based on the child object of the PyGen object) which would then be aligned to a spline linked via user data, and those clones could be driven/offset along the spline.

    Here is where I left it:

    import c4d
    from c4d.utils import SplineHelp
      
    def main() :
        topNull = c4d.BaseObject(c4d.Onull)    #Specify parent null for the PyGen object to return
        
        if op.GetDown() :                       #Check for children under the PyGen object
            link = op.GetDown()                #Assign the child as the LinkGRP
            linkName = link.GetName()            
            if link.GetChildren() :             #Check if there are sub-pivot nulls to be offset by LinkLen
                pivots = link.GetChildren()    #Assign these sub-pivots to a variable
            else: return
            link[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 2  #Show the master object B4 copying
            link[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
        else: return
        
        spline = op[c4d.ID_USERDATA,1]        #Link-field for user specified spline
        if spline:                            
            rspline = spline.GetRealSpline()  #Get real interpolation of spline
            sh = SplineHelp()                 #Create SplineHelp object...
            sh.InitSpline(rspline)            #Initiate the SH object with input spline
            slen = sh.GetSplineLength()       #Retrieve the total spline length
        else: return
        
        cnt = op[c4d.ID_USERDATA,3]        #Integer for number of links/copies
        offset  = op[c4d.ID_USERDATA,4]    #Percentage (0-100%) for offset along the spline
        phase   = op[c4d.ID_USERDATA,5]    #Percentage (0-100%) to phase forward/backward from offset
        linkLen = op[c4d.ID_USERDATA,7]    #Float/Meter(in) for distance between two pivot points
        
        ind = linkLen/slen                 #Calculate the % offset between pivot points
        for i in range(0,cnt) :
            linkGRP = link.GetClone()            #Create a copy of the PyGen child for insertion in virtual heirarchy
            pivotsCopy = linkGRP.GetChildren()   #Create a copy of the pivots list retrieved earlier
        
            j=0                                  #Pivot incremental index value
            for pivot in pivotsCopy:
                off = (((i/float(cnt)+ind*j)%1+offset)%1+phase)%1    #Calculate respective pivot offset using Modulo (1)
                pos = sh.GetPosition(off,0,True,True)                #Retrieve respective position on spline
                pivot.SetAbsPos(pos)                                 #Set the pivot's position
                pivot.SetName("Link_"+str(i+1)+"_Pivot_"+str(j))     #Incrememntally name the virtual pivots
                pivot.InsertUnder(linkGRP)                           #Insert the virtual pivot under the current linkGRP
                j+=1                                                 #Increment j
            linkGRP.SetName(linkName+"_"+str(i+1))                   #Incrementally name the linkGRP variables
            linkGRP.InsertUnderLast(topNull)                         #Insert the linkGRP in the virtual hierarchy under th parent null
                        
        sh.FreeSpline()    #Free the SplineHelp object
      
        link[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1  #HIDE the master object group so only returned GEO displays
        link[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 1
        
        return topNull     #Return the generated virtual hierarchy!
    

    The part that I was really after, however, was to leverage tags on those virtual objects.  I created appropriate IK tags, and linked up the respective end/goal  fields successfully, but the IK-Calculation would only update once I made the PyGen object editable.  As was stated in the aforementioned thread, I guess certain tags (Expression tags?) are only calculated once they are "alive" in the scene and not simply virtual.

    So thanks for the help, Yannick.  I have a much better grasp of the PyGen object now, thus I consider my original question on how to, "Dynamically add/remove obj under Python Generator" is answered.

    --Marcus


Log in to reply