uff, that is a lot of points. I'll try to comb through most of it, but I do not have that much time today. Feel free to ask for clarifications where I left out things.
Currently, I do this in the tag plugin's Execute() function. The tag plugin remembers the vertex color tag's pointer (initialized as nullptr) and dirty checksum (by C4DAtom::GetDirty(), and initialized as 0).
Never user pointers to long term keep track of scene elements. Something not being nullptr does not mean you point to valid data. The node you point to could be long deleted, and then you point to garbage and access attempts will then result in access violations and a crash. Either use a BaseLink to store a reference to a scene element, or use a weak pointer. I.e., a pointer which becomes invalid when the pointed data is being deleted. You can for example see here how to use a maxon::WeakRawPtr.
However, I found ExecutePasses() called in PrerollToTime() triggers the tag plugin's Execute() function (I think this is the cloned tag in the cloned document), and Execute() function tries to read the linked vertex color tag at the speficied frame again, so PrerollToTime() -> ExecutePasses() is called again, and triggers a new cloned tag in a new cloned document to run its Execute(), and so on.
Yes, that is true and a common problem (not only in Cinema 4D). The superficial reason is that my preroll code example also passes True for the third argument expressions (effectively API slang for tags). When you would pass there false, your tag would not be executed again. But also all other tags would not be executed (which is likely not what you want). The solution to this can be registering a plugin ID, e.g., ID_FOO_PLUGIN_IS_COMPUTE_PASS. When you then clone your document, or execute the passes on an existing document, you write under that ID for example a bool into the data container of the document. Your tags Execute(tag, ...) then gets the document from tag and checks for that flag being present, to stop what it does that causes the infinite update loop. When operating on a document not owned by you, you would then delete that flag or set it to false after you manually invoked the passes, so that future updates made by Cinema 4D (which owns and operates the document) then take the 'normal' route.
The better solution is to generally design things in a manner that such infinite update loops cannot happen in the first place. I do not understand your problem well enough to give more concrete advice here. These scenarios are also not always avoidable.
According to the sample code PrerollToTime(), the lastFrame (the target frame) must not be earlier than the current scene's time.
The preroll function was just an example, not something you must follow to the letter. There is also a little bug I just see now, it should be of course if (lastFrame < firstFrame - 1). But the general idea is, that when you have a scene with simulations, i.e., stuff like Pyro or particles, where the state of the current frame depends on the last frame, you must basically cycle through all the correct scene states to get to that state. You can of course also jump to a previous point, but then you do not have to sim from NOW to FUTURE but from 0 to PREVIOUS.
Prerolling is not necessary when the scene does not contain any simulations, e.g., just key framed position animations, all simulations are cached, or you do not care about simulations and their impact on the rest of scene. Given what kind of plugins you do develop, you likely want to preroll.
Just to stress this again: Prerolling can be extremely expensive. When the user has a scene with 300 frames of Pyro simulations worth 30 minutes of computing, and you try to preroll half of it, your preroll function will run for 15 minutes.
What to do here, really depends on the exact case. Generally you try to avoid having to reach into the future or the past, as this is often a big no-no. The most common "pro" solution to that is caching. E.g., that you write yourself a vertex color cache in your case. When you only need to reach into the past, and this only applies in rendering, you could also effectively hide this from the user, as you must have passed and naturally calculated that value before (and therefore need no explicit cache recording).
Will answer the (2) portion on Monday, running a bit out of Friday time right now.
that could be that I was wrong. But it was at least not an unintentional typo. That is how I remembered it and what is also usually the case in Cinema 4D. v3 is an alias for the k component of an i, j, k frame, i.e., the "z-axis". This is usually considered to be "the normal", i.e., when one constructs a frame, one makes it so that v3/k becomes the normal of what one is constructing that frame for. And to be verbose here, frame (of reference) is just some math slang for a coordinate system, i.e., what on a more general level is also called a "transform", or even more generic and how Cinema 4D handles it, a "matrix".
Bottom line is here, when v1 works for you as a normal, just use it, there is nothing which inherently makes k/v3 "the normal" of a frame. Maxime also touched spline transport some time ago, it could be that he switched things around.
There is only the Matrix Manual in Python, in C++ is nothing at all. Users are just supposed "to know" such stuff. I never find the time to do more than what I once did there. And just as warning, the normals of a spline are usually not what non-math people expect them to be, due to the fact that they are defined by curvature and that the concept of inflection points does exist. We once talked here about this, including an illustration.
That is why in CGI the normals of a spline are usually not the normals, but the parallel transport of some normal plus some upvector which determines the initial orientation. E.g., SplineHelp realizes parallel transport, so that you can for example have an "outline" of a spline which makes sense for artists. But as you might have noticed in the past as a user, outlining a spline does not always produce results a human would consider correct. Just search for "parallel tranport" here on the forum and limit the results to my user, we talked many times about it, including code examples etc.
So, long story short again, there is no mathematical way to compute the "perfect" normals for a spline, at least what humans consider this to be. And depending on the spline and the fanciness with which you implement parallel transport, you might be subject to normal banking and/or flipping issues. So, SplineHelp tries to give you normals/frames that humans would consider correct, but the quality of its efforts might vary.
Thank you for reaching out to us. Let's split these effectively two questions into two parts.
Evaluating a BaseDocument at time #t
When you want to know the state of a scene at a time, #t, you can do this by setting the time of the document (BaseDocument::SetTime) and then executing its passes (BaseDocument::ExecutePasses), which is CInema 4D API slang for updating the scene state. This is configurable and can evaluate animations, build caches, and execute expressions, i.e., do the whole scene evaluation. But when you have a very heavy document, executing the passes is of course a not so cheap operation.
A more clever solution can be to manually interpolate animations - when that is all you need - via CCurve.GetValue. But that only works for animations that defined by keyframes and are decomposable into float values. The former is here not given for vertex colors, as you can only animate them procedurally.
This is all further complicated in a real life scenario, as being at frame t, then jumping to frame t + 10, and then just executing the passes might not be enough. The reason is because what you to query might be dependent on simulations; and your output will then be wrong when you just execute frame t and t + 10, instead of t, t + 1, ..., t + 10.
So, a preroll function could look like this:
using namespace cinema;
static maxon::Result<void> PrerollToTime(BaseDocument* const doc, const BaseTime& time)
{
iferr_scope;
CheckArgument(doc, "doc"_s, "Document is nullptr."_s);
const Int32 fps = doc->GetFps();
const Int32 firstFrame = doc->GetTime().GetFrame(fps) + 1;
const Int32 lastFrame = time.GetFrame(fps);
if (lastFrame < firstFrame - 1)
return maxon::IllegalArgumentError(
MAXON_SOURCE_LOCATION, "The target time is before the current time."_s);
// Loop over all frames we need to reach #t and execute the passes. What to execute for each pass
// (caches, animations, expressions) depends on what you want to achieve, and will of course have
// an impact on the performance. Note that things likes cache building can have an impact on
// animations and expressions, and vice versa. So, when one needs the 'true' scene state, one must
// often calculate everything.
for (Int32 frame = firstFrame; frame <= lastFrame; ++frame)
{
doc->SetTime(BaseTime(frame, fps));
// Execute the passes for everything in the main thread (nullptr), use your thread instead when
// you have one.
if (!doc->ExecutePasses(nullptr, true, true, true, BUILDFLAGS::NONE))
return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Failed to execute pass."_s);
}
// For the last frame we should execute the passes twice, so that the scene can settle.
if (!doc->ExecutePasses(nullptr, true, true, true, BUILDFLAGS::NONE))
return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Failed to execute pass."_s);
// The document is now in the state of #time.
return maxon::OK;
}
Finding a scene element #e in two scenes
I am a bit surprised that you ask that, as this is sort of the bread and butter for what you are doing. So, I might be misunderstanding you here. If that is so, please clarify what you mean.
The identity of scene elements is expressed in Cinema 4D by GeMarker. Such markers are given when objects are allocated (and by default also when copied) and are a hash of the creating machine's mac address, a timestamp and some secret sauce. An alternative way to retrieve the same data is BaseList2D::FindUniqueID(MAXON_CREATOR_ID) which just returns the raw data of the GeMarker obtainable via BaseList2D::GetMarker. When you copy scene elements or whole scenes, by default they are given new markers. You can prevent that with COPYFLAGS::PRIVATE_IDENTMARKER.
Never insert multiple nodes with the same marker into a scene.
An alternative could be to use links, i.e., a BaseLinks, for example in a BaseContainer stored with the document. But that is also just built on top of GeMarker.
I hope you are well! Since I just answered a similar question in another topic, I am also answering here. We never make any guarantees regarding when something will be released in SDKs. And more-over, we never name upcoming Cinema 4D versions explicitly or when they will arrive. This also includes seemingly insignificant information such as if the next version will be a minor or a major version increment.
Please note that you as Maxon Registered Developer are bound by an NDA and also cannot disclose such information to the public.
But yes, I am pretty sure Maxime meant the next upcoming release.
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.
Please do not use other topics for your support requests, especially when they are bug tickets. Asking a follow-up questions such as "when will this be fixed" or "does this also apply to situation B" in a foreign topic is allowed and even encouraged.
But a specific case with your specific code and data always warrants a new thread. Please also familiarize yourself with Support Procedures: Asking Questions, as while it its clear that you put effort into make yourself understood (thanks!), this is also bordering a bit on the too much information.
The bug tracked by the other topic, is that there is currently no way in the Python API to use c4d.documents.RenderDocument in an OCIO document (i.e., every document since 2025.0) and then display that bitmap with the correct colors via c4d.bitmaps.ShowBitmap. It is important to understand that:
The bitmap data generated is just fine, when you save the bitmap to disk, everything is correct. It is just that the Picture Viewer does not display it incorrectly.
The reason for that is that in Python OCIO is not fully exposed in 2025.1.0 and you cannot set the OCIO profiles of the rendered bitmap there. Internally, we have been working on an OCIO port for Python allowing for color conversions and more things, including OCIO managing bitmaps. We also added a function to bake a display and view transform into an sRGB image - another route how this RenderDocument issue can be solved.
The goal is to publish this with an upcoming version of the Python SDK, ideally the next, but as always we cannot make any guarantees. You cannot fix this issue yourself in 2025.1.0 and lower.
from the above helper I only get the points the spline is set to (eg. adaptive 5°) but I perhaps want more points in between.
That is not correct. As I said, splines have theoretically infinite precision, you are only bound in practice by the floating point precision you use for your computations - double, i.e., 64 bit in Cinema 4D. You get in your code just the cache of your spline and iterate over its points. That is more or less the same as if you would have just called BaseObject.GetCache() on your SplineObject which I mentioned above.
When you want to interpolate your spline manually, you must use the interpolation methods of SplineHelp, e.g., GetMatrix, which is sort of the most potent of all, as it gives you a full frame for each sample (i.e., not just a point but also an orientation).
I have a hunch that the actual question is how to build the cache of a spline as if it would have higher interpolation settings. And for that you should copy your input (unless it is already a dangling, i.e., non-inserted node), change its settings, insert it into a dummy document, and then call ExecutePasses on that document. Then you can get the cache, and iterate over its points.
Cheers,
Ferdinand
import c4d
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
if not op:
return
helper = c4d.utils.SplineHelp()
if not helper.InitSplineWith(op, c4d.SPLINEHELPFLAGS_RETAINLINEOBJECT):
raise RuntimeError("Could not initialize spline helper.")
# Take exactly ten samples over the length of the spline, no matter how many vertices it has,
# and how many points its current LineObject cache has.
steps: int = 10
for i in range(steps + 1):
# Get a frame for the offset #t.
t: float = i/float(steps)
m: c4d.Matrix = helper.GetMatrix(t)
# Print the #t, the point #p on the spline, its normal #n, tangent #t, and bi-tangent #bt.
print(f"{t = }, p = {m.off}, n = {m.v3}, t = {m.v1}, bt = {m.v2}")
if __name__ == '__main__':
main()
Thank you for reaching out to us. What do you mean with "low" detail? Something like the sampling angle of a spline in "adaptive" mode? The interpolation settings of a spline only apply when you quantize it, i.e., the degree of precision with which a SplineObject is being quantized into its LineObject cache (i.e., what is returned for BaseObject.GetCache() for a spline).
Splines themselves are just series of polynomials and therefore have no inherent precision. So, when you call for example SplineHelp.GetMatrix() to get all the metadata for an offset, you are not subject to the interpolation settings. That is within the limits of what computers can do regarding arithmetic of irrational numbers. You can quite quickly get subject to floating point precision problems when working with splines. If they are fixable and how to fix them, depends on the concrete case.
When this is what you want, a Python Programming Tag has also a draw function, i.e., the scripting element inside Cinema 4D. Scripting elements make great prototypes for their full grown plugin counter part (for the Python tag that would be TagData) but can also be very potent on their own while providing phenomenally rapid development speed.
A tag can draw into a viewport My line of thinking was that you want some form of automatism/ streamlined product. E.g., an "enable/disable comb drawing" button (CommandData) and then a node (TagData/ObjectData) actually implementing it. When you just want to have the ability to manually enhance a spline with some drawing decorations, a tag would be a very good choice.
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.
This is your first posting and you did well, but I would point you to our 'singular question' rule, as lined out above in the 'Asking Questions' link. While we usually cut user some slack regrading that, the rule exists for a reason, as topics otherwise tend to become gigantic. There are way too many questions in this topic, please split your questions into multiple topics in the future.
There is no mechanism to package and ship plugins under Cinema 4D. You can just zip a plugin folder and the user unzips it then in a path where his or her Cinema 4D instance is looking for plugins.
xdl64 is the extension Cinema 4D uses for libraries under Windows, i.e., it is more or less an alias for dll (our library linking is kind of special and right between static and dynamic libraries, hence the custom name). So, no, Python plugins cannot be packaged as xdl64 because Python is not compiled into machine code but interpreted at run time.
You can mix C++ and Python plugins in the sense that Python plugins can depend/interact with C++ plugins with the means of the Cinema 4D API. E.g., a Python plugin can react to messages sent by a C++ plugin or data placed by a C++ plugin (or vice versa). That is how our Python API works under the hood. But C++ and Python code cannot interact directly of course, as they are vastly different languages. But you can run our Python VM from C++ and eval the results.
When you want to protect your intellectual property, you can use our Cinema 4D | Extensions > Tools > Source Code Protector which will encrypt a pyp Python module. But code must of course be decrypted at runtime to be sent to the Python VM, so, this will not really stop anyone who really wants your source code (a cracker for example). C++ is the by far safer option when intellectual property is important (but also C++ can be decompiled).
Thank you for reaching out to us. The answer depends a bit on the context, and I could write here an essay but I will try to keep it short. Feel free to ask follow-up questions.
In principle, there are two meaningful ways to do this:
Tool: All variants of tool hooks can draw into viewports, e.g., a ToolData hook in Python. The issue with this is that this would be then a tool, i.e., you could not have it enabled in parallel to the move tool. The advantage is that this is exposed in Python and relatively little work.
Scene Hook: Scene hooks are the native way to implement nodes which are present exactly once in every scene. They are 'spiders in the net' present in each scene, and can draw, listen to keyboard and mouse inputs, and much more. The issue is here that SceneHookData is not wrapped for Python. In our morning meeting we decided that we will change that in an upcoming version of the Python API, but it is not yet decided if we will include drawing for scene hooks, as the non-implementation of SceneHookData was born out of performance concerns (which are not very practical IMHO).
A CommandData and GeDialog combo will not work, because while you can retrieve the BaseDraw instances of a document at any time, you need the context of a drawing method for your drawing instructions to be actually registered (otherwise the viewport will just ignore your calls). And neither command nor dialogs have these methods.
In Python you can sort of emulate a scene hook with an MessageData and ObjectData combo, the message hook scans for new scenes being opened, and then inserts the object as a hidden node. The object could then do the drawing. Via MSG_DOCUMENTINFO the object could then remove itself shortly before the document is being saved. You could also do something similar with a CommandData and TagData combo, i.e., a toggle command which enables or disables "comb drawing" on an object by placing a hidden tag on it.
What to chose depends on your project details. Scene hooks are the cleanest option. When a tool somehow makes sense, I would go for that. The MessageData + ObjectData combo is very messy, but could technically draw on all objects at once. TheCommandData option would be more meant for drawing on objects one by one. But both of these latter options are not ideal and will require an experienced developer to make them smooth.
I have posted the code example which will be shipped with an upcoming C++ SDK here. The example does not do 100% what you are trying to do, the node type implementing the custom branch is for example an object and not a scene hook, as I had to keep common applicability a bit in mind.
But it realizes a node which inernalizes other scene elements in a custom branch of itself, and should get you at least going. When you still need help with your specific case, please ask questions in this thread. When you have generic branching questions tied to the example, please use the other thread.
Custom branching, i.e., adding custom data to a Cinema 4D scene graph, was always possible in the C++ API, but never clearly documented. I therefore added a new code example to our C++ SDK which demonstrates such custom branching. The example will be shipped with an upcoming SDK, but you can find a preview below.
Cheers,
Ferdinand
Result
Files
example.main.zip: Contains the changed parts of the example.main SDK project, i.e., the new objectdata_assetcontainer example.
Code
This is just the main code, for all the code and the resources, see the files.
/* Realizes a generator object acting as an "asset container" which can internalize other objects
- assets - into itself to be later retrieved and to be stored with a scene file.
This demonstrates how to implement storing scene elements with a custom purpose in a custom
dedicated part of the scene graph of a Cinema 4D document. As natively implemented for objects,
tags, materials, shaders, and many more things - which all have all have a custom purpose and
dedicated place in a Cinema 4D document.
Usage:
Open the Commander with "Shift + C" and type "Asset Container" to find and then add an Asset
Container object. Now add primitives like a cube and a sphere to the scene. Drag them into the
"Source" link of the Asset Container and press then "Add" to internalize them into the custom
branch of the container. You can now delete the originals, and display the internal assets with
the "Selection" parameter as the output of the node. When load or store an Asset Container with
a scene or copy one Asset Container to another, its internal branching data will persist.
See also:
Technical Overview:
Branching is one of the fundamental principles with which Cinema 4D realizes a scene graph.
Branches are a way to associate a scene element (#GeListNode) with another scene element other
than pure hierarchical relations also realized by #GeListNode (GetUp, GetDown, GetNext, etc.).
A Cinema 4D document (a #BaseDocument) is for example also a type of #GeListNode itself and has
an #Obase branch for the objects stored in itself and an #Mbase branch for the materials stored
itself. Inside the #Obase branch, objects are just linked hierarchically, just as users see them
in the Object Manger. But each #BaseObject (which is also of type #GeListNode) also has branches,
for example a Tbase branch which holds the tags of that object. With that #GeListNode realizes
via branching a scene graph as a set of trees which are linked via branching relations.
Some node tree Another node tree
Node.0 --- Foo branch of Node.0 ---> GeListHead of Foo Branch
Node.0.1 Node.A
Node.1 Node.B
Node.1.1 Node.B.A
...
GeListNode::GetBranchInfo exposes the branches of a scene element. Plugins can implement custom
branches by implementing NodeData::GetBranchInfo which feeds the former method. This code example
demonstrates implementing such custom branching relation.
See also:
gui/activeobject.cpp: A plugin which visualizes the branches in a Cinema 4D document.
*/
// Author: Ferdinand Hoppe
// Date: 14/02/2025
// Copyright: Maxon Computer
#include "c4d_baseobject.h"
#include "c4d_basedocument.h"
#include "c4d_general.h"
#include "c4d_objectdata.h"
#include "lib_description.h"
#include "oassetcontainer.h"
// Plugin ID of the #AssetContainerObjectData plugin interface. You must register your plugin IDs
// via developers.maxon.net.
static const cinema::Int32 Oassetcontainer = 1065023;
using namespace cinema;
using namespace maxon;
/// @brief Realizes a type which can internalize other nodes in a custom branch of itself.
///
/// @details These other nodes could be all of a specific (custom) type, or has shown here, of a
/// builtin base type such as a branch which can hold objects. When we want to store custom data
/// per node in our scene graph, e.g., some meta data for assets, we have two options:
///
/// 1. Just store the information in the container of a native node. We can register a plugin ID
/// and write a BaseContainer under that plugin ID into the data container of the native node.
/// And while this is technically supported, writing alien data into foreign containers is
/// never a good thing as it always bears the risk of ID collisions (always check before
/// writing alien data if the slot is empty or just occupied by a BaseContainer with your
/// plugin ID).
/// 2. Implement a custom node, so that we can customize its ::Read, ::Write, and ::CopyTo
/// functions to store our custom data.
///
/// The common pattern to join (2) with builtin data is using tags. So, let's assume we want to
/// store objects in a custom branch like we do here, but also decorate each asset with meta data.
/// We then implement a tag, where we (a) can freely write into the data container, and (b) also
/// manually serialize everything that does not easily fit into a BaseContainer. Each asset in the
/// branch would then be decorated with such tag. For re-insertion the tag could then either be
/// removed, or we could also have these tags persist in the Object Manager but hide them from the
/// user (Cinema 4D itself uses a lot of hidden tags to store data).
// ------------------------------------------------------------------------------------------------
class AssetContainerObjectData : public ObjectData
{
INSTANCEOF(AssetContainerObjectData, ObjectData)
private:
/// @brief The branch head of the custom asset data branch held by each instance of an
/// #Oassetcontainer scene element.
// ----------------------------------------------------------------------------------------------
AutoAlloc<GeListHead> _assetHead;
public:
/// @brief Allocator for this plugin hook.
// ----------------------------------------------------------------------------------------------
static NodeData* Alloc()
{
return NewObjClear(AssetContainerObjectData);
}
/// @brief Called by Cinema 4D to let the node initialize its values.
// ----------------------------------------------------------------------------------------------
Bool Init(GeListNode* node, Bool isCloneInit)
{
// Auto allocation of _assetHead has failed, this probably means we are out of memory or
// otherwise royally screwed. One of the rare cases where it makes sense to return false.
if (!_assetHead)
return false;
// Attach our branch head to its #node (#node is the #BaseObject representing this
// #AssetContainerObjectData instance).
_assetHead->SetParent(node);
return true;
}
// --- IO and Branching -------------------------------------------------------------------------
// One must always implement NodeData::Read, ::Write, and ::CopyTo together, even when one only
// needs one of them. We implement here our custom branch being read, written, copied, and accessed
// in scene traversal.
/// @brief Called by Cinema 4D when it deserializes #node in a document that is being loaded
/// to let the plugin do custom deserialization for data stored in #hf.
// ----------------------------------------------------------------------------------------------
Bool Read(GeListNode* node, HyperFile* hf, Int32 level)
{
// Call the base implementation, more formality than necessity in our case.
SUPER::Read(node, hf, level);
// Read our custom branch back. "Object" means here C4DAtom, i.e., any form of scene element,
// including GeListNode and its derived types.
if (!_assetHead->ReadObject(hf, true))
return false;
return true;
}
/// @brief Called by Cinema 4D when it serializes #node in a document that is being saved
/// to let the plugin do custom serialization of data into #hf.
// ----------------------------------------------------------------------------------------------
Bool Write(const GeListNode* node, HyperFile* hf) const
{
// Call the base implementation, more formality than necessity in our case.
SUPER::Write(node, hf);
// Write our custom branch into the file. "Object" is here also meant in an abstract manner.
if (!_assetHead->WriteObject(hf))
return false;
return true;
}
/// @brief Called by Cinema 4D when a node is being copied.
// ----------------------------------------------------------------------------------------------
Bool CopyTo(
NodeData* dest, const GeListNode* snode, GeListNode* dnode, COPYFLAGS flags, AliasTrans* trn) const
{
// The #AssetContainerObjectData instance of the destination node #dnode to which we are going
// to copy from #this.
AssetContainerObjectData* other = static_cast<AssetContainerObjectData*>(dest);
if (!other)
return false;
// Copy the branching data from #this to the #other.
if (!this->_assetHead->CopyTo(other->_assetHead, flags, trn))
return false;
return SUPER::CopyTo(dest, snode, dnode, flags, trn);
}
/// @brief Called by Cinema 4D to get custom branching information for this node.
///
/// @details This effectively makes our custom branch visible for every one else and with that
/// part of the scene graph.
// ----------------------------------------------------------------------------------------------
Result<Bool> GetBranchInfo(const GeListNode* node,
const ValueReceiver<const BranchInfo&>& info, GETBRANCHINFO flags) const
{
iferr_scope;
yield_scope;
NodeData::GetBranchInfo(node, info, flags) yield_return;
// Return branch information when the query allows for empty branches or when query is
// only for branches which do hold content and we do hold content.
if (!(flags & GETBRANCHINFO::ONLYWITHCHILDREN) || _assetHead->GetFirst())
{
// We return a new #BranchInfo wrapped in the passed value receiver, where we express
// information about our custom branch. Since #info is a value receiver, we could invoke
// it multiple times in a row to express multiple branches we create.
info(
BranchInfo {
// The head of our custom branch. MAXON_REMOVE_CONST is required as #this is const in
// this context due to this being a const method.
MAXON_REMOVE_CONST(this)->_assetHead,
// A human readble label/identifier for the branch. Cinema 4D does not care how we name
// our branch and if we choose the same name as other branches do. But making your branch
// name sufficiently unique is still recommend, for example by using the reverse domain
// notation syntax.
"net.maxonexample.branch.assetcontainer"_s,
// The identifier of this branch. This technically also does not have to be unique. Here
// often an ID is chosen which reflects the content to be found in the branch. E.g., Obase
// for branches that just hold objects. We choose here the ID of our node type.
Oassetcontainer,
// The branch flags, see docs for details.
BRANCHINFOFLAGS::NONE }) yield_return;
}
return true;
}
// --- End of IO --------------------------------------------------------------------------------
/// @brief Called by Cinema 4D to let this generator contstruct its geometry output.
// ----------------------------------------------------------------------------------------------
BaseObject* GetVirtualObjects(BaseObject* op, const HierarchyHelp* hh) {
// Cinema 4D calls all generators upon each scene update, no matter if any of the inputs for
// the generator has changed. It is up to the generator to decide if a new cache must be
// constructed. While this technically can be ignored, it will result in a very poor performance
// to always fully reconstruct one's caches from scratch.
// We consider ourselves as dirty, i.e., as requiring a recalculation of our ouput, when
// there is no cache yet or when one of our parameter values recently changed (DIRTYFLAGS::DATA).
// When we are not dirty, we just return our previous result, the cache of the object (#op
// is the #BaseObject representing #this #AssetContainerObjectData instance).
const Bool isDirty = op->CheckCache(hh) || op->IsDirty(DIRTYFLAGS::DATA);
if (!isDirty)
return op->GetCache();
// Build a new cache based on the asset the user selected in the "Selection" drop-down.
const BaseContainer* const data = op->GetDataInstance();
const Int32 selectionIndex = data->GetInt32(ASSETCONTAINER_SELECTION, NOTOK);
// When paramater access has failed (should not happen) or the user has selected 'None', we
// return a Null object as our new cache.
if (selectionIndex == NOTOK) // Should not happen.
return BaseObject::Alloc(Onull);
// Otherwise decrement the index by one (to compensate for 'None') and get a copy of the
// matching asset and return it (unless something went wrong, then again return a Null).
BaseObject* const asset = GetAssetCopy(selectionIndex - 1);
if (!asset)
return BaseObject::Alloc(Onull);
return asset ? asset : BaseObject::Alloc(Onull);
}
/// @brief Called by Cinema 4D to convey messages to a node.
///
/// @details We use it here to react to a click on our "Add" button.
// ----------------------------------------------------------------------------------------------
Bool Message(GeListNode *node, Int32 type, void *data)
{
switch (type)
{
// The user pressed some button in the UI of #node ...
case MSG_DESCRIPTION_COMMAND:
{
DescriptionCommand* const cmd = reinterpret_cast<DescriptionCommand*>(data);
if (!cmd)
return SUPER::Message(node, type, data);
const Int32 cmdId = cmd->_descId[0].id;
switch (cmdId)
{
// ... and it was our "Add" button.
case ASSETCONTAINER_ADD:
{
// Get the data container of #node and retrieve the object linked as the new asset.
BaseObject* const obj = static_cast<BaseObject*>(node);
if (!obj)
return true;
const BaseContainer* const bc = obj->GetDataInstance();
const BaseObject* const link = static_cast<
const BaseObject*>(bc->GetLink(ASSETCONTAINER_SOURCE, obj->GetDocument()));
if (!link)
return true;
// Attempt to add #link as an asset into the Oassetcontainer branch of #obj. Pressing
// a button and modifying our internal branch does not make #obj dirty on its own, we
// therefore flag ourself, so that our GUI is being rebuild (and also our cache).
if (AddAsset(link))
{
StatusSetText(FormatString("Added '@' as an asset.", link->GetName()));
obj->SetDirty(DIRTYFLAGS::DATA);
}
else
{
StatusSetText(FormatString("Could not add asset '@'.", link->GetName()));
}
break;
}
}
break;
}
}
return SUPER::Message(node, type, data);
}
/// @brief Called by Cinema 4D to let a node dynamically modify its description.
///
/// @details The .res, .str, and .h files of a node define its description, its GUI and language
/// strings. We use the method here to dynamically populate the content of the "Selection"
/// dropdown with the names of the nodes found below #_assetHead.
// ----------------------------------------------------------------------------------------------
Bool GetDDescription(const GeListNode* node, Description* description, DESCFLAGS_DESC& flags) const
{
// Get out when the description for #node->GetType() (i.e., Oassetcontainer) cannot be loaded.
// Doing this is important and not an 'this can never happen' precaution.
if (!description->LoadDescription(node->GetType()))
return false;
// The parameter Cinema 4D is currently querying to be updated and the parameter we want to
// update, our "Selection" drop-down. When Cinema does not give us the empty/null query and the
// query is not for "Selection", then get out to not update the description over and over again.
const DescID* queryId = description->GetSingleDescID();
const DescID paramId = ConstDescID(DescLevel(ASSETCONTAINER_SELECTION));
if (queryId && !paramId.IsPartOf(*queryId, nullptr))
return SUPER::GetDDescription(node, description, flags);
// Get the description container for the "Selection" parameter and start modifying it.
AutoAlloc<AtomArray> _;
BaseContainer* bc = description->GetParameterI(paramId, _);
if (bc)
{
// Create a container which holds entries for all assets which will be our drop-down item
// strings, following the form { 0: "None, 1: "Cube", 2: "Sphere, ... }.
BaseContainer items;
items.SetString(0, "None"_s);
BaseArray<const BaseObject*> assets;
if (!GetAssetPointers(assets))
return SUPER::GetDDescription(node, description, flags);
Int32 i = 1;
for (const BaseObject* const item : assets)
items.SetString(i++, item->GetName());
// Set this container as the cycle, i.e., drop-down, items of #ASSETCONTAINER_SELECTION and
// signal that we modified the description by appending #DESCFLAGS_DESC::LOADED.
bc->SetContainer(DESC_CYCLE, items);
flags |= DESCFLAGS_DESC::LOADED;
}
return SUPER::GetDDescription(node, description, flags);
}
/// --- Custom Methods (not part of NodeData/ObjectData) ----------------------------------------
/// @brief Returns a list of pointers to the internalized assets.
///
/// @param[out] assets The asset pointers to retrieve.
/// @return If the operation has been successful.
// ----------------------------------------------------------------------------------------------
bool GetAssetPointers(BaseArray<const BaseObject*>& assets) const
{
iferr_scope_handler
{
return false;
};
if (!_assetHead)
return false;
const BaseObject* asset = static_cast<const BaseObject*>(_assetHead->GetFirst());
while (asset)
{
assets.Append(asset) iferr_return;
asset = asset->GetNext();
}
return true;
}
/// @brief Returns a copy of the asset at the given #index.
///
/// @details We implement this so that our cache does not use the same data as stored in the
/// Oassetcontainer branch, as ObjectData::GetVirtualObjects should always return an object
/// tree that is not already part of a document.
///
/// @param[in] index The index of the asset to copy.
/// @return The copied asset or the nullptr on error.
// ----------------------------------------------------------------------------------------------
BaseObject* GetAssetCopy(const Int32 index)
{
BaseObject* asset = _assetHead ? static_cast<BaseObject*>(_assetHead->GetFirst()) : nullptr;
if (!asset)
return nullptr;
Int32 i = 0;
while (asset)
{
// It is important that we copy the node without its bit flags, as we otherwise run into
// troubles with cache building.
if (i++ == index)
return static_cast<BaseObject*>(asset->GetClone(COPYFLAGS::NO_BITS, nullptr));
asset = asset->GetNext();
}
return nullptr;
}
/// @brief Copies the given #asset and adds it to the internal asset data branch.
///
/// @details In a production environment, we should be more precise in what we copy and what not.
/// We copy here without any hierarchy but with branches. Branches of an object would be for
/// example its tags and tracks. Copying such nested data can never really cause severe technical
/// issue, but we could for example internalize a material tag like this which interferes with our
/// rendering. But when we set here COPYFLAGS::NO_BRANCHES, or alternatively just blindly remove
/// all tags from an object, we would also remove the hidden point and polygon data tags of an
/// editable polygon object and with that all its content. So, internalizing "assets" in such
/// manner will require good book-keeping in practice of what is needed and what not.
///
/// @param[in] asset The asset to add.
/// @return If the operation has been successful.
// ----------------------------------------------------------------------------------------------
bool AddAsset(const BaseObject* asset)
{
if (!_assetHead)
return false;
// Here we should ensure that we do not internalize the same asset twice, for example via
// GeMarker. I did not do this, as this would have required more book keeping and I wanted to
// keep the example compact.
// Copy the node (making sure to keep its marker via #PRIVATE_IDENTMARKER) and insert the copy.
BaseObject* const copy = static_cast<BaseObject*>(
asset->GetClone(COPYFLAGS::NO_HIERARCHY | COPYFLAGS::NO_BITS, nullptr));
if (!copy)
return false;
_assetHead->InsertLast(copy);
return true;
}
};
/// @brief Called by the main.cpp of this module to register the #Oassetcontainer plugin when
/// Cinema 4D is starting.
// ------------------------------------------------------------------------------------------------
Bool RegisterAssetContainerObjectData()
{
// You must use here a unqiue pluin ID such as #Oassetcontainer.
return RegisterObjectPlugin(Oassetcontainer, "Asset Container"_s, OBJECT_GENERATOR,
AssetContainerObjectData::Alloc, "oassetcontainer"_s, nullptr, 0);
}
first of all, questions are never annoying to us. You are right, I incorrectly used there an outdated signature of NodeData::GetBranchInfo. I removed my pseudo code example from above, as I decided today, that I will just write a code example for branching for the SDK (the Asset Container thing from below which has the custom branch net.maxonexample.branch.assetcontainer with a cube and a sphere object stored in it).
Just posting here so that you do not think we overlooked you, I will post the example on Monday, as I still have to make sure the example works properly in all aspects. For now I will sail of into the weekend
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.
You posted in the wrong forum (I have moved your question). You therefore also did not set the tags the Cinema 4D SDK forum asks users to set such as the API you want to use (I assume Python) and the version of Cinema 4D you want to target.
But, yes, that is possible. You must just iterate over the object tree and then replace things. The tricky thing is to figure out when two nodes (objects) are the same. Just using the point count would be of course an incredibly poor measure of equality. But even when you properly compare the geometry, you could have two objects with identical geometry but different parameters (e.g., one is in layer A and one in layer B) or different nodes attached to them (e.g., one has material tag, and one does not). This can all be dealt with but requires some work and knowledge of our API. What then often also happens is that you want changes for equability except for X and Y which you want to ignore (which then interferes with you just plainly comparing the data containers of nodes for example).
So, long story short, there might be more technical hurdles than you think, but that is all possible. But we cannot write a finished solution for you. We in fact expect users to post code and a concrete API question before we answer. Below you can find a sketch for how you could do this (only works in Cinema 4D 2025.0.0+ because I used mxutils.IterateTree.
Cheers,
Ferdinand
Result
Before
After (Cube.1 and .2 have been replaced with instances, but .3 not because it has different point values).
Code
"""Replaces all polygon objects that have the same topology as the selected object with instance
objects.
This will only operate on editable polygon objects. This script requires Cinema 4D 2025.0.0 or higher.
It could be translated to older versions, mxutils.IterateTree would then have to be replaced.
"""
import c4d
import mxutils
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def ComparePolygonObjects(a: c4d.PolygonObject, b: c4d.PolygonObject) -> bool:
"""Compares two polygon objects for equality, evaluated over their points and topology.
"""
# When the point values aren't the same, both objects cannot be the same.
if a.GetAllPoints() != b.GetAllPoints():
return False
# For the topology we unfortunately must be a bit more manual.
topologyA: list[c4d.CPolygon] = a.GetAllPolygons()
topologyB: list[c4d.CPolygon] = b.GetAllPolygons()
if len(topologyA) != len(topologyB):
return False
# We iterate over all polygons in #a and #b in pairs (x, y) and compare each vertex of the polygon
# for referencing the same point. Internally, Cinema 4D stores everything as quads, hence us
# blindly checking a, b, c, and d.
for x, y in zip(topologyA, topologyB):
if (x.a != y.a or x.b != y.b or x.c != y.c or x.d != y.d):
return False
return True
def ReplaceWithInstance(source: c4d.PolygonObject, target: c4d.PolygonObject) -> None:
"""Replaces #target with an instance object of #source, while copying over the transform and
name of #target.
"""
doc: c4d.documents.BaseDocument = source.GetDocument()
# Create an instance object and copy over the necessary data.
instance: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oinstance))
instance[c4d.INSTANCEOBJECT_LINK] = source # Link the source object.
instance.SetName(target.GetName()) # Copy the name.
if target.GetNBit(c4d.NBIT_OM1_FOLD): # Unfold the instance when the target was unfolded.
instance.ChangeNBit(c4d.NBIT_OM1_FOLD, c4d.NBITCONTROL_SET)
ml: c4d.Matrix = target.GetMl() # A copy of the PSR of #target in relation to its parent.
# Get an insertion point, remove the target, and insert the instance. Also move all the children
# from #target to #instance
previous: c4d.BaseObject = target.GetPred()
parent: c4d.BaseObject = target.GetUp()
doc.AddUndo(c4d.UNDOTYPE_DELETE, target)
target.Remove()
doc.InsertObject(instance, parent, previous)
doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, instance)
doc.AddUndo(c4d.UNDOTYPE_CHANGE, instance)
instance.SetMl(ml) # Write the local transform back to the instance.
for child in target.GetChildren():
doc.AddUndo(c4d.UNDOTYPE_CHANGE, child)
child.Remove()
child.InsertUnderLast(instance)
def main() -> None:
"""
"""
# Get out when the selected object is not a polygon object.
if not isinstance(op, c4d.PolygonObject):
raise TypeError("You must selected an editable PolygonObject to replace.")
# Get the points and polygons of the currently selected object.
basePoints: list[c4d.Vector] = op.GetAllPoints()
basePolygons: list[c4d.CPolygon] = op.GetAllPolygons()
# Now iterate over all objects in the scene, stepping over all non-polygon objects and ourselves,
# to find objects to remove.
removeNodes: list[c4d.PolygonObject] = []
for node in mxutils.IterateTree(doc.GetFirstObject(), True):
if not isinstance(node, c4d.PolygonObject):
continue
# Continue when #op and #node are the same entity or when they are not equal.
if op == node or not ComparePolygonObjects(op, node):
continue
removeNodes.append(node)
# And now finally remove all objects in one undo.
if removeNodes:
doc.StartUndo()
for node in removeNodes:
ReplaceWithInstance(op, node)
doc.EndUndo()
c4d.EventAdd()
if __name__ == '__main__':
main()
Thank you for reaching out to us. We were not 100% sure in our morning meeting if we understood your question correctly. But I basically understood it as 'how can I find out if a C4DAtom is referenced in a BaseLink in a scene?'. So, to pick an example, you would want to find out if the node A shown in the screen below is linked anywhere in the scene. In that case the answer would be 'yes', since A is linked in a Target Tag of B.
The goal then seems to be to delete such objects which are not referenced in any manner. There are two problems here at play:
Abstracted scene traversal, i.e., iterating/recursing over all elements of a scene: The assumption of your code is that a null object is only linked in tags. Which can work when you do not care about other cases of these nulls being referenced. But in practice any form of BaseList2D can hold a link to your null object, i.e., there could be other objects, materials, shaders, etc. linking to that null. To visit each node in a scene you then need such abstracted scene traversal. There are many postings about this subject in the forum, here is for example one. In 2025.0.0 we added mxutils.RecurseGraph which does that out of the box.
Figuring out if a given node holds a parameter of type DTYPE_BASELISTLINK which also happens to point towards your null. You can use C4DAtom.GetDescription to look for parameters of a specific type. @i_mazlov has once shown here how this can be done.
There are also some issues with your code, as for example that seems to point into an exponential time complexity direction, i.e., you seem to plan to iterate over all objects in a scene (to find potential objects to remove). That would be very slow.
Find below how I would write something like this. Please note that these scene operation tasks tend to be more finnicky than one usually assumes, so you might have to add features yourself. We cannot debug your code for you or write a solution for you.
Cheers,
Ferdinand
Result
Before:
After:
Code
"""Demonstrates how to search for a scene for nodes that do not appear as links in other objects.
Must be run as a script manager script. Will remove all null objects from a scene which do not appear
as linked objects in other tags or objects (and some other criteria, see main() for details).
"""
import c4d
from typing import Iterator
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def Iterate(node: c4d.BaseObject, yieldSiblings: bool = True) -> Iterator[c4d.GeListNode]:
"""Traverses a tree of objects and their tags, yielding each element, including the passed #node.
I implemented here a traversal tailored to your case. Check out scene traversal threads for more
abstracted traversal or use 2025's mxutils traversal methods.
"""
def iterate(node: c4d.GeListNode) -> Iterator[c4d.GeListNode]:
if not node:
return
yield node
# Yield tags when #node is an object.
if isinstance(node, c4d.BaseObject):
for t in node.GetTags():
yield t
# Traverse hierarchy.
for child in node.GetChildren():
yield from iterate(child)
while node:
yield from iterate(node)
node = node.GetNext() if yieldSiblings else None
def GetLinkedNodes(node: c4d.BaseList2D) -> set[c4d.BaseList2D]:
"""Returns the set (i.e., no repetitions) of nodes this #node has links established to.
"""
result: set = set()
for _, descid, _ in node.GetDescription(c4d.DESCFLAGS_DESC_NONE):
# We found a parameter of type baselink, append the value to the result when it is not null.
# You might want to fine tune things here, to exclude parameter IDs, e.g., ID_LAYER_LINK, or
# value types, e.g., c4d.BaseLayer, from being here links you want to consider.
if descid[0].dtype == c4d.DTYPE_BASELISTLINK and node[descid]:
result.add(node[descid])
return result
def main() -> None:
"""
"""
# Get all the nodes that are somewhere referenced in objects or their tags in the scene. Also
# store all the nodes which contain links.
linkSources: set = set()
linkTargets: set = set()
for node in Iterate(doc.GetFirstObject()):
links: set = GetLinkedNodes(node)
if links:
linkSources.add(node)
linkTargets = linkTargets.union(links)
# Now iterate the scene again, looking for nodes matching our removal criteria.
result: list[c4d.BaseList2D] = []
for node in Iterate(doc.GetFirstObject()):
# Continue when the node is not an object of type Onull, has children, or does appear as a
# linked node somewhere (e.g., a null object which has a target tag).
if (node.GetType() != c4d.Onull or node.GetChildren() or
node in linkTargets or linkSources.intersection(Iterate(node, False))):
continue
# This is a node we want to remove.
result.append(node)
# Remove the nodes we determined to be removed. We would have broken traversal if we just had
# removed #node in the loop above, as #Iterate is a generator/coroutine. Removing #node above
# would make it a disconnected node and #Iterate would not know how to continue. Could also be
# solved by first exhausting #Iterate, e.g., `data = list(Iterate(...))`and then iterate over
# the result (but that is not so nice performance-wise).
if result:
print (f"Removing '{len(result)}' nodes: {result}")
doc.StartUndo()
for node in result:
doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, node)
node.Remove()
doc.EndUndo()
c4d.EventAdd()
if __name__ == '__main__':
main()
as stated above, the public API only provides not so performant ways to draw into a viewport. You can get the state/buffer of a viewport with BaseDraw::GetViewportImage but that is read only. What you can do, is draw your render frame buffer into a BaseBitmap or ImageRef (we not to long ago talked about how a render engine could write into a shared image buffer here) and then draw that texture in screen space over every thing else in the HUD pass. But as indicated, that is not the greatest thing performance wise and also comes with some other hurdles. But some 3party vendors did it like that in the past I think.
The better way would be the Drawport API as exposed as BaseDraw::GetDrawport among other places in the public API. But that is a semi public API resereved for Maxon Registered Developers.
So, long story short: No, you cannot directly draw into the draw buffer of a viewport in the public API.
Thank you for reaching out to us. An axis as a directly manipulatable entity does not exist in the API. Objects have coordinate systems which are reflected as axis to users. We have talked many times about this subject on the forum, for example here. There is also the Python code example operation_transfer_axis_s26.py which I once added to cover this very common question.
But to give here a short answer: No, there is no API feature which would allow you to snap the axis of one object to another. You must do that manually as for example shown in operation_transfer_axis_s26.