Przeglądaj źródła

Conversion of euler angle curves to quaternion curves, and vice versa
Refactored FBXImporter so it uses the new animation curve class

BearishSun 9 lat temu
rodzic
commit
9eec1ad285

+ 8 - 2
Source/BansheeCore/Include/BsAnimationUtility.h

@@ -3,7 +3,7 @@
 #pragma once
 
 #include "BsCorePrerequisites.h"
-#include "BsCoreObject.h"
+#include "BsAnimationCurve.h"
 
 namespace BansheeEngine
 {
@@ -12,7 +12,7 @@ namespace BansheeEngine
 	 */
 
 	/** Helper class for dealing with animations, animation clips and curves. */
-	class BS_CORE_EXPORT AnimationUtility : public CoreObject
+	class BS_CORE_EXPORT AnimationUtility
 	{
 	public:
 		/**
@@ -24,6 +24,12 @@ namespace BansheeEngine
 		 * @param[in]		loop	If true the value will be wrapped, otherwise clamped to range.
 		 */
 		static void wrapTime(float& time, float start, float end, bool loop);
+
+		/** Converts a curve in euler angles (in degrees) into a curve using quaternions. */
+		static TAnimationCurve<Quaternion> eulerToQuaternionCurve(const TAnimationCurve<Vector3>& eulerCurve);
+
+		/** Converts a curve in quaternions into a curve using euler angles (in degrees). */
+		static TAnimationCurve<Vector3> quaternionToEulerCurve(const TAnimationCurve<Quaternion>& quatCurve);
 	};
 
 	/** @} */

+ 133 - 0
Source/BansheeCore/Source/BsAnimationUtility.cpp

@@ -1,6 +1,8 @@
 //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
 //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
 #include "BsAnimationUtility.h"
+#include "BsVector3.h"
+#include "BsQuaternion.h"
 
 namespace BansheeEngine
 {
@@ -26,4 +28,135 @@ namespace BansheeEngine
 				time = end;
 		}
 	}
+
+	TAnimationCurve<Quaternion> AnimationUtility::eulerToQuaternionCurve(const TAnimationCurve<Vector3>& eulerCurve)
+	{
+		// TODO: We calculate tangents by sampling. There must be an analytical way to calculate tangents when converting
+		// a curve.
+		const float FIT_TIME = 0.001f;
+
+		auto eulerToQuaternion = [&](INT32 keyIdx, float time, const Quaternion& lastQuat)
+		{
+			Vector3 angles = eulerCurve.evaluate(time, false);
+			Quaternion quat(
+				Degree(angles.x),
+				Degree(angles.y),
+				Degree(angles.z));
+
+			// Flip quaternion in case rotation is over 180 degrees (use shortest path)
+			if (keyIdx > 0)
+			{
+				float dot = quat.dot(lastQuat);
+				if (dot < 0.0f)
+					quat = -quat;
+			}
+
+			return quat;
+		};
+
+		INT32 numKeys = (INT32)eulerCurve.getNumKeyFrames();
+		Vector<TKeyframe<Quaternion>> quatKeyframes(numKeys);
+
+		// Calculate key values
+		Quaternion lastQuat;
+		for (INT32 i = 0; i < numKeys; i++)
+		{
+			float time = eulerCurve.getKeyFrame(i).time;
+			Quaternion quat = eulerToQuaternion(i, time, lastQuat);
+
+			quatKeyframes[i].time = time;
+			quatKeyframes[i].value = quat;
+			quatKeyframes[i].inTangent = Quaternion::ZERO;
+			quatKeyframes[i].outTangent = Quaternion::ZERO;
+
+			lastQuat = quat;
+		}
+
+		// Calculate extra values between keys so we can approximate tangents. If we're sampling very close to the key
+		// the values should pretty much exactly match the tangent (assuming the curves are cubic hermite)
+		for (INT32 i = 0; i < numKeys - 1; i++)
+		{
+			TKeyframe<Quaternion>& currentKey = quatKeyframes[i];
+			TKeyframe<Quaternion>& nextKey = quatKeyframes[i + 1];
+
+			float dt = nextKey.time - currentKey.time;
+			float startFitTime = currentKey.time + dt * FIT_TIME;
+			float endFitTime = currentKey.time + dt * (1.0f - FIT_TIME);
+
+			Quaternion startFitValue = eulerToQuaternion(i, startFitTime, currentKey.value);
+			Quaternion endFitValue = eulerToQuaternion(i, endFitTime, startFitValue);
+
+			float invFitTime = 1.0f / (dt * FIT_TIME);
+			currentKey.outTangent = (startFitValue - currentKey.value) * invFitTime;
+			nextKey.inTangent = (nextKey.value - endFitValue) * invFitTime;
+		}
+
+		return TAnimationCurve<Quaternion>(quatKeyframes);
+	}
+
+	TAnimationCurve<Vector3> AnimationUtility::quaternionToEulerCurve(const TAnimationCurve<Quaternion>& quatCurve)
+	{
+		// TODO: We calculate tangents by sampling. There must be an analytical way to calculate tangents when converting
+		// a curve.
+		const float FIT_TIME = 0.001f;
+
+		auto quaternionToEuler = [&](float time)
+		{
+			Quaternion quat = quatCurve.evaluate(time, false);
+
+			Radian x, y, z;
+			quat.toEulerAngles(x, y, z);
+
+			Vector3 euler(
+				x.valueDegrees(),
+				y.valueDegrees(),
+				z.valueDegrees()
+			);
+
+			return euler;
+		};
+
+		INT32 numKeys = (INT32)quatCurve.getNumKeyFrames();
+		Vector<TKeyframe<Vector3>> eulerKeyframes(numKeys);
+
+		// Calculate key values
+		for (INT32 i = 0; i < numKeys; i++)
+		{
+			float time = quatCurve.getKeyFrame(i).time;
+			Vector3 euler = quaternionToEuler(time);
+
+			eulerKeyframes[i].time = time;
+			eulerKeyframes[i].value = euler;
+			eulerKeyframes[i].inTangent = Vector3::ZERO;
+			eulerKeyframes[i].outTangent = Vector3::ZERO;
+		}
+
+		// Calculate extra values between keys so we can approximate tangents. If we're sampling very close to the key
+		// the values should pretty much exactly match the tangent (assuming the curves are cubic hermite)
+		for (INT32 i = 0; i < numKeys - 1; i++)
+		{
+			TKeyframe<Vector3>& currentKey = eulerKeyframes[i];
+			TKeyframe<Vector3>& nextKey = eulerKeyframes[i + 1];
+
+			float dt = nextKey.time - currentKey.time;
+			float startFitTime = currentKey.time + dt * FIT_TIME;
+			float endFitTime = currentKey.time + dt * (1.0f - FIT_TIME);
+
+			Vector3 startFitValue = quaternionToEuler(startFitTime);
+			Vector3 endFitValue = quaternionToEuler(endFitTime);
+
+			// If fit values rotate for more than 180 degrees, wrap them so they use the shortest path
+			for(int j = 0; j < 3; j++)
+			{
+				startFitValue[j] = fmod(startFitValue[j] - currentKey.value[j] + 180.0f, 360.0f) + currentKey.value[j] - 180.0f;
+				endFitValue[j] = nextKey.value[j] + fmod(nextKey.value[j] - endFitValue[j] + 180.0f, 360.0f) - 180.0f;
+			}
+			
+			float invFitTime = 1.0f / (dt * FIT_TIME);
+			currentKey.outTangent = (startFitValue - currentKey.value) * invFitTime;
+			nextKey.inTangent = (nextKey.value - endFitValue) * invFitTime;
+		}
+
+		return TAnimationCurve<Vector3>(eulerKeyframes);
+	}
 }

