Motion Paths

A lot of animation involves moving either a character or the camera along a path through the scene, and so I'll provide a set of macros for defining a path.
Some Helper Macros
To make the code that follows a lot neater, I'll be using the following helper macros:
#macro LinePoly(sVariable, sConstant, sLinear)
  (sLinear * sVariable + sConstant)
#end

#macro QuadPoly(sVariable, sConstant, sLinear, sQuadratic)
  ((sQuadratic * sVariable + sLinear) * sVariable + sConstant)
#end

#macro CubePoly(sVariable, sConstant, sLinear, sQuadratic, sCubic)
  (((sCubic * sVariable + sQuadratic) * sVariable + sLinear) * sVariable + sConstant)
#end
Continuity
As with the macros that I provided in the last article, a motion path can have multiple levels of continuity, some of which are only necessary for specific applications.
C0 Continuity
The minimal level of continuity is C0, which means that there are no open gaps in the path, but otherwise the motion is as simple as possible. The following macro supports this type of motion:
#macro PathLocation1(apPath, nIndex)
  #local cPath = dimension_size(apPath, 1);
  #local nSeg = floor(nIndex);

  #if (nSeg < 0)
    #local vReturn = apPath[0] * LinePoly(nIndex, 1, -1)
      + apPath[1] * LinePoly(nIndex, 0, 1);
  #elseif (nSeg > (cPath - 2))
    #local vReturn = apPath[cPath - 2] * LinePoly(nIndex - cPath + 2, 1, -1)
      + apPath[cPath - 1] * LinePoly(nIndex - cPath + 2, 0, 1);
  #else
    #local nI = nIndex - nSeg;
    #local vReturn = apPath[nSeg] * LinePoly(nI, 1, -1)
      + apPath[nSeg + 1] * LinePoly(nI, 0, 1);
  #end
  vReturn
