Graph Descriptions Manual

Learn how to write scripts that generate and update node materials using the graph description syntax.

Note

See graph_descriptions in the script examples for graph description code examples.

Overview

Graph descriptions are a simplified approach to creating and updating node materials. Common use cases for graph descriptions are:

  • Creating new materials based on sets of textures and material metadata, as usually provided by material websites.

  • Updating parts of existing materials, as for example change the color profile in all textures in all materials in a scene.

Graph descriptions hide away some of the verboseness of the Nodes API when using identifiers and wiring up graphs by introducing lazy syntaxes and implicit wirings. A graph description will often require considerably less code than the equivalent Nodes API code. Graph descriptions are also more readable than the Nodes API code, especially when it comes to complex graphs with many nodes and connections. But graph descriptions are not meant to be a replacement for the Nodes API; for complex tasks and full access to nodes data and settings, the Nodes API will always be the only choice.

import math
import maxon
from maxon import GraphDescription

GraphDescription.ApplyDescription(
    GraphDescription.GetGraph(name="Hello World"), {
        GraphDescription.Type: "Output",
        "Surface": {
            GraphDescription.Type: "Standard Material",
            "Base/Color": maxon.Vector(1, 0, 0),
            "Base/Metalness": 1.0,
            "Reflection/IOR": 2 * math.pi
        }
    }
)
../_images/example_simple.png

Fig. 1: A simple graph description and its output, a red metallic material in the Redshift node space.

A graph description describes a graph as a set of nested Python dictionaries where nodes are listed in a right-to-left order. Usually we read a node graph left-to-right, we read from the input nodes to the singular output node of the graph, its so called end-node. A graph description flips this on the head and describes a graph in the opposite direction, starting out with the end node and cascading from there into its inputs. When we take the example from Fig.1, we would normally read this as Standard Material, Output. The graph description flips this on the head and describes the graph in the order Output, Standard Material.

A graph description is composed of a nested set of scopes where each scope describes a node in the graph. A scope is a Python dictionary that contains the node’s type, its input ports, attributes, and other settings. Ports can either be set to a literal value (5 or maxon.Vector(1, 0, 0)) or form a connection to another node by describing a new node as their value. Find below a commented version of the simple graph description shown in Fig. 1.

import math
import maxon
from maxon import GraphDescription

# A GraphDescription.ApplyDescription call must take at least two arguments:
GraphDescription.ApplyDescription(

    # 1. A graph to write the description to, in this case we use GetGraph to create a new node
    # material and get its graph for the currently active material node space; Redshift by default.

    GraphDescription.GetGraph(name="Hello World"),

    # 2. And the graph description to write into that graph. It follows the inverted, right-to-left
    # pattern mentioned before. Each node in the graph has its own scope (a pair of curly braces)
    # in the graph description. Connections between nodes in the graph are described by nesting
    # scopes.

    {
        # This outmost scope is of type "Output". It tells Cinema 4D that we want to create an
        # Output node. Instead of GraphDescription.Type, we could also write "$type".
        GraphDescription.Type: "Output",

        # We reference its 'Surface' input port to either set a value or connect it to another
        # node. In this case we crate a new node, indicated by opening a new scope (the set of
        # curly braces). Because most (material) nodes only have one output port, we do not have
        # to specify here that we want to connect to the 'outColor' port of the 'Standard Material'
        # node we are going to create, the output port is implicit.
        "Surface": {
            # Just as before, we declare the type of node this scope describes.
            GraphDescription.Type: "Standard Material",

            "Base/Color": maxon.Vector(1, 0, 0),
            "Base/Metalness": 1.0,
            "Reflection/IOR": 2 * math.pi
        }
    }
)

Technical Overview

The graph description API is part of the Nodes Framework. The relevant entities are:

Special Fields

The graph description syntax uses special fields that neither reference ports nor attributes of a node to convey metadata about node or graph. The most important of these fields are:

Note

This manual will use the string aliases for these special fields in the following examples.

Limitations

Graph descriptions are a new feature in the Cinema 4D Python API and have some limitations at the moment.

  • English is the only supported language for interface label identifiers at the moment.

  • Group nodes and scaffolds are not supported at the moment.

  • Graph queries currently do not support nested queries.

  • Graph descriptions do not support scene nodes at the moment.

  • There is currently a bug in Cinema 4D 2025.1.0 which causes graph descriptions upon execution to print their description values. This will be fixed in the next release.

  • Graph descriptions currently incorrectly use 0-based indexing in variadic port label references, opposed to the 1-based indexing most variadic ports use. This will also be fixed in the next release.

Basic Features

Getting Graphs

An important aspect of writing graph descriptions is to get the graph to write the graph description to. The function maxon.GraphDescription.GetGraph() is a convenance function to hide away some of the technical Nodes API details of that task. It can be used to both create and get a graph for a given scene element and node space. The type maxon.GraphDescription operates on graphs in the canonical Nodes API graph format maxon.NodesGraphModelRef, and you can pass any graph in this format to it. The example shown below demonstrates common use cases of the method maxon.GraphDescription.GetGraph().

import c4d
import maxon

from mxutils import CheckType

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

# The most common use case for GetGraph is to create a new material and graph from scratch.
# The call below will result in a new material being created in the active document with the
# name "matA". Since we are not passing an explicit node space, the currently active material
# node space will be used.
graphA: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(name="matA")

# This will create a material named "matB" that has a graph in the Redshift material node space,
# no matter what the currently active material node space is.
graphB: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
    name="matB", nodeSpaceId=maxon.NodeSpaceIdentifiers.RedshiftMaterial)

# But we can also use the function to create or get a graph from an existing material. The call
# below will either create a new Redshift graph for #material or return the existing one.
material: c4d.BaseMaterial = CheckType(doc.GetFirstMaterial())
graphC: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
    material, nodeSpaceId=maxon.NodeSpaceIdentifiers.RedshiftMaterial)

# We can also pass a document or an object for the first argument #element of GetGraph. If we
# pass a document, it will yield the scene nodes graph of that document, and if we pass an object,
# it will yield its capsule graph (if it has one).
sceneGraph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(doc)
# capsuleGraph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(doc.GetFirstObject())

# Finally, we can determine the contents of a newly created graph. We can either get an empty
# graph or the so-called default graph with a few default nodes as it would be created in
# Cinema 4D when the user creates a new graph for that space.
emptyGraph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(name="emptyMat", createEmpty=True)
defaultGraph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(name="defaultMat", createEmpty=False)

Referencing Entities

When describing a graph we must reference its entities such as nodes or their ports. Graph descriptions can reference these entities by their API identifiers as used in the Nodes API. In addition, graph descriptions can also reference entities by their interface labels as shown in the Node Editor. The following sections will explain both approaches and their advantages and disadvantages.

Label References

Other than the Nodes API, graph descriptions can reference node types, ports, and attributes by their interface labels. The evaluation of these references is lazy, meaning that you only must type out what is required to uniquely identify the entity.

