Unpacking Animation Data Wrapped by the Motion System (NLA)

Hi,

I tried to access to keyframes that are attached to layers of Motion System tag, but it seems not possible.
Those methods used to get keyframe such as GetTracks() and GetKeyCount() only return values for Default Layer of the Motion System tag.
Is there any way to get correct information of keyframes on Motion Manager layers? Thank you.

Hey @kng_ito,

Thank you for reaching out to us. Could you please provide an example scene file and the code you have written?

Cheers,
Ferdinand

Hi @ferdinand,

The scene file:
GetKeyframesInMotionSystemLeyers.c4d

The Motion System tag on the cube object has 2 Layers (Layer 0 and Layer 1).
Layer 0 has 2 keyframes in Position . X track.
Layer 1 has 3 keyframes in Position . Y track.

After selecting the Cube object, run the following script.

import c4d

def main():
    # Print the name of track and the number of keyframes for each track
    tracks = op.GetCTracks()
    for track in tracks:
        curve = track.GetCurve()
        key_count = curve.GetKeyCount()
        print(track.GetName())
        print(key_count)

if __name__ == '__main__':
    main()

The result:

Position . X
1
Position . Y
1

Hey @kng_ito,

Thank you for your reply. I still must do some interpretation here, and your initial core question,

Is there any way to get correct information of key frames on Motion Manager layers?

seems to have been slightly misleading. As you do not seem to want to retrieve the key frames for the layers in your Motion System (e.g., an animation for their strength value), but the key frames of the animation the layer is wrapping. I have changed the title of the topic to better reflect that.

You cannot just iterate over the key frames of the object for that, as they are wrapped by the motion layers. What you see in the in the Timeline Manager is just smoke and mirrors, or more specifically the data from the motion sources. This is evident by the fact that switching the active layer will also change the displayed tracks and curves.

motion_keyframes.gif

Your Cube object is actually just carrying two tracks with one frame each, which serve as dummies to be dynamically filled by the Motion System when the document is animated. So, this output of yours was correct:

Position . X: 1 Key
Position . Y: 1 Key

Branching and the Motion System

The Motion System is more or less undocumented, even in the C++ API, with no dedicated interfaces for it being publicly provided. But you can access the data you want which is stored in the branches of the Motion System tag. I am not going to expand on the concepts of branches here in all detail, but you can think of them as alternative hierarchy paths. A BaseObject carries a tag branch for all its tags, a document an object branch for all its object, etc. So, instead of just a pure up, down, previous, and next tree, there are things attached to nodes which then contain a hierarchy of their own, the branches.

The data you are looking for is stored in the c4d.NLAbase branches of the Motion System tag controlling the animation.

To find out more about the concept of branches, you can read these two answers of mine on Plugin Café and read the C++ Docs Manual for GeListHead.

  1. Simple Branch Traversal (Python): Demonstrates how to traverse a node and all its descendants in all branches and printing them in a makeshift tree. The output shown below has been created with the code from this example.
  2. Complex Branch Traversal with Branch Content Mapping (Python): Demonstrates basically the same, only that the code provided here also predicts where it must branch into to find certain kinds of nodes.
  3. GeListHead Manual: GeListHead and GeListNode::GetBranchInfo implement the branching logic. This is the C++ Manual for that, but the manual is of questionable quality as it fails to explain the core concept and instead dabbles in technical details.

Accessing the Keyframes of Animations Wrapped by a Motion System

A Motion System tag has an c4d.NLAbase == 5349 branch called 'Motion System'. It contains all the animation layers managed by the system as BaseList2D instances. These are the nodes which are represented by the layer items with sliders in the tag. Each of these layers also has a c4d.NLAbase branch called Animation Layer which holds the 'Motion Sources' for the animation wrapped by the layer. In your case the cube objects on which the original animation has been created. Here you will find the key-frames which are wrapped by the animation layer. To clarify the somewhat wordy explanation, here is a cropped branching tree for the Motion System tag in your scene.

<c4d.BaseTag object called Motion System/Motion System ...          // The Motion System tag.
  Branching into 'Motion System (5349):'                            // Its NLA branch. 
    <c4d.GeListHead>
      Direct Children:                                              // The nodes inside this branch
        <c4d.BaseList2D object called Layer 0/Motion Layer>         // The first animation layer.
          Branching into 'Animation Layer (5349):'                  // Its NLA branch. 
            <c4d.GeListHead>
              Direct Children:                                      // The nodes inside this branch.
                <c4d.BaseObject object called Cube/Cube>            // A node holding animation data.
                  Branching into 'Tracks (5350):'                   // Here starts the animation data.
                    ...
        <c4d.BaseList2D object called Layer 1/Motion Layer>         // The second layer.
          Branching into 'Animation Layer (5349):'                  // The same game again ...
            <c4d.GeListHead>
              Direct Children: 
                <c4d.BaseObject object called Cube/Cube>
                  Branching into 'Tracks (5350):'
                    ...

