Solved Force cache rebuild in python

Hello,

I'm creating a generator and adding a modifier from a python script, but before the script ends, I need to access some data from the modifier that is calculated in ModifyObject(), when the generator deform cache is built. But I can't make it actually generate the cache before the end of the script. From the logs, I can see the cache is generated only after the script ends.

Here's some snippets of what I'm trying...

    gen = c4d.BaseObject(GENERATOR_ID)
    doc.InsertObject(gen)
    
    mod = c4d.BaseObject(MODIFIER_ID)
    doc.InsertObject(mod, gen)
    
    # This parameter is 50.0 by default
    len = mod.GetParameter(mod_LengthAttribute, c4d.DESCFLAGS_GET_0)
    print "mod_LengthAttribute 1 = "+str(len)

    # flag an update? no effect
    gen.Message(c4d.MSG_UPDATE);

    # Animate the object? no effect
    time = c4d.BaseTime(0)
    doc.AnimateObject(gen,time,0)
    
    # I understand this should rebuild the cache, but no effect
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0) 
    c4d.EventAdd()
    #c4d.EventAdd(c4d.EVENT_ANIMATE) # does not build too

    # Tried this too, no effect
    #modelingSettings = c4d.BaseContainer()
    #modelingSettings[c4d.MDATA_CURRENTSTATETOOBJECT_NOGENERATE] = True
    #res = c4d.utils.SendModelingCommand(c4d.MCOMMAND_CURRENTSTATETOOBJECT, [gen], c4d.MODELINGCOMMANDMODE_ALL, modelingSettings, doc)

    # Should be seeing something here, but it prints None
    print "GetDeformCache = " + str(gen.GetDeformCache())

    # ModifyObject() changes this to 100.0, but console still displays 50.0
    len = mod.GetParameter(mod_LengthAttribute, c4d.DESCFLAGS_GET_0)
    print "mod_LengthAttribute 2 = "+str(len)

After the script ends, I can see some debug messages from the modifier, and I can see the parameter I'm testing mod_LengthAttribute have changed.

Is it possible to force my generator to rebuild before the script ends?
Or is there a python command to "wait until next cycle" so the document can update and before it continues?

Hi @rsodre thanks for reaching out us.

With regard to your issue, first it's relevant to highlight that EventAdd() actually push an event in the Cinema queue. Because of the multi-threading system, the event evaluation can't be exactly defined on a temporal level so it's likely that when the next line in the script it's executed the objects' cache have not yet built.

Differently it's for BaseDocument::ExecutePasses() (see here in the BaseDocument Manual about Animate) where depending on the generators and on the deformer you could end up in calling up multiple ExecutePassesto properly evaluate the scene.

Last but not least I suggest you to check for the snippet included the BaseObject::GetCache() to properly access a generator's cache

In the code below I've used it once and the cache is right there

def DoRecursion(op):
    tp = op.GetDeformCache()
    if tp is not None:
        print "\tvalid deformed cache"
        DoRecursion(tp)
    else:
        tp = op.GetCache()
        if tp is not None:
            print "valid cache"
            DoRecursion(tp)
        else:
            if not op.GetBit(c4d.BIT_CONTROLOBJECT):
                if op.IsInstanceOf(c4d.Opolygon):
                    print "\t\t",op

    tp = op.GetDown()
    while tp is not None:
        DoRecursion(tp)
        tp = tp.GetNext()

# Main function
def main():
    gen = c4d.BaseObject(c4d.Ocube)
    doc.InsertObject(gen)
    gen.SetParameter(c4d.PRIM_CUBE_SUBY, 25, c4d.DESCFLAGS_SET_NONE)

    mod = c4d.BaseObject(c4d.Obend)
    doc.InsertObject(mod, gen)

    # set the modifier strength value
    mod.SetParameter(c4d.DEFORMOBJECT_STRENGTH, c4d.utils.DegToRad(50), c4d.DESCFLAGS_SET_NONE)
    
    # evaluate the scene
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0)
    
    # retrieve the cache
    DoRecursion(gen)
    
    # push into the Cinema 4D event queue
    c4d.EventAdd()