+ 4 - 4
Source/BansheeEditor/Source/BsGUISceneTreeView.cpp

@@ -94,10 +94,10 @@ namespace BansheeEngine
 
 			HSceneObject currentSOChild = currentSO->getChild(i);
 
-#if BS_DEBUG_MODE == 0
+//#if BS_DEBUG_MODE == 0
 			if (currentSOChild->hasFlag(SOF_Internal))
 				continue;
-#endif
+//#endif
 
 			SceneTreeElement* currentChild = static_cast<SceneTreeElement*>(element->mChildren[visibleChildCount]);
 			visibleChildCount++;
@@ -132,10 +132,10 @@ namespace BansheeEngine
 				// Only count it as a prefab instance if its not scene root (otherwise every object would be colored as a prefab)
 				bool isPrefabInstance = prefabParent != nullptr && prefabParent->getParent() != nullptr;
 
-#if BS_DEBUG_MODE == 0
+//#if BS_DEBUG_MODE == 0
 				if (isInternal)
 					continue;
-#endif
+//#endif
 
 				UINT64 curId = currentSOChild->getInstanceId();
 				bool found = false;

+ 6 - 24
Source/BansheeFBXImporter/Include/BsFBXImportData.h

@@ -7,6 +7,8 @@
 #include "BsColor.h"
 #include "BsVector2.h"
 #include "BsVector4.h"
+#include "BsQuaternion.h"
+#include "BsAnimationCurve.h"
 #include "BsSubMesh.h"
 
 namespace BansheeEngine
@@ -83,41 +85,21 @@ namespace BansheeEngine
 		INT32 indices[FBX_IMPORT_MAX_BONE_INFLUENCES];
 	};
 
-	/**
-	 * Represents a single frame in an animation curve. Contains a value at a specific time as well as the in and out 
-	 * tangents at that position.
-	 */
-	struct FBXKeyFrame
-	{
-		float time;
-		float value;
-		float inTangent;
-		float outTangent;
-	};
-
-	/**	Curve with a set of key frames used for animation of a single value. */
-	struct FBXAnimationCurve
-	{
-		Vector<FBXKeyFrame> keyframes;
-
-		float evaluate(float time);
-	};
-
 	/**	Animation curves required to animate a single bone. */
 	struct FBXBoneAnimation
 	{
 		FBXImportNode* node;
 
-		FBXAnimationCurve translation[3];
-		FBXAnimationCurve rotation[4];
-		FBXAnimationCurve scale[3];
+		TAnimationCurve<Vector3> translation;
+		TAnimationCurve<Quaternion> rotation;
+		TAnimationCurve<Vector3> scale;
 	};
 
 	/**	Animation curve required to animate a blend shape. */
 	struct FBXBlendShapeAnimation
 	{
 		String blendShape;
-		FBXAnimationCurve curve;
+		TAnimationCurve<float> curve;
 	};
 
 	/** Animation clip containing a set of bone or blend shape animations. */

