Game Development Community

dev|Pro Game Development Curriculum

Plastic Gem #3: Ease

by Paul Dana · 06/11/2008 (6:11 am) · 15 comments

Download Code File

i936.photobucket.com/albums/ad202/vincismurf/banner.jpg

Plastic Gem #3: Ease

Difficulty: Moderate

For a list of gems see the Gem A Day page.

Hello again from Plastic Games. It is time for gem number three in our Gem A Day effort. This gem implements an Ease class. You must install the files from Gem #1: Placeable Shapes and then Gem #2: Animation Threads before you can see the Grandfather Clock example explained at the end of this resource.

www.plasticgames.com/dev/blog_images/gems/ease.jpg
Easing refers to the specific type of interpolation used when animating. For an example consider what happens when you move your hand to pick up a pencil. Your hand does not achieve its speed instantly. It starts moving slowly at first then picks up speed. This is "ease in". Similarly when your hand gets near where the pencil is sitting it starts to slow down a bit, then more and more until it finally stops. This is "ease out". The whole process of picking up pencil would be called "ease in/out" as first your hand "eases into" the motion at the start and when it reaches the pencil it "eases out" of it.

It might be hard to see the value of this resource by itself but it is a key part of some upcoming resources. The power of easing can be used for such things as applying curves to animation threads, and animating GUIs. In this resource we apply the ease to animation threads and see the effect on our Grandfather Clock:

www.plasticgames.com/dev/blog_images/gems/linear_anim.gifPendulum Swing With No Ease (Linear Interpolation)

This image shows the pendulum animation with no ease, meaning linear interpolation. The angle moved by the pendulum between frames is constant.

www.plasticgames.com/dev/blog_images/gems/ease_anim.gifPendulum Swing With Ease

This image shows the pendulum animation with sinusoidal ease in/out. The angle moved by the pendulum between frames is notconstant: the angle change is smaller at the extremes of the swing.


The easing equations you will see were initially created by Robert Penner at www.robertpenner.com. The equations were then implemented by Paul Dana into the Torque frame work. If you would like to see an example of how powerful this code can be please refer to this flash demo provided by Mr. Penner

www.robertpenner.com/easing/easing_demo.html

This code can be implemented in TGEA 1.7 and TGE 1.5.2


1) First things first, add mEase.h and mEase.cc into the math folder.

2) Now to include the TypeEaseF as a supported Math Type.

In mathType.h
class Box3F;
// > pg ease
class EaseF;
// < pg ease

AND

after DefineConsoleType( TypeBox3F ) add
// > pg ease
DefineConsoleType( TypeEaseF ) // IN TGE 
DefineConsoleType( TypeEaseF, EaseF ) // IN TGEA
// < pg ease

NOTE Use appropriate line per engine

3) Now that it is declared let define it.


in math/mathType.cc
after #include "math/mBox.h" add
// > pg ease
#include "math/mEase.h"
#include "math/mathUtils.h"
// < pg ease

after ConsoleSetType( TypeBox3F ) add

// > pg ease

ConsoleType( EaseF, TypeEaseF, sizeof(EaseF) ) // IN TGE
ConsoleType( EaseF, TypeEaseF, EaseF ) // IN TGEA 

ConsoleGetType( TypeEaseF )
{
   const Box3F* pBox = (const Box3F*)dptr;

   const EaseF* pEase = (const EaseF*)dptr;

   char* returnBuffer = Con::getReturnBuffer(256);
   dSprintf(returnBuffer, 256, "%d %d %g %g",
            pEase->dir, pEase->type, pEase->param[0], pEase->param[1]);

   return returnBuffer;
}

