Print Custom User data to text doc
-
I am trying to write a python script that dumps all the custom user data of a given object per frame in the timeline and outputs that data to text file. right now if any data is in a "Group" it refuses to work, the script cant see the custom user data. Python not being my main coding library I'm not sure what I'm missing. its not actually failing so there are no breakpoints to check from my understanding. any help would be really cool.
import c4d from c4d import documents import os def fetch_user_data_recursive(obj, id_base=c4d.DescID(), frame_number=None): """Recursive function to fetch all types of user data from an object, including those in groups.""" userdata_container = obj.GetUserDataContainer() collected_data = [] for id, bc in userdata_container: # Construct the current ID based on the parent ID and the current ID current_id = c4d.DescID(id_base[0], id[0]) if id_base else id return collected_data def main(): # Get the active document and object doc = documents.GetActiveDocument() obj = doc.GetActiveObject() # Ensure that an object is selected if not obj: print("Nothing selected!") return # Get the active render data and fetch the frame range render_data = doc.GetActiveRenderData() start_frame = render_data[c4d.RDATA_FRAMEFROM].GetFrame(doc.GetFps()) end_frame = render_data[c4d.RDATA_FRAMETO].GetFrame(doc.GetFps()) # Prepare a list to collect the user data strings output_data = [] # Iterate over every frame and fetch user data for frame in range(start_frame, end_frame + 1): doc.SetTime(c4d.BaseTime(frame, doc.GetFps())) doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) output_data.extend(fetch_user_data_recursive(obj, frame_number=frame)) # Check if there's any user data collected if not output_data: print("Object has no user data.") return # Print data to the console for data in output_data: print(data) # Save the user data to a .txt file on the desktop desktop_path = os.path.expanduser("~/Desktop") file_path = os.path.join(desktop_path, "c4d_userdata_output.txt") with open(file_path, 'w') as f: f.write("\n".join(output_data)) print(f"User data saved to: {file_path}") c4d.EventAdd() # Execute main function if __name__ == '__main__': main()
-
Hello @apetownart,
Welcome to the Plugin Café forum and the Cinema 4D development 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 Guidelines, as they line out details about the Maxon SDK Group support procedures. Of special importance are:
- Support Procedures: Scope of Support: Lines out the things we will do and what we will not do.
- Support Procedures: Confidential Data: Most questions should be accompanied by code but code cannot always be shared publicly. This section explains how to share code confidentially with Maxon.
- Forum Structure and Features: Lines out how the forum works.
- Structure of a Question: Lines out how to ask a good technical question. It is not mandatory to follow this exactly, but you should follow the idea of keeping things short and mentioning your primary question in a clear manner.
About your First Question
You are only looking at the first
DescLevel
of an ID when iterating over the user data container. That level will always be of IDID_USERDATA
(alias for700
) and of typeDTYPE_SUBCONTAINER
. Only the second level in that ID than holds the ID and data type of that parameter. You can read more about the concept of parameter identifiers here.Your script therefore is missing a bit of the point, I am also not sure where the recursive bit comes into play here. You won't need any recursion to iterate over the description of a node, Cinema 4D will 'flatten' things for you on its own, you do not have to get things inside a group manually.
The rest of your script looks okay, you should however know that executing the passes (
BaseDocument::ExecutePasses
) is quite an expensive thing to do. Unless you explicitly expect your animated parameters to also be driven by expressions, e.g., Xpresso, in addition to the animation, evaluating the tracks, curves, and keys of a node will be much cheaper.I have provided a brief example at the end of my answer.
Cheers,
FerdinandInput:
Output:Name: A, ID: ((700, 5, 0), (2, 19, 0)), Type: 19, Value: 0.0 Name: B, ID: ((700, 5, 0), (4, 19, 0)), Type: 19, Value: 0.0 Name: C, ID: ((700, 5, 0), (6, 19, 0)), Type: 19, Value: 0.0 Name: D, ID: ((700, 5, 0), (7, 19, 0)), Type: 19, Value: 0.0 Name: E, ID: ((700, 5, 0), (8, 19, 0)), Type: 19, Value: 0.0 -------------------------------------------------------------------------------- Name: Icon File / ID, ID: (1041668, 7, 110050), Type: 7, Value: , User-Data: False Name: Icon Color, ID: (1041670, 15, 110050), Type: 15, Value: 0, User-Data: False Name: Color, ID: (1041671, 3, 110050), Type: 3, Value: Vector(1, 0.9, 0.4), User-Data: False Stepping over type which is inaccessible in Python. Name: Name, ID: (900, 130, 110050), Type: 130, Value: Null, User-Data: False Name: Layer, ID: (898, 133, 110050), Type: 133, Value: None, User-Data: False Stepping over type which is inaccessible in Python. Name: Viewport Visibility, ID: (901, 15, 5155), Type: 15, Value: 2, User-Data: False Name: Renderer Visibility, ID: (902, 15, 5155), Type: 15, Value: 2, User-Data: False Name: Enabled, ID: (906, 400006001, 5155), Type: 400006001, Value: 1, User-Data: False Name: Display Color, ID: (907, 15, 5155), Type: 15, Value: 0, User-Data: False Name: Color, ID: (908, 3, 5155), Type: 3, Value: Vector(1, 1, 1), User-Data: False Name: X-Ray, ID: (909, 400006001, 5155), Type: 400006001, Value: 0, User-Data: False Name: Position, ID: (903, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Rotation, ID: (904, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Scale, ID: (905, 23, 5155), Type: 23, Value: Vector(1, 1, 1), User-Data: False Name: Rotation Order, ID: (928, 15, 5155), Type: 15, Value: 6, User-Data: False Name: Quaternion Rotation, ID: (929, 400006001, 5155), Type: 400006001, Value: 0, User-Data: False Name: Frozen Position, ID: (917, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Frozen Rotation, ID: (918, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Frozen Scale, ID: (919, 23, 5155), Type: 23, Value: Vector(1, 1, 1), User-Data: False Name: Deform Position, ID: (931, 23, 5155), Type: 23, Value: None, User-Data: False Name: Deform Rotation, ID: (932, 23, 5155), Type: 23, Value: None, User-Data: False Name: Deform Scale, ID: (933, 23, 5155), Type: 23, Value: None, User-Data: False Name: Global Position, ID: (910, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Global Rotation, ID: (911, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Transformed Position, ID: (925, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Transformed Scale, ID: (927, 23, 5155), Type: 23, Value: Vector(1, 1, 1), User-Data: False Name: Transformed Rotation, ID: (926, 23, 5155), Type: 23, Value: Vector(0, 0, 0), User-Data: False Name: Shape, ID: (1000, 15, 5140), Type: 15, Value: 0, User-Data: False Name: Radius, ID: (1001, 19, 5140), Type: 19, Value: 10.0, User-Data: False Name: Aspect Ratio, ID: (1002, 19, 5140), Type: 19, Value: 1.0, User-Data: False Name: Orientation, ID: (1003, 15, 5140), Type: 15, Value: 0, User-Data: False Name: A, ID: ((700, 5, 0), (2, 19, 0)), Type: 19, Value: 0.0, User-Data: True Name: B, ID: ((700, 5, 0), (4, 19, 0)), Type: 19, Value: 0.0, User-Data: True Name: C, ID: ((700, 5, 0), (6, 19, 0)), Type: 19, Value: 0.0, User-Data: True Name: D, ID: ((700, 5, 0), (7, 19, 0)), Type: 19, Value: 0.0, User-Data: True Name: E, ID: ((700, 5, 0), (8, 19, 0)), Type: 19, Value: 0.0, User-Data: True
Code:
import c4d doc: c4d.documents.BaseDocument # The active document. op: c4d.BaseObject # The active object, can be `None`. # A few data types we want to ignore when we look for parameters which can be (directly) accessed, # i.e., for which we can get 'a value'. IGNORE_DIRECT_ACCESS_TYPES: tuple[int] = (c4d.DTYPE_BUTTON, c4d.DTYPE_CHILDREN, c4d.DTYPE_DYNAMIC, c4d.DTYPE_GROUP, c4d.DTYPE_SEPARATOR, c4d.DTYPE_STATICTEXT, c4d.DTYPE_SUBCONTAINER) def main() -> None: """ """ if not op: raise RuntimeError("Please select an object.") pid: c4d.DescID # The parameter ID of a user data parameter. bc: c4d.BaseContainer # The description container for the parameter. # Iterate over the user data container. for pid, bc in op.GetUserDataContainer(): # User data DescID will always take the form (ID_USERDATA, N), i.e., the first DescLevel # will be the user data container address. if pid.GetDepth() < 2 or pid[0].id != c4d.ID_USERDATA: raise RuntimeError("This should never happen.") # The second DescLevel describes the actual data type which is stored at #N. A desc level # is composed of (ID, DATATYPE, CREATOR) information. eid: int = pid[1].id # e.g., 2 dtype: int = pid[1].dtype # e.g. DTYPE_LONG # Step over some data types like groups, buttons, and separators which do not hold 'a value'. if dtype in IGNORE_DIRECT_ACCESS_TYPES: continue # Parameter access can fail in Python because not all data types are wrapped in Python. try: value: any = op[pid] except: print ("Stepping over type which is inaccessible in Python.") continue # Print the value. print(f"Name: {bc[c4d.DESC_NAME]}, ID: {pid}, Type: {dtype}, Value: {value}") print ("\n" + "-" * 80 + "\n") # We could also do this on a more abstract level using C4DAtom.GetDescription, as it will return # the full parameter container, and not only the user data part. for bc, pid, _ in op.GetDescription(c4d.DESCFLAGS_DESC_NONE): # Other than when iterating over the user data sub container alone, we will encounter here # things which are not user data and we therefore have then to look into their first level # for their data type. isUserData: bool = pid[0].id == c4d.ID_USERDATA dtype: int = pid[1].dtype if (isUserData and pid.GetDepth() > 1) else pid[0].dtype if dtype in IGNORE_DIRECT_ACCESS_TYPES: continue try: value: any = op[pid] except: print ("Stepping over type which is inaccessible in Python.") continue print(f"Name: {bc[c4d.DESC_NAME]}, ID: {pid}, Type: {dtype}, Value: {value}, " f"User-Data: {isUserData}") if __name__ == '__main__': main()
-
thank you so much this is super helpful, and I will for sure dig into the docs on the website further. I appreciated you taking the time to explain the logic tome it makes it much more clear.
-
you sir are my hero, with your explanation I was able to get the relevant data i needed per frame and was able to spit out a file that tracked those changes. thank you again @ferdinand
-
For anyone that wants it this si the completed script i used for my needs. so that i could pass the look dev information changes per frame onto the rest of my team.::
import c4d import sys import os # Your predefined constants and variables doc: c4d.documents.BaseDocument # The active document. op: c4d.BaseObject # The active object, can be `None`. IGNORE_DIRECT_ACCESS_TYPES: tuple[int] = (c4d.DTYPE_BUTTON, c4d.DTYPE_CHILDREN, c4d.DTYPE_DYNAMIC, c4d.DTYPE_GROUP, c4d.DTYPE_SEPARATOR, c4d.DTYPE_STATICTEXT, c4d.DTYPE_SUBCONTAINER) def main() -> None: """ Main function to iterate over each frame and print object data. """ if not op: raise RuntimeError("Please select an object.") # Get the start and end frames of the document's active timeline start_frame = doc.GetLoopMinTime().GetFrame(doc.GetFps()) end_frame = doc.GetLoopMaxTime().GetFrame(doc.GetFps()) # Iterate through each frame in the range for frame in range(start_frame, end_frame + 1): # Set the document's time to the current frame doc.SetTime(c4d.BaseTime(frame, doc.GetFps())) # Update the scene to reflect changes at the current frame c4d.DrawViews(c4d.DRAWFLAGS_FORCEFULLREDRAW) c4d.EventAdd() # Print frame information print(f"Frame: {frame}") # Iterate over the user data container for pid, bc in op.GetUserDataContainer(): if pid.GetDepth() < 2 or pid[0].id != c4d.ID_USERDATA: raise RuntimeError("This should never happen.") eid: int = pid[1].id dtype: int = pid[1].dtype if dtype in IGNORE_DIRECT_ACCESS_TYPES: continue try: value: any = op[pid] except: print ("Stepping over type which is inaccessible in Python.") continue # Print the value for each user data parameter print(f"Name: {bc[c4d.DESC_NAME]}, Value: {value}") print("-" * 80) if __name__ == '__main__': # Change the standard output to a file on the desktop desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop') output_file_path = os.path.join(desktop_path, 'c4d_output.txt') with open(output_file_path, 'w') as file: sys.stdout = file main() sys.stdout = sys.__stdout__
-
Hey @apetownart,
Thank you for sharing your solution, much appreciated! I have fenced in your code with a code block so that it is a bit more readable and has a copy button. Find out how to do that yourself in our Forum Markup documenation.
Cheers,
Ferdinand