Implementing Node Functionalities

Describes different patterns of implementing nodes and their core functionality of computing outputs with a list of inputs.

Core Node Implementations with Delegates

From a mathematical point of view, a true node in a node graph is an operation. That operation takes a list of inputs and returns a list of outputs. That functionality could be for example adding two values and returning the sum of them. Because of that, the most basic implementation of a node functionality is providing a delegate which implements the functionality of a node. The example below implements the operation of taking an input value and returning its absolute value as the return value of a delegate.

// Create the function that will be called when the node is used.
static Int HandBookAbs(const Int &value)
{
return Abs(value);
}
MAXON_CORENODE_FUNCTION(AbsHandbookNode, HandBookAbs, Int);
MAXON_CORENODE_REGISTER_PURE(AbsHandbookNode, "net.maxonexample.handbook.corenode.abs");
PyObject * value
Definition: abstract.h:715
#define MAXON_CORENODE_REGISTER_PURE(CLS, id,...)
Definition: corenodes.h:1034
#define MAXON_CORENODE_FUNCTION(N, func,...)
Definition: corenodes_implementer.h:202
maxon::Int Int
Definition: ge_sys_math.h:64
Float32 Abs(Float32 val)
Definition: apibasemath.h:195

This delegate can also be realized in the form of a type agnostic template. Which can be useful when there are many input data types which the functionality can be applied to.

template <typename T> T HandBookAbsTemplate(const T& value)
{
return Abs(value);
}
template <typename T> MAXON_CORENODE_FUNCTION(HandbookAbsNodeTemplate, HandBookAbsTemplate, T);
MAXON_CORENODE_REGISTER_PURE(HandbookAbsNodeTemplate,
"net.maxonexample.handbook.corenode.abstemplate", Int);
MAXON_CORENODE_REGISTER_PURE(HandbookAbsNodeTemplate,
"net.maxonexample.handbook.corenode.abstemplate", Float32);
MAXON_CORENODE_REGISTER_PURE(HandbookAbsNodeTemplate,
"net.maxonexample.handbook.corenode.abstemplate", Vector32);
maxon::Float32 Float32
Definition: ge_sys_math.h:68

For unary operations, the MAXON_CORENODE_OPERATOR_UNARY macro can be used to streamline the process of registration. A unary operation is an operation with a single operand and a single output. Taking the absolute or the inverse of a value is an example of unary operation.

// UNARY NODE
template <typename T> MAXON_CORENODE_OPERATOR_UNARY(HandbookNegNode, -, T);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Int32);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Int64);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Float32);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Float64);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Vector2d32);
#define MAXON_CORENODE_OPERATOR_UNARY(N, op, T)
Definition: corenodes_implementer.h:167
maxon::Int64 Int64
Definition: ge_sys_math.h:62
maxon::Float64 Float64
Definition: ge_sys_math.h:67
maxon::Int32 Int32
Definition: ge_sys_math.h:60
Vec2< Float32, 1 > Vector2d32
Definition: vector2d.h:30

For binary operations, the MAXON_CORENODE_OPERATOR_BINARY macro can be used to streamline the process of registration. A binary operation is an operation with two operands and a single output. Summing up or comparing two values are examples for binary operations.

template <typename T> MAXON_CORENODE_OPERATOR_BINARY(HandbookGreaterThanNode, > , T, T);
MAXON_CORENODE_REGISTER_PURE(HandbookGreaterThanNode,
"net.maxonexample.handbook.corenode.greaterthan", Float);
#define MAXON_CORENODE_OPERATOR_BINARY(N, op, T1, T2)
Definition: corenodes_implementer.h:184
maxon::Float Float
Definition: ge_sys_math.h:66

Core Node Delegate Registration Macros

The MAXON_CORENODE_FUNCTION, MAXON_CORENODE_OPERATOR_UNARY and MAXON_CORENODE_OPERATOR_BINARY macros simplify a core node implementation if the function is already available as a C++ function or operator. Such node implementations then need to be registered with MAXON_CORENODE_REGISTER_PURE macro.

