Explorar el Código

Added a way to sample a single frame of animation using Animation.Sample
Don't update mapped SO's that aren't attached to bones, if no changes were made by the animation (was resetting them to identity)
Fixing conflicts between record and playback within animation editor (changing frame during record won't cause keyframes to be added/updated)

BearishSun hace 9 años
padre
commit
0f25ea40a0

+ 21 - 1
Source/BansheeCore/Include/BsAnimation.h

@@ -56,6 +56,17 @@ namespace BansheeEngine
 		bool stopped = false;
 		bool stopped = false;
 	};
 	};
 
 
+	/** Type of playback for animation clips. */
+	enum class AnimPlaybackType
+	{
+		/** Play back the animation normally by advancing time. */
+		Normal,
+		/** Sample only a single frame from the animation. */
+		Sampled,
+		/** Do not play the animation. */
+		None
+	};
+
 	/** Internal information about a single playing animation clip within Animation. */
 	/** Internal information about a single playing animation clip within Animation. */
 	struct AnimationClipInfo
 	struct AnimationClipInfo
 	{
 	{
@@ -64,7 +75,8 @@ namespace BansheeEngine
 
 
 		HAnimationClip clip;
 		HAnimationClip clip;
 		AnimationClipState state;
 		AnimationClipState state;
-		
+		AnimPlaybackType playbackType;
+
 		float fadeDirection;
 		float fadeDirection;
 		float fadeTime;
 		float fadeTime;
 		float fadeLength;
 		float fadeLength;
@@ -307,6 +319,14 @@ namespace BansheeEngine
 		 */
 		 */
 		void crossFade(const HAnimationClip& clip, float fadeLength);
 		void crossFade(const HAnimationClip& clip, float fadeLength);
 
 
+		/**
+		 * Samples an animation clip at the specified time, displaying only that particular frame without further playback.
+		 *
+		 * @param[in] clip	Animation clip to sample.
+		 * @param[in] time	Time to sample the clip at.
+		 */
+		void sample(const HAnimationClip& clip, float time);
+
 		/** 
 		/** 
 		 * Stops playing all animations on the provided layer. Specify -1 to stop animation on the main layer 
 		 * Stops playing all animations on the provided layer. Specify -1 to stop animation on the main layer 
 		 * (non-additive animations). 
 		 * (non-additive animations). 

+ 58 - 7
Source/BansheeCore/Source/BsAnimation.cpp

@@ -9,11 +9,13 @@
 namespace BansheeEngine
 namespace BansheeEngine
 {
 {
 	AnimationClipInfo::AnimationClipInfo()
 	AnimationClipInfo::AnimationClipInfo()
-		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), curveVersion(0), layerIdx((UINT32)-1), stateIdx((UINT32)-1)
+		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), curveVersion(0), layerIdx((UINT32)-1)
+		, stateIdx((UINT32)-1), playbackType(AnimPlaybackType::Normal)
 	{ }
 	{ }
 
 
 	AnimationClipInfo::AnimationClipInfo(const HAnimationClip& clip)
 	AnimationClipInfo::AnimationClipInfo(const HAnimationClip& clip)
-		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), clip(clip), curveVersion(0), layerIdx((UINT32)-1), stateIdx((UINT32)-1)
+		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), clip(clip), curveVersion(0), layerIdx((UINT32)-1)
+		, stateIdx((UINT32)-1), playbackType(AnimPlaybackType::Normal)
 	{ }
 	{ }
 
 
 	Blend1DInfo::Blend1DInfo(UINT32 numClips)
 	Blend1DInfo::Blend1DInfo(UINT32 numClips)
@@ -112,7 +114,7 @@ namespace BansheeEngine
 		Vector<AnimationClipInfo>& clipInfos, const Vector<AnimatedSceneObject>& sceneObjects)
 		Vector<AnimationClipInfo>& clipInfos, const Vector<AnimatedSceneObject>& sceneObjects)
 	{
 	{
 		this->skeleton = skeleton;
 		this->skeleton = skeleton;
-		this->skeletonMask = skeletonMask;
+		this->skeletonMask = mask;
 
 
 		// Note: I could avoid having a separate allocation for LocalSkeletonPoses and use the same buffer as the rest
 		// Note: I could avoid having a separate allocation for LocalSkeletonPoses and use the same buffer as the rest
 		// of AnimationProxy
 		// of AnimationProxy
@@ -362,7 +364,7 @@ namespace BansheeEngine
 					if (isClipValid)
 					if (isClipValid)
 					{
 					{
 						state.curves = clipInfo.clip->getCurves();
 						state.curves = clipInfo.clip->getCurves();
-						state.disabled = false;
+						state.disabled = clipInfo.playbackType == AnimPlaybackType::None;
 					}
 					}
 					else
 					else
 					{
 					{
@@ -497,6 +499,9 @@ namespace BansheeEngine
 			state.loop = clipInfo.state.wrapMode == AnimWrapMode::Loop;
 			state.loop = clipInfo.state.wrapMode == AnimWrapMode::Loop;
 			state.weight = clipInfo.state.weight;
 			state.weight = clipInfo.state.weight;
 			state.time = clipInfo.state.time;
 			state.time = clipInfo.state.time;
+
+			bool isLoaded = clipInfo.clip.isLoaded();
+			state.disabled = !isLoaded || clipInfo.playbackType == AnimPlaybackType::None;
 		}
 		}
 	}
 	}
 
 
@@ -541,6 +546,9 @@ namespace BansheeEngine
 		{
 		{
 			AnimationState& state = layers[clipInfo.layerIdx].states[clipInfo.stateIdx];
 			AnimationState& state = layers[clipInfo.layerIdx].states[clipInfo.stateIdx];
 			state.time = clipInfo.state.time;
 			state.time = clipInfo.state.time;
+
+			bool isLoaded = clipInfo.clip.isLoaded();
+			state.disabled = !isLoaded || clipInfo.playbackType == AnimPlaybackType::None;
 		}
 		}
 	}
 	}
 
 
@@ -616,6 +624,7 @@ namespace BansheeEngine
 			clipInfo->state.speed = mDefaultSpeed;
 			clipInfo->state.speed = mDefaultSpeed;
 			clipInfo->state.weight = 1.0f;
 			clipInfo->state.weight = 1.0f;
 			clipInfo->state.wrapMode = mDefaultWrapMode;
 			clipInfo->state.wrapMode = mDefaultWrapMode;
+			clipInfo->playbackType = AnimPlaybackType::Normal;
 		}
 		}
 
 
 		mDirty |= AnimDirtyStateFlag::Value;
 		mDirty |= AnimDirtyStateFlag::Value;
@@ -649,6 +658,7 @@ namespace BansheeEngine
 				clipInfo->fadeLength = fadeLength;
 				clipInfo->fadeLength = fadeLength;
 			}
 			}
 
 
+			clipInfo->playbackType = AnimPlaybackType::Normal;
 			mDirty |= AnimDirtyStateFlag::Value;
 			mDirty |= AnimDirtyStateFlag::Value;
 		}
 		}
 	}
 	}
@@ -739,6 +749,8 @@ namespace BansheeEngine
 					clipInfo->state.weight = t;
 					clipInfo->state.weight = t;
 				else
 				else
 					clipInfo->state.weight = 0.0f;
 					clipInfo->state.weight = 0.0f;
+
+				clipInfo->playbackType = AnimPlaybackType::Normal;
 			}
 			}
 		}
 		}
 
 
@@ -755,6 +767,8 @@ namespace BansheeEngine
 			topLeftClipInfo->state.speed = 0.0f;
 			topLeftClipInfo->state.speed = 0.0f;
 			topLeftClipInfo->state.weight = (1.0f - t.x) * (1.0f - t.y);
 			topLeftClipInfo->state.weight = (1.0f - t.x) * (1.0f - t.y);
 			topLeftClipInfo->state.wrapMode = AnimWrapMode::Clamp;
 			topLeftClipInfo->state.wrapMode = AnimWrapMode::Clamp;
+
+			topLeftClipInfo->playbackType = AnimPlaybackType::Normal;
 		}
 		}
 
 
 		AnimationClipInfo* topRightClipInfo = addClip(info.topRightClip, (UINT32)-1, false);
 		AnimationClipInfo* topRightClipInfo = addClip(info.topRightClip, (UINT32)-1, false);
@@ -765,6 +779,8 @@ namespace BansheeEngine
 			topLeftClipInfo->state.speed = 0.0f;
 			topLeftClipInfo->state.speed = 0.0f;
 			topRightClipInfo->state.weight = t.x * (1.0f - t.y);
 			topRightClipInfo->state.weight = t.x * (1.0f - t.y);
 			topRightClipInfo->state.wrapMode = AnimWrapMode::Clamp;
 			topRightClipInfo->state.wrapMode = AnimWrapMode::Clamp;
+
+			topRightClipInfo->playbackType = AnimPlaybackType::Normal;
 		}
 		}
 
 
 		AnimationClipInfo* botLeftClipInfo = addClip(info.botLeftClip, (UINT32)-1, false);
 		AnimationClipInfo* botLeftClipInfo = addClip(info.botLeftClip, (UINT32)-1, false);
@@ -775,6 +791,8 @@ namespace BansheeEngine
 			topLeftClipInfo->state.speed = 0.0f;
 			topLeftClipInfo->state.speed = 0.0f;
 			botLeftClipInfo->state.weight = (1.0f - t.x) * t.y;
 			botLeftClipInfo->state.weight = (1.0f - t.x) * t.y;
 			botLeftClipInfo->state.wrapMode = AnimWrapMode::Clamp;
 			botLeftClipInfo->state.wrapMode = AnimWrapMode::Clamp;
+
+			botLeftClipInfo->playbackType = AnimPlaybackType::Normal;
 		}
 		}
 
 
 		AnimationClipInfo* botRightClipInfo = addClip(info.botRightClip, (UINT32)-1, false);
 		AnimationClipInfo* botRightClipInfo = addClip(info.botRightClip, (UINT32)-1, false);
@@ -785,6 +803,8 @@ namespace BansheeEngine
 			botRightClipInfo->state.speed = 0.0f;
 			botRightClipInfo->state.speed = 0.0f;
 			botRightClipInfo->state.weight = t.x * t.y;
 			botRightClipInfo->state.weight = t.x * t.y;
 			botRightClipInfo->state.wrapMode = AnimWrapMode::Clamp;
 			botRightClipInfo->state.wrapMode = AnimWrapMode::Clamp;
+
+			botRightClipInfo->playbackType = AnimPlaybackType::Normal;
 		}
 		}
 
 
 		mDirty |= AnimDirtyStateFlag::Value;
 		mDirty |= AnimDirtyStateFlag::Value;
@@ -806,6 +826,7 @@ namespace BansheeEngine
 			clipInfo->state.speed = mDefaultSpeed;
 			clipInfo->state.speed = mDefaultSpeed;
 			clipInfo->state.weight = 1.0f;
 			clipInfo->state.weight = 1.0f;
 			clipInfo->state.wrapMode = mDefaultWrapMode;
 			clipInfo->state.wrapMode = mDefaultWrapMode;
+			clipInfo->playbackType = AnimPlaybackType::Normal;
 
 
 			// Set up fade lengths
 			// Set up fade lengths
 			clipInfo->fadeDirection = 1.0f;
 			clipInfo->fadeDirection = 1.0f;
@@ -837,6 +858,21 @@ namespace BansheeEngine
 		mDirty |= AnimDirtyStateFlag::Value;
 		mDirty |= AnimDirtyStateFlag::Value;
 	}
 	}
 
 
+	void Animation::sample(const HAnimationClip& clip, float time)
+	{
+		AnimationClipInfo* clipInfo = addClip(clip, (UINT32)-1);
+		if (clipInfo != nullptr)
+		{
+			clipInfo->state.time = time;
+			clipInfo->state.speed = 0.0f;
+			clipInfo->state.weight = 1.0f;
+			clipInfo->state.wrapMode = mDefaultWrapMode;
+			clipInfo->playbackType = AnimPlaybackType::Sampled;
+		}
+
+		mDirty |= AnimDirtyStateFlag::Value;
+	}
+
 	void Animation::stop(UINT32 layer)
 	void Animation::stop(UINT32 layer)
 	{
 	{
 		bs_frame_mark();
 		bs_frame_mark();
@@ -907,7 +943,7 @@ namespace BansheeEngine
 		{
 		{
 			AnimationClipInfo& newInfo = mClipInfos.back();
 			AnimationClipInfo& newInfo = mClipInfos.back();
 			newInfo.clip = clip;
 			newInfo.clip = clip;
-			newInfo.layerIdx = layer;
+			newInfo.state.layer = layer;
 
 
 			output = &newInfo;
 			output = &newInfo;
 		}
 		}
@@ -973,12 +1009,19 @@ namespace BansheeEngine
 
 
 	void Animation::setState(const HAnimationClip& clip, AnimationClipState state)
 	void Animation::setState(const HAnimationClip& clip, AnimationClipState state)
 	{
 	{
+		if (state.layer == 0)
+			state.layer = (UINT32)-1;
+		else
+			state.layer -= 1;
+
 		AnimationClipInfo* clipInfo = addClip(clip, state.layer, false);
 		AnimationClipInfo* clipInfo = addClip(clip, state.layer, false);
 
 
 		if (clipInfo == nullptr)
 		if (clipInfo == nullptr)
 			return;
 			return;
 
 
 		clipInfo->state = state;
 		clipInfo->state = state;
+		clipInfo->playbackType = AnimPlaybackType::Normal;
+
 		mDirty |= AnimDirtyStateFlag::Value;
 		mDirty |= AnimDirtyStateFlag::Value;
 	}
 	}
 
 
@@ -1162,6 +1205,13 @@ namespace BansheeEngine
 			}
 			}
 		}
 		}
 
 
+		// Disable sampled animations
+		for (auto& clipInfo : mClipInfos)
+		{
+			if (clipInfo.playbackType == AnimPlaybackType::Sampled)
+				clipInfo.playbackType = AnimPlaybackType::None;
+		}
+
 		mDirty = AnimDirtyState();
 		mDirty = AnimDirtyState();
 	}
 	}
 
 
@@ -1262,12 +1312,13 @@ namespace BansheeEngine
 			}
 			}
 			else
 			else
 			{
 			{
+				if (mAnimProxy->sceneObjectPose.hasOverride[i])
+					continue;
+
 				so->setPosition(mAnimProxy->sceneObjectPose.positions[i]);
 				so->setPosition(mAnimProxy->sceneObjectPose.positions[i]);
 				so->setRotation(mAnimProxy->sceneObjectPose.rotations[i]);
 				so->setRotation(mAnimProxy->sceneObjectPose.rotations[i]);
 				so->setScale(mAnimProxy->sceneObjectPose.scales[i]);
 				so->setScale(mAnimProxy->sceneObjectPose.scales[i]);
 			}
 			}
-
-			soInfo.hash = so->getTransformHash();
 		}
 		}
 
 
 		// Must ensure that clip in the proxy and current primary clip are the same
 		// Must ensure that clip in the proxy and current primary clip are the same

+ 5 - 0
Source/BansheeCore/Source/BsAnimationManager.cpp

@@ -182,6 +182,8 @@ namespace BansheeEngine
 			}
 			}
 
 
 			// Update mapped scene objects
 			// Update mapped scene objects
+			memset(anim->sceneObjectPose.hasOverride, 1, sizeof(bool) * anim->numSceneObjects);
+
 			for(UINT32 i = 0; i < anim->numSceneObjects; i++)
 			for(UINT32 i = 0; i < anim->numSceneObjects; i++)
 			{
 			{
 				const AnimatedSceneObjectInfo& soInfo = anim->sceneObjectInfos[i];
 				const AnimatedSceneObjectInfo& soInfo = anim->sceneObjectInfos[i];
@@ -203,6 +205,7 @@ namespace BansheeEngine
 					{
 					{
 						const TAnimationCurve<Vector3>& curve = state.curves->position[curveIdx].curve;
 						const TAnimationCurve<Vector3>& curve = state.curves->position[curveIdx].curve;
 						anim->sceneObjectPose.positions[curveIdx] = curve.evaluate(state.time, state.positionCaches[curveIdx], state.loop);
 						anim->sceneObjectPose.positions[curveIdx] = curve.evaluate(state.time, state.positionCaches[curveIdx], state.loop);
+						anim->sceneObjectPose.hasOverride[curveIdx] = false;
 					}
 					}
 				}
 				}
 
 
@@ -213,6 +216,7 @@ namespace BansheeEngine
 						const TAnimationCurve<Quaternion>& curve = state.curves->rotation[curveIdx].curve;
 						const TAnimationCurve<Quaternion>& curve = state.curves->rotation[curveIdx].curve;
 						anim->sceneObjectPose.rotations[curveIdx] = curve.evaluate(state.time, state.rotationCaches[curveIdx], state.loop);
 						anim->sceneObjectPose.rotations[curveIdx] = curve.evaluate(state.time, state.rotationCaches[curveIdx], state.loop);
 						anim->sceneObjectPose.rotations[curveIdx].normalize();
 						anim->sceneObjectPose.rotations[curveIdx].normalize();
+						anim->sceneObjectPose.hasOverride[curveIdx] = false;
 					}
 					}
 				}
 				}
 
 
@@ -222,6 +226,7 @@ namespace BansheeEngine
 					{
 					{
 						const TAnimationCurve<Vector3>& curve = state.curves->scale[curveIdx].curve;
 						const TAnimationCurve<Vector3>& curve = state.curves->scale[curveIdx].curve;
 						anim->sceneObjectPose.scales[curveIdx] = curve.evaluate(state.time, state.scaleCaches[curveIdx], state.loop);
 						anim->sceneObjectPose.scales[curveIdx] = curve.evaluate(state.time, state.scaleCaches[curveIdx], state.loop);
+						anim->sceneObjectPose.hasOverride[curveIdx] = false;
 					}
 					}
 				}
 				}
 			}
 			}

+ 3 - 0
Source/BansheeEngine/Include/BsCAnimation.h

@@ -59,6 +59,9 @@ namespace BansheeEngine
 		/** @copydoc Animation::crossFade */
 		/** @copydoc Animation::crossFade */
 		void crossFade(const HAnimationClip& clip, float fadeLength);
 		void crossFade(const HAnimationClip& clip, float fadeLength);
 
 
+		/** @copydoc Animation::sample */
+		void sample(const HAnimationClip& clip, float time);
+
 		/** @copydoc Animation::stop */
 		/** @copydoc Animation::stop */
 		void stop(UINT32 layer);
 		void stop(UINT32 layer);
 
 

+ 6 - 0
Source/BansheeEngine/Source/BsCAnimation.cpp

@@ -78,6 +78,12 @@ namespace BansheeEngine
 			mInternal->crossFade(clip, fadeLength);
 			mInternal->crossFade(clip, fadeLength);
 	}
 	}
 
 
