Migrating Plugins to the 2024.0 API

Provides an overview of the fundamental API changes introduced with Cinema 4D 2024.0 API.


One of the major changes of Cinema 4D 2024 release is the general improvement of scene execution speed achieved trough careful modifications of central parts of the classic API. It is important that plugins follow these changes faithfully as plugins substantially determine the scene execution speed of a loaded document. Since scene execution happens largely in the so called classic API, most changes can be found in the cinema.framework. The most important abstract changes are:

  • Changes to the constness of overridable member functions.
  • Changes to the constness of return values of functions in general, and constructors in particular.
  • A new data access system for Classic API scene elements.
  • Changes to the sampling of FieldObject and EffectorData beyond pure constness changes.
  • We are moving towards removing all unsafe C-style casts from our code base. Avoiding C-style casts always has been a rule in our style guide, but plugin authors are more than ever encouraged to follow these rule too.

For a more detailed overview, please refer to the list below and the example plugin. As a general set of best practice rules:

  • The cost of migration is usually much lower than such large number of changes might imply. It took us two days to migrate the SDK code base and five days to migrate the Redshift code base, with a singular developer carrying out the changes each time.
  • Give plugins that derive from NodeData priority as they will be the most costly tasks.
  • Fixing is done best by attempting to compile a code base and then fixing errors one-by-one. Avoid doing fixes blindly.
  • Focus on fixing compilation errors on declarations first. For the average plugin, fixing declaration and definition signatures will be the vast majority of the work. Fixing the actual implementations will often only make up a small portion of the migration work.
  • When you run into troubles, please do not hesitate to reach out to us via backstage or sdk_support(at)maxon(dot)net, the SDK group is happy to help you along your migration journey.

We unfortunately cannot make any development cost predictions for multi-version code base solutions, as these never have been supported by the SDK group as stated in the Development Environments manual. In the custom circumstances of the 2024.0 API migrations we encourage developers who use such solutions and run into troubles, to reach out to us for help.

The change documentation is accompanied by a new section in the SDK, \plugins\example.migration_2024, containing a NodeData plugin migration example (Fig. 1) in oboundingbox.cpp and oboundingbox_legacy.cpp, as well as other examples in change_examples.cpp.

Fig 1: The example plugin ObjectData plugin Oboundingbox exemplifies a plugin that has been migrated from the 2023.2 to the 2024.0 API. The plugin generates a bounding box geometry for the its first child as its cache. Additionally, it counts the read access events to its Display Mode parameter. The migration example focuses on const-ness changes and patterns to deal with them, as well as the new data dependency system.
GitHub links set on this page do not yet resolve. Please use the manually provided sdk.zip instead.

Related Topics

SDK Plugins 2024.0 API Migration Example

Demonstrates the migration of a NodeData derived plugin from the 2023.2 to the 2024.0 API.

Changes Constness of Overridable Methods Demonstrates how adapt to the constness changes of overridable methods.
Node Initialization Demonstrates the purpose of the new isCloneInit argument of NodeData::Init.
Node Data Access System Demonstrates the the usage of the new node data access dependency system.
Node Data Container Access Demonstrates how to access the data container of scene elements.
Node Branching with GetBranchInfo Demonstrates the slightly changed syntax of accessing node branches.
Copy On Write Scene Data Demonstrates the copy-on-write changes made to VariableTag and BaseSelect.
Custom Data Type Access Demonstrates how to access custom data type fields in BaseContainer and GeData instances.
Instantiating DescID Identifiers Demonstrates how to construct compile-time and runtime constant DescID identifiers.
Accessing Observables Demonstrates the changes made to the allocation and access of observables.
Gradient and Field Sampling Demonstrates the changes made to Gradient and FieldObject sampling.
Storing Data with AttributeTuple Demonstrates the new and performant type maxon::AttributeTuple to store and index data with MAXON_ATTRIBUTE keys which are known at compile-time.
Casting Between Data Types Outlines the slightly changed style recommendations for casting between data types.
Support for Custom Qualifiers Demonstrates how to define custom function and variable qualifiers in the 2024 API.


Constness of Overridable Methods

Demonstrates how adapt to the constness changes of overridable methods.

The overarching theme of the 2024.0 API changes is the more precise distinction between mutable and immutable data. This is done so that data can be reused and so that scene execution can be more effectively parallelized. The foundation of all this is the correct application of const-ness to methods in the Cinema 4D API. This has happened for many non-overrideable types where adaptions in third party code are often not necessary or only yield a performance benefit. But legacy plugin code that implements overridable types such as ObjectData or TagData must be adapted. Multiple NodeData methods and the methods of derived types have been made cosnt to allow for such more efficient scene execution. An example for such change is NodeData::GetDParameter where both the method itself as well as the node passed to it have become const:

virtual Bool NodeData::GetDParameter(const GeListNode* node, const DescID& id, GeData& t_data, DESCFLAGS_GET& flags) const

const methods of NodeData derived plugin hooks signal that neither the plugin hook instance, e.g., MyObjectData, nor the scene element representing them, e.g., a BaseObject of type Omyobject, should be modified. These methods are usually executed in parallel.

In general, one should strive for embracing these restrictions, as circumventing them can be expensive both in development and runtime costs and can potentially lead to crashes when not carried out properly. The example shown below is taken from the example.migration_2024 plugin Oboundingbox. The legacy version of that plugin counted the access to one of its parameters in its NodeData::GetDParameter method by incrementing a class bound counter field _get_display_mode_count. The 2024.0 code shown below demonstrates different approaches for translating such feature requirement to a method which is now const.