Registering Node Implementations for Multiple Data Types

CoreNode implementations that can handle multiple data types for a singular input value have to be registered for each data type this input value can be.

MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Int32);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Int64);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Float32);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Float64);
MAXON_CORENODE_REGISTER_PURE(HandbookNegNode, "net.maxonexample.handbook.corenode.neg", Vector2d32);

Linking Core Node Inputs and Outputs to Resource Definitions

Every time a CoreNode is being implemented, a custom user node has to be created and linked to the CoreNode. For details read Implementing Custom User Nodes. Depending on the operator node type which is being implemented, the custom user node will need one or more input ports.

More specifically:

  • If only one port is needed, its identifier will be "in".
  • If two or more ports are needed, their identifiers will be "in1", "in2", ..., "inN".
  • The output port will always have the identifier "out".

To establish the connection of a custom user node's port identifiers and the CoreNode in- and outputs, a processor directive has to be provided in the Resource Editor. In the database for your plugin, click on an empty part of the tree view to have no element of the currently displayed data type selected and define the value of the processor parameter in the view on the right as:

net.maxonexample.handbook.corenode.abs<Int>

Class-based CoreNode Implementations

CoreNode implementations can also be provided in the form of class implementations, providing more options than the callback approach.

BasicMicroNode

BasicMicroNode is the base class for implementing the functionalities of a core node. It is comparable to NodeData in the classic API as it is also a central interface with which plugins will be implemented. While NodeData implemented arbitrary (GeListNode) nodes in the scene graph of the classic API, BasicMicroNode does the same for the Nodes API and implements a CoreNode. A BasicMicroNode receives a single set of input values in its Process() method and computes for it the output values defined by its functionality. A minimal BasicMicroNode implementation has to provide port definitions and its Process() method. The ports have to be defined by MAXON_PORT_INPUT or MAXON_PORT_OUTPUT macros in a group class. Usually, the BasicMicroNode class is a member of that micro node group class, as shown in the example below.

class HandbookBasicMicroNode
{
public:
// Declaration of the input and output ports of the micro node group.
// Implementation of the single custom micro node.
{
public:
// The Process method needs to specify which ports of the group the micro node accesses,
// in this case all ports.
// If the group contains more than one micro node, each micro node usually needs only
// a subset of the ports.
maxon::Result<void> Process(const Ports<in1, in2, out>& ports) const
{
ports.out.Update(ports.in1() + ports.in2());
return maxon::OK;
}
};
// The Init function will be called to set up the micro node group
// When you call CreateNode<MyAddNode>() or as part of the MAXON_CORENODE_REGISTER macro.
static maxon::Result<void> Init(const maxon::corenodes::MicroNodeGroupRef& group)
{
group.AddChild<Impl>() iferr_return;
return maxon::OK;
}
};
Definition: micronodes.h:208
Int64 Int
signed 32/64 bit int, size depends on the platform
Definition: apibase.h:188
return OK
Definition: apibase.h:2690
#define MAXON_PORT_OUTPUT(T, name,...)
Definition: micronodes_ports.h:223
#define MAXON_PORT_INPUT(T, name,...)
Definition: micronodes_ports.h:61
#define iferr_scope
Definition: resultbase.h:1384
#define iferr_return
Definition: resultbase.h:1519

BatchMicroNode

BatchMicroNode is the base class for custom micro nodes which receive a batch of input values in their Process method and compute the corresponding output values. If Process() method of a BasicMicroNode implementation only consists of a few simple operations, one will consider implementing a BatchMicroNode instead. It computes the output values for a whole batch of input values with a single call, reducing the calling overhead of nodes. The calling overhead of nodes can be significant, and it can be reduced when a single call to Process() handles a whole batch of values. But this only holds true, when there is a batch of values. In the context of shading for example, there is usually just one set of values. A Process() method implementation for a BatchMicroNode has then to loop over a batch of data as shown in the example below.

