UNSOLVED Keyframing morph pose animations in Python

Hello again,

I have a model with a Pose Morph tag.

I can get the tag from the object. What I want to achieve is control the values of the different poses through code and keyFrame those. I actually only need to toggle them on or off as they'll either be 0 or 100%,

I've toggled visibility of a model on and off using CTrack, CCurve and CKey. Would it work similarly with the poses?

How can I loop through all the poses and toglle them on and off and keyFrame that?

Edit (by @ferdinand consolidating posts):

So here's what I got so far:

    face = doc.SearchObject('Body')
    tpose = face.GetTag(c4d.Tposemorph)
    morph = tpose.GetMorph(2)
    morphId = morph.GetID()
    descId = morphId * c4d.ID_CA_POSE_ANIMATE_CNT + \
    c4d.ID_CA_POSE_ANIMATE_OFFSET + c4d.ID_CA_POSE_ANIMATE_STRENGTH
    print(descId)

    smirkTrack = obj.FindCTrack(descId)
    if not smirkTrack:
        smirkTrack =  c4d.CTrack(face, descId)
        print(smirkTrack)

It works up until the point it has to create the CTrack:

1201
Traceback (most recent call last):
  File "C:\Users\Augustin\AppData\Roaming\Maxon\Maxon Cinema 4D R25_1FE0824E\library\scripts\test.py", line 80, in <module>
  File "C:\Users\Augustin\AppData\Roaming\Maxon\Maxon Cinema 4D R25_1FE0824E\library\scripts\test.py", line 46, in main

BaseException: the plugin 'c4d.CTrack' (ID 5350) is missing. Could not allocate instance

It doesn't return this code if I use another descID like the one for editor visibility.

I realise my DescId or the way I calculate it is what's wrong but I don't where to find the documentation for that.

So I'm making progress. I was obviously not looking in the right place to find the DescId.
I went back to the doc and found this:

For a plugin and special tracks you pass the ID.

tr = c4d.CTrack(op, c4d.DescID(ID, ID, 0))
IDs for Cinema 4D’s special tracks are:

CTpla PLA.

CTsound Sound.

CTmorph Morph.

CTtime Time.

So I tried:
descId = c4d.DescID(c4d.CTmorph, c4d.CTmorph, 0)
But get hit with:
TypeError: argument 2 must be c4d.DescLevel, not int

I then realised it should be:
descId = c4d.DescID(c4d.DescLevel(c4d.CTmorph, c4d.CTmorph, 2))
So it would be good to fix the documentation

Here's my code so far:

    visTrack.FillKey (doc, obj, keyDict["key"])
    print(c4d.Tposemorph)

    face = doc.SearchObject('Body')
    tpose = face.GetTag(c4d.Tposemorph)
    print(tpose.GetMorphCount())
    firstMorph = tpose.GetMorph(2)
    morphId = firstMorph.GetID()
    print(morphId)
    
    descId =  c4d.DescID(c4d.DescLevel(c4d.CTmorph, c4d.CTmorph, 3))
    
    smirkTrack = face.FindCTrack(descId)
    if not smirkTrack:
        smirkTrack =  c4d.CTrack(face,descId)
        
    smirkCurve = smirkTrack.GetCurve()
    # kdict = curve.AddKey(curTime)
    # kdict.SetValue(0)
    # smirkTrack.FillKeu (doc, face, kdit["key"])
    # Creates a keyframe in the memory
    key = c4d.CKey()
    
    # Fills the key with the default data
    smirkTrack.FillKey(doc, face, key)
    
    # Defines the key value
    key.SetValue(smirkCurve, 0)

    # Defines the time value
    key.SetTime(smirkCurve, curTime)
    
    smirkCurve.InsertKey(key)

Unfortunately it doesn't seem to have any effect on the values of the pose, nor the keyFrames.
I've also tried applying all of the track and curve to the tag rather than the object but it did not have any more impact. I think I'm at the point where I could use some help.

