Best way to hide a child and get best perfomance
-
Hi, forum!
I'm working on a tool the principle of operation of which is similar like a "Sweep". It wraps one "profile" spline around a "path" spline. I want any geometric change to these splines to trigger an update. I think I've figured it out using this code.def GetVirtualObjects(self, op, hh): doc = op.GetDocument() path = op.GetDown() if path is None: return None profile = path.GetNext() if profile is None: return None dirty = op.CheckCache(hh)\ or op.IsDirty(c4d.DIRTY_DATA)\ or path.IsDirty(c4d.DIRTY_DATA)\ or path.IsDirty(c4d.DIRTY_MATRIX)\ or profile.IsDirty(c4d.DIRTY_DATA)\ or profile.IsDirty(c4d.DIRTY_MATRIX) if not dirty: return op.GetCache(hh) path = c4d.utils.SendModelingCommand( c4d.MCOMMAND_CURRENTSTATETOOBJECT, [path], c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc )[0] profile = c4d.utils.SendModelingCommand( c4d.MCOMMAND_CURRENTSTATETOOBJECT, [profile], c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc )[0]However, I'd like to hide the child elements, but I can't figure out how I can do it more correctly
I have try use c4d.BaseObject.Touch() and my child is hide, but I can't edit my path/profile dynamically. If I edit the path/profile, I need to refresh the viewport or op settings to see the changes.
I tried this code. Perhaps I didn't fully understand something and missed something.
path = op.GetDown() if path is None: return None profile = path.GetNext() if profile is None: return None path.Touch() profile.Touch() dirty = op.CheckCache(hh)\ or op.IsDirty(c4d.DIRTY_DATA)\ or path.IsDirty(c4d.DIRTY_DATA)\ or path.IsDirty(c4d.DIRTY_MATRIX)\ or profile.IsDirty(c4d.DIRTY_DATA)\ or profile.IsDirty(c4d.DIRTY_MATRIX) if not dirty: return op.GetCache(hh)Besides this, I tried using op.GetAndCheckHierarchyClone like it was in this thread https://developers.maxon.net/forum/topic/10491/13941_hide-child-of-object-plugin/3
path = op.GetDown() if path is None: return None profile = path.GetNext() if profile is None: return None ProfileClone = op.GetAndCheckHierarchyClone(hh, profile, c4d.HIERARCHYCLONEFLAGS_ASSPLINE, False) ProfileCloneDirty = ProfileClone["dirty"] ProfileCloneClone = ProfileClone["clone"] PathClone = op.GetAndCheckHierarchyClone(hh, path, c4d.HIERARCHYCLONEFLAGS_ASSPLINE, False) PathCloneDirty = PathClone["dirty"] PathCloneClone = PathClone["clone"] if not ProfileCloneDirty: return ProfileCloneClone if not PathCloneDirty: return PathCloneClone path_upd = c4d.utils.SendModelingCommand( c4d.MCOMMAND_CURRENTSTATETOOBJECT, [PathCloneClone], c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc )[0] profile_upd = c4d.utils.SendModelingCommand( c4d.MCOMMAND_CURRENTSTATETOOBJECT, [ProfileCloneClone], c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc )[0]However, this reduces performance compared to the code above. I would be very grateful if you could help me figure out how I can do this more correctly and optimized.
P.s I think my question will seem primitive, but I'm just learning the API, so don't judge me too harshly

-
Hey @Tpaxep,
Thank you for reaching out to us. There are few things to unpack here.
Hiding Input Objects
Input objects are hidden automatically when you implement an
ObjectDataplugin that uses the flag OBJECT_INPUT and 'touches' its input objects, by for example callingGetAndCheckHierarchyCloneon them as you do. This only works for direct children of the object, not grandchildren or even deeper descendants.Under the hood, this causes the input objects to have the flag BIT_CONTROLOBJECT to be set and then being touched which among other things will delete their cache, resulting in them becoming invisible. Technically you can do parts of this yourself (to hide also objects that are not direct children of your object) but:
- Cinema 4D evaluates its scene graph hierarchically top down. This is also why you need
GetAndCheckHierarchyCloneso that you can build the cache of your descendants which has not been build yet when you are being built because fundamentally your child objects come after you in a hierarchy. I cannot unpack this in all detail, but trying to sidestep the cache building logic of Cinema 4D is not recommended and not trivial. Your inputs should always be direct children of yourself, otherwise you will land in a world of hurt sooner or later. - 'Hiding' inputs is actually deleting them.
BaseObject.Touchliterally marks the cache of an object for deletion andBIT_CONTROLOBJECTthen later prevents the object from rebuilding it when it is its turn to naturally build the cache. No cache means there is nothing for the object to draw into the viewport. You can technically also hide objects with things like NBIT_OHIDE, but there you run into threading issues (see the section below) and also update issues. When your plugin does miss some case, the user ends up with a permanently hidden object in his or her scene, destroying his or her work. So, usingNBIT_OHIDEin this context is not a good idea.
Threading Restrictions
Your code violates the threading restrictions of Cinema 4D. You run a modelling command in the
GVOof your object plugin directly on an element attached to a loaded (in this case active) scene and with that risk crashes and loss of data. The subject is also explicitly being discussed for modelling commands in this code example (also read the file doc string as it explains some things).In general, you cannot change the state of scene elements from any threaded function such as
ObjectData.GetVirtualObjectsas this will sooner later lead to crashes. Functions such asGetVirtualObjectsorObjectData.GetContourwill always only be called in parallel. Functions such asNodeData.Messagemight be called from the main thread (you can check withc4d.threading.GeIsMainThread) and therefore you can then manipulate the scene from there. E.g., implement a button in an object or tag plugin UI that inserts another object, deletes something, etc.Cheers,
Ferdinand - Cinema 4D evaluates its scene graph hierarchically top down. This is also why you need
-
@Tpaxep
besides that I have a code which works for your scenario.def GetVirtualObjects(self, op, hh): profile_orig = op.GetDown() if profile_orig is None: return None path_orig = profile_orig.GetNext() if path_orig is None: return None # you need to make clones of the children, don't use the orig # first make clones and then use the GACHC Methode below profile = profile_orig.GetClone() path = path_orig.GetClone() dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_ALL) child_dirty = op.GetAndCheckHierarchyClone(hh, profile_orig, c4d.HIERARCHYCLONEFLAGS_ASSPLINE, True) if not any([dirty, child_dirty["dirty"]]): return child_dirty["clone"] print("GVO Executed") # calculation of your geometry sweep = c4d.BaseObject(c4d.Osweep) tag = c4d.BaseTag(c4d.Tphong) tag[c4d.PHONGTAG_PHONG_USEEDGES] = False sweep.InsertTag(tag) path.InsertUnder(sweep) profile.InsertUnder(sweep) return sweepyou can also track dirty manually:
def GetVirtualObjects(self, op, hh): profile_orig = op.GetDown() if profile_orig is None: return None path_orig = profile_orig.GetNext() if path_orig is None: return None profile = profile_orig.GetClone() path = path_orig.GetClone() dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_ALL) #Manually track dirty of the childs (Example) child_dirty = False for child in op.GetChildren(): child_dirty = child.IsDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX) if child_dirty: break # After Dirty Touch the childs profile_orig.Touch() path_orig.Touch() if not any([dirty, child_dirty]): return op.GetCache(hh) print("GVO Executed") # calculation of your geometry sweep = c4d.BaseObject(c4d.Osweep) tag = c4d.BaseTag(c4d.Tphong) tag[c4d.PHONGTAG_PHONG_USEEDGES] = False sweep.InsertTag(tag) path.InsertUnder(sweep) profile.InsertUnder(sweep) return sweep -
Regrading @ThomasB snippets, in the context of a GVO call I would curate my C4DAtom.GetClone more, e.g., with
COPYFLAGS_NO_HIERARCHY,COPYFLAGS_NO_ANIMATION, or evenCOPYFLAGS_NO_BRANCHES. Copying a full node can otherwise be quite costly because it is not just "a node" but a whole subset of the scene graph with all the things which are attached to it.And that is exactly the problem I talk about in the SMC example I linked to above: Copying can be expensive. And in the broadest scenario you will always need everything, because dependencies can be complex. And even go beyond a pure "down"-dependency (with for example base link fields) where this copying approach then fails.
COPYFLAGS_NO_HIERARCHY: Means no children or deeper hierarchical descendants are copied.COPYFLAGS_NO_ANIMATION: Means that no animation tracks or keyframes are copied (which are a form of branches).COPYFLAGS_NO_BRANCHES: Means that no branches are copied. Children are not branches but pretty much everything else is. tracks, curves, tags, special data, etc., they will be all lost. Editable polygon- and spline objects store for example their point, tangent, and polygon data as tags, so you would loose all that. But it can make sense to use this on generator objects (e.g., "Cube Object" or "Circle Spline") to strip them of everything, as they are self contained capsules.
For @ThomasB case it is probably more performant, to just build the caches for the inputs, i.e., the splines. Which will include all the crazy MoGraph fields and deformer dependencies with which splines could be modified, and then just use that cache as the input for the sweep. The caches of
SplineObjectinstances will beLineObjectinstances, which is a little bit wrong to put them under a sweep. So, to make it perfect, you would just grab all the points and segments of the line object and re-express them as a new linear spline object. -
Thanks for the answers! The method works great, but I ran into a problem when running the code further. My next part looks like this
spline = path.GetRealSpline() profile_spline = profile.GetRealSpline() if spline is None or profile_spline is None: return None points_profile = profile_spline.GetAllPoints() count_profile = len(points_profile) is_profile_closed = profile_spline.IsClosed() if count_profile < 2: return None sh = SplineHelp() if not sh.InitSplineWith( spline, c4d.SPLINEHELPFLAGS_GLOBALSPACE | c4d.SPLINEHELPFLAGS_USERDEFORMERS | c4d.SPLINEHELPFLAGS_CONTINUECURVE ): return NoneWith this approach, my path is no longer processed as GetRealSpline, while the profile is processed as expected.
I adjusted the code slightly, integrating the def GetCloneSpline function from this code.
Now I get this
profile_orig = op.GetDown() if profile_orig is None: return None path_orig = profile_orig.GetNext() if path_orig is None: return None dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_ALL) profileData = op.GetAndCheckHierarchyClone( hh, profile_orig, c4d.HIERARCHYCLONEFLAGS_ASSPLINE, True) if not any([dirty, profileData["dirty"]]): return profileData["clone"] profile = GetCloneSpline(profile_orig) path = GetCloneSpline(path_orig)Maybe I have missed something important (let me know), but overall everything works like i want. But I have a few questions
-
Now I'm using CSTO as a quick fix for the GetRealSpline issue, but it seems incorrect. I'd like to know which way was better:
use CSTO outside of GVO (using a temporary document, as in the example) for improvement, or if I should try to find an alternative solution? -
I didn't fully understand this message: is it better to use caches instead of cloning, or should I use both in combination?
-
-
I do not quite understand your problem and more importantly what your goal is. Where does 'my path is no longer processed as GetRealSpline' happen? I assume with 'processed' you mean that you call
BaseObject.GetRealSplineon something?- Using CSTO as a stand-in for
GetRealSplinesounds like a bad idea. - It is better to use caches when possible, because there you do not have to copy whole documents to ensure that all dependencies are included. But you still have to copy something (the cache) but that is less expensive. When the whole thing happens for splines, the issue is that splines return
LineObjectand notSplineObjectas their cache. See here for details. You can sort of just ignore the issue and treat theLineObjectinstances as if they wereSplineObjectinstances, but that might cause issues here and there. Some tools/objects such as the Extrude Object also acceptLineObjectinputs even though a user can never manually create them. But there is no guarantee that everything that can deal withSplineObjectinstances as inputs also supportsLineObject.
- Using CSTO as a stand-in for
-
Here's my prototype, if you are interested in my goal
Right now, everything works as expected, but only with these lines of code.
profile = GetCloneSpline(profile_orig) path = GetCloneSpline(path_orig)I'm happy with the current result. So, I'd love to get some additional advice on correctness and optimization. I think your previous answer was comprehensive enough, so I'll try to integrate some of it.
But I'd also be very grateful if you could take a look at my plugin and perhaps give me some more specific optimization tips. If that's not too much trouble, of course!


@ferdinand @ThomasB
Anyway, thanks for your replies and advices!