Implementing Custom User Nodes

Table of Contents

Custom user nodes are containers of data created with the resource editor in Cinema 4D and provide a functionality similar to resource descriptions in the Classic API. A custom user node does not provide any logic, but is only a container for data. The logic for a node is being provided by a core node which can be connected to a custom user node. But that is not mandatory since a custom user node can also exist on its own. Such a node will then act like a dummy node, representing a set of data. For example, a texture node will hold some information - the texture path, the first frame, the last frame, and the result - but will just output that data without processing it.

Code

First, the following code is needed to load and register the nodes from the resources that will be created later:

// This description processor must be used for all nodes of the namespace. This will define if the node is compatible with the current NodeSpace.
MAXON_DECLARATION_REGISTER(maxon::DescriptionProcessors, "net.maxonexample.handbook.nodespace.noderendererprocessor")
{
return maxon::nodes::CoreNodesDescriptionProcessor().Create(
[](const maxon::nodes::NodeSystemClass& cls) -> maxon::Result<maxon::Bool>
{
return cls.GetClass() == NodeRendererNodeSystemClassImpl::GetClass();
});
}

A BaseArray that will store GenericData.

A static function that will be called from LoadResources() to handle user node descriptions.

static maxon::Result<void> HandleUserNodeDescriptions(const maxon::Id& databaseId)
{
maxon::nodes::NodeSystemClass nodeSystemClass = NodeRendererNodeSystemClassImpl::GetClass().Create() iferr_return;
for (const maxon::IdAndVersion& id : ids)
{
const maxon::DataDescription description = maxon::DataDescriptionDatabaseInterface::LoadDescription(maxon::DATADESCRIPTION_CATEGORY_DATA, maxon::LanguageRef(), id.first) iferr_return;
const maxon::DataDictionary info = description.GetInfo();
const maxon::Id classification = info.Get(maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION, maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION.ENUM_UNCLASSIFIED);
if (classification == maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION.ENUM_NODE)
{
maxon::nodes::NodeTemplate node = maxon::nodes::NodesLib::CreateLazyTemplate(id.first,
[id, nodeSystemClass]() -> maxon::Result<maxon::nodes::NodeTemplate>
{
return maxon::nodes::NodesLib::BuildNodeFromDescription(id.first, nodeSystemClass);
}, nodeSystemClass) iferr_return;
maxon::GenericData data = maxon::nodes::BuiltinNodes::Register(id.first, node) iferr_return;
g_freeAtEnd.Append(std::move(data)) iferr_return;
}
}
return maxon::OK;
}

The function HandleUserNodeDescriptions() is used in the original LoadResources() function.

HandleUserNodeDescriptions(g_nodeRendererDatabaseID) iferr_return;

g_freeAtEnd is freed in FreeResources().

g_freeAtEnd.Reset();

Resource Editor

The string identifiers defined in the resource editor can be exported to symbol definitions in the form of header files.

This has the advantage that the identifiers themselves can change over the development of a plugin, while their references in code, the symbols, do not have to. There are two options for generating such header files.

  • Writing header files: When running Cinema 4D with the startup argument g_autoExportHeaderPaths="{name_of_the_file}.h", modifications to a node database with the Resource Editor can be reflected in the header file defined in the startup argument. This write operation still has to be manually invoked and will write the identifiers of the database that are not being ignored into a header file.
  • Comparison tools: When running Cinema 4D with the startup argument g_externalCompareTool="{absolute_path_to}\{application}", modifications to a node database with the Resource Editor can be reflected with a comparison tool. Instead of writing the non-ignored identifiers to a specified file, a temporary file will be created that will the be opened alongside the original file in the comparison tool.

To use this feature, compile the plugin, start Cinema 4D and open the Resource Editor.

Switch the Resource Editor mode to "Developer Mode" in the menu.
In "Database" select the custom database net.maxonexample.handbook.nodes.registereddatabase. The selected database should be the one registered for the plugin, as described in Registering Node Databases.net.maxonexample.handbook.nodes.registereddatabase
Add a new data type by invoking "Data Type/Add Data Type" in the menu of the Resource Editor.
Set the data type identifier to "net.maxonexample.handbook.noderenderer.usernode"
Once a data type has been defined, attributes can be defined for it by running the menu command "Attribute/Add Attribute/New".
In the new attribute, select "Command/Include" and set the field "Identifier" as "net.maxon.node.base". Check "Data", "UI" and "String". Select "net.maxonexample.handbook.noderenderer.usernode" again, in order to update the Resource Editor. This will include different attributes:
  • "net.maxon.object.base.name" defines the name of the node.
  • "net.maxon.object.base.tags" defines the tags which describe the node in the Asset Browser.
  • "net.maxon.object.base.annotations" defines the annotation of the node in the Asset Browser.