ConsoleSetType( TypeEaseF )
{
   EaseF* pDst = (EaseF*)dptr;

   // defaults...
   pDst->param[0] = -1.0f;
   pDst->param[1] = -1.0f;
   if (argc == 1) {
      U32 args = dSscanf(argv[0], "%d %d %f %f", // the two params are optional and assumed -1 if not present...
                         &pDst->dir, &pDst->type, &pDst->param[0],&pDst->param[1]);
      AssertWarn(args >= 2, "Warning, EaseF probably not read properly");
   } else {
      Con::printf("EaseF must be set as "dir type [param0 param1]"");
   }
}
// < pg ease

NOTE Use appropriate line per engine

It is a good time to describe the EaseType, it is a type that can take 2 to 4 parameters each separated by a space
The first parameter is how the ease is applied. You have 3 options:

In and Out = 0
In Only = 1
Out only = 2

The second Parameter is what type of Ease to use:
Linear = 0
Quadratic = 1
Cubic = 2
Quartic = 3
Quintic = 4
Sinusoidal = 5
Exponential = 6
Circular = 7
Elastic = 8
Back = 9
Bounce = 10

Two types of ease, Elastic and Back, require additional values to be passed in.

Elastic's first additional parameter is amplitude and its second parameter is period.

Back, on the other hand, only needs one additional parameter scale.

This is similar to the curve editor functions used in Max.
Example :
Cubic Ease In and Out
%ease = "0 2" ;
or use the globals
%ease = $Ease::InOut SPC $Ease::Cubic;


4) Now we need to make some globals for the types of ease and how it is applied.

Game/Game.cc (or T3d/gameFunctions.cpp in TGEA ) after includes
// > pg ease
#include "math/mEase.h"

static S32 gEaseInOut = Ease::InOut;
static S32 gEaseIn = Ease::In;
static S32 gEaseOut = Ease::Out;

static S32 gEaseLinear = Ease::Linear;
static S32 gEaseQuadratic= Ease::Quadratic;
static S32 gEaseCubic= Ease::Cubic;
static S32 gEaseQuartic = Ease::Quartic;
static S32 gEaseQuintic = Ease::Quintic;
static S32 gEaseSinusoidal= Ease::Sinusoidal;
static S32 gEaseExponential = Ease::Exponential;
static S32 gEaseCircular = Ease::Circular;
static S32 gEaseElastic = Ease::Elastic;
static S32 gEaseBack = Ease::Back;
static S32 gEaseBounce = Ease::Bounce;	  

// < pg ease

5) Then we'll expose those globals to the console.

void GameInit() in add ( or void RegisterGameFunctions() in TGEA)
// > pg ease
   // tuck our constants here...
   Con::addVariable("Ease::InOut", TypeS32, &gEaseInOut);
   Con::addVariable("Ease::In", TypeS32, &gEaseIn);
   Con::addVariable("Ease::Out", TypeS32, &gEaseOut);

   Con::addVariable("Ease::Linear", TypeS32, &gEaseLinear);
   Con::addVariable("Ease::Quadratic", TypeS32, &gEaseQuadratic);
   Con::addVariable("Ease::Cubic", TypeS32, &gEaseCubic);
   Con::addVariable("Ease::Quartic", TypeS32, &gEaseQuartic);
   Con::addVariable("Ease::Quintic", TypeS32, &gEaseQuintic);
   Con::addVariable("Ease::Sinusoidal", TypeS32, &gEaseSinusoidal);
   Con::addVariable("Ease::Exponential", TypeS32, &gEaseExponential);
   Con::addVariable("Ease::Circular", TypeS32, &gEaseCircular);
   Con::addVariable("Ease::Elastic", TypeS32, &gEaseElastic);
   Con::addVariable("Ease::Back", TypeS32, &gEaseBack);
   Con::addVariable("Ease::Bounce", TypeS32, &gEaseBounce);
   // < pg ease



6) Now that TypeEaseF is defined lets add the ease support for the 3d objects.

Ts/tsShapeInstance.h after includes
// > pg ease
#ifndef _MEASE_H_
#include "math/mEase.h"
#endif
// < pg ease

after
void setTimeScale(TSThread * thread, F32);

