Editable Object Plugin returns Null after scaling
-
I'm creating a basic ObjectData plugin that returns a cube with a modified scale. However, when making the object editable (C key), sometimes the result becomes a Null object with the cube inside, especially after changing the scale.
import c4d from c4d import plugins, utils class MyObjectPlugin(plugins.ObjectData): def Init(self, op): return True def GetVirtualObjects(self, op, hierarchyhelp): dirty = op.CheckCache(hierarchyhelp) or op.IsDirty(c4d.DIRTY_DATA) if not dirty: return op.GetCache(hierarchyhelp) cube = c4d.BaseObject(c4d.Ocube) if cube is None: return None cube[c4d.ID_BASEOBJECT_REL_SCALE, c4d.VECTOR_X] = 2.0 cube[c4d.ID_BASEOBJECT_REL_SCALE, c4d.VECTOR_Y] = 1.0 cube[c4d.ID_BASEOBJECT_REL_SCALE, c4d.VECTOR_Z] = 0.5 return cube
Am I doing something wrong? My intention is specifically that when making the generator editable, the result is the scaled object, but without the null object that is generated when modifying it, and without modifying the generator scale.
Thanks in advance for any help!
PS: I would also like to know if there is a specific way to return several objects or a specific one when making the generator editable, making it more customizable.
-
Hello @JH23,
Thank you for reaching out to us. Your code is looks correct, but you should not return
None
on a critical error (e.g., when you run out of memory to allocate things), but returnc4d.BaseObject(c4d.Onull)
instead, that is also what Cinema 4D will do, when it cannot make sense out of your method. E.g. it should be:if not(cube := c4d.BaseObject(c4d.Ocube)): return c4d.BaseObject(c4d.Onull) cube[c4d.ID_BASEOBJECT_REL_SCALE, c4d.VECTOR_X] = 2.0 ...
Also, does your object have child inputs it depends on? E.g., like an Extrude object which depends on a child spline? I assume it does not, right? You should then change your manual dirty check to this, because your code also tries to build the children of this node which could lead to issues.
# Only build the cache of the object when one of its parameters has changed or when there is no cache. if not op.IsDirty(c4d.DIRTY_DATA) and op.GetCache(): return op.GetCache() # you could also pass the hh here, but that is not necessary in this case
Do you see any errors in the console when this happens? Cinema 4D returning a null object for a cache means that building the cache failed. There must be somewhere a bug in your code, I am not sure though that my suggestions will fix it.
I would also like to know if there is a specific way to return several objects or a specific one when making the generator editable, making it more customizable.
A cache must always terminate into a singular object because it is just part of an object hierarchy. When you want your cache to contain multiple things, you must parent them to a null and return that null as your cache.
As to reacting to
CTSO
, yes that is possible, but not in Python. When an object is being built for CSTO, itsHierachyHelp->GetFlags()
will beBUILDFLAGS::EXTERNALRENDERER | BUILDFLAGS::ISOPARM
butHierachyHelp
has not been wrapped for Python. There isMSG_CURRENTSTATE_END
emitted, and you can capture this in Python too viaNodeData::Message
, but that is only the signal that CSTO has finished (and that you might want to revert to a non-CSTO specialized cache).But in general, we advise against such trickery where objects switch out their cache in certain contexts. It is valid to do this, but often requires intimate knowledge of the API to be done sucessfully.
Cheers,
Ferdinand -
Hi @ferdinand,
Apologies for the lack of context earlier. I didn’t mean that it returns null, but rather that it returns an object inside a null container. When I try to modify the scale of the resulting object, this behavior persists. I’ve noticed a similar pattern when working with Python generators. -
Hey @JH23,
there is no need to apologize for a lack of context. Now that I read your first posting again, it is actually perfectly clear what you are asking for. I just did not read it properly, my bad, sometimes this happens when I am in a hurry.
The answer to your question is not trivial, but the TLDR is that you have only little control over it.
There exist two commands, 'Current State to Object' (CSTO) and 'Make Editable' (ME). CSTO is basically the less aggressive version of ME, where it runs through all the elements of a cache of something and tries to collapse them gracefully (which could result in output that is still collapsible itself). ME more aggressively flattens the cache hierarchy.
For both commands, at the very basic level, there is the distinction between generators and and non-generators. When you CSTO some generator object which just holds another generator in its cache (imagine your
GetVirtualObjects
ormain
just returningc4d.BaseObject(c4d.Ocube)
), it will just return a copy of that generator (which is still a generator, i.e., something that has a cache). But when your generator returns a non-generator, i.e., a discretePolygonObject
, it will wrap the copy of this cache in a null when returning it.But that is not all of it. Because, there are also functions such as
TransferDynamicProperties
andOptimizeHierarchy
which ensure that relevant scene data is kept and the output is as compact as possible. They run after ME/CSTO by further manipulating the ME/CSTO output. The original MEed/CSTOed object might have had a transform which must be copied onto the flattened output, so that it has the same transform in world space. Objects often also hold hidden data in form of tags in Cinema 4D which might have to be copied from the original object onto the flattened result. This all might lead to the functions either removing unnecessary null objects which before have been created by ME/CSTO or adding new ones.That your scale manipulations result in an extra null is a bit surprising, but that must have to do with the transform normalization code which runs after ME/CSTO. At first I thought, it might transfer the scale to the null object, so that it can normalize the scale of the cache. But that is not the case, it is still the cache object which has the scale
2
when you CSTO/ME your generator. I would really have to debug this in detail, to find out why exactly this happens.But in general, I would advise against scaling the matrix/transform of an object, and instead, apply the scale to the points of a discrete
PolygonObject
or the parameters of aBaseObject
itself. Transforms which have axis components of non-unit length (i.e., a scale != 1.0), often lead to problems.The important message is that even when you find a solution which gives you the desired flat cache for this problem (by for example scaling the points), there could be countless other scenarios such as a user having a MoGraph tag on your generator or something like that, where then MEing or CSTOing would result in a null + cache output. You should not build your code on the assumption that the MEed or CSTOed output of your generator will always be flat (or the opposite), because you simply cannot guarantee that.
Not the most satisfying answer, I know, but I hope it helps to clarify the situation.
Cheers,
FerdinandHere is how you could post process points with a transform:
def main(): obj: c4d.PointObject = Cube() # We could of course also just pass this #transform into your #Cube function, to do it right # there. But we are making a point of doing this as a post processing step (so that we can do # this at any point in time). We scale all points by a factor of 2 and rotate them 45° on the # Y axis in the coordinate system of the object. When we now CTSO your object, we have a flat # output. transform: c4d.Matrix = (c4d.utils.MatrixScale(c4d.Vector(2, 2, 2)) * c4d.utils.MatrixRotY(c4d.utils.DegToRad(45))) obj.SetAllPoints([n * transform for n in obj.GetAllPoints()]) obj.Message(c4d.MSG_UPDATE) return obj