SOLVED How to detect a new light and pram change?

Hello

How to detect a new light add into the doc and refresh some settings?

I want to make a light manager, but now I have to refresh it to loop the doc again
So can it update automaticlly when a new light add or change some light prameters?

Thanks

Hello @dunhou,

Thank you for reaching out to us. What you want to do is possible but not as trivial as you might think it is. Find at the end of my posting a script which implements the fundamental principles for what you want to do. I would also like to point out Light Lister by @m_adam which does what you want to do. As a warning: @m_adam wrote this before he joined Maxon, so not everything you find there is reference material. But it will certainly be worth a look.

Cheers,
Ferdinand

Technical Explanation

The classic API of Cinema 4D has only a very coarse event system. To track changes in a scene, only the broad message c4d.EVMSG_CHANGE is being broadcasted to inform the user that something has changed, but not what. Cinema 4D has no (core message) mechanism which would inform specifically about an object, or a light being added. EVMSG_CHANGE is a core message which can be received in the CoreMessage methods of a MessageData plugin, a GeDialog, or a GeUserArea.

Identifying Objects

To solve this, you must track the scene graph and its changes yourself. In a simplified manner this means that you traverse everything, in your case the objects, in the scene and create a look-up table of things which you have encountered. So, when we have this scene,

Cube
| +- Light_0
| +- Cone
Sphere
  +- Light_1

you would end up with the list [Light_0, Light_1] and if on the next EVMSG_CHANGE the scene would be as follows,

Cube
| +- Light_0
| +- Cone
Sphere
  +- Light_1
  +- Light_2

then you could deduce that the change event was for Light_2 having been added. There is however one problem with this: You cannot simply store references to the objects themselves as Cinema 4D reallocates nodes in background. So, when you have established this list

myLookup: list[c4d.BaseObject] = [Light_0, Light_1]

at some point, then an EVMSG_CHANGE is being broadcasted at a later point, you then traverse the scene graph, and reach the object which is named Light_0 again, and do this:

if Light_0 in myLookup:
    DoSomething()

DoSomething() might not trigger, although you have put Light_0 into myLookup before. The reason is then that Cinema 4D has reallocated Light_0 in the background at some point. The reference in myLookup to now a dangling reference to an object which does not exist anymore and was located at an entirely different place in memory than the currently existing Light_0. The data of both objects can still be entirely the same, it is only that in C++ you cannot rely on BaseList2D pointers for storing access to parts of a scene, and by extension storing references to them in Python. You can test for something like this having happened in the Python API with C4DAtom.IsAlive(), such dangling scene element, an object, material, shader, document, etc., will then return False.

This problem can be solved in multiple ways, but one intended by Cinema 4D is markers. Cinema 4D allows metadata to be attached to scene elements to signify a context. In Python this mechanism is primarily accessible with C4DAtom.FindUniqueID() which returns the stored data of such markers. The marker, which is here of particular interest to you, is the one with the identifier c4d.MAXON_CREATOR_ID. When Cinema 4D creates a new object from scratch, it will store there a hash which uniquely identifies the object; this also works over saving and reloading a scene. The important bit is that this hash will not change when Cinema 4D reallocates a node. So, this statement in pseudo code holds true:

uuid_0: bytes = bytes(Light_0.FindUniqueID(c4d.MAXON_CREATOR_ID))
uuid_1: bytes = bytes(Light_0_Reallocated.FindUniqueID(c4d.MAXON_CREATOR_ID))

uuid_0 == uuid_1

In practice it does not work exactly like that, because when there is already a Light_0_Reallocated, Light_0 is already a dangling reference by then, a 'dead' node in Python terms. So, you must store the UUID of Light_0 beforehand.

Identifying Parameter Changes

When you have jumped though all these hoops, tracking parameter changes is relatively simple. All atoms, e.g., light-objects, have the method C4DAtom.GetDirty which tracks the change checksums of that atom. With the identifier c4d.DIRTYFLAGS_DATA you can retrieve the change checksums for the data container of an object, i.e., its parameter values. You only have then to store that information in your tracking data.

The Code