#end
The argument apPath should be an array of the values that lie along the path, and nIndex is the point along the path at which you want motion to be sampled. The path will consist of three portions:
  • A linear segment between every two adjacent points. The first segment will go from apPath[0] to apPath[1], the second segment will go from apPath[1] to apPath[2], and so on. The final segment will go from apPath[n - 2] to apPath[n - 1], where n is the number of points in the path.
  • A straight line extending past apPath[n - 1], following the same direction as the last segment of the path.
  • A straight line leading up to apPath[0], following the same direction as the first segment of the path.
  • The values of nIndex are mapped to the path so that apPath[0] is returned when nIndex equals zero, apPath[1] is returned when nIndex equals one, and so on, with the values between being evenly spaced along the line between the two points.
    I could have included a macro to provide the velocity along the path, but for animation purposes it would not be very useful. Among other things, the velocity at the ends of each segment is not well-defined if the adjoining segments don't have the same length or direction. A macro to define the acceleration would be even less useful; the returned value would be zero at every point except the at the ends of each segment, where it would be a delta function (a concept from control systems theory and other disciplines where calculus lurks) which does not represent a physically possible value.
    C1 Continuity
    The next level of continuity is C1, which has all of the properities of C0 continuity, and also the property that the velocity along the path changes smoothly at every point. The following macro supports this type of motion:
    #macro PathLocation2(apPath, nIndex)
      #local cPath = dimension_size(apPath, 1);
      #if (nIndex <= .5)
        #local vReturn = apPath[0] * LinePoly(nIndex, 1,  -1)
          + apPath[1] * LinePoly(nIndex, 0, 1);
      #elseif (nIndex >= cPath - 1.5)
        #local vReturn = apPath[cPath - 2] * LinePoly(nIndex - cPath + 2, 1, -1)
          + apPath[cPath - 1] * LinePoly(nIndex - cPath + 2, 0, 1);
      #else
        #local nFloor = floor(nIndex - .5);
      	#local nI = nIndex - nFloor - .5;
        #local vReturn = apPath[nFloor] * QuadPoly(nI, 1/2, -1, 1/2)
          + apPath[nFloor + 1] * QuadPoly(nI, 1/2, 1, -1)
          + apPath[nFloor + 2] * QuadPoly(nI, 0, 0, 1/2);
      #end
      vReturn
    #end
    
    The arguments have the same meaning as for the first macro. The resulting path will consist of three portions:
  • For every sequence of three points on the line there will be a quadratic segment going from the mid-point between the first and second points to the mid-point between the second and third points. It will pass through the middle point if and only if there is no bend at this middle point.
  • A straight line extending past the mid-point between apPath[n - 2] and apPath[n - 1], following the same direction as the line segment between these two points.
  • A straight line leading up to the mid-point between apPath[0] and apPath[1], following the same direction as the line segment between these two points.
  • The values of nIndex are mapped to the path so that apPath[0] is returned when nIndex equals zero, apPath[n-1] is returned when nIndex equals n - 1 (where n equals the number of points in apPath.
    Because the velocity is continuous at every point along the path, it necessarily has a well-defined value at every point along the path as well (that's actually part of the definition of continuity), and so a macro to return the velocity at each point is useful:
    #macro PathVelocity2(apPath, nIndex)
      #local cPath = dimension_size(apPath, 1);
      #if (nIndex <= .5)
        #local vReturn = apPath[1] - apPath[0];
      #elseif (nIndex >= cPath - 1.5)
        #local vReturn = apPath[cPath - 1] - apPath[cPath - 2];
      #else
        #local nFloor = floor(nIndex - .5);
      	#local nI = nIndex - nFloor - .5;
        #local vReturn = apPath[nFloor] * LinePoly(nI, -1, 1)
          + apPath[nFloor + 1] * LinePoly(nI, 1, -2)
          + apPath[nFloor + 2] * LinePoly(nI, 0, 1);
      #end
      vReturn
    #end
    
    C2 Continuity
    The next level of continuity is C2, which has all of the properities of C1 continuity, and also the property that the acceleration along the path changes smoothly at every point. The following macro supports this type of motion:
    #macro PathLocation3(apPath, nIndex)
      #local cPath = dimension_size(apPath, 1);
    
      #if (nIndex < 0)
        #local vReturn = apPath[0] * LinePoly(nIndex, 1, -1)
          + apPath[1] * LinePoly(nIndex, 0, 1);
      #elseif (nIndex < 1)
        #local vReturn = apPath[0] * CubePoly(nIndex, 1, -1, 0, 1/6)
          + apPath[1] * CubePoly(nIndex, 0, 1, 0, -1/3)
          + apPath[2] * CubePoly(nIndex, 0, 0, 0, 1/6);
      #elseif (nIndex >= cPath - 1)
        #local vReturn = apPath[cPath - 1] * LinePoly(nIndex - cPath + 1, 1, 1)
          + apPath[cPath - 2] * LinePoly(nIndex - cPath + 1, 0, -1);
      #elseif (nIndex >= cPath - 2)
        #local vReturn = apPath[cPath - 3] * CubePoly(nIndex - cPath + 2, 1/6, -1/2, 1/2, -1/6)
          + apPath[cPath - 2] * CubePoly(nIndex - cPath + 2, 2/3, 0, -1,  1/3)
          + apPath[cPath - 1] * CubePoly(nIndex - cPath + 2, 1/6, 1/2, 1/2, -1/6);
      #else
        #local nFloor = floor(nIndex);
        #local nI = nIndex - nFloor;
        #local vReturn = apPath[nFloor - 1] * CubePoly(nI, 1/6, -1/2, 1/2, -1/6)
          + apPath[nFloor] * CubePoly(nI, 2/3, 0, -1, 1/2)
          + apPath[nFloor + 1] * CubePoly(nI, 1/6, 1/2, 1/2, -1/2)
          + apPath[nFloor + 2] * CubePoly(nI, 0, 0, 0, 1/6);
      #end
      vReturn
    #end
    
    The arguments have the same meaning as for the first macro. The values of nIndex are mapped to the path so that apPath[0] is returned when nIndex equals zero, apPath[n-1] is returned when nIndex equals n - 1 (where n equals the number of points in apPath).
    The velocity is continuous at every point along the path, and so a macro to return the velocity at each point is useful:
    #macro PathVelocity3(apPath, nIndex)
      #local cPath = dimension_size(apPath, 1);
    
      #if (nIndex < 0)
        #local vReturn = apPath[1] - apPath[0];
      #elseif (nIndex < 1)
        #local vReturn = apPath[0] * QuadPoly(nIndex, -1, 0, 1/2)
          + apPath[1] * QuadPoly(nIndex, 1, 0, -1)
          + apPath[2] * QuadPoly(nIndex, 0, 0, 1/2);
      #elseif (nIndex >= cPath - 1)
        #local vReturn = apPath[cPath - 1] - apPath[cPath - 2];
      #elseif (nIndex >= cPath - 2)
        #local vReturn = apPath[cPath - 3] * QuadPoly(nIndex - cPath + 2, -1/2, 1, -1/2)
          + apPath[cPath - 2] * QuadPoly(nIndex - cPath + 2, 0, -2,  1)
          + apPath[cPath - 1] * QuadPoly(nIndex - cPath + 2, 1/2, 1, -1/2);
      #else
        #local nFloor = floor(nIndex);
        #local nI = nIndex - nFloor;
        #local vReturn = apPath[nFloor - 1] * QuadPoly(nI, -1/2, 1, -1/2)
          + apPath[nFloor] * QuadPoly(nI, 0, -2, 3/2)
          + apPath[nFloor + 1] * QuadPoly(nI, 1/2, 1, -3/2)
          + apPath[nFloor + 2] * QuadPoly(nI, 0, 0, 1/2);
      #end
      vReturn
    #end
    
    The acceleration is also continuous at every point along the path, and so this macro allows us to sample it along the path:
    #macro PathAcceleration3(apPath, nIndex)
      #local cPath = dimension_size(apPath, 1);
    
      #if (nIndex < 0)
        #local vReturn = apPath[0] - apPath[0];
      #elseif (nIndex < 1)
        #local vReturn = (apPath[0] * LinePoly(nIndex, 0, 1)
          + apPath[1] * LinePoly(nIndex, 0, -2)
          + apPath[2] * LinePoly(nIndex, 0, 1));
      #elseif (nIndex >= cPath - 1)
        #local vReturn = apPath[0] - apPath[0];
      #elseif (nIndex >= cPath - 2)
        #local vReturn = (apPath[cPath - 3] * LinePoly(nIndex - cPath + 2, 1, -1)
          + apPath[cPath - 2] * LinePoly(nIndex - cPath + 2, -2,  2)
          + apPath[cPath - 1] * LinePoly(nIndex - cPath + 2, 1, -1));
      #else
        #local nFloor = floor(nIndex);
        #local nI = nIndex - nFloor;
        #local vReturn = apPath[nFloor - 1] * LinePoly(nI, 1, -1)
          + apPath[nFloor] * LinePoly(nI, -2, 3)
          + apPath[nFloor + 1] * LinePoly(nI, 1, -3)
          + apPath[nFloor + 2] * LinePoly(nI, 0, 1);
      #end
      vReturn
    #end
    
    Some Final Notes
    These macros are designed to extend the paths along a stright line leading into and leading from the points that form the path so that scripts which use these macros won't break if you pass an index that is outside of the expected range. If you don't want these default paths appearing in your animation, then in your test renders place a series of simple objects along the path and adjust the path array so that the resulting path meets your requirements.
    You may have noticed that the macros assign a value to a local variable and then return the value of that variable. This is due to a quirk of the POV-Ray scene description language; when the POV-Ray is evaluating an expression during parsing, it gets confused if one of the terms in the expression is nested in a conditional statement.

    Comments

    Popular posts from this blog

    Transition Macros

    Titling Made Simple

    Star Field Generator