When we put this information together, we can reach into the Motion System data of a Motion System tag to retrieve the nodes which hold the original information, two Ocube objects in your example scene. It is not possible to retrieve the same data from tracks of the Ocube object hosting the Motion System tag, as it is simply not stored there. It is the nodes stored deep inside the tag which drive the animation of the node hosting the animation (and its direct hierarchical descendants).

Find an example which puts all these things together below.

Cheers,
Ferdinand

Result:

The full node graph for <c4d.BaseTag object called Motion System/Motion System with ID 465003000 at 3006362080256>:
----------------------------------------------------------------------------------------------------

<c4d.BaseTag object called Motion System/Motion System with ID 465003000 at 3006362080256>
  Branching into 'Motion System (5349):'
    <c4d.GeListHead object at 0x000002BBF9250DC0>
      Direct Children:'
        <c4d.BaseList2D object called Layer 0/Motion Layer with ID 465003001 at 3006362071744>
          Branching into 'Animation Layer (5349):'
            <c4d.GeListHead object at 0x000002BBF924AF40>
              Direct Children:'
                <c4d.BaseObject object called Cube/Cube with ID 5159 at 3006362212608>
                  Branching into 'Tracks (5350):'
                    <c4d.GeListHead object at 0x000002BBF926CAC0>
                      Direct Children:'
                        <c4d.CTrack object called Position . X/Track with ID 5350 at 3006362078784>
                          Branching into 'Sequences (5351):'
                            <c4d.GeListHead object at 0x000002BBF9244140>
                              Direct Children:'
                                <c4d.CCurve object called  with ID 5351 at 3006362070592>
        <c4d.BaseList2D object called Layer 1/Motion Layer with ID 465003001 at 3006362062912>
          Branching into 'Animation Layer (5349):'
            <c4d.GeListHead object at 0x000002BBF927CE80>
              Direct Children:'
                <c4d.BaseObject object called Cube/Cube with ID 5159 at 3006362173952>
                  Branching into 'Tracks (5350):'
                    <c4d.GeListHead object at 0x000002BBF9273840>
                      Direct Children:'
                        <c4d.CTrack object called Position . Y/Track with ID 5350 at 3006362111168>
                          Branching into 'Sequences (5351):'
                            <c4d.GeListHead object at 0x000002BBF927E700>
                              Direct Children:'
                                <c4d.CCurve object called  with ID 5351 at 3006362199424>
<c4d.BaseTag object called Phong/Phong with ID 5612 at 3006362101184>
----------------------------------------------------------------------------------------------------

Traversing the NLA branches of <c4d.BaseTag object called Motion System/Motion System with ID 465003000 at 3006362080256> for animation data:

<c4d.BaseList2D object called Layer 0/Motion Layer with ID 465003001 at 3006362235840>
<c4d.BaseObject object called Cube/Cube with ID 5159 at 3006362091456>
	Position . X
		Key 0: Value: 0.0, Data: None
		Key 1: Value: 100.0, Data: None
<c4d.BaseList2D object called Layer 1/Motion Layer with ID 465003001 at 3006362238144>
<c4d.BaseObject object called Cube/Cube with ID 5159 at 3006362083840>
	Position . Y
		Key 0: Value: 0.0, Data: None
		Key 1: Value: 34.0, Data: None
		Key 2: Value: 122.0, Data: None

Code:

"""Demonstrates how to unpack animation data stored inside a Motion System tag.

To run this Script Manager script, one must select an object which holds a Motion System tag. It 
will print out all keyframes which are wrapped by the animation layers of the tag. The animation
data data is stored in the #NLAbase branch relations of the tag. 

See https://plugincafe.maxon.net/topic/14298 for a more complete overview of the subject.

Note:
    This example uses two symbols exposed with `2023.0.0`. To run the script in prior versions, one
    must replace them with their integer values or attach attributes of these names and values to
    the c4d module.

    c4d.Tmotionsystem: int = 465003000
    c4d.NLAbase: int = 5349
"""
__version__ = "2023.0.0"

import c4d
import typing

op: typing.Optional[c4d.BaseObject]

