Solved Generator object's children visibility

Hello,

I would like to check if an object is hidden when attached to a generator object.

Example:

  • Array
    -- Cube

I tried using:

  • CheckEditorVisibility(),
  • CheckDisplayFilter() with GENERATOR flag,
  • GetEditorMode() != MODE_OFF,
  • GetDeformMode(),
    but without any success.

I loop through a selection (in this case a parent(Array) and a child (Cube))
and, as I mentioned before, I want to check if an object is hidden by a generator object. I would appreciate it if you could tell me if there is a way to check it.

Hi @konradsawicki,

first of all, there already exist methods for retrieving the bounding box of an object and and they should respect empty caches correctly, see BaseObject::GetRad, ::GetMp, and ::GetDimension for details. Check also this therad for recursive bounding box computation.

Secondly, you seem to have misconceptions regarding caches. I would recommend having a look at either the ActiveObject plugin from the C++ SDK or this thread I already linked to above. A few satements:

  • The generator cache of an BaseObject is the output the ObjectData::GetVirtualObjects of its corresponding plugin hook. It is basically a hidden hierarchy of objects.
  • Caches of objects are built when an object is part of a document and BaseDocument::ExecutePasses is called on that document (with at least the generator/cache pass enabled).
  • Freshly instantiated objects or touched objects will have no caches, even when being part of a document.
  • You can set as many parameters as you want on an object with no cache, e.g., its global position. It will be correctly reflected, and for some parameters, e.g., the side length of a cube, it will cause caches to be built once you add an event or manually execute the passes yourself.

But overall, I currently do not see the necessity to go down this cache and hidden object rabbit hole in the first place for computing the bounding box of things. But I might be overlooking something here.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hello @konradsawicki,

Welcome to the Plugin Café forum and the Cinema 4D development community, it is great to have you with us!

Getting Started

Before creating your next postings, we would recommend making yourself accustomed with our Forum and Support Guidelines, as they line out details about the Maxon SDK Group support procedures. Of special importance are:

About your First Question

Your question is slightly ambiguous, let me dissect two things first:

  1. I assume with visible/hidden you are referring to the render-visibility flags of an object as reflected by the green/gray/red dots in the Object Manager.
  2. One could interpret a lot into the conjunction 'when' you did choose in your sentence. One could understand it as a hierarchical condition (check only objects that are parented to my generator) or as a temporal condition (check objects when they are parented to my generator or when they change).

And now let me answer these two points:

  1. The editor and render visibility of an object are determined by the parameters with the symbols ID_BASEOBJECT_VISIBILITY_EDITOR and ID_BASEOBJECT_VISIBILITY_RENDER. The visibility of an object in the object manger is determined by the NBIT NBIT_OHIDE .
  2. Only checking the children is easy, the causal/temporal restriction is not, because a parent node P is not always informed when one of its children or grandchildren has changed. There are methods like BaseList2D.AddEventNotification which can help here, but the method is private for a reason and stuff like this can get very complicated. Normally you should not build relations between data like this.
:warning: It sounds a bit like you are trying to modify the scene your generator is contained in from one of its methods. Note that the threading restrictions do apply here and ignoring them can lead to crashes.

Cheers,
Ferdinand

Result

Before: eeb839c9-03d7-4f3a-ad1b-4f5766bc7b32-image.png

After:
414012cf-0f1c-4e8c-8502-1adc53e79374-image.png

'Cube': node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 0
'Cube': node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
'Cube.1': node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 2
'Cube.1': node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 0
'Cube.2': node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 2
'Cube.2': node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
'Cube.3': node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1
'Cube.3': node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
'Cube.4': node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 2
'Cube.4': node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
>>> 

Code