class HandBookBatchAddNode
{
public:
// Declaration of the input and output ports of the micro node group.
// Implementation of the single custom micro node.
{
public:
// The PORTMODE template parameter allows to instantiate the Process method with different
// port modes, see PortMode.
// Note that there's no const in front of Batch!
maxon::Result<void> Process(Batch<in1, in2, out>& batch) const
{
// Iterate over the batch. Again, there's no const in front of auto.
for (auto& ports : batch)
{
// Do the computation for a single set of values.
ports.out.Update(ports.in1() + ports.in2());
}
return maxon::OK;
}
};
// The Init function will be called to set up the micro node group
// When you call CreateNode<MyBatchAddNode>() or as part of the MAXON_CORENODE_REGISTER macro.
static maxon::Result<void> Init(const maxon::corenodes::MicroNodeGroupRef& group)
{
group.AddChild<Impl>() iferr_return;
// RegisterValueChangedMessage allow you to register a delegate function that will be called
// when the value of a port is changed by the user.
// The delegate will not be called if the value of the port is changed by a connection.
maxonexample::HANDBOOK::NODE::ADDNODE::GetId(),
maxonexample::HANDBOOK::NODE::ADDNODE::IN1,
maxon::DESCRIPTION::UI::BASE::COMMANDCONTEXT.ENUM_NIMBUSCORE,
nullptr,
nullptr,
ConstantValueChanged)) iferr_return;
return maxon::OK;
}
};
MAXON_CORENODE_REGISTER_PURE(HandBookBatchAddNode,
"net.maxonexample.handbook.corenode.HandBookBatchAddNode");
static MAXON_METHOD Result< void > RegisterValueChangedMessage(const Id &dataTypeId, const Id &attributeId, const DescriptionMessageFunction &func)
Definition: tuple.h:611
Definition: micronodes.h:293

OperatorNode

The OperatorNode is a helper class to implement core nodes. This core node will be composed of a function, where some data processing will be defined, and a single output port. This processing function has parameters, one reference to the output port, and references for each entry port to retrieve their values. If this class has template arguments, the operator node must be registered with all the data types that this node can handle.

This example shows how to create such an OperatorNode. The CoreNode with the id net.maxonexample.handbook.corenode.operator.sin this will be used later as a parameter for the Description Processor.

// Implement a basic operator node. This will implement a single batchMicroNode
// The output port has the name "out", the input ports are named "in1", "in2", ...
// (or just "in" for a function with a single parameter)
namespace maxon
{
namespace corenodes
{
template <typename T> class HandbookSinNode : public OperatorNode<HandbookSinNode<T>, T(T)>
{
public:
static ResultOk<void> Process(T& out, const T& value) { out = Sin(value); return OK; }
};
// Register the implementation for each datatype that are supported.
// Those have to be registered under the same ID.
"net.maxonexample.handbook.corenode.operator.sin", Float64);
"net.maxonexample.handbook.corenode.operator.sin", Vector32);
} // end namespace corenodes
} // end namespace maxon
OK
Ok.
Definition: ge_prepass.h:0
MAXON_ATTRIBUTE_FORCE_INLINE Float32 Sin(Float32 val)
Calculates the sine of a value.
Definition: apibasemath.h:198
The maxon namespace contains all declarations of the MAXON API.
Definition: autoweight.h:14