def PrintRelatedNodes(node: c4d.GeListNode, indent: int=0):
    """Traverses all node relations of #node and prints them out in a tree-like manner.

    This functions is only used to demonstrate the structure of a motion tag. See 
    https://plugincafe.maxon.net/topic/14056/copy-layershader-to-a-new-material/6 for the original
    and more densely commented code.
    """
    def RepresentNode(node: c4d.GeListNode) -> None:
        """Represents a node over its string representation and branches.
        """
        print(f"{'  ' * indent}{node}")
        if node.GetBranchInfo() == None:
            return

        for item in node.GetBranchInfo():
            branchHead = item["head"]
            name = item["name"]
            tid = item["id"]

            if branchHead.GetDown() == None:
                continue
            
            print (f"{'  ' * (indent + 1)}Branching into '{name} ({tid}):'")
            PrintRelatedNodes(branchHead, indent + 2)

    if not isinstance(node, c4d.GeListNode):
        return

    if indent == 0:
        print (f"{'-' * 100}\n")

    while node:
        RepresentNode(node)
        if node.GetDown():
            print (f"{'  ' * (indent + 1)}Direct Children:'")
            PrintRelatedNodes(node.GetDown(), indent + 2)

        node = node.GetNext()

    if indent == 0:
        print (f"{'-' * 100}\n")

# --- Actual code starts here ----------------------------------------------------------------------

# A type alias for a generator which yields BaseList2D instances.
BaseList2DGenerator: typing.Type = typing.Generator[c4d.BaseList2D, None, None]

def GetMotionData(node: c4d.BaseList2D) -> BaseList2DGenerator:
    """Yields the relevant nodes of a motion system stored in #node by unpacking the branched data.
    """
    def branch(node: c4d.GeListNode, branchTypes: list[int]) -> BaseList2DGenerator:
        """Yields the branches of #node whose ID is contained in #branchTypes.

        Yielded will not be the branch head but the first node in it. Branches with a matching type
        but no nodes will be ignored.
        """
        branchCollection: typing.Optional[list[dict]] = node.GetBranchInfo()
        if branchCollection is None:
            return

        for branch in branchCollection:
            if branch["head"] and branch["id"] in branchTypes and branch["head"].GetDown():
                yield branch["head"].GetDown()

    def iter(node: c4d.GeListNode, branchTypes: list[int]) -> BaseList2DGenerator:
        """Yields the descendants of #node, including its branches and the branches of descendants.
        
        Traversed will only be branches whose ID is contained in #branchTypes.
        """
        if not node:
            return
        
        while (node):
            yield node
            for branchNode in branch(node, branchTypes):
                for child in iter(branchNode, branchTypes):
                    yield child

            for child in node.GetChildren():
                for item in iter(child, branchTypes):
                    yield item
            node = node.GetNext()

    # Iterate over all descendants of #node, branching into all branches that are of type NLAbase.
    # Both motion systems and animation layers live under this branch ID.
    motionTypes: list[int] = [c4d.NLAbase]
    for head in branch(node, motionTypes):
        for node in iter(head, motionTypes):
            yield node

def main() -> None:
    """Runs the example.
    """
    # Check if there is a selected object and that it has a Motion System tag.
    if not op:
        print ("Please select an object.")
        return
    
    tag: c4d.BaseTag = op.GetTag(c4d.Tmotionsystem)
    if not isinstance(tag, c4d.BaseTag):
        print ("Please select an object that has a motion system tag.")
        return

    # To understand what is going on here, we should look at the node data stored in the tag, i.e.,
    # unpack its branches. The tag stores an object for each animation layer it holds under the
    # animation layer branches of its motion system branch.
    print ("The full node graph for {tag}:")
    PrintRelatedNodes(tag)

    # Traverse #tag for the key frames contained within it. When you would have animated the layer
    # strength of the Animation layers, this would also be printed here. Adjust to your liking to
    # retrieve only the data you need.
    print ("Traversing the NLA branches of {tag} for animation data:")
    for node in GetMotionData(tag):
        print (f"Node in an NLA branch: {node}")

        # Iterate over the tracks and curves of the NLA node.
        for track in node.GetCTracks():
            print (f"\t{track.GetName()}")
            curve: c4d.CCurve = track.GetCurve()

            for kid in range(curve.GetKeyCount()):
                key: c4d.CKey = curve.GetKey(kid)
                print(f"Key {kid}: Value: {key.GetValue()}, Data: {key.GetGeData()}")


if __name__ == '__main__':
    main()

Hi @ferdinand,

Thank you for the detailed explanation and the code.
It works perfectly and the problem is solved!