Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware 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
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Print Custom User data to text doc

    Cinema 4D SDK
    python
    2
    6
    874
    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.
    • A
      apetownart
      last edited by ferdinand

      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()
      

      Screenshot 2023-11-14 135921.png

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

        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 ID ID_USERDATA (alias for 700) and of type DTYPE_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,
        Ferdinand

        Input:
        08456366-8d3d-4d86-9501-bf7809cacd65-image.png
        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()
        
        

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • A
          apetownart
          last edited by

          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.

          1 Reply Last reply Reply Quote 0
          • A
            apetownart
            last edited by

            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

            1 Reply Last reply Reply Quote 0
            • A
              apetownart
              last edited by 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__
              
              ferdinandF 1 Reply Last reply Reply Quote 0
              • ferdinandF
                ferdinand @apetownart
                last edited by

                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

                MAXON SDK Specialist
                developers.maxon.net

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