What's really happening inside a CKey?
-
Hello once again;
I have expanded some scripted automatisms for CTrack, CCurve and CKey from pure float values to other types, and it turned out to be somewhat irritating. The class
CKey
provides two different methods to set/get values:SetValue
andSetGeData
. Documentation tells me that the former is for floats, the latter for "any", as Python does not have aGeData
class.Although... the doc now gets cryptic.
SetGeData
precisely says it sets "the data" of the key - not "the value". The C++ doc tells me to useSetValue
only for floats and "UseGetParameter()
to read non-real values" (with an example).SetGeData
in the C++ docs says "Sets the data of the key" as well; it's so consistent that I begin to suspect "value" and "data" of a key are actually two different things.As
SetValue
works fine for floats, I trySetGeData
for aDTYPE_LONG
track anyway (PRIM_CUBE_SUBX
). Doesn't work as expected. In the Timeline, the result is always 0, and the animation widget in the Attribute Manager shows up as a yellow circle as if that frame doesn't have any key.On the other hand, for a
DTYPE_BOOL
track,SetGeData
works fine.After some searching, I get an answer on this very forum: I must take the track category into account.
CTRACK_CATEGORY_VALUE
requiresGetValue
, andCTRACK_CATEGORY_DATA
needsGetGeData
. AndCTRACK_CATEGORY_PLUGIN
, well, I still have not the slightest idea, but I supposeGetParameter
may be the thing here?I'm doing the following test script:
(this works only for a selected cube primitive and creates keys for the Fillet boolean parameter and the X segments value)import c4d from c4d import gui def SetKey (currentTime, obj, parameterDesc, value): track = obj.FindCTrack(parameterDesc) print ("To set:", value, type(value)) if track == None: # create track for this parameter track = c4d.CTrack(obj, parameterDesc) obj.InsertTrackSorted(track) curve = track.GetCurve() # always present cat = track.GetTrackCategory() if cat == c4d.CTRACK_CATEGORY_PLUGIN: print ("Track is Plugin") return elif cat == c4d.CTRACK_CATEGORY_VALUE: print ("Track is Value") elif cat == c4d.CTRACK_CATEGORY_DATA: print ("Track is Data") else: print ("Track is unknown") return def SetKey_Value(key, value): if cat == c4d.CTRACK_CATEGORY_VALUE: origData = key.GetValue() print ("Orig:", origData, type(origData)) key.SetValue(curve, value) postData = key.GetValue() print ("Post:", postData, type(postData)) elif cat == c4d.CTRACK_CATEGORY_DATA: origData = key.GetGeData() print ("Orig:", origData, type(origData)) key.SetGeData(curve, value) postData = key.GetGeData() print ("Post:", postData, type(postData)) currentKey = curve.FindKey(currentTime) if currentKey: key = currentKey['key'] # key reference to change doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, key) SetKey_Value(key, value) curve.SetKeyDirty() # Key attributes found under C++ API doc: ckvalue.h File Reference else: defkey, dub = doc.GetDefaultKey() # new key to insert key = defkey.GetClone() # needed? SetKey_Value(key, value) key.SetTime(curve, currentTime) # key.SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE) curve.InsertKey(key, True) doc.AddUndo(c4d.UNDOTYPE_NEW, key) def main(): if op == None : return descSubX = c4d.DescID(c4d.DescLevel(c4d.PRIM_CUBE_SUBX, c4d.DTYPE_LONG, 0)) descFillet = c4d.DescID(c4d.DescLevel(c4d.PRIM_CUBE_DOFILLET, c4d.DTYPE_BOOL, 0)) doc.StartUndo() val = op[c4d.PRIM_CUBE_SUBX] SetKey (doc.GetTime(), op, descSubX, val) val = op[c4d.PRIM_CUBE_DOFILLET] SetKey (doc.GetTime(), op, descFillet, val) doc.EndUndo() c4d.EventAdd() if __name__=='__main__': main()
This works fine, and tells me in the console output that actually a
DTYPE_LONG
DescID has created a track of categoryCTRACK_CATEGORY_VALUE
, and my integer value that I'm trying to pass to the key is converted into afloat
. And theDTYPE_BOOL
DescID has created a track of categoryCTRACK_CATEGORY_DATA
, and my Boolean value that I'm trying to pass to the key is converted into anint
.While I now have a function that seems to work for two track categories (I haven't even started on the "plugin" category), this is not what I would have expected from the documentation. I later discover the track category thing in the C++ CKey manual as well, but there it only again mentions
float
values.Questions:
- Is my solution for
DTYPE_LONG
even correct? It works, but the solution is pretty much not what I gather from the API docs. - So a
CKey
stores an int as float, and a bool as int, and... what's the mapping here? - Are the
Set/GetValueLeft/Right
functions similarly dispositioned? The Python docs say "Just for keys with float values." but right now I suspect that they will also work forDTYPE_LONG
tracks as the according keys seem to work with floats internally. (Also, the Timeline shows me tangents.) - Checking the track categories, I find that a PLA track (as the C++ CKey API doc shows as an example) is a Plugin track. So, am I right with my theory that Plugin tracks need
GetParameter
, just like Value tracks needGetValue
and Data tracks needGetGeData
? The doc says "UseGetParameter()
to read non-real values." in the same place, but that is not takingGetGeData
in account at all? (I'm not even going to mention thatGetParameter
also usesGeData
...)
- Is my solution for
-
hi,
1-2
As you said, if the track is CTRACK_CATEGORY_VALUE the value of the key will be stored as a float.
if the track is CTRACK_CATEGORY_DATA, the data will be stored in a GeData, so the data can be anything that GeData can store.
CTRACK_CATEGORY_PLUGIN will be for the plugins that create track (and neither a float nor a GeData)3- left and right values are stored as float. the track type isn't checked. (by the way, the function accept a float as parameter)
4-
well, our comments are not that clear about the key storing data and pla.
For plugins track, you need to use Set/GetParameter because cinema4D doesn't know how to store that parameter (just like a custom datatype).
Cheers,
Manuel -
The internal behavior seems to be even more convoluted... I just tried writing out the Track Category to the console, and an integer / DTYPE_LONG value does not even lead to the same category necessarily.
If you have an integer with a simple numeric control in the AM, like a cube's subdivisions, you get a value track (and the value itself is stored as float internally).
If you have an integer with a restricted GUI, like a Cycle or Radio Button control, you get a data track, although it's DescID is also a DTYPE_LONG.
I conclude that the track gets its category - for which there is no setter function - from extra information about the attribute, which must be stored in the animated object (because it cannot be part of the DescID). I tried that for a few animation tracks that I created through the GUI (not by script) on a Platonic, and get:
Track: Blubberlutsch (user data, numeric field) -- DescID: ((700, 5, 0), (1, 15, 0)) -- Value track Track: Quibbelplaff (user data, radio buttons) -- DescID: ((700, 5, 0), (2, 15, 0)) -- Data track Note how this is almost the same DescID as Blubberlutsch (except for the ID), yet the track category comes out as data, not value! Track: Position . Y -- DescID: ((903, 23, 5155), (1001, 19, 23)) -- Value track Track: Enabled -- DescID: (906, 400006001, 5155) -- Data track Track: Display Color -- DescID: (907, 15, 5155) -- Data track Track: Radius -- DescID: (1120, 19, 5161) -- Value track Track: Segments -- DescID: (1121, 15, 5161) -- Value track Track: Type -- DescID: (1122, 15, 5161) -- Data track
In any way, the documentation is at least misleading.
I am not sure if you're right about Set/GetParameter. The signature for these functions in C++ both contain a GeData & t_data, so the values for this track are read and written through a GeData object either way. As GeData can hold a CustomDataType, the key's function Set/GetGeData should be able to handle that as well.
The question seems to be what happens in Python in that case, as there is no GeData. So, I used the PLA track to cobble together an ultimate example:import c4d def main(): if op == None: return for track in op.GetCTracks(): name = track.GetName() desc = track.GetDescriptionID() print ("Track:", name) print ("-- DescID:", desc) cat = track.GetTrackCategory() if cat == c4d.CTRACK_CATEGORY_VALUE: print ("-- Value track") if cat == c4d.CTRACK_CATEGORY_DATA: print ("-- Data track") if cat == c4d.CTRACK_CATEGORY_PLUGIN: print ("-- Plugin track") if name == "PLA": curve = track.GetCurve() currentKey = curve.FindKey(doc.GetTime()) key = currentKey['key'] bc = key.GetGeData() print (bc) print (len(bc)) for index, value in bc: print(f"id: {index}, val: {value}") if __name__=='__main__': main()
Took me additional iterations to find that GetGeData returns a BaseContainer, but now I know that this BaseContainer contains an element which is a c4d.PLAData object.
So, at least in this case it is not necessary to access the key through GetParameter. I suspect that the line
Data of special tracks can be read and set using C4DAtom::GetParameter() and C4DAtom::SetParameter().
from the C++ CKey manual does not refer to the data content of the keys, but to the track's data itself.E.g. the Sound track does not have keys but stores its data in the CTrack object, and since there is no special derived class for a sound track, there cannot be specialized functions to retrieve that data, so if you want the sound's filename, you have to use GetParameter.
Which can, under Python, be shortened to the square bracket notation as this is only a shorthand for GetParameter anyway.Correct me if I'm wrong...
-
Hi,
cKey::SetDParameter is called with a GeData.
This GeData is set depending on the type of the description gadget. This is done in C4DAtom::SetParameter like so
const BaseContainer *bc = desc.GetParameter(id, temp, ar); ... switch (bc->GetId()) case DTYPE_LONG: // special case if it's a cycle // clamp the value to min or max if needed. // data is a Int32 case DTYPE_REAL: // clamp the value to min or max if needed // data is a Float // both color and vector are the same case DTYPE_COLOR: case DTYPE_VECTOR: // special case as a vector is a bit special // data is a Vector case DTYPE_TIME: // data is a BaseTime case DTYPE_BOOL: // data is a Int32 case DTYPE_GROUP: case DTYPE_SEPARATOR: break; default: // try to find a plugin for that data
now on CKey::SetDParameter, as you can see depending on the track type, SetValue or SetGeData Is called.
switch (track type) case CTRACK_CATEGORY_VALUE: SetValue(seq, t_data.GetFloat()); flags |= DESCFLAGS_SET::PARAM_SET; break; case CTRACK_CATEGORY_DATA: SetGeData(seq, t_data); flags |= DESCFLAGS_SET::PARAM_SET; break; case CTRACK_CATEGORY_PLUGIN: CriticalStop(); break;
I'm not sure what's the goal here. We all know that our documentation isn't ultra-precise in all points, and it's not its goal.
Specially on things that can be old like track and animation.I could spend more time and do lots of more test to see when and when not this function is called or not but as you know c4d's code is huge and we can't dissect all part of the code like this. (i wish we could)
I am missing something here?
Cheers,
Manuel -
@m_magalhaes said in What's really happening inside a CKey?:
I'm not sure what's the goal here. We all know that our documentation isn't ultra-precise in all points, and it's not its goal.
Specially on things that can be old like track and animation.
I could spend more time and do lots of more test to see when and when not this function is called or not but as you know c4d's code is huge and we can't dissect all part of the code like this. (i wish we could)
I am missing something here?Thanks for answering; no, I don't think you're missing anything. I was just trying to explain the topic in my Python course, based on a ton of code I have written in the past, and I came to notice that tracks of type integer don't behave as I expected them.
That is probably my fault as I have only really used float tracks in the past (mostly MoCap stuff) so the issue never presented itself before. It's also a bit more ambiguous in Python than in C++ since Python does not have the GeData class but uses "any" type.
I believe I have written and checked enough code now to understand the handling for non-float tracks, so I am going to close the topic.
(It would be interesting to follow up some more about your second code sample, as it gets to a CriticalStop on a Plugin track, while I definitely can use GetGeData on the key of a PLA track which is a Plugin track... but I suppose this is too much internal Cinema code to analyze, and I cannot follow the call stack here all the way down to SetDParameter. Maybe some other time...)
Thanks again.
-
@cairyn said in What's really happening inside a CKey?:
(It would be interesting to follow up some more about your second code sample, as it gets to a CriticalStop on a Plugin track, while I definitely can use GetGeData on the key of a PLA track which is a Plugin track... but I suppose this is too much internal Cinema code to analyze, and I cannot follow the call stack here all the way down to SetDParameter. Maybe some other time...)
This is in CKey::SetDParameter and that is probably never called for a plugin track. That is why it's doing nothing. But to be sure, i would need to create (or use our sdk example) plugin track.
I add a task to our task pool but we have so much to document before that...
Cheers,
Manuel -
Hello @Cairyn,
I have just updated the Python docs regarding the class
CKey
. These changes will be reflected in an upcoming release of the Python SDK. One minor thing for you and to avoid confusion for future readers of this thread: The information brought forwards here that keys of typeDTYPE_BOOL
must be written withCKey.SetValue()
is incorrect..SetValue()
must only be used for writingDTYPE_REAL
andDTYPE_LONG
parameters, i.e.,float
andint
values in Python terms.DYTPE_BOOL
key values must be written withCKey.SetGeData()
.Cheers,
Ferdinandimport c4d def main(): """Example for writing the 'use color' track of a material. The function expects the first material in the document to have a 'use-color' track, i.e., an animation for the little check box for the color channel. It will write into the first key of that track. """ # Get the first material of the document and its first track. mat = doc.GetFirstMaterial() if mat is None: raise RuntimeError("No material found.") track = mat.GetFirstCTrack() if track is None: raise RuntimeError("Material has no tracks.") # Bail when this first track is not for the boolean "use color" parameter # of the material. if track.GetDescriptionID()[0] != c4d.MATERIAL_USE_COLOR: raise RuntimeError("'Use-Color' track not found") # Get the curve of the track and its first key. curve = track.GetCurve(c4d.CCURVE_CURVE) if curve.GetKeyCount() < 1: raise RuntimeError("Curve has no keys.") key = curve.GetKey(0) # Print the current value of the boolean key and then set it to `True`. print (key.GetGeData()) key.SetGeData(curve, True) # Push an update event to Cinema 4D. c4d.EventAdd() # Execute main() if __name__=='__main__': main()
-
@ferdinand said in What's really happening inside a CKey?:
I have just updated the Python docs regarding the class CKey. These changes will be reflected in an upcoming release of the Python SDK.
Thank you!
One minor thing for you and to avoid confusion for future readers of this thread: The information brought forwards here that keys of type DTYPE_BOOL must be written with CKey.SetValue() is incorrect.
Hmm, I just tried finding where that was claimed, can't see it right now. Anyway, if you go by the track's CATEGORY, you can easily make the distinction between SetValue and SetGeData. That would work even in ambiguous cases.
-
Hi @Cairyn,
I do not really know where this has been claimed, but there was a note in our internal systems which summarized this thread as that the data types
DTYPE_REAL
,DTYPE_LONG
, andDTYPE_BOOL
should be written with.SetValue()
. For which our documentation currently makes the slightly misleading claim that is should be used for float values. Which I believe was at least partially the starting point of this thread.I just wanted to make sure that no unclarities remain for future readers,
DTYPE_BOOL
keys/tracks should be written withSetGeData()
and.SetValue()
can be used forDTYPE_REAL
andDTYPE_LONG
type keys.Cheers,
Ferdinand -
DTYPE_LONG can be used with SetGeData if the track category is CTRACK_CATEGORY_DATA, see the third post in this thread. This is the only ambiguous datatype at the moment, and as other datatypes wouldn't make sense with SetValue I think it will remain the only one. But I do claim that the category of the track determines whether you need to use SetValue or SetGeData. (That is almost the same in effect... but in case of DTYPE_LONG not quite.)