Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Recent
    • Tags
    • Users
    • Register
    • Login
    1. Maxon Developers Forum
    2. ferdinand
    3. Posts
    Offline
    • Profile
    • Following 0
    • Followers 17
    • Topics 58
    • Posts 3,293
    • Groups 2

    Posts

    Recent Best Controversial
    • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

      Hey,

      so I gave this a shot but I cannot reproduce it. Please provide a scene file as asked for when this is still a problem. I did not fully understand your instructions, so I created a simple scene where a cube is being rigged.

      When I print out the weights of the rig while running your script, I cannot see any change of data. Your video shows some change of values, but overlays and GUI values in general can be unreliable, and I am also not quite sure where the "cube_joint" overlay draws its data from.

      Cheers,
      Ferdinand

      Result

      my scene: joints.c4d
      3349d252-8642-4041-a6f3-1b69cddb7e8c-image.png

      import c4d
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          tag = op.GetTag(c4d.Tweights)
          if not tag:
              return
          j_cnt = tag.GetJointCount()
          p_cnt = op.GetPointCount()
          print("="*200)
          for i in range(10):
              weights = [tag.GetWeightMap(j_id,p_cnt) for j_id in range(j_cnt)]
              print(weights)
              for j_id in range(j_cnt):
                  tag.SetWeightMap(j_id,weights[j_id])
              tag.WeightDirty()
          
          
          c4d.EventAdd()
      
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: MergeDocument() crash C4D

      Hey @chuanzhen,

      we will need both the scene you are merging and merging into to make here any sensible statement. We will also need more than this one sole line of code.

      And while a crash always indicates that we could do something better, my hunch would be that that fault lies mostly with some plugin. Because Cinema itself uses heavily MergeDocument and if there would be a principal bug with the very common flags c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, we would have found and fixed it before.

      The first thing that comes to mind with your line of code is that you are off-main thread and then run into problems because you try to let the code run on the MT by passing None for thread. But off-main-thread, merging documents would be a bit unusual (and also illegal if one of the participating documents is a loaded document). You can use c4d.threading.GeGetCurrentThread() to get the current thread and pass it for thread.

      Another cause could be some NodeData plugin which holds data outside of its data container and which does not correctly serialize that extra data (does not implement NodeData::Read, ::Write, and ::CopyTo) which can then lead to crashes when Cinema tries to run the merged scene with only partially copied node data (assuming the plugin in questions also fails to do sanity/existence checks on its internal extra data).

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: List of selected edges

      Hey @Kantronin,

      you might want to look at geometry_polgyon_edges_2024.0.py, it should answer all your questions.

      The TLDR is: full-edge maps as you hint at in your posting do exist in Cinema via helper functions, but they are often also not very useful. Internally, Cinema is operating as any other DCC application with the concept of what is often called a "half-edge" data structure. So, when you have the two polygons A and B:

        a---b---e
        | A | B |
        d---c---f
      

      Which share an edge over the vertices b and c, this is actually two edges and not one, due to how normals work. For both polygons to have a normal facing in the same direction, they must have opposite winding orders, which means that the edge b-c is actually two edges: b->c and c->b. So, A could for example index the edge as bc, and B as cb. The example goes into more detail with this.

      Cheers,
      Ferdinand

      posted in General Talk
      ferdinandF
      ferdinand
    • RE: Disable all animation in document

      Hello @ceen,

      Yes, this is possible. @Anlv is right that ID_CTRACK_ANIMOFF is the toggle you are looking for (thank you for helping out!). You can then either combine it with manual scene traversal code, or use mxutils.RecurseGraph which is for most users probably the simpler option when they do not have intimate knowledge of the Cinema scene graph.

      Cheers,
      Ferdinand

      """Loops over all tracks in the scene and toggles their animation state.
      
      I.e., running this once will disable all tracks, running it again will enable all tracks, and so on.
      """
      
      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          # We walk the document using RecurseGraph, finding all tracks in the scene, no matter where they
          # are hiding. This is a relatively inefficient call as we really walk everything, and experts
          # could fine tune this. But for a simple script like this or any code that is not performance 
          # critical, this is just fine.
          for track in mxutils.RecurseGraph(doc, yieldBranches=True, yieldHierarchy=True, nodeFilter=[c4d.CTbase]):
              track[c4d.ID_CTRACK_ANIMOFF] = not track[c4d.ID_CTRACK_ANIMOFF]
      
          c4d.EventAdd()
      
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

      Thanks I will have a look, but it might take a few days before I find the time.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

      Hey @chuanzhen,

      thank you for reaching out to us. Please provide a code example and scene file to reproduce the issue, without it is hard to give a concrete answer.

      @Anlv and @ThomasB are correct in bringing forward that floating point precision could be an issue in principle. But IEEE 754 corresponds to roughly 17 significant digits in the interval [0, 1] without loss of precision for the data type Float64, a change in the third significant digit as you report is somewhat unlikely in the interval [0, 1] (but not impossible).

      And more importantly, all integer values, e.g., 1.0, are exactly representable up to 2^53 in IEEE 754-64, so you should never see a loss for the value 1.0. More over, round trips should mean no accumulated loss when you do not do additional computations with the value. So, you first write the value X which might or might not be representable as IEEE 754 -64 because you entered it as some kind of literal in your code or via a GUI. Once written X will be X', i.e., the closest value of X which is representable in IEEE 754-64. Further read and write events will not change that value, unless you do some arithmetic operations with it.

      Overall this sounds all bit like the weight tag is somehow post processing the passed data, and you therefore encounter some error creep. But I can only say anything concrete with code and a scene.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: After calling BaseContainer.Sort(), the stored values are lost

      Hey @chuanzhen,

      Thank you for reporting this. Yes, you are misunderstanding this function a bit. But I have to admit that I did too first, because the function is very weird in what it does. But when you look at the C++ docs, there is a warning.

      846d8773-a0bf-4ac4-b6b0-6207ca995bf6-image.png

      I have just updated the Python docs for the next release to also contain the warning. So, this function is effectively only useable for containers holding strings. As you can see by my example below, it does not only remove all values but turns them into strings (and then leaves them empty when the source was not a string). The function is badly named.

      Find also a little example below to do what you want: sort a container by values.

      Cheers,
      Ferdinand

      Result

      Root (None , id = -1):
      ├── 4 (DTYPE_LONG): 4
      ├── 5 (DTYPE_LONG): 5
      ├── 6 (DTYPE_LONG): 6
      ├── 7 (DTYPE_LONG): 7
      ├── 8 (DTYPE_LONG): 8
      ├── 9 (DTYPE_LONG): 9
      ├── 10 (DTYPE_LONG): 10
      ├── 11 (DTYPE_LONG): 11
      ├── 0 (DTYPE_LONG): 0
      ├── 1 (DTYPE_LONG): 1
      └── 2 (DTYPE_LONG): 2
      --------
      Root (None , id = -1):
      ├── 1 (DTYPE_STRING):
      ├── 4 (DTYPE_STRING):
      ├── 5 (DTYPE_STRING):
      ├── 6 (DTYPE_STRING):
      ├── 7 (DTYPE_STRING):
      ├── 8 (DTYPE_STRING):
      ├── 9 (DTYPE_STRING):
      ├── 10 (DTYPE_STRING):
      ├── 11 (DTYPE_STRING):
      ├── 0 (DTYPE_STRING):
      └── 2 (DTYPE_STRING):
      

      Code

      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          bc = c4d.BaseContainer()
          for i in range(4,12):
              bc.SetInt32(i,i)
          for i in range(3):
              bc.SetInt32(i,i)
          print(mxutils.GetContainerTreeString(bc))
          print("--------")
          bc.Sort()
          print(mxutils.GetContainerTreeString(bc))
          
          
      if __name__ == '__main__':
          main()
      

      Custom Sorting Function

      Result

      Root (None , id = -1):
      ├── 0 (DTYPE_LONG): 0
      ├── 1 (DTYPE_LONG): 1
      ├── 2 (DTYPE_LONG): 2
      ├── 3 (DTYPE_LONG): 3
      ├── 5 (DTYPE_LONG): 5
      ├── 6 (DTYPE_LONG): 6
      ├── 7 (DTYPE_LONG): 7
      ├── 8 (DTYPE_LONG): 8
      ├── 9 (DTYPE_LONG): 9
      ├── 10 (DTYPE_LONG): 10
      ├── 11 (DTYPE_LONG): 11
      ├── 12 (DTYPE_LONG): 12
      ├── 13 (DTYPE_LONG): 13
      ├── 14 (DTYPE_LONG): 14
      ├── 15 (DTYPE_LONG): 15
      ├── 16 (DTYPE_LONG): 16
      ├── 17 (DTYPE_LONG): 17
      ├── 18 (DTYPE_LONG): 18
      ├── 19 (DTYPE_LONG): 19
      └── 4 (DTYPE_SUBCONTAINER , id = -1):
          ├── 0 (DTYPE_LONG): 0
          ├── 1 (DTYPE_LONG): 1
          ├── 2 (DTYPE_LONG): 2
          ├── 3 (DTYPE_LONG): 3
          ├── 4 (DTYPE_LONG): 4
          ├── 5 (DTYPE_LONG): 5
          ├── 6 (DTYPE_LONG): 6
          ├── 7 (DTYPE_LONG): 7
          ├── 8 (DTYPE_LONG): 8
          ├── 9 (DTYPE_LONG): 9
          ├── 10 (DTYPE_LONG): 10
          ├── 11 (DTYPE_LONG): 11
          ├── 12 (DTYPE_LONG): 12
          ├── 13 (DTYPE_LONG): 13
          ├── 14 (DTYPE_LONG): 14
          ├── 15 (DTYPE_LONG): 15
          ├── 16 (DTYPE_LONG): 16
          ├── 17 (DTYPE_LONG): 17
          ├── 18 (DTYPE_LONG): 18
          └── 19 (DTYPE_LONG): 19
      

      Code

      import c4d
      import mxutils
      
      import random
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def SortContainer(bc: c4d.BaseContainer, mode: str = "value") -> None:
          """Sorts the given container by ID or value.
      
          This function reallocates sub-containers but modifies the passed in container in place. It would also be
          possible to modify the sub-container in place, but more work to implement and also more complex to run.
          Due to the fact that we have to copy container data, this is also not the cheapest function.
          """
          def copy(bc: c4d.BaseContainer) -> list[tuple[int, any]]:
              """Copies the contents of the given BaseContainer to a list of (ID, value) tuples.
              """
              result = []
              for i, v in bc:
                  if isinstance(v, c4d.BaseContainer):
                      v = copy(v)
                  result.append((i, v))
              return result
      
          def build(bc: c4d.BaseContainer, items: list[tuple[int, any]]) -> None:
              """Builds a BaseContainer from the given list of (ID, value) tuples.
              """
              bc.FlushAll()
              for i, v in items:
                  if isinstance(v, list):
                      sub = c4d.BaseContainer()
                      build(sub, v)
                      v = sub
                  bc[i] = v
      
          def sort_items(items: list[tuple[int, any]], mode: str) -> list[tuple[int, any]]:
              """Sorts the given list of (ID, value) tuples by ID or value. Nested containers are sorted recursively.
              """
              def is_nested(item: tuple[int, any]) -> bool:
                  return isinstance(item[1], (list, tuple))
      
              def sort_key(item: tuple[int, any]) -> tuple[bool, any]:
                  item_id, value = item
      
                  # The first key puts nested values after scalar values.
                  # The second key is only compared within the same group.
                  if is_nested(item):
                      return True, item_id if mode == "id" else 0
      
                  return False, item_id if mode == "id" else value
      
              # Sort nested contents recursively before sorting this level.
              for index, (item_id, value) in enumerate(items):
                  if is_nested((item_id, value)):
                      items[index] = (item_id, sort_items(value, mode))
      
              return sorted(items, key=sort_key)
      
          items: list[tuple[int, any]] = copy(bc)
          return build(bc, sort_items(items, mode))
      
      def main() -> None:
          """
          """
          bc: c4d.BaseContainer = c4d.BaseContainer()
          data: list[int] = list(range(20))
          random.shuffle(data)
          for v in data:
              bc[v] = v
      
          copy: c4d.BaseContainer = bc.GetClone(0)
          bc[4] = copy
      
          print(mxutils.GetContainerTreeString(bc))
          print("--------")
          SortContainer(bc)
          print(mxutils.GetContainerTreeString(bc))
          
          
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Redshift Maya Extension SDK Access Request (Custom Hair Translator)

      There seems to be some solution, please stay tuned until the RS team has sorted out things. I will then update this topic.

      posted in General Talk
      ferdinandF
      ferdinand
    • RE: Alembic Export to Unreal - RootUVs?

      What I meant was to use the Cineware Unreal bindings, but I am not sure if they support it. Going the export route is a big backwards when Cinema has specific bindings.

      https://www.maxon.net/en/cineware

      I will give the Unreal dev a bump when I see him again.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Redshift Maya Extension SDK Access Request (Custom Hair Translator)

      Hey @shiryuta,

      thank you for reaching out to us. There is no public Redshift for Maya SDK. There is a semi-public Redshift Core SDK, but I doubt that this will help you much in this case, as you communicate directly with the Redshift Core in the GPU (and the access is also fairly limited). The Core SDK is also semi-public, i.e., we do not give access to everyone.

      I have pinged the Redshift team about your case. Please have some patience, as some key people are there currently on vacation and will only return in early August. What I can already tell you now, is that we will not meet your request in all points. We will for example never publish internal headers, and we also never write code example for non-public APIs.

      But that does not necessarily mean that there will not be a solution for your problem, only the Redshift for Maya team can clear up the details.

      Cheers,
      Ferdinand

      posted in General Talk
      ferdinandF
      ferdinand
    • RE: [Free Plugin]Plugin Debug Manager v1.3.0— Python Plugin Reloading for Cinema 4D

      I have pinned this as extra tooling is always welcome 🙂 Thanks for sharing it !

      posted in General Talk
      ferdinandF
      ferdinand
    • RE: Programmatically created Redshift Lights in an Object Plugin converts lights into Null-Objekts without an function after making Objekt-Plugin editable

      This is not common knowledge, otherwise I would have spotted that earlier.

      So, you are not really expected to know this as a third party dev. You can look at the geometry_caches_s26.py example and there at this line, under [2] I mention that the cache of that object is muted. But that is of course only very indirect information and not enough to deduce this. I think we also talked about it a couple of times on the forum.

      The flag BIT_CONTROLOBJECT is set on input objects of generators, e.g., the to be extruded spline of an Extrude object, the profile and rail spine of a Loft object, the to be cloned cube and sphere objects of a MoGraph cloner, etc. As shown in the caching example, Cinema will mute the caches of input objects. To be very formal, they are actually built but then again deleted. BIT_CONTROLOBJECT marks such input objects.

      Your light in that scene should not have that flag, as it is not the input for some generator. But it somehow got it anyway, there is probably somewhere a bug. For normal scene evaluation that bug is also not really a problem (because as you saw from your own test, the light still converted correctly when you inserted it on its own outside of caches). The problems start when that irregularly as BIT_CONTROLOBJECT marked object is part of a cache. 'Make Editable' then incorrectly collapses the cache. Not only does it not stop at nested generators which are incorrectly marked like this, it also seems to apply the cache muting logic for input objects (resulting in the empty null object instead of the content of the cache of the collapsed generator).

      I already told the relevant devs that I consider this borderline buggy but they do not seem very inclined to do anything about this. What is effectively missing somewhere in cache resolvement is the evaluation if the flag BIT_CONTROLOBJECT 'makes sense on an object'. That is of not so easy to do, as generator - input object relations can be quite complex in Cinema. Which is probably also why the modelling team does not want to touch this.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Alembic Export to Unreal - RootUVs?

      Hey @mp-grafix,

      I am not really an expert on this subject, but the Alembic format does not provide a "root uv" concept as you somewhat imply. There are zero hits for these terms in the Alembic docs. groom_root_uv is a custom Alembic attribute that is supported (and defined) by Unreal (link). This link also explains how to compute this a bit ambiguous term. One might think a root uv would be the uv coordinate on some scalp mesh, but it is actually just the spherical projection of a hair root (that is at least how it is explained in the Unreal link). Unreal will also autogenerate this attribute for your when it does not exist.

      The UV computation of our hair library can be found in c4d.modules.hair.HairLibrary.GetPolyPointST but this is per polygon. You can look at this example for how to use it on a whole mesh. But as far as I understand the Unreal docs, that is not what Unreal wants from you.

      The official Alembic repo also contains the Python bindings, PyAlembic, with them you could add any metadata you want after Cinema exported the file. Hurdle number one is that the Alembic repo does not seem to provide binaries, i.e., they expect you to compile this yourself, which might be a hurdle to high for some users. Hurdle number two is to match the exact format Unreal expects. That is best solved in Unreal forums.

      I am not very much into Unreal, but does our Cineware Unreal binding not solve this? I would say you have much better chances to be heard there, rather than us conforming with our Alembic export to the very specific custom attribute format Unreal expects.

      Cheers,
      Ferdinand

      edit: Our Cineware for Unreal developer is currently on vaction until the middle of August. When you bump this topic then again, I am happy to ask him what his take on this is.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Cache Proxy Tag

      I am afraid there is not really good way to do this. You could expose the tags as BaseLink's in the parameters of your object, so that users can drag them, but that is also clunky and a also a bit 'yikes' to expose the cache internals like that.

      Unless the object and selections are very complex, I would simply go by creating the selections in the cache, and then either documenting them in the docs, using short names such as C1 and C2 for cap one and cap two, or alternatively display a little static text at bottom of your node in the attribute manager which lists these names. This also worked a long time for Cinema itself like this, before we got seven years ago or so the proxy concept.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Cache Proxy Tag

      Hey @Dunhou,

      No, you cannot create such tags from the public C++ or Python API. You can resolve existing tags via MSG_GETREALTAGDATA to their real underlying tag, but you cannot create new ones. At least we do not expose the internal interfaces we use for this.

      You can print the data container of a proxy tag, and will find something like this:

      Root (None , id = -1):
      ├── 2003 (DTYPE_REAL): 0.5
      ├── 2002 (DTYPE_LONG): 0
      ├── 2001 (CUSTOMDATATYPE_FIELDLIST): <c4d.FieldList object at 0x000002B6CF99C8C0>
      ├── 1011 (DTYPE_LONG): 0
      ├── 1000 (DTYPE_LONG): 5673
      ├── 1050449 (DTYPE_SUBCONTAINER , id = -1):
      │   ├── 1 (DTYPE_LONG): 4001
      │   └── 3 (DTYPE_LONG): 4101
      └── 1041671 (DTYPE_VECTOR): Vector(1, 0.9, 0.4)
      

      Where 1050449 is the ID for Tcacheproxytag and this sub-container then contains two fields which represent these two IDs:

      e50d1391-9aae-4765-adfd-9792028c8859-image.png

      We seem to have removed the old BaseLink approach we used before. And the code for resolving the 'name' (which as you can see is actually DTYPE_LONG) is not super trivial. If I would pull out this information for you and others, I would also have to maintain it, which you can see by the deprecation of the base-link, would be ongoing work. When you can make it work with the hinted at information, go for it. But this is a private interface for now, and I cannot reveal its details.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Adding an Icon to a GeDialog Menu Group

      Hey @mogh,

      It depends a bit on what you mean with 'this'. Loading menu resources still does not seem to work in Python. I just tried, and while loading for example the main menu resource in your plugin just works fine, I cannot make it work with a locally defined plugin resource. Below you can see me loading M_EDITOR, i.e., the main menu resource, into the py-cmd_gui_resources_2024 example.

      9f49bdf7-33b1-4ba7-9114-52365c51e3f7-image.png

      That is probably what I was referring to a bit fuzzily with 'I am sorry to inform you, that this is currently not possible. Because the procedures used by Cinema internally cannot be reflected to Python at the moment.' six years ago. I would have to sit down and debug this piece by piece to see what is going wrong with loading locally defined menu resources (or if I just made a mistake in the resource files). Resources in Cinema 4D are a bit different from plugin resources, and menu resources have never been publicly documented or demonstrated, neither in C++ nor in Python. So, this being broken is not impossible.

      What you can do right now, is define a string menu item with an icon. But you will not be able to use this to create submenus with an icon as the original question was about. You can only create a string menu item with an icon, but not a submenu with an icon.

      Cheers,
      Ferdinand

      548c045e-dde1-40f1-8507-aaf26363a986-image.png

      
      import c4d
      
      class IconMenuDialog(c4d.gui.GeDialog):
          """
          """
          def CreateLayout(self) -> bool:
              """Called by Cinema 4D when the GUI of the dialog is being built.
              """
              self.GroupBorderSpace(5, 5, 5, 5)
      
              # Add a menu called "Items".
              self.MenuSubBegin("Items")
              # And a submenu called "Objects".
              self.MenuSubBegin("Objects")
              # Here we use string menu items with the &i icon embed code. This only works for items and
              # not for submenus, therefore resources are currently the only way to define icons for submenus.
              self.MenuAddString(1000, f"Item 1&i{c4d.Ocube}&")
              self.MenuAddString(1001, f"Item 2&i{c4d.Osphere}&")
              self.MenuSubEnd()
      
              # Same thing, but here we use commands instead of string menu items. This will automatically 
              # add the icon and label of the command to the menu item. When invoked, this will execute also 
              # the command without us having to implement this ourself.
              self.MenuSubBegin("Splines")
              self.MenuAddCommand(c4d.Osplinecircle)
              self.MenuAddCommand(c4d.Osplinerectangle)
              self.MenuSubEnd()
      
              self.MenuSubEnd()
             
              return True
      
      if __name__ == "__main__":
          dialog: IconMenuDialog = IconMenuDialog()
          dialog.Open(c4d.DLG_TYPE_ASYNC, defaultw=200, defaulth=100)
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Programmatically created Redshift Lights in an Object Plugin converts lights into Null-Objekts without an function after making Objekt-Plugin editable

      Hey @ThomasB,

      my bad, I did miss that. The reason why you are still having problems is that in the scene data you load from your asset file, has generator and scene building flags set on its nodes that are visible in the object manager. When you then put them into the cache of your object, this leads to buggy/unintended behavior, as these flags should not appear in caches.

      Cheers,
      Ferdinand

      Result

      The initial state:
      7e7b61ce-1255-487b-a969-27469b392a5c-image.png

      Collapsing your generator, it now correctly preserves the nested generators:
      1c9f0097-2ee8-4fad-a0d5-744c91588140-image.png

      Collapsing once more the RS Light:
      c6c4e92c-81fe-4a64-935b-a7be307e9a8c-image.png

      Code

      This builds on the cleaned up code example I posted above. But effectively you only need ResetFlags and call it on your cache before you return it. But you should also remove the reference (self.container) to your own cache in your code example from your code, as this could cause some serious issues.

          def ResetFlags(self, node: c4d.BaseObject) -> None:
              """Resets selected flags in the local hierarchy of the given node.
              """
              def ResetFlags(node: c4d.BaseObject) -> None:
                  node.DelBit(c4d.BIT_CONTROLOBJECT)
                  node.DelBit(c4d.BIT_EDITOBJECT)
      
              ResetFlags(node)
              for child in mxutils.IterateTree(node, True):
                  ResetFlags(child)
      
              return node
      
          def GetVirtualObjects(self, op: BaseObject, hh: object) -> BaseObject | None:
              """
              """
              # Return the cached object if it is not dirty, otherwise rebuild it.
              dirty: bool = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_ALL)
              if not dirty:
                  return op.GetCache(hh)
              
              # Returning None in an ObjectData.GetVirtualObjects() call will tell Cinema that a memory error
              # occurred, and with that cause Cinema to stop calling/building the object. This is almost never
              # what we want, return a Null object instead.
              null: c4d.BaseObject | None = c4d.BaseObject(c4d.Onull)
              if not null:
                  raise c4d.BaseObject(c4d.Onull)
              
              payload: c4d.BaseObject | None = self.GetAssetPayload()
              if payload:
                  payload.InsertUnder(null)
      
              #  --- DEBUG ---
      
              # Some debug code I wrote to figure out what is going on here. I then quickly figured out 
              # that the parameters were okay, but that this light always malfunctioned. And that returning 
              # a fresh light worked fine. So, the culprit must be outside of the data container of the 
              # light but still inside the light object. I.e., BIT or NBIT flags, which then directly 
              # made me think of the BIT_CONTROLOBJECT and BIT_EDITOBJECT flags.
              #
              # WARNING: The code below is highly illegal code, as we insert objects into the active 
              # document from a non-main thread. This is just for debugging purposes, as I did not mind 
              # Cinema potentially crashing, and should never be done in production code.
              #
              # for child in mxutils.IterateTree(null, True):
              #     if child.GetType() == c4d.Orslight:
              #         print("-" * 100)
              #         print(mxutils.GetParameterTreeString(child))
              #         doc: c4d.documents.BaseDocument | None = c4d.documents.GetActiveDocument()
              #         if doc:
              #             doc.InsertObject(clone)
              #             c4d.EventAdd()
      
              #  --- FIX ---
      
              # Now we reset the build flags of all objects in the hierarchy. Not doing this caused 'Make 
              # Editable' to incorrectly collapse the cache of this object, going much deeper than it 
              # should. When you have this setup, i.e., a generator within the cache of another generator:
              #
              #   SomeGenerator (Obase)
              #   └── Cache
              #       └── Null (Onull)
              #           └── CubeGenerator (Obase) // Generator that is inside the cache of SomeGenerator
              #               └── Cache
              #                   └── Null (Onull)
              #                       └── PolygonObject (Opolygon)
              #
              # then the collapsed cache of SomeGenerator should be (important is the type in parentheses, 
              # not the name in front) as follows:
              #
              #   SomeGenerator (Onull)
              #   └── CubeGenerator (Obase)         // the uncollapsed CubeGenerator generator which was 
              #                                     // before inside the cache of SomeGenerator
              #
              # And not this.
              #
              #   SomeGenerator (Onull)
              #   └── CubeGenerator (Onull)
              #       └── PolygonObject (Opolygon)
              #
              # But the former is exactly what happened when we do not reset the build flags of all objects 
              # in the hierarchy. On top of that comes some bug / side effect of when you do this nested
              # collapsing that it seems to 'forget' half of the cache, 'PolygonObject (Opolygon)' in the
              # example above, or the converted standard light from the RS light in your case.
              self.ResetFlags(null)
      
              return null
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Problem with Undo when using COLOR field in a description resource

      I tried with a Cube where the only way to change colour is to use the display colour in the basic tab. At first sight this doesn't cause the problem, but in fact it does.

      Yes, that was what I meant. Thanks for confirming. The issue likely already has vanished in the current alpha, because there I could not reproduce it. But in 2026.3.2 I can. What is a bit odd is that no one seems to have touched the relevant code in the last months. But that is something for the GUI team to figure out.

      posted in Bugs
      ferdinandF
      ferdinand
    • RE: Custom node-locked licensing: Bypassing checks for Team Renderer, Clients, Server safely?

      Hey @ThomasB,

      while I understand the direction, your question is a bit ambiguous. What would you consider 'reliable and secure against spoofing'? This very much a question of perspective. Everything can be faked and altered with enough knowledge and determination when the data is local. The functions you mention are based on our licensing API (and are also a bit outdated, you should use the newer endpoints). This API is used by Maxon products itself to verify licenses and is considered secure enough for that purpose. However, nothing prevents a user from patching the Cinema binary so that these functions return whatever the user wants. But that is of course expert knowledge domain.

      I would recommend using the relative new Licensing Manual which also explains how to identify the current product and how to find out how many licenses of a given type are available in a user account. In the video I also touch the subject of hardening for plugin licenses, the TLDR is: Do not over-do it, it is not worth the effort. Everything can be cracked.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Problem with Undo when using COLOR field in a description resource

      Hey @spedler,

      I did give this a shot. I cannot reproduce this after even quite a few attempts, but one of my colleagues (Fritz) could. There is probably some undo code missing in the color chip dialog. I assume that was what you did, Steve, right? You set the color via the popup color chip dialog and not with the inline control? Fritz could reproduce this without your plugin. So, my hunch was right, your plugin is not the culprit.

      Since I cannot fix what I cannot reproduce on my machine, I have created a bug ticket for this and sent it off to the owners of the COLOR control. The ticket is ITEM#652737 Parameters of type COLOR do not always correctly invoke UNDOs in descriptions.

      Out of curiosity: Can you also reproduce this on your machine with a a builtin object, e.g., the Cube object? Fritz and I were both on macOS while testing. But this being OS-specific seems a bit unlikely.

      Cheers,
      Ferdinand

      I have moved this topic into "bugs".

      edit: As always with these things now I can reproduce it too 😄

      posted in Bugs
      ferdinandF
      ferdinand