"""Demonstrates different concepts of visibility on the first object in a scene and its descendants.

WARNING: This script will 'brick' a scene by permanently hiding the first object in it from the Object Manger.
Run the script again on the scene to 'unbrick' it.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.

def WalkTree(node: c4d.GeListNode) -> typing.Iterator[c4d.GeListNode]:
    """Yields all descendants of #node in depth first manner.

    Includes #node itself in the output.
    """
    if not isinstance(node, c4d.GeListNode):
        return
    
    yield node
    for child in node.GetChildren():
        for descendant in WalkTree(child):
            yield descendant

def main() -> None:
    """
    """
    # Get the 
    op: c4d.BaseObject = doc.GetFirstObject()
    if not op:
        return
    
    # Print out the editor and render visibility of each node below #op, including #op itself.
    for node in WalkTree(op):
        print(f"'{node.GetName()}': {node[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = }")
        print(f"'{node.GetName()}': {node[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = }")
    
    # Toggle the NBIT that controls the visibility of the first object in the Object Manager itself.
    op.ChangeNBit(c4d.NBIT_OHIDE, c4d.NBITCONTROL_TOGGLE)
    c4d.EventAdd()
    
if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand,

Thank you for your detailed answer. I am sorry for the ambiguity. I'll try to explain my problem more thoroughly.

By a generator object I meant, for example, an 'Array' or 'Symmetry':

1f536462-d44d-4635-b157-0fea950e2e62-image.png

My goal is to calculate the extents of an objects' selection.

Now let's assume such situation:
b7abf3ab-0bcf-428d-8f09-e0a8d19f45fa-image.png

You can see that that the selection consists of 'Array' (parent) and 'Cube' (child). When I attached 'Cube' to 'Array', C4D automatically hid 'Cube' in the scene.

Here you can see that only 'Cube' is selected, but only its local coordinate system is visible (it was hidden by C4D).
65ea640f-c627-4bab-ac57-9e930de4e76e-image.png

So the problem is that functions such:

  • CheckEditorVisibility(),
  • CheckDisplayFilter() with GENERATOR flag,
  • GetEditorMode(),
  • GetDeformMode(),
  • GetNBit with OHIDE flag (checked after @ferdinand answer)

don't provide useful info whether 'Cube' was hidden by C4D in the scene (due to 'Cube' being a child of 'Array'), thus I cannot exclude those hidden objects from extents.

I am sorry for all the ambiguities, please let me know if I can explain it more clearly.

Hey @konradsawicki,

There is no need to be sorry, it is in the nature of asking technical questions that ambiguities come up. But at the same time, I also must point them out relatively unceremoniously as we otherwise might risk talking about different things.

I understand now better what you are trying to do, and it also explains why you were trying all these methods which did not make too much sense for me. There are multiple ways how geometry can be hidden from the users eyes other than the user facing gray/green/red toggle dots. Most of them are NBIT flags, e.g., NBIT_EHIDE or NBIT_LOD_HIDE, see our documentation for details. Another way how objects can be hidden, is them being BaseObject.Touch'ed. This unfortunately named method marks objects as input objects for a generator. It cleans their caches and sets the BIT flag BIT_CONTROLOBJECT.

Combine all of these, and you have a relatively reliable predictor for if something is hidden in the viewport or not.

Cheers,
Ferdinand

Result

For this input and running the script first on "Extrude" and then on "Array":
1b3ff1d0-3777-4fe3-b6f3-a89bc1ff8b0e-image.png

We get this output. Note that generators do not use any of the NBIT flags to hide their inputs. But other things might. It depends a bit on what you want to implement, in what you would have to respect in your code. Do you just want to cover "generator-induced-invisibility", or do also want to cover other cases as for example tools (temporarily) hiding things.

'Extrude':
	node.GetNBit(c4d.NBIT_EHIDE) = 0
	node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
	node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
	node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
	node.GetBit(c4d.BIT_CONTROLOBJECT) = True
'Rectangle':
	node.GetNBit(c4d.NBIT_EHIDE) = 0
	node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
	node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
	node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
	node.GetBit(c4d.BIT_CONTROLOBJECT) = True
'Array':
	node.GetNBit(c4d.NBIT_EHIDE) = 0
	node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
	node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
	node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
	node.GetBit(c4d.BIT_CONTROLOBJECT) = True
'Cube':
	node.GetNBit(c4d.NBIT_EHIDE) = 0
	node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
	node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
	node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
	node.GetBit(c4d.BIT_CONTROLOBJECT) = True

Code

"""Demonsttrates diffrents concepts of viewport visibilty on the selected object in a scene and its descendants.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
op: c4d.BaseObject | None # The currently selected object, can be #None.

