SOLVED SetTime() and document updating

I've created two Python scripts (tag based) that transfer animation from one character to another. The first one simply copies the source character joint rotation keyframes to the target character joints. This script works fine.

The second script is a bit different. Here the source character is setup with IK chains and goals. I first perform my animation using the goals plus some xPresso that also moves the joints. In this case, the joints don't have any keyframes to copy because the joints are being rotates by external means. I wrote my script to read the source joints rotation values, then copy them to the target character joints creating keyframe. I'm using the doc.SetTime() function to move to the next frame in the timeline. The problem is, it looks like doc.SetTime() doesn't actually change the current frame while the script is running. My target character does get new keyframes for each frame, but it's only of the first frame. It's like time line (and source character) isn't getting updated every time doc.SetTime() is called. Is there a Python function that tells Cinema 4D to update everything when the current frame is changed by the script?

I'm using Cinema 4D R21.

@brucek5

Well, first of all you wouldn't need Event Notification if the button is on the Python tag itself, instead of the null. For its own controls, the message function already receives all events:

import c4d

def message(msg_type, data):
    if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
        print (data)
        desc_id = data['id']
        print (desc_id, " received")

def main():
    pass

(to be used in a Python tag with one button)

This is still called very often but at least doesn't require the whole notification shebang which is listed as private in the documentation, and as long as you use early return methods, it's not bad.
There may be reasons why you'd want the GUI attached to the null but in this case I would readily put it on the Python tag.

As far as Python scripts go: Yes, the advantage of a Python tag is that the code is bound to the document/project/file, which is not the case with a script, which resides separately in the script folder, or a full Python plugin, which goes in the plugin folder. There are arguments for both ways to store Python code - the script or plugin is available for all projects (if properly generalized); the plugin even allows setting up a GUI that can be embedded in the layout.

In your case, I am honestly not sure whether I recommend a script or plugin; I guess I personally would realize it as such (being a script fan 😁 ) but if the code is tightly bound to your project, doing it within the tag is just as fine, as long as you execute the code as reaction to a button (in the message function) and not as part of the scene evaluation (in the main function).

Setting up your own dialog (or even plugin, for which you'd need a unique plugin ID to be obtained from Maxon) indeed requires a bit more work than the bare user data. Here's a sample script (not plugin) that opens a very simple dialog with two buttons:

import c4d
from c4d import gui, utils

ID_EDIT_BUTTONGROUP = 1007
ID_EDIT_OKBUTTON = 1008
ID_EDIT_CANCELBUTTON = 1009

class settingsDialog(c4d.gui.GeDialog):

    def __init__(self):
        c4d.gui.GeDialog.__init__(self)
        self.exitok = False


    def CreateLayout(self):
        self.SetTitle("Several text fields")
        if self.GroupBegin(ID_EDIT_BUTTONGROUP, c4d.BFH_CENTER, 2, 0):
            self.AddButton(ID_EDIT_OKBUTTON, c4d.BFH_LEFT, name="OK")
            self.AddButton(ID_EDIT_CANCELBUTTON, c4d.BFH_LEFT, name="Cancel")
        self.GroupEnd()
        return True


    def Command(self, id, msg):
        if id==ID_EDIT_OKBUTTON:
            print ("OK clicked")
            self.exitok = True
            self.Close()
        if id==ID_EDIT_CANCELBUTTON:
            print ("Cancel clicked")
            self.exitok = False
            self.Close()
        return True


def main():
    sDialog = settingsDialog()
    if sDialog.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE):
        print ("Exit:", sDialog.exitok)


if __name__=='__main__':
    main()

I do have lessons for script dialogs galore here (section 10):
https://www.patreon.com/cairyn
and also simple command plugins that allow you to embed the dialog in the GUI, but that is not really the core of the question. (There are also examples here on the forum if you search for GeDialog, I assume.)

Back to the original topic:
I have set up a simple example with a cube falling on a plane through dynamics.
The Python tag contains a button that, if clicked, executes the scene for frames 0-80 and creates keyframes for rotation and translation of the cube. The tag's code looks like this:

