nodes_material_create_redshift

Demonstrates setting up a Redshift node material composed out of multiple nodes.

nodes_material_create_redshift.py

#coding: utf-8
"""Demonstrates setting up a Redshift node material composed out of multiple nodes.

Creates a new node material with a graph in the Redshift material space, containing two texture 
nodes and a mix node, in addition to the default RS Standard Material and Output node of the 
material.

Topics:
    * Creating a node material and adding a graph.
    * Adding nodes to a graph.
    * Setting the value of ports without wires.
    * Connecting ports with a wires.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__date__ = "04/06/2024"
__license__ = "Apache-2.0 License"
__version__ = "2024.0.0"


import c4d
import maxon

doc: c4d.documents.BaseDocument # The active document.

def main() -> None:
    """Runs the example.
    """
    # The asset URLs for the "RustPaint0291_M.jpg" and "Sketch (HR basic026).jpg" texture assets in 
    # "tex/Surfaces/Dirt Scratches & Smudges/". These could also be replaced with local texture URLs,
    # e.g., "file:///c:/textures/stone.jpg". These IDs can be discovered with the #-button in the info
    # area of the Asset Browser.
    urlTexRust: maxon.Url = maxon.Url(r"asset:///file_edb3eb584c0d905c")
    urlTexSketch: maxon.Url = maxon.Url(r"asset:///file_3b194acc5a745a2c")

    # The node asset IDs for the two node types to be added in the example; the texture node and the
    # mix node. These and all other node IDs can be discovered in the node info overlay in the 
    # bottom left corner of the Node Editor. Open the Cinema 4D preferences by pressing CTRL/CMD + E
    # and enable Node Editor -> Ids in order to see node and port IDs in the Node Editor.
    idOutputNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.node.output")
    idStandardMaterial: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial")
    idTextureNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler")
    idMixNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix")

    # Instantiate a material, get its node material, and add a graph for the RS material space.
    material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
    if not material:
        raise MemoryError(f"{material = }")

    nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
    redshiftNodeSpaceId: maxon.Id =  maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")
    graph: maxon.GraphModelRef = nodeMaterial.CreateEmptyGraph(redshiftNodeSpaceId)
    if graph.IsNullValue():
        raise RuntimeError("Could not add Redshift graph to material.")

    # Open an undo operation and insert the material into the document. We must do this before we 
    # modify the graph of the material, as otherwise the viewport material will not correctly display 
    # the textures of the material until the user manually refreshes the material. It is also 
    # important to insert the material after we added the default graph to it, as otherwise we will 
    # end up with two undo steps in the undo stack.
    if not doc.StartUndo():
        raise RuntimeError("Could not start undo stack.")
    
    doc.InsertMaterial(material)
    if not doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, material):
        raise RuntimeError("Could not add undo item.")

    # Define the user data for the transaction. This is optional, but can be used to tell the Nodes
    # API to add the transaction to the current undo stack instead of creating a new one. This will
    # then have the result that adding the material, adding the graph, and adding the nodes will be
    # one undo step in the undo stack.
    userData: maxon.DataDictionary = maxon.DataDictionary()
    userData.Set(maxon.nodes.UndoMode, maxon.nodes.UNDO_MODE.ADD)

    # Start modifying the graph by opening a transaction. Node graphs follow a database like 
    # transaction model where all changes are only finally applied once a transaction is committed.
    with graph.BeginTransaction(userData) as transaction:

        # Add the output, i.e., the terminal end node of the graph, as well as a standard material
        # node to the graph.
        outNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idOutputNode)
        materialNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idStandardMaterial)

        # Add two texture nodes and a blend node to the graph.
        rustTexNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idTextureNode)
        sketchTexNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idTextureNode)
        mixNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idMixNode)

        # Get the input 'Surface' port of the 'Output' node and the output 'Out Color' port of the
        # 'Standard Material' node and connect them.
        surfacePortOutNode: maxon.GraphNode = outNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.node.output.surface")
        outcolorPortMaterialNode: maxon.GraphNode = materialNode.GetOutputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.standardmaterial.outcolor")
        outcolorPortMaterialNode.Connect(surfacePortOutNode)

        # Set the default value of the 'Mix Amount' port, i.e., the value the port has when no 
        # wire is connected to it. This is equivalent to the user setting the value to "0.5" in 
        # the Attribute Manager.
        mixAmount: maxon.GraphNode = mixNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.rscolormix.mixamount")
        mixAmount.SetPortValue(0.5)

        # Set the path sub ports of the 'File' ports of the two image nodes to the texture URLs 
        # established above. Other than for the standard node space image node, the texture is 
        # expressed as a port bundle, i.e., a port which holds other ports. The texture of a texture
        # node is expressed as the "File" port, of which "Path", the URL, is only one of the possible
        # sub-ports to set.
        pathRustPort: maxon.GraphNode = rustTexNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path")
        pathSketchPort: maxon.GraphNode = sketchTexNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path")
        pathRustPort.SetPortValue(urlTexRust)
        pathSketchPort.SetPortValue(urlTexSketch)

        # Get the color output ports of the two texture nodes and the color blend node.
        rustTexColorOutPort: maxon.GraphNode = rustTexNode.GetOutputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")
        sketchTexColorOutPort: maxon.GraphNode = sketchTexNode.GetOutputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")
        mixColorOutPort: maxon.GraphNode = mixNode.GetOutputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.rscolormix.outcolor")

        # Get the fore- and background port of the blend node and the color port of the BSDF node.
        mixInput1Port: maxon.GraphNode = mixNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.rscolormix.input1")
        mixInput2Port: maxon.GraphNode = mixNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.rscolormix.input2")
        stdBaseColorInPort: maxon.GraphNode = materialNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color")

        # Wire up the two texture nodes to the blend node and the blend node to the BSDF node.
        rustTexColorOutPort.Connect(mixInput1Port, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
        sketchTexColorOutPort.Connect(mixInput2Port, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
        mixColorOutPort.Connect(stdBaseColorInPort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)

        # Finish the transaction to apply the changes to the graph.
        transaction.Commit()

    if not doc.EndUndo():
        raise RuntimeError("Could not end undo stack.")

    c4d.EventAdd()
    
if __name__ == "__main__":
    main()