Finding, Renaming, and Updating all References to Assets
-
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 thechannelId
'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:
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. -
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 -
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.