The ‘Standard Material’ node shown in Fig. 2 has a port named ‘Color’ in its ‘Base’ category. Because there are other ports in this node that are also named Color, as for example the Reflection/Color port, we must at least include the attribute group ‘Base’ to reference the port as Base/Color. Note that the tabs seen at top of the Attribute Manager on the left (‘Basic’, ‘Preview’, ‘Base Properties’, etc) are also attribute groups.

If there was somewhere another port named ‘Color’ with an attribute group named ‘Base’, we would have to be even more specific in our reference and write Base Properties/Base/Color. On the other hand, we can reference entities such as the ‘Metalness’ port simply as Metalness because the label is unique in the ‘Standard Material’ node. When referencing node types itself, such as Standard Material for example, there is no such thing as attribute groups with which a label could be disambiguated.

There is no guarantee that all node types, ports, and attributes can be referenced by their label as some entities might be unresolvable ambiguous. In such case one must reference the entity by its identifier.

../_images/node_references.png

Fig. 2: Complex nodes such as the Redshift Standard Material node often have ambiguous port names where we must be more specific in our label references.

Note

Interface label identifiers are supported only in the English (“en-US”) language at the moment. Interface label identifiers can only be used for node type identifiers and port and attributes identifiers, but not port value identifiers. E.g., when you have a node with a port named “Type” whose value is a dropdown with the interface labels “a”, “b”, and “c”, you cannot set the value of the “Type” port by these interface label and instead must use their API identifiers. See Resource Editor for how to discover these identifiers.

import maxon

maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World"), {
        # This is a label reference to a node type.
        "$type": "Output",
        "Surface": {
            # This is also a label reference to a node type.
            "$type": "Standard Material",
            # This is a label reference to an input port. We need here the "Base/" because the
            # node has more than one "Color" port.
            "Base/Color": maxon.Vector(1, 0, 0),
            # This would be even more precise but is not required in this case.
            "Base Properties/Base/Color": maxon.Vector(1, 0, 0),
            # This is unambiguous because the "Metalness" port is unique in the node.
            "Metalness": 1.0,
            # This is not unambiguous, it will raise an error as there are many ports called
            # "Color" in this node.
            "Color": maxon.Vector(1, 0, 0)
        }
    }
)

Cinema 4D will tell you when a reference is ambiguous or not found in an error message. The line Color”: maxon.Vector(1, 0, 0) in the example above will for example raise the following error in the Python console of Cinema 4D.

...
RuntimeError: The node attribute reference 'Color' (space: 'com.redshift3d.redshift4c4d.class.nodespace',
lang: 'en-US', node: 'com.redshift3d.redshift4c4d.nodes.core.standardmaterial') is ambiguous.
# Here it lists all the identifiers which ambiguously match the given label reference.
Use a more precise reference or one of the IDs: { net.maxon.node.base.color, ... }
In:
    { <- Error in this scope ->
        Metalness: 1
        $type: Standard Material
        Base/Color: (1,0,0)
        Base Properties/Base/Color: (1,0,0)
        Color: (1,0,0)
    } [graphdescription_impl.cpp(167)]

Identifier References

But node types, ports, and attributes can also be referenced by their API identifier as commonly done in the Nodes API.

To discover such node identifiers, open the ‘Preferences’ menu from Edit/Preferences in the main menu of Cinema 4D. There select the ‘Node Editor’ category and enable the option ‘IDs’. When you select now a node in the Node Editor, you will see its identifiers in the selection info box in the lower left corner. The ‘Asset Id’ of a node is its type identifier, it must be used when a node is instantiated, in graph descriptions with the GraphDescription.Type field. The ‘Id’ of a node or port is the identifier for that specific instance of something. For nodes it can be used to reference an existing node in a graph or graph description, for ports it is used to reference a port in a node. To see the Id of a port, you must select the port in a node.

API identifiers in graph descriptions are always prefixed with a hash (#) so that they can be disambiguated from label references. Just as for label references, there are two ways to define identifiers:

  • Absolute: The full identifier of a node or port. This is the most precise and robust way to reference an entity; but it is also the most verbose form. The full identifier of the ‘Standard Material’ node in the Redshift node space is #com.redshift3d.redshift4c4d.nodes.core.standardmaterial. The full identifier of the ‘Color’ port in the ‘Base’ attribute group of the ‘Standard Material’ node is #com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color.

  • Lazy: A lazy identifier is a shortened version of an absolute identifier where we only write out what we deem relevant for identifying something. Lazy identifiers are signaled by the ~ character in front or the end of an identifier omitting information in that part. The absolute identifier #net.maxon.foo.bar could for example be written as the lazy identifiers #~.foo.bar. The tilde character acts here as a wildcard operator, declaring that we mean the node or port that ends in .foo.bar. The implication is that there are many lazy identifiers for each absolute one, e.g., we could also write #~.bar, #~ar, or #net.maxon.~. But this also means that we must ensure that a lazy identifier is unambiguous. It could for example very well be that there is another node or port that ends in ‘ar’, so #~ar is likely not a good identifier. Similarly, #net.maxon.~ is not a good lazy identifier, since literally all Nodes API entities published by Maxon Computer use this domain.

Cinema 4D will raise an error when a lazy identifier is ambiguous or has no match at all. Lazy port identifiers are matched in the context of the node they are used in and also split by in- and output ports. This means that one for example has only to worry about avoiding collisions between input ports of a node type when lazily referencing an input port of that node type.

../_images/node_identifiers.png

Fig. 3: Top - The place in the preferences where to enable displaying node identifiers. Bottom - Once enabled, the Node Editor will show the IDs and Asset IDs of selected nodes and ports.

Note

  • Absolute API identifiers are actually not identifiers but node paths, a concept from the Nodes API to describe entities in a graph tree. This is why absolute input port identifiers must start with #< and output port identifiers with #>, as the the < and > characters are used to denote the port type.

  • Nested ports are separated by slashes in API identifiers, e.g., #<parent_port/child_port.

  • Make sure to include leading dots in lazy identifiers, e.g., #~.base_color to avoid matching against foo.barbase_color. The same applies for nested ports, write #~/path and not #~path.

  • Lazy label and identifier ambiguity is resolved in context; an input port reference cannot collide with an output port reference and reference to ports of a node of type A cannot collide with the ports of a node of type B. Lazy references have therefore only to be unique in their local context.

Find below the ‘Hello World’ example written once with absolute API identifiers and once with lazy API identifiers.

import maxon

# The description with absolute identifiers, this gets hard to read quite quickly, especially when using Redshift.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World"), {
        # An output node.
        "$type": "#com.redshift3d.redshift4c4d.node.output",
        # Its surface input port.
        "#<com.redshift3d.redshift4c4d.node.output.surface": {
            # A standard material node.
            "$type": "#com.redshift3d.redshift4c4d.nodes.core.standardmaterial",
            # And its base color, metalness, and reflection IOR input ports.
            "#<com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color": maxon.Vector(1, 0, 0),
            "#<com.redshift3d.redshift4c4d.nodes.core.standardmaterial.metalness": 1.0,
            "#<com.redshift3d.redshift4c4d.nodes.core.standardmaterial.refl_ior": 1.5
        }
    }
)
import maxon

