ragdolldynamics / ragdoll-maya

Real-time physics for Maya
https://ragdolldynamics.com
148 stars 18 forks source link

Recording Performance Research #4

Open mottosso opened 3 years ago

mottosso commented 3 years ago

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

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.

https://user-images.githubusercontent.com/2152766/138073786-f2b62d28-ae7b-435b-9174-38f13615743d.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 commented 3 years ago

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.