Monitoring object parameter changes
-
Hello,
I can see this post:
https://developers.maxon.net/forum/topic/15621/python-api-document-eventsI can understand the concept and it can be applied to my case too, but this was created for python programming.
My question is: is the situation the same with the C++ API or maybe we have better tools to do it?Thanks,
Márton -
Hello @Márton,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
- Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
- Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
- Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
About your First Question
Thank you for reaching out to us. It is the same in C++, this is a principal flaw/design choice in the API.
Cheers,
Ferdinand -
Ok, and thank you for the quick answer!
Márton
-
Hello again,
I was not sure if I should start a new thread or continue this discussion....
This is related to the first question but could have its own thread too.I am confused about guids provided by c4d.
I know there is a lot of info already about these unique identifiers on this forum, and as I have understood from those GetGUID() should be good enough for me.But my test is very simple, and still I get the same guids for different objects many times.
I use my own visitor object derived from maxon:: Hierarchy.
I create a Figure object (menu > Create > Mesh > Figure)
Start visitor -> and many different body parts of the Figure has the exact same guid.
Is it normal? I am not sure what is the best way to uniquely identify these....Thanks,
Márton -
Hello @Márton,
Thank you for reaching out to us. No, I would say we are fine here in this thread since the subject is literally 'Monitoring object parameter changes'.
The primary issue is that you are using the wrong method. You should be using
BaseObject::GetUniqueIP
but there is also a gotcha as it is not as unique as one might think it is. Because it is only unique within a cache. And since caches can contain caches, this can then lead to ambiguity.I would also question a bit as how sensible it is that you poke around that deeply in caches. Even for render engines which must do scene complex synchronization, I find this a bit questionable. I understand the desire for doing this for performance reasons. But caches are not meant to be updated/scanned partially, when an object is dirty and rebuilt its cache, you should sync the full cache. I would always try to stay on the
GeMarker
/MAXON_CREATOR_ID
level when possible on pull over whole new caches when a generator went dirty. Partially updating caches, i.e., you try to figure out what in a cache changed and only update that, strikes me as very error prone as this relies on the assumption that the plugin/object implementing that cache is doing nothing hacky itself for performance reasons (and there is no such gurantee). When carefully designed, one can problably write something that chery picks only the 'relevant' stuff, but I would consider this a workaround or hack.I would also recommend having a look at How to detect a new light and pram change? for a practical example of how to sync a scene graph (and I deliberately did not go into the cache level there). The example is in Python but translates relatively directly to C++. As metioned in my code example below, in C++ one would usually use
BaseList2D::GetGeMarker
instead ofMAXON_REATOR_ID
as the former is the data the latter is based on (but markers are inaccessible in Python). Finally, here is some older thread about the subject of IDs.Find below a brief example in Python. Again, I am aware that you are on C++. Please come back when you need help with translating any example code to C++.
Cheers,
FerdinandResult
Output in Cinema 4D for a Figure object, running the script a second time after changing the Segments paramater.
The diff. The GUID and marker ID obviously changed. But in the case of the Figure object it seems also to be the case that its hierarchy is not stable, something in the "Right Joint" hierarchy jumped. This also highlights why I would avoid tracking caches when I can, as they are meant to be dynamic and each object implementation can pull off countless hacky things with its cache. I would strongly recommend tracking the dirtiness of an object and operate on the generator level as shown in How to detect a new light and pram change?.
Code
"""Stores the different identifiers of the objects in the cache of the selected object and compares them to a previous state saved to disk. To run this script, first save it to a file, e.g., `inspect_cache.py`, as it infers other file paths from its own location. Then select an object in the Object Manager and execute the script. The script will print the current cache of the selected object to the console and save it to disk. Now invoke a cache rebuild on the object by for example changing a parameter. Run the script again and it will print the new cache and compare it to the previous one. If the caches differ, a HTML diff will be created and opened in the default web browser. """ import os import difflib import webbrowser 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`. DIFF_CHANGES: bool = True # If `True`, the script will open an HTML dif for the changes. PATH_PREV: str = os.path.join(os.path.dirname(__file__), "id_prev.txt") # Path to the previous cache. PATH_DIFF: str = os.path.join(os.path.dirname(__file__), "id_diff.html") # Path to the diff file. def main() -> None: """Called by Cinema 4D when the script is being executed. """ if op is None: raise ValueError("No object selected.") # Iterate over the cache of the selected object #op. result: str = "" for node, _, depth, *_ in mxutils.RecurseGraph(op, yieldCaches=True, yieldHierarchy=True, complexYield=True): if not isinstance(node, c4d.BaseObject): continue # Should not happen since we do not step into branches. # The padded name of the object, e.g., " + Cube ". head: str = f"{' ' * depth} + {node.GetName()}".ljust(40) # This identifies an object uniquely in a cache and is persistent over cache rebuilding. # A cache hierarchy [a, b, c] will always return the same unique IPs [s, t, u], even when # the user changed a parameter resulting in the same hierarchy (but possibly otherwise # different data). But there is a gotcha: These IDs are only unique within the same cache. # And since caches can contain caches, you can end up with this: # # generator # a - First cache element of #generator, UIP: 1 # b - First cache element of #a, UIP: 1 # c - Second cache element of #a, UIP: 2 # b - Second cache element of #generator, UIP: 2 # a - First cache element of #b, UIP: 1 # c - Second cache element of #b, UIP: 2 # c - Third cache element of #generator, UIP: 3 # # There is no builtin way to safely decompose caches in that detail safely, you must write # something yourself (you could for example hash everything up to the generator and call # that the unique ID of the object). # uip: str = str(node.GetUniqueIP()).ljust(30) # I have quite frankly no idea for what this ID is good for, I never used it. The docs # also strike me as not quite correct, since they claim that this falls back to #uip, but in # some cases, e.g., the Figure object, this does not seem to hold true, as this ID changes # over cache rebuilds. This is also not persistent over Cinema 4D sessions (load/save). guid: str = str(node.GetGUID()).ljust(30) # This is the ID assigned to an object when it is being created. It wil change for cache # rebuilds but is persistent over different Cinema 4D sessions (load/save). Since caches # are being rebuilt (the whole point of generators) it is useless for identifying objects # in a cache but for everything else it is the ID of choice. In C++ you can use GeMarker # and BaseList2D::GetMarker() to directly access the data the MAXON_CREATOR_ID is based on. uid: str = str(bytes(node.FindUniqueID(c4d.MAXON_CREATOR_ID))) result += f"{head} uip = {uip} guid = {guid} uid = {uid}\n" # Print our little tree to the console and check for changes. print(f"Current hierarchy of {op}:") print(result) if os.path.exists(PATH_PREV): cache: str = "" with open(PATH_PREV, "r") as f: cache: str = f.read() print("-" * 80) print("Previous hierarchy:") print(cache) print("-" * 80) print("Cache is matching current hierarchy:", cache == result) # Build the HTML diff when asked for and the cache has changed. Open it in the browser. if cache != result and DIFF_CHANGES: diff: str = difflib.HtmlDiff().make_file( cache.splitlines(), result.splitlines(), fromdesc="Previous cache", todesc="Current hierarchy") with open(PATH_DIFF, "w") as f: f.write(diff) webbrowser.open(PATH_DIFF) # Write the current hierarchy as the new previous state to disk. with open(PATH_PREV, "w") as f: f.write(result) print("-" * 80) print(f"Cache written to {PATH_PREV}.") if __name__ == '__main__': main()
-
Thank you for your detailed answer!
So if I understand now correctly, these are the main points:
- use GetGeMarker -> this is the most reliable identifier in this case
- do not monitor cached objects because it is not easy to follow what is happening with those and can cause tricky situations.
Is it right?
Márton
-
Hey @Márton,
It depends a bit on what you are doing concretely. There could be a case where you want to stream hyper-complex scene data between the scene representations of two apps: E.g., particles or maybe some super heavy CAD or sculpting data. And you want your app to be the new standard for "man, this is snappy" where you might have to roll up your sleeves and optimize what you copy over from the Cinema 4D graph to yours and vice versa.
But generally I would avoid assuming that caches always have the same layout or having to identify things in a cache (at the cost of a bit of performance) and treat caches as monolithic things instead. And yes, then
GeMarker
/MAXON_CREATOR_ID
are by far the best option.GeMarker
is a hash of the mac address of the machine, the timestamp of creation, and a special secret sauce. Markers can be copied on copy events with C4DAtom::CopyTo whenPRIVATE_IDENTMARKER
is passed as a flag to maintain the identity of nodes (which will be done by Cinema 4D when it 'reallocates' nodes, shoves them around in the backend).Effectively this means that
GeMarker
/MAXON_CREATOR_ID
is persistent over node reallocation, scene loading/unloading, and scene duplication events (a node will have the same marker in the document the user is using for editing as in the cloned version of that document used in a rendering running in the background while the user is editing). Or spoken very plainly: They will (almost) never change.Cheers,
Ferdinand -
Thanks you!