import c4d

def SetKey (currentTime, obj, parameterDesc, value):
    track = obj.FindCTrack(parameterDesc)

    if track == None:
        # create track for this parameter
        track = c4d.CTrack(obj, parameterDesc)
        obj.InsertTrackSorted(track)
    curve = track.GetCurve() # always present
    currentKey = curve.FindKey(currentTime) # returns dictionary "key" (Key), "idx" (Index)
    if currentKey:
        key = currentKey['key'] # key reference to change
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, key)
        key.SetValue(curve, value)
        curve.SetKeyDirty()
    else:
        defkey, dub = doc.GetDefaultKey() # new key to insert
        key = defkey.GetClone() # needed?
        key.SetValue(curve, value)
        key.SetTime(curve, currentTime)
        key.SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE)
        curve.InsertKey(key, True)
        doc.AddUndo(c4d.UNDOTYPE_NEW, key)

def message(msg_type, data):
    if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
        print (data) # irrelevant since there is only one button
        fps = doc.GetFps()
        theCube = doc.GetFirstObject()
        for frame in range(81):
            doc.SetTime(c4d.BaseTime(frame, fps))
            doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_INTERNALRENDERER)
            print ("Rot:", theCube[c4d.ID_BASEOBJECT_REL_ROTATION])
            print ("Pos:", theCube[c4d.ID_BASEOBJECT_REL_POSITION])

            descX = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
            )
            descY = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
            )
            descZ = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
            )
            val = theCube[descX]
            SetKey (doc.GetTime(), theCube, descX, val)
            val = theCube[descY]
            SetKey (doc.GetTime(), theCube, descY, val)
            val = theCube[descZ]
            SetKey (doc.GetTime(), theCube, descZ, val)
            descX = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
            )
            descY = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
            )
            descZ = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
            )
            val = theCube[descX]
            SetKey (doc.GetTime(), theCube, descX, val)
            val = theCube[descY]
            SetKey (doc.GetTime(), theCube, descY, val)
            val = theCube[descZ]
            SetKey (doc.GetTime(), theCube, descZ, val)

def main():
    pass

As you see, the scene evaluation is triggered in the aforementioned way by setting the frame and then calling ExecutePasses, which will realize the dynamics. Then the keyframes are generated for the new cube position. You can then switch off dynamics and use only the keyframes. (I am using BUILDFLAGS_INTERNALRENDERER here but I am not sure whether this makes a difference.)

You can play with that; I suppose without knowing your exact code I can't help much more.

EDIT: Here's the scene for your convenience:
Python tag triggers scene evaluation.c4d

That's correct, SetTime() doesn't perform an evaluation of the scene. You need to call BaseDocument.ExecutePasses() to do that.

Hi Cairyn,
I already tried doc.ExecutePasses(None,True,True,True, c4d.BUILDFLAGS_NONE) after the SetTime() function, but it doesn't make a difference. Are there different parameters I should be using?

Ah, I see now you wrote "tag based". Since the tag is evaluated within the pass execution, I don't think you can call ExecutePasses() at all (but then, you shouldn't be able to SetTime() either? Are you executing the script as response to a button press, or during the regular execution of the tag?) I am now a bit confused about the logic of it all. You create keyframes, so why is this a tag instead of a regular script that you would only trigger once?

If you want to create keyframes that reflect a certain time-based state, you could run through the scene in a script (not tag) and ExecutePasses from there, running the script only when necessary. This would essentially be a form of baking your dynamic target values into keyframes.

Hi Cairyn,
Thanks for the reply. Much of your insight is correct, here is the breakdown.

  1. I created a null and added user data for parameters, object links and a button. This is the only way I know how to create an interface.

  2. I attached a Python tag to the null. The Python tag has a AddEventNotification and a message function to capture when the button is pressed, which in turn calls the Python code that does the re-targeting.

  3. When the re-targeting code starts, it first obtains all the user data (interface) attached to the null, then performs the re-targeting.

I'm guessing the AddEventNotification is stealing Cinema 4D performance because it's being called anytime anything happens in Cinema 4D.