def WalkTree(node: c4d.GeListNode) -> typing.Iterator[c4d.GeListNode]:
    """Yields all descendants of #node in depth first manner.

    Includes #node itself in the output.
    """
    if not isinstance(node, c4d.GeListNode):
        return
    
    yield node
    for child in node.GetChildren():
        for descendant in WalkTree(child):
            yield descendant

def main() -> None:
    """
    """
    if not op:
        return
    
    for node in WalkTree(op):
        print(f"'{node.GetName()}':")
        print(f"\t{node.GetNBit(c4d.NBIT_EHIDE) = }")
        print(f"\t{node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = }")
        print(f"\t{node.GetNBit(c4d.NBIT_LOD_HIDE) = }")
        print(f"\t{node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = }")
        print(f"\t{node.GetBit(c4d.BIT_CONTROLOBJECT) = }")

    
if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand,

Thank you for the detailed answer once again. It cleared a lot of things for me.

From what I understood, I should use BIT_CONTROLBOJECT to detect if the object is 'Touched' by a generator object (in my sample case: child 'Cube' hidden by parent 'Array').

I tried doing so, but it looks like BIT_CONTROLOBJECT of 'Cube' is true regardless if it is attached to 'Array'.

Please see the screenshots below.

For this input:
a9d31d3e-0140-4600-8232-25e000e2f11a-image.png

I get the following console output:

---Cube---
OHIDE: false
EHIDE: false
HIDEEXCEPTVIEWSELECT: false
LOD_HIDE: false
SUBOBJECT_AM: false
CONTROLOBJECT: true

You can see that the BIT_CONTROLOBJECT is true when 'Cube' is not attached to anything.

my pseudo code:

        // Loop through selection.
	for (Int32 i = 0; i < selection->GetCount(); ++i)
	{
		auto obj = (BaseObject*)selection->GetIndex(i);

		std::unordered_set<BaseObject*> objs;
		objs.insert(obj);

		// Collect child objects recursively.
		CollectChildren(obj, objs);

		for (auto o : objs)
		{
			DiagnosticOutput("---@---", o->GetName());
			DiagnosticOutput("OHIDE: @", o->GetNBit(NBIT::OHIDE));
			DiagnosticOutput("EHIDE: @", o->GetNBit(NBIT::EHIDE));
			DiagnosticOutput("HIDEEXCEPTVIEWSELECT: @", o->GetNBit(NBIT::HIDEEXCEPTVIEWSELECT));
			DiagnosticOutput("LOD_HIDE: @", o->GetNBit(NBIT::LOD_HIDE));
			DiagnosticOutput("SUBOBJECT_AM: @", o->GetNBit(NBIT::SUBOBJECT_AM));
			DiagnosticOutput("CONTROLOBJECT: @", o->GetBit(BIT_CONTROLOBJECT));

			// If o is visible, then calculate extents.
		}
	}

I also recreated analogous scenario that you provided ('Array' as a parent and 'Cube' as a child) and got the same console output as you did.

@ferdinand said in Generator object's children visibility:

'Array':
node.GetNBit(c4d.NBIT_EHIDE) = 0
node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
node.GetBit(c4d.BIT_CONTROLOBJECT) = True
'Cube':
node.GetNBit(c4d.NBIT_EHIDE) = 0
node.GetNBit(c4d.NBIT_HIDEEXCEPTVIEWSELECT) = 0
node.GetNBit(c4d.NBIT_LOD_HIDE) = 0
node.GetNBit(c4d.NBIT_SUBOBJECT_AM) = 0
node.GetBit(c4d.BIT_CONTROLOBJECT) = True

You can see that BIT_CONTROLOBJECT is true when 'Cube' is attached to 'Array', so I cannot differentiate if the object is invisible due to generator object, because before attaching it to a generator object, BIT_CONTROLOBJECT was already true.

Please let me know, if I am misunderstanding the things that you wrote.

Hey @konradsawicki.

I haven't overlooked your reply here. I will answer tomorrow.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hello @konradsawicki,

yes, you are right BIT_CONTROLOBJECT is a poor indicator. There are more flags which are used for driving the visibility of objects, but they won't help you either [1].