"""Provides a bare-bones example for tracking changes in the scene graph of a document.

Run this example as a Script Manager script. It will track you adding standard or Redshift lights to a scene and if you change their parameters.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument  # The active document
op: typing.Optional[c4d.BaseObject]  # The selected object, can be None.

class LightTrackerDialog (c4d.gui.GeDialog):
    """Tracks the existence of light objects and their parameter changes in a scene.

    This is a very simplistic implementation, as it for example assumes always to be fed by data
    from the same active document. You should add more rigorous checks.
    """
    # The light types we are going to track.
    Orslight: int = 1036751
    TRACKED_TYPES: tuple[int] = (c4d.Olight, Orslight)

    # Some minor symbols for managing the internal lookup table.
    NEW_LIGHT: int = 0     # A new light has been found
    UPDATED_LIGHT: int = 1 # An existing light has been updated.

    def __init__(self) -> None:
        """Initializes a LightTrackerDialog and its internal table tracking a scene state.
        """
        self._lightTable: dict[bytes: dict] = {}

    def CreateLayout(self) -> bool:
        """Adds GUI gadgets to the dialog.

        Not needed in this case, as we do not want to use GeDialog as a dialog, but for its ability
        to receive core messages.
        """
        self.SetTitle("Light Tracker Dialog")
        self.GroupBorderSpace(5, 5, 5, 5)
        self.AddStaticText(id=1000, flags=c4d.BFH_SCALEFIT, inith=25, name='This GUI has no items.')
        return True

    def CoreMessage(self, mid: int, data: c4d.BaseContainer) -> bool:
        """Receives core messages broadcasted by Cinema 4D.
        """
        # Some change has been made to a document.
        if mid == c4d.EVMSG_CHANGE:
            # Set off the _trackLights() method with the currently active document.
            self._trackLights(doc=c4d.documents.GetActiveDocument())
        return 0

    def _trackLights(self, doc: c4d.documents.BaseDocument) -> None:
        """Handles both finding new light objects, and tracking parameter changes on already tracked
        objects.
        """
        # The meat of this example, _getLights will traverse the scene graph and compare the results
        # to the data stored by this dialog, _lightTable. The returned list of byte objects are hashes
        # for the light objects which are new, i.e., light objects which have an UUID which has
        # been never encountered before.
        newLights: list[bytes] = self._getLights(doc)
        # This tracks if the data container checksum of any of the tracked lights is higher than its
        # cached value.
        modifiedLights: list[bytes] = self._checkLights()

        # Print out the changes.
        if len(newLights) < 1:
            print ("No lights have been added.")
        else:
            print ("The following lights have been found:")
            for key in newLights:
                light: c4d.BaseObject = self._lightTable[key]["light"]
                print(f"\tuuid: {key}, light: {light}")

        if len(modifiedLights) < 1:
            print ("No lights have been modified.")
        else:
            print ("The following lights have been modified:")
            for key in modifiedLights:
                light: c4d.BaseObject = self._lightTable[key]["light"]
                print(f"\tuuid: {key}, light: {light}")


    def _setLightData(self, key: bytes, light: c4d.GeListNode) -> int:
        """Sets the data of an entry in the internal light object tracking table.

        Returns:
            NEW_LIGHT if #key was never encountered before, UPDATED_LIGHT otherwise.
        """
        if key in self._lightTable.keys():
            self._lightTable[key]["light"] = light
            return LightTrackerDialog.UPDATED_LIGHT
        else:
            self._lightTable[key] = {"dirty": light.GetDirty(c4d.DIRTYFLAGS_DATA), "light": light}
            return LightTrackerDialog.NEW_LIGHT


    def _getLights(self, doc: c4d.documents.BaseDocument) -> list[bytes]:
        """Traverses the object tree of #doc to find all light objects in it and update the internal
        tracking table.

        Returns:
            A list of light object UUIDs which have never been encountered before.
        """
        def iterate(node: c4d.BaseObject) -> c4d.BaseObject:
            """Walks an object tree depth first and yields all nodes that are of a type which is
            contained in TRACKED_TYPES.
            """
            while isinstance(node, c4d.BaseObject):
                if node.GetType() in LightTrackerDialog.TRACKED_TYPES:
                    yield node

                for child in iterate(node.GetDown()):
                    yield child

                node = node.GetNext()

        # The list to store the newly encountered UUIDs in.
        result: list[bytes] = []

        # For all tracked light type objects in the passed document.
        for light in iterate(doc.GetFirstObject()):
            # Get the MAXON_CREATOR_ID marker to uniquely identify the object.
            uuid: memoryview = light.FindUniqueID(c4d.MAXON_CREATOR_ID)
            if not isinstance(uuid, memoryview):
                print (f"Skipping illegal non-marked light object: {light}")
                continue

            # FindUniqueID() returns a memoryview, we cast this to bytes as this more convenient
            # for us here.
            uuid: bytes = bytes(uuid)

            # Write the light object #light under #uuid into the internal tracking table. The
            # method _setLightData() will return NEW_LIGHT when #uuid has never been encountered
            # before. We know then that this must be a new object.
            if self._setLightData(uuid, light) is LightTrackerDialog.NEW_LIGHT:
                result.append(uuid)

        # Return the newly encountered object UUIDs.
        return result

    def _checkLights(self) -> list[bytes]:
        """Traverses the internal light table to search for light objects where the parameters have 
        changed.

        Returns:
            A list of light object UUIDs for which the data container has changed.
        """
        # The result and a new internal light object table. #newTable is technically not necessary,
        # as we could fashion #_lightTable and this method in such way that we could modify it in
        # place, but that would be harder to read and this is an example :)
        result: list[bytes] = []
        newTable: dict = {}

        # For each key and data dictionary in the internal table.
        for key, data in self._lightTable.items():
            # Get the old dirty count and the light object reference.
            oldDirty: int = data.get("dirty", None)
            light: c4d.BaseObject = data.get("light", None)

            # The data container was malformed, should not happen.
            if not isinstance(light, c4d.BaseObject) or not isinstance(oldDirty, int):
                raise RuntimeError(f"Found malformed light tracker data.")

            # There is a dangling object reference in the table, we step over it and by that remove
            # it.
            if not light.IsAlive():
                print (f"Found severed object pointer.")
                continue

            # Get the current DIRTYFLAGS_DATA checksum of the object and compare it to the cached
            # value. When it is different, we know the object parameters must have changed.
            newDirty: int = light.GetDirty(c4d.DIRTYFLAGS_DATA)
            if newDirty != oldDirty:
                result.append(key)

            # Write the light object and its new dirty checksum into the new table.
            newTable[key] = {"dirty": newDirty, "light": light}

        # Replace the old table with the new one, and by that drop dangling objects and update all
        # dirty checksums to their current state.
        self._lightTable = newTable

        # Return the UUIDs of the light objects where parameter changes occurred.
        return result



if __name__ == '__main__':
    # This global variable is a hack to keep async dialogs alive in a Script Manager script, please do
    # not use it in a production environment. It is only being used here to demonstrate the issue.
    # You must implement a plugin, e.g., a CommandData plugin, in a production environment to safely
    # handle async dialogs.
    global dlg
    dlg = LightTrackerDialog()
    dlg.Open(c4d.DLG_TYPE_ASYNC)
```

@ferdinand So much thanks for this extremely detailed explain !It is very friendly for beginner to start .

@gr4ph0s 's plugin seems doesn't work in S26 and Redshift 3.5. And sadly it's no comment and it's too complicate to me now:( .I tried to read it as a reference before . In fact ,Your such a detailed code also has some lines I have to google a lot to understand😧 (due to my bad self python learning and bad English,It's a very good example.)

Technical Explanation part is very very excelent to understand. Is there somewhere else I can find this basic underhood work principle? Or a relationship between c4d sdk moudles works explain. it almost the biggest problem to get bit deep scripting😊

Back to this code, it work well in my test, but as I said, I am a bad pythoner and bad Englisher , and It's takes habbit time to work with scripting, it might take times . Could I post deeper problems in this page?

Thanks again for your detailed explain .

Hey 🙂

Is there somewhere else I can find this basic underhood work principle?

Unfortunately, not. Which is why I created this little write up.

Could I post deeper problems in this page?

Sure, that is why we are here 🙂 The only thing I would ask you to do is to follow our Forum Guidelines and open a new topic when your follow-up questions stray too far from the original topic.