# The same description with lazy identifiers, this is much more readable. It is recommended
# to include leading dots in lazy identifiers, e.g., "#~.base_color" and not "#~base_color"
# to avoid matching against "foo.barbase_color".
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World"), {
        "$type": "#~.output",
        "#~.surface": {
            "$type": "#~.standardmaterial",
            "#~.base_color": maxon.Vector(1, 0, 0),
            "#~.metalness": 1.0,
            "#~.refl_ior": 1.5
        }
    }
)

Nested Ports

In the previous sections we only discussed ports and attributes that were direct children of a node. But the Nodes API has two concepts of ports which can hold ports: port bundles and variadic ports.

Types of Nested Ports

Port bundles are the representation of complex data types where a singular parent port, the data type, has multiple child ports, the fields of the data type. The characteristic of a port bundle is that it has a fixed number of children and that the user can both connect the whole data type parent port and its children individually.

Variadic ports are ports where the user can dynamically add and remove child ports with ‘+’ and ‘-‘ buttons in the node UI. Variadic ports are used to represent lists of multiple things of the same type, such as the points of a spline or the layers of a material. The characteristic of a variadic port is that it has a dynamic number of children and other than for a port bundle, the user cannot set all nested ports by connecting the parent port.

The different nested port types can be visually distinguished as follows:

  • Elements printed in bold which have elements parented to them, e.g., the ‘Image’ element in Fig. 4, are not ports at all but attributes groups as described in Label References.

  • Port bundles are identifiable by not being printed in bold and by ‘not’ having a ‘+’ and ‘-‘ buttons shown on them when hovered.

  • Variadic ports are identifiable by not being printed in bold and by having a ‘+’ and ‘-‘ buttons shown on them when hovered.

While the GUI for variadic ports is unified in the Node Editor, it is not in the Attribute Manager. Many nodes implement ‘Add’ and ‘Remove’ buttons for their variadic ports, e.g., the ‘Material’ node from the ‘Standard Renderer’ for its ‘Surface’ layers. But there is no guarantee to find them here, as exemplified by the ‘RS Scalar Ramp’ node which is using the spline GUI to manage its points.

An ‘RS Texture’ node has the ‘Filename’ port which is a port bundle (Fig. 4). The port is of datatype com.redshift3d.redshift4c4d.portbundle.file and we could find that data type by typing ‘file’ in the ‘Database’ field of the Resource Editor. If we would look up the RS Texture node in the Resource Editor, we would see that where we see a tree of multiple ports in the Node Editor, there is only a singular attribute/port defined in the resource which is of type portbundle.file.

An ‘RS Scalar Ramp’ node has the ‘Points’ port which is a variadic port (Fig. 5). We can distinguish it from a port bundle by the fact that we can add and remove ports as a user. Other than for a port bundle, where each child can be a port of a different data type, the children of a variadic port are all of the same data type. What we can also see at the Scalar Ramp example is also that variadic ports and port bundles often coexist. The ‘Ramp/Ramp’ port of the RS Scalar Ramp node is a port bundle of type net.maxon.render.portbundle.spline which contains a variadic port ‘Points’ which holds a list of port bundle ports of type net.maxon.render.portbundle.splinepoint.

../_images/port_bundle.gif

Fig. 4: An ‘RS Texture’ node has the ‘Filename’ port which is a port bundle.

../_images/variadic_port.gif

Fig. 5: An ‘RS Scalar Ramp’ node has the ‘Points’ port which is a variadic port.

Referencing Nested Ports

Nested ports references are composed with the / character as a delimiter between components, this applies to both label and API identifier references.

import maxon

# Applies a simple graph description that just creates a RS Texture node and sets the path of
# the texture.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(name="Referencing Port Bundles (Label)"),
{
    "$type": "Texture",

    # We set the #Path port of the #Filename port bundle to the URL of an asset. 'General' and
    # 'Image' are both not ports but attribute groups and this is the most verbose form of a
    # label reference.
    "General/Image/Filename/Path": maxon.Url("asset:///file_eb62aff065c50a2b"),

    # These would all also work due to the ability of graph descriptions to lazily resolve label
    # references and the fact that in this case each of them unambiguously identifies an input port
    # in an RS Texture node.
    "Image/Filename/Path": maxon.Url("asset:///file_eb62aff065c50a2b"),
    "Filename/Path": maxon.Url("asset:///file_eb62aff065c50a2b"),
    "Path": maxon.Url("asset:///file_eb62aff065c50a2b"),
})

For API identifier references things are very similar:

import maxon

# Applies a simple graph description that just creates a RS Texture node and sets the path of
# the texture.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(name="Referencing Port Bundles (ID)"),
{
    "$type": "Texture",

    # This is the absolute API identifier reference for the #Path port in the #Filename port bundle,
    # where:
    #
    #   #                                        denotes a description identifier reference
    #   <                                        denotes an input port
    #   com.redshift3d...texturesampler.tex0     denotes the #Filename port bundle
    #   /                                        denotes a child element
    #   path                                     denotes the #Path port of the #Filename port bundle
    "#<com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0/path": maxon.Url(
        "asset:///file_eb62aff065c50a2b"),

    # We could also use lazy identifiers to shorten this, although I would not recommend using the
    # last variant as it is very ambiguous for human readers despite being technically unambiguous.
    "#~.texturesampler.tex0/path": maxon.Url("asset:///file_eb62aff065c50a2b"),
    "#~.tex0/path": maxon.Url("asset:///file_eb62aff065c50a2b"),
    "#~/path": maxon.Url("asset:///file_eb62aff065c50a2b"),
})

There is currently not a direct way to get hold of the full API identifier path of a nested port in the Node Editor. One must inspect the parents of a port and manually combine the components as shown in Fig. 6. This can be a bit cumbersome but can be offset by the ability of graph descriptions to lazily reference ports, so that we do not have to always resolve the full chain.

Variadic ports are bit more complicated as they are completely unbound when it comes to naming and IDing their dynamically created child ports.

In general, most variadic ports follow the ID assignment pattern _0, _1, _2, _3, … where each new port is consecutively named. But there is unfortunately no guarantee that every node will always ID new variadic port children like this. The Standard Renderer ‘Material’ node will for example come with a default ‘Surface’ layer with the ID 1 and then start naming new children after that _0, _1, _2, _3, …, effectively offsetting the ID of each node by one. Analogously there could be nodes which do not follow this ID pattern at all, or decide to break it when some condition X is met. You should always assure that adding N variadic ports to a node will result in the IDs you think it would.

Even more unpredictable are variadic port labels. Their default pattern is {Datatype Name}.{index}, e.g., Point.2 for a second point in a list of points. But there is no guarantee that all nodes follow this pattern, as each can implement it manually, and secondly, the name of a variadic port can change at runtime. When we again take the ‘Material’ node of the Standard Renderer as an example, we can see that the name of its ‘Surface’ variadic port children changes, based on the to which port the node is connected to. By default the port names are ‘BSDF Layer.1’, ‘BSDF Layer.2’, ‘BSDF Layer.3’ and so on, but when we then for example connect a BSDF Diffuse node, they will change their name to ‘Diffuse’.

  • Graph descriptions cache port labels by their datatype-index pattern.

  • Graph descriptions cannot predict to which node one might want to connect the port to and which port name that would entail. As this would require building and executing the graph which is not possible when the very purpose of graph descriptions is to do exactly that.

  • It is recommended to use (lazy) API identifier reference for variadic ports when problems occur.