Internally, this has been implemented quite unevenly, which in the end makes it impossible for users of the public API to properly react to this.

As said before, BIT_CONTROLOBJECT is only the front facing part of some being BaseObject::Touch'ed. The result of this is then that the object has this bit flag set and its caches being flushed. So, when you take your array/cube example, you can see that the Cube object will return nullptr when its BaseObject::GetCache() is being called, while the same cube will return its polygonal cache when outisde of the array object (check this in case you are a bit lost, as I talked there by chance exactly about this array/cube case). And when the cache of a generator object is empty, then there is also nothing to draw.

The problem is, that this all does not hold true for splines as input objects, when you for example put a circle spline below an Extrude object, it will still return something other than nullptr for ::GetCache(). This thread (which I had totally forgotten) sheds some light indirectly on the subject. For splines to be hidden in this manner, one must call GetHierarchyClone or GetAndCheckHierarchyClone on these input objects (other than for a polygon object generator which can just touch its inputs). As I wrote there, this method also calls ::Touch in the end, but for splines this is nor permanent and there is also non-public data involved. Which is then used by the super custom drawing routines (not) drawing such splines.

What you could do is, check with GeListNode::GetInfo() if an object has the flag OBJECT_ISSPLINE and then check if its parent has the flag OBJECT_INPUT, i.e., something like this:

const BaseObject* const op;
if (!op)
    return NOTOK;

const BaseObject* const parent = op->GetUp();
if (!parent)
    return NOTOK;

if ((op->GetInfo() & OBJECT_ISSPLINE) & (op->GetInfo() & OBJECT_INPUT))
    // Do something under the assumption that #op is a spline input for a generator that is hidden.

But as explained above, this is not the actual mechanism which hides things, and you will run into many false positives and false negatives with this, because how custom all the spline input generators are with their inputs.

Long story short, checking if an object O is a currently hidden input object for a generator object is unfortunately not possible to do in the public API. For an upcoming API we have added a data dependency system which will make it easier to determine this, but for now you are out of luck.

Maybe you and I can find an alternative approach? What you want to do is a bit unusual and to me it is not clear why you are trying to do this. Could you elaborate why this is useful and how you want to use it?

Cheers,
Ferdinand

[1] I used this to explore how the flags behave on generator object input objects and their caches. Simply select an object an run this script, it will dump a little tree to the console.

"""Prints the hierarchy and cache hierachy of the currently selected object and dumps visibility 
information for each node in that hierarchy.
"""
import c4d

# The primary selected object in the scene, can be `None`.
op: c4d.BaseObject | None


def PrintSceneTree(node: c4d.BaseObject, indent: int = 0) -> None:
    """Prints the hierarchy and cache hierachy of #node.
    """
    if not isinstance(node, c4d.BaseObject):
        return

    nodeName: str = f"{node.GetName()} ({node.GetRealType()})"
    tab: str = "   " * indent
    tabtab: str = "   " * (indent + 1)

    nodeString: str = f"{tab}{nodeName} ("
    space: str = " " * len(nodeString)
    bits: list[tuple(str, int)] = [
        ("BIT_CONTROLOBJECT", c4d.BIT_CONTROLOBJECT),
        ("BIT_IGNOREDRAW", c4d.BIT_IGNOREDRAW),
        ("BIT_TEMPDRAW_VISIBLE_CACHECHILD", c4d.BIT_TEMPDRAW_VISIBLE_CACHECHILD),
        ("BIT_TEMPDRAW_VISIBLE_DEFCACHECHILD", c4d.BIT_TEMPDRAW_VISIBLE_DEFCACHECHILD),
        ("BIT_TEMPDRAW_VISIBLE_CHILD", c4d.BIT_TEMPDRAW_VISIBLE_CHILD)
    ]
    nbits: list[tuple(str, int)] = [
        ("NBIT_EHIDE", c4d.NBIT_EHIDE),
        ("NBIT_HIDEEXCEPTVIEWSELECT", c4d.NBIT_HIDEEXCEPTVIEWSELECT),
    ]

    for i, (symbol, bitValue) in enumerate(bits):
        nodeString += f"{space if i != 0 else ''}{symbol}: {node.GetBit(bitValue)}\n"
    for symbol, bitValue in nbits:
        nodeString += f"{space}{symbol}: {node.GetNBit(bitValue)}\n"

    nodeString = nodeString[:-1] + ")"
    print(nodeString)

    for funcName, cache in ((f.__name__, f())
                            for f in [node.GetCache, node.GetDeformCache] if f()):
        print(f"{tabtab}{nodeName} returns for {funcName}():")
        PrintSceneTree(cache, indent + 2)

    if not node.GetChildren():
        return

    print(f"{tabtab}{nodeName} returns for GetChildren():")
    for child in node.GetChildren():
        PrintSceneTree(child, indent + 2)


