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,
FerdinandTechnical 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 theCoreMessage
methods of aMessageData
plugin, aGeDialog
, or aGeUserArea
.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 nextEVMSG_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 listmyLookup: 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 putLight_0
intomyLookup
before. The reason is then that Cinema 4D has reallocatedLight_0
in the background at some point. The reference inmyLookup
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 existingLight_0
. The data of both objects can still be entirely the same, it is only that in C++ you cannot rely onBaseList2D
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 withC4DAtom.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 identifierc4d.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 ofLight_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 identifierc4d.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.Cheers,
FerdinandCode:
"""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.
-
@Dunhou
I fork that redshift light manager last time.
https://github.com/bentraje/c4d_redshift_light_listerI just change the minimum for it to work on the latest C4D versions.
It works on C4D 2023, last time I check, and on RS 3.5, but not all parameters are fully supported since I didn't refactor that much. -
@bentraje Thanks a lot ! I always want to spend some thing to do this , but unfortunately I am struggling to work with rendering .
If I have some time hopes I can change this a bit more
-