Warning

The graph description API is currently using a zero-based indexing instead of a one-based indexing for variadic port label references. E.g., what is ‘Point.1’ in the UI is ‘Point.0’ in a description. This will be fixed in the next release.

../_images/combine_paths.gif

Fig. 6: To find the full API identifier of the ‘RS Texture’ ‘Filename/Path’ port, we must inspect two elements, the port bundle ‘Filename’ and its child ‘Path’ to combine their individual identifiers into the full path.

../_images/variadic_mess.gif

Fig. 7: The ‘Material’ node of the ‘Standard Render’ is a good example for how convoluted variadic port IDs and their labels can be.

The syntax for addressing children of variadic ports is then the same as for port bundles, the references then often are quite complex as there are often multiple levels of nesting.

import maxon

# Applies a simple description that adds an RS Scalar Ramp node to the graph.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(name="Referencing Variadic Ports (Label)"),
{
    "$type": "Scalar Ramp",

    # Sets the Y position of the 0th point (what is shown as Point.1 in the UI). The decomposed
    # reference is:
    #
    #   Ramp            denotes the attribute group 'Ramp' in the node.
    #   Ramp            denotes the port bundle #Ramp of type 'net.maxon.render.portbundle.spline'.
    #   Points          denotes the variadic port #Points in that port bundle which holds a list
    #                    of 'net.maxon.render.portbundle.splinepoint'.
    #   Point.0         denotes the first 'splinepoint' port bundle held by the variadic port.
    #   Position Y      denotes one of the fields of a 'splinepoint' port bundle.
    "Ramp/Ramp/Points/Point.0/Position Y": 1.0,

    # For good measure we now write a few more values and at the same time demonstrate that we can
    # also use lazy references here.
    "Point.0/Position Y": 1.0,
    "Point.0/Interpolation": "linear",
    "Point.1/Position Y": 0.0,
    "Point.1/Interpolation": "linear"
})

For API identifier references, things again are very similar to what we did for port bundles:

import maxon

# Applies a simple description that adds an RS Scalar Ramp node to the graph.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(name="Referencing Variadic Ports (ID)"),
{
    "$type": "Scalar Ramp",

    # Sets the Y position of the 0th point. The decomposed reference is:
    #
    #   #<                          denotes an input port graph description ID reference.
    #   com....rsscalarramp.ramp    denotes the port bundle #Ramp of type 'net.maxon.render.portbundle.spline'.
    #   points                      denotes the variadic port #Points in that port bundle which holds a list
    #                                of 'net.maxon.render.portbundle.splinepoint'.
    #   _0                          denotes the first 'splinepoint' port bundle held by the variadic port.
    #   positiony                   denotes one of the fields of a 'splinepoint' port bundle.
    "#<com.redshift3d.redshift4c4d.nodes.core.rsscalarramp.ramp/points/_0/positiony": 1.0,

    # For good measure we now write a few more values and at the same time demonstrate that we can
    # also use lazy references here.
    "#~_0/positiony": 1.0,
    "#~_0/interpolation": "linear",
    "#~_1/positiony": 0.0,
    "#~_1/interpolation": "linear",
})

Note

See Setting Port Counts for an overview of how to change the number of variadic ports a node has.

Building Graphs

The last basic concept of graph descriptions besides getting a graph and identifying entities in that graph is to describe relations in that graph by setting values of nodes or wiring up multiple nodes. Graph descriptions use here multiple concepts bring a graph into a written form but at the same time keep that description compact.

Scopes and Literals

Each node in a graph description is described by a scope. A scope is a Python dictionary (denoted by curly braces, e.g.,`{ “a”: 1, “b”: True }`) that describes the type of a node, its input ports, attributes, and other settings. To connect two nodes, a new scope is nested inside the scope of the node to connect to. A side effect of this is that graph descriptions construct a graph from its output to its inputs and not from the inputs to the output as we would usually read a graph.

A scope describes its node as key-value pairs. The scope {“Base/Color”: maxon.Color(1, 0, 0)} for example has the key Base/Color which describes an input port by its label and a value that is the color red. Here we have assigned a literal value to the port, which means that we did not construct another node to set the value of the port, but set it directly. But we could also assign a new scope to the port and with that to connect it to another and new node. This is done by opening a new scope inside the scope of the node to connect to. The scope {“Base/Color”: {“$type”: “Texture”}} for example would connect the ‘Color’ port of the described node to a new node of type ‘Texture’.

When we look at the nested scope in {“Base/Color”: {“$type”: “Texture”}} we can see an unusual key named $type. It is an alias for maxon.GraphDescription.Type and is used to declare the mandatory type of the node a scope describes. There are multiple special keys that can appear in a scope and they are all prefixed by a $.

Note

Use Partial Descriptions when you want to construct graphs that do not terminate into a singular node.

../_images/reading_direction.png

Fig. 8: Graph descriptions have an inverted reading direction compared to the Node Editor, the outmost node described is the node all other nodes are connected to.

Referenced Nodes

Another special key that can appear in a scope is $id which is used to set the identifier of a node in a graph. When we want to connect an existing node in a graph description, we can pass its identifier prefixed by the hash character as the value of the port where we want to connect the node to. The scope {“$type”: “Texture”, $id”: “mytex”} defines a node of type ‘Texture’ with the identifier mytex. We can now connect this texture node to the color port of a another node by writing {“Color”: “#mytex”}. Node identifiers must be unique in a graph to work correctly and a graph description must define a node and its identifier before it is referenced.

Note

Referenced nodes are the more complex feature of Graph Queries in disguise.

../_images/example_scopes.png

Fig. 9: This graph is created by the ‘Scopes and Values Example’ shown below, it demonstrates the use of scopes and values in graph descriptions, as well as the use of the $id field to reference nodes in a graph description.

The example shown below creates a more complex setup than the simple examples shown before, demonstrating the inverted structure a graph description uses to describe a graph compared to our reading habits in the Node Editor. Shown is also the use of the $id field to reference a node again in a graph description.

import maxon

maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Scopes and Values Example"), {
    "$type": "Output",
    # The surface input port of the output node does not have a literal value but describes
    # its value with a nested scope (the {}).
    "Surface -> outColor": {
        # Directly described is a standard material node.
        "$type": "Standard Material",
        # The metalness port is set as a literal value, here we do not open a new scope.
        "Base/Metalness": 1.,
        # But the base color is not a literal but connected to a texture node, we open a new scope.
        "Base/Color": {
            "$type": "Texture",
            # We set a literal value to the port-bundle child port Path by referencing an asset
            # by its asset URL. If we wanted to reference a file on disk, we would use the "file"
            # scheme, e.g., "file:///C:/path/to/file.png".
            "Image/Filename/Path": maxon.Url("asset:///file_eb62aff065c50a2b"),
            # The scale of the texture is again not a literal but connected to a value node.
            "Scale": {
                "$type": "Value",
                # Here we do something new, we set the id of this node in the graph so that we
                # can later reference it as #texTransform.
                "$id": "texTransform",
                "Input": maxon.Color(2, 2, 1)
            }
        },
        # The reflection roughness of the Standard Material node, we use another texture here.
        "Reflection/Roughness": {
            "$type": "Texture",
            "Image/Filename/Path": maxon.Url("asset:///file_8bf49bb0b9992dbe"),
            # Here is something new again, we neither set a literal value nor do we define
            # a new node/scope to set the value of this Scale port. We reference the node with
            # the id #texTransform as the value of the scale port. This will result in both
            # ports being connected to the same value node.
            "Scale": "#texTransform"
        },
        # The Bump Map port of the Standard Material node, we connect a Bump Map node
        # connected to a texture node reusing our #texTransform value node again.
        "Geometry/Bump Map": {
            "$type": "Bump Map",
            "Input Map Type": 0,
            "Height Scale": .2,
            "Input": {
                "$type": "Texture",
                "Image/Filename/Path": maxon.Url("asset:///file_e706df60a3a533d2"),
                "Scale": "#texTransform"
            }
        }
    }
})