Bool BoundingBoxObject::GetDParameter(
const GeListNode* node, const DescID& id, GeData& t_data, DESCFLAGS_GET& flags) const
return false;
// When the #ID_OBOUNDBOX_DISPLAY_MODE parameter is being accessed, increment the counter field.
// In 2024.0 we cannot do this anymore, as this method is now const.
// always to design an application in such manner that const methods do not have modify field
// values.
// It might be tempting to simply remove the constness, but that SHOULD BE AVOIDED AT ALL COSTS
// as it CAN LEAD TO CRASHES. We can guard _get_display_mode_count with a lock to circumvent
// the access violation problem but since #this is const, the lock can not be instance bound
// (as engaging the lock means modifying a field value too). As a compromise, we can use a
// global lock that guards access to #_get_display_mode_count of all #BoundingBoxObject
// instances at once. With that we forcefully sequentialize the execution of GetDParameter
// calls of all #BoundingBoxObject instances; which is of course undesirable.
// When casting away the constness, we should use the macro #MAXON_REMOVE_CONST. It is just
// an alias for #const_cast, but it will help us to later find places in our code where we made
// such quick and dirty solution compromises which should be fixed later.
maxon::ScopedLock guard (g_boundingbox_object_data_lock);
// When possible, it is best to use one of the Atomic<T> templates instead to store values which
// must be modified in const contexts. Fields of such type can be modified within a const method.
// But just as guarding a common field with a lock, this type can be expensive to use. But it
// is always faster than a lock/field combination shown above.
// Alternatively, we can defer things to the main thread with ExecuteOnMainThread. Removing the
// constness of #node is here unproblematic, as consecutive access is guaranteed on the main
// thread. But the execution order is not guaranteed, queuing [A, B, C] can result in the
// execution order [B, C, A]. Which is unproblematic in our case since incrementing a value is
// commutative. It is important to call #ExecuteOnMainThread with WAITMODE::DONT_WAIT as we
// would otherwise tie this method to the main thread. This approach is relatively fast, but we
// give up order of operation and we cannot rely directly on a changed state since we do not
// wait for state changes by design.
// We use a WeakRawPtr around #node to shield ourselves against access violations when #node
// is already deleted once the MT runs the lambda below. #weakPtr.Get() will then return the
// #nullptr.
const Int32 currentValue = _message_get_display_mode_count;
[&currentValue, weakPtr = std::move(weakPtr)]()
if (!weakPtr.Get())
// Call the Message method of #node which a message that indicates that its counter
// field should be incremented. We could of course also have passed #this onto the MT and
// could directly modify a field from there.
BaseContainer msgData = BaseContainer(PID_OBOUNDINGBOX);
msgData.SetInt32(0, currentValue);
MAXON_REMOVE_CONST(weakPtr.Get())->Message(MSG_BASECONTAINER, &msgData);
// AVOID SENDING CORE MESSAGES to convey data changes from within const methods. While
// SpecialEventAdd will also run from an non-main-thread thread, calling it acquires the global
// core message lock which is slow. It will also take its toll by broadcasting many messages to
// everyone hooked into the core message stream, as NodeData methods can be called quite often.
// Finally, it will also very likely lead to access violation related crashes.
// NEVER DO THIS: Send a message to the core that the counter for the message data (void*)node
// should be incremented.
// SpecialEventAdd(ID_INCREMENT_COUNTER, reinterpret_cast<std::uintptr_t>(node), 0);
return SUPER::GetDParameter(node, id, t_data, flags);
PyCompilerFlags * flags
Definition: ast.h:14
Definition: c4d_basecontainer.h:48
void SetInt32(Int32 id, Int32 l)
Definition: c4d_basecontainer.h:579
Definition: lib_description.h:355
Definition: c4d_gedata.h:83
Represents a C4DAtom that resides in a 4D list.
Definition: c4d_baselist.h:1844
Definition: spinlock.h:255
Definition: weakrawptr.h:26
maxon::Bool Bool
Definition: ge_sys_math.h:55
maxon::Int32 Int32
Definition: ge_sys_math.h:60
Definition: apibase.h:2898
Definition: ge_prepass.h:3317
Message with a container, for example from a Python plugin. The corresponding data is BaseContainer.
Definition: c4d_baselist.h:371
#define MAXON_UNLIKELY(...)
Definition: compilerdetection.h:422
auto ExecuteOnMainThread(FN &&fn, WAITMODE waitMode=WAITMODE::DEFAULT, TimeValue wait=TIMEVALUE_INFINITE) -> decltype(fn())
Definition: thread.h:659
For ExecuteOnMainThread only: Do not wait.
Definition: node.h:10

Node Initialization

Demonstrates the purpose of the new isCloneInit argument of NodeData::Init.

While it is possible to ignore this change except for the formal signature change, third parties are strongly encouraged to minimize their initialization costs by avoiding unnecessary data container and other initializations in favor of copying data with NodeData::CopyTo.

With the introduction of the Asset Browser, scene elements often must be reinitialized to drive the Asset API preset system. isCloneInit indicates now such cloned node allocations which are usually deallocated right after their allocation. It is not necessary to initialize the data container of a node in these cases, as Cinema 4D will copy the data from a source node right after such isCloneInit call. Using this argument and NodeData::CopyTo, plugin authors can now also avoid doing expensive initialization computations more than once for the same data.

The example below is taken from the example.migration_2024 plugin Oboundingbox. The ObjectData plugin skips initializing its data container when isCloneInit is true.

Bool BoundingBoxObject::Init(GeListNode* node, Bool isCloneInit)
// We should use C++ style casts instead of C style casts.
BaseObject* obj = static_cast<BaseObject*>(node);
BaseContainer bc = obj->GetDataInstanceRef();
// In 2024.0 the #isCloneInit argument has been added to throttle initialization overhead for
// cloned scene elements. The data container values will be copied right after this
// NodeData::Init call and we therefore do not have to initalize the data of #node. Overwrite
// #NodeData::CopyTo to customize the copying behavior, to for example also copy fields of your
// hook instance. All expensive computations should be done after such #isCloneInit check.
if (isCloneInit)
return true;
// The conversion of atomic values, such as float to int should be done with constructors
// instead of C-style casts.
_get_display_mode_count = Int32(0.0);
// An Atomic<T> field should be initialized with its Set method.
return true;
Bool BoundingBoxObject::CopyTo(NodeData* dest, const GeListNode* snode, GeListNode* dnode,
COPYFLAGS flags, AliasTrans* trn) const
// In 2024.0, it should become much more common to overwrite ::CopyTo. NodeData::Init can be
// called very often now, and expensive intilization cost should be therefore avoided for node
// coping events using the #isCloneInit argument of ::Init.
// Copying our get access counter fields serves here as a stand-in for copying expensive to
// compute initialization data.
BoundingBoxObject* hook = static_cast<BoundingBoxObject*>(dest);
if (!hook)
return false;
// Copy the get access states to the plugin hook instance for the copied node #dnode.
hook->_get_display_mode_count = this->_get_display_mode_count;
hook->_message_get_display_mode_count = this->_message_get_display_mode_count;
return SUPER::CopyTo(dest, snode, dnode, flags, trn);
Definition: c4d_baselist.h:3157
Definition: c4d_baseobject.h:248
Definition: c4d_nodedata.h:40
PyObject * obj
Definition: complexobject.h:60
Definition: ge_prepass.h:2855
struct _node node