To export the header for this node, the attribute we just included has to be ignored. We have to do this because the identifier, "net.maxon.object.base", is one provided by Cinema 4D and therefore already has a symbol; which can be found as maxon::NODE::BASE in nodes_corenodes_base.h. An attribute can be ignored by checking the field "Ignore on Header Export" in the "Development" tab of the attribute.
Now the settings for exporting identifiers have to be defined. Clear the selection of the tree view by clicking into an empty area of the gadget and then in the view on the right:
  • Select "node" in "Classification",
  • set "Description Processor" to "net.maxonexample.handbook.nodespace.noderendererprocessor",
  • set "Menu Category" to "Uncategorized",
  • and select the header file to which the identifiers should be exported to with "Include file".
Now a new attribute can be defined which will be exported:
  • Set "Command" to "attribute",
  • set "Identifier" to "net.maxonexample.handbook.node.usernode.out",
  • select "Yes", when an "Invalid Identifier" warning message box opens,
  • set "Datatype" to maxon::ColorA,
  • set "Classification" to "output",
  • set "Default Value" to some color,
  • set "String" to "Result".
Once a file has been generated, the following information has to be added:
  • A header guard,
  • some includes, at least "maxon/fid.h", so that Maxon macros and data types can be used,
  • at the end of the file, but before the header guard #endif, add an include-statement to include the files generated by the source processor for the node-project.
    #include "<name_of_the_file>1.hxx"
    #include "<name_of_the_file>2.hxx"


Final Code

If all steps have been followed, the code should look like the following:

