Przeglądaj źródła

Merge pull request #856 from sgrenier/next

Added support for smoothing endpoints of animation clips
Steve Grenier 13 lat temu
rodzic
commit
bbb56e7c39

+ 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

+ 18 - 1
gameplay/src/Game.cpp

@@ -258,13 +258,30 @@ void Game::resume()
 
 void Game::exit()
 {
-	// Schedule a call to shutdown rather than calling it right away.
+    // Only perform a full/clean shutdown if FORCE_CLEAN_SHUTDOWN or
+    // GAMEPLAY_MEM_LEAK_DETECTION is defined. Every modern OS is able to
+    // handle reclaiming process memory hundreds of times faster than it
+    // would take us to go through every pointer in the engine and release
+    // them nicely. For large games, shutdown can end up taking long time,
+    // so we'll just call ::exit(0) to force an instant shutdown.
+
+#if defined FORCE_CLEAN_SHUTDOWN || defined GAMEPLAY_MEM_LEAK_DETECTION
+
+    // Schedule a call to shutdown rather than calling it right away.
 	// This handles the case of shutting down the script system from
 	// within a script function (which can cause errors).
 	static ShutdownListener listener;
 	schedule(0, &listener);
+
+#else
+
+    // End the process immediately without a full shutdown
+    ::exit(0);
+
+#endif
 }
 
+
 void Game::frame()
 {
     if (!_initialized)