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,
Ferdinand
Result

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)