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

    Programmatically created Redshift Lights in an Object Plugin converts lights into Null-Objekts without an function after making Objekt-Plugin editable

    Scheduled Pinned Locked Moved Cinema 4D SDK
    2026202520242023pythonwindows
    12 Posts 2 Posters 685 Views 2 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • ThomasBT Offline
      ThomasB @ferdinand
      last edited by ThomasB

      @ferdinand

      Okay, thank you Ferdinand for your reply.
      Okay, that's strange—it works in your video.

      I don't think the issue lies with my code; it simply loads a Cinema 4D scene in which the objects are already prepared.

      But here is the unprotected plugin.
      As previously mentioned, this is just a simple, simplified plugin to demonstrate that the problem behaves exactly as described above when using "Make Editable."
      Download unprotected plugin

      and the code if this helps out:

      from typing import Optional
      
      import c4d
      from c4d import plugins, bitmaps, BaseObject
      import os
      
      
      PLUGIN_ID = 1000200
      
      
      def load_house():
      
          house_path = os.path.join(path, "res", "models", "build_1.c4d")
          doc = None
          if os.path.exists(house_path):
              doc = c4d.documents.LoadDocument(house_path, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS)
      
          return doc
      
      
      
      
      class RedshiftLightsTest(plugins.ObjectData):
      
          def __init__(self):
              self.SetOptimizeCache(True)
      
              self._temp_doc = load_house()
      
          def Init(self, op, isCloneInit):
      
      
              return True
      
          
      
          def GetVirtualObjects(self, op: BaseObject, hh: object) -> Optional[BaseObject]:
      
              cdirty = False
              for child in op.GetChildren():
      
                  cdirty = child.CheckCache(hh) or child.IsDirty(c4d.DIRTYFLAGS_DATA)
                  if cdirty:
      
                      break
      
              dirty = op.CheckCache(hh) or op.IsDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_ALL) or cdirty
              if not dirty:
                  return op.GetCache(hh)
      
      
              if self._temp_doc:
                  container = c4d.BaseObject(c4d.Onull)
      
                  base: c4d.BaseObject = self._temp_doc.GetFirstObject().GetDown().GetClone()
                  base.InsertUnder(container)
      
                  return container
              else:
                  return None
      
          def GetDDescription(self, op, description, flags):
              if not description.LoadDescription(op.GetType()):
      
                  return False
      
              single_id = description.GetSingleDescID()
              
      
              return False, (flags | c4d.DESCFLAGS_DESC_LOADED)            
      
         
      
          def GetDEnabling(self, op, did, t_data, flags, itemdesc):        
      
              return True
      
      
      if __name__ == "__main__":
          path, file = os.path.split(__file__)
          file = "icon.tif"
          new_path = os.path.join(path, "res", file)
          bitmap = bitmaps.BaseBitmap()
          bitmap.InitWith(new_path)
          plugins.RegisterObjectPlugin(id=PLUGIN_ID, str="redshift_lights_test", g=RedshiftLightsTest, description="redshift_lights_test", icon=bitmap,
                                       info=c4d.OBJECT_GENERATOR)
      
      

      Thanks,
      T.B

      1 Reply Last reply Reply Quote 0
      • ferdinandF Offline
        ferdinand
        last edited by ferdinand

        Hey @ThomasB,

        I do not think that this is an API issue. I do not meant with that that I will not help you, as this still happens in the context of a plugin, I say this simply to classify the issue.

        When I try to manually open your build_1.c4d file, it loads in 2026.2.0 but when I then press C on that light in question, I get exactly the output you report. And I cannot really explain why as this is just a normal rectangle shape area light (well, I sort of can, see below 😄 ).

        26b330be-2ffb-4079-94bd-e35821b41c29-image.png

        The plot however thickens when I try to open the file with 2026.3.1:

        3f42e837-826c-4145-87ae-9436d31c51f2-image.png

        This file seems to be somehow corrupted. After some poking around I manged to create this build_2.c4d which loads in 2026.3. But the light still did not work correctly. I then created a new RS light instance and copied over the whole data container via Get/SetData, which again resulted in a broken light. Only when I manually recreated the light by manually copying values, I ended up with a valid light. Which then also revealed that this light should have a much different light representation in the VP.

        Your light:

        8bb73cb1-8cf6-4851-9001-83dff031795e-image.png

        The manual copy:

        31f028b3-6054-4078-b9c0-82f04b49b7a9-image.png

        I.e., you seem to have there a fundamentally corrupted scene and especially light objects. The fact that I could reproduce this with a fresh light instance and a Get/SetData copy indicates this the data container of the light is corrupted. We could now start comparing the data containers of your light and the manually created one, to dive deeper, but I think I stop here for now. Did you programmatically modify this scene? Because your plugin only seems to load it. The question is now if you have more scenes like this. Otherwise I would just use my fixed scene (which also contains the fixed light) and move one.

        Cheers,
        Ferdinand

        PS: Okay, now I see it, the actual scene in much more complex.

        When you want to dig deeper yourself, a great tool to debug this would be mxutils.GetContainerTreeString. You can just dump both containers and then either visually compare them or use a diff tool. Or you use mxutils.GetParameterTreeString directly on the light objects.

        MAXON SDK Specialist
        developers.maxon.net

        ThomasBT 1 Reply Last reply Reply Quote 0
        • ThomasBT Offline
          ThomasB @ferdinand
          last edited by ThomasB

          @ferdinand
          thank you for your time Ferdinand. We all really appreciate your work. Without you, there would be only half as many plugins for Cinema 4D.

          Oh man, I'm sorry about the corrupted scene.

          Did you programmatically modify this scene? Because your plugin only seems to load it.

          The Road to a Corrupted File

          • Since I prefer modeling in R23, I did my modeling work there and saved the scene in R23.
          • Then I opened it in the latest version, 2026.3.1.
          • It notified me that it needed to modify the Cinema 4D scene and that it might no longer work with older versions of Cinema 4D.
          • Then, I just saved over it normally using 2026.3.1. That version runs on the MRD license.
          • I just wanted to make sure it was saved using the latest version.
            ➡️Apparently, it didn't correctly convert the scene to the new Redshift core. I'll compare the two versions later using mxutils. For now, everything looks normal in the new scene.

          I didn't modify the scene via code.
          Otherwise, the only other explanation I can think of for the corrupted file structure is the ZIP compression or a temporary license problem, between R23 and the MRD License. R23 runs on a Commercial License, Redshift runs on MRD.

          New scene:
          However, I’ve now created and saved a completely new scene—exclusively in 2026.3.1—and uploaded it as a new plugin. The file cannot be corrupt at this stage. Using the mxutils methods reveals no issues.

          Result:
          However, the result is the same. Something seems to struggle with the conversion process during the "Make Editable" when those lights originate from a loaded virtual document.
          So, if I create the area light directly via code as you did and then press "C", then it works, it appears in the scene as a standard Redshift Area Light. But you simply can't generate everything via code, so I need the ability to take things from pre-made scenes.

          Important:
          I’ve gone a step further and implemented a button that simply extracts the light from the virtual document—or rather, the container—and inserts it into the scene. Oddly enough, the light is inserted correctly into the active document this way.
          The user could bring the lights into the scene before pressing "C"—that would be a workaround.
          However, the problem with "Make Editable" still persists.

          Download New Plugin Version

          I wish you a relaxing weekend. As a Maxon SDK specialist, your head must be spinning quite often.
          Cheers

          Thanks,
          T.B

          1 Reply Last reply Reply Quote 0
          • ferdinandF Offline
            ferdinand
            last edited by

            Thanks, I will have a look. This all is starting to make sense now. We rewrote the RS Light and Camera objects a while ago. And at least the RS Camera object is not absolutely non-trivial to operate. This either sounds like that you have to jump through specific hoops or there is a bug in the importer code, that transforms pre 2024 Light and Camera objects (I think this was when we revamped them).

            It might take me a week or so to figure this out. When this is very urgent for you, I would recommend fixing the scene(s) by manually recreating the lights. You could probably also automate this by just selectively copying data (which is likely going to be the solution I am going to provide, unless I come to the conclusion that this must be fixed on our side, as for example the importer being faulty).

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

            ThomasBT 1 Reply Last reply Reply Quote 0
            • ThomasBT Offline
              ThomasB @ferdinand
              last edited by ThomasB

              @ferdinand

              Yeah, thanks—there’s no rush; I have until the end of July before the plugin is released.

              But as I mentioned, manually swapping the lights doesn't work; the problem persists even if the scene is created in Cinema 4D 2026.3.1. The only way it would work is programmatically as you mentioned above—via code in the plugin that iterates through all the lights in the loaded scene and swaps the data. I’ll give it a try; if load times and performance suffer, or if the effort isn't worth it, I’ll stick with the "Lights Import" button method for now.


              Summary of the Core Issue

              So, when scenes are imported via LoadDocument or MergeDocument in the code and parts returned within the GVO, they are correctly converted into polygon objects (after "Make Editable")—but Redshift lights are not converted into Redshift lights; instead, they are converted into Null objects. Numerous tests have confirmed this, regardless of whether the scene was created in the latest version of C4D or an older one, such as 2023.
              ➡️A programmatic replacement of the lights is necessary after the virtual document has loaded.

              Best Regards
              T.B


              Edit:

              I’ve implemented a light replacer in the example plugin again.
              It searches self.temp_doc for all Redshift lights, iterates through the list, creates a completely new light programmatically, sets the key values ​​to match the old light, applies the GetDataInstance to the light in the temp doc, and sets the global matrix.
              ➡️As discussed, this approach doesn't work either; the lights are still being converted into Null objects.
              That means I would have to replace the lights entirely with programmatically generated ones, which would require replicating the entire light and temp_doc hierarchy—including cases where lights contain other lights or objects. The effort involved simply isn't worth the benefit. Sorry.

              In this case, it would be better to look for the cause of the problem.

              Thanks,
              T.B

              ferdinandF 1 Reply Last reply Reply Quote 0
              • ferdinandF Offline
                ferdinand @ThomasB
                last edited by ferdinand

                Hey @ThomasB,

                so, I had a look and figured it out. When you diff the two objects from my file, there are quite few differences (as I was not 100% faithful in copying all values), but the culprit is this:
                cef8fb22-88d1-4073-8b29-1e2958c0db14-image.png

                The RS Light uses a standard light to accomplish its VP representation. When you disable the 'Illumination' checkmark, the RS Light GetVirtualObjects will simply return a null object, i.e., the behavior you experienced. I filed a ticket for this (ITEM#651586 Inconsistent Redshift Light 'Make Editable' behavior), as I would consider this so unintuitive that it borders on a bug. But this is outside of the SDK/Cinema 4D domain, so I cannot just fix it myself.

                In Python, there is currently no way to fix this on your side. The message MSG_CURRENTSTATE_END is not wrapped for Python (with it you can manipulate the output of a "Make Editable" action before it is inserted). The other route, the build flags on HierarchyHelp are also not possible in Python either, because HierarchyHelp is also not wrapped for Python.

                But I just changed the latter route, and in the next major update you will be able to react to build flags. There will also be a new code example named py-objectdata_buildflags_2027 which showcases this feature. You can then react to your object being made editable, built for rendering, built for export, etc. and react by changing its output.

                In your case you would then probably not disable the illumination when your object is being made editable. So, for now, you either have to live with the behavior, or turn on "preview > illumination".

                Cheers,
                Ferdinand

                Result

                The output, i.e., diff, with the offending line highlighted.
                d0a8e14c-d913-4990-9ec5-29e4a5150cf3-image.png

                Code

                This assumes my scene from above with everything deleted but the two objects.

                import c4d
                import mxutils
                import difflib
                
                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.
                    """
                    data: list[str] = [
                        mxutils.GetParameterTreeString(node) 
                        for node in mxutils.IterateTree(doc.GetFirstObject(), True)
                    ]
                
                    # Diff the first two strings in the list and print the result to the console.
                    if len(data) < 2:
                        return c4d.gui.MessageDialog("Not enough data to perform diff.")
                    
                    a: str = data[0]
                    b: str = data[1]
                
                    diff: list[str] = list(difflib.unified_diff(a.splitlines(), b.splitlines(), lineterm=''))
                    if not diff:
                        print("No differences found.")
                    
                    print("\n".join(diff))
                    print("\n\n" + "=" * 80 + "\n\n")
                    print(a)
                    print("\n" + "-" * 80 + "\n")
                    print(b)
                    
                    c4d.EventAdd()
                    
                
                if __name__ == '__main__':
                    main()
                

                MAXON SDK Specialist
                developers.maxon.net

                ThomasBT 1 Reply Last reply Reply Quote 0
                • ThomasBT Offline
                  ThomasB @ferdinand
                  last edited by ThomasB

                  @ferdinand

                  Okay, great that you really dug into this. That could well have something to do with it.

                  However, if you look at my latest test plugin—the C4D scene—the "Illumination" checkbox on the Redshift light is enabled, yet when you select "Make Editable," the light still turns into just a Null Object. So, regardless of whether the illumination is on or not..

                  But as I mentioned earlier this only happens if the light originates from a temporary document in the code. Or rather, when the scene with the light is loaded into a temp_doc.

                  • e.g. LoadDocument() or MergeDocument()

                  That is why simply checking the "Illumination" box does not work.

                  Cheers

                  Thanks,
                  T.B

                  1 Reply Last reply Reply Quote 0
                  • ferdinandF Offline
                    ferdinand
                    last edited by ferdinand

                    Hey,

                    sorry for the delay, I did read your reply. When I set the parameter in your scene the issue disappears. I did not check the LoadDocument/MergeDocument aspect yet. But I am 99.99% sure that there is no bug in Orslight in the conventional sense, it is just that the Orslight::GetVirtualObjects method is a bit of a fever dream.

                    The issue is Orslight::GetVirtualObjects returns a null object in some cases and a null with a light attached in other cases. LoadDocument/MergeDocument having a significant enough impact on the scene graph that causes Orslight::GetVirtualObjects to change its cache building behaviour from "null + light" to "null" sounds a bit unlikely but is not impossible (that would mean the scene importer is buggy).

                    I will try to have another look tomorrow or next week. When you want to help yourself, you could just do what I did: Diff the light from the loaded scene with a light your have already in the document which you know that works.

                    Cheers,
                    Ferdinand

                    MAXON SDK Specialist
                    developers.maxon.net

                    1 Reply Last reply Reply Quote 0
                    • ThomasBT Offline
                      ThomasB
                      last edited by ThomasB

                      @ferdinand
                      Ok , I will give it another try tomorrow.
                      I will print this out again using this diff tool and compare it.


                      I understand your point of view and your perseverance. 👍. But quite honestly, Ferdinand. That workaround might have to be removed again with Maxon's next fix, which in turn might necessitate a further update to my plugin, and so on. The plugin's strength lies in its extensibility. Therefore, my focus regarding the plugin is on extensions, rather than on removing workarounds or adding new features.

                      Given the highly complex document structure and the various types of lights involved, a particularly complex method has to be written for this. Furthermore, as I mentioned earlier, some lights have other lights as child objects, and those in turn have their own, with the specific setup varying from building to building. And every light of the same type might even have slightly different settings as well. This isn't really how I wanted to handle it.


                      But I will give it a try. I know, complaining doesn't help. So I'll test it again with two lights, just like you did. One light from the document and the other generated programmatically. Then I'll compare the two again.
                      However, this will be the last feature before the release, as it simply needs to be ready for delivery.
                      The lighting feature is a bit of a thorn in my side anyway, since it’s very resource-intensive—but you know what users are like (myself included!). They want the all-singing, all-dancing, do-it-all solution.

                      All things considered, the plugin has turned out quite well and is on the verge of release. My actual implemented workaround for importing the lights into the scene works, too, and requires just a single click from the user. Since the lights for each level are contained within a null object, this light importer represents the most pragmatic approach for me, involving the least amount of effort.

                      So, if I don't manage to fix the "bug" according to your requirements now, I’m going to release it anyway. Unfortunately, I can't live on thin air. 😊

                      Respectfully,
                      T.B.

                      Person: I love AI, I want to do everything with AI...
                      Developer: So you want to replace your brain with a language model? bye Bye! There's the door 👉

                      Thanks,
                      T.B

                      ferdinandF 1 Reply Last reply Reply Quote 0
                      • ferdinandF Offline
                        ferdinand @ThomasB
                        last edited by ferdinand

                        Hey @ThomasB,

                        so I did a clean writing of your plugin demo code in preparation to debug the output and I cannot reproduce the issue you report. For me the light works. There were some issues with your code, but none of them should cause the light to malfunction as your reported on LoadDocument. What you did there with self.container was a bit dicey and could have caused the issue but it seems somewhat unlikely.

                        I am today on my Mac, it seems quite unlikely that this is an OS related issue.

                        The object after pressing the button in your plugin:
                        f6aea3b3-a566-4779-99f4-e71a61cea4f0-image.png
                        And the object after collapsing it:
                        56138dee-a9f5-415d-bc69-19e0a7695df1-image.png

                        Cheers,
                        Ferdinand

                        from c4d import plugins, bitmaps, BaseObject, GeListNode
                        
                        import c4d
                        import mxutils
                        import os
                        
                        
                        PLUGIN_ID: int = 1000200
                        PY_ADD_LIGHTS: int = 10000
                        
                        
                        class RedshiftLightsTest(plugins.ObjectData):
                            """
                            """
                            HOUSE_PATH: str = os.path.join(os.path.dirname(__file__), "res", "models", "build_1.c4d")
                        
                            def __init__(self) -> None:
                                """
                                """
                                self.SetOptimizeCache(True)
                                self._asset_doc: c4d.documents.BaseDocument | None = None
                        
                                # No, never store a reference to what you return as your cache in GVO!
                                # self.container = None
                        
                            def Init(self, op, isCloneInit: bool = False) -> bool:
                                """
                                """
                                if not os.path.exists(self.HOUSE_PATH):
                                    raise FileNotFoundError(f"House model not found at {self.HOUSE_PATH}")
                                
                                # For cloning it would be better to copy over the document to avoid reloading it, but since
                                # we do not implement NodeData.CopyTo() we will just reload it for now.
                                self._asset_doc = mxutils.CheckType(
                                    c4d.documents.LoadDocument(self.HOUSE_PATH, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS))
                        
                                return True
                            
                            def GetAssetPayload(self) -> c4d.BaseObject | None:
                                """Returns a copy of the asset payload for the plugin.
                                """
                                if not isinstance(self._asset_doc, c4d.documents.BaseDocument):
                                    raise RuntimeError("Asset not loaded correctly.") # Should not happen, but just in case.
                                
                                root: c4d.BaseObject = self._asset_doc.GetFirstObject()
                                if not root:
                                    raise RuntimeError("Asset document has no root object.")
                                
                                payload: c4d.BaseObject = root.GetDown()
                                if not payload:
                                    raise RuntimeError("Asset document has no payload object.")
                                
                                return payload.GetClone()
                            
                        
                            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:
                                    return c4d.BaseObject(c4d.Onull)
                                
                                payload: c4d.BaseObject | None = self.GetAssetPayload()
                                if payload:
                                    payload.InsertUnder(null)
                        
                                return null
                        
                            def Message(self, node: GeListNode, type: int, data: object) -> bool:
                                """
                                """
                                if type == c4d.MSG_DESCRIPTION_COMMAND:
                                    if data["id"][0].id == PY_ADD_LIGHTS:
                                        self.ImportLights(node)
                                        return True
                        
                                return True
                            
                            def ImportLights(self, node: GeListNode) -> bool:
                                """
                                """
                                if not isinstance(node, c4d.BaseObject) or not c4d.threading.GeIsMainThreadAndNoDrawThread():
                                    return False
                                
                                # This is a bit tedious but each of these calls can fail, and you cannot, or better, should
                                # not just do chain calls such as GetDown().GetDown().GetNext().GetNext(). One could abstract
                                # this away with a function such as Traverse(node, "DDNN") where D = Down, N = Next, but for 
                                # this example I just did it manually.
                                doc: c4d.documents.BaseDocument = node.GetDocument()
                                if not doc:
                                    return False
                                
                                payload: c4d.BaseObject | None = self.GetAssetPayload()
                                if not payload:
                                    return False
                                
                                down: c4d.BaseObject | None = payload.GetDown()
                                if not down:
                                    return False
                                
                                item: c4d.BaseObject | None = down.GetNext()
                                if not item:
                                    return False
                                
                                light: c4d.BaseObject | None = item.GetNext()
                                if not light:
                                    return False
                                
                                clone: c4d.BaseObject | None = light.GetClone()
                                if not clone:
                                    return False
                        
                                doc.InsertObject(clone)
                                c4d.EventAdd()
                                return True
                        
                        if __name__ == "__main__":
                            path, file = os.path.split(__file__)
                            file = "icon.tif"
                            new_path = os.path.join(path, "res", file)
                            bitmap = bitmaps.BaseBitmap()
                            bitmap.InitWith(new_path)
                            plugins.RegisterObjectPlugin(id=PLUGIN_ID, str="redshift_lights_test", g=RedshiftLightsTest, description="redshift_lights_test", icon=bitmap,
                                                         info=c4d.OBJECT_GENERATOR)
                        

                        MAXON SDK Specialist
                        developers.maxon.net

                        1 Reply Last reply Reply Quote 0
                        • First post
                          Last post