autocreate RS node material based texture sets in tex folder
-
Hi in advanced im sorry if im asking for help with something the forumn is not used for, im just tryign to help out a buddies pipeline and im by no means the best with the maxon api documentation. So far my script lets me auto make materials based on texture sets in the texture folder. any help to understand the error so i cna implement a fix would be super helpfula and by no means expected. thanks in advanced.
import c4d import maxon import os import re # Redshift Constants ID_REDSHIFT_ENGINE = 1036219 ID_REDSHIFT_NODESPACE = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") ID_NODE_EDITOR_MODE_MATERIAL = 465002360 # Texture Naming Pattern (Assumes: mesh_textureSet_mapType.png) TEXTURE_NAME_PATTERN = re.compile(r"(.+?)_(.+?)_.+\.(png|jpg|tif|exr)") def get_texture_sets(tex_folder): """Extracts unique texture set names from files in the 'tex' folder.""" texture_sets = set() if not os.path.exists(tex_folder): return texture_sets for file in os.listdir(tex_folder): match = TEXTURE_NAME_PATTERN.match(file) if match: mesh, texture_set = match.groups()[:2] texture_sets.add(f"{mesh}_{texture_set}") return texture_sets def create_redshift_material(name): """Creates a Redshift Node Material with the given name.""" doc = c4d.documents.GetActiveDocument() # Set the Node Editor to Material Mode if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL): c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL) # Ensure Redshift is the active render engine render_settings = doc.GetActiveRenderData() if render_settings: render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE # Create a new Redshift Material c4d.CallCommand(1036759) # Redshift Material c4d.EventAdd() # Get the newly created material material = doc.GetActiveMaterial() if not material: raise RuntimeError("Failed to create Redshift Material.") material.SetName(name) # Get Node Material Reference node_material = material.GetNodeMaterialReference() if not node_material: raise RuntimeError("Failed to retrieve the Node Material reference.") # Ensure the material is in the Redshift Node Space if not node_material.HasSpace(ID_REDSHIFT_NODESPACE): graph = node_material.AddGraph(ID_REDSHIFT_NODESPACE) else: graph = node_material.GetGraph(ID_REDSHIFT_NODESPACE) if graph is None: raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.") # Update and refresh Cinema 4D material.Message(c4d.MSG_UPDATE) c4d.EventAdd() print(f" Created Redshift Node Material: {name}") return material def main(): """Main function to create Redshift materials based on texture sets in 'tex' folder.""" doc = c4d.documents.GetActiveDocument() tex_folder = os.path.join(doc.GetDocumentPath(), "tex") texture_sets = get_texture_sets(tex_folder) if not texture_sets: c4d.gui.MessageDialog("No texture sets found in 'tex' folder!") return for texture_set in texture_sets: create_redshift_material(texture_set) # Execute the script if __name__ == '__main__': main()
This works great so far. the issues im running into is when i try and auto assign texture nodes in the node graph and assign them to the material inputs
import c4d import maxon import os import re # Constants ID_REDSHIFT_ENGINE = 1036219 ID_NODE_EDITOR_MODE_MATERIAL = 465002360 TEXTURE_NAME_PATTERN = re.compile(r"(.+?)_(.+?)_.+\.(png|jpg|tif|exr)") # Mapping texture types to their corresponding Redshift shader node IDs TEXTURE_TYPES = { "BaseColor": "com.redshift3d.redshift4c4d.nodes.core.texturesampler", "Normal": "com.redshift3d.redshift4c4d.nodes.core.bumpmap", "Roughness": "com.redshift3d.redshift4c4d.nodes.core.texturesampler", "Metalness": "com.redshift3d.redshift4c4d.nodes.core.texturesampler", "Opacity": "com.redshift3d.redshift4c4d.nodes.core.texturesampler", "Displacement": "com.redshift3d.redshift4c4d.nodes.core.displacement", "EmissionColor": "com.redshift3d.redshift4c4d.nodes.core.texturesampler" } def get_texture_sets(tex_folder): """Extracts unique texture set names and maps available textures.""" texture_sets = {} if not os.path.exists(tex_folder): return texture_sets for file in os.listdir(tex_folder): match = TEXTURE_NAME_PATTERN.match(file) if match: mesh, texture_set, _ = match.groups() key = f"{mesh}_{texture_set}" if key not in texture_sets: texture_sets[key] = {} for channel in TEXTURE_TYPES.keys(): if channel.lower() in file.lower(): texture_sets[key][channel] = os.path.join(tex_folder, file) return texture_sets def create_redshift_material(name, texture_files): """Creates a Redshift Node Material and assigns textures.""" doc = c4d.documents.GetActiveDocument() # Set the Node Editor to Material Mode if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL): c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL) # Ensure Redshift is the active render engine render_settings = doc.GetActiveRenderData() if render_settings: render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE # Create a new Redshift Material c4d.CallCommand(1036759) # Redshift Material c4d.EventAdd() # Get the newly created material material = doc.GetActiveMaterial() if not material: raise RuntimeError("Failed to create Redshift Material.") material.SetName(name) # Get Node Material Reference node_material = material.GetNodeMaterialReference() if not node_material: raise RuntimeError("Failed to retrieve the Node Material reference.") # Ensure the material is in the Redshift Node Space redshift_nodespace_id = maxon.NodeSpaceIdentifiers.RedshiftMaterial if not node_material.HasSpace(redshift_nodespace_id): graph = node_material.AddGraph(redshift_nodespace_id) else: graph = node_material.GetGraph(redshift_nodespace_id) if graph is None: raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.") print(f" Created Redshift Node Material: {name}") # Create Texture Nodes and Connect Them with maxon.GraphTransaction(graph) as transaction: texture_nodes = {} for channel, file_path in texture_files.items(): print(f"πΉ Creating node for {channel}: {file_path}") # Create the texture node node_id = maxon.Id(TEXTURE_TYPES[channel]) node = graph.AddChild("", node_id, maxon.DataDictionary()) if node is None: print(f" Failed to create node for {channel}") continue # Set the filename parameter filename_port = node.GetInputs().FindChild("filename") if filename_port: filename_port.SetDefaultValue(maxon.Url(file_path)) else: print(f" 'filename' port not found for {channel} node.") texture_nodes[channel] = node print(f" Successfully created {channel} texture node.") # Connect Texture Nodes to Material Inputs material_node = graph.GetRoot() if material_node: for channel, tex_node in texture_nodes.items(): input_port_id = f"{channel.lower()}_input" # Example: 'basecolor_input' material_input_port = material_node.GetInputs().FindChild(input_port_id) if material_input_port: tex_output_port = tex_node.GetOutputs().FindChild("output") if tex_output_port: material_input_port.Connect(tex_output_port) print(f"π Connected {channel} texture to material.") else: print(f" 'output' port not found on {channel} texture node.") else: print(f" '{input_port_id}' port not found on material node.") else: print(" Material node (root) not found in the graph.") transaction.Commit() # Update and refresh Cinema 4D material.Message(c4d.MSG_UPDATE) c4d.EventAdd() return material def main(): """Main function to create Redshift materials based on texture sets in 'tex' folder.""" doc = c4d.documents.GetActiveDocument() tex_folder = os.path.join(doc.GetDocumentPath(), "tex") texture_sets = get_texture_sets(tex_folder) if not texture_sets: c4d.gui.MessageDialog("No texture sets found in 'tex' folder!") return for texture_set, texture_files in texture_sets.items(): create_redshift_material(texture_set, texture_files) # Execute the script if __name__ == '__main__': main()
The error im geting if it does nto just crash outright
Traceback (most recent call last): line 147, in <module> line 143, in main line 84, in create_redshift_material line 295, in __init__ line 164, in __init__ raise TypeError("passed object is not of same type to create copy") TypeError: passed object is not of same type to create copy```
-
Hello @apetownart,
Thank you for reaching out to us. Do you have an example texture set we could use? Without it, it is a bit hard to have a look at your code example, due to the pattern
"(.+?)_(.+?)_.+\.(png|jpg|tif|exr)"
you use. Graph descriptions could also be an alternative for you. You could at least use GraphDescription.GetGraph to avoid some boiler plate code when you otherwise prefer approaching this more manually.Cheers,
Ferdinand -
thank you for the reply. its looking at texture sets from SUbstance painter. here are some examples.
-
Hello @apetownart,
so, I had a look at your code, and there are unfortunately quite a few issues with it. Did you write this code yourself? We have a code example which is quite close to what you are trying to do. Your code strikes me as AI-generated, but I might be wrong.
Please understand that we cannot provide support on this level of issues, we expect users to generally follow our documentation, we cannot provide here line-by-line support. I have attached below your script in a commented version up to the point to which I debugged it. What you want to do, is certainly possible with the low level Nodes API in Python.
I would recommend to use the more artist friendly high level graph description API in your case. I have attached an example below.
maxon.GraphDescription.ApplyDescription
currently prints junk to the console when executed. This is a known issue and will be fixed in the next release.Cheers,
FerdinandResult
Code
"""Demonstrates how to compose materials using graph descriptions for texture bundles provided by a texture website or an app like substance painter. The goal is to construct materials over the regular naming patterns such texture packs exhibit. """ import os import c4d import maxon import mxutils doc: c4d.documents.BaseDocument # The active document. # The texture formats we support loading, could be extended to support more formats. TEXTURE_FORMATS: tuple[str] = (".png", ".jpg", ".tif", ".exr") # Texture postfix classification data. This map exists so that you could for example map both the # keys "color" and "diffuse" to the same internal classification "color". I did not make use of this # multi routing here. TEXTURE_CLASSIFICATIONS: dict = { "color": "color", "roughness": "roughness", "normal": "normal", } def GetTextureData(doc: c4d.documents.BaseDocument) -> dict[str, dict[str, str]]: """Ingests the texture data from the 'tex' folder of the passed document and returns it as a dictionary. This is boilerplate code that is effectively irrelevant for the subject of using graph descriptions to construct materials. """ # Get the document's texture folder path. path: str = mxutils.CheckType(doc).GetDocumentPath() if not path: raise RuntimeError("Cannot operate on an unsaved document.") texFolder: str = os.path.join(path, "tex") if not os.path.exists(texFolder): raise RuntimeError(f"Texture folder '{texFolder}' does not exist.") # Start building texture data. data: dict[str, dict[str, str]] = {} for file in os.listdir(texFolder): # Fiddle paths, formats, and the classification label into place. fPath: str = os.path.join(texFolder, file) if not os.path.isfile(fPath): continue name: str = os.path.splitext(file)[0].lower() ext: str = os.path.splitext(file)[1].lower() if ext not in TEXTURE_FORMATS: continue items: list[str] = name.rsplit("_", 1) if len(items) != 2: raise RuntimeError( f"Texture file '{file}' is not following the expected naming convention.") # Get the label, i.e., unique part of the texture name, and its classification postfix. label: str = items[0].lower() classification: str = items[1].lower() if classification not in TEXTURE_CLASSIFICATIONS.keys(): continue # Map the classification to its internal representation and write the data. container: dict[str, str] = data.get(label, {}) container[TEXTURE_CLASSIFICATIONS[classification]] = fPath data[label] = container return data def CreateMaterials(doc: c4d.documents.BaseDocument) -> None: """Creates Redshift materials based on the texture data found in the 'tex' folder of the passed document. """ # Get the texture data for the document. mxutils.CheckType(doc) data: dict[str, dict[str, str]] = GetTextureData(doc) # Iterate over the texture data and create materials. for label, container in data.items(): # Create a Redshift Material and get its graph. graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph( name=label, nodeSpaceId=maxon.NodeSpaceIdentifiers.RedshiftMaterial) # We could have also created a graph with the default nodes in it in the call above, but # it is just easier to do things manually here. So, we create an empty graph and add here # an output node with a standard material attached. See # # https://developers.maxon.net/docs/py/2025_1_0/manuals/manual_graphdescription.html # # for more information on how to use the graph description API. description: dict = { "$type": "Output", "Surface": { "$type": "Standard Material", } } # Extend the description based on the textures we found in the current texture pack. # Attach a texture node with the color texture to the "Base/Color" port of the Standard # Material node. if "color" in container: description["Surface"]["Base/Color"] = { "$type": "Texture", "General/Image/Filename/Path": maxon.Url(f"file:///{container['color']}") } # The same for roughness. if "roughness" in container: description["Surface"]["Base/Diffuse Roughness"] = { "$type": "Texture", "General/Image/Filename/Path": maxon.Url(f"file:///{container['roughness']}") } # Basically the same again, just that we also add a "Bump Map" node when we link a # normal map. if "normal" in container: description["Surface"]["Geometry/Bump Map"] = { "$type": "Bump Map", "Input Map Type": 1, # Tangent Space "Height Scale": .2, # Adjust this value to match the normal map intensity. "Input": { "$type": "Texture", "Image/Filename/Path": maxon.Url(f"file:///{container['normal']}") } } # And finally apply it to the graph. maxon.GraphDescription.ApplyDescription(graph, description) # Execute the script if __name__ == '__main__': CreateMaterials(doc) c4d.EventAdd()
Your Code
def create_redshift_material(name, texture_files): """Creates a Redshift Node Material and assigns textures.""" doc = c4d.documents.GetActiveDocument() # (FH): Not necessary and counter productive. if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL): c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL) # (FH): That is not sufficient, see code examples. You also have to add the post effect. # Ensure Redshift is the active render engine. render_settings = doc.GetActiveRenderData() if render_settings: render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE # (FH): Avoid commands, use the API instead. Invoking events multiple times in scripts is also # pointless as a script manager script is blocking. I.e., having a loop which changes the scene # and calls EventAdd() on each iteration, or the same loop and calling EventAdd() once at the # end of the script will have the same effect. c4d.CallCommand(1036759) # Redshift Material c4d.EventAdd() # (FH): Could be condensed to a single line using maxon.GraphDescription.GetGraph() material = doc.GetActiveMaterial() if not material: raise RuntimeError("Failed to create Redshift Material.") material.SetName(name) node_material = material.GetNodeMaterialReference() if not node_material: raise RuntimeError("Failed to retrieve the Node Material reference.") redshift_nodespace_id = maxon.NodeSpaceIdentifiers.RedshiftMaterial if not node_material.HasSpace(redshift_nodespace_id): graph = node_material.AddGraph(redshift_nodespace_id) else: graph = node_material.GetGraph(redshift_nodespace_id) if graph is None: raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.") print(f" Created Redshift Node Material: {name}") # (FH) Incorrect call and the cause for your crashes. # with maxon.GraphTransaction(graph) as transaction: with graph.BeginTransaction() as transaction: texture_nodes = {} for channel, file_path in texture_files.items(): # (FH) Avoid using such exotic Unicode symbols, Python supports them but not every # system in Cinema 4D does. print(f"πΉ Creating node for {channel}: {file_path}") node_id = maxon.Id(TEXTURE_TYPES[channel]) node = graph.AddChild("", node_id, maxon.DataDictionary()) if node is None: print(f" Failed to create node for {channel}") continue # (FH) That cannot work, #filename is not a port which is a direct child of a RS # texture node, but a child port of the "tex0" port bundle, the port is also called # "path" and not "filename". # See: scripts/05_modules/node/create_redshift_nodematerial_2024.py in # https://github.com/Maxon-Computer/Cinema-4D-Python-API-Examples/ for an example for # how to construct a Redshift Node Material with Python, likely covering most what you # need to know. filename_port = node.GetInputs().FindChild("filename") if filename_port: # Will fail because filename_port is not a valid port. filename_port.SetDefaultValue(maxon.Url(file_path)) else: print(f" 'filename' port not found for {channel} node.") # (FH): Stopped debugging from here on out ... texture_nodes[channel] = node print(f" Successfully created {channel} texture node.") material_node = graph.GetRoot() if material_node: for channel, tex_node in texture_nodes.items(): input_port_id = f"{channel.lower()}_input" material_input_port = material_node.GetInputs().FindChild(input_port_id) if material_input_port: tex_output_port = tex_node.GetOutputs().FindChild("output") if tex_output_port: material_input_port.Connect(tex_output_port) print(f"π Connected {channel} texture to material.") else: print(f" 'output' port not found on {channel} texture node.") else: print(f" '{input_port_id}' port not found on material node.") else: print(" Material node (root) not found in the graph.") transaction.Commit() return material def main(): """Main function to create Redshift materials based on texture sets in 'tex' folder.""" # (FH) Not necessary, #doc is already available in the global scope. doc = c4d.documents.GetActiveDocument() tex_folder = os.path.join(doc.GetDocumentPath(), "tex") texture_sets = get_texture_sets(tex_folder) if not texture_sets: c4d.gui.MessageDialog("No texture sets found in 'tex' folder!") return for texture_set, texture_files in texture_sets.items(): create_redshift_material(texture_set, texture_files)
-
thanks for the help. I will adjust it and fix it.
-
I was getting super explicit with node space calls because i was running into api issues where the consoole would have problems finding the right node space so i was trying to fix that originally. Most of the Python as a backend dev so i was usingertign it more generic as opposed to using API functions.
-
@ferdinand I probably know the answer to this before i ask but ill ask anyway. is there a version agnostic way to call the node space so the script runs ok in 2024 or will i need to use a method not depricated.
-
Hi,
I am not quite sure how you mean this, what do you consider to be a 'version agnostic way to call the node space'? Do you mean if there are node space ID symbols exposed in earlier versions than 2025? Unfortunately not. But
maxon.NodeSpaceIdentifiers
is actually one of the very few things we define in Python (inresource\modules\python\libs\python311\maxon\frameworks\nodes.py
), so you could just monkey patch earlier versions.import maxon # Copied from `nodes.py`, I just made the namespaces explicit. class NodeSpaceIdentifiers: """ Holds the identifiers for all node spaces shipped with Cinema 4D. .. note:: When you want to reference the node space added by a plugin, you must ask the plugin vendor for the node space id literal of that plugin, e.g., `com.fancyplugins.nodespace.foo`. When asked for a node space ID in the API, you must then pass `Id("com.fancyplugins.nodespace.foo")`. """ #: References the node space used by node materials for the Standard renderer. StandardMaterial: maxon.Id = maxon.Id("net.maxon.nodespace.standard") #: References the node space used by node materials for the Redshift renderer. RedshiftMaterial: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") #: References the node space used by Scene Nodes graphs. SceneNodes: maxon.Id = maxon.Id("net.maxon.neutron.nodespace") # Attache the class to `maxon` under `NodeSpaceIdentifiers` when it is not there. You could also # add a version check to be extra sure. if not hasattr(maxon, "NodeSpaceIdentifiers"): maxon.NodeSpaceIdentifiers = NodeSpaceIdentifiers print (maxon.NodeSpaceIdentifiers.SceneNodes)
edit:
NodeSpaceIdentifiers
has actually been introduced with 2024.3