Updated Redshift node material examples
-
Dear community,
a support request alerted us to the fact that the C++ code example Create a Redshift Material does not correctly display textures of the material in the viewport when applied to an object until the material is edited at least once by the user.
This is caused by a limitation of node materials and the Nodes API in general which does not fair well for graphs which are not attached to a document (e.g., a material not inserted into a document). I have updated the C++ example to a variant which deals with this issue by moving inserting the material ahead. This requires then more verbose error handling. I also added a more verbose undo handling, as this is non-trivial in this case.
The corresponding Python code example did this already correctly (mostly by accident). I updated it too, and modernized things like using
CreateEmptyGraph
in favour ofCreateDefaultGraph
andSetPortValue
in favour ofSetDefaultValue
. The Python example now also demonstrates how to consolidate an undo operation for creating a node material.The examples will be shipped in a future documentation update. Find a preview of both updated examples below.
Cheers,
FerdinandC++
#include "c4d.h" #include "c4d_basedocument.h" #include "c4d_baselist.h" #include "c4d_basematerial.h" #include "c4d_tools.h" #include "lib_description.h" #include "lib_noise.h" #include "maxon/apibase.h" #include "maxon/graph.h" #include "maxon/nodesgraph.h" #include "maxon/graph_helper.h" #include "maxon/node_undo.h" //! [create_redshift_material] /// Creates a small node setup of four nodes for a Redshift node material and inserts the material /// into the passed document. /// /// The example uses two textures delivered as assets with Cinema 4D so that they can be linked in /// shaders. This can be ignored when locally provided textures should be used instead. /// /// @param[in, out] doc The document to insert the node material into. maxon::Result<void> CreateRedshiftMaterial(BaseDocument* doc) { // --- Error handling ---------------------------------------------------------------------------- // We must declare our node material here, so that the error handler below can access it. NodeMaterial* material; Bool didInsertMaterial = false; // This is the error handler for the scope of this function. It is necessary and the exit // point for code which returns errors like for example `iferr_return` or a return statement. // What is not necessary, is how explict we are about this. We could replace the whole handler // with an `iferr_scope;`, but would then give up the special handling we do below. // // We use the error handler here to gracefully unwind the document state when an error // occurred while we tried to build the material graph. The primary thing we do, is remove // the material #material from the passed document #doc. This error handler is also special // in that it demonstrates how to raise new errors while handling errors. iferr_scope_handler -> maxon::Error { // We declare a few errors in advance to make our code a bit cleaner (and a tiny bit // less performant). We need these when errors occur while handling the error #err // which is passed to this error scope. maxon::AggregatedError aggError { MAXON_SOURCE_LOCATION }; maxon::IllegalStateError undoError { MAXON_SOURCE_LOCATION, "Could not unroll or finalize undo stack upon error handling."_s }; maxon::UnexpectedError critError { MAXON_SOURCE_LOCATION, "Critical error while aggregating errors."_s }; // There is a valid document and material and we inserted the material. We try to // unwind the document state before returning the error. if (doc && material && didInsertMaterial) { // We try to add an undo item for removing the material, on success, we try // to remove the material and close the undo. Bool isUndoError = !doc->AddUndo(UNDOTYPE::DELETEOBJ, material); if (!isUndoError) { material->Remove(); isUndoError = !doc->EndUndo(); } // An error occurred while handing the undo, we attempt to use an AggregatedError, a // container which can hold multiple errors, to return both the original error #err which // has been passed into this scope, as well as #undoError to indicate that while handling // #err another error occurred. if (isUndoError) { // AggregatedError::AddError is itself of type Result<>, i.e., can fail. We use here // iferr (Result<T>) to return #critError in such cases. iferr (aggError.AddError(maxon::Error(err))) return critError; iferr (aggError.AddError(undoError)) return critError; // Building the aggregated error succeeded we return it instead of #err. return aggError; } } // We return the original error as there was either no document handling to do, or there was // no error in doing so. The special thing is here that we must wrap the #err in an Error // because #err is an error pointer and our precise error handling with error aggregation // requires us to return a error reference instead (as indicated by the return type of this // handler). return maxon::Error(err); }; // --- Logic starts here ------------------------------------------------------------------------- // This main thread check is not strictly necessary in all cases, but when #doc is a loaded // document, e.g., the active document, then we are bound by threading restrictions and must not // modify such document off-main-thread. if (!GeIsMainThreadAndNoDrawThread()) return maxon::IllegalStateError(MAXON_SOURCE_LOCATION, "Must run on main thread."_s); if (!doc) return maxon::NullptrError(MAXON_SOURCE_LOCATION, "Invalid document pointer."_s); // We start an undo item. This example is written with the goal to wrap the whole operation // into a singular undo operation. All the manual undo-handling in the function can be omitted but // depending on the context, we might then end up with more than one undo step to revert to // the previous document state. if (!doc->StartUndo()) return maxon::IllegalStateError( MAXON_SOURCE_LOCATION, "Could not start undo item for document."_s); // The asset URL of the "rust" and "sketch" texture assets delivered with Cinema 4D. See the // Asset API Handbook in the Cinema 4D C++ documentation for more information on the Asset AP. const maxon::Url rustTextureUrl { "asset:///file_edb3eb584c0d905c"_s }; const maxon::Url sketchTextureUrl { "asset:///file_3b194acc5a745a2c"_s }; // Create a NodeMaterial by instantiating a Mmaterial BaseMaterial and casting it to a // NodeMaterial. This could also be done in two steps (first allocating the Mmaterial and // then casting it) or by casting an already existing Mmaterial into a NodeMaterial. material = static_cast<NodeMaterial*>(BaseMaterial::Alloc(Mmaterial)); if (!material) return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate node material."_s); // Define the ID of the Redshift node space and add an empty graph. const maxon::LiteralId redshiftId("com.redshift3d.redshift4c4d.class.nodespace"); material->CreateEmptyGraph(redshiftId) iferr_return; const maxon::nodes::NodesGraphModelRef& graph = material->GetGraph(redshiftId) iferr_return; // Insert the material into the passed document. When we do not do this before the transaction, // textures will not be properly reflected in the viewport unless we do a dummy commit at the // end. It is also quite important that we do this after we called CreateEmptyGraph or // CreateDefaultGraph as these will otherwise add an undo item (without us being able to // consolidate this into our undo). doc->InsertMaterial(material); didInsertMaterial = true; if (!doc->AddUndo(UNDOTYPE::NEWOBJ, material)) return maxon::IllegalStateError( MAXON_SOURCE_LOCATION, "Could not add undo for adding material to document."_s); // The ids of the nodes which are required to build the graph. These ids can be discovered in the // Asset Browser with the #-button in the "Info Area" of the Asset Browser. const maxon::Id outputNodeTypeId("com.redshift3d.redshift4c4d.node.output"); const maxon::Id materialNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.standardmaterial"); const maxon::Id colormixNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.rscolormix"); const maxon::Id noiseNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.maxonnoise"); const maxon::Id textureNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.texturesampler"); // Define a settings dictionary for the transaction we start below. Doing this is not necessary, // as the userData argument to BeginTransaction is optional. The here used attribute and enum // requires the nodespace.framework and `node_undo.h` to be included with the module. We // instruct the graph to not open a new undo operation for this transaction and instead add items // to the ongoing undo operation of the document. maxon::DataDictionary userData; userData.Set(maxon::nodes::UndoMode, maxon::nodes::UNDO_MODE::ADD) iferr_return; // Start modifying the graph by starting a graph transaction. Transactions in the Nodes API work // similar to transactions in databases and implement an all-or-nothing model. When an error occurs // within a transaction, all already carried out operations will not be applied to the graph. The // modifications to a graph are only applied when the transaction is committed. The scope // delimiters { and } used here between BeginTransaction() and transaction.Commit() are not // technically required, but they make a transaction more readable. maxon::GraphTransaction transaction = graph.BeginTransaction(userData) iferr_return; { // Add the new nodes to the graph which are required for the setup. Passing the empty ID as // the first argument will let Cinema 4D choose the node IDs, which is often the best option // when newly created nodes must not be referenced by their ID later. maxon::GraphNode outNode = graph.AddChild(maxon::Id(), outputNodeTypeId) iferr_return; maxon::GraphNode materialNode = graph.AddChild(maxon::Id(), materialNodeTypeId) iferr_return; maxon::GraphNode colormixNode = graph.AddChild(maxon::Id(), colormixNodeTypeId) iferr_return; maxon::GraphNode noiseNode = graph.AddChild(maxon::Id(), noiseNodeTypeId) iferr_return; maxon::GraphNode rustTexNode = graph.AddChild(maxon::Id(), textureNodeTypeId) iferr_return; maxon::GraphNode sketchTexNode = graph.AddChild(maxon::Id(), textureNodeTypeId) iferr_return; // The port IDs which are used here can be discovered with the "IDs" option "Preferences/Node // Editor" option enabled. The selection info overlay in the bottom left corner of the Node // Editor will now show both the IDs of the selected node or port. // Get the "Out Color" out-port of the "Standard Material" node and the "Surface" in-port of // from the "Output" node in this graph. maxon::GraphNode outcolorPortMaterialNode = materialNode.GetOutputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.outcolor")) iferr_return; maxon::GraphNode surfacePortOutNode = outNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.node.output.surface")) iferr_return; // Connect the "Out Color" port to the "Surface" port. The connection order of an out-port // connecting to an in-port is mandatory. outcolorPortMaterialNode.Connect(surfacePortOutNode) iferr_return; // Connect the "outColor" out-port of the the "Texture" node for the rust texture to the // "Input 1" in-port of the "Color Mix" node. maxon::GraphNode outcolorPortRustTexNode = rustTexNode.GetOutputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")) iferr_return; maxon::GraphNode input1PortColormixNode = colormixNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.input1")) iferr_return; outcolorPortRustTexNode.Connect(input1PortColormixNode) iferr_return; // Connect the output port of the sketch "Texture" node to the "Input 2" in-port of the // "Color Mix" node. maxon::GraphNode outcolorPortSketchTexNode = sketchTexNode.GetOutputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")) iferr_return; maxon::GraphNode input2PortColormixNode = colormixNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.input2")) iferr_return; outcolorPortSketchTexNode.Connect(input2PortColormixNode) iferr_return; // Connect the output port of the "Color Mix" node to the "Base > Color" in-port of the "Standard // Material" node. maxon::GraphNode outcolorPortColormixNode = colormixNode.GetOutputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.outcolor")) iferr_return; maxon::GraphNode basecolorPortMaterialNode = materialNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color")) iferr_return; outcolorPortColormixNode.Connect(basecolorPortMaterialNode) iferr_return; // Connect the output port of the "Maxon Noise" node both to the "Mix Amount" in-port of // the "Color Mix" node and the in-port "Reflection > Roughness" of the "Standard Material" node. maxon::GraphNode outcolorPortNoiseNode = noiseNode.GetOutputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.maxonnoise.outcolor")) iferr_return; maxon::GraphNode amountPortColormixNode = colormixNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.mixamount")) iferr_return; maxon::GraphNode roughnessPortMaterialNode = materialNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.refl_roughness")) iferr_return; outcolorPortNoiseNode.Connect(amountPortColormixNode) iferr_return; outcolorPortNoiseNode.Connect(roughnessPortMaterialNode) iferr_return; // Set the noise type of the "Maxon Noise" node to "Wavy Turbulence" // Setting a value of a port without a connection is done by setting the default value of a // port. What is done here is the equivalent to a user changing the value of a noise type of // the noise node to "Wavy Turbulence" in the Attribute Manager of Cinema 4D. maxon::GraphNode noisetypePortNoiseNode = noiseNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.maxonnoise.noise_type")) iferr_return; noisetypePortNoiseNode.SetPortValue(NOISE_WAVY_TURB) iferr_return; // Set the texture paths of both texture nodes to the texture asset URLs defined above. // Here we encounter something new, a port which has ports itself is a "port-bundle" in terms // of the Nodes API. The "Filename" port of a texture node is a port bundle which consists out // of two sub-ports, the "path" and the "layer" port of the texture. The texture URL must be // written into the "path" sub-port. maxon::GraphNode pathPortRustTexNode = rustTexNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")).FindChild( maxon::Id("path")) iferr_return; pathPortRustTexNode.SetPortValue(rustTextureUrl) iferr_return; maxon::GraphNode pathPortSketchTexNode = sketchTexNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")).FindChild( maxon::Id("path")) iferr_return; pathPortSketchTexNode.SetPortValue(sketchTextureUrl) iferr_return; } transaction.Commit() iferr_return; // Finalize our undo item. if (!doc->EndUndo()) return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Failed to finalize undo item."_s); return maxon::OK; } //! [create_redshift_material]
Python
#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) 2023 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()