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:



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.