[Solved] How to setup a CTrack on TAG plugin UI slider? [with extended details]
-
Hello there,
I'm currently working on a plugin development in Python under R26 and I'm struggling to reach a resource ID on the GUI to assign it an animation track.
I tried to implement what I was using for the objects hoping it will worked, but it isn't - I'm getting error 5350 doing so.Reading the SDK, it seems, as far as I understand, that BaseTag inherit of BaseList2D and should so inherit of CTrack; but there should be something wrong here in the code or SDK which I do not understand. As the plugin is declared as a
plugins.RegisterTagPlugin()
shouldn't it
be inherit of BaseList2D?Here below the excerpt of the code for a better understanding,
mAXIS_X = 1550 mAXIS_Y = 1551 mAXIS_Z = 1552 def Init(self, node): mmObj = c4d.documents.GetActiveDocument().GetActiveTag() if mmObj is not None: print ( f"mmObj: { mmObj }" ) print ( f"Object: { mmObj.GetObject() }" ) print ( f"tagName : {mmObj} / {mmObj.GetObject().GetName()} plugin ID: { mPLUGIN_ID }" ) descID = c4d.DescID( c4d.DescLevel(c4d.ID_BASEOBJECT_ROTATION, c4d.DTYPE_VECTOR, 0), c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0) ) c4d.CTrack(mmObj, descID )
gives the following console log and error,
mmObj: <c4d.BaseTag object called mMover/mMover with ID 1059303 at 11778301312> Object: <c4d.BaseObject object called mMover/Null with ID 5140 at 11778302720> tagName : <c4d.BaseTag object called mMover/mMover with ID 1059303 at 11778301312> / mMover plugin ID: 1059303 Traceback (most recent call last): File "~/Library/C4D_Plugins/C4D_mLab_Plugins_R23/mMover.pyp", line 266, in Init c4d.CTrack(mmObj, descID ) BaseException: the plugin 'c4d.CTrack' (ID 5350) is missing. Could not allocate instance
The pict below shows the UI on which 3 sliders exists (mAXIS_XX, ID from 1550 to 1552). That those sliders on which I would like to create a CTrack. Once done, the button "Clear all animation tracks" will remove them.
I setup a .res resource file with this,
GROUP { DEFAULT 1; COLUMNS 2; REAL mAXIS_X { MIN -180.0; MAX 180.0; UNIT DEGREE; STEP 0.05; CUSTOMGUI REALSLIDER; SCALE_H; } BOOL mEN_X { ANIM OFF; } STATICTEXT { JOINENDSCALE; } REAL mAXIS_Y { MIN -180.0; MAX 180.0; UNIT DEGREE; STEP 0.05; CUSTOMGUI REALSLIDER; SCALE_H; } BOOL mEN_Y { ANIM OFF; } STATICTEXT { JOINENDSCALE; } REAL mAXIS_Z { MIN -180.0; MAX 180.0; UNIT DEGREE; STEP 0.05; CUSTOMGUI REALSLIDER; SCALE_H; } BOOL mEN_Z { ANIM OFF; } STATICTEXT { JOINENDSCALE; } }
The sliders have the following IDs no .h file,
mAXIS_X = 1550, mAXIS_Y, mAXIS_Z,
I tried many ways to reach the right ID (1550 here), but I never succeed; and I never got the Animation track created in any way for this slider.
When using the Execute method of the plugin I can easily reach the sliders values, but again, I can't succeed to assign a CTrack on them,
def Execute(self, tag, doc, op, bt, priority, flags): print ( f"X Slider value: { c4d.utils.RadToDeg(tag[mAXIS_X]) }") descID = c4d.DescID( c4d.DescLevel(c4d.ID_BASEOBJECT_ROTATION, c4d.DTYPE_VECTOR, 0), c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0) ) xTrack = c4d.CTrack(tag, descID ) xTrack.GetCurve()
returns the following,
X Slider value:: 0.0 mMover/Null with ID 5140 at 11746145920> Traceback (most recent call last): File "~/Library/C4D_Plugins/C4D_mLab_Plugins_R23/mMover/mMover.pyp", line 313, in Execute xTrck = c4d.CTrack(tag, descID ) BaseException: the plugin 'c4d.CTrack' (ID 5350) is missing. Could not allocate instance
If someone knows how to get rid of this problem it would help me a lot, as well as maybe the whole community.
Thanks!
Christophe -
Hey @mocoloco,
Thank you for reaching out to us and thank you for your extensive documentation efforts.
There is unfortunately no executable code which makes it always hard to understand and find problems, but what stands out to me is:
- It is not clear to me at whose
Init
method we are looking at, but from the context I assume it ismMoverTagData.Init
mmObj = c4d.documents.GetActiveDocument().GetActiveTag()
should not be used in aNodeData
plugin. You should usenode
passed into the function when you want to reference the tag instance itself. WhenmmObj
is not meant to be the tag itself, then you should get the node document withnode.GetDocument()
and search there for your tag.mmObj
is either aBaseTag
orNone
.- You construct a
DescID
for(ID_BASEOBJECT_ROTATION, VECTOR_X)
which expands to(904, 1000)
. - A
BaseTag
has no vector parameters where aBaseObject
has its base type vector parameters as for exampleID_BASEOBJECT_ROTATION
, etc. (a tag has no transform). - The float parameters
mAXIS_X
,mAXIS_Y
, andmAXIS_Z
in your plugin start at1550
. - With
c4d.CTrack(mmObj, descID)
:- You are trying to write into an non-existent parameter in
mmObj
(unless you defined a vector at904
and did not show that). - It is a bit ambiguous what you want to do here, but I assume you want to create a track for your
mAXIS_X
parameter. - But your
DescId
is for a float component inside a vector and you define a float, not a vector.
- You are trying to write into an non-existent parameter in
You seem to misunderstand how
DescID
work, which is not too surprising since they are sparsely documented. Find below an example or look at this posting for more information.# This DescID addresses the X component of a vector parameter called ID_M_MOVE_AXIS. descId: c4d.DescID= c4d.DescID( # The first desc level, we are addressing here the vector itself. c4d.DescLevel( c4d.ID_M_MOVE_AXIS, # The ID of the parameter. c4d.DTYPE_VECTOR, # Its data type. 0), # The creator ID, 0 means undefined. # The second desc level, here we are addressing a sub-channel/parameter within a parameter, here # the X component of #ID_M_MOVE_AXIS. c4d.DescLevel( c4d.VECTOR_X, # The ID c4d.DTYPE_REAL, # The data type. 0) # The creator ID. )
So, when you want to create a track for the
mAXIS_X
parameter on an instance of your tag plugin, its ID would be:xAxisId: c4d.DescID = c4d.DescID(c4d.DescLevel(c4d.mAXIS_X, c4d.DTYPE_REAL, 0))
Cheers,
Ferdinand - It is not clear to me at whose
-
Hi @ferdinand,
Thanks a lot for the whole explanation. Indeed, it's a bit confuse on how to point some ID. I'm sorry to not share the whole source code but it's under NDA, so I can only share some excerpt, and that do not help to have the big picture, sorry for that.
I went a bit in-depth and tried some stuff after reading your explanations, and the posting on DescID (even if I still don't get why I have to write
c4d.mAXIS_X
instead ofmAXIS_X
).Let me rephrase some part to be on the same page and to remove a the confusions.
First, as I was stuck on getting what I tried on
Execute()
, which was the place where I would like to set theCTrack
, I indeed tried to get it set in themMoverTagData.Init()
- which I agree, is maybe not the right way to do.Then,
mAXIS_X
is indeed a real and not a vector which is displayed in a slider form.So, I went back to my first idea to set the
CTrack
inside theExecute(self, tag, doc, op, bt, priority, flags)
method and made the DescID from what I understood. To be clear I want to create aCTrack
for themAXIS_X
parameter with ID1550
I wrote this,
mPLUGIN_ID = 1059303 #Unique ID mTAG_NAME = "mMover" mAXIS_A1 = 1550 class mMover ( plugins.TagData ): def __init__ (self): super(mMover, self).__init__() pass def Execute(self, tag, doc, op, bt, priority, flags): print ( f"GetTags {op.GetTag( mPLUGIN_ID )}" ) currentTAG = op.GetTag( mPLUGIN_ID ) descID = c4d.DescID(c4d.DescLevel(c4d.mAXIS_X, c4d.DTYPE_REAL, 0)) # descID = c4d.DescID( c4d.DescLevel( c4d.ID_BASEOBJECT_ROTATION, c4d.DTYPE_VECTOR, 0), c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0) ) xTrck = c4d.CTrack( currentTAG, descID ) xCrv = xTrck.GetCurve() # Plug-in initialisation and declaration if __name__ == "__main__": # Load the plugin icon icon_absolute_path = os.path.join(os.path.dirname(__file__), 'res/icons', 'icon.png') plugin_icon = c4d.bitmaps.BaseBitmap() if plugin_icon is None: raise RuntimeError("Failed to load icon resource.") if plugin_icon.InitWith(icon_absolute_path)[0] != c4d.IMAGERESULT_OK: raise RuntimeError("Failed to initialize icon resource.") # Register the plugin plugins.RegisterTagPlugin( id = mPLUGIN_ID, str = mTAG_NAME, g = mMover, description = 'mMover', info = c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE | c4d.TAG_IMPLEMENTS_DRAW_FUNCTION | c4d.TAG_HIERARCHICAL, icon = plugin_icon )
This code show in console,
GetTags <c4d.BaseTag object called mMover/mMover with ID 1059303 at 4892750336>
No more error, but no CTrack created, at list it does not appears in the Dope Sheet; so I should still makes something wrong somewhere.
Does my pointing is now correct or still wrong for my DescID?Does this helps to point the problem?
Thanks in advance,
Christophe -
Small addition to my previous post, the CTrack seems exists but do not appears in the Dope Sheet, is that normal, and if so why?
Doing this,
def Execute(self, tag, doc, op, bt, priority, flags): print ( f"GetTags {op.GetTag( mPLUGIN_ID )}" ) currentTAG = op.GetTag( mPLUGIN_ID ) descID = c4d.DescID(c4d.DescLevel(c4d.mAXIS_X, c4d.DTYPE_REAL, 0)) # descID = c4d.DescID( c4d.DescLevel( c4d.ID_BASEOBJECT_ROTATION, c4d.DTYPE_VECTOR, 0), c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0) ) xTrck = c4d.CTrack( currentTAG, descID ) xCrv = xTrck.GetCurve() print(f"CTrack . { xTrck }")
Returns,
CTrack . <c4d.CTrack object called axis_X/Track with ID 5350 at 4892757504>
If I'm creating keyframes, will they be visibles in the Dope Sheet viewer or will they remains invisible?
-
Hi @mocoloco,
I still don't get why I have to write
c4d.mAXIS_X
instead ofmAXIS_X
You do not have to but it is convenient. An ID ist just a number and integer symbols like
c4d.ID_USERDATA
are just aliases for such number.700
andID_USERDATA
do the same thing. When you provide a resource file, here one for your tag, Cinema 4D automatically exposes all resource symbols in thec4d
namespace. You can also just redefine that symbol yourself, it is all the same. But it is extra work# These are all the same. bobIsYourUncle: int = 1550 mMoverTag[c4d.mAXIS_X] = 1.0 mMoverTag[1550] = 1.0 mMoverTag[bobIsYourUncle] = 1.0
No more error, but no CTrack created, at list it does not appears in the Dope Sheet; so I should still makes something wrong somewhere. Does my pointing is now correct or still wrong for my DescID?
Your
DescID
is correct, the problem is that you never add the track to your tag, you never calltag.InsertTrackSorted(xTrack)
. But you should not do this inTagData.Execute
, as lined out below.First, as I was stuck on getting what I tried on
Execute()
, which was the place where I would like to set theCTrack
, I indeed tried to get it set in themMoverTagData.Init()
- which I agree, is maybe not the right way to do.I never intended to imply that you should use
TagData.Execute
. You cannot add or remove elements from a scene as for example objects, tags, tracks or points in this method. Because you are outside of the main thread inExecute
and bound to the threading restrictions.Init
would be sort of okay, but a node has not yet been attached to a document when it is called, which probably would complicate things.But I think I understand what you want to do generally. Although I still do not understand why you want to create a track on your
mAXIS_X
parameter itself, which then leads to weird feedback loops when the parameter is used to both define the values of a track but is also driven by it.The other problem is that what you want to do in general, modify a scene from a NodeData
plugin is not the greatest design pattern. Doing this will often lead to trouble in Cinema 4D. It is okay to do this in the form of a button and when the user clicks the button, something is added or removed. But in the dynamic fashion you want to do it, this is not a good path. If you want to dynamically generate animations based on a set of parameters, you should implement aCommandData
plugin instead.I have provided nonetheless an example for both driving an x-position animation of an object hosted by a plugin tag with tracks, as well as driving the same animation procedurally. It is only the latter which is intended to be done with tags. If you want to to do this, you must offload your scene modifications to
NodeData.Message
, as this method is commonly called on the main thread. But you must still check for being there.Cheers,
FerdinandPlugin: pc14315.zip
Result: I am first driving the animation of the cube procedurally and then in the second half with tracks. In both cases the x-axis position of the object hosting the tag is driven by the parameters in the tag.
Code:
"""Implements a tag which drives the position of a host object procedurally or via tracks. The tag implements two modes, a "Procedural" and a "Track" mode. In procedural mode, the tag does what tags are supposed to do, it drives procedurally parameters of the scene, here the x-axis position of its host object. In track mode, it generates the same animation for its host object, but uses a key framed animation instead which is generated every time the user interacts with the GUI. Note: It is not recommended to use NodeData plugins to add or remove elements (e.g., objects, materials, tags, shaders, tracks, keys, points, etc.) from a scene in the manner shown here. It is okay to do this based on non-animateable parameters, e.g., a button. But as this is based on a set of animateable parameters #P, one can run into problems. This can happen when the document is rendered and #P is also animated. The only safe way to add and remove elements are therefore buttons in NodeData plugins. Respected must be in any case the threading restrictions of TagData.Execute, it is never allowed to add or remove scene elements from there. This solution will not crash, but is bound to the main thread which can cause the plugin to (intentionally) stop operating in the mentioned scenario. """ import c4d class mMoverTagData(c4d.plugins.TagData): """Implements a tag which drives the position of a host object procedurally or via tracks. """ # The plugin ID and a precise definition of the relative x-axis position parameter of an object. ID_PLUGIN: int = 1059303 DID_OBJECT_REL_POSITION_X: c4d.DescID = c4d.DescID( c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0), c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)) def Init(self, node: c4d.GeListNode) -> bool: """Called to initialize a mMover tag instance. Args: node: The BaseTag instance representing this plugin object. """ self.InitAttr(node, int, c4d.ID_MMOVER_MODE) self.InitAttr(node, c4d.BaseTime, c4d.ID_MMOVER_ANIMATION_MIN) self.InitAttr(node, c4d.BaseTime, c4d.ID_MMOVER_ANIMATION_MAX) self.InitAttr(node, float, c4d.ID_MMOVER_AXIS_X_MIN) self.InitAttr(node, float, c4d.ID_MMOVER_AXIS_X_MAX) node[c4d.ID_MMOVER_MODE] = c4d.ID_MMOVER_MODE_PROCEDURAL node[c4d.ID_MMOVER_ANIMATION_MIN] = c4d.BaseTime(0) node[c4d.ID_MMOVER_ANIMATION_MAX] = c4d.BaseTime(1) node[c4d.ID_MMOVER_AXIS_X_MIN] = 0. node[c4d.ID_MMOVER_AXIS_X_MAX] = 1000. return True def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject, bt: c4d.threading.BaseThread, priority: int, flags: int) -> int: """Called when expressions are evaluated to let a tag modify a scene. It is important to understand that tags are also bound by threading restrictions in this method, we are here NOT on the main thread. I.e., we cannot add or remove nodes from the scene as for example tracks. All we can do, is change parameters or the values of other data. We can for example change the point positions of a point object, but we cannot add or remove points. Args: tag: The BaseTag instance representing this plugin object. op: The BaseObject hosting the tag. doc: The document both the tag and object are attached to. """ # Bail when the tag is not in procedural mode. if tag[c4d.ID_MMOVER_MODE] != c4d.ID_MMOVER_MODE_PROCEDURAL: return c4d.EXECUTIONRESULT_OK # Set the x axis position of the tag host for the current time. mg: c4d.Matrix = op.GetMg() v: float = mMoverTagData._GetMoveOffset(tag, doc.GetTime()) mg.off = c4d.Vector(v, mg.off.y, mg.off.z) op.SetMg(mg) return c4d.EXECUTIONRESULT_OK def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool: """Called to signal events to the tag. Used in this case to react to parameter changes, inducing track modifications on the host object of the tag when the tag is in "Track" mode. Args: node: The BaseTag instance representing this plugin object. mid: The ID of the raised event. data: The data associated with the event. """ # Bail when the tag is not attached to a document or an object as we will need both. doc: c4d.documents.BaseDocument = node.GetDocument() obj: c4d.BaseObject = node.GetObject() if None in (doc, obj): return True # Bail when this is not an event for a parameter value being changed. if mid != c4d.MSG_DESCRIPTION_POSTSETPARAMETER: return True # Get the DescID of the parameter which has changed and get the ID of the first desc level. pid: int = data["descid"][0].id # Bail when the mode of the tag is "Procedural" and it was not the mode which has changed. isProceduralMode: bool = node[c4d.ID_MMOVER_MODE] == c4d.ID_MMOVER_MODE_PROCEDURAL if pid != c4d.ID_MMOVER_MODE and isProceduralMode: return True # Bail when this is not being called on the main thread, as all modifications we intend to # do below require use being on the main thread (adding or removing tracks or keys). if not c4d.threading.GeIsMainThreadAndNoDrawThread(): print ("Warning: Failed to execute 'mMover' tag logic due to threading restrictions.") return True # The mode switched from "Track" to "Procedural", we attempt to remove an existing track. if pid == c4d.ID_MMOVER_MODE and isProceduralMode: mMoverTagData._RemoveTrack(obj) # The user either turned on track mode or changed one of the parameters while being in # track mode, we add or modify the track. else: mMoverTagData._AddOrModifyTrack(obj, node) return True # --- Custom Methods --------------------------------------------------------------------------- @staticmethod def _GetMoveOffset(tag: c4d.BaseTag, t: c4d.BaseTime) -> float: """Computes a new x-axis offset based on the data in #tag and the given time #t. This is a custom method of this plugin and not part of the TagData interface. Args: tag: The tag to compute the offset for. t: The time at which to compute the offset. """ # Bail when the tag is not attached to a document. doc: c4d.documents.BaseDocument = tag.GetDocument() if not doc: raise RuntimeError(f"{tag} is not attached to a document.") # Get the min, max, and current frame for the animation described by the tag data. fps: int = doc.GetFps() minFrame: int = tag[c4d.ID_MMOVER_ANIMATION_MIN].GetFrame(fps) maxFrame: int = tag[c4d.ID_MMOVER_ANIMATION_MAX].GetFrame(fps) currentFrame: int = t.GetFrame(fps) # Get the min and max offset values and compute with them and the time values the offset for # the given time #t. minValue: float = tag[c4d.ID_MMOVER_AXIS_X_MIN] maxValue: float = tag[c4d.ID_MMOVER_AXIS_X_MAX] v: float = c4d.utils.RangeMap(currentFrame, minFrame, maxFrame, minValue, maxValue, True) return v @staticmethod def _AddOrModifyTrack(obj: c4d.BaseObject, tag: c4d.BaseTag) -> None: """Adds or modifies the track for a relative x-position animation on #obj. This is a custom method of this plugin and not part of the TagData interface. Args: obj: The object to add or modify the track for. tag: The mMover tag carrying the data. """ # Attempt to retrieve an existing track or create a new one. track: c4d.CTrack = obj.FindCTrack(mMoverTagData.DID_OBJECT_REL_POSITION_X) if track is None: track = c4d.CTrack(obj, mMoverTagData.DID_OBJECT_REL_POSITION_X) obj.InsertTrackSorted(track) # Flush the keys of the curve associated with the track in case we are dealing with an # already existing track. curve: c4d.CCurve = track.GetCurve() curve.FlushKeys() # Get the time and animation values and write them into two keys. minTime: c4d.BaseTime = tag[c4d.ID_MMOVER_ANIMATION_MIN] maxTime: c4d.BaseTime = tag[c4d.ID_MMOVER_ANIMATION_MAX] minValue: float = mMoverTagData._GetMoveOffset(tag, minTime) maxValue: float = mMoverTagData._GetMoveOffset(tag, maxTime) for time, value in ((minTime, minValue), (maxTime, maxValue)): key: c4d.CKey = curve.AddKey(time)["key"] if key: key.SetValue(curve, value) @staticmethod def _RemoveTrack(obj: c4d.BaseObject) -> None: """Removes the track for a relative x-position animation on #obj. This is a custom method of this plugin and not part of the TagData interface. Args: obj: The object to attempt to remove the track from. """ track: c4d.CTrack = obj.FindCTrack(mMoverTagData.DID_OBJECT_REL_POSITION_X) if track: track.Remove() def RegisterPlugins() -> None: """Registers the plugins contained in this file. """ if not c4d.plugins.RegisterTagPlugin( id=mMoverTagData.ID_PLUGIN, str="mMover Tag", info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE, g=mMoverTagData, description="tmmover", icon=c4d.bitmaps.InitResourceBitmap(c4d.ID_MODELING_MOVE)): print("Warning: Failed to register 'mMover' tag plugin.") if __name__ == "__main__": RegisterPlugins()
-
Hi @ferdinand,
I went a bit further yesterday and finally found the whole way to get it work.
The code part is here below for the community.I'm still stuck on the force redraw of the GUI when creating CTrack or removing them.
c4d.EventAdd()
seems not working fine, andc4d.gui.GeUpdateUI()
can only be invoked from main thread. Is that possible to force a GUI redraw from a TAG?This is the excerpt from the whole code for the CTrack only.
You have to use this code by adapting it in your own context# Create CTrack when not available currentTAG = op.GetTag( mPLUGIN_ID ) descID = c4d.DescID(c4d.DescLevel(c4d.mAXIS_X, c4d.DTYPE_REAL, 0)) if currentTAG.FindCTrack(descID) is None: trck_X = c4d.CTrack( currentTAG, descID ) currentTAG.InsertTrackSorted(trck_X) print( "CTrack created" ) c4d.EventAdd() # Remove all the CTracks currentTAG = op.GetTag( mPLUGIN_ID ) if (len (currentTAG.GetCTracks()) > 0): for track in currentTAG.GetCTracks(): track.FlushData() track.Remove() print( "CTracks removed" )
Thanks,
Christophe -
Hi @ferdinand
I haven't refreshed the thread prior posting my last message, so I missed your reply with the whole explanation and example. Thanks a lot, this fulfil the missing gap.
Thanks a lot,
Christophe -
Hey @mocoloco,
good morning
Is that possible to force a GUI redraw from a TAG?
You are bound there to the same threading restrictions as with adding or removing things. But other than for modifying the scene, which Cinema 4D will carry out outside of the main thread and then possibly crash, raising events, as for example a redraw event, is not possible at all from outside of the main thread. The first thing methods like
c4d.DrawViews
do, is check for being on the main (drawing) thread and when they are not, they get out.So, it is possible, but you must be on the main thread. This will never happen on
TagData.Execute
, but it will onTagData.Message
for example. So you can force a redraw when a user interacted with a parameter for example.Cheers,
Ferdinand -
Good morning @ferdinand!
I modified the whole code to always do my changes inside
TagData.Message
now instead of doing them inTagData.Execute
, which should prevent any crash - even if I didn't got one, it seems far better to stick on the good rules and habit.Have a good day!
Christophe -