+	void CAnimation::sample(const HAnimationClip& clip, float time)
+	{
+		if (mInternal != nullptr)
+			mInternal->sample(clip, time);
+	}
+
 	void CAnimation::stop(UINT32 layer)
 	void CAnimation::stop(UINT32 layer)
 	{
 	{
 		if (mInternal != nullptr)
 		if (mInternal != nullptr)

+ 29 - 10
Source/MBansheeEditor/Windows/AnimationWindow.cs

@@ -80,10 +80,18 @@ namespace BansheeEditor
             }
             }
             else if (state == State.Recording)
             else if (state == State.Recording)
             {
             {
-                float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
-                if(RecordState(time))
-                    guiCurveEditor.Redraw();
+                if (!delayRecord)
+                {
+                    float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
+                    if (RecordState(time))
+                    {
+                        ApplyClipChanges();
+                        guiCurveEditor.Redraw();
+                    }
+                }
             }
             }
+
+            delayRecord = false;
         }
         }
 
 
         private void OnDestroy()
         private void OnDestroy()
@@ -448,6 +456,8 @@ namespace BansheeEditor
                 SwitchState(State.Normal);
                 SwitchState(State.Normal);
 
 
                 ApplyClipChanges();
                 ApplyClipChanges();
+                PreviewFrame(currentFrameIdx);
+
                 EditorApplication.SetProjectDirty();
                 EditorApplication.SetProjectDirty();
             };
             };
             guiCurveEditor.OnClicked += () =>
             guiCurveEditor.OnClicked += () =>