Now that the CoreNode is implemented, the user node can be created with the Resource Editor. For more information please have a look at Implementing Custom User Nodes

  • In the database for the plugin, invoke Data Type -> Add Data Type to add a new data type and define its name as: net.maxonexample.handbook.nodes.trigonometry.sin.
  • Add a new attribute with the identifier datatype. Set its data type to id, its classification to Input, and define the Gui Type ID to net.maxon.ui.enum. This cycle will be filled automatically with the data types that have registered for the CoreNode.
  • Create the input and output port. The datatype of those ports must be defined as Data so it will be able to handle any Maxon data type. The input port identifier will be in and the output port identifier will be out. Check "Is Converter Node" so the input and output will be automatically connected to the wire if a user drops the node on a wire that is compatible with the data type.
  • The most important part is to define the field Description Processor. Deselect any parameter on the attribute list, this will display the info and development tabs of this data type.
  • As the CoreNode was registered for several data types, a port will be created, providing the user to select which data type he wants to use. Add a new attribute with the identifier datatype with the check-boxes Data, UI and String checked. Define the datatype as id and the string to Data Type.
  • In the input list Description Processor define the processor to CoreNode Description Processor. This field allows defining which description processor will instantiate and register the template for this data type. The field Additional Parameter is where is defined what core node will execute its code to process the data. For this, enter the identifier of the CodeNode created previously.
  • The CoreNode must know which data type is used by the user node. A parameter, the identifier of the port where the user has selected the data type, must be added after the CoreNode identifier. The parameter should look like this net.maxonexample.handbook.corenode.operator.sin<datatype>. This also makes the Description Processor aware that it have to fill the enum list of the data type port with the enum list of the registered data types registered for this CoreNode.
  • After a reboot, the node should be visible on the material editor and neutron.

Short Circuit Rules

Short circuit rules are a set of conditions defined when creating a CoreNode that will allow the CoreNode to know the result of its process function without processing the data. For example, a node that multiplies a port A with port B, if one of the values is zero, no matter what the value of the other port is, the result will be zero. If one of the values is 1, the result will be the other value unmodified. In those cases, the result does not need to be calculated.

Note
Short circuit rules can only be created if the CoreNode has only one output port. For CoreNodes that have multiple outputs, the function Optimizer must be implemented. In this case, that function will be used automatically as an optimizer.

The ShortCircuitRule must be created and registered with the same macro that is used to register the CofreNode itself. As this CofreNode is usually a simple node, OperatorNode or OperatorBinary can be used. The CoreNode must be registered with metadata to define the optimizer it must use.

The following example shows how to use a binary operator to create a boolean node that processes an AND operation with short circuit rules to process the operation faster.