Making progress:

for i in range( tpose.GetMorphCount()):
        descID = tpose.GetMorphID(i)
        currentValue = tpose.GetParameter(descID, c4d.DESCFLAGS_GET_NONE)
        print(descID, currentValue)
        tpose.SetParameter(descID, 0, c4d.DESCFLAGS_GET_NONE )

returns all the values of the morphs as expected. I suppose they're the descIds I should use for the keyframes. But should the keyframe be on the tag or on the object the tag belongs to?

I tried something like this:

for i in range( tpose.GetMorphCount()):
        descID = tpose.GetMorphID(i)
        currentValue = tpose.GetParameter(descID, c4d.DESCFLAGS_GET_NONE)
        print(descID, currentValue)
        tpose.SetParameter(descID, 0, c4d.DESCFLAGS_GET_NONE )
        track = tpose.FindCTrack(descId)
        if not track:
            track = c4d.CTrack(tpose, descId)
            tpose.InsertTrackSorted(track)
        curve = track.GetCurve()
        keyDict = curve.AddKey(curTime)
        track.FillKey (doc, tpose, keyDict["key"])

to no avail, I also tried the same thing with the object and it didn't do much either

I also tried like this:

 for i in range( tpose.GetMorphCount()):
        descID = tpose.GetMorphID(i)
        currentValue = tpose.GetParameter(descID, c4d.DESCFLAGS_GET_NONE)
        print(descID, currentValue)
        tpose.SetParameter(descID, 1.26, c4d.DESCFLAGS_GET_NONE )
        track = face.FindCTrack(descId)
        if not track:
            print('no')
            track = c4d.CTrack(face, descId)
            face.InsertTrackSorted(track)
        curve = track.GetCurve()
        # keyDict = curve.AddKey(curTime)
        key = c4d.CKey()
        key.SetValue(curve, 0)
        key.SetTime(curve, curTime)
        curve.InsertKey(key)

Didn't do much more. However it does appear that the CTracks exist.
My brain is fried. I'm admitting defeat for tonight.

Hello @augustin,

thank you for reaching out to us. Please take a look at our Forum Guidlines regarding the support procedures and especially the structure of a question. I have consolidated your postings into one since this strayed too far from a single concise question.

Please also note that these forums are not meant to be used as a development diary. Please post only code that is related to a question and when you have made progress after you have asked a question, then you should edit your original post to reflect these updates, ideally also removing code and questions which have become irrelevant to you by then. But editing a post after a longer period should be the exception, not the rule since we must build on your questions.

So, please post a here a recent version of your executable code and your current question. Which I assume is now about where to add keyframes in some PoseMorph scenario (which would technically constitute a new topic, since the title of the topic is about how to create keyframes for a PoseMorph).

Cheers,
Ferdinand

HI @ferdinand , duly noted I'll try to be more concise in the future. Thanks for your help. So I'm at a point where I can change the values of the properties but not keyframe it. When I try to get the CTrack I get hit with following error:

BaseException: the plugin 'c4d.CTrack' (ID 5350) is missing. Could not allocate instance

Which leads me to believe I'm not using the proper descId. Here's the current code:

    face = doc.SearchObject('Body')
    tpose = face.GetTag(c4d.Tposemorph)
    
    for i in range( tpose.GetMorphCount()):
        descID = tpose.GetMorphID(i)
        currentValue = tpose.GetParameter(descID, c4d.DESCFLAGS_GET_NONE)
        print(descID, currentValue)
        tpose.SetParameter(descID, 1.0, c4d.DESCFLAGS_GET_NONE )
        track = face.FindCTrack(descID)
        if not track:
            print('no')
            track = c4d.CTrack(face, descID)
            face.InsertTrackSorted(track)
        curve = track.GetCurve()
        keyDict = curve.AddKey(curTime)
        track.FillKey (doc, face, keyDict["key"])

