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

    Capturing and Restoring Take Overrides in Cinema 4D Using JSON

    Cinema 4D SDK
    python 2023
    2
    2
    219
    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.
    • F
      Futurium
      last edited by

      Hi,
      I'm working on a code to copy take overrides from one scene to another. One script captures details from each take in my reference scene and saves them as a JSON file. Another script loads the JSON and compares all takes to identify differences in names, hierarchy, applied materials, transforms, visibility, etc.

      I want the script to both print the differences between the captured JSON data and the current scene and automatically apply the take-specific changes. I assume the take hierarchy, scene objects, and materials are identical (which I verify elsewhere).

      I can capture many details but struggle to compare takes with the JSON data and restore them correctly. I also suspect my code is unnecessarily long and complex for such a simple task. Could you review both my capture and restore scripts and help me get them working efficiently?

      I've attached my test scenes and scripts.
      Thank you in advance.

      Best regards,
      Tomasz

      #GrabReferenceSnapshot.py
      import c4d
      import collections
      import maxon
      from c4d import utils
      import json
      import os
      
      def create_snapshot(doc, filepath):
          def return_full_path_per_object(op, parent_mesh_object_name=""):
              if not op:
                  return ""
              
              path_parts = []
              current = op
              
              # Build path from current object up to root
              while current:
                  name = current.GetName()
                  if name == parent_mesh_object_name:
                      break
                  path_parts.insert(0, name)
                  current = current.GetUp()
              
              # Join path parts with forward slashes
              return "/".join(path_parts)
          
          def serialize_descid(desc_id):
              """Converts a c4d.DescID to a serializable format."""
              return [desc_id[i].id for i in range(desc_id.GetDepth())]
      
          try:
              # Ensure the main take is active
              take_data = doc.GetTakeData()
              if take_data is None:
                  raise RuntimeError("No take data in the document.")
              main_take = take_data.GetMainTake()
              take_data.SetCurrentTake(main_take)
              
              # Initialize the snapshot dictionary
              snapshot = {
                  'takes': {}
              }
              
              # Get takes information
              take_data = doc.GetTakeData()
              if take_data:
                  main_take = take_data.GetMainTake()
                  
                  def process_take_changes(take):
                      """Gets the overrides for a specific take."""
                      if take == main_take:
                          return {"name": take.GetName(), "overrides": []}  # Skip overrides for main take
                      
                      take_data.SetCurrentTake(take)
                      doc = c4d.documents.GetActiveDocument()
                      doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0)
                      
                      take_changes = {
                          "name": take.GetName(),
                          "overrides": []
                      }
                      
                      # Get only the overrides for this take
                      override_data = take.GetOverrides()
                      if override_data:
                          for override in override_data:
                              override_obj = override.GetSceneNode()
                              if not override_obj:
                                  continue
                                  
                              # Log the object being processed
                              print(f"Processing override for object: {override_obj.GetName()}")
                              
                              # Use the material name as a unique identifier
                              material_name = override_obj.GetName() if isinstance(override_obj, c4d.BaseMaterial) else None
                              
                              overridden_params = override.GetAllOverrideDescID()
                              for desc_id in overridden_params:
                                  description = override_obj.GetDescription(c4d.DESCFLAGS_DESC_NONE)
                                  if description:
                                      parameter_container = description.GetParameter(desc_id, None)
                                      if parameter_container:
                                          param_name = parameter_container[c4d.DESC_NAME]
                                          value = override.GetParameter(desc_id, c4d.DESCFLAGS_GET_0)
                                          
                                          # Use return_full_path_per_object to get the object path
                                          print (f"in take{take.GetName()} checking object path for {override_obj.GetName()}")
                                          object_path = return_full_path_per_object(override_obj, "")
                                          if "/" in object_path:
                                              print (f"object_path: {object_path}")
                                          
                                          take_changes["overrides"].append({
                                              "object_name": override_obj.GetName(),
                                              "object_path": object_path,
                                              "parameter": param_name,
                                              "parameter_id": serialize_descid(desc_id),
                                              "value": str(value) if not isinstance(value, (int, float, bool, str)) else value,
                                              "material_name": material_name  # Add material name for comparison
                                          })
                                          
                                          print(f"Captured override: {param_name} = {value} for object {object_path}")
                      
                      return take_changes
      
                  def process_take(take):
                      if not take:
                          return
                      
                      # Store only serializable data
                      take_info = {
                          'name': take.GetName(),
                          'parent': take.GetUp().GetName() if take.GetUp() else None,
                          'changes': process_take_changes(take)  # Capture take changes
                      }
                      
                      # Store in snapshot using take name as key
                      snapshot['takes'][take.GetName()] = take_info
                      
                      # Process child takes
                      child = take.GetDown()
                      while child:
                          process_take(child)
                          child = child.GetNext()
                  
                  process_take(main_take)
              
              # Save the snapshot to JSON file
              try:
                  # Create all necessary directories
                  os.makedirs(os.path.dirname(filepath), exist_ok=True)
                  
                  # Save the file
                  with open(filepath, 'w', encoding='utf-8') as f:
                      json.dump(snapshot, f, indent=4)
                      
                  return True
                  
              except Exception as e:
                  print(f"Error saving snapshot file: {str(e)}")
                  return False
              
          except Exception as e:
              print(f"Error creating snapshot: {str(e)}")
              return False
      
      def main():
          doc = c4d.documents.GetActiveDocument()
          
          # Get both the document name and path
          doc_name = doc.GetDocumentName()
          doc_path = doc.GetDocumentPath()
          
          full_path = os.path.join(doc_path, doc_name)
          print("full_path: " + str(full_path))
          
          project_folder = os.path.dirname(full_path)
          print("project_folder : " + str(project_folder))
          
          project_name = os.path.basename(project_folder)
          print("project_name : " + str(project_name))
      
          # Construct final path
          final_path = os.path.join(project_folder, "snapshots", project_name + "_final.json")
          
          print("\nPath verification:")
          print(f"Final snapshot path: {final_path}")
          
          print("\nAttempting to create final snapshot...")
          try:
              result = create_snapshot(doc, final_path)
              print(f"create_snapshot call completed. Result: {result}")
          except Exception as e:
              print(f"Error in create_snapshot: {str(e)}")
              import traceback
              print(f"Traceback:\n{traceback.format_exc()}")
      
      if __name__ == "__main__":
          c4d.CallCommand(13957) # Clear Console
          main()
      
      #RestoreSnapshot
      import c4d
      import collections
      import maxon
      from c4d import utils
      import os
      import c4d
      import json
      
      def return_full_path_per_object(op, parent_mesh_object_name=""):
          if not op:
              return ""
          
          path_parts = []
          current = op
          
          # Build path from current object up to root
          while current:
              name = current.GetName()
              if name == parent_mesh_object_name:
                  break
              path_parts.insert(0, name)
              current = current.GetUp()
          
          # Join path parts with forward slashes
          return "/".join(path_parts)
      
      def serialize_descid(desc_id):
          """Converts a c4d.DescID to a serializable format."""
          return [desc_id[i].id for i in range(desc_id.GetDepth())]
      
      def check_takes(doc, snapshot):
          
          try:
              print("\nPerforming takes check...")
              
              take_data = doc.GetTakeData()
              if take_data is None:
                  raise RuntimeError("No take data in the document.")
                  
              main_take = take_data.GetMainTake()
              snapshot_takes = snapshot.get('takes', {})
              
              def get_take_overrides(take):
                  """Gets the overrides for a specific take."""
                  if take == main_take:
                      return []  # Skip overrides for main take
                  
                  # Activate the take before checking its overrides
                  take_data.SetCurrentTake(take)
                  doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0)
                  
                  override_data = take.GetOverrides()
                  overrides = []
                  
                  if override_data:
                      for override in override_data:
                          override_obj = override.GetSceneNode()
                          if not override_obj:
                              continue
                              
                          overridden_params = override.GetAllOverrideDescID()
                          for desc_id in overridden_params:
                              description = override_obj.GetDescription(c4d.DESCFLAGS_DESC_NONE)
                              if description:
                                  parameter_container = description.GetParameter(desc_id, None)
                                  if parameter_container:
                                      param_name = parameter_container[c4d.DESC_NAME]
                                      value = override.GetParameter(desc_id, c4d.DESCFLAGS_GET_0)
                                      
                                      # Get object path using the helper function
                                      object_path = return_full_path_per_object(override_obj)
                                      
                                      # Use the material name or another unique identifier
                                      material_name = override_obj.GetName() if isinstance(override_obj, c4d.BaseMaterial) else None
                                      
                                      overrides.append({
                                          "object_name": override_obj.GetName(),
                                          "object_path": object_path,
                                          "parameter": param_name,
                                          "parameter_id": str(desc_id),  # Convert DescID to string for comparison
                                          "value": str(value) if not isinstance(value, (int, float, bool, str)) else value,
                                          "material_name": material_name  # Add material name for comparison
                                      })
                  
                  return overrides
              
              def compare_takes(current_take, snapshot_take_data):
                  # Debugging: Print snapshot_take_data to verify its structure
                  print(f"Snapshot data for take '{current_take.GetName()}': {snapshot_take_data}")
      
                  # Ensure 'overrides' key exists in snapshot_take_data
                  if 'overrides' not in snapshot_take_data:
                      print(f"Error: 'overrides' key not found in snapshot data for take '{current_take.GetName()}'")
                      return False
      
                  # Initialize current_overrides with the current scene's override data
                  current_overrides = []  # Ensure this is defined before use
      
                  # Retrieve the current overrides for the take
                  override_data = current_take.GetOverrides()
                  if override_data:
                      for override in override_data:
                          override_obj = override.GetSceneNode()
                          if not override_obj:
                              continue
      
                          # Use the material name as a unique identifier
                          material_name = override_obj.GetName() if isinstance(override_obj, c4d.BaseMaterial) else None
      
                          overridden_params = override.GetAllOverrideDescID()
                          for desc_id in overridden_params:
                              description = override_obj.GetDescription(c4d.DESCFLAGS_DESC_NONE)
                              if description:
                                  parameter_container = description.GetParameter(desc_id, None)
                                  if parameter_container:
                                      param_name = parameter_container[c4d.DESC_NAME]
                                      value = override.GetParameter(desc_id, c4d.DESCFLAGS_GET_0)
      
                                      # Use return_full_path_per_object to get the object path
                                      object_path = return_full_path_per_object(override_obj, "")
      
                                      current_overrides.append({
                                          "object_name": override_obj.GetName(),
                                          "object_path": object_path,
                                          "parameter": param_name,
                                          "parameter_id": serialize_descid(desc_id),
                                          "value": str(value) if not isinstance(value, (int, float, bool, str)) else value,
                                          "material_name": material_name  # Add material name for comparison
                                      })
      
                  # Now compare current_overrides with snapshot_take_data['overrides']
                  snapshot_overrides = snapshot_take_data['overrides']
      
                  for curr_override, snap_override in zip(current_overrides, snapshot_overrides):
                      # Compare using material names if available
                      if curr_override.get('material_name') != snap_override.get('material_name'):
                          print(f"  Material name mismatch in take '{current_take.GetName()}':")
                          print(f"  Expected: {snap_override.get('material_name')}")
                          print(f"  Found: {curr_override.get('material_name')}")
                          return False
      
                      # Compare other attributes
                      if (curr_override['object_path'] != snap_override['object_path'] or
                          curr_override['parameter'] != snap_override['parameter'] or
                          curr_override['value'] != snap_override['value']):
                          print(f"  Override mismatch in take '{current_take.GetName()}':")
                          print(f"  Object: {curr_override['object_path']}")
                          print(f"  Parameter: {curr_override['parameter']}")
                          print(f"  Expected: {snap_override['value']}")
                          print(f"  Found: {curr_override['value']}")
                          return False
      
                  return True
              
              # Start comparison with main take
              all_match = True
              
              def process_take(take):
                  nonlocal all_match
                  if not take:
                      return
                  
                  take_name = take.GetName()
                  if take_name not in snapshot_takes:
                      print(f"\nTake not found in snapshot: {take_name}")
                      all_match = False
                      return
                  
                  # Compare current take with snapshot
                  if not compare_takes(take, snapshot_takes[take_name]):
                      all_match = False
                  
                  # Process child takes
                  child = take.GetDown()
                  while child:
                      process_take(child)
                      child = child.GetNext()
              
              try:
                  # Start with main take
                  process_take(main_take)
                  
                  # Restore main take at the end
                  take_data.SetCurrentTake(main_take)
                  doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0)
                  
                  if all_match:
                      print("\nAll takes match.")
                  else:
                      print("\nTakes check failed.")
                  
                  return all_match
                  
              except Exception as e:
                  print(f"Error during take comparison: {str(e)}")
                  # Ensure main take is restored even if an error occurs
                  take_data.SetCurrentTake(main_take)
                  doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0)
                  return False
              
          except Exception as e:
              print(f"Error checking takes: {str(e)}")
              return False
      
      def restore_snapshot(doc, snapshot_path, loose_comparison=False):
      
          try:
              print("Loading snapshot file...")
              # Load the snapshot file
              with open(snapshot_path, 'r', encoding='utf-8') as f:
                  snapshot = json.load(f)
              
              # Ensure the main take is active
              take_data = doc.GetTakeData()
              if take_data is None:
                  raise RuntimeError("No take data in the document.")
              main_take = take_data.GetMainTake()
              take_data.SetCurrentTake(main_take)
                      
              # Check takes
              if not check_takes(doc, snapshot):
                  return False
              
              print("Snapshot restoration check completed successfully.")
              return True
              
          except Exception as e:
              print(f"Error checking snapshot: {str(e)}")
              return False
      
      
      def main():
          doc = c4d.documents.GetActiveDocument()
          
          # Get both the document name and path
          doc_name = doc.GetDocumentName()
          doc_path = doc.GetDocumentPath()
          
          full_path = os.path.join(doc_path, doc_name)
          print("full_path: " + str(full_path))
          
          project_folder = os.path.dirname(full_path)
          print("project_folder : " + str(project_folder))
          
          project_name = os.path.basename(project_folder)
          print("project_name : " + str(project_name))
      
          # Construct path to the snapshot we want to restore
          snapshot_path = os.path.join(project_folder, "snapshots", project_name + "_final.json")
          
          print("\nPath verification:")
          print(f"Snapshot path to restore: {snapshot_path}")
          
          # Verify the snapshot file exists
          if not os.path.exists(snapshot_path):
              print(f"Error: Snapshot file not found at {snapshot_path}")
              return
          
          print("\nAttempting to restore snapshot...")
          try:
              result = restore_snapshot(doc, snapshot_path)
              print(f"restore_snapshot call completed. Result: {result}")
          except Exception as e:
              print(f"Error in restore_snapshot: {str(e)}")
              import traceback
              print(f"Traceback:\n{traceback.format_exc()}")
      
      if __name__ == "__main__":
          c4d.CallCommand(13957) # Clear Console
          main()
      

      Scenes for testing:
      SimpleScene4Reference.c4d
      SimpleScene4Restore.c4d

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

        Hello @Futurium,

        thank you for reaching out to us. It pains me a bit to say that, because you made so nicely sure that we have all resources, but your topic is out of scope of support, specifically the points:

        • We provide code examples but we do not provide full solutions or design applications.
        • We cannot debug your code for you and instead provide answers to specific problems.

        Sometimes we cut people some slack here but in your case this is just too much work.

        Some High-level Advice

        The Take system of Cinema 4D is one of its most liked features. What comes to users sometimes as a surprise, is the technical complexity of the Take System/API; one could also say that it has a bit the reputation of being overly difficult to deal with. While the API is manageable when you get used to it, I would really avoid trying to write a custom serialization and deserialization routine for take data, due to all that complexity. It is certainly possible to do, but at least I would be very hesitant to do that due to all that complexity wich must be met.

        In general, you can serialize and deserialize scene elements (nodes) with C4DAtom.Write/Read. Since takes are also of type C4DAtom, you could in theory also serialize/deserialize them like that. I added there the 'in theory' because we generally discourage third parties from using such low-level IO, as you can relatively easily brick your data like this by missing important internal dependencies and/or crash Cinema 4D like that. For takes this could mean that internal data is not copied/serialized properly or that one of the many (Base)links used in takes fails to reattach upon being loaded in another scene.

        I personally would try to use the Take Asset Presets to do what you want. Saving a preset is easy, just select it, and then call the command for it. Loading the take preset programmatically might be a bit trickier. Take assets were added after I wrote for S26 the Asset API documentation, and I never used them myself. I am not sure if you can load a take asset just with maxon.AssetManagerInterface.LoadAssets or if you have to get your hands dirty yourself with a more manual approach.

        Okay scratch that, I just tried out Take Assets and they probably do not do what you want to do (we might ourself have deemed this a too complex task to fully reattach takes in a new scene). In the end, doing it manually with JSON might indeed be the only option for what you want to do, but we cannot do that for you or debug your code for you.

        Cheers,
        Ferdinand

        MAXON SDK Specialist
        developers.maxon.net

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