Просмотр исходного кода

Added "loopBlendTime" property for animation clips to support interpolating between the end points of an animation clip when looping. This allows clips to animate smoother when the end points of a clip do not exactly match up (which is all too common with animated character content).

A new overloaded version of Curve::evaluate was also added to support this feature.

gameplay-encoder binary was updated for windows, but still needs to be updated for mac and linux.
Steve Grenier 13 лет назад
Родитель
Сommit
c3b419a6c2

+ 9 - 4
gameplay-encoder/src/FBXSceneEncoder.cpp

@@ -482,11 +482,16 @@ void FBXSceneEncoder::loadAnimationChannels(FbxAnimLayer* animLayer, FbxNode* fb
     // Evaulate animation curve in increments of frameRate and populate channel data.
     FbxAMatrix fbxMatrix;
     Matrix matrix;
-    float increment = 1000.0f / frameRate;
-    for (float time = startTime; time <= stopTime; time += increment)
+    double increment = 1000.0 / (double)frameRate;
+    int frameCount = (int)ceil((double)(stopTime - startTime) / increment) + 1; // +1 because stop time is inclusive
+    for (int frame = 0; frame < frameCount; ++frame)
     {
-        // Clamp time to stopTime
-        time = std::min(time, stopTime);
+        double time = startTime + (frame * (double)increment);
+
+        // Note: We used to clamp time to stop time, but FBX sdk does not always produce
+        // and accurate stopTime (sometimes it is rounded down for some reason), so I'm
+        // disabling this clamping for now as it seems more accurate under normal circumstances.
+        //time = std::min(time, (double)stopTime);
 
         // Evalulate the animation at this time
         FbxTime kTime;

+ 3 - 1
gameplay/src/Animation.cpp

@@ -254,7 +254,7 @@ void Animation::createClips(Properties* animationProperties, unsigned int frameC
         int begin = pClip->getInt("begin");
         int end = pClip->getInt("end");
 
-        AnimationClip* clip = createClip(pClip->getId(), ((float) begin / frameCount) * _duration, ((float) end / frameCount) * _duration);
+        AnimationClip* clip = createClip(pClip->getId(), ((float)begin / (frameCount-1)) * _duration, ((float)end / (frameCount-1)) * _duration);
 
         const char* repeat = pClip->getString("repeatCount");
         if (repeat)
@@ -279,6 +279,8 @@ void Animation::createClips(Properties* animationProperties, unsigned int frameC
             clip->setSpeed(value);
         }
 
+        clip->setLoopBlendTime(pClip->getFloat("loopBlendTime")); // returns zero if not specified
+
         pClip = animationProperties->getNextNamespace();
     }
 }

+ 61 - 36
gameplay/src/AnimationClip.cpp

@@ -11,7 +11,7 @@ namespace gameplay
 
 AnimationClip::AnimationClip(const char* id, Animation* animation, unsigned long startTime, unsigned long endTime)
     : _id(id), _animation(animation), _startTime(startTime), _endTime(endTime), _duration(_endTime - _startTime), 
-      _stateBits(0x00), _repeatCount(1.0f), _activeDuration(_duration * _repeatCount), _speed(1.0f), _timeStarted(0), 
+      _stateBits(0x00), _repeatCount(1.0f), _loopBlendTime(0), _activeDuration(_duration * _repeatCount), _speed(1.0f), _timeStarted(0), 
       _elapsedTime(0), _crossFadeToClip(NULL), _crossFadeOutElapsed(0), _crossFadeOutDuration(0), _blendWeight(1.0f), 
       _beginListeners(NULL), _endListeners(NULL), _listeners(NULL), _listenerItr(NULL), _scriptListeners(NULL)
 {
@@ -106,11 +106,14 @@ void AnimationClip::setRepeatCount(float repeatCount)
 
     if (repeatCount == REPEAT_INDEFINITE)
     {
-        _activeDuration = _duration;
+        _activeDuration = _duration + _loopBlendTime;
     }
     else
     {
-        _activeDuration = _repeatCount * _duration;
+        _activeDuration = _duration * _repeatCount;
+
+        if (repeatCount > 1.0f && _loopBlendTime > 0.0f)
+            _activeDuration += std::ceil(repeatCount - 1.0f) * _loopBlendTime;
     }
 }
 
@@ -121,14 +124,15 @@ float AnimationClip::getRepeatCount() const
 
 void AnimationClip::setActiveDuration(unsigned long duration)
 {
+    GP_ASSERT(duration > 0.0f);
+
     if (duration == REPEAT_INDEFINITE)
     {
-        _repeatCount = REPEAT_INDEFINITE;
-        _activeDuration = _duration;
+        _activeDuration = _duration + _loopBlendTime;
     }
     else
     {
-        _activeDuration = _duration;
+        _activeDuration = duration;
         _repeatCount = (float)_activeDuration / (float)_duration;
     }
 }
@@ -166,6 +170,18 @@ float AnimationClip::getBlendWeight() const
     return _blendWeight;
 }
 