Node Data Access System

Demonstrates the the usage of the new node data access dependency system.

Implementing this method is not necessary for a plugin to compile or function. The base implementation will then take over and mark everything as dependent. But to optimize especially computationally complex plugins, the method should be implemented for the relevant contexts.

The classic API provides now an all new data dependency system for scene elements. It allows for the more effective parallelized execution of certain stages of the scene execution such as generator or deformation cache building. While the system does realize a long desired dependency system between classic API scene elements, it is not a user oriented system at the moment. Its only purpose is the optimized parallelization of scene execution. The frontend of the system can be found in BaseList2D:

The backend of the system can be found in NodeData and c4d_accessedobjects.h:

  • NodeData::GetAccessedObjects: Override this method to express custom data access information for a node type.
  • EffectorData::GetAccessedObjects: Override this method to express custom data access information for an effector type.
  • AccessedObjectsCallback: Used to signal data access information to the core of Cinema 4D.
  • AccessedObjectsCallback::MayAccess: Signals read and write access information for a given node to the core of Cinema 4D.
  • METHOD_ID: Expresses data access events such as the building of caches or the sampling of fields.
  • ACCESSED_OBJECTS_MASK: Expresses the type of data that is being accessed in a node, as for example caches, transforms, or the data container.

The example below is taken from the example.migration_2024 plugin Oboundingbox. The ObjectData plugin returns a bounding box geometry for its first child object as its generator cache. In its NodeData::GetAccessedObjects method, the plugin then expresses its data dependencies for building that generator cache.