#include "maxon/module.h"
#include "maxon/nodeslib.h"
// This description processor must be used for all nodes of the namespace. This will define if the node is compatible with the current NodeSpace.
MAXON_DECLARATION_REGISTER(maxon::DescriptionProcessors, "net.maxonexample.handbook.nodespace.noderendererprocessor")
{
return maxon::nodes::CoreNodesDescriptionProcessor().Create(
[](const maxon::nodes::NodeSystemClass& cls) -> maxon::Result<maxon::Bool>
{
return cls.GetClass() == NodeRendererNodeSystemClassImpl::GetClass();
});
}
static maxon::Result<void> HandleUserNodeDescriptions(const maxon::Id& databaseId)
{
maxon::nodes::NodeSystemClass nodeSystemClass = NodeRendererNodeSystemClassImpl::GetClass().Create() iferr_return;
for (const maxon::IdAndVersion& id : ids)
{
const maxon::DataDescription description = maxon::DataDescriptionDatabaseInterface::LoadDescription(maxon::DATADESCRIPTION_CATEGORY_DATA, maxon::LanguageRef(), id.first) iferr_return;
const maxon::DataDictionary info = description.GetInfo();
const maxon::Id classification = info.Get(maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION, maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION.ENUM_UNCLASSIFIED);
if (classification == maxon::DESCRIPTION::DATA::INFO::CLASSIFICATION.ENUM_NODE)
{
maxon::nodes::NodeTemplate node = maxon::nodes::NodesLib::CreateLazyTemplate(id.first,
[id, nodeSystemClass]() -> maxon::Result<maxon::nodes::NodeTemplate>
{
return maxon::nodes::NodesLib::BuildNodeFromDescription(id.first, nodeSystemClass);
}, nodeSystemClass) iferr_return;
maxon::GenericData data = maxon::nodes::BuiltinNodes::Register(id.first, node) iferr_return;
g_freeAtEnd.Append(std::move(data)) iferr_return;
}
}
return maxon::OK;
}
static maxon::Result<void> ConfigurePreviewImageRequest(maxon::DataDictionaryObjectRef request)
{
request.Set(maxon::nodes::PREVIEWIMAGEREQUEST::PROVIDER, PreviewImageProviderExampleHandbook::GetClass()) iferr_return;
return maxon::OK;
}
MAXON_METHOD maxon::Result<void> NodeRendererNodeSpaceImpl::Init(maxon::DataDictionary spaceData)
{
_class = NodeRendererNodeSystemClassImpl::GetClass().Create() iferr_return;
spaceData.Set(maxon::nodes::NODESPACE::NODESYSTEMCLASS, _class) iferr_return;
super.Init(spaceData) iferr_return;
return maxon::OK;
}
static maxon::Id g_nodeRendererDatabaseID = maxon::Id("net.maxonexample.handbook.nodes.registereddatabase");
static maxon::GenericData g_exampleNodeSpace;
static maxon::Result<void> LoadResources()
{
{
err.CritStop();
return err;
};
// Get plugin location
const maxon::Url& binaryUrl = maxon::g_maxon.GetUrl();
// Get plugin folder
maxon::Url pluginDir = binaryUrl.GetDirectory();
// Get resource folder (this folder must exist)
const maxon::Url resourceUrl = pluginDir.Append("res"_s).Append("nodes"_s) iferr_return;
// Register database
// Register the node space
// Use the same ID as in the resource files
const maxon::Id spaceDescriptionId = maxon::Id("net.maxonexample.handbook.nodespace.node_renderer");
// Load the description
maxon::DataDictionary nodeRendererData = maxon::nodes::NodeSpaceHelpersInterface::LoadDescription(spaceDescriptionId) iferr_return;
maxon::nodes::NodeSpaceRef nodeRendererSpace = NodeRendererNodeSpaceImpl::CreateInit(nodeRendererData) iferr_return;
// Register the node space implementation
g_exampleNodeSpace = maxon::nodes::MaterialNodeSpaces::Register(NodeRendererNodeSpaceImpl::GetDescriptor().GetId(), nodeRendererSpace) iferr_return;
HandleUserNodeDescriptions(g_nodeRendererDatabaseID) iferr_return;
return maxon::OK;
}
static void FreeResources()
{
{
err.CritStop();
return;
};
// Unregister a database
g_exampleNodeSpace = maxon::GenericData();
g_freeAtEnd.Reset();
}
MAXON_INITIALIZATION(LoadResources, FreeResources);
maxon::DataDescriptionDefinitionDatabaseInterface::RegisterDatabaseWithUrl
static MAXON_METHOD Result< void > RegisterDatabaseWithUrl(const Id &databaseId, const Url &url)
maxon::DataDescriptionDatabaseInterface::LoadDescription
static MAXON_METHOD Result< DataDescription > LoadDescription(const Id &category, const LanguageRef &language, const Id &dataType)
MAXON_DECLARATION_REGISTER
#define MAXON_DECLARATION_REGISTER(...)
Definition: module.h:883
maxon::IdAndVersion
Tuple< Id, Id > IdAndVersion
IdAndVersion is used for assets to store the asset id (first) and version (second) in a single tuple.
Definition: idandversion.h:14
node_spaces.h
maxon
The maxon namespace contains all declarations of the MAXON API.
Definition: c4d_basedocument.h:15
nodeslib.h
datadescriptiondatabase.h
module.h
maxon::DataDescriptionDefinitionDatabaseInterface::GetRegisteredDescriptions
static MAXON_METHOD Result< BaseArray< IdAndVersion > > GetRegisteredDescriptions(const Id &databaseId, const Id &category, const LanguageRef &language)
maxon::OK
return OK
Definition: apibase.h:2546
maxon::Id
Definition: apibaseid.h:250
iferr_return
#define iferr_return
Definition: resultbase.h:1465
nodes_corenodes_base.h
datadescriptiondefinitiondatabase.h
previewimageprovider.h
maxon::nodes::NodesLib::CreateLazyTemplate
static MAXON_METHOD Result< NodeTemplate > CreateLazyTemplate(const Id &nodeId, Delegate< Result< NodeTemplate >()> &&creator, Delegate< Result< Bool >(const NodeSystemClass &cls)> &&support={})
maxon::BaseArray
Definition: basearray.h:366
maxon::Url
Definition: url.h:855
MAXON_INITIALIZATION
#define MAXON_INITIALIZATION(...)
Definition: module.h:746
maxon::DATADESCRIPTION_CATEGORY_DATA
const Id DATADESCRIPTION_CATEGORY_DATA
Definition: datadescriptiondatabase.h:15
maxon::Result
Definition: apibase.h:316
MAXON_METHOD
#define MAXON_METHOD
Definition: interfacebase.h:877
maxon::BaseArray::Append
MAXON_ATTRIBUTE_FORCE_INLINE ResultRef< T > Append()
Definition: basearray.h:569
iferr_scope
#define iferr_scope
Definition: resultbase.h:1374
maxon::DataDescriptionDefinitionDatabaseInterface::UnregisterDatabase
static MAXON_METHOD Result< void > UnregisterDatabase(const Id &databaseId)
maxon::GenericData
Definition: genericdata.h:19
iferr_scope_handler
#define iferr_scope_handler
Definition: resultbase.h:1392
maxon::nodes::NodeSpaceHelpersInterface::LoadDescription
static MAXON_METHOD Result< DataDictionary > LoadDescription(const Id &spaceDescriptionId)
maxon::BaseArray::Reset
void Reset()
Deletes all elements (calls destructors and frees memory).
Definition: basearray.h:495
maxon::nodes::NodesLib::BuildNodeFromDescription
static MAXON_METHOD Result< NodeSystemBasedNodeTemplate > BuildNodeFromDescription(const Id &id, const NodeSystemClass &nodeClass, Bool addDependencies=true, const Delegate< Result< void >(const MutableRoot &root)> &finalizer={})
datadescription_nodes.h
maxon::LiteralId::Get
const Id & Get() const
Definition: apibaseid.h:183