Explicit Connections

You might have noticed by now that the node-connecting pattern {“Some Port”: { “$type”: “Some Node” }} somehow omits the information of which ports exactly are connected between the inner and outer scope. As only the input port of the outer scope is named, Some Port in this case, but not the output port of the inner scope; Some Node in this case.

This is another short hand syntax of graph descriptions which deliberately makes the output port name implicit. Because many nodes have only a singular output port, which makes the information where to connect to unnecessary. In cases where multiple output ports exist, we must specify the output port we want to connect to by using the -> syntax to describe what to connect; e.g., {“Input Port -> Output Port”: { #Node of “Output Port” }}. Just a graph descriptions in general, explicit connections follow an inverted order for consistency; one must connect an input to an output and not the other way around.

import maxon

# The standard "Hello World" graph description shown in many examples before.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World (Explicit Connections)"), {
        "$type": "Output",
        # We describe the only connection in the graph explicitly. We connect the
        # StandardMaterial.outColor port to the Output.Surface port.
        "Surface -> outColor": {
            "$type": "Standard Material",
            "Base/Color": maxon.Vector(1, 0, 0),
            "Base/Metalness": 1.0,
        }
    }
)

Advanced Features

Lines out features which are required by advanced users and can be ignored when taking the first steps with graph descriptions, or when wanting to reduce the complexity of graph descriptions in general. But some features such as Graph Queries and Graph Commands will be sooner or later required by all users who want to use graph descriptions beyond their most simple form.

Partial Descriptions

Unlike introduced in earlier sections of this manual, a graph description is actually not a singular Python dictionary but a list of dictionaries. A singular dictionary in a graph description is called a partial description. When a user passes only singular dictionary, a partial description, to ApplyDescription as shown in previous examples, it will be automatically wrapped by a list by the API. But we can also pass a list of partial descriptions ourself as this can help with:

  • Structuring a graph description in a more readable manner. Imagine having a subgraph whose output is used in multiple places in a graph. We can realize such setup as shown here before by defining the subgraph the first time we need it in the main graph and then use its identifier in the remaining places. With partial descriptions we can first define the subgraph in its own partial description and then always just reference it in the main graph description.

  • Modifying existing graphs. When we want to modify an existing graph, we often want to make multiple modifications at once. Partial descriptions allow us to run multiple queries on a graph in a row and modify it with each query.

  • Adding multiple sub-graphs not connected to a singular end-node in the graph. Imagine for example preloading a material with a handful of texture setups to be manually wired into a more complex setup by an artist.

Note

  • Partial descriptions are a convenance feature and will entail exactly the same result as applying multiple graph descriptions to the same graph. But partial descriptions will be slightly to considerably more performant, based on the complexity of descriptions.

  • Unlike shown in previous examples, graph descriptions do not have to form a valid graph. As shown in this example, graph descriptions can also produce graphs with disjunct nodes or other configurations which do not result in a valid graph with valid output.

../_images/example_partial.png

Fig. 10: The output of the simple partial description example shown below. Each Color node has been added by a separate partial description, and only the fourth partial description created the two Mix Color nodes and wired up all nodes.

import maxon

maxon.GraphDescription.ApplyDescription(
    # A graph description actually consists of a list partial descriptions applied to a graph.
    maxon.GraphDescription.GetGraph(name="Partial Descriptions"), [
    # First we have three partial descriptions that add the Color nodes for red, green and blue to the graph.
    {
        "$type": "Color",
        "$id": "red",
        "Inputs/Color": maxon.ColorA(1, 0, 0, 1)
    },
    {
        "$type": "Color",
        "$id": "green",
        "Inputs/Color": maxon.ColorA(0, 1, 0, 1)
    },
    {
        "$type": "Color",
        "$id": "blue",
        "Inputs/Color": maxon.ColorA(0, 0, 1, 1)
    },
    # If we would end our description here, we would just have added three Color nodes to the graph. But we
    # add a fourth partial description to add the two Color Mix nodes and wires up all five nodes.
    {
        "$type": "Color Mix",
        "Mix Amount": 0.5,
        "Input 1": "#red",
        "Input 2": {
            "$type": "Color Mix",
            "Mix Amount": 0.5,
            "Input 1": "#green",
            "Input 2": "#blue",
        },
    }]
)

# The former example could also be written as four #ApplyDescription calls and will result in exactly
# the same graph. This shows also what might have been obfuscated a bit by earlier simpler examples:
# #ApplyDescription indeed applies a description to a graph and can very well build on existing
# content in a graph. This is an important fact for understanding what graph queries do.
graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(name="Singular Descriptions")

# Add the three Color nodes in their own #ApplyDescription call each.
maxon.GraphDescription.ApplyDescription(
    graph, {
        "$type": "Color",
        "$id": "red",
        "Inputs/Color": maxon.ColorA(1, 0, 0, 1)
    }
)

maxon.GraphDescription.ApplyDescription(
    graph, {
        "$type": "Color",
        "$id": "green",
        "Inputs/Color": maxon.ColorA(0, 1, 0, 1)
    }
)

maxon.GraphDescription.ApplyDescription(
    graph, {
        "$type": "Color",
        "$id": "blue",
        "Inputs/Color": maxon.ColorA(0, 0, 1, 1)
    }
)

# And finally wire everything together in a final call. We could also have done the same with a
# material graph loaded from disk which contained these three disjunct color Nodes.
maxon.GraphDescription.ApplyDescription(
    graph, {
        "$type": "Color Mix",
        "Mix Amount": 0.5,
        "Input 1": "#red",
        "Input 2": {
            "$type": "Color Mix",
            "Mix Amount": 0.5,
            "Input 1": "#green",
            "Input 2": "#blue",
        },
    }
)

Graph Queries

Graph queries select one or multiple existing parts in a graph with the goal to reuse or modify these parts. The node references which were already introduced in the section Referenced Nodes are an implicit form of graph queries. When we have the following section in a graph description,

...
"Reflection/Roughness": {
    "$type": "Texture",
    ...
    # Assigning the value "#texTransform" to
    # this port is an implicit graph query.
    "Scale": "#texTransform"
}, ...