Reading between the lines of your last reply, I should be calling a Python script? Does this mean a standalone Python file? If so, do you know of any examples of that demonstrate how to create a user interface that is displayed that the Python code can obtain the parameters and run specific code when buttons is pressed?

@brucek5

Well, first of all you wouldn't need Event Notification if the button is on the Python tag itself, instead of the null. For its own controls, the message function already receives all events:

import c4d

def message(msg_type, data):
    if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
        print (data)
        desc_id = data['id']
        print (desc_id, " received")

def main():
    pass

(to be used in a Python tag with one button)

This is still called very often but at least doesn't require the whole notification shebang which is listed as private in the documentation, and as long as you use early return methods, it's not bad.
There may be reasons why you'd want the GUI attached to the null but in this case I would readily put it on the Python tag.

As far as Python scripts go: Yes, the advantage of a Python tag is that the code is bound to the document/project/file, which is not the case with a script, which resides separately in the script folder, or a full Python plugin, which goes in the plugin folder. There are arguments for both ways to store Python code - the script or plugin is available for all projects (if properly generalized); the plugin even allows setting up a GUI that can be embedded in the layout.

In your case, I am honestly not sure whether I recommend a script or plugin; I guess I personally would realize it as such (being a script fan 😁 ) but if the code is tightly bound to your project, doing it within the tag is just as fine, as long as you execute the code as reaction to a button (in the message function) and not as part of the scene evaluation (in the main function).

Setting up your own dialog (or even plugin, for which you'd need a unique plugin ID to be obtained from Maxon) indeed requires a bit more work than the bare user data. Here's a sample script (not plugin) that opens a very simple dialog with two buttons:

import c4d
from c4d import gui, utils

ID_EDIT_BUTTONGROUP = 1007
ID_EDIT_OKBUTTON = 1008
ID_EDIT_CANCELBUTTON = 1009

class settingsDialog(c4d.gui.GeDialog):

    def __init__(self):
        c4d.gui.GeDialog.__init__(self)
        self.exitok = False


    def CreateLayout(self):
        self.SetTitle("Several text fields")
        if self.GroupBegin(ID_EDIT_BUTTONGROUP, c4d.BFH_CENTER, 2, 0):
            self.AddButton(ID_EDIT_OKBUTTON, c4d.BFH_LEFT, name="OK")
            self.AddButton(ID_EDIT_CANCELBUTTON, c4d.BFH_LEFT, name="Cancel")
        self.GroupEnd()
        return True


    def Command(self, id, msg):
        if id==ID_EDIT_OKBUTTON:
            print ("OK clicked")
            self.exitok = True
            self.Close()
        if id==ID_EDIT_CANCELBUTTON:
            print ("Cancel clicked")
            self.exitok = False
            self.Close()
        return True


def main():
    sDialog = settingsDialog()
    if sDialog.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE):
        print ("Exit:", sDialog.exitok)


if __name__=='__main__':
    main()

I do have lessons for script dialogs galore here (section 10):
https://www.patreon.com/cairyn
and also simple command plugins that allow you to embed the dialog in the GUI, but that is not really the core of the question. (There are also examples here on the forum if you search for GeDialog, I assume.)

Back to the original topic:
I have set up a simple example with a cube falling on a plane through dynamics.
The Python tag contains a button that, if clicked, executes the scene for frames 0-80 and creates keyframes for rotation and translation of the cube. The tag's code looks like this:

import c4d

def SetKey (currentTime, obj, parameterDesc, value):
    track = obj.FindCTrack(parameterDesc)

    if track == None:
        # create track for this parameter
        track = c4d.CTrack(obj, parameterDesc)
        obj.InsertTrackSorted(track)
    curve = track.GetCurve() # always present
    currentKey = curve.FindKey(currentTime) # returns dictionary "key" (Key), "idx" (Index)
    if currentKey:
        key = currentKey['key'] # key reference to change
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, key)
        key.SetValue(curve, value)
        curve.SetKeyDirty()
    else:
        defkey, dub = doc.GetDefaultKey() # new key to insert
        key = defkey.GetClone() # needed?
        key.SetValue(curve, value)
        key.SetTime(curve, currentTime)
        key.SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE)
        curve.InsertKey(key, True)
        doc.AddUndo(c4d.UNDOTYPE_NEW, key)

