SOLVED Creating standaard node material with several nodes (and connections)?

I want to create a standaard c4d node material with several nodes.
I can create the standard / initial material, but I cannot add node (diffuse, image, etc.).
Could you please provide an example creating a standaard node material with several nodes (and connections).

Regards,
Pim

Hey @pim,

Thank you for reaching out to us. You are right, we are missing an example for how to set up a node graph. In C++, I wrote the Create a Redshift Material example which goes over the basics of creating a Redshift nodes material, but that is probably only mildly helpful for most Python users. I therefore added an example for creating a simple node graph in Python to the Github examples. The examples will be published in an upcoming release, find a preview of the example below.

Cheers,
Ferdinand

Code:

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

This will create a new node material with a graph in the standard material space, containing two
texture nodes and a blend node, in addition to the default BSDF and material 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
    * (Asset API): Using texture assets in a node graph

"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2023 MAXON Computer GmbH"
__date__ = "09/01/2023"
__license__ = "Apache-2.0 License"
__version__ = "2023.0.0"


import c4d
import maxon

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

def GetFileAssetUrl(aid: maxon.Id) -> maxon.Url:
    """Returns the asset URL for the given file asset ID.
    """
    # Bail when the asset ID is invalid.
    if not isinstance(aid, maxon.Id) or aid.IsEmpty():
        raise RuntimeError(f"{aid = } is not a a valid asset ID.")

    # Get the user repository, a repository which contains almost all assets, and try to find the
    # asset description, a bundle of asset metadata, for the given asset ID in it.
    repo: maxon.AssetRepositoryRef = maxon.AssetInterface.GetUserPrefsRepository()
    if repo.IsNullValue():
        raise RuntimeError("Could not access the user repository.")
    
    asset: maxon.AssetDescription = repo.FindLatestAsset(
        maxon.AssetTypes.File(), aid, maxon.Id(), maxon.ASSET_FIND_MODE.LATEST)
    if asset.IsNullValue():
        raise RuntimeError(f"Could not find file asset for {aid}.")

    # When an asset description has been found, return the URL of that asset in the "asset:///"
    # scheme for the latest version of that asset.
    return maxon.AssetInterface.GetAssetUrl(asset, True)


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 = GetFileAssetUrl(maxon.Id("file_edb3eb584c0d905c"))
    urlTexSketch: maxon.Url = GetFileAssetUrl(maxon.Id("file_3b194acc5a745a2c"))

    # The node asset IDs for the two node types to be added in the example; the image node and the
    # blend 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.
    idImageNode: maxon.Id = maxon.Id("net.maxon.pattern.node.generator.image")
    idBlendNode: maxon.Id = maxon.Id("net.maxon.pattern.node.effect.blend")

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

    nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
    graph: maxon.GraphModelRef = nodeMaterial.AddGraph(maxon.Id("net.maxon.nodespace.standard"))
    if graph.IsNullValue():
        raise RuntimeError("Could not add standard graph to material.")

    # This default graph already contains two nodes, one of which is a BSDF node. We attempt to find
    # this node in the graph to include it in our setup.
    result: list[maxon.GraphNode] = []
    maxon.GraphModelHelper.FindNodesByAssetId(
        graph, maxon.Id("net.maxon.render.node.bsdf"), True, result)
    if len(result) < 1:
        raise RuntimeError("Could not find BSDF node in material.")
    bsdfNode: maxon.GraphNode = result[0]

    # 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() as transaction:

        # Add two texture nodes and a blend node to the graph.
        rustImgNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idImageNode)
        sketchImgNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idImageNode)
        blendNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idBlendNode)

        # Set the default value of the blend node blend mode port, i.e., the value the field/port 
        # has when no wire is connected to it. This is equivalent to the user setting the value to
        # "Darken" in the Attribute Manager.
        blendPort: maxon.GraphNode = blendNode.GetInputs().FindChild("blendmode")
        if blendPort.IsNullValue():
            raise RuntimeError("Could not find 'Blend' port.")
        blendPort.SetDefaultValue(maxon.Id("net.maxon.render.blendmode.darken"))

        # Do the same for the two image nodes, their 'File' ports, and the two texture URLs 
        # established above.
        urlRustTexPort: maxon.GraphNode = rustImgNode.GetInputs().FindChild("url")
        if urlRustTexPort.IsNullValue():
            raise RuntimeError("Could not find 'File' port.")
        urlRustTexPort.SetDefaultValue(urlTexRust)

        urlSketchTexPort: maxon.GraphNode = sketchImgNode.GetInputs().FindChild("url")
        if urlSketchTexPort.IsNullValue():
            raise RuntimeError("Could not find 'File' port.")
        urlSketchTexPort.SetDefaultValue(urlTexSketch)

        # Get the color output ports of the two texture nodes and the color blend node.
        rustTexColorOutPort: maxon.GraphNode = rustImgNode.GetOutputs().FindChild("result")
        sketchTexColorOutPort: maxon.GraphNode = sketchImgNode.GetOutputs().FindChild("result")
        blendColorOutPort: maxon.GraphNode = blendNode.GetOutputs().FindChild("result")

        # Get the fore- and background port of the blend node and the color port of the BSDF node.
        blendForegroundInPort: maxon.GraphNode = blendNode.GetInputs().FindChild("foreground")
        blendBackgroundInPort: maxon.GraphNode = blendNode.GetInputs().FindChild("background")
        bsdfColorInPort: maxon.GraphNode = bsdfNode.GetInputs().FindChild("color")

        # Wire up the two texture nodes to the blend node and the blend node to the BSDF node.
        rustTexColorOutPort.Connect(blendForegroundInPort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
        sketchTexColorOutPort.Connect(blendBackgroundInPort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
        blendColorOutPort.Connect(bsdfColorInPort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)

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

    # Insert the material into the document and push an update event.
    doc.InsertMaterial(material)
    c4d.EventAdd()
    
if __name__ == "__main__":
    main()

Result:
Screenshot 2023-01-09 at 17.47.46.png

Works great for standard materials, thanks!
Could you please do the same for a Redshift material?

Regards,
Pim

@pim Ok, I managed to do about the same in Python.
Create RS mat, RS node and connect nodes.

One question: in the C++ file you state
" // The port IDs which are used here can be discovered with the view/"Show ID" option in the
// dialog menu of the Node Editor. Selecting ports with this option enabled will show their
// ID in the info overlay in the bottom left corner of the Node Editor. "

However, I do not see it?

1326af97-261e-42c8-93d4-624265608712-image.png

Hey @pim,

it is mainly a question of updating the IDs, the game is more or less the same. The major difference is that Redshift port IDs are much longer than standard node space ones and that the URL of a RS Texture Node, the somewhat counter part of a standard node space Image Node, is expressed as a port bundle, a port within ports. Which is slightly more complicated to access. Find an example below.

Regarding the IDs: I wrote the RS example for S26.1 and things were a bit different then, find instructions in the code I have posted above for how to do it now:

# 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.

Screenshot 2023-01-10 at 19.35.10.png

Cheers,
Ferdinand

Result:
Screenshot 2023-01-10 at 19.34.37.png

Code:

#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 core material and material 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
    * (Asset API): Using texture assets in a node graph

"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2023 MAXON Computer GmbH"
__date__ = "09/01/2023"
__license__ = "Apache-2.0 License"
__version__ = "2023.0.0"


import c4d
import maxon

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

def GetFileAssetUrl(aid: maxon.Id) -> maxon.Url:
    """Returns the asset URL for the given file asset ID.
    """
    # Bail when the asset ID is invalid.
    if not isinstance(aid, maxon.Id) or aid.IsEmpty():
        raise RuntimeError(f"{aid = } is not a a valid asset ID.")

    # Get the user repository, a repository which contains almost all assets, and try to find the
    # asset description, a bundle of asset metadata, for the given asset ID in it.
    repo: maxon.AssetRepositoryRef = maxon.AssetInterface.GetUserPrefsRepository()
    if repo.IsNullValue():
        raise RuntimeError("Could not access the user repository.")
    
    asset: maxon.AssetDescription = repo.FindLatestAsset(
        maxon.AssetTypes.File(), aid, maxon.Id(), maxon.ASSET_FIND_MODE.LATEST)
    if asset.IsNullValue():
        raise RuntimeError(f"Could not find file asset for {aid}.")

    # When an asset description has been found, return the URL of that asset in the "asset:///"
    # scheme for the latest version of that asset.
    return maxon.AssetInterface.GetAssetUrl(asset, True)


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 = GetFileAssetUrl(maxon.Id("file_edb3eb584c0d905c"))
    urlTexSketch: maxon.Url = GetFileAssetUrl(maxon.Id("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.
    idTextureNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler")
    idMixNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix")
    idCoreMaterialNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.material")

    # Instantiate a material, get its node material and the 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()
    graph: maxon.GraphModelRef = nodeMaterial.AddGraph(
        maxon.Id("com.redshift3d.redshift4c4d.class.nodespace"))
    if graph.IsNullValue():
        raise RuntimeError("Could not add RS graph to material.")

    doc.InsertMaterial(material)
    c4d.EventAdd()

    # Attempt to find the core material node contained in the default graph setup.
    result: list[maxon.GraphNode] = []
    maxon.GraphModelHelper.FindNodesByAssetId(graph, idCoreMaterialNode, True, result)
    if len(result) < 1:
        raise RuntimeError("Could not find standard node in material.")
    standardNode: maxon.GraphNode = result[0]

    # 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() as transaction:
        # 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)

        # 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.SetDefaultValue(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.SetDefaultValue(urlTexRust)
        pathSketchPort.SetDefaultValue(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")
        coreDiffusePort: maxon.GraphNode = standardNode.GetInputs().FindChild(
            "com.redshift3d.redshift4c4d.nodes.core.material.diffuse_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(coreDiffusePort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)

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

    # Insert the material into the document and push an update event.
    doc.InsertMaterial(material)
    c4d.EventAdd()
    
if __name__ == "__main__":
    main()

Hi Ferdinand,
Great, thanks again!

One question though (maybe in another post?),
I see that in your code you often add the resulting type with an assignment.

For example
material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial) were
material = c4d.BaseMaterial(c4d.Mmaterial) could be enough.

I can see it is very helpfull.
Is it always necessary?

Regards,
Pim

Hey @pim,

What you see there is called type hinting, it is a feature of modern Python (>= 3.5). Type hinting appears in many places but is entirely optional. E.g., these two code snippets are functionally the same.

import c4d
import typing

# Here we use type hinting to declare the existence of a module attribute, it is actually defined
# by the C++ layer when it starts the VM on this script. This whole line is not technically 
# necessary (there is no = sign) and only there to make the code more readable and auto complete and
# linters to work.
doc: c4d.documents.BaseDocument

# Here the type hinting is used to define the signature of a function
def GetCount(collection: list[typing.Union[float, int]]) -> int:
    """Blah blah.
    """
    if not isinstance(collection, (list, tuple)):
        raise TypeError(f"{collection = }")

    # Type hinting for a variable within a scope.
    count: int = len(collection)
    return count

Is identical to:

import c4d

def GetCount(collection):
    """Blah blah.
    """
    if not isinstance(collection, (list, tuple)):
        raise TypeError(f"{collection = }")

    count = len(collection)
    return count

There are two reasons why we type hint in our examples.

  • It makes it easier for beginners to read the code, as you literally see what is what.
  • Type hinting will also usually give better auto complete results when using for example VS Code, as the IntelliSense engine then does better understand what entities are meant to be.

Type hinting is also a hallmark of modern Python I would say and will likely become more and more important with future versions of Python, as its typing ambiguity is both one of its major strengths and weaknesses. But the final verdict is there still pending, as there are also people who strongly dislike the idea of a quasi-statically typed CPython. But when you follow the recent releases of Python, type hinting and runtime type evaluation has been a big topic, and I would not be surprised if type hinting would be picked up again in the course of the planned performance improvements of Python starting with 3.11.

Cheers,
Ferdinand

Thanks for the good explanation.