// > pg ease
   const EaseF &getEase(TSThread * thread);       ///< Get the ease of the thread
   void setEase(TSThread * thread, const EaseF &ease);               ///< Set the ease of the thread
   // < pg ease


7) Next we'll add Ease to the Thread class and provide an interface to set and get ease on threads.

after
F32 pos;
// > pg ease
   EaseF ease;
   // < pg ease

after
void setPos(F32);
// > pg Ease
   const EaseF &getEase();
   void setEase(const EaseF &ease);
   // < pg Ease

8) Ease is declared, so lets define and implement the ease into the existing system.
Ts/tsThread.cc after includes
// > pg ease
#ifndef _MEASE_H_
#include "math/mEase.h"
#endif
// < pg ease

9) Replace selectKeyframes to use ease.

void TSThread::advancePos(F32 delta) prior to last bracket place
// select keyframes
// > pg ease
   // ADD EASE HERE...
   // we don't handle ease types that extrapolate so pass 'true' to clamp value from 0..1
   selectKeyframes(ease.getUnitValue(pos,true),sequence,&keyNum1,&keyNum2,&keyPos);
   // < pg ease

10) Don't forget to initialize our new created ease into the thread system.

in TSThread::TSThread(TSShapeInstance * _shapeInst)
after timeScale = 1.0f;

// > pg ease
   ease.set(0,0);
   // < pg ease

11) Defining our new ease interface functions is easy just add this to the end of file.

// > pg ease
const EaseF &TSThread::getEase()
{
   return ease;
}

void TSThread::setEase(const EaseF &ease)
{
   this->ease = ease;
}

const EaseF & TSShapeInstance::getEase(TSThread * thread)
{
   return thread->getEase();
}

void TSShapeInstance::setEase(TSThread * thread, const EaseF &ease)
{
   thread->setEase(ease);
}
// < pg ease

Ok to add the ease Include into some classes, although they won't be used just yet they will be used in upcoming resources


12) Ease Support for Shapebased objects.

Game(T3d)/Shapebase.h after includes
// > pg ease
#ifndef _MEASE_H_
#include "math/mEase.h"
#endif
// < pg ease

13) Ease support for GUIs

Gui/core/guicontrol.h after includes
// > pg ease h  

#ifndef _MEASE_H_
#include "math/mEase.h"
#endif
// < pg ease


14) Provide ease variable in the shapeBase thread structure and uncomment lines provided in the Gem 3 : thread resource

In game(T3d)/shapeBase.h
In shapeBase class
After F32 speed

Uncomment
//EaseF ease;

15) Declare interface for ease in thread manipulation

void stopThreadSound(Thread& thread);
after bool setThreadPos(U32 slot, F32 pos);

Uncomment
// bool setThreadEase(U32 slot, EaseF ease);

16) Initialize ease for our thread variables

In game(T3d)/shapeBase.cc
In ShapeBase::ShapeBase()
After line mScriptThread[i].speed = 1;

Uncomment
// mScriptThread[i].ease.set(0,0); //

17) Incorporate ease into existing function

In void ShapeBase::updateThread(Thread& st)
After line mShapeInstance>setTimeScale(st.thread,st.forward?
st.speed: -st.speed);

Uncomment
// mShapeInstance->setEase(st.thread,st.ease);

18) Define Ease interface functions

After bool ShapeBase::setThreadPos(U32 slot, F32 pos)

Uncomment entire function by removing /* and */
/* bool ShapeBase::setThreadEase(U32 slot, EaseF ease)
{
   Thread& st = mScriptThread[slot];
   bool diff = ease.type != st.ease.type         || ease.dir != st.ease.dir ||
               ease.param[0] != st.ease.param[0] || ease.param[1] != st.ease.param[1];
   if (st.sequence != -1) {
      if (diff)
      {
        setMaskBits(ThreadMaskN << slot);
        st.ease = ease;
        updateThread(st);
      }
      return true;
   }
   return false;
}  
*/

19) Now add network support for our new ease variables