def message(msg_type, data):
    if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
        print (data) # irrelevant since there is only one button
        fps = doc.GetFps()
        theCube = doc.GetFirstObject()
        for frame in range(81):
            doc.SetTime(c4d.BaseTime(frame, fps))
            doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_INTERNALRENDERER)
            print ("Rot:", theCube[c4d.ID_BASEOBJECT_REL_ROTATION])
            print ("Pos:", theCube[c4d.ID_BASEOBJECT_REL_POSITION])

            descX = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
            )
            descY = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
            )
            descZ = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
            )
            val = theCube[descX]
            SetKey (doc.GetTime(), theCube, descX, val)
            val = theCube[descY]
            SetKey (doc.GetTime(), theCube, descY, val)
            val = theCube[descZ]
            SetKey (doc.GetTime(), theCube, descZ, val)
            descX = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
            )
            descY = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
            )
            descZ = c4d.DescID(
                    c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                    c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
            )
            val = theCube[descX]
            SetKey (doc.GetTime(), theCube, descX, val)
            val = theCube[descY]
            SetKey (doc.GetTime(), theCube, descY, val)
            val = theCube[descZ]
            SetKey (doc.GetTime(), theCube, descZ, val)

def main():
    pass

As you see, the scene evaluation is triggered in the aforementioned way by setting the frame and then calling ExecutePasses, which will realize the dynamics. Then the keyframes are generated for the new cube position. You can then switch off dynamics and use only the keyframes. (I am using BUILDFLAGS_INTERNALRENDERER here but I am not sure whether this makes a difference.)

You can play with that; I suppose without knowing your exact code I can't help much more.

EDIT: Here's the scene for your convenience:
Python tag triggers scene evaluation.c4d

Hi Cairyn,
Thanks for the VERY detailed reply and sample project! I'll try re-doing my project.

BTW - I'm trying to join your Patreon (if I get the web site to send me an email to reset my password). I have another Python joint animation re-targeting project I'm having challenges with matrixes and changing joint rotation orientation.

Bruce Kingsley

Hi Cairyn,
Your suggested changed fixed my issue! How do I access the user data for parameters from the tag?
My old method was to first find the null object, then the user data within it:

# Find this controller
contlr = doc.SearchObject("Re-Target_Controller")
if not contlr:
    c4d.gui.MessageDialog("ERROR  Motion Retarget Controller not found!")
    return

baseSource = contlr[c4d.ID_USERDATA, 2]
if not baseSource:
    c4d.gui.MessageDialog("ERROR  Source object joint root not provided!")
    return

There are predefined variables available inside the execution context of the Python tag. op is the tag itself (just like doc is the document). So you can just write op[c4d.ID_USERDATA, 2]
obj = op.GetObject() is the object the tag is on, e.g. the null. It makes sense to place the Python tag appropriately, so you don't have to use SearchObject, which is an "expensive" operation.

Note that in my sample code I use doc.GetFirstObject() to get the cube, which is of course bad code and you shouldn't do that 😉

Hi,

and thanks a lot @Cairyn for the answers.

I have almost nothing to add, we have an example on github about how to copy keyframes from an object to another that could help. This will just copy keyframes. You only need to execute passes if you want to retrieve data driven by expression or dynamics.

I hope i did not missed any question, if so, just ask.

Cheers,
Manuel

Thanks again Cairyn. It sounds like 'op' is similar to 'this' in c#.
BTW - I was able to join your Patreon.

This posting is now closed.

@brucek5 said in SetTime() and document updating:

It sounds like 'op' is similar to 'this' in c#

op is a pre-defined variable. Using a tag to script your python, op point to the tag itself. But using the visual selector, op will point to the object in the link and could be None.

You can have a look at our manuals page where you will find Python Scripting Nodes that will give information about pre-defined variables for each node where you can use python.

Cheers,
Manuel