I've also encountered this issue...I am able to use SetParameter using GetMorphID, but keyframes do not get created on the morphs. Not sure what the issue is.

I've also tried using tag.GetCTracks, and iterating through the resulting list. This seems to let me set the keyframes, but just sets the strength of all keyframes to 0, not the actual value I am passing.

        morph_tracks = morph_tag.GetCTracks()
        for track in morph_tracks:
            morph_tag.ExitEdit(doc, True)
            create_key(track.GetCurve(), get_current_time(), 0.5, c4d.CINTERPOLATION_STEP)
            morph_tag.UpdateMorphs()

I would expect the above to set all morphs to a strength value of 50%, but they all are just 0%.

This is using the same CreateKey function from here: https://github.com/PluginCafe/cinema4d_py_sdk_extended/blob/master/scripts/04_3d_concepts/scene_elements/animation/ctrack_create_keys_r13.py

Would anyone happen to have context on how to set these keyframes???

@jmckeehen personally morph_tracks returns an empty array in this case.
C4d is so incredibly obscure with the way to get the proper CTracks it blows my mind.

@ferdinand or @m_magalhaes, if either of you think you can help me solve this problem, or are available asap to discuss this issue further I really need to solve this very quick. I'm ready to pay for a private 1 on 1 explanation of certain core concepts as I can't afford to waste hours inching towards a potential solution to what seems like a very fundamental concept.

Thank you in advance.

Hello @augustin,

thank you for clarifying your question. The reason you are getting an exception is primarily because you attempt to create the track on the wrong object, your face - which is a BaseObject. You must pass in instead the morph tag. Your loop must also be range(1, count) since the first morph target is the base morph which cannot be animated. Which is a bit counterintuitive, it was at least for me, so we will add a small warning to the docs in a future API docs update.

Find below an example which demonstrates how to create a morph animation.

Cheers,
Ferdinand

The file: morph_example.c4d
The result:

The code:

"""Showcases how to create a short animation for a pose morph tag.

The example must be run as a script manger script and assumes an object to 
be selected which has pose morph tag attached. The tag must contain at least
one already setup morph target. The example will then create an animation 
track form sec 0 to 1, animating the strength from 0% to 100%.
"""

import c4d

def main():
    """Creates a short morph animation.
    """
    # Sort out if there is a selected object with a pose morph tag with at 
    # least one morph target.
    if op is None:
        raise RuntimeError("Please select an object.")

    tag = op.GetTag(c4d.Tposemorph)
    if tag is None:
        raise AttributeError("Object has no pose morph tag.")

    if tag.GetMorphCount() < 2:
        raise RuntimeErrorr("Not enough morph targets.")

    # Get the DescID for the animation slider for that first morph. Note that
    # while the morph index is zero based, the first morph is the base morph
    # which cannot be animated. So, we must pass in 1 to get the first real 
    # morph target.
    did = tag.GetMorphID(1)

    # Get the animation track for that slider.
    track = tag.FindCTrack(did)
    if track is None:
        # You also passed in here the wrong node, namely your 'face' which was
        # a BaseObject which cannot have a pose morph track.
        track = c4d.CTrack(tag, did)
        if track is None:
            raise MemoryError("Could not allocate track.")
        # You also attempted to insert the morph track into the BaseObject.
        tag.InsertTrackSorted(track)

    # Add or modify the keys at 0 and 1 sec. The written value will be equal
    # to the time value.
    curve = track.GetCurve()
    for t in (0., 1.):
        result = curve.FindKey(c4d.BaseTime(t))
        if result is None:
            result = curve.AddKey(c4d.BaseTime(t))
            if result is None:
                raise MemoryError("Could not allocate key.")

        result["key"].SetValue(curve, t)
        result["key"].SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE)

    # Push an update event to Cinema 4D.
    c4d.EventAdd()

if __name__ == '__main__':
    main()