@@ -824,6 +834,7 @@ namespace BansheeEditor
 
 
         private State state = State.Empty;
         private State state = State.Empty;
         private SerializedSceneObject soState;
         private SerializedSceneObject soState;
+        private bool delayRecord = false;
 
 
         /// <summary>
         /// <summary>
         /// Transitions the window into a different state. Caller must validate state transitions.
         /// Transitions the window into a different state. Caller must validate state transitions.
@@ -963,8 +974,11 @@ namespace BansheeEditor
         private void StartRecord()
         private void StartRecord()
         {
         {
             float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
             float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
-            if(RecordState(time))
+            if (RecordState(time))
+            {
+                ApplyClipChanges();
                 guiCurveEditor.Redraw();
                 guiCurveEditor.Redraw();
+            }
 
 
             recordButton.Value = true;
             recordButton.Value = true;
         }
         }
@@ -1024,7 +1038,7 @@ namespace BansheeEditor
                             for (int i = 0; i < 2; i++)
                             for (int i = 0; i < 2; i++)
                             {
                             {
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
-                                if (!MathEx.ApproxEquals(value[i], curveVal))
+                                if (!MathEx.ApproxEquals(value[i], curveVal, 0.001f))
                                 {
                                 {
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     changesMade = true;
                                     changesMade = true;
@@ -1039,7 +1053,7 @@ namespace BansheeEditor
                             for (int i = 0; i < 3; i++)
                             for (int i = 0; i < 3; i++)
                             {
                             {
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
-                                if (!MathEx.ApproxEquals(value[i], curveVal))
+                                if (!MathEx.ApproxEquals(value[i], curveVal, 0.001f))
                                 { 
                                 { 
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     changesMade = true;
                                     changesMade = true;
@@ -1056,7 +1070,7 @@ namespace BansheeEditor
                                 for (int i = 0; i < 4; i++)
                                 for (int i = 0; i < 4; i++)
                                 {
                                 {
                                     float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
                                     float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
-                                    if (!MathEx.ApproxEquals(value[i], curveVal))
+                                    if (!MathEx.ApproxEquals(value[i], curveVal, 0.001f))
                                     { 
                                     { 
                                         addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                         addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                         changesMade = true;
                                         changesMade = true;
@@ -1070,7 +1084,7 @@ namespace BansheeEditor
                                 for (int i = 0; i < 4; i++)
                                 for (int i = 0; i < 4; i++)
                                 {
                                 {
                                     float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
                                     float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
-                                    if (!MathEx.ApproxEquals(value[i], curveVal))
+                                    if (!MathEx.ApproxEquals(value[i], curveVal, 0.001f))
                                     { 
                                     { 
                                         addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                         addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                         changesMade = true;
                                         changesMade = true;
@@ -1086,7 +1100,7 @@ namespace BansheeEditor
                             for (int i = 0; i < 4; i++)
                             for (int i = 0; i < 4; i++)
                             {
                             {
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
                                 float curveVal = KVP.Value.curveInfos[i].curve.Evaluate(time);
-                                if (!MathEx.ApproxEquals(value[i], curveVal))
+                                if (!MathEx.ApproxEquals(value[i], curveVal, 0.001f))
                                 { 
                                 { 
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     addOrUpdateKeyframe(KVP.Value.curveInfos[i].curve, time, value[i]);
                                     changesMade = true;
                                     changesMade = true;
@@ -1123,7 +1137,7 @@ namespace BansheeEditor
                             float value = property.GetValue<float>();
                             float value = property.GetValue<float>();
 
 
                             float curveVal = KVP.Value.curveInfos[0].curve.Evaluate(time);
                             float curveVal = KVP.Value.curveInfos[0].curve.Evaluate(time);
-                            if (!MathEx.ApproxEquals(value, curveVal))
+                            if (!MathEx.ApproxEquals(value, curveVal, 0.001f))
                             { 
                             { 
                                 addOrUpdateKeyframe(KVP.Value.curveInfos[0].curve, time, value);
                                 addOrUpdateKeyframe(KVP.Value.curveInfos[0].curve, time, value);
                                 changesMade = true;
                                 changesMade = true;
@@ -1766,6 +1780,11 @@ namespace BansheeEditor
         {
         {
             SetCurrentFrame(frameIdx);
             SetCurrentFrame(frameIdx);
             PreviewFrame(currentFrameIdx);
             PreviewFrame(currentFrameIdx);
+
+            // HACK: Skip checking for record changes this frame, to give the preview a chance to update, otherwise
+            // the changes would be detected any time a frame is delayed. A proper fix for this would be to force the
+            // animation to be evaluated synchronously when PreviewFrame is called.
+            delayRecord = true;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 25 - 4
Source/MBansheeEngine/Animation/Animation.cs

@@ -296,6 +296,22 @@ namespace BansheeEngine
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Samples an animation clip at the specified time, displaying only that particular frame without further playback.
+        /// </summary>
+        /// <param name="clip">Animation clip to sample.</param>
+        /// <param name="time">Time to sample the clip at.</param>
+        public void Sample(AnimationClip clip, float time)
+        {
+            switch (state)
+            {
+                case State.Active:
+                case State.EditorActive:
+                    _native.Sample(clip, time);
+                    break;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Stops playing all animations on the provided layer.
         /// Stops playing all animations on the provided layer.
         /// </summary>
         /// </summary>
@@ -387,11 +403,16 @@ namespace BansheeEngine
             switch (state)
             switch (state)
             {
             {
                 case State.EditorActive:
                 case State.EditorActive:
-                    AnimationClipState clipState = AnimationClipState.Create();
-                    clipState.time = startTime;
-                    clipState.speed = freeze ? 0.0f : 1.0f;
+                    if (freeze)
+                        Sample(clip, startTime);
+                    else
+                    {
+                        AnimationClipState clipState = AnimationClipState.Create();
+                        clipState.time = startTime;
+
+                        SetState(clip, clipState);
+                    }
 
 
-                    SetState(clip, clipState);
                     RefreshClipMappings();
                     RefreshClipMappings();
 
 
                     break;
                     break;

+ 12 - 0
Source/MBansheeEngine/Animation/Interop/NativeAnimation.cs

@@ -83,6 +83,15 @@ namespace BansheeEngine
             Internal_CrossFade(mCachedPtr, clipPtr, fadeLength);
             Internal_CrossFade(mCachedPtr, clipPtr, fadeLength);
         }
         }
 
 
+        public void Sample(AnimationClip clip, float time)
+        {
+            IntPtr clipPtr = IntPtr.Zero;
+            if (clip != null)
+                clipPtr = clip.GetCachedPtr();
+
+            Internal_Sample(mCachedPtr, clipPtr, time);
+        }
+
         public void Stop(int layer)
         public void Stop(int layer)
         {
         {
             Internal_Stop(mCachedPtr, layer);
             Internal_Stop(mCachedPtr, layer);
@@ -185,6 +194,9 @@ namespace BansheeEngine
         [MethodImpl(MethodImplOptions.InternalCall)]
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern void Internal_CrossFade(IntPtr thisPtr, IntPtr clipPtr, float fadeLength);
         private static extern void Internal_CrossFade(IntPtr thisPtr, IntPtr clipPtr, float fadeLength);
 
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void Internal_Sample(IntPtr thisPtr, IntPtr clipPtr, float time);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern void Internal_Stop(IntPtr thisPtr, int layer);
         private static extern void Internal_Stop(IntPtr thisPtr, int layer);
 
 

+ 1 - 0
Source/SBansheeEngine/Include/BsScriptAnimation.h

@@ -46,6 +46,7 @@ namespace BansheeEngine
 		static void internal_Blend1D(ScriptAnimation* thisPtr, MonoObject* info, float t);
 		static void internal_Blend1D(ScriptAnimation* thisPtr, MonoObject* info, float t);
 		static void internal_Blend2D(ScriptAnimation* thisPtr, MonoObject* info, Vector2* t);
 		static void internal_Blend2D(ScriptAnimation* thisPtr, MonoObject* info, Vector2* t);
 		static void internal_CrossFade(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, float fadeLength);
 		static void internal_CrossFade(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, float fadeLength);
+		static void internal_Sample(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, float time);
 
 
 		static void internal_Stop(ScriptAnimation* thisPtr, UINT32 layer);
 		static void internal_Stop(ScriptAnimation* thisPtr, UINT32 layer);
 		static void internal_StopAll(ScriptAnimation* thisPtr);
 		static void internal_StopAll(ScriptAnimation* thisPtr);

+ 10 - 0
Source/SBansheeEngine/Source/BsScriptAnimation.cpp

@@ -40,6 +40,7 @@ namespace BansheeEngine
 		metaData.scriptClass->addInternalCall("Internal_Blend1D", &ScriptAnimation::internal_Blend1D);
 		metaData.scriptClass->addInternalCall("Internal_Blend1D", &ScriptAnimation::internal_Blend1D);
 		metaData.scriptClass->addInternalCall("Internal_Blend2D", &ScriptAnimation::internal_Blend2D);
 		metaData.scriptClass->addInternalCall("Internal_Blend2D", &ScriptAnimation::internal_Blend2D);
 		metaData.scriptClass->addInternalCall("Internal_CrossFade", &ScriptAnimation::internal_CrossFade);
 		metaData.scriptClass->addInternalCall("Internal_CrossFade", &ScriptAnimation::internal_CrossFade);
+		metaData.scriptClass->addInternalCall("Internal_Sample", &ScriptAnimation::internal_Sample);
 
 
 		metaData.scriptClass->addInternalCall("Internal_Stop", &ScriptAnimation::internal_Stop);
 		metaData.scriptClass->addInternalCall("Internal_Stop", &ScriptAnimation::internal_Stop);
 		metaData.scriptClass->addInternalCall("Internal_StopAll", &ScriptAnimation::internal_StopAll);
 		metaData.scriptClass->addInternalCall("Internal_StopAll", &ScriptAnimation::internal_StopAll);
@@ -132,6 +133,15 @@ namespace BansheeEngine
 		thisPtr->getInternal()->crossFade(nativeClip, fadeLength);
 		thisPtr->getInternal()->crossFade(nativeClip, fadeLength);
 	}
 	}
 
 
+	void ScriptAnimation::internal_Sample(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, float time)
+	{
+		HAnimationClip nativeClip;
+		if (clip != nullptr)
+			nativeClip = clip->getHandle();
+
+		thisPtr->getInternal()->sample(nativeClip, time);
+	}
+
 	void ScriptAnimation::internal_Stop(ScriptAnimation* thisPtr, UINT32 layer)
 	void ScriptAnimation::internal_Stop(ScriptAnimation* thisPtr, UINT32 layer)
 	{
 	{
 		thisPtr->getInternal()->stop(layer);
 		thisPtr->getInternal()->stop(layer);