Within ShapeBase::packUpdate{
stream->writeFlag(st.atEnd);
After if (stream->writeFlag(st.stopPos != 1.0f))
stream->write(st.stopPos);

Uncomment entire if statement by removing /* and */
/* if (stream->writeFlag(st.ease.type != 0))
            {
               stream->write(st.ease.type);
               stream->write(st.ease.dir);
               stream->write(st.ease.param[0]);
               stream->write(st.ease.param[1]);
            }
		*/

AND

Within ShapeBase::unpackUpdate{
st.atEnd = stream->readFlag();
After else
st.stopPos = 1.0f;

Uncomment entire if-else statement by removing /* and */
/* if (stream->readFlag())
            {
               stream->read(&st.ease.type);
               stream->read(&st.ease.dir);
               stream->read(&st.ease.param[0]);
               stream->read(&st.ease.param[1]);
            }
            else
            {
               st.ease.type = 0;
               // the other types should be set already but in any case are not USEd when type == 0
               //st.ease.dir = 0;
               //st.ease.param[0] = -1;
               //st.ease.param[1] = -1;
            }
		*/

20) Finish definitions and make an ease console function

ConsoleMethod( ShapeBase, setThreadSpeed, bool, 4, 4, "(int slot, int Speed)") // After this function

Uncomment entire function by removing the /* and */

/* ConsoleMethod( ShapeBase, setEase, bool, 4, 4, "(int slot, easef ease)")
{
   int slot = dAtoi(argv[2]);
   if (object->isLockedError("setEase",slot))
      return false;
   EaseF ease;
   ease.set(argv[3]);
   if (slot >= 0 && slot < ShapeBase::MaxScriptThreads) {
      if (object->setThreadEase(slot,ease))
         return true;
   }
   return false;
}
*/

21) Example of use

You must install the files from Gem #1: Placeable Shapes and then Gem #2: Animation Threads before you can see the Grandfather Clock example explained here.

Unzip the pg03_ease.zip file provided with this resource. Copy the clock.cs file to your ~/server/scripts folder. There will already be a file of that name there from the previous two gems. Allow that file to be over-written - we want the new file.

Let's see what's changed in the Grandfather Clock now that we have ease.

The only change is the addition of this code to the onAdd() method:

// let's try out a non linear ease
   %ease = $Ease::InOut SPC $Ease::Sinusoidal;
   %obj.setEase(0, %ease);

   // and go a bit slower...
   %obj.setThreadSpeed(0, 0.5);

This code tells the pendulum to interpolate each swing using a Sinusoidal In/Out Ease. This makes it look more natural like an actual gravity operated pendulum.

22) Placing Shape in mission Editor

Place the Grandfather Clock in the mission editor using the same steps described in Gem #1: Placeable Shapes. The pendulum should look a lot more realistic now. Clearly this could have been animated "correctly" to begin with, but this example does show the most important aspect of the Ease class. You can save your team time by allowing them to tweak things, like interpolation, without needing assets to go through the art pipeline again. This is a theme we explore again and again in our gems: streamlining work and saving time. It's literally money in the bank when this is how you make your living.

23) Types of Ease

In script an ease is represented by a space separated string of numeric params, for example "0 0" means Linear interpolation. The first param is a code indicating either: InOut, In, or Out. The second param indicates the type of ease involved such as Quadratic or Sinusoidal, etc. Depending on the type of ease there might be other params (currently up to a max of 4 params) but if you do not provide a param that is considered to be interpreted as equivalent to the *default* value.

From script you can access ease with any of these predefined constants:

// in/out codes...
   $Ease::InOut
   $Ease::In
   $Ease::Out

   // ease codes...
   $Ease::Linear
   $Ease::Quadratic
   $Ease::Cubic
   $Ease::Quartic
   $Ease::Quintic
   $Ease::Sinusoidal
   $Ease::Exponential
   $Ease::Circular
   $Ease::Elastic
   $Ease::Back
   $Ease::Bounce

For example an exponential in/out ease ("0 6") would be best written as:

$Ease::InOut  SPC $Ease::Exponential

24) The Next Gem.

This example of ease is really rather weak, since the pendulum could have been just animated "correctly" to begin with, and using ease with 3D animation threads is something we have used, but not a great deal. We started with this example because it allowed us to introduce the Ease class before the next gem, which is rather large.

The next gem implements animation in the context of the Torque GUI system and uses the Ease class to boot. It is quite a large resource and takes a while to merge in but it serves as a much better example of the power of ease.

#1
06/11/2008 (6:20 am)
Wow, very very handy resource here! Thanks very much for these, nice contribution.
#2
06/11/2008 (7:37 am)
Now that is a slick resource =)
#3
06/11/2008 (7:39 am)
Thanks, really handy one. I've used a bit more "messy" version of this :) Thanks again!

Here is a quick-made function to use ease in scripts:
ConsoleFunction( mGetEase, F32, 4, 5, "(S32 direction, S32 type, F32 time, [noExtrapolation = false])")
{
   EaseF ease;
   ease.set(dAtoi(argv[1]), dAtoi(argv[2]));
   return ease.getUnitValue(dAtof(argv[3]), (argc==5 ? dAtob(argv[4]) : false));
}
Example usage:
%value = mGetEase($Ease::InOut, $Ease::Quartic, 0.7);
#4
06/11/2008 (7:24 pm)
Wow, just awesome. I'm really looking forward to the next ones.
#5
06/11/2008 (7:27 pm)
Thanks for the kind words all!

bank - hey thanks for posting that. That method would be useful for anyone wanting the ability to calculate an ease directly from the script code.

Also we have spruced up the Gem resources so if you are an "early adopter" who has seen the first three gems as they were posted you might want to REFRESH those pages and appreciate our spiffy Gem A Day banner. :-)
#6
06/12/2008 (9:12 pm)
Great resource. Keep it up.
#7
06/13/2008 (6:03 am)
You rock.
#8
07/25/2008 (5:33 pm)
some reason I'm getting this error
..\engine\game\game.cc(563) : error C2065: 'gEaseCubic' : undeclared identifier

Any ideas?
#9
07/28/2008 (10:02 am)
looks like there is a typo change
static S32 gEase3 = Ease::Cubic;
to
static S32 gEaseCubic  = Ease::Cubic;
resource and zip updated
#10
09/16/2008 (1:16 am)
I get link error.
Btw the first step look weird

In mathType.h
class Box3F;

// > pg ease
class EaseF;
// < pg ease

there is no such "class Box3F" in mathType.h
is there any way to make it work on TGEA 1.0.3 ?
#11
10/17/2008 (6:24 pm)
I found "class Box3F;" in sim/sceneObject.h and added "class EaseF;" after that. I'm using TGE 1.5.2.

These gems have been wonderful! Thank you so much for the fantastic resources!
#12
11/01/2008 (1:15 pm)
Excellent stuff!

The shape file path in clock.cs should be "~/data/shapes/clock/clock.dts", instead of "plastic/data/shapes/clock/clock.dts".
#13
03/06/2009 (9:08 am)
Incase you encounter this error

Quote:
1>mEase.cpp
1>..\..\..\source\plastic\ease\mEase.cpp(62) : error C3861: 'dSscanf': identifier not found
1>Build log was saved at "file://c:\Plastic_Games\TGEA18\GameExamples\T3D\buildFiles\Link\VC2k5.Release.Win32\T3D\BuildLog.htm"

in TGEA

Add this include to mEase.cpp
#include "core/strings/stringFunctions.h"

As of March 6 09 this code is 1.8.1 compliant
#14
05/29/2009 (8:44 am)
Thanks, works in Torque3D without a problem.
#15
02/18/2010 (2:20 pm)
Straight into 1.8.2 without issue, nice feature to have! Thanks!