Best, Riccardo

@r_gigante I see, your script works fine with me, but I think my modifier take too long to be cached right away when I do the same.

Using some threading I can wait for the cache to build. But instead of sleeping would be better to detect from the object or modifier if they are completely finished. Can I get that?

def MakeObject():
    # Generate the object and execute passes

def RunAfterBuild():
    # process built object

def ExecuteAfterSeconds(seconds,callback):
    time.sleep(seconds)
    callback()

object = MakeObject()
t = Thread(target=ExecuteAfterSeconds, args=(1,RunAfterBuild))
t.start()

Hi @rsodre, may I ask you in which case you think a modifier is not correctly built? It seems from my research there is no flag/message sent after a cache is built.
But before to ask the confirmation to the development team, I would like to understand your issue since I'm not able to reproduce it.
Here with a pretty intensive cube and generator/deformer, it's working nicely. Do you have a precise example where it does not work?

import c4d

def DeformedPolygonCacheIterator(op):
    """
    A Python Generator to iterate over all PolygonCache of passed BaseObject
    :param op: The BaseObject to retrieves all PolygonObject cache.
    """
    if not isinstance(op, c4d.BaseObject):
        raise TypeError("Expected a BaseObject or derived class got {0}".format(op.__class__.__name__))

    # Try to retrieves the deformed cache of the object
    temp = op.GetDeformCache()
    if temp is not None:
        # If there is a deformed cache we iterate over him, a deformed cache can also contain deformed cache
        # e.g. in case of a nested deformer
        for obj in DeformedPolygonCacheIterator(temp):
            yield obj

    # Try to retrieves the cache of the Object
    temp = op.GetCache()
    if temp is not None:
        # If there is a cache iterate over its, a cache can also contain deformed cache
        # e.g. an instance, have a cache of its linked object but if this object is deformed, then you have a deformed cache as well
        for obj in DeformedPolygonCacheIterator(temp):
            yield obj

    # If op is not a generator / modifier
    if not op.GetBit(c4d.BIT_CONTROLOBJECT):
        # If op is a PolygonObject we return it
        if op.IsInstanceOf(c4d.Opolygon):
            yield op

    # Then finally iterates over the child of the current object to retrieves all objects
    # e.g. in a cloner set to Instance mode, all clones is a new object.
    temp = op.GetDown()
    while temp:
        for obj in DeformedPolygonCacheIterator(temp):
            yield obj
        temp = temp.GetNext()

# Main function
def main():
	# Creates a subdivision surface
    gen = c4d.BaseObject(1007455)
    doc.InsertObject(gen)
    
	# Creates a cube
    obj = c4d.BaseObject(c4d.Ocube)
    obj[c4d.PRIM_CUBE_SUBX] = 100
    obj[c4d.PRIM_CUBE_SUBY] = 100
    obj[c4d.PRIM_CUBE_SUBZ] = 100
    doc.InsertObject(obj, gen)
    
	# Creates a bend deformer
    mod = c4d.BaseObject(c4d.Obend)
    mod[c4d.DEFORMOBJECT_STRENGTH] = 1.45
    doc.InsertObject(mod, obj)
        
	# Builds teh cache
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_INTERNALRENDERER)
    
	# Iterates the cache of the generator and creates a null for each point position
    masterNull = c4d.BaseObject(c4d.Onull)
    for obj in DeformedPolygonCacheIterator(gen):
        
        for pt in obj.GetAllPoints():
            null = c4d.BaseObject(c4d.Onull)
            null.SetAbsPos(pt)
            null.InsertUnder(masterNull)
            
    doc.InsertObject(masterNull)
            
    c4d.EventAdd()
    

# Execute main()
if __name__=='__main__':
    main()

Cheers,
Maxime.

@m_adam Thanks Maxime. Your example really shows that the cache must be built independent of how much processing it takes.

Leaves me knowing that there must be something wrong on my modifiers, and indeed there was. I was checking if bt != nullptr inside ModifyObject(). I remember I copied this from some example, and it was interrupting the modifier to process. When the script is over, it would fire again with bt filled and finish deforming.