then the reference of the ‘Scale’ port to a node with the identifier #texTransform (which in this example is defined somewhere else) is an implicit graph query. A graph query is resolved when the graph description is applied to a graph, and in this case, the query will attempt to match a node in the graph that has the identifier #texTransform and connect the output port of that node to the ‘Scale’ port. But what we see in this case is only a shorthand form for graph queries for the special case of referencing a node by its identifier.

../_images/example_query.png

Fig. 11: The code example shown at the end of this section uses a graph query to all select texture nodes in a material that use the file_2e316c303b15a330 asset. When the example is applied to the output of the ‘Callback’ example, it will only set the gamma value of the reflection roughness texture because only it matches the query.

Internally, the value of the ‘Scale’ port will be expanded into a query scope as shown below. We could also write each identifier reference to a node like this, it would have the same outcome as the shorthand form shown before.

...
"Reflection/Roughness": {
    "$type": "Texture",
    ...
    # A new scope is opened to describe a node to connect this Scale port to.
    "Scale": {
        # But instead of setting the type of a new node with the "$type" field, a "$query" field
        # is used to search for a node with certain properties. The scope containing the query
        # will then act as if it was the node matched by the query.
        "$query": {
            # The query will match a node with the ID "texTransform".
            "$id": "texTransform"
        }
    }
}, ...

A graph query is indicated by a scope which does not have the otherwise mandatory $type field and instead a $query field. Assigned to that $query field is then a scope which describes the to be searched node(s). Graph queries can be run both for nodes existing in a graph before a graph description has been applied and nodes that have been created by the currently executed graph description.

Note

Queries currently do not support nested queries where a query scope contains nested scope to define nodes a queried node is connected to. Queried can be at the moment only literal values of a node. The query flag QUERY_FLAGS.SELECT_THIS is intended for nested queries and unused at the moment.

Graph queries can define the field $qmode (or QueryMode) to specify how the query should be carried out. The same query can have vastly different outcomes based on the query modes which are applied.

  • QUERY_FLAGS.NONE: Realizes an ‘exactly once’ query where the queried node must appear exactly once in the graph. Any deviation from that requirement, e.g., a graph with either no or multiple nodes matching the query, will result in an error by default. It is useful when one wants to write descriptions that operate on graphs of exactly known structure, and query mismatches indicate bugs in the query or the input data which should be signaled by an error. This is the implicit default mode when no query mode is set.

  • QUERY_FLAGS.MATCH_FIRST: Realizes a ‘first match’ query, matching the first node in the graph that matches the query. The graph tree is evaluated depth first and will match any graph with 1 … N nodes matching the query. Will raise an error by default on graphs which do not hold a matching node. Only in very specific scenarios useful.

  • QUERY_FLAGS.MATCH_ALL: Realizes an ‘at least once’ query, matching all nodes in the graph that match the given query. Will raise an error by default when no node in the graph matches the query.

  • QUERY_FLAGS.MATCH_MATCH_MAYBE: Marks a query as lazy, making a non-match not an error. The affected non-matching description scopes are then simply not executed. Is usually used alongside other query modes, e.g., $qmode”: MATCH_ALL | MATCH_MATCH_MAYBE would for example match zero, one, or many nodes in a graph. Can be useful to carry out modifications on graphs where one is not sure about their content.

  • QUERY_FLAGS.SELECT_THIS: Marks a selection in a deep query. Currently not implemented.

import c4d
import maxon

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

# For each material in the document which has a Redshift material node space.
for material in doc.GetMaterials():
    nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
    if not nodeMaterial.HasSpace(maxon.NodeSpaceIdentifiers.RedshiftMaterial):
        continue

    # We use here CrateGraph to get the existing Redshift material graph from #material.
    graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
        material, maxon.NodeSpaceIdentifiers.RedshiftMaterial)

    # We query the graph for all nodes which are of type "Texture" and have the asset file
    # "file_2e316c303b15a330" set as their image file ...
    maxon.GraphDescription.ApplyDescription(
        graph, {
            "$query": {
                # Here we define that this query scope is meant to match everything ...
                "$qmode": (maxon.GraphDescription.QUERY_FLAGS.MATCH_ALL |
                           maxon.GraphDescription.QUERY_FLAGS.MATCH_MATCH_MAYBE)
                # ... that has the properties defined here ...
                "$type": "Texture",
                "Image/Filename/Path": maxon.Url("asset:///file_2e316c303b15a330")
            },
            # ... so that we can set the gamma value of all matching nodes to 2.2.
            "Image/Custom Gamma": 2.2
        }
        # Because we defined our query also as #MATCH_MATCH_MAYBE, we can run this script blindly
        # on any file. And for graphs where is no match, it will simply silently do nothing to the
        # materials. When we would define the query only as #MATCH_ALL, the script would raise
        # an error on graphs which do not hold any matching textures. And if we would define it
        # as NONE | MATCH_MATCH_MAYBE (which is the same as just MATCH_MATCH_MAYBE), we would
        # match graphs which either contain no matching texture at all, or exactly one, but raise
        # an error on graphs where the query would match many nodes.
    )

Graph Commands

Graph commands provide a set of predefined actions that can be carried out on a graph, such as deleting nodes, adding ports, or hiding ports. Graph commands are similar to the in Python API unexposed Node Functions of the Nodes API but an entirely separate feature. Graph commands rely conceptually on Graph Queries and Partial Descriptions, as both will often happen in the context of graph commands. For an in depth understanding of graph commands, it is therefore recommended to familiarize oneself with these concepts first.

Graph commands are set with the field $commands (or Commands) in the scope of a node. Adding the key-value pair “$commands”: “$cmd_hide_ports” to the scope of a node, would for example hide all ports of the node that are not part of a connection. Graph commands are context insensitive, just as the rest of graph descriptions. This means that it does not matter where in the scope of a node a command is placed, e.g., both following command invocations are equivalent:

...
# Hides all the ports of the Texture node that are not part of a connection.
"Reflection/Roughness": {
    "$type": "Texture",
    ...
    "Scale": "#texTransform",
    "$commands": "$cmd_hide_ports"
}, ...
...
# Does exactly the same as the previous example.
"Reflection/Roughness": {
    "$commands": "$cmd_hide_ports",
    "$type": "Texture",
    ...
    "Scale": "#texTransform"
}, ...

A graph command is in most cases applied right after its node has been created or found via a query but before the rest of its scope has been executed. This is important to understand, as for example defaulting a node with the command DefaultCommand and then setting values in the same scope will not result in a defaulted node, but a node which has been defaulted and then had the respective values been set.

Warning

Although already listed in the API $cmd_group and $cmd_ungroup are currently not yet functional.

Defaulting Nodes

The command $cmd_default (or DefaultCommand) will reset the values of all or the given ports and attributes of a given node. It can be useful when loading existing scenes to clean out parts of a graph before writing new values.

../_images/cmd_default.png

Fig. 12: The output of the ‘Hello World (Default)’ example shown below. Because the Standard Material node is reset to its default values in the second part of the graph description, the resulting material does not have the red metallic look as the original ‘Hello World’ example.

import maxon