+ 3 - 5
Source/BansheeFBXImporter/Include/BsFBXImporter.h

@@ -98,7 +98,8 @@ namespace BansheeEngine
 			FBXAnimationClip& clip, FBXImportScene& importScene);
 
 		/**	Converts a single FBX animation curve into an engine curve format, resampling it if necessary. */
-		void importCurve(FbxAnimCurve* fbxCurve, FBXImportOptions& importOptions, FBXAnimationCurve& curve, float start, float end);
+		template<class T, int C>
+		TAnimationCurve<T> importCurve(FbxAnimCurve*(&fbxCurve)[C], FBXImportOptions& importOptions, float start, float end);
 
 		/** Converts FBX animation clips into engine-ready animation curve format. */
 		void convertAnimations(const Vector<FBXAnimationClip>& clips, const Vector<AnimationSplitInfo>& splits, 
@@ -108,10 +109,7 @@ namespace BansheeEngine
 		 * Removes identical sequential keyframes for the provided set of curves. The keyframe must be identical over all
 		 * the curves in order for it to be removed.
 		 */
-		void reduceKeyframes(FBXAnimationCurve(&curves)[3]);
-
-		/**	Converts a set of curves containing rotation in euler angles into a set of curves using	quaternion rotation. */
-		void eulerToQuaternionCurves(FBXAnimationCurve(&eulerCurves)[3], FBXAnimationCurve(&quatCurves)[4]);
+		TAnimationCurve<Vector3> reduceKeyframes(TAnimationCurve<Vector3>& curve);
 
 		/**
 		 * Converts all the meshes from per-index attributes to per-vertex attributes.

+ 0 - 41
Source/BansheeFBXImporter/Source/BsFBXImportData.cpp

@@ -22,45 +22,4 @@ namespace BansheeEngine
 		for (auto& mesh : meshes)
 			bs_delete(mesh);
 	}
-
-	float FBXAnimationCurve::evaluate(float time)
-	{
-		INT32 keyframeCount = (INT32)keyframes.size();
-		if (keyframeCount == 0)
-			return 0.0f;
-
-		INT32 keyframeIdx = -1;
-		for (INT32 i = 0; i < keyframeCount; i++)
-		{
-			if (time <= keyframes[i].time)
-			{
-				keyframeIdx = i;
-				break;
-			}
-		}
-
-		if (keyframeIdx == -1)
-			keyframeIdx = keyframeCount - 1;
-
-		if (keyframeIdx == 0)
-			return keyframes[0].value;
-
-		FBXKeyFrame& kfPrev = keyframes[keyframeIdx - 1];
-		FBXKeyFrame& kfCur = keyframes[keyframeIdx];
-
-		float delta = kfCur.time - kfPrev.time;
-		float offset = time - kfPrev.time;
-		float t = offset / delta;
-
-		// Evaluate Cubic Hermite spline
-		float t2 = t * t;
-		float t3 = t2 * t;
-
-		float x0 = (2 * t3 - 3 * t2 + 1) * kfPrev.value;
-		float x1 = (t3 - 2 * t2 + t) * kfPrev.outTangent;
-		float x2 = (-2 * t3 + 3 * t2) * kfCur.value;
-		float x3 = (t3 - t2) * kfCur.inTangent;
-
-		return x0 + x1 + x2 + x3;
-	}
 }

+ 119 - 293
Source/BansheeFBXImporter/Source/BsFBXImporter.cpp

@@ -10,7 +10,6 @@
 #include "BsVector2.h"
 #include "BsVector3.h"
 #include "BsVector4.h"
-#include "BsQuaternion.h"
 #include "BsVertexDataDesc.h"
 #include "BsFBXUtility.h"
 #include "BsMeshUtility.h"
@@ -19,6 +18,7 @@
 #include "BsPhysicsMesh.h"
 #include "BsAnimationCurve.h"
 #include "BsAnimationClip.h"
+#include "BsAnimationUtility.h"
 #include "BsSkeleton.h"
 #include "BsPhysics.h"
 
@@ -444,79 +444,9 @@ namespace BansheeEngine
 			
 			for (auto& bone : clip.boneAnimations)
 			{
-				// Translation curves
-				{
-					assert((bone.translation[0].keyframes.size() == bone.translation[1].keyframes.size()) &&
-						(bone.translation[0].keyframes.size() == bone.translation[2].keyframes.size()));
-
-					UINT32 numKeyframes = (UINT32)bone.translation[0].keyframes.size();
-					Vector <TKeyframe<Vector3>> keyFrames(numKeyframes);
-					for (UINT32 i = 0; i < numKeyframes; i++)
-					{
-						const FBXKeyFrame& keyFrameX = bone.translation[0].keyframes[i];
-						const FBXKeyFrame& keyFrameY = bone.translation[1].keyframes[i];
-						const FBXKeyFrame& keyFrameZ = bone.translation[2].keyframes[i];
-
-						keyFrames[i].value = Vector3(keyFrameX.value, keyFrameY.value, keyFrameZ.value);
-
-						assert((keyFrameX.time == keyFrameY.time) && (keyFrameX.time == keyFrameZ.time));
-						keyFrames[i].time = keyFrameX.time;
-						keyFrames[i].inTangent = Vector3(keyFrameX.inTangent, keyFrameY.inTangent, keyFrameZ.inTangent);
-						keyFrames[i].outTangent = Vector3(keyFrameX.outTangent, keyFrameY.outTangent, keyFrameZ.outTangent);
-					}
-
-					curves->position.push_back({ bone.node->name, keyFrames });
-				}
-
-				// Rotation curves
-				{
-					assert((bone.rotation[0].keyframes.size() == bone.rotation[1].keyframes.size()) &&
-						(bone.rotation[0].keyframes.size() == bone.rotation[2].keyframes.size()) &&
-						(bone.rotation[0].keyframes.size() == bone.rotation[3].keyframes.size()));
-
-					UINT32 numKeyframes = (UINT32)bone.rotation[0].keyframes.size();
-					Vector <TKeyframe<Quaternion>> keyFrames(numKeyframes);
-					for (UINT32 i = 0; i < numKeyframes; i++)
-					{
-						const FBXKeyFrame& keyFrameX = bone.rotation[0].keyframes[i];
-						const FBXKeyFrame& keyFrameY = bone.rotation[1].keyframes[i];
-						const FBXKeyFrame& keyFrameZ = bone.rotation[2].keyframes[i];
-						const FBXKeyFrame& keyFrameW = bone.rotation[3].keyframes[i];
-
-						keyFrames[i].value = Quaternion(keyFrameW.value, keyFrameX.value, keyFrameY.value, keyFrameZ.value);
-
-						assert((keyFrameX.time == keyFrameY.time) && (keyFrameX.time == keyFrameZ.time) && (keyFrameX.time == keyFrameW.time));
-						keyFrames[i].time = keyFrameX.time;
-						keyFrames[i].inTangent = Quaternion(keyFrameW.inTangent, keyFrameX.inTangent, keyFrameY.inTangent, keyFrameZ.inTangent);
-						keyFrames[i].outTangent = Quaternion(keyFrameW.outTangent, keyFrameX.outTangent, keyFrameY.outTangent, keyFrameZ.outTangent);
-					}
-
-					curves->rotation.push_back({ bone.node->name, keyFrames });
-				}
-
-				// Scale curves
-				{
-					assert((bone.scale[0].keyframes.size() == bone.scale[1].keyframes.size()) &&
-						(bone.scale[0].keyframes.size() == bone.scale[2].keyframes.size()));
-
-					UINT32 numKeyframes = (UINT32)bone.scale[0].keyframes.size();
-					Vector <TKeyframe<Vector3>> keyFrames(numKeyframes);
-					for (UINT32 i = 0; i < numKeyframes; i++)
-					{
-						const FBXKeyFrame& keyFrameX = bone.scale[0].keyframes[i];
-						const FBXKeyFrame& keyFrameY = bone.scale[1].keyframes[i];
-						const FBXKeyFrame& keyFrameZ = bone.scale[2].keyframes[i];
-
-						keyFrames[i].value = Vector3(keyFrameX.value, keyFrameY.value, keyFrameZ.value);
-
-						assert((keyFrameX.time == keyFrameY.time) && (keyFrameX.time == keyFrameZ.time));
-						keyFrames[i].time = keyFrameX.time;
-						keyFrames[i].inTangent = Vector3(keyFrameX.inTangent, keyFrameY.inTangent, keyFrameZ.inTangent);
-						keyFrames[i].outTangent = Vector3(keyFrameX.outTangent, keyFrameY.outTangent, keyFrameZ.outTangent);
-					}
-
-					curves->scale.push_back({ bone.node->name, keyFrames });
-				}
+				curves->position.push_back({ bone.node->name, bone.translation });
+				curves->rotation.push_back({ bone.node->name, bone.rotation });
+				curves->scale.push_back({ bone.node->name, bone.scale });
 			}
 
 			// See if any splits are required. We only split the first clip as it is assumed if FBX has multiple clips the
@@ -1562,27 +1492,18 @@ namespace BansheeEngine
 			FBXBoneAnimation& boneAnim = clip.boneAnimations.back();
 			boneAnim.node = importScene.nodeMap[node];
 
-			importCurve(translation[0], importOptions, boneAnim.translation[0], clip.start, clip.end);
-			importCurve(translation[1], importOptions, boneAnim.translation[1], clip.start, clip.end);
-			importCurve(translation[2], importOptions, boneAnim.translation[2], clip.start, clip.end);
-
-			importCurve(scale[0], importOptions, boneAnim.scale[0], clip.start, clip.end);
-			importCurve(scale[1], importOptions, boneAnim.scale[1], clip.start, clip.end);
-			importCurve(scale[2], importOptions, boneAnim.scale[2], clip.start, clip.end);
-
-			FBXAnimationCurve tempCurveRotation[3];
-			importCurve(rotation[0], importOptions, tempCurveRotation[0], clip.start, clip.end);
-			importCurve(rotation[1], importOptions, tempCurveRotation[1], clip.start, clip.end);
-			importCurve(rotation[2], importOptions, tempCurveRotation[2], clip.start, clip.end);
+			boneAnim.translation = importCurve<Vector3, 3>(translation, importOptions, clip.start, clip.end);
+			boneAnim.scale = importCurve<Vector3, 3>(scale, importOptions, clip.start, clip.end);
 
+			TAnimationCurve<Vector3> eulerAnimation = importCurve<Vector3, 3>(rotation, importOptions, clip.start, clip.end);
 			if(importOptions.reduceKeyframes)
 			{
-				reduceKeyframes(boneAnim.translation);
-				reduceKeyframes(boneAnim.scale);
-				reduceKeyframes(tempCurveRotation);
+				boneAnim.translation = reduceKeyframes(boneAnim.translation);
+				boneAnim.scale = reduceKeyframes(boneAnim.scale);
+				eulerAnimation = reduceKeyframes(eulerAnimation);
 			}
 
-			eulerToQuaternionCurves(tempCurveRotation, boneAnim.rotation);
+			boneAnim.rotation = AnimationUtility::eulerToQuaternionCurve(eulerAnimation);
 		}
 
 		if (importOptions.importBlendShapes)
@@ -1607,7 +1528,8 @@ namespace BansheeEngine
 							FBXBlendShapeAnimation& blendShapeAnim = clip.blendShapeAnimations.back();
 							blendShapeAnim.blendShape = channel->GetName();
 
-							importCurve(curve, importOptions, blendShapeAnim.curve, clip.start, clip.end);
+							FbxAnimCurve* curves[1] = { curve };
+							blendShapeAnim.curve = importCurve<float, 1>(curves, importOptions, clip.start, clip.end);
 						}
 					}
 				}
@@ -1622,268 +1544,172 @@ namespace BansheeEngine
 		}
 	}
 
-	void FBXImporter::reduceKeyframes(FBXAnimationCurve(&curves)[3])
+	TAnimationCurve<Vector3> FBXImporter::reduceKeyframes(TAnimationCurve<Vector3>& curve)
 	{
-		UINT32 keyCount = (UINT32)curves[0].keyframes.size();
-
-		assert((keyCount == (UINT32)curves[1].keyframes.size()) &&
-			(keyCount == (UINT32)curves[2].keyframes.size()));
+		UINT32 keyCount = curve.getNumKeyFrames();
 
-		Vector<FBXKeyFrame> newKeyframes[3];
+		Vector<TKeyframe<Vector3>> newKeyframes;
 
 		bool lastWasEqual = false;
 		for (UINT32 i = 0; i < keyCount; i++)
 		{
 			bool isEqual = true;
 
+			const TKeyframe<Vector3>& curKey = curve.getKeyFrame(i);
 			if (i > 0)
 			{
-				for (int j = 0; j < 3; j++)
-				{
-					FBXKeyFrame& curKey = curves[j].keyframes[i];
-					FBXKeyFrame& prevKey = newKeyframes[j].back();
+				TKeyframe<Vector3>& prevKey = newKeyframes.back();
 
-					isEqual = Math::approxEquals(prevKey.value, curKey.value) &&
-						Math::approxEquals(prevKey.outTangent, curKey.inTangent) && isEqual;
-				}
+				isEqual = Math::approxEquals(prevKey.value, curKey.value) &&
+					Math::approxEquals(prevKey.outTangent, curKey.inTangent) && isEqual;
 			}
 			else
 				isEqual = false;
 
-			for (int j = 0; j < 3; j++)
+			// More than two keys in a row are equal, remove previous key by replacing it with this one
+			if (lastWasEqual && isEqual)
 			{
-				FBXKeyFrame& curKey = curves[j].keyframes[i];
-
-				// More than two keys in a row are equal, remove previous key by replacing it with this one
-				if (lastWasEqual && isEqual)
-				{
-					FBXKeyFrame& prevKey = newKeyframes[j].back();
+				TKeyframe<Vector3>& prevKey = newKeyframes.back();
 
-					// Other properties are guaranteed unchanged
-					prevKey.time = curKey.time;
-					prevKey.outTangent = curKey.outTangent;
+				// Other properties are guaranteed unchanged
+				prevKey.time = curKey.time;
+				prevKey.outTangent = curKey.outTangent;
 
-					continue;
-				}
-
-				newKeyframes[j].push_back(curKey);
+				continue;
 			}
 
+			newKeyframes.push_back(curKey);
 			lastWasEqual = isEqual;
 		}
 
-		for (int j = 0; j < 3; j++)
-		{
-			curves[j].keyframes.clear();
-			std::swap(curves[j].keyframes, newKeyframes[j]);
-		}
+		return TAnimationCurve<Vector3>(newKeyframes);
 	}
-
-	void FBXImporter::eulerToQuaternionCurves(FBXAnimationCurve(&eulerCurves)[3], FBXAnimationCurve(&quatCurves)[4])
+	
+	template<class T>
+	void setKeyframeValues(TKeyframe<T>& keyFrame, int idx, float value, float inTangent, float outTangent)
 	{
-		const float FIT_TIME = 0.33f;
-
-		INT32 numKeys = (INT32)eulerCurves[0].keyframes.size();
-
-		if (numKeys != (INT32)eulerCurves[1].keyframes.size() || numKeys != (INT32)eulerCurves[2].keyframes.size())
-			return;
-
-		auto eulerToQuaternion = [&](INT32 keyIdx, float time, const Quaternion& lastQuat)
-		{
-			Degree x = (Degree)eulerCurves[0].evaluate(time);
-			Degree y = (Degree)eulerCurves[1].evaluate(time);
-			Degree z = (Degree)eulerCurves[2].evaluate(time);
-
-			Quaternion quat(x, y, z);
-
-			// Flip quaternion in case rotation is over 180 degrees
-			if (keyIdx > 0)
-			{
-				float dot = quat.dot(lastQuat);
-				if (dot < 0.0f)
-					quat = -quat;
-			}
-
-			return quat;
-		};
+		keyFrame.value = value;
+		keyFrame.inTangent = inTangent;
+		keyFrame.outTangent = outTangent;
+	}
 
-		struct FitKeyframe
-		{
-			float time;
-			Quaternion value;
-		};
+	template<>
+	void setKeyframeValues<Vector3>(TKeyframe<Vector3>& keyFrame, int idx, float value, float inTangent, float outTangent)
+	{
+		keyFrame.value[idx] = value;
+		keyFrame.inTangent[idx] = inTangent;
+		keyFrame.outTangent[idx] = outTangent;
+	}
 
-		Vector<FitKeyframe> fitQuaternions(numKeys * 2);
+	template<class T, int C>
+	TAnimationCurve<T> FBXImporter::importCurve(FbxAnimCurve*(&fbxCurve)[C], FBXImportOptions& importOptions,
+		float start, float end)
+	{
+		// If curve key-counts don't match, we need to force resampling 
+		bool forceResample = fbxCurve[0]->KeyGetCount() != fbxCurve[1]->KeyGetCount() ||
+			fbxCurve[0]->KeyGetCount() != fbxCurve[2]->KeyGetCount();
 
-		Quaternion lastQuat;
-		for (INT32 i = 0; i < numKeys; i++)
+		// Read keys directly
+		if(!importOptions.animResample && !forceResample)
 		{
-			float time = eulerCurves[0].keyframes[i].time;
-			Quaternion quat = eulerToQuaternion(i, time, lastQuat);
-
-			// Calculate extra values between keys so we can better approximate tangents
-			if ((i + 1) < numKeys)
-			{
-				float nextTime = eulerCurves[0].keyframes[i + 1].time;
-				float dt = nextTime - time;
-
-				FitKeyframe& fitStart = fitQuaternions[i * 2 + 0];
-				FitKeyframe& fitEnd = fitQuaternions[i * 2 + 1];
-
-				fitStart.time = time + dt * FIT_TIME;
-				fitEnd.time = time + dt * (1.0f - FIT_TIME);
-
-				fitStart.value = eulerToQuaternion(i, fitStart.time, quat);
-				fitEnd.value = eulerToQuaternion(i, fitEnd.time, fitStart.value);
-
-				lastQuat = fitStart.value;
-			}
-
-			// TODO - If animation is looping I should also compare last and first for continuity
-
-			for (INT32 j = 0; j < 4; j++)
+			bool foundMismatch = false;
+			int keyCount = fbxCurve[0]->KeyGetCount();
+			Vector<TKeyframe<T>> keyframes;
+			for (int i = 0; i < keyCount; i++)
 			{
-				quatCurves[j].keyframes.push_back(FBXKeyFrame());
-				FBXKeyFrame& keyFrame = quatCurves[j].keyframes.back();
-				keyFrame.time = time;
-				keyFrame.value = quat[j];
-
-				keyFrame.inTangent = 0;
-				keyFrame.outTangent = 0;
-			}
-		}
+				FbxTime fbxTime = fbxCurve[0]->KeyGetTime(i);
+				float time = (float)fbxTime.GetSecondDouble();
 
-		// Recalculate tangents for quaternion curves
+				// Ensure times from other curves match
+				fbxTime = fbxCurve[1]->KeyGetTime(i);
+				float time1 = (float)fbxTime.GetSecondDouble();
 
-		// TODO - There must be an analytical way to convert euler angle tangents
-		//        to quaternion tangents, but I don't want to bother figuring it out
-		//        until I have a test-bed for animation.
-		if (numKeys > 1)
-		{
-			// TODO - I could check per-key curve interpolation originally assigned in FBX
-			//        and use that to generate linear/constant slopes. Currently I assume 
-			//        its all cubic.
+				fbxTime = fbxCurve[2]->KeyGetTime(i);
+				float time2 = (float)fbxTime.GetSecondDouble();
 
-			// First key
-			{
-				const FitKeyframe& fitKeyFrame = fitQuaternions[0];
-
-				for (INT32 j = 0; j < 4; j++)
+				if(!Math::approxEquals(time, time1) || !Math::approxEquals(time, time2))
 				{
-					FBXKeyFrame& keyFrame = quatCurves[j].keyframes[0];
-
-					float dt = fitKeyFrame.time - keyFrame.time;
-
-					keyFrame.inTangent = (fitKeyFrame.value[j] - keyFrame.value) / dt;
-					keyFrame.outTangent = keyFrame.inTangent;
+					foundMismatch = true;
+					break;
 				}
-			}
-
-			// In-between keys
-			{
-				for (INT32 i = 1; i < (numKeys - 1); i++)
-				{
-					const FitKeyframe& fitPointStart = fitQuaternions[i * 2 - 1];
-					const FitKeyframe& fitPointEnd = fitQuaternions[i * 2 + 0];
-
-					for (INT32 j = 0; j < 4; j++)
-					{
-						FBXKeyFrame& keyFrame = quatCurves[j].keyframes[i];
 
-						float dt0 = fitPointEnd.time - keyFrame.time;
-						float dt1 = keyFrame.time - fitPointStart.time;
+				if (time < start || time > end)
+					continue;
 
-						float t0 = fitPointEnd.value[j] - keyFrame.value;
-						float t1 = keyFrame.value - fitPointStart.value[j];
+				keyframes.push_back(TKeyframe<T>());
+				TKeyframe<T>& keyFrame = keyframes.back();
 
-						keyFrame.inTangent = t0 / (0.5f * dt0) + t1 / (0.5f * dt1);
-						keyFrame.outTangent = keyFrame.inTangent;
-					}
-				}
-			}
-
-			// Last key
-			{
-				const FitKeyframe& fitKeyFrame = fitQuaternions[(numKeys - 2) * 2];
+				keyFrame.time = time;
 
-				for (INT32 j = 0; j < 4; j++)
+				for (int j = 0; j < C; j++)
 				{
-					FBXKeyFrame& keyFrame = quatCurves[j].keyframes[numKeys - 2];
-
-					float dt = keyFrame.time - fitKeyFrame.time;
-
-					keyFrame.inTangent = (keyFrame.value - fitKeyFrame.value[j]) / dt;
-					keyFrame.outTangent = keyFrame.inTangent;
+					setKeyframeValues(keyFrame, j,
+						fbxCurve[j]->KeyGetValue(i),
+						fbxCurve[j]->KeyGetLeftDerivative(i),
+						fbxCurve[j]->KeyGetRightDerivative(i));
 				}
 			}
+
+			if (!foundMismatch)
+				return TAnimationCurve<T>(keyframes);
+			else
+				forceResample = true;
 		}
-	}
 
-	void FBXImporter::importCurve(FbxAnimCurve* fbxCurve, FBXImportOptions& importOptions, FBXAnimationCurve& curve, float start, float end)
-	{
-		if (fbxCurve == nullptr)
-			return;
+		if (!importOptions.animResample && forceResample)
+			LOGWRN("Animation has different keyframes for different curve components, forcing resampling.");
 
-		INT32 keyCount = fbxCurve->KeyGetCount();
-		if (importOptions.animResample)
-		{
-			float curveStart = std::numeric_limits<float>::infinity();
-			float curveEnd = -std::numeric_limits<float>::infinity();
+		// Resample keys
+		float curveStart = std::numeric_limits<float>::infinity();
+		float curveEnd = -std::numeric_limits<float>::infinity();
 
-			for (INT32 i = 0; i < keyCount; i++)
+		for (INT32 i = 0; i < C; i++)
+		{
+			int keyCount = fbxCurve[i]->KeyGetCount();
+			for (INT32 j = 0; j < keyCount; j++)
 			{
-				FbxTime fbxTime = fbxCurve->KeyGetTime(i);
+				FbxTime fbxTime = fbxCurve[i]->KeyGetTime(j);
 				float time = (float)fbxTime.GetSecondDouble();
 
 				curveStart = std::min(time, curveStart);
 				curveEnd = std::max(time, curveEnd);
 			}
+		}
 
-			curveStart = Math::clamp(curveStart, start, end);
-			curveEnd = Math::clamp(curveEnd, start, end);
+		curveStart = Math::clamp(curveStart, start, end);
+		curveEnd = Math::clamp(curveEnd, start, end);
 
-			float curveLength = curveEnd - curveStart;
-			INT32 numSamples = Math::ceilToInt(curveLength / importOptions.animSampleRate);
+		float curveLength = curveEnd - curveStart;
+		INT32 numSamples = Math::ceilToInt(curveLength / importOptions.animSampleRate);
 
-			// We don't use the exact provided sample rate but instead modify it slightly so it
-			// completely covers the curve range including start/end points while maintaining
-			// constant time step between keyframes.
-			float dt = curveLength / (float)numSamples; 
+		// We don't use the exact provided sample rate but instead modify it slightly so it
+		// completely covers the curve range including start/end points while maintaining
+		// constant time step between keyframes.
+		float dt = curveLength / (float)numSamples; 
 
-			INT32 lastKeyframe = 0;
-			INT32 lastLeftTangent = 0;
-			INT32 lastRightTangent = 0;
-			for (INT32 i = 0; i < numSamples; i++)
-			{
-				float sampleTime = std::min(curveStart + i * dt, curveEnd);
-				FbxTime fbxSampleTime;
-				fbxSampleTime.SetSecondDouble(sampleTime);
-
-				curve.keyframes.push_back(FBXKeyFrame());
-				FBXKeyFrame& keyFrame = curve.keyframes.back();
-				keyFrame.time = sampleTime;
-				keyFrame.value = fbxCurve->Evaluate(fbxSampleTime, &lastKeyframe);
-				keyFrame.inTangent = fbxCurve->EvaluateLeftDerivative(fbxSampleTime, &lastLeftTangent);
-				keyFrame.outTangent = fbxCurve->EvaluateRightDerivative(fbxSampleTime, &lastRightTangent);
-			}
-		}
-		else
+		INT32 lastKeyframe[] = { 0, 0, 0 };
+		INT32 lastLeftTangent[] = { 0, 0, 0 };
+		INT32 lastRightTangent[] = { 0, 0, 0 };
+
+		Vector<TKeyframe<T>> keyframes(numSamples);
+		for (INT32 i = 0; i < numSamples; i++)
 		{
-			for (int i = 0; i < keyCount; i++)
-			{
-				FbxTime fbxTime = fbxCurve->KeyGetTime(i);
-				float time = (float)fbxTime.GetSecondDouble();
+			float sampleTime = std::min(curveStart + i * dt, curveEnd);
+			FbxTime fbxSampleTime;
+			fbxSampleTime.SetSecondDouble(sampleTime);
 
-				if (time < start || time > end)
-					continue;
+			TKeyframe<T>& keyFrame = keyframes[i];
+			keyFrame.time = sampleTime;
 
-				curve.keyframes.push_back(FBXKeyFrame());
-				FBXKeyFrame& keyFrame = curve.keyframes.back();
-				keyFrame.time = time;
-				keyFrame.value = fbxCurve->KeyGetValue(i);
-				keyFrame.inTangent = fbxCurve->KeyGetLeftDerivative(i);
-				keyFrame.outTangent = fbxCurve->KeyGetRightDerivative(i);
+			for (int j = 0; j < C; j++)
+			{
+				setKeyframeValues(keyFrame, j,
+					fbxCurve[j]->Evaluate(fbxSampleTime, &lastKeyframe[j]),
+					fbxCurve[j]->EvaluateLeftDerivative(fbxSampleTime, &lastLeftTangent[j]),
+					fbxCurve[j]->EvaluateRightDerivative(fbxSampleTime, &lastRightTangent[j]));
 			}
 		}
+
+		return TAnimationCurve<T>(keyframes);
 	}
 }

+ 3 - 27
Source/SBansheeEngine/Source/BsScriptAnimationCurves.cpp

@@ -7,6 +7,7 @@
 #include "BsMonoField.h"
 #include "BsAnimationCurve.h"
 #include "BsAnimationClip.h"
+#include "BsAnimationUtility.h"
 #include "BsMath.h"
 #include "BsVector3.h"
 
@@ -31,30 +32,6 @@ namespace BansheeEngine
 		sFloatCurvesField = metaData.scriptClass->getField("FloatCurves");
 	}
 
-	TAnimationCurve<Quaternion> eulerAngleToQuaternionCurve(const TAnimationCurve<Vector3>& inCurve)
-	{
-		UINT32 numKeys = (UINT32)inCurve.getNumKeyFrames();
-		Vector<TKeyframe<Quaternion>> quatKeys(numKeys);
-		for (UINT32 j = 0; j < numKeys; j++)
-		{
-			// TODO - Not implemented. Convert euler angle rotation to quaternion.
-		}
-
-		return TAnimationCurve<Quaternion>(quatKeys);
-	}
-
-	TAnimationCurve<Vector3> quaternionToEulerAngleCurve(const TAnimationCurve<Quaternion>& inCurve)
-	{
-		UINT32 numKeys = (UINT32)inCurve.getNumKeyFrames();
-		Vector<TKeyframe<Vector3>> eulerKeys(numKeys);
-		for (UINT32 j = 0; j < numKeys; j++)
-		{
-			// TODO - Not implemented. Convert quaternion rotation to euler angles.
-		}
-
-		return TAnimationCurve<Vector3>(eulerKeys);
-	}
-
 	SPtr<AnimationCurves> ScriptAnimationCurves::toNative(MonoObject* instance)
 	{
 		SPtr<AnimationCurves> output = bs_shared_ptr_new<AnimationCurves>();
@@ -85,7 +62,7 @@ namespace BansheeEngine
 
 				TNamedAnimationCurve<Quaternion> quatRotation;
 				quatRotation.name = eulerRotation.name;
-				quatRotation.curve = eulerAngleToQuaternionCurve(eulerRotation.curve);
+				quatRotation.curve = AnimationUtility::eulerToQuaternionCurve(eulerRotation.curve);
 
 				output->rotation.push_back(quatRotation);
 			}
@@ -138,7 +115,7 @@ namespace BansheeEngine
 		{
 			TNamedAnimationCurve<Vector3> eulerRotationCurve;
 			eulerRotationCurve.name = curves->rotation[i].name;
-			eulerRotationCurve.curve = quaternionToEulerAngleCurve(curves->rotation[i].curve);
+			eulerRotationCurve.curve = AnimationUtility::quaternionToEulerCurve(curves->rotation[i].curve);
 
 			MonoObject* monoCurve = ScriptNamedVector3Curve::toManaged(eulerRotationCurve);
 			scriptRotationCurves.set(i, monoCurve);
@@ -235,7 +212,6 @@ namespace BansheeEngine
 		}
 
 		// Populate keyframe values
-
 		Vector<TKeyframe<Vector3>> keyframeList(keyFrames.size());
 		for(auto& entry : keyFrames)
 		{