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?
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:
- 2 start points: Value is always 1 and the position is directly controlled.
- 2 end points: “Target value” is 0 and the “target position” is offset from their start point, controlled directly by an offset value. But their “true value and position” varies, depending on three possible states this setup can be in:
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:
- Value = 1
- Position = driven directly
- 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:
- Value = 1
- Position = driven directly (/irrelevant)
- 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:
- Value = 1
- Position = driven directly
… 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:
- When the falloffs start to overlap, the distance between the start points must be equal to the sum of both falloffs: distancestartPoints = sumfalloff
- 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.
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
NodeCalculator code snippet
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, end_height, 2), # Left end graph point (end_position, end_height, 2), # Right end graph point (r_start, 1, 2), # Right start graph point ], )