# A two part graph description, where the first part creates the 'Hello World' graph, and the
# second part resets the Standard Material in that graph to its default values.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World (Default)"), [
    # Create the 'Hello World' graph.
    {
        "$type": "Output",
        "Surface": {
            "$type": "Standard Material",
            "$id": "material",
            "Base/Color": (1, 0, 0),
            "Base/Metalness": 1.0,
            "Reflection/IOR": 3.14159,
        }
    },
    # Carry our $cmd_default on #material.
    {
        "$query": "#material",
        "$commands": "$cmd_default"
    }
])

Disconnecting Nodes

The command $cmd_disconnect (or DisconnectCommand) will disconnect all or the given ports of a given node. It can be useful to rewire existing node graphs.

../_images/cmd_disconnect.png

Fig. 13: The output of the ‘Hello World (Disconnect)’ example shown below. Because the Standard Material node is being disconnected in the second part of the graph description, we end up with a graph with two disjunct nodes.

import maxon

# A two part graph description, where the first part creates the 'Hello World' graph, and the
# second part disconnects the Standard Material node in that graph from the rest of the graph.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World (Default)"), [
    # Create the 'Hello World' graph.
    {
        "$type": "Output",
        "Surface": {
            "$type": "Standard Material",
            "$id": "material",
            "Base/Color": (1, 0, 0),
            "Base/Metalness": 1.0,
            "Reflection/IOR": 3.14159,
        }
    },
    # Carry our $cmd_disconnect on #material.
    {
        "$query": "#material",
        "$commands": "$cmd_disconnect"
    }
])

Showing/Hiding Ports

The commands $cmd_show_ports (or ShowPortsCommand) and $cmd_hide_ports (or HidePortsCommand) will show or hide all or the selected ports in a node.

The show/hide commands are always executed before the rest of the scope of the node is executed, which means that actively set connections will be shown even if the port is set to be hidden, as connecting two ports will make them visible again. This is a dedicated behavior of graph descriptions which aligns with the end user facing nature of graph descriptions. In the Nodes API one can also hide ports that are part of a connection or connect hidden ports without making them visible.

../_images/cmd_show_ports.png

Fig. 14: The output of the ‘Hello World (Show/Hide Ports)’ example shown below. The Standard Material node only shows the one input and one output port that are actively used in the graph and the Noise node shows all its ports.

import maxon

# A slightly changed "Hello World!" graph which connects a Noise node to the Reflection Color
# port of a Standard Material, hides all ports of the Standard Material, and shows all ports of
# the Noise node.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World (Show/Hide Ports)"), [
    {
        "$type": "Output",
        # A Standard Material node connected to the "Surface" port of the Output node where all
        # ports are hidden in the Standard Material except for the port connections we explicitly
        # describe here ('outColor' and 'Reflection/Color'). Note that the explicit out port
        # reference in 'Surface -> outColor' is not necessary, we just use it to make the example
        # more verbose. Also note that just setting values without a connection (as in 'Base/Color')
        # will not cause a port to be shown when all ports have been hidden before.
        "Surface -> outColor": {
            "$type": "Standard Material",
            "$commands": "$cmd_hide_ports",

            "Base/Color": (1, 0, 0),
            # A Noise node connected to the 'Reflection/Color' of the Standard Material where all
            # ports are shown. This will affect both input and output ports. But this will have
            # no effect on the number of variadic ports a node has.
            "Reflection/Color": {
                "$type": "Noise",
                "$commands": "$cmd_show_ports"
            }
        }
    }
])

Removing Nodes

The command $cmd_remove (or RemoveCommand) will remove the node it is applied to from its graph, permanently deleting the node.

Note

Consider Disconnecting Nodes instead of removing them to avoid to removing data you might later regret to have removed. Functionally, disconnecting a node will result in the same nodes graph output as removing it; as a disconnected node will become an orphaned node in the graph which does not affect the output of the graph.

../_images/cmd_remove.png

Fig. 15: The output of the ‘Hello World (Remove)’ example shown below. Because we remove the Standard Material node in the second part of the graph description, we end up with a graph with a singular Output node.

import maxon

# A two part graph description, where the first part creates the 'Hello World' graph, and the
# second part deletes the Standard Material from that graph, leaving only the the Output node
# in the graph. This example is a bit clinical, as it makes usually little sense to first
# create a node, and then delete it again within a singular graph description. This command
# is intended to be used in the context of graphs loaded from a material stored in an existing
# scene.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Hello World (Remove)"), [
    # Create the 'Hello World' graph.
    {
        "$type": "Output",
        "Surface": {
            "$type": "Standard Material",
            "$id": "material",
            "Base/Color": (1, 0, 0),
            "Base/Metalness": 1.0,
            "Reflection/IOR": 3.14159,
        }
    },
    # Carry our $cmd_remove on #material.
    {
        "$query": "#material",
        "$commands": "$cmd_remove"
    }
])

Setting Port Counts

The command $cmd_set_port_count (or SetPortCountCommand) will set the number of children a specific or all variadic ports of a node have. The command can be both used to increase and decrease the number of ports. It is currently not possible to insert new ports at a specific position or remove ports at a specific position.

Note

See Nested Ports for an introduction into what ‘variadic ports’ are.

../_images/cmd_set_port_count.png

Fig. 16: The output of the ‘Set Variadic Port Count: 3’ example shown below. Because we change the variadic port count of the spline from its default of two to three, we can define a smoother transition.

import maxon

# Applies a simple description that adds an RS Scalar Ramp node to the graph.
maxon.GraphDescription.ApplyDescription(
maxon.GraphDescription.GetGraph(name="Set Variadic Port Count: 3"),
{
    "$type": "Scalar Ramp",

    # Explicitly set the point count of the ramp to three (adding one point from its default of
    # two). #cmd_set_port_count will only add or remove as many points as necessary. We can there
    # also use it to ensure that port has exactly N children.
    "$commands": {
        "$cmd_set_port_count": { "Ramp/Ramp/Points": 3 }},

    # We set the three points to where we want them to be and the desired interpolation mode. We
    # are using here lazy references, the absolute reference for the first points Y position
    # would be "Ramp/Ramp/Points/Point.0/Position Y".
    "Point.0/Position X": 0.0,
    "Point.0/Position Y": 0.0,
    "Point.0/Interpolation": "cubic",
    "Point.1/Position X": 0.5,
    "Point.1/Position Y": 0.25,
    "Point.1/Interpolation": "cubic",
    "Point.2/Position X": 1.0,
    "Point.2/Position Y": 1.0,
    "Point.2/Interpolation": "cubic",
})

# We can also use the same syntax to remove points. Here we will end up with constant 50% grey ramp.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Set Variadic Port Count: 1"),
    {
        "$type": "Scalar Ramp",
        "$commands": {
            "$cmd_set_port_count": { "Ramp/Ramp/Points": 1 },
        },

        "Point.0/Position Y": 0.5
    }
)

# There is also a shorthand syntax which sets the number of ports for all variadic ports of node
# which due to the fact that a "Scalar Ramp" node only has one variadic port is exactly the same
# as the former call.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Set Variadic Port Count: 1"),
    {
        "$type": "Scalar Ramp",
        "$commands": {
            "$cmd_set_port_count": 1,
        },

        "Point.0/Position Y": 0.5
    }
)