if __name__ == "__main__":
    PrintSceneTree(op)
    print ("-" * 100)

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand,

What I am trying to do, is to calculate the bounding box's extents of selected objects by the user. Before, I was using BaseObject::GetCache() to calculate the extents of the selection and it worked, because as you said:

So, when you take your array/cube example, you can see that the Cube object will return nullptr when its BaseObject::GetCache() is being called, while the same cube will return its polygonal cache when outisde of the array object.

So the hidden object wasn't included because its cache is nullptr.

But the problem is that, later I need to call on the selected objects BaseObject::GetMg() and then BaseObject::SetMg(). Documentation says: "Only valid if the object is attached to a document. Virtual objects in caches and deform caches are not attached to a document, so this cannot be used for those objects."
Additionally, I need to include objects which don't have Opolygon objects in their cache (e.g. empty generator object, light, camera, rectangle - not sure if they even have a cache).

So after examining possible solutions, I was left with the one that I mentioned before:

@konradsawicki said in Generator object's children visibility:

// Loop through selection.
for (Int32 i = 0; i < selection->GetCount(); ++i)
{
auto obj = (BaseObject*)selection->GetIndex(i);

  std::unordered_set<BaseObject*> objs;
  objs.insert(obj);

  // Collect child objects recursively.
  CollectChildren(obj, objs);

  for (auto o : objs)
  {
  	DiagnosticOutput("---@---", o->GetName());
  	DiagnosticOutput("OHIDE: @", o->GetNBit(NBIT::OHIDE));
  	DiagnosticOutput("EHIDE: @", o->GetNBit(NBIT::EHIDE));
  	DiagnosticOutput("HIDEEXCEPTVIEWSELECT: @", o->GetNBit(NBIT::HIDEEXCEPTVIEWSELECT));
  	DiagnosticOutput("LOD_HIDE: @", o->GetNBit(NBIT::LOD_HIDE));
  	DiagnosticOutput("SUBOBJECT_AM: @", o->GetNBit(NBIT::SUBOBJECT_AM));
  	DiagnosticOutput("CONTROLOBJECT: @", o->GetBit(BIT_CONTROLOBJECT));

  	// If o is visible, then calculate extents.
  }

}

Hi @konradsawicki,

first of all, there already exist methods for retrieving the bounding box of an object and and they should respect empty caches correctly, see BaseObject::GetRad, ::GetMp, and ::GetDimension for details. Check also this therad for recursive bounding box computation.

Secondly, you seem to have misconceptions regarding caches. I would recommend having a look at either the ActiveObject plugin from the C++ SDK or this thread I already linked to above. A few satements:

  • The generator cache of an BaseObject is the output the ObjectData::GetVirtualObjects of its corresponding plugin hook. It is basically a hidden hierarchy of objects.
  • Caches of objects are built when an object is part of a document and BaseDocument::ExecutePasses is called on that document (with at least the generator/cache pass enabled).
  • Freshly instantiated objects or touched objects will have no caches, even when being part of a document.
  • You can set as many parameters as you want on an object with no cache, e.g., its global position. It will be correctly reflected, and for some parameters, e.g., the side length of a cube, it will cause caches to be built once you add an event or manually execute the passes yourself.

But overall, I currently do not see the necessity to go down this cache and hidden object rabbit hole in the first place for computing the bounding box of things. But I might be overlooking something here.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hello @konradsawicki ,

without further questions or postings, we will consider this topic as solved by Friday, the 11th of august 2023 and flag it accordingly.

Thank you for your understanding,
Maxon SDK Group