// Binary node can be registered using this macro. Specifying the operator used is enough
namespace maxon
{
namespace corenodes
{
MAXON_CORENODE_OPERATOR_BINARY(HandbookBooleanOperatorANDNode, &&, Bool, Bool);
// Define the short circuit rule that will be used with this node
static const ShortCircuitRule g_handbookAndRules[] = {
{0, 0, -1, 0}, // if input 0 is 0, return 0
{1, 0, -1, 0}, // if input 1 is 0, return 0
{0, 1, 1, -1}, // if input 0 is 1, return input 1
{1, 1, 0, -1} }; // if input 1 is 1, return input 0
// Register the corenode using meta to specify the ShortCircuitRule it must use as an optimizer
MAXON_CORENODE_REGISTER_PURE_WITH_METADATA(HandbookBooleanOperatorANDNode,
"net.maxonexample.handbook.corenode.and",
} // end namespace corenodes
} // end namespace maxon
static MAXON_METHOD Optimizer CreateOptimizer(const Block< const ShortCircuitRule > &rules)
#define MAXON_CORENODE_REGISTER_PURE_WITH_METADATA(CLS, id, META,...)
Definition: corenodes.h:1052
maxon::Bool Bool
Definition: ge_sys_math.h:55
Delegate< Result< Opt< OptimizationInfo > >(const CoreNode &node, const Block< const Tuple< TrivialDataPtr, CORENODE_PORT_FLAGS > > &args)> Optimizer
Definition: corenodes.h:49

This is the same with an OperatorNode. In this example, the class and the function that will process the data must be defined.

// Create the class to store the process function that will be executed for the core node.
namespace maxon
{
namespace corenodes
{
class HandbookBooleanOperatorNANDNode : public OperatorNode<HandbookBooleanOperatorNANDNode,
Bool(Bool, Bool)>
{
public:
static ResultOk<void> Process(Bool& out, Bool a, Bool b) { out = !(a & b); return OK; }
};
// Create the short circuit rule that will be registered as the one to be used for this node
static const ShortCircuitRule g_handbookNandRules[] = { {0, 0, -1, 0}, // if input 0 is 0, return 1
{1, 0, -1, 0} }; // if input 1 is 0, return 1
// Register the corenode using meta to specify the ShortCircuitRule it must use as an optimizer
MAXON_CORENODE_REGISTER_PURE_WITH_METADATA(HandbookBooleanOperatorNANDNode,
"net.maxonexample.handbook.corenode.nand",
} // end namespace corenodes
} // end namespace maxon

Implementing a CoreNode Providing Constants

A BasicMicroNode implementation can also be used to provide a selectable constant. This will be then, from an end-user point of view, a node without an input, as the user will usually only select the constant manually in a drop down menu. But the selected value of the drop is still an input port which could be driven. Examples for constants which could be provided in such a way would be mathematical constants like the numbers PI and e, a null-vector or physical constants like a certain color temperature in Kelvin. This can be implemented with a BasicMicroNode and a switch statement at the heart of its Process() method. The value of the input port, the drop down menu, could be interpreted as an enum for more clarity in a code project.

namespace maxon
{
using namespace corenodes;
class HandBookBasicConstant
{
public:
// Declaration of the input and output ports of the micro node group.
// Implementation of the single custom micro node.
{
public:
MAXON_ATTRIBUTE_FORCE_INLINE maxon::Result<void> Process(const Ports<in1, result>& ports) const
{
ports.result.Update(123456.789);
switch (ports.in1())
{
case 0:
ports.result.Update(maxon::PI);
break;
case 1:
ports.result.Update(maxon::PI05);
break;
case 2:
ports.result.Update(maxon::PI2);
break;
case 3:
ports.result.Update(maxon::PI_INV);
break;
case 4:
ports.result.Update(maxon::PI05_INV);
break;
case 5:
ports.result.Update(maxon::PI2_INV);
break;
case 6:
ports.result.Update(1.61803398875);
break;
}
return maxon::OK;
}
};
static maxon::Result<void> Init(const maxon::corenodes::MicroNodeGroupRef& group)
{
group.AddChild<Impl>() iferr_return;
// RegisterValueChangedMessage allow you to register a delegate function that will be called
// when the value of a port is changed by the user.
// The delegate will not be called if the value of the port is changed by a connection.
maxonexample::HANDBOOK::NODE::CONSTANTNODE::GetId(),
maxonexample::HANDBOOK::NODE::CONSTANTNODE::IN1,
maxon::DESCRIPTION::UI::BASE::COMMANDCONTEXT.ENUM_NIMBUSCORE,
nullptr,
nullptr,
ConstantValueChanged)) iferr_return;
return maxon::OK;
}
};
MAXON_CORENODE_REGISTER_PURE(HandBookBasicConstant,
"net.maxonexample.handbook.corenode.HandBookBasicConstant");
}
PyObject PyObject * result
Definition: abstract.h:43
#define MAXON_ATTRIBUTE_FORCE_INLINE
Definition: apibase.h:111
Float64 Float
Definition: apibase.h:197
static constexpr Float64 PI2
floating point constant: 2.0 * PI
Definition: apibasemath.h:145
static constexpr Float64 PI2_INV
floating point constant: 1.0 / (2.0 * PI)
Definition: apibasemath.h:148
static constexpr Float64 PI_INV
floating point constant: 1.0 / PI
Definition: apibasemath.h:142
static constexpr Float64 PI
floating point constant: PI
Definition: apibasemath.h:139
static constexpr Float64 PI05_INV
floating point constant: 1.0 / (0.5 * PI)
Definition: apibasemath.h:154
static constexpr Float64 PI05
floating point constant: 0.5 * PI
Definition: apibasemath.h:151

But the node description system also allows for declaring the port data type directly as shown in the screen-shot below. If the port should not to appear in the node editor, in order to present it as a node "without inputs" to the user, the option "Hide Port in Nodegraph" has to be checked for the input port.