+void AnimationClip::setLoopBlendTime(float loopBlendTime)
+{
+    _loopBlendTime = loopBlendTime;
+    if (_loopBlendTime < 0.0f)
+        _loopBlendTime = 0.0f;
+}
+
+float AnimationClip::getLoopBlendTime() const
+{
+    return _loopBlendTime;
+}
+
 bool AnimationClip::isPlaying() const
 {
     return (isClipStateBitSet(CLIP_IS_PLAYING_BIT) && !isClipStateBitSet(CLIP_IS_PAUSED_BIT));
@@ -358,57 +374,59 @@ bool AnimationClip::update(float elapsedTime)
     {
         return false;
     }
-    else if (isClipStateBitSet(CLIP_IS_MARKED_FOR_REMOVAL_BIT))
-    {   // If the marked for removal bit is set, it means stop() was called on the AnimationClip at some point
+
+    if (isClipStateBitSet(CLIP_IS_MARKED_FOR_REMOVAL_BIT))
+    {
+        // If the marked for removal bit is set, it means stop() was called on the AnimationClip at some point
         // after the last update call. Reset the flag, and return true so the AnimationClip is removed from the 
         // running clips on the AnimationController.
         onEnd();
         return true;
     }
-    else if (!isClipStateBitSet(CLIP_IS_STARTED_BIT))
+
+    if (!isClipStateBitSet(CLIP_IS_STARTED_BIT))
     {
+        // Clip is just starting
         onBegin();
     }
     else
     {
+        // Clip was already running
         _elapsedTime += elapsedTime * _speed;
 
         if (_repeatCount == REPEAT_INDEFINITE && _elapsedTime <= 0)
+        {
+            // Elapsed time is moving backwards, so wrap it back around the end when it falls below zero
             _elapsedTime = _activeDuration + _elapsedTime;
+
+            // TODO: account for _loopBlendTime
+        }
     }
 
+    // Current time within a loop of the clip
     float currentTime = 0.0f;
+
     // Check to see if clip is complete.
     if (_repeatCount != REPEAT_INDEFINITE && ((_speed >= 0.0f && _elapsedTime >= _activeDuration) || (_speed <= 0.0f && _elapsedTime <= 0.0f)))
     {
+        // We finished our active duration (including repeats), so clamp to our end value.
         resetClipStateBit(CLIP_IS_STARTED_BIT);
-        
-        if (_speed >= 0.0f)
-        {
-            // If _duration == 0, we have a "pose". Just set currentTime to 0.
-            if (_duration == 0)
-            {
-                currentTime = 0.0f;
-            }
-            else
-            {
-                currentTime = (float)(_activeDuration % _duration); // Get's the fractional part of the final repeat.
-                if (currentTime == 0.0f)
-                    currentTime = _duration;
-            }
-        }
-        else
-        {
-            currentTime = 0.0f; // If we are negative speed, the end value should be 0.
-        }
+
+        // Ensure we end off at the endpoints of our clip (-speed==0, +speed==_duration)
+        currentTime = _speed < 0.0f ? 0.0f : _duration;
     }
     else
     {
         // If _duration == 0, we have a "pose". Just set currentTime to 0.
         if (_duration == 0)
+        {
             currentTime = 0.0f;
-        else // Gets portion/fraction of the repeat.
-            currentTime = fmodf(_elapsedTime, _duration);
+        }
+        else
+        {
+            // Animation is running normally.
+            currentTime = fmodf(_elapsedTime, _duration + _loopBlendTime);
+        }
     }
 
     // Notify any listeners of Animation events.
@@ -445,12 +463,15 @@ bool AnimationClip::update(float elapsedTime)
     // Add back in start time, and divide by the total animation's duration to get the actual percentage complete
     GP_ASSERT(_animation);
 
-    // If the animation duration is zero (start time == end time, such as when there is only a single keyframe),
-    // then prevent a divide by zero and set percentComplete = 1.
-    float percentComplete = _animation->_duration == 0 ? 1 : ((float)_startTime + currentTime) / (float)_animation->_duration;
-    
-    percentComplete = MATH_CLAMP(percentComplete, 0.0f, 1.0f);
+    // Compute percentage complete for the current loop (prevent a divide by zero if _duration==0).
+    // Note that we don't use (currentTime/(_duration+_loopBlendTime)). That's because we want a
+    // % value that is outside the 0-1 range for loop smoothing/blending purposes.
+    float percentComplete = _duration == 0 ? 1 : currentTime / (float)_duration;
 
+    if (_loopBlendTime == 0.0f)
+        percentComplete = MATH_CLAMP(percentComplete, 0.0f, 1.0f);
+
+    // If we're cross fading, compute blend weights
     if (isClipStateBitSet(CLIP_IS_FADING_OUT_BIT))
     {
         GP_ASSERT(_crossFadeToClip);
@@ -503,6 +524,9 @@ bool AnimationClip::update(float elapsedTime)
     AnimationValue* value = NULL;
     AnimationTarget* target = NULL;
     size_t channelCount = _animation->_channels.size();
+    float percentageStart = (float)_startTime / (float)_animation->_duration;
+    float percentageEnd = (float)_endTime / (float)_animation->_duration;
+    float percentageBlend = (float)_loopBlendTime / (float)_animation->_duration;
     for (size_t i = 0; i < channelCount; i++)
     {
         channel = _animation->_channels[i];
@@ -514,7 +538,8 @@ bool AnimationClip::update(float elapsedTime)
 
         // Evaluate the point on Curve
         GP_ASSERT(channel->getCurve());
-        channel->getCurve()->evaluate(percentComplete, value->_value);
+        channel->getCurve()->evaluate(percentComplete, percentageStart, percentageEnd, percentageBlend, value->_value);
+
         // Set the animation value on the target property.
         target->setAnimationPropertyValue(channel->_propertyId, value, _blendWeight);
     }

+ 17 - 0
gameplay/src/AnimationClip.h

@@ -178,6 +178,22 @@ public:
      */
     float getBlendWeight() const;
 
+    /**
+     * Sets the time (in milliseconds) to append to the clip's active duration
+     * to use for blending the end points of the clip when looping.
+     *
+     * @param loopBlendTime Time spent blending end points of clip when looping.
+     */
+    void setLoopBlendTime(float loopBlendTime);
+
+    /**
+     * Returns the amount of time (in milliseconds) spent blending the clip's 
+     * end points when looping.
+     *
+     * @return Time spent blending end points of the clip when looping.
+     */
+    float getLoopBlendTime() const;
+
     /**
      * Checks if the AnimationClip is playing.
      *
@@ -392,6 +408,7 @@ private:
     unsigned long _duration;                            // The total duration.
     unsigned char _stateBits;                           // Bit flag used to keep track of the clip's current state.
     float _repeatCount;                                 // The clip's repeat count.
+    unsigned int _loopBlendTime;                        // Time spent blending the last frame of animation with the first frame, when looping.
     unsigned long _activeDuration;                      // The active duration of the clip.
     float _speed;                                       // The speed that the clip is playing. Default is 1.0. Negative goes in reverse.
     double _timeStarted;                                // The game time when this clip was actually started.

+ 79 - 18
gameplay/src/Curve.cpp

@@ -178,30 +178,93 @@ void Curve::setTangent(unsigned int index, InterpolationType type, float* inValu
 
 void Curve::evaluate(float time, float* dst) const
 {
-    assert(dst && time >= 0 && time <= 1.0f);
+    assert(dst);
 
-    // Check if the point count is 1.
-    // Check if we are at or beyond the bounds of the curve.
-    if (_pointCount == 1 || time <= _points[0].time)
+    evaluate(time, 0.0f, 1.0f, 0.0f, dst);
+}
+
+void Curve::evaluate(float time, float startTime, float endTime, float loopBlendTime, float* dst) const
+{
+    assert(dst && startTime >= 0.0f && startTime <= endTime && endTime <= 1.0f && loopBlendTime >= 0.0f);
+
+    // If there's only one point on the curve, return its value.
+    if (_pointCount == 1)
     {
         memcpy(dst, _points[0].value, _componentSize);
         return;
     }
-    else if (time >= _points[_pointCount - 1].time)
+
+    unsigned int min = 0;
+    unsigned int max = _pointCount - 1;
+    float localTime = time;
+    if (startTime > 0.0f || endTime < 1.0f)
+    {
+        // Evaluating a sub section of the curve
+        min = determineIndex(startTime, 0, max);
+        max = determineIndex(endTime, min, max);
+
+        // Convert time to fall within the subregion
+        localTime = _points[min].time + (_points[max].time - _points[min].time) * time;
+    }
+
+    if (loopBlendTime == 0.0f)
+    {
+        // If no loop blend time is specified, clamp time to end points
+        if (localTime < _points[min].time)
+            localTime = _points[min].time;
+        else if (localTime > _points[max].time)
+            localTime = _points[max].time;
+    }
+
+    // If an exact endpoint was specified, skip interpolation and return the value directly
+    if (localTime == _points[min].time)
+    {
+        memcpy(dst, _points[min].value, _componentSize);
+        return;
+    }
+    if (localTime == _points[max].time)
     {
-        memcpy(dst, _points[_pointCount - 1].value, _componentSize);
+        memcpy(dst, _points[max].value, _componentSize);
         return;
     }
 
-    // Locate the points we are interpolating between using a binary search.
-    unsigned int index = determineIndex(time);
-    
-    Point* from = _points + index;
-    Point* to = _points + (index + 1);
+    Point* from;
+    Point* to;
+    float scale;
+    float t;
+    unsigned int index;
+
+    if (localTime > _points[max].time)
+    {
+        // Looping forward
+        index = max;
+        from = &_points[max];
+        to = &_points[min];
 
-    // Calculate the fractional time between the two points.
-    float scale = (to->time - from->time);
-    float t = (time - from->time) / scale;
+        // Calculate the fractional time between the two points.
+        t = (localTime - from->time) / loopBlendTime;
+    }
+    else if (localTime < _points[min].time)
+    {
+        // Looping in reverse
+        index = min;
+        from = &_points[min];
+        to = &_points[max];
+
+        // Calculate the fractional time between the two points.
+        t = (from->time - localTime) / loopBlendTime;
+    }
+    else
+    {
+        // Locate the points we are interpolating between using a binary search.
+        index = determineIndex(localTime, min, max);
+        from = &_points[index];
+        to = &_points[index == max ? index : index+1];
+
+        // Calculate the fractional time between the two points.
+        scale = (to->time - from->time);
+        t = (localTime - from->time) / scale;
+    }
 
     // Calculate the value of the curve discretely if appropriate.
     switch (from->type)
@@ -1157,11 +1220,9 @@ void Curve::interpolateQuaternion(float s, float* from, float* to, float* dst) c
         Quaternion::slerp(to[0], to[1], to[2], to[3], from[0], from[1], from[2], from[3], s, dst, dst + 1, dst + 2, dst + 3);
 }
 
-int Curve::determineIndex(float time) const
+int Curve::determineIndex(float time, unsigned int min, unsigned int max) const
 {
-    unsigned int min = 0;
-    unsigned int max = _pointCount - 1;
-    unsigned int mid = 0;
+    unsigned int mid;
 
     // Do a binary search to determine the index.
     do 

+ 34 - 2
gameplay/src/Curve.h

@@ -345,13 +345,45 @@ public:
     void setTangent(unsigned int index, InterpolationType type, float* inValue, float* outValue);
     
     /**
-     * Evaluates the curve at the given position value (between 0.0 and 1.0 inclusive).
+     * Evaluates the curve at the given position value.
+     *
+     * Time should generally be specified as a value between 0.0 - 1.0, inclusive.
+     * A value outside this range can also be specified to perform an interpolation
+     * between the two end points of the curve. This can be useful for smoothly
+     * interpolating a repeat of the curve.
      *
      * @param time The position to evaluate the curve at.
      * @param dst The evaluated value of the curve at the given time.
      */
     void evaluate(float time, float* dst) const;
 
+    /**
+     * Evaluates the curve at the given position value (between 0.0 and 1.0 inclusive)
+     * within the specified subregion of the curve.
+     *
+     * This method is useful for evaluating sub sections of the curve. A common use for
+     * this is when evaluating individual animation clips that are positioned within a
+     * larger animation curve. This method also allows looping to occur between the
+     * end points of curve sub regions, with optional blending/interpolation between
+     * the end points (using the loopBlendTime parameter).
+     *
+     * Time should generally be specified as a value between 0.0 - 1.0, inclusive.
+     * A value outside this range can also be specified to perform an interpolation
+     * between the two end points of the curve. This can be useful for smoothly
+     * interpolating a repeat of the curve.
+     *
+     * @param time The position within the subregion of the curve to evaluate the curve at.
+     *      A time of zero representes the start of the subregion, with a time of one
+     *      representing the end of the subregion.
+     * @param startTime Start time for the subregion (between 0.0 - 1.0).
+     * @param endTime End time for the subregion (between 0.0 - 1.0).
+     * @param loopBlendTime Time (in milliseconds) to blend between the end points of the curve
+     *      for looping purposes when time is outside the range 0-1. A value of zero here
+     *      disables curve looping.
+     * @param dst The evaluated value of the curve at the given time.
+     */
+    void evaluate(float time, float startTime, float endTime, float loopBlendTime, float* dst) const;
+
     /**
      * Linear interpolation function.
      */
@@ -459,7 +491,7 @@ private:
     /**
      * Determines the current keyframe to interpolate from based on the specified time.
      */ 
-    int determineIndex(float time) const;
+    int determineIndex(float time, unsigned int min, unsigned int max) const;
 
     /**
      * Sets the offset for the beginning of a Quaternion piece of data within the curve's value span at the specified