Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recording Performance Research #4

Open
mottosso opened this issue Oct 20, 2021 · 1 comment
Open

Recording Performance Research #4

mottosso opened this issue Oct 20, 2021 · 1 comment
Labels
help wanted Extra attention is needed

Comments

@mottosso
Copy link
Collaborator

mottosso commented Oct 20, 2021

Recording is currently a two-step process, each running through the full timeline, e.g. frame 1-100.

  • For each frame:
    1. Evaluate rig, step simulation and record worldspace position of each marker
    2. Evaluate rig, convert world space positions to local space and write into Translate/Rotate channels

Step 1 is fast as the simulation is fast, and as far as Maya is concerned can run in parallel. No Maya data is modified during this step, so the Maya Evaluation Manager won't have to recreate its internal graph.

Step 2 is slow because we must once again evaluate the rig a second time, but this time cannot do it in parallel as we modify the rig each frame. As a result, evaluation happens in a single thread, and the evaluation graph is recreated each frame. It can be infinitely slower, depending on the complexity of such a rebuild. From taking 2x longer than simulating, to 100x longer. From taking 5 seconds, to taking far too long for comfort.

So, is there a way to avoid step 2?

  1. For an entirely kinematic hierarchy, we can internally compute local positions given a worldspace position, by assuming that the dynamic and kinematic parents are the same. If so, we can invert the dynamic parent and multiply it with our world matrix to arrive at a local matrix. An example of entirely kinematic is a mocap joint hierarchy. But anyting beyond that, especially involving IK and constraints and offset groups, break this requirement.
  2. For a character rig whose offsets between markers never change, we can once again use the parent marker alongside a pre-computed offset matrix to arrive at a local matrix. However this is very hard to prove up-front, as even things like rotatePivot can be animated and change over time. Resulting in an unreliable recording unless the animator explicitly guarantees that this relationship holds true.
  3. ...?

I find this problem difficult to solve, but also difficult to explain. It's complicated. But important. It's the difference between taking 500 ms and 50,000 ms to record a simulation. Given that we can evaluate the entire rig, read-only, over the full timeline, in parallel there must be a way in which we can then turn around and use what we evaluated to generate appropriate local Translate/Rotate channels without re-evaluating the rig.

So I'm leaving this here as a record of where I'm at right now and hope to solve this in the future as my understanding of the problem grows. If this sounds familiar to you reading this, or if you have any further ideas, feel free to comment on this issue.


Example

Here's an example of an IK hierarchy, representing our markers, being animated and recorded in worldspace. And then re-applied to another hierarchy, that assumes a linear, FK relationship between each recorded marker.

record2.mp4
def make_chain(name):
    with cmdx.DagModifier() as mod:
        group = mod.create_node("transform", name=name)
        root = mod.create_node("joint", name="root", parent=group)
        knee = mod.create_node("joint", name="knee", parent=root)
        foot = mod.create_node("joint", name="foot", parent=knee)
        tip = mod.create_node("joint", name="tip", parent=foot)
        
        mod.set_attr(root["translateY"], 10)
        mod.set_attr(knee["translateX"], 5)
        mod.set_attr(foot["translateX"], 5)
        mod.set_attr(tip["translateX"], 2)
        
        mod.set_attr(root["jointOrientZ"], cmdx.radians(-45))
        mod.set_attr(knee["jointOrientX"], cmdx.radians(180))
        mod.set_attr(knee["jointOrientZ"], cmdx.radians(-90))
        mod.set_attr(foot["jointOrientX"], cmdx.radians(180))
        mod.set_attr(foot["jointOrientZ"], cmdx.radians(-135))

    return group, root, knee, foot, tip

outputs = make_chain("outputs")
inputs = make_chain("inputs")

cmds.select(str(outputs[1]), str(outputs[3]))
handle, eff = map(cmdx.encode, cmds.ikHandle())

start_pos = handle["translateY"].read()
anim = {
    1: start_pos,
    20: start_pos + 5,
    40: start_pos
}

with cmdx.DagModifier() as mod:
    mod.set_attr(handle["translateY"], anim)

outputs = tuple(
    (node, {})
    for node in outputs[1:]
)

# Evaluate and Store
before = cmdx.current_time()
for frame in range(1, 45):
    time = cmdx.om.MTime(frame, cmdx.TimeUiUnit())
    cmdx.current_time(time)

    for node, mats in outputs:
        mat = node["worldMatrix"][0].as_matrix()
        mats[frame] = mat

cmdx.current_time(before)


# Restore, no evaluation
with cmdx.DagModifier() as mod:
    # Move others to the side
    mod.set_attr(inputs[0]["tz"], -5)

    for index, (node, mats) in enumerate(outputs):
        parent_node, parent_mats = outputs[index - 1] if index > 0 else (None, None)

        anim = {"rx": {}, "ry": {}, "rz": {}}
        for frame, mat in mats.items():
            parent_inverse = cmdx.Mat4()
            if parent_mats:
                assert parent_node == node.parent(), "%s != %s" % (parent_node, node)
                parent_inverse = parent_mats[frame].inverse()

            tm = cmdx.Tm(mat * parent_inverse)
            r = tm.rotation()
            anim["rx"][frame] = r.x
            anim["ry"][frame] = r.y
            anim["rz"][frame] = r.z
    
        other = cmdx.encode(node.path().replace("outputs", "inputs"))
        mod.set_attr(other["jo"], (0, 0, 0))
        mod.set_attr(other["rx"], anim["rx"])
        mod.set_attr(other["ry"], anim["ry"])
        mod.set_attr(other["rz"], anim["rz"])
        print("Animated %s" % other)
@mottosso mottosso added the help wanted Extra attention is needed label Oct 20, 2021
@mottosso
Copy link
Collaborator Author

One thing that came to mind is that, well yes we are modifying the rig each frame. But so is regular old playback! Whenever the hip moves, so does all the children, and Maya is able to cope with this in parallel. Yes, we are modifying animation curves.. But does adding keys matter? What if we pre-create keys and only modify their values? Furthermore, what if we didn't do keys, but instead tried a setAttr approach? Having the hip move via a curve node versus a setAttr should not matter, right? If it does, then what if we make a node specifically for this purpose, that we can manipulate outside of calling setAttr, because odds are the command itself forces a re-evaluation. If changes are coming from our node, then that would be treated like any other output from any other node.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant