Motive Animation System
An open source project by FPL.
 All Classes Functions Variables Typedefs Friends Pages
MotiveProcessors

Creating Your Own Animation Algorithms

An animation algorithm is a formula that describes the motion of a variable. Each animation algorithm is processed by a MotiveProcessor.

A MotiveProcessor holds the data for every Motivator that uses its algorithm. Once per frame, MotiveProcessor::AdvanceFrame is called and all Motivators are updated, in bulk. This bulk storage and processing allows us to optimize with SIMD and multiple threads, which is important for scalability.

You can add your own custom animation algorithms by deriving classes from one of the MotiveProcessor subclasses. To create a class that animates a one-dimensional variable, derive from MotiveProcessor1f. For a 4x4 matrix variable, derive from MatrixProcessor4f.

Example

Each MotiveProcessor subclass has its own interface that must be instantiated by your derivation. Additionally, there are macros MOTIVE_INTERFACE() and MOTIVE_INSTANCE() that you must add to your code.

For example, here is a simple one-dimensional algorithm that moves linearly from the current value to the target value, in the target time.

Put your initialization code in a header file:

// The init structure is the only code that should be exposed externally.
// When you call Motivator::Initialize() with initialization parameters of
// type LinearInit, the initialization call will be routed to
// LinearMotiveProcessor::InitializeIndices().
//
class LinearInit : public MotivatorInit {
public:
LinearInit() : MotivatorInit(kType) {}
// Defines 'kType' and other functions necessary to register this type of
// motivator.
MOTIVE_INTERFACE();
// This motivator type is rather boring. It has no configuration parameters
// at all. We could add some extra parameters here, though, if we like.
};

The remaining code can be in a .cpp file. There is no need to expose it, since it gets registered with the MotiveEngine, and all calls to it come through MotiveEngine.

class LinearMotiveProcessor : public MotiveProcessorNf {
public:
virtual ~LinearMotiveProcessor() {}
// Once per frame, the MotiveEngine calls this function. In this one call,
// we advance the simulation of _all_ linear Motivators. Bulk processing
// allows us to use SIMD or multi-threading when appropriate.
virtual void AdvanceFrame(MotiveTime delta_time) {
// We could optimize this using SIMD.
for (size_t i = 0; i < data_.size(); ++i) {
LinearData& d = data_[i];
float& value = values_[i];
// Advance the value by linearly interpolating towards the target.
if (d.target_time > 0.0f) {
const float percent_complete = delta_time / d.target_time;
value = mathfu::Lerp(value, d.target_value, percent_complete);
} else {
value = d.target_value;
}
// Decrement the target time.
d.target_time -= delta_time;
}
}
virtual MotivatorType Type() const { return LinearInit::kType; }
virtual int Priority() const { return 0; }
// Accessors to allow the user to get and set simulation values.
virtual const float* Values(MotiveIndex index) const {
return &values_[index];
}
virtual void Velocities(MotiveIndex index, MotiveDimension dimensions,
float* out) const {
const LinearData* d = &Data(index);
const float* values = &values_[index];
for (MotiveDimension i = 0; i < dimensions; ++i, ++d, ++values) {
out[i] = d->target_time <= 0.0f ? 0.0f : (d->target_value - *values) /
d->target_time;
}
}
virtual void TargetValues(MotiveIndex index, MotiveDimension dimensions,
float* out) const {
const LinearData* d = &Data(index);
for (MotiveDimension i = 0; i < dimensions; ++i, ++d) {
out[i] = d->target_value;
}
}
virtual void TargetVelocities(MotiveIndex /*index*/, MotiveDimension dimensions,
float* out) const {
for (MotiveDimension i = 0; i < dimensions; ++i) {
out[i] = 0.0f;
}
}
virtual void Differences(MotiveIndex index, MotiveDimension dimensions,
float* out) const {
const LinearData* d = &Data(index);
const float* values = &values_[index];
for (MotiveDimension i = 0; i < dimensions; ++i, ++d, ++values) {
out[i] = d->target_value - *values;
}
}
virtual MotiveTime TargetTime(MotiveIndex index,
MotiveDimension dimensions) const {
MotiveTime greatest = std::numeric_limits<MotiveTime>::min();
for (MotiveDimension i = 0; i < dimensions; ++i) {
greatest = std::max(greatest,
static_cast<MotiveTime>(Data(index).target_time));
}
return greatest;
}
// Target values are set in bulk. Please see MotiveTarget1f for a description
// of the format. It's basically an array of way points. In our case, we're
// only interested in (at most) two way points: current and target.
virtual void SetTargets(MotiveIndex index, MotiveDimension dimensions,
const MotiveTarget1f* ts) {
for (int i = 0; i < dimensions; ++i) {
LinearData& d = Data(index + i);
const MotiveTarget1f& t = ts[i];
// If the first node specifies time=0, that means we want to override the
// current values with the values specified in the first node.
const MotiveNode1f& node0 = t.Node(0);
const bool override_current = node0.time == 0;
if (override_current) {
values_[index + i] = node0.value;
}
// If the first node specifies time > 0, that means we want to override
// the
// target values with it. Or, if two nodes are specified, we use the
// second for the target values.
const MotiveNode1f* target_node =
override_current ? (t.num_nodes() > 1 ? &t.Node(1) : nullptr)
: &node0;
if (target_node != nullptr) {
d.target_value = target_node->value;
d.target_time = static_cast<float>(target_node->time);
}
}
}
protected:
struct LinearData {
float target_value;
float target_time;
LinearData() { Reset(); }
void Reset() {
target_value = 0.0f;
target_time = 0.0f;
}
};
// When an Motivator is initialized with LinearInit, this function will
// be called. We initialize data for the new Motivator.
virtual void InitializeIndices(const MotivatorInit& init, MotiveIndex index,
MotiveDimension dimensions,
MotiveEngine* /*engine*/) {
(void)init;
assert(init.type() == LinearInit::kType);
for (MotiveIndex i = index; i < index + dimensions; ++i) {
Data(i).Reset();
values_[i] = 0;
}
}
// This function is called when an index is removed. We don't have to do
// anything, but for ease of debugging, we call reset.
virtual void RemoveIndices(MotiveIndex index, MotiveDimension dimensions) {
for (MotiveIndex i = index; i < index + dimensions; ++i) {
Data(i).Reset();
values_[i] = 0;
}
}
// The base class endeavors to keep our data contiguous in memory, so
// whenever Defragment() is called, we may shuffle some indices around.
virtual void MoveIndices(MotiveIndex old_index, MotiveIndex new_index,
MotiveDimension dimensions) {
MotiveIndex old_i = old_index;
MotiveIndex new_i = new_index;
for (MotiveDimension i = 0; i < dimensions; ++i, ++new_i, ++old_i) {
data_[new_i] = data_[old_i];
values_[new_i] = values_[old_i];
}
}
// When new Motivators are being added or removed, this function may be
// called. As we grow, resize() might cause reallocs to occur, which is
// slow. However, once we reach the high-water mark, reallocs will stop.
// This is a reasonable tradeoff between the benefits and slowdowns of
// dynamically growing arrays.
virtual void SetNumIndices(MotiveIndex num_indices) {
data_.resize(num_indices);
values_.resize(num_indices);
}
// Handy accessors that double-check the validity of 'index'.
const LinearData& Data(MotiveIndex index) const {
assert(ValidIndex(index));
return data_[index];
}
LinearData& Data(MotiveIndex index) {
assert(ValidIndex(index));
return data_[index];
}
// Contiguous array of data. During Defragment(), we plug the holes from
// indices that have been removed. This allows us to process data by
// streaming it in, and maximize memory bandwidth.
std::vector<LinearData> data_;
std::vector<float> values_;
};
MOTIVE_INSTANCE(LinearInit, LinearMotiveProcessor);

The MOTIVE_INSTANCE() macro sets up the LinearMotiveProcessor to be registered with the MotiveEngine, but it does not actually register it. The registration must happen in the main program.

LinearInit::Register();

The Register() call is under LinearInit since it is generally the only thing exposed in a header file.

Now that your processor is registered, you can instantiate a motivator like so,

// Move from 10 --> -5 in 100 internal time units.
const MotiveTarget1f target = motive::CurrentToTarget1f(10, 0, -5, 0, 100);
// Create the one dimensional motivator of type Linear by passing in
// LinearInit.
Motivator1f linear_motivator(LinearInit(), &engine, target);

Your processor gets updated with all the other processors, during MotiveEngine::AdvanceFrame().

// Advance the simulation one tick at a time by calling engine.AdvanceFrame().
std::vector<vec2> points;
points.reserve(target.EndTime() + 1);
for (MotiveTime t = 0; t <= target.EndTime(); ++t) {
points.push_back(vec2(static_cast<float>(t), linear_motivator.Value()));
engine.AdvanceFrame(1);
}
printf("\n%s",
motive::Graph2DPoints(&points[0], static_cast<int>(points.size()))
.c_str());

The final output of our example is below. Notice that the variable animates linearly from 10 down to -5.

y = 10.000000
|
**
| ***
|    **
|      ***
|         ***
|            **
|              ***
|                 ***
|                    ***
|                       **
|                         ***
|                            ***
|                               ***
|                                  **
|                                    ****
|                                       ***
|                                          **
|                                            ***
|                                               ***
|--------------------------------------------------***---------------------------
|                                                     **
|                                                       ***
|                                                          ***
|                                                             ***
|                                                                **
|                                                                  ***
|                                                                     ***
|                                                                        **
|                                                                          ***
|                                                                             **
y = -5.000000

If you'd like to experiment more with this program, it is compiled for you from src/samples/linear_processor/linear_processor.cpp.