Sunday, October 9, 2011

Useful math functions

While writing gameplay, AI, shaders, or any kind of programming involving spacial transformations or even any kind of interpolation between values (position, speed, color, sound volume, etc), we may be tempted to go the easy way and use linear interpolation, which often results in terrible looking animations.

I have rassembled over time some very useful functions that I use all the time, most of them being a generalisation of Robert Penner's easing equations.



float linear(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return x;
}
     
float inPow(float x, float p)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return pow(x, p);
}
     
float outPow(float x, float p)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
           
   int sign = p % 2 == 0 ? -1 : 1;
   return (sign * (pow(x - 1f, p) + sign));
}
     
float inOutPow(float x, float p)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
           
   x *= 2;
   if (x < 1) return inPow(x, p) / 2;
           
   int sign = p % 2 == 0 ? -1 : 1;
   return (sign / 2.0f * (pow(x - 2f, p) + sign * 2f));
}
     
float inSin(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return -cos(x * (PI / 2.0f)) + 1;
}
     
float outSin(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return sin(x *(PI / 2.0f ));
}
     
float inOutSin(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return -0.5f * (cos(PI * x) - 1.0f);
}
     
float inExp(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return pow(2.0f, 10.0f * (x - 1.0f));
}
     
float outExp(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return -pow(2, -10 * x) + 1;
}
     
float inOutExp(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return x < 0.5f ? 0.5f * pow(2f, 10f * (2f*x - 1f)) :
      0.5f * (-pow(2f, 10f * (-2f*x + 1f)) + 2f);
}
     
float inCirc(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return -(sqrt(1f - x * x) - 1f);
}
     
float outCirc(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return sqrt(1f - (x - 1f) * (x - 1f));
}
     
float inOutCirc(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return x < 1f ? -0.5f * (sqrt(1f - x * x) - 1f) :
      0.5f * (sqrt(1f - ((2f * x) - 2f) * ((2f * x)- 2f)) + 1f);
}
     
float rebound(float x)

   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
           
   if(x < (1f / 2.75f)) return 1f - 7.5625f * x * x;
   else if(x < (2f / 2.75f)) return 1f - (7.5625f * (x - 1.5f / 2.75f) * 
      (x - 1.5f / 2.75f) + 0.75f);
   else if(x < (2.5f / 2.75f)) return 1f - (7.5625f * (x - 2.25f / 2.75f) * 
      (x - 2.25f / 2.75f) + 0.9375f);
   else return 1f - (7.5625f * (x - 2.625f / 2.75f) * (x - 2.625f / 2.75f) + 
      0.984375f);
}
     
float inBack(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return x * x * ((1.70158f + 1f) * x - 1.70158f);
}
     
float outBack(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return (x - 1f) * (x - 1f) * ((1.70158f + 1f) * (x - 1f) + 1.70158f) + 1f;
}
     
float inOutBack(float x)
{
   if(x < 0.0f) return 0.0f;
   if(x > 1.0f) return 1.0f;
   return x < 0.5f ? 0.5f * (4f * x * x * ((2.5949f + 1f) * 2f * x - 2.5949f)) :
      0.5f * ((2f * x - 2f) * (2f * x - 2f) * ((2.5949f + 1f) * (2f * x - 2f) +
      2.5949f) + 2f);
}
     
float impulse(float x, float k) //k controls the stretching of the func
{
   float h = k*x;
   return h*exp(1.0f-h);
}
     
float expPulse( float x, float k, float n )
{
   return exp(-k*pow(x,n));
}


There is a few things to notice about these functions. They are all normalized between [0, 1] in x, and almost all normalized (all but in/out Back) between [0, 1] in y. And if x < 0 they return 0, if x > 1 they return 1. This makes it very easy to interpolate any kind of value (floats, vectors, matrix, colors, etc), and the fact that they are valid even when the x value is not in their comprised domain [0, 1] makes it easy to combine several of them on a timeline.

One of the most recurring use of these functions is to interpolate a value a with a value b in x seconds with a delay d, relative to time t using a function f. This translates into this generic function:


y = f((t - d) / x) * (b - a) + a


Some Examples

1) Move an object from the point posA to posB in 10 seconds, rapidly at first, with a rapidly decreasing speed.


vec3 posA(10, 10, 0);
vec3 posB(0, 0, 0);
...

void update()
{    
   position = outPow(time / 10.0f, 2.0f) * (posB - posA) + posA;
}


2) A mechanical object that falls and rebound on the ground, wait down some time, goes up slowly, and wait up some time before falling again.

void update()
{
   float time = mod(time, 8.0f); //loop every 8 sec
           
   position.y = (1.0f - rebound(time / 2.0f) +   //falls and rebound for 2 sec
                 outSin((time - 4.0f) / 2.0f)) * //ascend for 2 sec after 4 sec
                 (maxHeight - minHeight) + minHeight;
}


1 comment: