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

    Finding, Renaming, and Updating all References to Assets

    Cinema 4D SDK
    python 2025
    2
    3
    93
    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.
    • d_keithD
      d_keith
      last edited by d_keith

      Hi,

      Are there any helper methods for finding & updating all references to assets in a Cinema 4D document?

      I'm attempting to write a script that lets the user rename textures. GetAllAssetsNew() makes it easy to find the assets, but not all elements support owner[channelId] = newName style assignment, and many of the channelId's come back as -1.

      Cinema 4D includes native functionality that can discover & rename all usages of a give asset in the Project Asset Inspector, Scene Info, and the RS Asset Manager:

      Scene Info

      Project Asset Inspector

      RS Asset Manager

      Is there a simple method for updating all asset references (alembic, caches, sound effector, etc, textures, etc)? Manually handling different asset types (like Standard vs RS Materials) feels really brittle and not particularly future proof.


      I've create a script (with liberal assistance of GitHub Copilot and one of @ferdinand's examples) that lets me rename Redshift textures, but I'm looking for a more general solution that doesn't force me to write custom interactions for every renderer / object type:

      """Name-en-US: Find and Rename Textures
      Description-en-US: Lists all assets in scene and lets you find/replace parts of their names.
      
      Credit:
      - Donovan Keith
      - GitHub Copilot with Claude 3.7
      - @ferdinand
      
      References:
      - https://developers.maxon.net/forum/topic/14388/change-textures-path-from-getallassetnew-in-the-new-node-editor
      - https://developers.maxon.net/docs/py/2024_4_0a/modules/c4d.documents/BaseDocument/index.html#BaseDocument.GetAllTextures
      - https://developers.maxon.net/docs/py/2024_4_0a/modules/c4d.documents/index.html#c4d.documents.GetAllAssetsNew
      """
      
      import c4d
      import maxon
      import os
      
      
      def get_scene_assets(doc):
          """Get all assets from the active document."""
          if not doc:
              return []
          
          asset_list = []
          last_path = ""
          c4d.documents.GetAllAssetsNew(
              doc, 
              allowDialogs=False, 
              lastPath=last_path, 
              flags=c4d.ASSETDATA_FLAG_NODOCUMENT, 
              assetList=asset_list
          )
          return asset_list
      
      
      def get_search_replace_strings():
          """Prompt user for search and replace strings."""
          search_str = c4d.gui.InputDialog("Enter the string to find in asset names:", preset="old-name")
          if search_str is None:
              return None, None
              
          replace_str = c4d.gui.InputDialog("Enter the replacement string for asset names:", preset="new-name")
          if replace_str is None:  # User canceled
              return None, None
          
          return search_str, replace_str
      
      
      def find_assets_to_rename(asset_list, search_str, replace_str):
          """Find assets containing the search string."""
          assets_to_rename = []
          for asset in asset_list:
              if search_str in asset['assetname']:
                  new_name = asset['assetname'].replace(search_str, replace_str)
                  assets_to_rename.append((asset, new_name))
          return assets_to_rename
      
      
      def show_preview_dialog(assets_to_rename):
          """Show preview of assets to be renamed and ask for confirmation."""
          if not assets_to_rename:
              c4d.gui.MessageDialog("No assets found with the specified string.")
              return False
              
          preview_msg = f"Found {len(assets_to_rename)} assets to rename:\n\n"
          MAX_PREVIEW_ITEMS = 10
          for i, (asset, new_name) in enumerate(assets_to_rename[:MAX_PREVIEW_ITEMS], 1):
              preview_msg += f"{i}. {asset['assetname']} → {new_name}\n"
              
          if len(assets_to_rename) > MAX_PREVIEW_ITEMS:
              preview_msg += f"...and {len(assets_to_rename) - MAX_PREVIEW_ITEMS} more\n"
      
          preview_msg += "\nDo you want to continue?"
          return c4d.gui.QuestionDialog(preview_msg)
      
      
      def rename_file_on_disk(file_path, new_name):
          """Rename the actual file on disk."""
          if not file_path or not os.path.exists(file_path):
              return False
          
          try:
              # Create new file path with the same directory but new name
              new_file_path = os.path.join(os.path.dirname(file_path), os.path.basename(new_name))
              
              # Create destination folder if it doesn't exist
              new_dir = os.path.dirname(new_file_path)
              if not os.path.exists(new_dir):
                  os.makedirs(new_dir)
                  
              # Rename the file
              os.rename(file_path, new_file_path)
              print(f"Renamed file on disk: {file_path} → {new_file_path}")
              return True
          except Exception as e:
              print(f"Error renaming file: {e}")
              return False
      
      
      def update_standard_shader(owner, param_id, new_name):
          """Update standard shader asset reference."""
          owner[param_id] = new_name
          print(f"Updated standard shader: {owner.GetName()}")
      
      
      def update_redshift_node(owner, node_path, node_space, original_name, new_name):
          """Update Redshift nodal material references."""
          # Get the node material associated with the material
          node_material = owner.GetNodeMaterialReference()
          if not node_material:
              return False
              
          graph = node_material.GetGraph(node_space)
          if graph.IsNullValue():
              print(f"Invalid node space for {owner.GetName()}: {node_space}")
              return False
          
          # Start a graph transaction and get the node
          with graph.BeginTransaction() as transaction:
              node = graph.GetNode(maxon.NodePath(node_path))
              if node.IsNullValue():
                  print(f"Could not retrieve target node {node_path}")
                  return False
              
              try:
                  path_port = node.GetInputs().FindChild(
                      "com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path")
                  if path_port.IsNullValue():
                      print("Could not find path port for Redshift texture node")
                      return False
                  
                  # Set the new value
                  path_port.SetPortValue(new_name)
                  print(f"Updated Redshift texture reference in material: {owner.GetName()}")
                  
                  # Get the current node name
                  node_name = node.GetValue(maxon.NODE.BASE.NAME)
                  
                  # Get the texture filename (without path)
                  old_filename = os.path.basename(original_name)
                  new_filename = os.path.basename(new_name)
                  
                  # If node name matches the old texture filename, update it
                  if node_name == old_filename or node_name == os.path.splitext(old_filename)[0]:
                      node.SetValue(maxon.NODE.BASE.NAME, new_filename)
                      print(f"Renamed node from '{node_name}' to '{new_filename}'")
                  
                  transaction.Commit()
                  return True
              
              except Exception as e:
                  print(f"Error updating Redshift node: {e}")
                  return False
      
      
      def process_asset_rename(doc, asset, new_name, rename_files):
          """Process the renaming of a single asset."""
          original_name = asset['assetname']
          print(f"Renaming asset: {original_name} to {new_name}")
      
          owner = asset.get('owner')
          if not owner:
              return False
              
          doc.AddUndo(c4d.UNDOTYPE_CHANGE, owner)
          
          # Rename the actual file if requested
          if rename_files:
              file_path = asset.get('filename', '')
              rename_file_on_disk(file_path, new_name)
          
          # Handle different types of assets
          
          # 1. Handle standard shader assets (like Bitmap shader)
          param_id = asset.get('paramId', -1)
          if isinstance(owner, c4d.BaseShader) and param_id != -1 and param_id != c4d.NOTOK:
              if asset.get('nodePath', '') == '':  # Regular shader
                  update_standard_shader(owner, param_id, new_name)
          
          # 2. Handle Redshift nodal material references
          node_path = asset.get('nodePath', '')
          node_space = asset.get('nodeSpace', '')
          
          if isinstance(owner, c4d.BaseMaterial) and node_path and node_space:
              update_redshift_node(owner, node_path, node_space, original_name, new_name)
          
          return True
      
      
      def main():
          doc = c4d.documents.GetActiveDocument()
          if not doc:
              return
          
          # Get all assets from the scene
          asset_list = get_scene_assets(doc)
          
          # Print found assets for debugging
          for asset in asset_list:
              print(f"Found asset: {asset['assetname']}")
      
          # Get search and replace strings
          search_str, replace_str = get_search_replace_strings()
          if not search_str or replace_str is None:
              return
              
          # Find assets to rename
          assets_to_rename = find_assets_to_rename(asset_list, search_str, replace_str)
          
          # Show preview and ask for confirmation
          if not show_preview_dialog(assets_to_rename):
              return
              
          # Ask if user wants to rename files on disk as well
          rename_files = c4d.gui.QuestionDialog("Do you also want to rename the files on disk?")
          
          # Process rename operations
          doc.StartUndo()
          for asset, new_name in assets_to_rename:
              process_asset_rename(doc, asset, new_name, rename_files)
          doc.EndUndo()
      
      
      if __name__ == '__main__':
          main()
      

      Research / Related Posts
      https://developers.maxon.net/forum/topic/12893/changing-assets-paths-feature-request
      https://developers.maxon.net/forum/topic/14241/api-for-project-asset-inspector - Seems like there's a method for finding all assets.

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

        Hey @d_keith,

        Thank you for reaching out to us. Please file this request (which I assume is not private) internally, the forum is not the right place for this.

        I am not quite sure what the exact question of yours is :D. GetAllAssets(New) is the middleware for the Project Asset Inspector (PAI) and the backend messages MSG_GETALLASSETS and MSG_RENAMETEXTURES. The latter is not wrapped at all for Python and the former only as an outgoing message. So, GetAllAssets is the 'helper' method used by the PAI to realize all its functionalities you are looking for.

        There was once the thread Get all textures for a material which was in scope very similar to yours. We very recently also had this thread where we talked about how you can abstract away some stuff.

        As always, we cannot debug code for users, so I did not run your code to find potential problems. At first glance it looks okay. I would however recommend to use the flag ASSETDATA_FLAG_MULTIPLEUSE as you will otherwise step over nodes using an already yielded asset. Which you probably do not want to do in your case.

        You are also very broad in your terminology - ' finding & updating all references to assets in a Cinema 4D document'. The rest of your posting seems to be however very focused on textures, while your code then again does not really make an attempt to distinguish what kind of asset you are handling. Documents can contain media (textures, movies, audio), scene data (documents, volumes, xrefs, substances, ies, ...) and font assets. There is also some super exotic internal data you sometimes come along. Not filtering at all what asset data you handle does not seem to be a good idea.

        Cheers,
        Ferdinand

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • d_keithD
          d_keith
          last edited by

          Thanks for the response!

          I appreciate the ASSETDATA_FLAG_MULTIPLEUSE tip.

          My hope was that there was some sort of existing RenameAsset SDK method that would also update all references to that asset without implementing custom handling for various element types (Redshift Nodal Material, Cinema 4D Classic Material, Arnold Dome Light, XRef, etc). Evidently such a public python SDK method doesn't exist.

          My example code was texture specific as that's the part of the problem that's most pressing to me and I was able to implement with custom per-type handling - if I manage to get other types working, I'll attempt to update over time.

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