Fake graph intersection

I recently had to solve this issue for a lip-zip setup:
Combine two falloffs (1 to 0) from either end of a remapValue-node graph. The only controls exposed to animators should be the 2 start positions and falloffs.
Basically: Merge the blue and red line into the green one.

That turned out to be trickier than I first thought, so I wanted to share this little brain-teaser. How would you have solved this, with as few native Maya nodes as possible?

Breakdown

I will use the terms used in the remapValue node in Maya: Any point in the graph is defined by a position (horizontal axis) and a value (vertical axis).

Two start points with falloff means that we are dealing with 4 points in our graph:

Case 1 & 3 are fairly trivial, so we’ll look at those first and save the juicy case 2 for last.

Case 1: No overlap

All points are driven directly:

Start points:

  • Value = 1
  • Position = driven directly

End points:

  • Value = 0
  • Position = offset from their start points by given falloff amount

Case 3: Start points passed each other

Once the two start positions passed each other, all points should be at the maximum value of 1. Since all points are at 1 their positions don’t really matter:

Start points:

  • Value = 1
  • Position = driven directly (/irrelevant)

End points:

  • Value = 1
  • Position = irrelevant

Case 2: Ranges overlap

Now let’s look at the interesting case. The start points are still controlled 100% by the animator, but the 2 falloff points turn into 1 point, which sits where the falloff curves intersect:

Start points:

  • Value = 1
  • Position = driven directly

End points:

… Let’s tackle each axis separately:

Value of intersection:

I was happy to approximate the value of the intersection point, as long as it would ease into the extremes (0 & 1). The simplest way to mimic ease in & out was to use a remapValue node in the setup itself. It’s not mathematically precise, but it’s aesthetically pleasing and simple to set up.


I made these 2 assumptions/observations:

  1. When the falloffs start to overlap, the distance between the start points must be equal to the sum of both falloffs: distancestartPoints = sumfalloff
  2. The bigger sumfalloff becomes in comparison to the distancestartPoints, the closer the intersection value should rise towards 1

Based on these 2 observations I chose the division distancestartPoints/sumfalloff which has these properties:

  • is 1 when both terms are the same (as we enter the case of overlapping falloffs).
  • goes towards 0 when the falloff sum gets bigger and bigger in comparison to the space between the start points.

Because this division goes from 1 to 0 (instead of 0 to 1) we need to swap the inputMin & inputMax of our helper remapValue node and switch the interpolation type of the graph points to “smooth” for a nice ease in & out:

Position of intersection:

My first intuition was to use the middle between the start positions. But that is way off, as soon as the falloff values aren’t equal.

Naturally my second thought was “Ah! It must be the middle between the falloff end points then!”. But of course that was a pretty dumb idea, too:
Large, one-sided falloff ranges even shift the position outside the start points.

Both first attempts were correct in some sense: Changing any of the 4 points shifts the position of the intersection. So the solution must include start and end points.

This is the formula I ended up with:

My reasoning was: Starting from the left startPosition (SL), the intersection is somewhere between left & right startPosition (SDist). The bigger the left falloff is, the closer the intersection should shift towards the right side and vice versa (bias). These are the 2 possible extremes and what happens in the formula:

Rearranging the formula gives us:

The fraction on the right is what we used to drive the remapNode for the intersection value: distancestartPoints/sumfalloff. That means we can re-use the nodes from that part.

Conclusion

This setup is quite handy for a zip setup, especially in combination with a custom remap node that allows to sample a graph at multiple points. With Maya’s default nodes you would need one remapValue node for each sample.

Final node graph & file

[Download the Maya 2018 scene with the setup in the images/GIFs]

NodeCalculator code snippet

More information about the NodeCalculator

import node_calculator.core as noca


# Define animatable attributes (=inputs)
driver_loc = noca.locator(name="test_driver_loc")
default_falloff = 0.25
l_start = driver_loc.add_float("leftStart", value=0) - default_falloff
l_falloff = driver_loc.add_float("leftFalloff", value=default_falloff, min=0.001)
r_start = (1 + default_falloff) - driver_loc.add_float("rightStart", value=0)
r_falloff = driver_loc.add_float("rightFalloff", value=default_falloff, min=0.001)

# Calculate "set positons" (simply combining zip with falloff)
l_end_set_pos = l_start + l_falloff
r_end_set_pos = r_start - r_falloff

# Calculate how big falloffs are in comparison to distance between start points
start_point_distance = r_start - l_start
falloff_sum = l_falloff + r_falloff
falloff_amount = start_point_distance / falloff_sum

# Calculate position of end points
intersection_pos = l_start + falloff_amount * l_falloff
end_position = noca.Op.condition(
    l_end_set_pos < r_end_set_pos,  # If the falloff doesn't overlap:
    [l_end_set_pos, r_end_set_pos],  # use the set positions directly
    [intersection_pos, intersection_pos],  # otherwise use mid-point for both!
)

# Calculate height of end points
intersection_value_ease_in_out = noca.Op.remap_value(
    falloff_amount,
    input_min=1,
    input_max=0,
    values=[(0, 0, 2), (1, 1, 2)],
)
end_height = noca.Op.condition(
    l_end_set_pos < r_end_set_pos,  # If the falloff doesn't overlap:
    [0, 0],  # Use a height of 0 for both end points
    noca.Op.condition(  # furthermore...
        l_start >= r_start,  # If the left point passed the right point:
        [1, 1],  # Use a height of 1 for both end points
        intersection_value_ease_in_out,  # Otherwise fade height 0 to 1
                                         # for no overlap to full overlap.
    )
)

# Attach setup to driven
driven_locator = noca.locator(name="test_driven_loc")
sample_value = driven_locator.add_float("sampleValue", min=0, max=1)
driven_locator.ty = noca.Op.remap_value(
    sample_value,
    values=[
        (l_start, 1, 2),  # Left start graph point
        (end_position[0], end_height[0], 2),  # Left end graph point
        (end_position[1], end_height[1], 2),  # Right end graph point
        (r_start, 1, 2),  # Right start graph point
    ],
)

Leave a comment