maxon::Result<Bool> BoundingBoxObject::GetAccessedObjects(
// Express data access for the event that caches must be built with ::GetVirtualObjects (GVO).
// We first deal with the case that there are valid inputs for our plugin node, i.e., the case
// that it has a child object. The data access on the actual plugin node, here #node, in GVO
// #op, is also affected by this, because in GVO we act differently with #op when there is no
// no child object.
const BaseList2D* const firstChild = static_cast<const BaseList2D*>(node->GetDown());
if (firstChild)
// We express which data is being accessed on the actual plugin node, i.e., #op in GVO. Our
// implementation reads its data container and matrix and writes the cache. If our GVO would
// also modify the data container of #op (which is not advisable to do) we would have to
node, // Plugin node
// We express which data is being accessed for the only input node of our plugin node,
// i.e., the first child object #input in GVO. We read the global matrix and the cache of
// #input, but do not write any data. The CACHE read access is a result of us calling
// BaseObject::GetMp() on #input. When unsure about the data access of a method,
// use ::ALL | ::GLOBAL_MATRIX to mark everything as relevant (ALL does not include
// matrix changes).
firstChild, // Child node
// #op/#node has no child node and we blindly return `BaseObject::Alloc(Onull)` in GVO. So,
// the only the thing we do is modify the cache of #node. Accessing the hierarchy of a node,
// e.g., `op->GetDown()`, does not count as data access because the scene state is assumed to
// be static in parallelized methods such as GetVirtualObjects().
node, // Plugin node
// --- snip -----------------------------------------------------------------------------------
// The two extra cases shown below are not necessary for this plugin and have a purely
// illustrative purpose here.
// In cases where a node relies on whole hierarchies, we can use one of the convenience methods
// on BaseList2D such as ::GetAccessedObjectsRec or ::GetAccessedObjectsOfHierarchy to build
// all access information in one call, passing in our #access object.
static const Bool dependsOnFullHierachy = false && firstChild;
if (MAXON_UNLIKELY(dependsOnFullHierachy))
if (!result)
return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not gather accessed data."_s);
// In cases where we must read data from nodes to make further decisions on what is accessed
// data, we must avoid access violations by using AccessedObjectsCallback::EnsureReadable.
static const Bool hasLinkedNode = false;
if (MAXON_UNLIKELY(hasLinkedNode))
// We indicate that we want to access the data container of #node. Calling ::EnsureReadable
// will wait until we can access the data.
node, // The node to ensure access for.
ACCESSED_OBJECTS_MASK::DATA // The data to access.
// We then attempt to access a BaseLink at ID 2000. When the link is populated, we mark the
// global matrix and data container of the linked node as relevant data to read.
GeData data;
node->GetParameter(ConstDescID(DescLevel(2000)), data, DESCFLAGS_GET::NONE);
const BaseList2D* const link = data.GetLink(node->GetDocument());
if (link)
// --- snip -----------------------------------------------------------------------------------
// Otherwise terminate the data access polling for this context by returning #true.
return true;
// For all other cases, let the base implementation take over. IT IS IMPORTANT TO DO THIS when
// overwriting the method, as Cinema 4D will otherwise loose all data access information on this
// node type which has not explicitly been implemented by us (the base implementation marks
// everything as as relevant), possibly leading to crashes.
return SUPER::GetAccessedObjects(node, method, access);
The cache of the object will be accessed. Combination of CACHE_FLAG and BITS as CACHE_FLAG implies BI...
The matrix will be accessed (including e.g. frozen matrix). This doesn't include the global matrix.
Data accessible via Get/SetParameter (including data stored in the BaseContainer) will be accessed.
The global matrix will be accessed. Combination of GLOBAL_MATRIX_FLAG and MATRIX as GLOBAL_MATRIX_FLA...
Definition: c4d_accessedobjects.h:87
BaseObject::GetVirtualObjects method of generators. For spline generators this includes the methods G...
Definition: c4d_accessedobjects.h:132
MAXON_METHOD maxon::Result< void > EnsureReadable(const BaseList2D *, ACCESSED_OBJECTS_MASK)
Definition: c4d_baselist.h:2245
maxon::Result< Bool > GetAccessedObjectsOfHierarchy(ACCESSED_OBJECTS_MASK read, ACCESSED_OBJECTS_MASK write, METHOD_ID method, AccessedObjectsCallback &access) const
const BaseList2D * GetLink(const BaseDocument *doc, Int32 instanceof=0) const
Definition: resultbase.h:766
PyObject PyObject * result
Definition: abstract.h:43
#define yield_return
Definition: delegate.h:896
#define yield_scope
Definition: delegate.h:895
Definition: memoryallocationbase.h:67
#define ConstDescID(...)
Definition: lib_description.h:594
#define iferr_scope
Definition: resultbase.h:1386
#define iferr_return
Definition: resultbase.h:1521
struct _Py_Identifier struct _Py_Identifier PyObject struct _Py_Identifier PyObject PyObject struct _Py_Identifier PyObject PyObject PyObject ** method
Definition: object.h:330
Represents a level within a DescID.
Definition: lib_description.h:298

Node Data Container Access

Demonstrates how to access the data container of scene elements.

Legacy code will compile and run without adaptions but adhering to these changes will yield performance improvements.

Reworked was also the data container access for scene elements, now differentiating clearly differentiating between read and write access. It is recommended to use the reference methods over the pointer methods.

Containers should be copied with the BaseContainer copy constructors or one of its copy methods. BaseContainer BaseList2D::GetData will be deprecated, as its copying behavior has often been invoked unintentionally.

// Access the data container of the scene element #op for pure read access.
const BaseContainer readOnly = op->GetDataInstanceRef();
const String name = readOnly.GetString(ID_BASELIST_NAME, "default"_s);
// Access the data container of the scene element #op for read and write access.
BaseContainer readWrite = op->GetDataInstanceRef();
if (readWrite.GetString(ID_BASELIST_NAME, "default"_s) == "Hello World"_s)
readWrite.SetString(ID_BASELIST_NAME, "42 is the best number"_s);
// A data container should not be copied anymore with BaseList2D::GetData as doing this makes it
// hard to spot cases where this is done unintentionally. Use the copy constructor or methods
// instead.
BaseContainer copy = BaseContainer(op->GetDataInstanceRef());
BaseContainer alsoCopy = *op->GetDataInstanceRef().GetClone(COPYFLAGS::NONE, nullptr);
BaseContainer yetAnotherCopy;
op->GetDataInstanceRef().CopyTo(&yetAnotherCopy, COPYFLAGS::NONE, nullptr);
const char const char * name
Definition: abstract.h:195
Definition: ge_autoptr.h:37
String GetString(Int32 id, const maxon::String &preset=maxon::String()) const
Definition: c4d_basecontainer.h:424
void SetString(Int32 id, const maxon::String &s)
Definition: c4d_basecontainer.h:643
Definition: c4d_string.h:39
#define CheckArgument(condition,...)
Definition: errorbase.h:486
Definition: obaselist.h:7

Node Branching with GetBranchInfo

Demonstrates the slightly changed syntax of accessing node branches.

GeListNode::GetBranchInfo now operates with the concept of a maxon::ValueReceiver and therefore also returns a maxon::Result. This eliminates the need of predicting the number of branches that must be accessed and allows for terminating searches early.

BaseTag* tag;
// Branching information is now dealt with a ValueReciever which frees us from the burden of
// having to set an upper limit of branches we want to access at most. The method now also returns
// a Result<Bool> as all methods do which have a ValueReciever argument.
// As always, the most convenient form of providing a value receiver is a lambda. Because it
// allows us to filter while searching and then break the search early once we found our data.
// Find the first phong tag in the branches of #op, but only got into branches which actually
// holds nodes (Cinema 4D often creates branches which are empty for later usage).
Bool result = op->GetBranchInfo(
ApplicationOutput("Branch of @ with name: @", op->GetName(), info.name);
ApplicationOutput("Branch of @ with id: @", op->GetName(), info.id);
if (info.head->GetFirst() && info.head->GetFirst()->GetType() == Tphong)
tag = static_cast<BaseTag*>(info.head->GetFirst());
return true; // Break the search early since we found a phong tag.
return false; // Continue searching/iterating.
ApplicationOutput("Brach traversal has been terminated early: @", result); // Will be true
ApplicationOutput("Tag: @", tag ? tag->GetName() : nullptr);
// Alternatively, we can also use a collection type as the ValueReciever when we are interested
// in receiving all values.
op->GetBranchInfo(branches, GETBRANCHINFO::ONLYWITHCHILDREN) iferr_return;
// In cases where we want to replace legacy code and do not want to use a lambda with a counter
// variable but do want to retain an upper limit of retained branches, we can use a
// #BufferedBaseArray. Also remember that for dealing with error results in methods without error
// handling, we can use #iferr_scope_handler.
auto myFunc = [&op]() -> bool
// Error handler which could be for example inside a Message() -> bool function. When an error
// occurs, we print it to the diagnostics output and terminate.
return false;
// Only get the first 8 branches of #op.
return true;
String GetName() const
Definition: c4d_baselist.h:2412
Definition: basearray.h:415
Only return branch if it is in use, i.e. has content.
typename BufferedBaseArraySelector< COUNT, MINCHUNKSIZE, MEMFLAGS, ALLOCATOR >::template Type< T > BufferedBaseArray
Definition: basearray.h:1810
#define ApplicationOutput(formatString,...)
Definition: debugdiagnostics.h:210
#define DebugOutput(flags, formatString,...)
Definition: debugdiagnostics.h:168
Diagnostic output, shows up if this group of output is activated. This is also the default.
#define Tphong
Definition: ge_prepass.h:1399
_Py_clock_info_t * info
Definition: pytime.h:197
#define iferr_scope_handler
Definition: resultbase.h:1404
Definition: c4d_baselist.h:1216

Copy On Write Scene Data

Demonstrates the copy-on-write changes made to VariableTag and BaseSelect.

Legacy code will compile and run without adaptions but adhering to these changes will yield performance improvements.

The types VariableTag and BaseTag now store their internal data as copy-on-write (COW) references. This has far-reaching consequences because VariableTag is the storage form of most fundamental scene data such as points, polygons, normals, tangents, and vertex data. Creating for example two Cube object instances with the exact same settings, will have the result that the Tpoint, Tpolygon, and `Tuvw tags in their generator caches reference the same data in memory (Fig. 2). Effectively, there is only the geometry data for one cube object being stored memory, without any explicit actions from the user such as an Instance object.

Fig 2: Shown is a scene containing two cubes objects with identical parameters in their Object tab. Using the newly added RefCnt column of the Active Object plugin in the C++ SDK, we can see that the Tuvw(5671), Tpoint(5600), and Tpolygon(5604) tags in the caches of these two generator objects each have a reference count of two; the geometry data is being shared between the two objects.

Similar optimizations can also happen for editable geometry. Internally, these optimizations rely on the already existing memoization optimization of the modeling core of Cinema 4D. The referencing of data is being updated on the following events:

  • Load: Cinema 4D will attempt to build reference relations when a document is loaded and by that compact scene data.
  • Copy: When objects or tags are copied, the copy will be in a reference relation with the source.
  • Update: Modifying the parameters of a generator or deformer or modifying editable geometry can sever or reestablish reference relations.

The example shown below highlights the differences between read and write access for VariableTag and BaseTag instances.

// #polyCube is a PolygonObject, see the full example for details. We get the point data of the
// object stored in its #Tpoint tag.
VariableTag* const pointTag = static_cast<VariableTag*>(polyCube->GetTag(Tpoint));
// Requesting access to the writable data of a VariableTag with ::GetLowlevelDataAddressW will
// always trigger a copy being made when data is being shared between multiple entities; no matter
// if write operations actually occur or not. We should therefore be very conservative with calling
// this method.
// Will copy the point data of #pointTag when the reference count is higher than one, although
// we actually do not write any data.
Vector* points = reinterpret_cast<Vector*>(pointTag->GetLowlevelDataAddressW());
ApplicationOutput("points[0] = @", points[0]);
// Similarly, selection states as represented by the type BaseSelect are manged in a COW-manner
// too. This applies to implicitly stored selection states such as the selected and hidden element
// states on #Point-, #Spline-, and #PolygonObject instances.
// Access the point selection state of #polyCube for read-only purposes.
const BaseSelect* const readPointSelection = polyCube->GetPointS();
// Access the point selection state of #polyCube for read-write purposes.
BaseSelect* const readWritePointSelection = polyCube->GetWritablePointS();
// And it applies to explicitly stored selection states in form of Selection tags. Data being
// shared among multiple entities is more likely here due to the long-term storage nature of
// selection tags.
// Attempt to get the first point selection tag on #polyCube.
SelectionTag* const selectionTag = static_cast<SelectionTag*>(polyCube->GetTag(Tpointselection));
// Access read-only data by using #GetBaseSelect, shared data references cannot be severed, as
// it is not possible to accidentally call one of the methods causing that.
const BaseSelect* const readStoredPointSelection = selectionTag->GetBaseSelect();
for (Int32 i = 0; i < readStoredPointSelection->GetCount(); i++)
ApplicationOutput("Point @ is selected: @", i, readStoredPointSelection->IsSelected(i));
// Opposed to #VariableTag read-write access, accessing a writable #BaseSelect instance with
// methods like #::GetWritablePointS or #::GetWritableBaseSelect will not automatically cause
// the internal data to be copied. Only when invoking one of the non-const methods of #BaseSelect
// that modify the state of the selection, such as Select(All), Deselect(All), Toggle(All),
// CopyTo, etc., will the COW-mechanism trigger and copy the data when it is also referenced by
// others.
// Reading data on a writable BaseSelect will never cause COW copies to trigger.
BaseSelect* const readWriteStoredPointSelection = selectionTag->GetWritableBaseSelect();
for (Int32 i = 0; i < readWriteStoredPointSelection->GetCount(); i++)
ApplicationOutput("Point @ is selected: @", i, readWriteStoredPointSelection->IsSelected(i));
// But calling for example #Select on a shared BaseSelect will.
Py_ssize_t i
Definition: abstract.h:645
Definition: c4d_baseselect.h:33
Bool Select(Int32 num)
Definition: c4d_baseselect.h:82
Int32 GetCount() const
Definition: c4d_baseselect.h:63
Bool IsSelected(Int32 num) const
Definition: c4d_baseselect.h:163
Definition: c4d_basetag.h:356
BaseSelect * GetWritableBaseSelect()
const BaseSelect * GetBaseSelect() const
Definition: c4d_basetag.h:129
void * GetLowlevelDataAddressW()
#define Tpointselection
Point selection - SelectionTag.
Definition: ge_prepass.h:1413
#define Tpoint
Point - PointTag.
Definition: ge_prepass.h:1398

Custom Data Type Access

Demonstrates how to access custom data type fields in BaseContainer and GeData instances.

Access to custom data type fields has been streamlined with read-only access methods and templated methods which directly return the data without any casting.

GeData data;
// Read only access to the falloff gradient of the light object.
const Gradient* const a = data.GetCustomDataType<const Gradient>();
// The legacy interface for the same operation. Should not be used anymore, but can serve as a
// temporary fix by adding the "I" to otherwise already modern legacy code.
const Gradient* b = static_cast<const Gradient*>(data.GetCustomDataTypeI(CUSTOMDATATYPE_GRADIENT));
// Read and write access to the falloff gradient of the light object, in both the new templated,
// as well as the legacy variant.
// Similar changes have also been made to BaseContainer.
// Get the read and write container reference of the node so that we can show both read and
// write access for custom data types.
BaseContainer& bc = op->GetDataInstanceRef();
// Get read and read-write access to the object in-exclusion list of the light. For BaseContainer
// exist I-postfix legacy methods analogously to GeData.
DATATYPE * GetCustomDataTypeWritableObsolete(Int32 id)
Definition: c4d_basecontainer.h:531
const DATATYPE * GetCustomDataType(Int32 id) const
Definition: c4d_basecontainer.h:525
const DATATYPE * GetCustomDataType() const
Definition: c4d_gedata.h:542
const CustomDataType * GetCustomDataTypeI(Int32 datatype) const
Definition: c4d_gedata.h:558
CustomDataType * GetCustomDataTypeWritableI(Int32 datatype)
Definition: c4d_gedata.h:569
DATATYPE * GetCustomDataTypeWritable()
Definition: c4d_gedata.h:547
Definition: customgui_gradient.h:117
InExclude custom data type (CUSTOMDATATYPE_INEXCLUDE_LIST).
Definition: customgui_inexclude.h:115
PyFrameObject * f
Definition: ceval.h:26
Definition: unicodeobject.h:1200
Gradient custom data ID.
Definition: customgui_gradient.h:25
#define Olight
Definition: ge_prepass.h:1040
Py_ssize_t * e
Definition: longobject.h:89
Definition: olight.h:259
Definition: olight.h:299

Instantiating DescID Identifiers

Demonstrates how to construct compile-time and runtime constant DescID identifiers.

The type DescID now distinguishes between compile-time and runtime constant instances of itself for performance reasons.

  • ConstDescID: Instantiates a DescID where all level identifiers are compile-time constants.
  • CreateDescID: Instantiates a DescID where one or many level identifiers are only known at runtime.

There are also constructors and a ::Create method on DescID which are wrapped by these macros, but they should not be called manually. Most DescID constructor calls in old code should be replaced by ConstDescID, since most parameter IDs are usually compile-time constants.

Note that the macros cannot mimic the legacy constructor DescID::DescID(Int32 id1). Legacy code such as DescID(ID_MY_STUFF) MUST be replaced with ConstDescID(DescLevel(ID_BASELIST_NAME)). Ignoring this currently leads to cryptic compilation errors such as error C2065: XID_BASELIST_NAME undeclared identifier.
// Construct an identifier where all levels are compile-time constants, as for example here for
// the node name. This should replace most old DescID() constructor calls, as IDs are most of the
// time known at compile-time.
const DescID compileTimeId = ConstDescID(DescLevel(ID_BASELIST_NAME));
// We can also use the macro as an rvalue in a throwaway fashion.
GeData data;
ApplicationOutput("Name: @", data.GetString());
// In the rare cases where the levels of a DescID are only known at runtime, we must use
// CreateDescID instead. We here for example construct the first five user data identifiers from
// an array.
for (const auto item : { 1, 2, 3, 4, 5 })
const String & GetString() const
Definition: c4d_gedata.h:498
PyObject PyObject * item
Definition: dictobject.h:42
User data ID.
Definition: lib_description.h:25
#define CreateDescID(...)
Definition: lib_description.h:595

Accessing Observables

Demonstrates the changes made to the allocation and access of observables.

Observables are now only being allocated on demand to avoid allocating event-handlers (observables) without any subscribers (observers) being present.

MAXON_OBSERVABLE is a macro that causes the Source Processor to create a function on the reference of the interface the macro is placed on. These functions are therefore not part of the document space of Doxygen and cannot be documented. Each observable access function has a singular bool argument called create. Passing true will force a non-existing observable to be created, passing false will return an empty reference and effectively terminate chained calls such as .AddObserver, .Notify, or .RemoveObserver.

The decision what to pass in each case for create should be guided by the goal of minimizing idle event-handlers. As a rule of thumb, adding observers requires the observable to be created, but removing observers from an observable or notifying an observable can be terminated when the observable does not yet exist in the first place. There could, however, be cases where it for example only makes sense to add an observer when the observable does already exist or analogously to allocate a non-existing observable to notify it without any observers yet being present.

maxon::Result<void> AttachRepositoryObservers(const maxon::AssetRepositoryRef& repository,
// ...
// We attach a lambda observer to the "AssetStored" event of the passed #AssetRepositoryRef. By
// passing #true, we force the observable to be allocated when it hasn't been yet, and by
// that ensure that our observer is actually being attached.
maxon::FunctionBaseRef assetStoredFunc = repository.ObservableAssetStored(true).AddObserver(
[](const maxon::AssetDescription& newAsset) -> void
// ...
maxon::Result<void> DetachRepositoryObservers(const maxon::AssetRepositoryRef& repository,
// ...
// When removing an observer however, we do not really care if the observable does actually
// exist, and with that if the operation is actually carried out. When there is no observable,
// then there is also nothing to remove.
// ...
Definition: hashmap.h:1115
PyObject Py_tracefunc func
Definition: ceval.h:10

Gradient and Field Sampling

Demonstrates the changes made to Gradient and FieldObject sampling.

Gradient and FieldObject sampling has been changed so that all sampling related methods are now const to ensure performant sampling. This is achieved in both cases by helper data structures returned by the respective initialization methods, GradientRenderData for gradient sampling and maxon::GenericData for field sampling. Similar changes have been applied to EffectorData sampling, but operating plugin hooks is not part of the public API and therefore not here documented. Please reach out to the SDK group via sdk_support(at)maxon(dot)net in case you must sample your own EffectorData instances and run into troubles doing so.

Gradient sampling:

Gradient* alphaGradient = gradient->GetAlphaGradient();
// Define the gradient knots abstractly as ColorA, position tuples so that we do not have to set
// each alpha value manually.
// Add a knot at position 0% of solid red, a green knot with an alpha of 50% at position 25%, and
// finally a blue knot with an alpha of 100% at position 100%.
knotData.Append(Knot(maxon::ColorA(1., 0., 0., 0.), 0.)) iferr_return;
knotData.Append(Knot(maxon::ColorA(0., 1., 0., .5), .25)) iferr_return;
knotData.Append(Knot(maxon::ColorA(0., 0., 1., 1.), 1.)) iferr_return;
// Write the knot data into the gradient.
for (const auto& item : knotData)
// Write the color into the color gradient.
color.col = maxon::Color(item.first.r, item.first.g, item.first.b);
color.pos = item.second;
// Write the alpha into the color gradient.
alpha.col = maxon::Color(item.first.a);
alpha.pos = item.second;
// Sample the color portion of a gradient alone using ::PrepareRenderData.
maxon::GradientRenderData renderData = gradient->PrepareRenderData(irs) iferr_return;
maxon::Color color0 = renderData.CalcGradientPixel(0.);
maxon::Color color1 = renderData.CalcGradientPixel(1.);
ApplicationOutput("Sampling color information only: color0 = @, color1 = @", color0, color1);
// Sample the color and the alpha portion of a gradient alone using ::PrepareRenderDataWithAlpha.
// A GradientRenderDataTuple is just a Tuple<GradientRenderData, GradientRenderData> where the
// first element for evaluating the color gradient and second for evaluating the alpha gradient.
GradientRenderDataTuple renderDataTuple = gradient->PrepareRenderDataWithAlpha(irs) iferr_return;
color0 = renderDataTuple.first.CalcGradientPixel(0.);
color1 = renderDataTuple.first.CalcGradientPixel(1.);
maxon::Color alpha0 = renderDataTuple.second.CalcGradientPixel(0.);
maxon::Color alpha1 = renderDataTuple.second.CalcGradientPixel(1.);
"Sampling color and alpha information: color0 = @, color1 = @, alpha0 = @, alpha1 = @",
color0, color1, alpha0, alpha1);
BaseDocument * GetActiveDocument()
Int32 InsertKnot(const maxon::GradientKnot &knot)
Definition: c4d_shader.h:351
const BaseDocument * doc
The document to render. Can be nullptr, always check.
Definition: c4d_shader.h:390
Appends a new element at the end of the array and constructs it using the forwarded value.
Definition: basearray.h:619
Definition: tuple.h:611
Int32 Gradient mode: GRADIENTMODE
Definition: customgui_gradient.h:95
Color and alpha.
Definition: customgui_gradient.h:88
Col3< Float, 1 > Color
Definition: vector.h:84
A color consisting of three components R, G, B and an alpha.
Definition: col4.h:16
Represents a knot in a gradient.
Definition: gradient.h:40
Float pos
Definition: gradient.h:43
Color col
Definition: gradient.h:41

FieldObject sampling:

// #field is a #FieldObject sampled by the #BaseObject #effector. See the full example for details.
// The input location we want to sample and the output data. We are only interested in sampling
// FIELDSAMPLE_FLAG::VALUE, i.e., the field influence value of the field at point x.
FieldInput inputs(Vector(0, 50, 0));
FieldOutput outputs;
outputs.Resize(inputs.GetCount(), FIELDSAMPLE_FLAG::VALUE) iferr_return;
FieldOutputBlock outputBlock = outputs.GetBlock();
// Create the field info for sampling the sample data #inputs for the caller #effector.
// Sample the field. In 2024.0 we now must pass on the extra data generated by the sampling
// initialization so that #field can remain const for this operation.
maxon::GenericData extraData = field->InitSampling(info) iferr_return;
field->Sample(inputs, outputBlock, info, extraData, FIELDOBJECTSAMPLE_FLAG::NONE) iferr_return;
// Iterate over the output values.
for (const maxon::Float value : outputBlock._value)
ApplicationOutput("Sampled value: @", value);
field->FreeSampling(info, extraData);
PyObject * value
Definition: abstract.h:715
maxon::Vec3< maxon::Float64, 1 > Vector
Definition: ge_math.h:145
Float64 Float
Definition: apibase.h:222
Sample only the value at the current point (minimum must still sample the value)
Thread local information for this field sample invocation.
Definition: c4d_fielddata.h:840
static maxon::Result< FieldInfo > Create(const BaseList2D *caller, FIELDSAMPLE_FLAG callingFlags=FIELDSAMPLE_FLAG::VALUE)
Definition: c4d_fielddata.h:472
Definition: c4d_fielddata.h:345
maxon::Block< BFloat > _value
Definition: c4d_fielddata.h:453
Definition: c4d_fielddata.h:115
FieldOutputBlock GetBlock()
maxon::Result< void > Resize(Int newSize, FIELDSAMPLE_FLAG sampleFlags=FIELDSAMPLE_FLAG::ALL, maxon::COLLECTION_RESIZE_FLAGS resizeFlags=maxon::COLLECTION_RESIZE_FLAGS::DEFAULT)

Storing Data with AttributeTuple

Demonstrates the new and performant type maxon::AttributeTuple to store and index data with MAXON_ATTRIBUTE keys which are known at compile-time.

Instances of maxon::DataDictionary can become a bottleneck when being accessed thousands of times in quick succession. The type maxon::AttributeTuple provides a lightweight alternative for cases where the data schema is known at compile-time which acts like a named tuple and shares the the performance characteristics of native C-data-structures.

// An AttributeTuple can be used to replace DataDictionary instances carrying MAXON_ATTRIBUTE
// keys. The type is much more performant than a dictionary and should be used when all required
// fields are already known at compile-time. The type is equal in performance to native
// c-structures as access to its members is carried out directly without lookup or indirection.
// Define a data structure that carries the name and tags of an object ...
using NameTagContainer = maxon::AttributeTuple<true,
// ... and use it in a read and write fashion. The MAXON_ATTRIBUTE entities ::NAME and ::TAG
// are the field access keys for our #NameTagContainer data structure.
NameTagContainer myStuff;
myStuff[maxon::OBJECT::BASE::NAME] = "Hello world!"_s;
myStuff[maxon::OBJECT::BASE::TAGS] = "Bob; is; your; uncle; !;"_s;
ApplicationOutput("myStuff[maxon::OBJECT::BASE::NAME] = @", myStuff[maxon::OBJECT::BASE::NAME]);
ApplicationOutput("myStuff[maxon::OBJECT::BASE::TAGS] = @", myStuff[maxon::OBJECT::BASE::TAGS]);
Definition: attributetuple.h:38

Casting Between Data Types

Outlines the slightly changed style recommendations for casting between data types.

With the API overhaul, we removed a substantial amount of C-style casts from our code base because such casts are dangerous due to their unbound nature. C++ style casting is and will not become mandatory in Cinema 4D projects in the near future. But we encourage all third party developers more than ever to follow our example and remove them from their projects. A minor change has been made to our style guide, which now does recommend conversion constructors over C-style casts for fundamental types.

// C-style casts should be avoided in favor of explicit C++ casts.
const BaseObject* constOp = const_cast<BaseObject*>(op);
void* data = reinterpret_cast<void*>(op);
BaseList2D* a = (BaseList2D*)op; // No
BaseList2D* b = static_cast<BaseList2D*>(op); // Yes
BaseList2D* c = (BaseList2D*)data; // No
BaseList2D* d = reinterpret_cast<BaseList2D*>(data); // Yes
BaseObject* e = (BaseObject*)constOp; // No
BaseObject* f = const_cast<BaseObject*>(constOp); // Yes
// C-style casts between fundamental types should be avoided in favor of conversion constructors.
Float32 floatValue = 3.14F;
maxon::CString cString = "Hello World!"_cs;
Int32 g = (Int32)floatValue; // No
Int32 h = Int32(floatValue); // Yes
String i = (String)cString; // No
String j = String(cString); // Yes
Definition: string.h:1490
maxon::Float32 Float32
Definition: ge_sys_math.h:68
const char const char grammar * g
Definition: parsetok.h:52

Support for Custom Qualifiers

Demonstrates how to define custom function and variable qualifiers in the 2024 API.

With 2024, the source processor already comes with a predefined CONST_2024 symbol. Being provided is only the registration of the symbol not a define. But one can use that symbol to define a macro as shown below. Maxon still does not support multi version solutions and we decided to add this feature as a compromise due to the special circumstances of the 2024 release. Note that you will have to backport changes of the source processor to older versions for this syntax to work in older APIs. The relevant changes can be found in sdk/frameworks/settings/sourceprocessor in the files sourceprocessor.py and lexer.py. The change of the source processor is primarily intended as a fix to prevent similar problems in future versions of the API, as now qualifiers can be retroactively added without having to modify the source processor.

Migrating to the 2024 API might require third parties to use code such as shown below when they use multi version solutions:

#if API_VERSION >= 2024000
#define SDK_CONST const
#define SDK_CONST
class MyClass
SDK_CONST bool Foo(SDK_CONST String& value) SDK_CONST;

Although this is perfectly well-formed C++, compiling such code will fail on the source processor with an error message such as:

Source processor:
/.../solution/a.module/source/foo.h:12:3: error: Expected ';', found '}' instead.
PyObject * error
Definition: codecs.h:206
const Py_UNICODE * source
Definition: unicodeobject.h:54

The culprit is the custom qualifier SDK_CONST, as the source processor only allowed for a predefined pool of such function and variable qualifiers in the past. With 2024.0 the source processor allows to define custom qualifiers on a framework and file level.

// stylecheck.register-qualifiers = SDK_CONST
#if API_VERSION >= 2024000
#define SDK_CONST const
#define SDK_CONST
class MyClass
SDK_CONST bool Foo(SDK_CONST String& value) SDK_CONST;

With // stylecheck.register-qualifiers = Qualifier; [Qualifier; Qualifier; ...] we can register one to many qualifiers. Multiple stylecheck.register-qualifiers comments in a file act additively. It is recommended to put the registration calls at the top of the file, but it is only mandatory to place them before the first usage (not definition) of a custom qualifier symbol. So, this works too:

// Register two qualifiers at once.
// stylecheck.register-qualifiers = FOO; BAR
#define FOO const
#define BAR const
#include "boo.h" // Import BOO
class MyClass
FOO bool Foo(FOO String& value) FOO;
BAR bool Bar(BAR String& value) BAR;
// This is the latest point where we can register BOO in this file because in the next line it
// is being used. This call is additive, so the registered qualifiers are now FOO, BAR, and BOO
// in this file.
// stylecheck.register-qualifiers = BOO
BOO bool Bar(BOO String& value) BOO;

The parsing horizon of the source processor is a singular file, so all registrations made as comments in code only apply to the file they are made in. When you want to add modifiers on a broader level, you can add them to the project definition of a framework or module. Note that here only a singular statement is being supported and that their ins intentionally no broader scope than frameworks to register qualifiers.

The file solution/a.module/project/projectdefinition.txt:

// Configuration of a custom plugin in the projectdefinition.txt file
// support Windows and macOS
// this is a plugin
// this plugin depends on these frameworks:
cinema.framework; \
misc.framework; \
image.framework; \
// defines the level of rigour of the source processor's style check
// plugin/module ID
// Registers the qualifiers FOO and BAR for the a.module
stylecheck.register-qualifiers = FOO; BAR
Definition: ge_prepass.h:1

In the file solution/a.module/source/foo.h we can now do this:

#define FOO const
#define BAR const
#include "boo.h" // Import BOO
class MyClass
// Relies on FOO defined in the `projectdefinition.txt`.
FOO bool Foo(FOO String& value) FOO;
// BOO is registered on a file level, this is still additive.
// stylecheck.register-qualifiers = BOO
BOO bool Bar(BOO String& value) BOO;
// Relies on BAR defined in the `projectdefinition.txt`.
BAR bool Bar(BAR String& value) BAR;

But in a file solution/b.module/source/other.h this would not work because FOO and BAR have only been registered for the a.module.