Description Callbacks

Graph description callbacks are another way to modify graphs in a programmatic way. Other than Graph Commands, which are carried out on the final node graph, description callbacks are carried out on the description itself. This can be useful to set repetitively set properties of a node, as for example coloring in all nodes of a certain type in a certain color.

Description callbacks are carried out by passing a callable as the argument callback to maxon.GraphDescription.ApplyDescription(). The callable must follow the signature SomeName (data: dict) -> dict. The callable will be called for each scope in the description, allowing it to return a modified version. This can be useful when many nodes in a graph share attribute or port values that otherwise would be tedious to set every time by hand.

Note

Although not shown in the example, the callable can also add or remove scopes, i.e., nodes, to the passed scope. Such added scopes will then be passed through the callable as any other. Be careful not to create infinite recursion when adding scopes via a callable.

../_images/example_callback.png

Fig. 16: In this graph, the preview of all nodes has been hidden and the title bar of all ‘Texture’ nodes has been colored red using a callback function.

The code example shown below uses a callback function to toggle off the preview of nodes and color in “Texture” node title bars. The callback function is called for each node/scope in the graph description. It is used to modify the description before it is applied to the graph. The callback function is passed to maxon.GraphDescription.ApplyDescription() as the argument callback.

import maxon

def MyCallback(scope: dict) -> dict:
    """Realizes a callback function which is called by #ApplyDescription for each node/scope in
    the graph description.

    We use it here to toggle off the preview of nodes and color in "Texture" node title bars.
    """
    scope["Basic/Show Preview"] = False
    if scope.get("$type", None) == "Texture":
        scope["Basic/Color"] = maxon.Color(0.25, 0, 0)
    return scope

# This is the ApplyDescription call where we use our callback function.
graph: maxon.GraphModelRef = maxon.GraphDescription.GetGraph(name="Callback Example")
maxon.GraphDescription.ApplyDescription(
    graph, {
        "$type": "Output",
        "Surface": {
            "$type": "Standard Material",
            "Base/Metalness": 1.,
            "Base/Color": {
                "$type": "TriPlanar",
                "Coordinates/Scale": {
                    "$type": "Value",
                    "$id": "texScale",
                    "Input": .02},
                "Texture/Image X": {
                    "$type": "Texture",
                    "Basic/Name": "Tex: Base Color",
                    "Image/Filename/Path": maxon.Url("asset:///file_dcd3db0487f2def6")}
            },
            "Reflection/Roughness": {
                "$type": "Scalar Ramp",
                "Input/Alt Input": {
                    "$type": "TriPlanar",
                    "Coordinates/Scale": "#texScale",
                    "Texture/Image X": {
                        "$type": "Texture",
                        "Basic/Name": "Tex: Reflection Roughness",
                        "Image/Filename/Path": maxon.Url("asset:///file_2e316c303b15a330")}
                }
            }
        }
    # We pass our callback function to ApplyDescription.
    }, callback=MyCallback)

Graph description commands allow users to carry out actions such as adding (variadic) ports to a node, delete nodes from a graph, or hide certain ports.

Resource Editor

An important aspect of using the Nodes API is the Resource Editor of Cinema 4D. This also applies to some degree to graph descriptions, as constants such as the value of data type or the value dropdown are not always known to the user. The Resource Editor can be used to discover these constants and their identifiers. The Resource Editor can be opened by pressing SHIFT + C and typing its name in the Commander.

../_images/find_resource_editor.png

Fig. 17: To open the Resource Editor, press SHIFT + C and type its name.

The Resource Editor is the GUI for the databases that make up data types and interface elements of parts of Cinema 4D which have been realized using the Maxon API. You will not find anything Cinema API related in the Resource Editor, especially not the resources of a Cinema API NodeData plugin. The Editor can be split into seven sections as shown below:

../_images/resource_editor.png

Fig. 18: The main sections of the Resource Editor.

  • Language (left side, red): Selects the browsed interface language. This will impact the values of localized strings in resources and should be set to “en-US” when working with graph descriptions as they currently only support English interface labels.

  • Database (left side, yellow): Selects a database. All elements that are not root elements in the attached menu are not databases but entities in that database, i.e., we can select a database and an entity in it in one operation. A database is usually context bound, e.g., Redshift material nodes have their own database.

  • Entities (left side, green): Selects an entity in the selected database. An entity can be the description of a node type or a data type - and some other cases. The Resource Editor calls the elements of a database descriptions which is just terminological noise. Think of them as the resources/entities of the selected database.

  • Structure (left side, blue): Displays the structure of the selected entity. Here we can find attribute groups, attributes, ports, fields, and more elements of the selected entity.

  • Templates (left side, pinks): Provides a set of ready made templates for an entity. This field is irrelevant for graph descriptions.

  • Editor (middle, teal): This is the editor for the currently selected entity. Here we can see its string, GUI, and data settings and also edit them when we have write access to the database.

  • Preview (right side, purple): Shows a preview of the selected entity. This is not relevant for graph descriptions.

To find the identifiers of a port or attribute value, we must find the node which holds the port or attribute. Let us assume we want to set the ‘Input Map Type’ port of a Redshift ‘Bump Map’ node. We cannot use interface labels but must use the identifier instead.

../_images/node_editor_bump_map.png

Fig. 19: We want to discover the raw values a Redshift Bump Map node ‘Input Map Type’ port can take.

Ee search for the ‘Bump Map’ node in the Resource Editor by typing the word ‘bump’ in the opened database context menu to narrow its content down. It is here helpful to know the API identifier of the node as shown in the Node Editor because entities are stored under their API identifier in Resource Editor databases. We then select the ‘Input Map Type’ input port in the ‘Structure’ section of the selected ‘Bump Map’ entity. The ‘Editor’ section will then show us the raw values the enum of the port can take in its ‘Enum List’ field. The values are 0, 1, 2 in this case. By expanding the ‘Enum List’ field, we can also explore the label-value pairs of the enum in more detail.

Fig. 20: Using the Resource Editor to find an enum value.

Another common task is to find data types, this can be done analogously by for example searching for ‘file’ to find the port-bundle datatype ‘Filename’ used for example in a Redshift ‘Texture’ node. Sometimes we must also find out the API identifier of a data type, for example when we must set the data type of a ‘Value’ Node. And while we could do that with the Resource Editor, it is often easier to use the special private _dt field of a maxon.Data instance for that. We can either use the console to print the identifier or make use of the field in a graph description.

../_images/console_datatype.png

Fig. 21: We use the console and a dummy instance to find out the identifier of all maxon data types.

Alternatively, we can also use the _dt field in a graph description:

import maxon

# Unless we set the data type of a Value node to a specific type, it will only work for real
# numbers.
maxon.GraphDescription.ApplyDescription(
    maxon.GraphDescription.GetGraph(name="Data Types"), {
        "$type": "Value",
        # We use the _dt field to set the data type of the Value node to a color with an alpha.
        "Data Type": str(maxon.ColorA()._dt),
        "Input": maxon.ColorA(1, 0, 0, 1)
    }
)