Bladeren bron

Merge branch 'master' of https://github.com/BearishSun/BansheeEngine.git

marco.bellan 9 jaren geleden
bovenliggende
commit
78bbbaad6c
60 gewijzigde bestanden met toevoegingen van 3871 en 1141 verwijderingen
  1. 2 0
      Source/BansheeCore/CMakeSources.cmake
  2. 106 14
      Source/BansheeCore/Include/BsAnimation.h
  3. 11 1
      Source/BansheeCore/Include/BsAnimationClip.h
  4. 11 0
      Source/BansheeCore/Include/BsAnimationCurve.h
  5. 3 0
      Source/BansheeCore/Include/BsAnimationCurveRTTI.h
  6. 8 2
      Source/BansheeCore/Include/BsAnimationUtility.h
  7. 13 2
      Source/BansheeCore/Include/BsSkeleton.h
  8. 53 0
      Source/BansheeCore/Include/BsSkeletonMask.h
  9. 331 42
      Source/BansheeCore/Source/BsAnimation.cpp
  10. 19 14
      Source/BansheeCore/Source/BsAnimationClip.cpp
  11. 56 27
      Source/BansheeCore/Source/BsAnimationManager.cpp
  12. 133 0
      Source/BansheeCore/Source/BsAnimationUtility.cpp
  13. 23 5
      Source/BansheeCore/Source/BsMultiRenderTexture.cpp
  14. 11 2
      Source/BansheeCore/Source/BsRenderTexture.cpp
  15. 32 9
      Source/BansheeCore/Source/BsSkeleton.cpp
  16. 36 0
      Source/BansheeCore/Source/BsSkeletonMask.cpp
  17. 1 1
      Source/BansheeD3D11RenderAPI/Source/BsD3D11RenderWindow.cpp
  18. 1 1
      Source/BansheeEditor/Include/BsDropDownWindow.h
  19. 10 6
      Source/BansheeEditor/Source/BsDropDownWindow.cpp
  20. 3 3
      Source/BansheeEngine/Include/BsGUIDropDownHitBox.h
  21. 17 4
      Source/BansheeEngine/Source/BsGUISliderHandle.cpp
  22. 6 24
      Source/BansheeFBXImporter/Include/BsFBXImportData.h
  23. 3 5
      Source/BansheeFBXImporter/Include/BsFBXImporter.h
  24. 0 41
      Source/BansheeFBXImporter/Source/BsFBXImportData.cpp
  25. 119 293
      Source/BansheeFBXImporter/Source/BsFBXImporter.cpp
  26. 1 1
      Source/MBansheeEditor/GUI/GUIListBoxField.cs
  27. 23 1
      Source/MBansheeEditor/General/EditorApplication.cs
  28. 4 1
      Source/MBansheeEditor/General/EditorPersistentData.cs
  29. 3 3
      Source/MBansheeEditor/General/Program.cs
  30. 143 0
      Source/MBansheeEditor/Inspectors/BoneInspector.cs
  31. 4 1
      Source/MBansheeEditor/MBansheeEditor.csproj
  32. 125 12
      Source/MBansheeEditor/Utility/EdAnimationCurve.cs
  33. 459 0
      Source/MBansheeEditor/Windows/Animation/EditorAnimInfo.cs
  34. 47 73
      Source/MBansheeEditor/Windows/Animation/GUIAnimEvents.cs
  35. 29 130
      Source/MBansheeEditor/Windows/Animation/GUIAnimFieldDisplay.cs
  36. 18 97
      Source/MBansheeEditor/Windows/Animation/GUICurveDrawing.cs
  37. 552 30
      Source/MBansheeEditor/Windows/Animation/GUICurveEditor.cs
  38. 1 1
      Source/MBansheeEditor/Windows/Animation/GUIFieldSelector.cs
  39. 7 146
      Source/MBansheeEditor/Windows/Animation/GUIGraphTime.cs
  40. 200 0
      Source/MBansheeEditor/Windows/Animation/GUITimelineBase.cs
  41. 222 77
      Source/MBansheeEditor/Windows/AnimationWindow.cs
  42. 10 5
      Source/MBansheeEditor/Windows/Scene/SceneCamera.cs
  43. 602 4
      Source/MBansheeEngine/Animation/Animation.cs
  44. 14 5
      Source/MBansheeEngine/Animation/AnimationClip.cs
  45. 54 1
      Source/MBansheeEngine/Animation/AnimationCurve.cs
  46. 125 0
      Source/MBansheeEngine/Animation/Bone.cs
  47. 46 0
      Source/MBansheeEngine/Animation/Interop/NativeAnimation.cs
  48. 1 0
      Source/MBansheeEngine/MBansheeEngine.csproj
  49. 1 1
      Source/MBansheeEngine/Physics/Collider.cs
  50. 13 1
      Source/MBansheeEngine/Scene/Component.cs
  51. 4 0
      Source/SBansheeEngine/Include/BsManagedComponent.h
  52. 8 0
      Source/SBansheeEngine/Include/BsScriptAnimation.h
  53. 1 0
      Source/SBansheeEngine/Include/BsScriptAnimationClip.h
  54. 2 0
      Source/SBansheeEngine/Include/BsScriptAnimationCurves.h
  55. 1 0
      Source/SBansheeEngine/Include/BsScriptComponent.h
  56. 24 23
      Source/SBansheeEngine/Source/BsManagedComponent.cpp
  57. 45 0
      Source/SBansheeEngine/Source/BsScriptAnimation.cpp
  58. 16 1
      Source/SBansheeEngine/Source/BsScriptAnimationClip.cpp
  59. 22 31
      Source/SBansheeEngine/Source/BsScriptAnimationCurves.cpp
  60. 36 0
      Source/SBansheeEngine/Source/BsScriptComponent.cpp

+ 2 - 0
Source/BansheeCore/CMakeSources.cmake

@@ -505,6 +505,7 @@ set(BS_BANSHEECORE_INC_ANIMATION
 	"Include/BsAnimationManager.h"
 	"Include/BsCurveCache.h"
 	"Include/BsAnimationUtility.h"
+	"Include/BsSkeletonMask.h"
 )
 
 set(BS_BANSHEECORE_SRC_ANIMATION
@@ -514,6 +515,7 @@ set(BS_BANSHEECORE_SRC_ANIMATION
 	"Source/BsAnimation.cpp"
 	"Source/BsAnimationManager.cpp"
 	"Source/BsAnimationUtility.cpp"
+	"Source/BsSkeletonMask.cpp"
 )
 
 source_group("Header Files\\Components" FILES ${BS_BANSHEECORE_INC_COMPONENTS})

+ 106 - 14
Source/BansheeCore/Include/BsAnimation.h

@@ -6,6 +6,7 @@
 #include "BsCoreObject.h"
 #include "BsFlags.h"
 #include "BsSkeleton.h"
+#include "BsSkeletonMask.h"
 #include "BsVector2.h"
 
 namespace BansheeEngine
@@ -102,6 +103,24 @@ namespace BansheeEngine
 		HAnimationClip botRightClip;
 	};
 
+	/** Contains a mapping between a scene object and an animation curve it is animated with. */
+	struct AnimatedSceneObject
+	{
+		HSceneObject so;
+		String curveName;
+	};
+
+	/** Contains information about a scene object that is animated by a specific animation curve. */
+	struct AnimatedSceneObjectInfo
+	{
+		UINT64 id; /**< Instance ID of the scene object. */
+		INT32 boneIdx; /**< Bone from which to access the transform. If -1 then no bone mapping is present. */
+		INT32 layerIdx; /**< If no bone mapping, layer on which the animation containing the referenced curve is in. */
+		INT32 stateIdx; /**< If no bone mapping, animation state containing the referenced curve. */
+		AnimationCurveMapping curveIndices; /**< Indices of the curves used for the transform. */
+		UINT32 hash; /**< Hash value of the scene object's transform. */
+	};
+
 	/** Represents a copy of the Animation data for use specifically on the animation thread. */
 	struct AnimationProxy
 	{
@@ -115,26 +134,30 @@ namespace BansheeEngine
 		 * Rebuilds the internal proxy data according to the newly assigned skeleton and clips. This should be called
 		 * whenever the animation skeleton changes.
 		 *
-		 * @param[in]		skeleton	New skeleton to assign to the proxy.
-		 * @param[in, out]	clipInfos	Potentially new clip infos that will be used for rebuilding the proxy. Once the
-		 *								method completes clip info layout and state indices will be populated for 
-		 *								further use in the update*() methods.
+		 * @param[in]		skeleton		New skeleton to assign to the proxy.
+		 * @param[in]		mask			Mask that filters which skeleton bones are enabled or disabled.
+		 * @param[in, out]	clipInfos		Potentially new clip infos that will be used for rebuilding the proxy. Once the
+		 *									method completes clip info layout and state indices will be populated for 
+		 *									further use in the update*() methods.
+		 * @param[in]		sceneObjects	A list of scene objects that are influenced by specific animation curves.
 		 *
 		 * @note	Should be called from the sim thread when the caller is sure the animation thread is not using it.
 		 */
-		void rebuild(const SPtr<Skeleton>& skeleton, Vector<AnimationClipInfo>& clipInfos);
+		void rebuild(const SPtr<Skeleton>& skeleton, const SkeletonMask& mask, Vector<AnimationClipInfo>& clipInfos, 
+			const Vector<AnimatedSceneObject>& sceneObjects);
 
 		/** 
 		 * Rebuilds the internal proxy data according to the newly clips. This should be called whenever clips are added
 		 * or removed, or clip layout indices change.
 		 *
-		 * @param[in, out]	clipInfos	New clip infos that will be used for rebuilding the proxy. Once the method completes
-		 *								clip info layout and state indices will be populated for further use in the
-		 *								update*() methods.
+		 * @param[in, out]	clipInfos		New clip infos that will be used for rebuilding the proxy. Once the method 
+		 *									completes clip info layout and state indices will be populated for further use 
+		 *									in the update*() methods.
+		 * @param[in]		sceneObjects	A list of scene objects that are influenced by specific animation curves.
 		 *
 		 * @note	Should be called from the sim thread when the caller is sure the animation thread is not using it.
 		 */
-		void rebuild(Vector<AnimationClipInfo>& clipInfos);
+		void rebuild(Vector<AnimationClipInfo>& clipInfos, const Vector<AnimatedSceneObject>& sceneObjects);
 
 		/** 
 		 * Updates the proxy data with new information about the clips. Caller must guarantee that clip layout didn't 
@@ -144,6 +167,14 @@ namespace BansheeEngine
 		 */
 		void updateValues(const Vector<AnimationClipInfo>& clipInfos);
 
+		/**
+		 * Updates the proxy data with new scene object transforms. Caller must guarantee that clip layout didn't 
+		 * change since the last call to rebuild().
+		 *
+		 * @note	Should be called from the sim thread when the caller is sure the animation thread is not using it.
+		 */
+		void updateTransforms(const Vector<AnimatedSceneObject>& sceneObjects);
+
 		/** 
 		 * Updates the proxy data with new clip times. Caller must guarantee that clip layout didn't change since the last
 		 * call to rebuild().
@@ -159,9 +190,15 @@ namespace BansheeEngine
 		AnimationStateLayer* layers;
 		UINT32 numLayers;
 		SPtr<Skeleton> skeleton;
+		SkeletonMask skeletonMask;
+		UINT32 numSceneObjects;
+		AnimatedSceneObjectInfo* sceneObjectInfos;
+		Matrix4* sceneObjectTransforms;
 
 		// Evaluation results
-		LocalSkeletonPose localPose;
+		LocalSkeletonPose skeletonPose;
+		LocalSkeletonPose sceneObjectPose;
+		UINT32 numGenericCurves;
 		float* genericCurveOutputs;
 	};
 
@@ -182,6 +219,12 @@ namespace BansheeEngine
 		 */
 		void setSkeleton(const SPtr<Skeleton>& skeleton);
 
+		/** 
+		 * Sets a mask that allows certain bones from the skeleton to be disabled. Caller must ensure that the mask matches
+		 * the skeleton assigned to the animation.
+		 */
+		void setMask(const SkeletonMask& mask);
+
 		/** 
 		 * Changes the wrap mode for all active animations. Wrap mode determines what happens when animation reaches the 
 		 * first or last frame. 
@@ -260,6 +303,17 @@ namespace BansheeEngine
 		/** Checks if any animation clips are currently playing. */
 		bool isPlaying() const;
 
+		/** Returns the total number of animation clips influencing this animation. */
+		UINT32 getNumClips() const;
+
+		/** 
+		 * Returns one of the animation clips influencing this animation. 
+		 *
+		 * @param[in]	idx		Sequential index of the animation clip to retrieve. In range [0, getNumClips()].
+		 * @return				Animation clip at the specified index, or null if the index is out of range.
+		 */
+		HAnimationClip getClip(UINT32 idx) const;
+
 		/** 
 		 * Retrieves detailed information about a currently playing animation clip. 
 		 *
@@ -280,12 +334,31 @@ namespace BansheeEngine
 		void setState(const HAnimationClip& clip, AnimationClipState state);
 
 		/** 
-		 * Triggers any events between the last frame and current one. 
+		 * Ensures that any position/rotation/scale animation of a specific animation curve is transfered to the
+		 * the provided scene object. Also allow the opposite operation which can allow scene object transform changes
+		 * to manipulate object bones.
 		 *
-		 * @param[in]	lastFrameTime	Time of the last frame.
-		 * @param[in]	delta			Difference between the last and this frame.
+		 * @param[in]	curve	Name of the curve (bone) to connect the scene object with. Use empty string to map to the
+		 *						root bone, regardless of the bone name.
+		 * @param[in]	so		Scene object to influence by the curve modifications, and vice versa.
 		 */
-		void triggerEvents(float lastFrameTime, float delta);
+		void mapCurveToSceneObject(const String& curve, const HSceneObject& so);
+
+		/** Removes the curve <-> scene object mapping that was set via mapCurveToSceneObject(). */
+		void unmapSceneObject(const HSceneObject& so);
+
+		/** 
+		 * Retrieves an evaluated value for a generic curve with the specified index. 
+		 *
+		 * @param[in]	curveIdx	The curve index referencing a set of curves from the first playing animation clip. 
+		 *							Generic curves from all other clips are ignored.
+		 * @param[out]	value		Value of the generic curve. Only valid if the method return true.
+		 * @return					True if the value was retrieved successfully. The method might fail if animation update
+		 *							didn't yet have a chance to execute and values are not yet available, or if the
+		 *							animation clip changed since the last frame (the last problem can be avoided by ensuring
+		 *							to read the curve values before changing the clip).
+		 */
+		bool getGenericCurveValue(UINT32 curveIdx, float& value);
 
 		/** Creates a new empty Animation object. */
 		static SPtr<Animation> create();
@@ -306,6 +379,14 @@ namespace BansheeEngine
 
 		Animation();
 
+		/** 
+		 * Triggers any events between the last frame and current one. 
+		 *
+		 * @param[in]	lastFrameTime	Time of the last frame.
+		 * @param[in]	delta			Difference between the last and this frame.
+		 */
+		void triggerEvents(float lastFrameTime, float delta);
+
 		/** 
 		 * Updates the animation proxy object based on the currently set skeleton, playing clips and dirty flags. 
 		 *
@@ -313,6 +394,13 @@ namespace BansheeEngine
 		 */
 		void updateAnimProxy(float timeDelta);
 
+		/**
+		 * Applies any outputs stored in the animation proxy (as written by the animation thread), and uses them to update
+		 * the animation state on the simulation thread. Caller must ensure that the animation thread has finished
+		 * with the animation proxy.
+		 */
+		void updateFromProxy();
+
 		/** 
 		 * Registers a new animation in the specified layer, or returns an existing animation clip info if the animation is
 		 * already registered. If @p stopExisting is true any existing animations in the layer will be stopped. Layout
@@ -326,7 +414,11 @@ namespace BansheeEngine
 		AnimDirtyState mDirty;
 
 		SPtr<Skeleton> mSkeleton;
+		SkeletonMask mSkeletonMask;
 		Vector<AnimationClipInfo> mClipInfos;
+		UnorderedMap<UINT64, AnimatedSceneObject> mSceneObjects;
+		Vector<float> mGenericCurveOutputs;
+		bool mGenericCurveValuesValid;
 
 		// Animation thread only
 		SPtr<AnimationProxy> mAnimProxy;

+ 11 - 1
Source/BansheeCore/Include/BsAnimationClip.h

@@ -122,7 +122,7 @@ namespace BansheeEngine
 		void setEvents(const Vector<AnimationEvent>& events) { mEvents = events; }
 
 		/**
-		 * Maps skeleton bone names to animation clip names, and returns a set of indices that can be easily used for
+		 * Maps skeleton bone names to animation curve names, and returns a set of indices that can be easily used for
 		 * locating an animation curve based on the bone index.
 		 *
 		 * @param[in]	skeleton	Skeleton to create the mapping for.
@@ -132,6 +132,16 @@ namespace BansheeEngine
 		 */
 		void getBoneMapping(const Skeleton& skeleton, AnimationCurveMapping* mapping) const;
 
+		/** 
+		 * Attempts to find translation/rotation/scale curves with the specified name and fills the mapping structure with
+		 * their indices, which can then be used for quick lookup.
+		 *
+		 * @param[in]	name		Name of the curves to look up.
+		 * @param[out]	mapping		Triple containing the translation/rotation/scale indices of the found curves. Indices
+		 *							will be -1 for curves that haven't been found.
+		 */
+		void getCurveMapping(const String& name, AnimationCurveMapping& mapping) const;
+
 		/** 
 		 * Checks are the curves contained within the clip additive. Additive clips are intended to be added on top of
 		 * other clips.

+ 11 - 0
Source/BansheeCore/Include/BsAnimationCurve.h

@@ -154,11 +154,22 @@ namespace BansheeEngine
 		float mLength;
 	};
 
+	/** Flags that described an TAnimationCurve<T>. */
+	enum class AnimationCurveFlag
+	{
+		/** Signifies that the curve was imported from an external file, and not created manually in-engine. */
+		ImportedCurve
+	};
+
+	typedef Flags<AnimationCurveFlag> AnimationCurveFlags;
+	BS_FLAGS_OPERATORS(AnimationCurveFlag);
+
 	/** An animation curve and its name. */
 	template <class T>
 	struct TNamedAnimationCurve
 	{
 		String name;
+		AnimationCurveFlags flags;
 		TAnimationCurve<T> curve;
 	};
 

+ 3 - 0
Source/BansheeCore/Include/BsAnimationCurveRTTI.h

@@ -110,6 +110,7 @@ namespace BansheeEngine
 			memory += sizeof(UINT32);
 
 			memory = rttiWriteElem(data.name, memory, size);
+			memory = rttiWriteElem(data.flags, memory, size);
 			memory = rttiWriteElem(data.curve, memory, size);
 
 			memcpy(memoryStart, &size, sizeof(UINT32));
@@ -122,6 +123,7 @@ namespace BansheeEngine
 			memory = rttiReadElem(size, memory);
 
 			memory = rttiReadElem(data.name, memory);
+			memory = rttiReadElem(data.flags, memory);
 			memory = rttiReadElem(data.curve, memory);
 
 			return size;
@@ -132,6 +134,7 @@ namespace BansheeEngine
 		{
 			UINT64 dataSize = sizeof(UINT32);
 			dataSize += rttiGetElemSize(data.name);
+			dataSize += rttiGetElemSize(data.flags);
 			dataSize += rttiGetElemSize(data.curve);
 
 			assert(dataSize <= std::numeric_limits<UINT32>::max());

+ 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);
 	};
 
 	/** @} */

+ 13 - 2
Source/BansheeCore/Include/BsSkeleton.h

@@ -11,6 +11,8 @@
 
 namespace BansheeEngine
 {
+	class SkeletonMask;
+
 	/** @addtogroup Animation-Internal
 	 *  @{
 	 */
@@ -40,6 +42,7 @@ namespace BansheeEngine
 	{
 		SPtr<AnimationCurves> curves; /**< All curves in the animation clip. */
 		AnimationCurveMapping* boneToCurveMapping; /**< Mapping of bone indices to curve indices for quick lookup .*/
+		AnimationCurveMapping* soToCurveMapping; /**< Mapping of scene object indices to curve indices for quick lookup. */
 
 		TCurveCache<Vector3>* positionCaches; /**< Cache used for evaluating position curves. */
 		TCurveCache<Quaternion>* rotationCaches; /**< Cache used for evaluating rotation curves. */
@@ -49,6 +52,7 @@ namespace BansheeEngine
 		float time; /**< Time to evaluate the curve at. */
 		float weight; /**< Determines how much of an influence will this clip have in regard to others in the same layer. */
 		bool loop; /**< Determines should the animation loop (wrap) once ending or beginning frames are passed. */
+		bool disabled; /**< If true the clip state will not be evaluated. */
 	};
 
 	/** Contains animation states for a single animation layer. */
@@ -112,6 +116,7 @@ namespace BansheeEngine
 		 *
 		 * @param[out]	pose		Output pose containing the requested transforms. Must be pre-allocated with enough space
 		 *							to hold all the bone matrices of this skeleton.
+		 * @param[in]	mask		Mask that filters which skeleton bones are enabled or disabled.
 		 * @param[out]	localPose	Output pose containing the local transforms. Must be pre-allocated with enough space
 		 *							to hold all the bone data of this skeleton.
 		 * @param[in]	clip		Clip to evaluate.
@@ -121,7 +126,8 @@ namespace BansheeEngine
 		 * @note	It is more efficient to use the other getPose overload as sequential calls can benefit from animation
 		 *			evaluator cache.
 		 */
-		void getPose(Matrix4* pose, LocalSkeletonPose& localPose, const AnimationClip& clip, float time, bool loop = true);
+		void getPose(Matrix4* pose, LocalSkeletonPose& localPose, const SkeletonMask& mask, const AnimationClip& clip, 
+			float time, bool loop = true);
 
 		/** 
 		 * Outputs a skeleton pose containing required transforms for transforming the skeleton to the values specified by
@@ -129,12 +135,14 @@ namespace BansheeEngine
 		 *
 		 * @param[out]	pose		Output pose containing the requested transforms. Must be pre-allocated with enough space
 		 *							to hold all the bone matrices of this skeleton.
+		 * @param[in]	mask		Mask that filters which skeleton bones are enabled or disabled.
 		 * @param[out]	localPose	Output pose containing the local transforms. Must be pre-allocated with enough space
 		 *							to hold all the bone data of this skeleton.
 		 * @param[in]	layers		One or multiple layers, containing one or multiple animation states to evaluate.
 		 * @param[in]	numLayers	Number of layers in the @p layers array.
 		 */
-		void getPose(Matrix4* pose, LocalSkeletonPose& localPose, const AnimationStateLayer* layers, UINT32 numLayers);
+		void getPose(Matrix4* pose, LocalSkeletonPose& localPose, const SkeletonMask& mask, 
+			const AnimationStateLayer* layers, UINT32 numLayers);
 
 		/** Returns the total number of bones in the skeleton. */
 		UINT32 getNumBones() const { return mNumBones; }
@@ -142,6 +150,9 @@ namespace BansheeEngine
 		/** Returns information about a bone at the provided index. */
 		const SkeletonBoneInfo& getBoneInfo(UINT32 idx) const { return mBoneInfo[idx]; }
 
+		/** Searches all bones to find a root bone. Returns -1 if no root can be found. */
+		UINT32 getRootBoneIndex() const;
+
 		/** Returns the inverse bind pose for the bone at the provided index. */
 		const Matrix4& getInvBindPose(UINT32 idx) const { return mInvBindPoses[idx]; }
 

+ 53 - 0
Source/BansheeCore/Include/BsSkeletonMask.h

@@ -0,0 +1,53 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+#pragma once
+
+#include "BsCorePrerequisites.h"
+
+namespace BansheeEngine
+{
+	/** @addtogroup Animation
+	 *  @{
+	 */
+
+	/** 
+	 * Contains a bitfield that determines which skeleton bones are enabled or disabled during skeletal animation. Use
+	 * SkeletonMaskBuilder to create a mask for a specific skeleton.
+	 */
+	class BS_CORE_EXPORT SkeletonMask
+	{
+	public:
+		SkeletonMask() {}
+		SkeletonMask(UINT32 numBones);
+
+		/** 
+		 * Checks is the bone at the specified index enabled. Caller is expected to know which skeleton is the skeleton
+		 * mask tied with, in order to determine the bone index. 
+		 */
+		bool isEnabled(UINT32 boneIdx) const;
+
+	private:
+		friend class SkeletonMaskBuilder;
+
+		Vector<bool> mIsDisabled;
+	};
+
+	/** Builds a SkeletonMask for a specific skeleton. */
+	class BS_CORE_EXPORT SkeletonMaskBuilder
+	{
+	public:
+		SkeletonMaskBuilder(const SPtr<Skeleton>& skeleton);
+
+		/** Enables or disables a bone with the specified name. */
+		void setBoneState(const String& name, bool enabled);
+
+		/** Teturns the built skeleton mask. */
+		SkeletonMask getMask() const { return mMask; }
+
+	private:
+		SPtr<Skeleton> mSkeleton;
+		SkeletonMask mMask;
+	};
+
+	/** @} */
+}

+ 331 - 42
Source/BansheeCore/Source/BsAnimation.cpp

@@ -4,15 +4,16 @@
 #include "BsAnimationManager.h"
 #include "BsAnimationClip.h"
 #include "BsAnimationUtility.h"
+#include "BsSceneObject.h"
 
 namespace BansheeEngine
 {
 	AnimationClipInfo::AnimationClipInfo()
-		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), layerIdx(0), curveVersion(0), stateIdx(0)
+		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), curveVersion(0), layerIdx((UINT32)-1), stateIdx((UINT32)-1)
 	{ }
 
 	AnimationClipInfo::AnimationClipInfo(const HAnimationClip& clip)
-		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), clip(clip), curveVersion(0), layerIdx(0), stateIdx(0)
+		: fadeDirection(0.0f), fadeTime(0.0f), fadeLength(0.0f), clip(clip), curveVersion(0), layerIdx((UINT32)-1), stateIdx((UINT32)-1)
 	{ }
 
 	Blend1DInfo::Blend1DInfo(UINT32 numClips)
@@ -29,7 +30,8 @@ namespace BansheeEngine
 	}
 
 	AnimationProxy::AnimationProxy(UINT64 id)
-		:id(id), layers(nullptr), numLayers(0), genericCurveOutputs(nullptr)
+		: id(id), layers(nullptr), numLayers(0), numSceneObjects(0), sceneObjectInfos(nullptr)
+		, sceneObjectTransforms(nullptr), genericCurveOutputs(nullptr)
 	{ }
 
 	AnimationProxy::~AnimationProxy()
@@ -83,59 +85,59 @@ namespace BansheeEngine
 						state.boneToCurveMapping[k].~AnimationCurveMapping();
 				}
 
+				if(state.soToCurveMapping != nullptr)
+				{
+					for(UINT32 k = 0; k < numSceneObjects; k++)
+						state.soToCurveMapping[k].~AnimationCurveMapping();
+				}
+
 				state.~AnimationState();
 			}
 
 			layer.~AnimationStateLayer();
 		}
 
-		// All the memory is part of the same buffer, so we only need to free the first element
+		// All of the memory is part of the same buffer, so we only need to free the first element
 		bs_free(layers);
 		layers = nullptr;
 		genericCurveOutputs = nullptr;
+		sceneObjectInfos = nullptr;
+		sceneObjectTransforms = nullptr;
 
 		numLayers = 0;
+		numSceneObjects = 0;
+		numGenericCurves = 0;
 	}
 
-	void AnimationProxy::rebuild(const SPtr<Skeleton>& skeleton, Vector<AnimationClipInfo>& clipInfos)
+	void AnimationProxy::rebuild(const SPtr<Skeleton>& skeleton, const SkeletonMask& mask, 
+		Vector<AnimationClipInfo>& clipInfos, const Vector<AnimatedSceneObject>& sceneObjects)
 	{
 		this->skeleton = skeleton;
+		this->skeletonMask = skeletonMask;
 
-		// Note: I could avoid having a separate allocation for LocalSkeletonPose 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
 		if (skeleton != nullptr)
-			localPose = LocalSkeletonPose(skeleton->getNumBones());
-		else
-		{
-			UINT32 numPosCurves = 0;
-			UINT32 numRotCurves = 0;
-			UINT32 numScaleCurves = 0;
-
-			// Note: I'm recalculating this both here and in follow-up rebuild() call, it could be avoided.
-			for (auto& clipInfo : clipInfos)
-			{
-				if (!clipInfo.clip.isLoaded())
-					continue;
-
-				SPtr<AnimationCurves> curves = clipInfo.clip->getCurves();
-				numPosCurves += (UINT32)curves->position.size();
-				numRotCurves += (UINT32)curves->rotation.size();
-				numScaleCurves += (UINT32)curves->scale.size();
-			}
+			skeletonPose = LocalSkeletonPose(skeleton->getNumBones());
 
-			localPose = LocalSkeletonPose(numPosCurves, numRotCurves, numScaleCurves);
-		}
+		numSceneObjects = (UINT32)sceneObjects.size();
+		if (numSceneObjects > 0)
+			sceneObjectPose = LocalSkeletonPose(numSceneObjects);
+		else
+			sceneObjectPose = LocalSkeletonPose();
 
-		rebuild(clipInfos);
+		rebuild(clipInfos, sceneObjects);
 	}
 
-	void AnimationProxy::rebuild(Vector<AnimationClipInfo>& clipInfos)
+	void AnimationProxy::rebuild(Vector<AnimationClipInfo>& clipInfos, const Vector<AnimatedSceneObject>& sceneObjects)
 	{
 		clear();
 
 		bs_frame_mark();
 		{
+			FrameVector<bool> clipLoadState(clipInfos.size());
 			FrameVector<AnimationStateLayer> tempLayers;
+			UINT32 clipIdx = 0;
 			for (auto& clipInfo : clipInfos)
 			{
 				UINT32 layer = clipInfo.state.layer;
@@ -150,14 +152,19 @@ namespace BansheeEngine
 					return x.index == layer;
 				});
 
+				bool isLoaded = clipInfo.clip.isLoaded();
+				clipLoadState[clipIdx] = isLoaded;
+
 				if (iterFind == tempLayers.end())
 				{
 					tempLayers.push_back(AnimationStateLayer());
 					AnimationStateLayer& newLayer = tempLayers.back();
 
 					newLayer.index = layer;
-					newLayer.additive = clipInfo.clip.isLoaded() && clipInfo.clip->isAdditive();
+					newLayer.additive = isLoaded && clipInfo.clip->isAdditive();
 				}
+
+				clipIdx++;
 			}
 
 			std::sort(tempLayers.begin(), tempLayers.end(), 
@@ -178,18 +185,63 @@ namespace BansheeEngine
 			UINT32 numPosCurves = 0;
 			UINT32 numRotCurves = 0;
 			UINT32 numScaleCurves = 0;
-			UINT32 numGenCurves = 0;
 
+			clipIdx = 0;
 			for (auto& clipInfo : clipInfos)
 			{
-				if (!clipInfo.clip.isLoaded())
+				bool isLoaded = clipLoadState[clipIdx++];
+				if (!isLoaded)
 					continue;
 
 				SPtr<AnimationCurves> curves = clipInfo.clip->getCurves();
 				numPosCurves += (UINT32)curves->position.size();
 				numRotCurves += (UINT32)curves->rotation.size();
 				numScaleCurves += (UINT32)curves->scale.size();
-				numGenCurves += (UINT32)curves->generic.size();
+			}
+
+			numGenericCurves = 0;
+			if(clipInfos.size() > 0 && clipLoadState[0])
+			{
+				SPtr<AnimationCurves> curves = clipInfos[0].clip->getCurves();
+				numGenericCurves = (UINT32)curves->generic.size();
+			}
+
+			UINT32* mappedBoneIndices = (UINT32*)bs_frame_alloc(sizeof(UINT32) * numSceneObjects);
+			for (UINT32 i = 0; i < numSceneObjects; i++)
+				mappedBoneIndices[i] = -1;
+
+			UINT32 numBoneMappedSOs = 0;
+			if (skeleton != nullptr)
+			{
+				for (UINT32 i = 0; i < numSceneObjects; i++)
+				{
+					if (sceneObjects[i].so.isDestroyed(true))
+						continue;
+
+					// Empty string always means root bone
+					if (sceneObjects[i].curveName.empty())
+					{
+						UINT32 rootBoneIdx = skeleton->getRootBoneIndex();
+						if (rootBoneIdx != (UINT32)-1)
+						{
+							mappedBoneIndices[i] = rootBoneIdx;
+							numBoneMappedSOs++;
+						}
+					}
+					else
+					{
+						for (UINT32 j = 0; j < numBones; j++)
+						{
+							if (skeleton->getBoneInfo(j).name == sceneObjects[i].curveName)
+							{
+								mappedBoneIndices[i] = j;
+
+								numBoneMappedSOs++;
+								break;
+							}
+						}
+					}
+				}
 			}
 
 			UINT32 numBoneMappings = numBones * numClips;
@@ -199,11 +251,13 @@ namespace BansheeEngine
 			UINT32 posCacheSize = numPosCurves * sizeof(TCurveCache<Vector3>);
 			UINT32 rotCacheSize = numRotCurves * sizeof(TCurveCache<Quaternion>);
 			UINT32 scaleCacheSize = numScaleCurves * sizeof(TCurveCache<Vector3>);
-			UINT32 genCacheSize = numGenCurves * sizeof(TCurveCache<float>);
-			UINT32 genericCurveOutputSize = numGenCurves * sizeof(float);
+			UINT32 genCacheSize = numGenericCurves * sizeof(TCurveCache<float>);
+			UINT32 genericCurveOutputSize = numGenericCurves * sizeof(float);
+			UINT32 sceneObjectIdsSize = numSceneObjects * sizeof(AnimatedSceneObjectInfo);
+			UINT32 sceneObjectTransformsSize = numBoneMappedSOs * sizeof(Matrix4);
 
 			UINT8* data = (UINT8*)bs_alloc(layersSize + clipsSize + boneMappingSize + posCacheSize + rotCacheSize + 
-				scaleCacheSize + genCacheSize + genericCurveOutputSize);
+				scaleCacheSize + genCacheSize + genericCurveOutputSize + sceneObjectIdsSize + sceneObjectTransformsSize);
 
 			layers = (AnimationStateLayer*)data;
 			memcpy(layers, tempLayers.data(), layersSize);
@@ -240,16 +294,28 @@ namespace BansheeEngine
 			data += scaleCacheSize;
 
 			TCurveCache<float>* genCache = (TCurveCache<float>*)data;
-			for (UINT32 i = 0; i < numGenCurves; i++)
+			for (UINT32 i = 0; i < numGenericCurves; i++)
 				new (&genCache[i]) TCurveCache<float>();
 
 			data += genCacheSize;
 
 			genericCurveOutputs = (float*)data;
+			data += genericCurveOutputSize;
+
+			sceneObjectInfos = (AnimatedSceneObjectInfo*)data;
+			data += sceneObjectIdsSize;
+
+			sceneObjectTransforms = (Matrix4*)data;
+			for (UINT32 i = 0; i < numBoneMappedSOs; i++)
+				sceneObjectTransforms[i] = Matrix4::IDENTITY;
+
+			data += sceneObjectTransformsSize;
 
 			UINT32 curLayerIdx = 0;
 			UINT32 curStateIdx = 0;
 
+			// Note: Hidden dependency. First clip info must be in layers[0].states[0] (needed for generic curves which only
+			// use the primary clip).
 			for(UINT32 i = 0; i < numLayers; i++)
 			{
 				AnimationStateLayer& layer = layers[i];
@@ -258,8 +324,10 @@ namespace BansheeEngine
 				layer.numStates = 0;
 
 				UINT32 localStateIdx = 0;
-				for(auto& clipInfo : clipInfos)
+				for(UINT32 j = 0; j < (UINT32)clipInfos.size(); j++)
 				{
+					AnimationClipInfo& clipInfo = clipInfos[j];
+
 					UINT32 clipLayer = clipInfo.state.layer;
 					if (clipLayer == (UINT32)-1)
 						clipLayer = 0;
@@ -291,13 +359,17 @@ namespace BansheeEngine
 					state.weight = weight;
 
 					// Set up individual curves and their caches
-					bool isClipValid = clipInfo.clip.isLoaded();
-					if(isClipValid)
+					bool isClipValid = clipLoadState[j];
+					if (isClipValid)
+					{
 						state.curves = clipInfo.clip->getCurves();
+						state.disabled = false;
+					}
 					else
 					{
 						static SPtr<AnimationCurves> zeroCurves = bs_shared_ptr_new<AnimationCurves>();
 						state.curves = zeroCurves;
+						state.disabled = true;
 					}
 
 					state.positionCaches = posCache;
@@ -347,7 +419,56 @@ namespace BansheeEngine
 
 				// Must be larger than zero otherwise the layer.states pointer will point to data held by some other layer
 				assert(layer.numStates > 0);
-			}			
+			}
+
+			UINT32 boneIdx = 0;
+			for(UINT32 i = 0; i < numSceneObjects; i++)
+			{
+				HSceneObject so = sceneObjects[i].so;
+				AnimatedSceneObjectInfo& soInfo = sceneObjectInfos[i];
+				soInfo.id = so.getInstanceId();
+				soInfo.boneIdx = mappedBoneIndices[i];
+
+				bool isSOValid = !so.isDestroyed(true);
+				if (isSOValid)
+					soInfo.hash = so->getTransformHash();
+				else
+					soInfo.hash = 0;
+
+				// If no bone mapping, find curves directly
+				if(soInfo.boneIdx == -1)
+				{
+					soInfo.curveIndices = { (UINT32)-1, (UINT32)-1, (UINT32)-1 };
+
+					if (isSOValid)
+					{
+						for (UINT32 j = 0; j < (UINT32)clipInfos.size(); j++)
+						{
+							AnimationClipInfo& clipInfo = clipInfos[j];
+
+							soInfo.layerIdx = clipInfo.layerIdx;
+							soInfo.stateIdx = clipInfo.stateIdx;
+
+							bool isClipValid = clipLoadState[j];
+							if (isClipValid)
+							{
+								// Note: If there are multiple clips with the relevant curve name, we only use the first
+
+								clipInfo.clip->getCurveMapping(sceneObjects[i].curveName, soInfo.curveIndices);
+								break;
+							}
+						}
+					}
+				}
+				else
+				{
+					// No need to check if SO is valid, if it has a bone connection it must be
+					sceneObjectTransforms[boneIdx] = so->getWorldTfrm();
+					boneIdx++;
+				}
+			}
+
+			bs_frame_free(mappedBoneIndices);
 		}
 		bs_frame_clear();
 	}
@@ -364,6 +485,28 @@ namespace BansheeEngine
 		}
 	}
 
+	void AnimationProxy::updateTransforms(const Vector<AnimatedSceneObject>& sceneObjects)
+	{
+		UINT32 boneIdx = 0;
+		for (UINT32 i = 0; i < numSceneObjects; i++)
+		{
+			HSceneObject so = sceneObjects[i].so;
+			if (so.isDestroyed(true))
+			{
+				sceneObjectInfos[i].hash = 0;
+				continue;
+			}
+
+			sceneObjectInfos[i].hash = so->getTransformHash();
+
+			if (sceneObjectInfos[i].boneIdx == -1)
+				continue;
+
+			sceneObjectTransforms[boneIdx] = sceneObjects[i].so->getWorldTfrm();
+			boneIdx++;
+		}
+	}
+
 	void AnimationProxy::updateTime(const Vector<AnimationClipInfo>& clipInfos)
 	{
 		for (auto& clipInfo : clipInfos)
@@ -375,6 +518,7 @@ namespace BansheeEngine
 
 	Animation::Animation()
 		: mDefaultWrapMode(AnimWrapMode::Loop), mDefaultSpeed(1.0f), mDirty(AnimDirtyStateFlag::Skeleton)
+		, mGenericCurveValuesValid(false)
 	{
 		mId = AnimationManager::instance().registerAnimation(this);
 		mAnimProxy = bs_shared_ptr_new<AnimationProxy>(mId);
@@ -391,6 +535,12 @@ namespace BansheeEngine
 		mDirty |= AnimDirtyStateFlag::Skeleton;
 	}
 
+	void Animation::setMask(const SkeletonMask& mask)
+	{
+		mSkeletonMask = mask;
+		mDirty |= AnimDirtyStateFlag::Skeleton;
+	}
+
 	void Animation::setWrapMode(AnimWrapMode wrapMode)
 	{
 		mDefaultWrapMode = wrapMode;
@@ -762,6 +912,19 @@ namespace BansheeEngine
 		mDirty |= AnimDirtyStateFlag::Value;
 	}
 
+	UINT32 Animation::getNumClips() const
+	{
+		return (UINT32)mClipInfos.size();
+	}
+
+	HAnimationClip Animation::getClip(UINT32 idx) const
+	{
+		if (idx >= (UINT32)mClipInfos.size())
+			return HAnimationClip();
+
+		return mClipInfos[idx].clip;
+	}
+
 	void Animation::triggerEvents(float lastFrameTime, float delta)
 	{
 		for (auto& clipInfo : mClipInfos)
@@ -800,6 +963,30 @@ namespace BansheeEngine
 		}
 	}
 
+	void Animation::mapCurveToSceneObject(const String& curve, const HSceneObject& so)
+	{
+		AnimatedSceneObject animSo = { so, curve };
+		mSceneObjects[so.getInstanceId()] = animSo;
+
+		mDirty |= AnimDirtyStateFlag::Skeleton;
+	}
+
+	void Animation::unmapSceneObject(const HSceneObject& so)
+	{
+		mSceneObjects.erase(so.getInstanceId());
+
+		mDirty |= AnimDirtyStateFlag::Skeleton;
+	}
+
+	bool Animation::getGenericCurveValue(UINT32 curveIdx, float& value)
+	{
+		if (!mGenericCurveValuesValid || curveIdx >= (UINT32)mGenericCurveOutputs.size())
+			return false;
+
+		value = mGenericCurveOutputs[curveIdx];
+		return true;
+	}
+
 	SPtr<Animation> Animation::create()
 	{
 		Animation* anim = new (bs_alloc<Animation>()) Animation();
@@ -832,14 +1019,116 @@ namespace BansheeEngine
 		}
 		else
 		{
+			auto getAnimatedSOList = [&]()
+			{
+				Vector<AnimatedSceneObject> animatedSO(mSceneObjects.size());
+				UINT32 idx = 0;
+				for (auto& entry : mSceneObjects)
+					animatedSO[idx++] = entry.second;
+
+				return animatedSO;
+			};
+
+			bool didFullRebuild = false;
 			if (mDirty.isSet(AnimDirtyStateFlag::Skeleton))
-				mAnimProxy->rebuild(mSkeleton, mClipInfos);
+			{
+				Vector<AnimatedSceneObject> animatedSOs = getAnimatedSOList();
+
+				mAnimProxy->rebuild(mSkeleton, mSkeletonMask, mClipInfos, animatedSOs);
+				didFullRebuild = true;
+			}
 			else if (mDirty.isSet(AnimDirtyStateFlag::Layout))
-				mAnimProxy->rebuild(mClipInfos);
+			{
+				Vector<AnimatedSceneObject> animatedSOs = getAnimatedSOList();
+
+				mAnimProxy->rebuild(mClipInfos, animatedSOs);
+				didFullRebuild = true;
+			}
 			else if (mDirty.isSet(AnimDirtyStateFlag::Value))
 				mAnimProxy->updateValues(mClipInfos);
+
+			// Check if there are dirty transforms
+			if(!didFullRebuild)
+			{
+				UINT32 numSceneObjects = (UINT32)mSceneObjects.size();
+				for (UINT32 i = 0; i < numSceneObjects; i++)
+				{
+					UINT32 hash;
+
+					HSceneObject so = mSceneObjects[i].so;
+					if (so.isDestroyed(true))
+						hash = 0;
+					else
+						hash = so->getTransformHash();
+
+					if(hash != mAnimProxy->sceneObjectInfos[i].hash)
+					{
+						Vector<AnimatedSceneObject> animatedSOs = getAnimatedSOList();
+						mAnimProxy->updateTransforms(animatedSOs);
+						break;
+					}
+				}
+			}
 		}
 
 		mDirty = AnimDirtyState();
 	}
+
+	void Animation::updateFromProxy()
+	{
+		// Write TRS animation results to relevant SceneObjects
+		for(UINT32 i = 0; i < mAnimProxy->numSceneObjects; i++)
+		{
+			const AnimatedSceneObjectInfo& soInfo = mAnimProxy->sceneObjectInfos[i];
+
+			auto iterFind = mSceneObjects.find(soInfo.id);
+			if (iterFind == mSceneObjects.end())
+				continue;
+
+			HSceneObject so = iterFind->second.so;
+			if (so.isDestroyed(true))
+				continue;
+
+			if(soInfo.boneIdx != -1)
+			{
+				so->setPosition(mAnimProxy->skeletonPose.positions[soInfo.boneIdx]);
+				so->setRotation(mAnimProxy->skeletonPose.rotations[soInfo.boneIdx]);
+				so->setScale(mAnimProxy->skeletonPose.scales[soInfo.boneIdx]);
+			}
+			else
+			{
+				so->setPosition(mAnimProxy->sceneObjectPose.positions[i]);
+				so->setRotation(mAnimProxy->sceneObjectPose.rotations[i]);
+				so->setScale(mAnimProxy->sceneObjectPose.scales[i]);
+			}
+		}
+
+		// Must ensure that clip in the proxy and current primary clip are the same
+		mGenericCurveValuesValid = false;
+		if(mAnimProxy->numLayers > 0 || mAnimProxy->layers[0].numStates > 0)
+		{
+			const AnimationState& state = mAnimProxy->layers[0].states[0];
+
+			if(!state.disabled && mClipInfos.size() > 0)
+			{
+				const AnimationClipInfo& clipInfo = mClipInfos[0];
+
+				if (clipInfo.stateIdx == 0 && clipInfo.layerIdx == 0)
+				{
+					if (clipInfo.clip.isLoaded() && clipInfo.curveVersion == clipInfo.clip->getVersion())
+					{
+						UINT32 numGenericCurves = (UINT32)clipInfo.clip->getCurves()->generic.size();
+						mGenericCurveValuesValid = numGenericCurves == mAnimProxy->numGenericCurves;
+					}
+				}
+			}
+		}
+
+		if(mGenericCurveValuesValid)
+		{
+			mGenericCurveOutputs.resize(mAnimProxy->numGenericCurves);
+
+			memcpy(mGenericCurveOutputs.data(), mAnimProxy->genericCurveOutputs, mAnimProxy->numGenericCurves * sizeof(float));
+		}
+	}
 }

+ 19 - 14
Source/BansheeCore/Source/BsAnimationClip.cpp

@@ -15,7 +15,7 @@ namespace BansheeEngine
 		if (iterFind != position.end())
 			iterFind->curve = curve;
 		else
-			position.push_back({ name, curve });
+			position.push_back({ name, AnimationCurveFlags(), curve });
 	}
 
 	void AnimationCurves::addRotationCurve(const String& name, const TAnimationCurve<Quaternion>& curve)
@@ -25,7 +25,7 @@ namespace BansheeEngine
 		if (iterFind != rotation.end())
 			iterFind->curve = curve;
 		else
-			rotation.push_back({ name, curve });
+			rotation.push_back({ name, AnimationCurveFlags(), curve });
 	}
 
 	void AnimationCurves::addScaleCurve(const String& name, const TAnimationCurve<Vector3>& curve)
@@ -35,7 +35,7 @@ namespace BansheeEngine
 		if (iterFind != scale.end())
 			iterFind->curve = curve;
 		else
-			scale.push_back({ name, curve });
+			scale.push_back({ name, AnimationCurveFlags(), curve });
 	}
 
 	void AnimationCurves::addGenericCurve(const String& name, const TAnimationCurve<float>& curve)
@@ -45,7 +45,7 @@ namespace BansheeEngine
 		if (iterFind != generic.end())
 			iterFind->curve = curve;
 		else
-			generic.push_back({ name, curve });
+			generic.push_back({ name, AnimationCurveFlags(), curve });
 	}
 
 	void AnimationCurves::removePositionCurve(const String& name)
@@ -195,18 +195,23 @@ namespace BansheeEngine
 		{
 			const SkeletonBoneInfo& boneInfo = skeleton.getBoneInfo(i);
 
-			auto iterFind = mNameMapping.find(boneInfo.name);
-			if(iterFind != mNameMapping.end())
-			{
-				const UINT32* indices = iterFind->second;
+			getCurveMapping(boneInfo.name, mapping[i]);
+		}
+	}
 
-				mapping[i].position = indices[(UINT32)CurveType::Position];
-				mapping[i].rotation = indices[(UINT32)CurveType::Rotation];
-				mapping[i].scale = indices[(UINT32)CurveType::Scale];
-			}
-			else
-				mapping[i] = { (UINT32)-1, (UINT32)-1, (UINT32)-1 };
+	void AnimationClip::getCurveMapping(const String& name, AnimationCurveMapping& mapping) const
+	{
+		auto iterFind = mNameMapping.find(name);
+		if (iterFind != mNameMapping.end())
+		{
+			const UINT32* indices = iterFind->second;
+
+			mapping.position = indices[(UINT32)CurveType::Position];
+			mapping.rotation = indices[(UINT32)CurveType::Rotation];
+			mapping.scale = indices[(UINT32)CurveType::Scale];
 		}
+		else
+			mapping = { (UINT32)-1, (UINT32)-1, (UINT32)-1 };
 	}
 
 	RTTITypeBase* AnimationClip::getRTTIStatic()

+ 56 - 27
Source/BansheeCore/Source/BsAnimationManager.cpp

@@ -41,10 +41,10 @@ namespace BansheeEngine
 
 		// Trigger events
 		for (auto& anim : mAnimations)
+		{
+			anim.second->updateFromProxy();
 			anim.second->triggerEvents(mAnimationTime, gTime().getFrameDelta());
-
-		// TODO - Write TRS animation results to relevant SceneObjects
-		// TODO - Transfer generic curve evaluated data back to Animation
+		}
 
 		mWorkerRunning = false;
 	}
@@ -118,44 +118,71 @@ namespace BansheeEngine
 				info.numBones = numBones;
 
 				Matrix4* boneDst = renderData.transforms.data() + curBoneIdx;
-				anim->skeleton->getPose(boneDst, anim->localPose, anim->layers, anim->numLayers);
+
+				// Copy transforms from mapped scene objects
+				UINT32 boneTfrmIdx = 0;
+				for(UINT32 i = 0; i < anim->numSceneObjects; i++)
+				{
+					const AnimatedSceneObjectInfo& soInfo = anim->sceneObjectInfos[i];
+
+					if (soInfo.boneIdx == -1)
+						continue;
+
+					boneDst[soInfo.boneIdx] = anim->sceneObjectTransforms[boneTfrmIdx];
+					boneTfrmIdx++;
+				}
+
+				// Animate bones
+				anim->skeleton->getPose(boneDst, anim->skeletonPose, anim->skeletonMask, anim->layers, anim->numLayers);
 
 				renderData.poseInfos[anim->id] = info;
 				curBoneIdx += numBones;
 			}
-			else
+
+			// Update mapped scene objects
+			for(UINT32 i = 0; i < anim->numSceneObjects; i++)
 			{
-				// Note: No blending for non-skeletal animations, just use first animation
-				if(anim->numLayers > 0 && anim->layers[0].numStates > 0)
-				{
-					const AnimationState& state = anim->layers[0].states[0];
+				const AnimatedSceneObjectInfo& soInfo = anim->sceneObjectInfos[i];
+
+				// We already evaluated bones
+				if (soInfo.boneIdx != -1)
+					continue;
 
+				const AnimationState& state = anim->layers[soInfo.layerIdx].states[soInfo.stateIdx];
+				if (state.disabled)
+					continue;
+
+				{
+					UINT32 curveIdx = soInfo.curveIndices.position;
+					if (curveIdx != (UINT32)-1)
 					{
-						UINT32 numCurves = (UINT32)state.curves->position.size();
-						for(UINT32 i = 0; i < numCurves; i++)
-						{
-							const TAnimationCurve<Vector3>& curve = state.curves->position[i].curve;
-							anim->localPose.positions[i] = curve.evaluate(state.time, state.positionCaches[i], state.loop);
-						}
+						const TAnimationCurve<Vector3>& curve = state.curves->position[curveIdx].curve;
+						anim->sceneObjectPose.positions[curveIdx] = curve.evaluate(state.time, state.positionCaches[curveIdx], state.loop);
 					}
+					else
+						anim->sceneObjectPose.positions[curveIdx] = Vector3::ZERO;
+				}
 
+				{
+					UINT32 curveIdx = soInfo.curveIndices.rotation;
+					if (curveIdx != (UINT32)-1)
 					{
-						UINT32 numCurves = (UINT32)state.curves->rotation.size();
-						for (UINT32 i = 0; i < numCurves; i++)
-						{
-							const TAnimationCurve<Quaternion>& curve = state.curves->rotation[i].curve;
-							anim->localPose.rotations[i] = curve.evaluate(state.time, state.rotationCaches[i], state.loop);
-						}
+						const TAnimationCurve<Quaternion>& curve = state.curves->rotation[curveIdx].curve;
+						anim->sceneObjectPose.rotations[curveIdx] = curve.evaluate(state.time, state.rotationCaches[curveIdx], state.loop);
 					}
+					else
+						anim->sceneObjectPose.rotations[curveIdx] = Quaternion::ZERO;
+				}
 
+				{
+					UINT32 curveIdx = soInfo.curveIndices.scale;
+					if (curveIdx != (UINT32)-1)
 					{
-						UINT32 numCurves = (UINT32)state.curves->scale.size();
-						for (UINT32 i = 0; i < numCurves; i++)
-						{
-							const TAnimationCurve<Vector3>& curve = state.curves->scale[i].curve;
-							anim->localPose.scales[i] = curve.evaluate(state.time, state.scaleCaches[i], state.loop);
-						}
+						const TAnimationCurve<Vector3>& curve = state.curves->scale[curveIdx].curve;
+						anim->sceneObjectPose.scales[curveIdx] = curve.evaluate(state.time, state.scaleCaches[curveIdx], state.loop);
 					}
+					else
+						anim->sceneObjectPose.scales[curveIdx] = Vector3::ONE;
 				}
 			}
 
@@ -163,6 +190,8 @@ namespace BansheeEngine
 			if (anim->numLayers > 0 && anim->layers[0].numStates > 0)
 			{
 				const AnimationState& state = anim->layers[0].states[0];
+				if (state.disabled)
+					continue;
 
 				{
 					UINT32 numCurves = (UINT32)state.curves->generic.size();

+ 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);
+	}
 }

+ 23 - 5
Source/BansheeCore/Source/BsMultiRenderTexture.cpp

@@ -149,15 +149,24 @@ namespace BansheeEngine
 			const TextureProperties& curTexProps = mColorSurfaces[i]->getTexture()->getProperties();
 			const TextureProperties& firstTexProps = firstSurfaceDesc->getTexture()->getProperties();
 
+			UINT32 curMsCount = curTexProps.getMultisampleCount();
+			UINT32 firstMsCount = firstTexProps.getMultisampleCount();
+
+			if (curMsCount == 0)
+				curMsCount = 1;
+
+			if (firstMsCount == 0)
+				firstMsCount = 1;
+
 			if (curTexProps.getWidth() != firstTexProps.getWidth() ||
 				curTexProps.getHeight() != firstTexProps.getHeight() ||
-				curTexProps.getMultisampleCount() != firstTexProps.getMultisampleCount())
+				curMsCount != firstMsCount)
 			{
 				String errorInfo = "\nWidth: " + toString(curTexProps.getWidth()) + "/" + toString(firstTexProps.getWidth());
 				errorInfo += "\nHeight: " + toString(curTexProps.getHeight()) + "/" + toString(firstTexProps.getHeight());
-				errorInfo += "\nMultisample Count: " + toString(curTexProps.getMultisampleCount()) + "/" + toString(firstTexProps.getMultisampleCount());
+				errorInfo += "\nMultisample Count: " + toString(curMsCount) + "/" + toString(firstMsCount);
 
-				BS_EXCEPT(InvalidParametersException, "Provided texture and depth stencil buffer don't match!" + errorInfo);
+				BS_EXCEPT(InvalidParametersException, "Provided color textures don't match!" + errorInfo);
 			}
 		}
 
@@ -190,13 +199,22 @@ namespace BansheeEngine
 				return;
 
 			const TextureProperties& depthTexProps = mDepthStencilSurface->getTexture()->getProperties();
+			UINT32 depthMsCount = depthTexProps.getMultisampleCount();
+			UINT32 colorMsCount = firstTexProps.getMultisampleCount();
+
+			if (depthMsCount == 0)
+				depthMsCount = 1;
+
+			if (colorMsCount == 0)
+				colorMsCount = 1;
+
 			if (depthTexProps.getWidth() != firstTexProps.getWidth() ||
 				depthTexProps.getHeight() != firstTexProps.getHeight() ||
-				depthTexProps.getMultisampleCount() != firstTexProps.getMultisampleCount())
+				depthMsCount != colorMsCount)
 			{
 				String errorInfo = "\nWidth: " + toString(depthTexProps.getWidth()) + "/" + toString(firstTexProps.getWidth());
 				errorInfo += "\nHeight: " + toString(depthTexProps.getHeight()) + "/" + toString(firstTexProps.getHeight());
-				errorInfo += "\nMultisample Count: " + toString(depthTexProps.getMultisampleCount()) + "/" + toString(firstTexProps.getMultisampleCount());
+				errorInfo += "\nMultisample Count: " + toString(depthMsCount) + "/" + toString(colorMsCount);
 
 				BS_EXCEPT(InvalidParametersException, "Provided texture and depth stencil buffer don't match!" + errorInfo);
 			}

+ 11 - 2
Source/BansheeCore/Source/BsRenderTexture.cpp

@@ -148,13 +148,22 @@ namespace BansheeEngine
 		const TextureProperties& colorProps = mColorSurface->getTexture()->getProperties();
 		const TextureProperties& depthProps = mDepthStencilSurface->getTexture()->getProperties();
 
+		UINT32 colorMsCount = colorProps.getMultisampleCount();
+		UINT32 depthMsCount = depthProps.getMultisampleCount();
+
+		if (colorMsCount == 0)
+			colorMsCount = 1;
+
+		if (depthMsCount == 0)
+			depthMsCount = 1;
+
 		if (colorProps.getWidth() != depthProps.getWidth() ||
 			colorProps.getHeight() != depthProps.getHeight() ||
-			colorProps.getMultisampleCount() != depthProps.getMultisampleCount())
+			colorMsCount != depthMsCount)
 		{
 			String errorInfo = "\nWidth: " + toString(colorProps.getWidth()) + "/" + toString(depthProps.getWidth());
 			errorInfo += "\nHeight: " + toString(colorProps.getHeight()) + "/" + toString(depthProps.getHeight());
-			errorInfo += "\nMultisample Count: " + toString(colorProps.getMultisampleCount()) + "/" + toString(depthProps.getMultisampleCount());
+			errorInfo += "\nMultisample Count: " + toString(colorMsCount) + "/" + toString(depthMsCount);
 
 			BS_EXCEPT(InvalidParametersException, "Provided texture and depth stencil buffer don't match!" + errorInfo);
 		}

+ 32 - 9
Source/BansheeCore/Source/BsSkeleton.cpp

@@ -2,6 +2,7 @@
 //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
 #include "BsSkeleton.h"
 #include "BsAnimationClip.h"
+#include "BsSkeletonMask.h"
 #include "BsSkeletonRTTI.h"
 
 namespace BansheeEngine
@@ -107,8 +108,8 @@ namespace BansheeEngine
 		return bs_shared_ptr<Skeleton>(rawPtr);
 	}
 
-	void Skeleton::getPose(Matrix4* pose, LocalSkeletonPose& localPose, const AnimationClip& clip, float time,
-		bool loop)
+	void Skeleton::getPose(Matrix4* pose, LocalSkeletonPose& localPose, const SkeletonMask& mask, 
+		const AnimationClip& clip, float time, bool loop)
 	{
 		bs_frame_mark();
 		{
@@ -129,6 +130,7 @@ namespace BansheeEngine
 			state.rotationCaches = rotationCache.data();
 			state.scaleCaches = scaleCache.data();
 			state.genericCaches = nullptr;
+			state.disabled = false;
 
 			AnimationStateLayer layer;
 			layer.index = 0;
@@ -138,13 +140,13 @@ namespace BansheeEngine
 
 			clip.getBoneMapping(*this, state.boneToCurveMapping);
 
-			getPose(pose, localPose, &layer, 1);
+			getPose(pose, localPose, mask, &layer, 1);
 		}
 		bs_frame_clear();
 	}
 
-	void Skeleton::getPose(Matrix4* pose, LocalSkeletonPose& localPose, const AnimationStateLayer* layers,
-		UINT32 numLayers)
+	void Skeleton::getPose(Matrix4* pose, LocalSkeletonPose& localPose, const SkeletonMask& mask, 
+		const AnimationStateLayer* layers, UINT32 numLayers)
 	{
 		// Note: If more performance is required this method could be optimized with vector instructions
 
@@ -176,6 +178,8 @@ namespace BansheeEngine
 			for (UINT32 j = 0; j < layer.numStates; j++)
 			{
 				const AnimationState& state = layer.states[j];
+				if (state.disabled)
+					continue;
 
 				float normWeight = state.weight * invLayerWeight;
 
@@ -185,8 +189,10 @@ namespace BansheeEngine
 
 				for (UINT32 k = 0; k < mNumBones; k++)
 				{
-					const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
+					if (!mask.isEnabled(k))
+						continue;
 
+					const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
 					if (mapping.position != (UINT32)-1)
 					{
 						const TAnimationCurve<Vector3>& curve = state.curves->position[mapping.position].curve;
@@ -196,8 +202,10 @@ namespace BansheeEngine
 
 				for (UINT32 k = 0; k < mNumBones; k++)
 				{
-					const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
+					if (!mask.isEnabled(k))
+						continue;
 
+					const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
 					if (mapping.scale != (UINT32)-1)
 					{
 						const TAnimationCurve<Vector3>& curve = state.curves->scale[mapping.scale].curve;
@@ -209,8 +217,10 @@ namespace BansheeEngine
 				{
 					for (UINT32 k = 0; k < mNumBones; k++)
 					{
-						const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
+						if (!mask.isEnabled(k))
+							continue;
 
+						const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
 						if (mapping.rotation != (UINT32)-1)
 						{
 							const TAnimationCurve<Quaternion>& curve = state.curves->rotation[mapping.rotation].curve;
@@ -226,8 +236,10 @@ namespace BansheeEngine
 				{
 					for (UINT32 k = 0; k < mNumBones; k++)
 					{
-						const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
+						if (!mask.isEnabled(k))
+							continue;
 
+						const AnimationCurveMapping& mapping = state.boneToCurveMapping[k];
 						if (mapping.rotation != (UINT32)-1)
 						{
 							const TAnimationCurve<Quaternion>& curve = state.curves->rotation[mapping.rotation].curve;
@@ -277,6 +289,17 @@ namespace BansheeEngine
 		bs_stack_free(isGlobal);
 	}
 
+	UINT32 Skeleton::getRootBoneIndex() const
+	{
+		for (UINT32 i = 0; i < mNumBones; i++)
+		{
+			if (mBoneInfo[i].parent == (UINT32)-1)
+				return i;
+		}
+
+		return (UINT32)-1;
+	}
+
 	SPtr<Skeleton> Skeleton::createEmpty()
 	{
 		Skeleton* rawPtr = new (bs_alloc<Skeleton>()) Skeleton();

+ 36 - 0
Source/BansheeCore/Source/BsSkeletonMask.cpp

@@ -0,0 +1,36 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+#include "BsSkeletonMask.h"
+#include "BsSkeleton.h"
+
+namespace BansheeEngine
+{
+	SkeletonMask::SkeletonMask(UINT32 numBones)
+		:mIsDisabled(numBones)
+	{ }
+
+	bool SkeletonMask::isEnabled(UINT32 boneIdx) const
+	{
+		if (boneIdx >= (UINT32)mIsDisabled.size())
+			return true;
+
+		return !mIsDisabled[boneIdx];
+	}
+
+	SkeletonMaskBuilder::SkeletonMaskBuilder(const SPtr<Skeleton>& skeleton)
+		:mSkeleton(skeleton), mMask(skeleton->getNumBones())
+	{ }
+
+	void SkeletonMaskBuilder::setBoneState(const String& name, bool enabled)
+	{
+		UINT32 numBones = mSkeleton->getNumBones();
+		for(UINT32 i = 0; i < numBones; i++)
+		{
+			if(mSkeleton->getBoneInfo(i).name == name)
+			{
+				mMask.mIsDisabled[i] = !enabled;
+				break;
+			}
+		}
+	}
+}

+ 1 - 1
Source/BansheeD3D11RenderAPI/Source/BsD3D11RenderWindow.cpp

@@ -605,7 +605,7 @@ namespace BansheeEngine
 		SAFE_RELEASE(pDXGIDevice);
 
 		if (FAILED(hr))
-			BS_EXCEPT(RenderingAPIException, "Unable to create swap chain");
+			BS_EXCEPT(RenderingAPIException, "Unable to create swap chain. Error code: " + toString(hr));
 
 		BS_INC_RENDER_STAT_CAT(ResCreated, RenderStatObject_SwapChain);
 	}

+ 1 - 1
Source/BansheeEditor/Include/BsDropDownWindow.h

@@ -55,7 +55,7 @@ namespace BansheeEngine
 		friend class DropDropWindowManager;
 
 		/**	Triggered when the user clicks outside of the drop down area. */
-		void dropDownFocusLost();
+		void dropDownFocusGained();
 
 		SPtr<RenderWindow> mRenderWindow;
 		HSceneObject mSceneObject;

+ 10 - 6
Source/BansheeEditor/Source/BsDropDownWindow.cpp

@@ -23,15 +23,14 @@ namespace BansheeEngine
 
 		mGUI = mSceneObject->addComponent<CGUIWidget>(camera);
 
-		mGUI->setDepth(0); // Needs to be in front of everything
+		mGUI->setDepth(1); // Needs to be in front of everything (except other drop down and tooltip elements)
 		mGUI->setSkin(BuiltinEditorResources::instance().getSkin());
 
 		mRootPanel = mGUI->getPanel()->addNewElement<GUIPanel>();
 		
 		GUIPanel* frontHitBoxPanel = mRootPanel->addNewElement<GUIPanel>(std::numeric_limits<INT16>::min());
 		mFrontHitBox = GUIDropDownHitBox::create(false, false);
-		mFrontHitBox->onFocusLost.connect(std::bind(&DropDownWindow::dropDownFocusLost, this));
-		mFrontHitBox->setFocus(true);
+		mFrontHitBox->onFocusGained.connect(std::bind(&DropDownWindow::dropDownFocusGained, this));
 		frontHitBoxPanel->addElement(mFrontHitBox);
 
 		GUIPanel* backHitBoxPanel = mRootPanel->addNewElement<GUIPanel>(std::numeric_limits<INT16>::max());
@@ -98,13 +97,18 @@ namespace BansheeEngine
 		DropDownAreaPlacement::HorzDir horzDir;
 		DropDownAreaPlacement::VertDir vertDir;
 		Rect2I placementBounds = dropDownPlacement.getOptimalBounds(width, height, availableBounds, horzDir, vertDir);
+		placementBounds.width = width;
+		placementBounds.height = height;
 
 		mRootPanel->setPosition(placementBounds.x, placementBounds.y);
 		mRootPanel->setWidth(width);
 		mRootPanel->setHeight(height);
 
-		mFrontHitBox->setBounds(Rect2I(placementBounds.x, placementBounds.y, width, height));
-		mBackHitBox->setBounds(Rect2I(placementBounds.x, placementBounds.y, width, height));
+		Vector<Rect2I> nonDropDownBounds;
+		availableBounds.cut(placementBounds, nonDropDownBounds);
+
+		mFrontHitBox->setBounds(nonDropDownBounds);
+		mBackHitBox->setBounds(placementBounds);
 
 		mWidth = width;
 		mHeight = height;
@@ -115,7 +119,7 @@ namespace BansheeEngine
 		DropDownWindowManager::instance().close();
 	}
 
-	void DropDownWindow::dropDownFocusLost()
+	void DropDownWindow::dropDownFocusGained()
 	{
 		close();
 	}

+ 3 - 3
Source/BansheeEngine/Include/BsGUIDropDownHitBox.h

@@ -61,13 +61,13 @@ namespace BansheeEngine
 		void updateClippedBounds() override;
 
 		/** @copydoc GUIElementContainer::_commandEvent */
-		virtual bool _commandEvent(const GUICommandEvent& ev) override;
+		bool _commandEvent(const GUICommandEvent& ev) override;
 
 		/** @copydoc GUIElementContainer::_mouseEvent */
-		virtual bool _mouseEvent(const GUIMouseEvent& ev) override;
+		bool _mouseEvent(const GUIMouseEvent& ev) override;
 
 		/** @copydoc GUIElementContainer::_isInBounds */
-		virtual bool _isInBounds(const Vector2I position) const override;
+		bool _isInBounds(const Vector2I position) const override;
 
 		Vector<Rect2I> mBounds;
 		bool mCaptureMouseOver;

+ 17 - 4
Source/BansheeEngine/Source/BsGUISliderHandle.cpp

@@ -327,14 +327,23 @@ namespace BansheeEngine
 
 						newHandleSize = std::max((INT32)mMinHandleSize, right - newLeft);
 						newLeft = right - newHandleSize;
-						newHandlePos = newLeft / (float)(maxSize - newHandleSize);
+
+						float scrollableSize = (float)(maxSize - newHandleSize);
+						if (scrollableSize > 0.0f)
+							newHandlePos = newLeft / scrollableSize;
+						else
+							newHandlePos = 0.0f;
 					}
 					else // Right resize
 					{
 						INT32 newRight = clickPosPx;
 						newHandleSize = std::max((INT32)mMinHandleSize, std::min(newRight, (INT32)maxSize) - left);
 
-						newHandlePos = left / (float)(maxSize - newHandleSize);
+						float scrollableSize = (float)(maxSize - newHandleSize);
+						if (scrollableSize > 0.0f)
+							newHandlePos = left / scrollableSize;
+						else
+							newHandlePos = 0.0f;
 					}
 
 					_setHandleSize(newHandleSize / (float)maxSize);
@@ -493,8 +502,12 @@ namespace BansheeEngine
 
 	void GUISliderHandle::setHandlePosPx(INT32 pos)
 	{
-		float maxScrollAmount = (float)getMaxSize() - getHandleSize();
-		_setHandlePos(pos / maxScrollAmount);
+		float scrollableSize = (float)getMaxSize() - getHandleSize();
+
+		if (scrollableSize > 0.0f)
+			_setHandlePos(pos / scrollableSize);
+		else
+			_setHandlePos(0.0f);
 	}
 
 	UINT32 GUISliderHandle::getMaxSize() const

+ 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"
 
@@ -446,79 +446,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, AnimationCurveFlag::ImportedCurve, bone.translation });
+				curves->rotation.push_back({ bone.node->name, AnimationCurveFlag::ImportedCurve, bone.rotation });
+				curves->scale.push_back({ bone.node->name, AnimationCurveFlag::ImportedCurve, bone.scale });
 			}
 
 			// See if any splits are required. We only split the first clip as it is assumed if FBX has multiple clips the
@@ -1564,27 +1494,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)
@@ -1609,7 +1530,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);
 						}
 					}
 				}
@@ -1624,268 +1546,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);
 	}
 }

+ 1 - 1
Source/MBansheeEditor/GUI/GUIListBoxField.cs

@@ -26,7 +26,7 @@ namespace BansheeEditor
         public event OnSelectionChangedDelegate OnSelectionChanged;
 
         /// <summary>
-        /// Index of the list box entry currently selected.
+        /// Index of the list box entry currently selected. Returns -1 if nothing is selected.
         /// </summary>
         public int Index
         {

+ 23 - 1
Source/MBansheeEditor/General/EditorApplication.cs

@@ -182,6 +182,14 @@ namespace BansheeEditor
             }
         }
 
+        /// <summary>
+        /// Returns an object that can be used for storing data that persists throughout the entire editor session.
+        /// </summary>
+        internal static EditorPersistentData PersistentData
+        {
+            get { return persistentData; }
+        }
+
         /// <summary>
         /// Returns the path where the script compiler is located at.
         /// </summary>
@@ -630,12 +638,17 @@ namespace BansheeEditor
         [ToolbarItem("Save Project", ToolbarIcon.SaveProject, "Save project", 1999)]
         public static void SaveProject()
         {
+            // Apply changes to any animation clips edited using the animation editor
+            foreach (var KVP in persistentData.dirtyAnimClips)
+                KVP.Value.SaveToClip();
+
+            // Save all dirty resources to disk
             foreach (var KVP in persistentData.dirtyResources)
             {
                 string resourceUUID = KVP.Key;
                 string path = ProjectLibrary.GetPath(resourceUUID);
                 if (!IsNative(path))
-                    continue; // Native resources can't be changed
+                    continue; // Imported resources can't be changed
 
                 Resource resource = ProjectLibrary.Load<Resource>(path);
 
@@ -643,6 +656,7 @@ namespace BansheeEditor
                     ProjectLibrary.Save(resource);
             }
 
+            persistentData.dirtyAnimClips.Clear();
             persistentData.dirtyResources.Clear();
             SetStatusProject(false);
 
@@ -711,6 +725,14 @@ namespace BansheeEditor
             persistentData.dirtyResources[resource.UUID] = true;
         }
 
+        /// <summary>
+        /// Marks the current project dirty (requires saving in order for changes not to be lost).
+        /// </summary>
+        public static void SetProjectDirty()
+        {
+            SetStatusProject(true);
+        }
+
         /// <summary>
         /// Marks the current scene as dirty.
         /// </summary>

+ 4 - 1
Source/MBansheeEditor/Windows/Inspector/EditorPersistentData.cs → Source/MBansheeEditor/General/EditorPersistentData.cs

@@ -5,7 +5,7 @@ using System.Collections.Generic;
 
 namespace BansheeEditor
 {
-    /** @addtogroup Inspector
+    /** @addtogroup General
      *  @{
      */
 
@@ -16,6 +16,9 @@ namespace BansheeEditor
     {
         [SerializeField]
         internal Dictionary<string, bool> dirtyResources = new Dictionary<string, bool>();
+
+        [SerializeField]
+        internal Dictionary<string, EditorAnimClipInfo> dirtyAnimClips = new Dictionary<string, EditorAnimClipInfo>();
     }
 
     /** @} */

+ 3 - 3
Source/MBansheeEditor/General/Program.cs

@@ -140,7 +140,7 @@ namespace BansheeEditor
         /// <summary>
         /// Attempts to save the current scene, and keeps retrying if failed or until user cancels.
         /// </summary>
-        static void TrySaveScene()
+        static void TrySaveSceneOnQuit()
         {
             Action success = () =>
             {
@@ -148,7 +148,7 @@ namespace BansheeEditor
                 EditorApplication.Quit();
             };
 
-            EditorApplication.SaveScene(success, TrySaveScene);
+            EditorApplication.SaveScene(success, TrySaveSceneOnQuit);
         }
 
         /// <summary>
@@ -161,7 +161,7 @@ namespace BansheeEditor
             (result) =>
             {
                 if (result == DialogBox.ResultType.Yes)
-                    TrySaveScene();
+                    TrySaveSceneOnQuit();
                 else if (result == DialogBox.ResultType.No)
                 {
                     EditorApplication.SaveProject();

+ 143 - 0
Source/MBansheeEditor/Inspectors/BoneInspector.cs

@@ -0,0 +1,143 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+using System.Collections;
+using System.Collections.Generic;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /** @addtogroup Inspectors
+     *  @{
+     */
+
+    /// <summary>
+    /// Renders an inspector for the <see cref="Bone"/> component.
+    /// </summary>
+    [CustomInspector(typeof(Bone))]
+    internal class BoneInspector : Inspector
+    {
+        private GUIListBoxField boneField;
+        private InspectableState modifyState;
+
+        private string selectedBoneName;
+
+        /// <inheritdoc/>
+        protected internal override void Initialize()
+        {
+            BuildGUI();
+        }
+
+        /// <inheritdoc/>
+        protected internal override InspectableState Refresh()
+        {
+            Bone bone = InspectedObject as Bone;
+            if (bone == null)
+                return InspectableState.NotModified;
+
+            if (selectedBoneName != bone.Name)
+            {
+                string[] boneNames = GetBoneNames(bone);
+
+                if (boneNames != null)
+                {
+                    for (int i = 0; i < boneNames.Length; i++)
+                    {
+                        if (bone.Name == boneNames[i])
+                        {
+                            boneField.Index = i;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            InspectableState oldState = modifyState;
+            if (modifyState.HasFlag(InspectableState.Modified))
+                modifyState = InspectableState.NotModified;
+
+            return oldState;
+        }
+
+        /// <summary>
+        /// Recreates all the GUI elements used by this inspector.
+        /// </summary>
+        private void BuildGUI()
+        {
+            Layout.Clear();
+
+            Bone bone = InspectedObject as Bone;
+            if (bone == null)
+                return;
+
+            string[] boneNames = GetBoneNames(bone);
+            if(boneNames == null)
+                boneNames = new string[0];
+
+            boneField = new GUIListBoxField(boneNames, false, new LocEdString("Bone"));
+
+            Layout.AddElement(boneField);
+
+            boneField.OnSelectionChanged += x =>
+            {
+                selectedBoneName = boneNames[x];
+                bone.Name = selectedBoneName;
+                
+                MarkAsModified();
+                ConfirmModify();
+            };
+        }
+
+        /// <summary>
+        /// Finds all available bones for the animation the provided bone is a part of.
+        /// </summary>
+        /// <param name="bone">Bone for which to return the parent skeleton's bones.</param>
+        /// <returns>List of bones if parent skeleton is found, or null.</returns>
+        private string[] GetBoneNames(Bone bone)
+        {
+            Animation animParent = null;
+
+            SceneObject currentSO = bone.SceneObject;
+            while (currentSO != null)
+            {
+                animParent = currentSO.GetComponent<Animation>();
+                if (animParent != null)
+                    break;
+
+                currentSO = currentSO.Parent;
+            }
+
+            if(animParent == null)
+                return null;
+
+            Renderable renderable = animParent.SceneObject.GetComponent<Renderable>();
+            if (renderable == null)
+                return null;
+
+            Mesh mesh = renderable.Mesh;
+            if (mesh == null)
+                return null;
+
+            //Skeleton skeleton = mesh.Skeleton;
+            return null;
+        }
+        
+        /// <summary>
+        /// Marks the contents of the inspector as modified.
+        /// </summary>
+        protected void MarkAsModified()
+        {
+            modifyState |= InspectableState.ModifyInProgress;
+        }
+
+        /// <summary>
+        /// Confirms any queued modifications.
+        /// </summary>
+        protected void ConfirmModify()
+        {
+            if (modifyState.HasFlag(InspectableState.ModifyInProgress))
+                modifyState |= InspectableState.Modified;
+        }
+    }
+
+    /** @} */
+}

+ 4 - 1
Source/MBansheeEditor/MBansheeEditor.csproj

@@ -45,12 +45,14 @@
     <Compile Include="Inspectors\AudioClipInspector.cs" />
     <Compile Include="Inspectors\AudioListenerInspector.cs" />
     <Compile Include="Inspectors\AudioSourceInspector.cs" />
+    <Compile Include="Inspectors\BoneInspector.cs" />
     <Compile Include="Inspectors\PostProcessSettingsInspector.cs" />
     <Compile Include="Utility\EdAnimationCurve.cs" />
     <Compile Include="Utility\SerializedDiff.cs" />
     <Compile Include="Utility\SerializedObject.cs" />
     <Compile Include="Windows\AboutBox.cs" />
     <Compile Include="Windows\AnimationWindow.cs" />
+    <Compile Include="Windows\Animation\EditorAnimInfo.cs" />
     <Compile Include="Windows\Animation\FieldSelectionWindow.cs" />
     <Compile Include="Windows\Animation\GUIAnimEvents.cs" />
     <Compile Include="Windows\Animation\GUIAnimFieldDisplay.cs" />
@@ -60,12 +62,13 @@
     <Compile Include="Windows\Animation\GUIGraphValues.cs" />
     <Compile Include="Windows\Animation\GUIGraphTicks.cs" />
     <Compile Include="Windows\Animation\GUIGraphTime.cs" />
+    <Compile Include="Windows\Animation\GUITimelineBase.cs" />
     <Compile Include="Windows\BrowseDialog.cs" />
     <Compile Include="Windows\Build\BuildManager.cs" />
     <Compile Include="Windows\Build\BuildWindow.cs" />
     <Compile Include="Script\CodeEditor.cs" />
     <Compile Include="Windows\ColorPicker.cs" />
-    <Compile Include="Windows\Inspector\EditorPersistentData.cs" />
+    <Compile Include="General\EditorPersistentData.cs" />
     <Compile Include="General\IGlobalShortcuts.cs" />
     <Compile Include="Inspectors\BoxColliderInspector.cs" />
     <Compile Include="Inspectors\CapsuleColliderInspector.cs" />

+ 125 - 12
Source/MBansheeEditor/Utility/EdAnimationCurve.cs

@@ -5,27 +5,72 @@ using BansheeEngine;
 
 namespace BansheeEditor
 {
+    /** @addtogroup AnimationEditor
+     *  @{
+     */
+
+    /// <summary>
+    /// Type of tangent on a keyframe in an animation curve.
+    /// </summary>
     internal enum TangentType
     {
         In = 1 << 0,
         Out = 1 << 1
     }
 
+    /// <summary>
+    /// Flags that are used for describing how are tangents calculated for a specific keyframe in an animation curve. 
+    /// Modes for "in" and "out" tangents can be combined.
+    /// </summary>
     [Flags]
     internal enum TangentMode
     {
-        Auto        = 0,
-        InAuto      = TangentType.In | 1 << 2,
-        InFree      = TangentType.In | 1 << 3,
-        InLinear    = TangentType.In | 1 << 4,
-        InStep      = TangentType.In | 1 << 5,
-        OutAuto     = TangentType.Out | 1 << 6,
-        OutFree     = TangentType.Out | 1 << 7,
-        OutLinear   = TangentType.Out | 1 << 8,
-        OutStep     = TangentType.Out | 1 << 9,
-        Free        = 1 << 10,
+        /// <summary>
+        /// Both tangents are calculated automatically based on the two surrounding keyframes.
+        /// </summary>
+        Auto = 0,
+        /// <summary>
+        /// Left tangent is calculated automatically based on the two surrounding keyframes.
+        /// </summary>
+        InAuto = TangentType.In | 1 << 2,
+        /// <summary>
+        /// Left tangent is manually adjusted by the user.
+        /// </summary>
+        InFree = TangentType.In | 1 << 3,
+        /// <summary>
+        /// Tangent is calculated automatically based on the previous keyframe.
+        /// </summary>
+        InLinear = TangentType.In | 1 << 4,
+        /// <summary>
+        /// Tangent is infinite, ensuring there is a instantaneus jump between previous and current keyframe value.
+        /// </summary>
+        InStep = TangentType.In | 1 << 5,
+        /// <summary>
+        /// Right tangents are calculated automatically based on the two surrounding keyframes.
+        /// </summary>
+        OutAuto = TangentType.Out | 1 << 6,
+        /// <summary>
+        /// Right tangent is manually adjusted by the user.
+        /// </summary>
+        OutFree = TangentType.Out | 1 << 7,
+        /// <summary>
+        /// Tangent is calculated automatically based on the next keyframe.
+        /// </summary>
+        OutLinear = TangentType.Out | 1 << 8,
+        /// <summary>
+        /// Tangent is infinite, ensuring there is a instantaneus jump between current and next keyframe value.
+        /// </summary>
+        OutStep = TangentType.Out | 1 << 9,
+        /// <summary>
+        /// Both tangents are manually adjusted by the user.
+        /// </summary>
+        Free = 1 << 10,
     }
 
+    /// <summary>
+    /// <see cref="AnimationCurve"/> wrapper for use in editor only. Allows easier manipulation of animation keyframes, and
+    /// also stores keyframe tangent modes which are not required for non-editor curves.
+    /// </summary>
     internal class EdAnimationCurve
     {
         private AnimationCurve native;
@@ -33,16 +78,27 @@ namespace BansheeEditor
         private KeyFrame[] keyFrames;
         private TangentMode[] tangentModes;
 
+        /// <summary>
+        /// Returns tangent modes for each keyframe. Array is guaranteed to be the same size as <see cref="KeyFrames"/>.
+        /// If modifying the array values, make sure to call <see cref="Apply"/> to save the changes on the curve.
+        /// </summary>
         public TangentMode[] TangentModes
         {
             get { return tangentModes; }
         }
 
+        /// <summary>
+        /// All keyframes belonging to the animation curve. If modifying the keyframe values, make sure to call 
+        /// <see cref="Apply"/> to save the changes on the curve.
+        /// </summary>
         public KeyFrame[] KeyFrames
         {
             get { return keyFrames; }
         }
 
+        /// <summary>
+        /// Creates a new animation curve with zero keyframes.
+        /// </summary>
         internal EdAnimationCurve()
         {
             keyFrames = new KeyFrame[0];
@@ -51,7 +107,13 @@ namespace BansheeEditor
             tangentModes = new TangentMode[0];
         }
 
-        // Tangent modes should match number of curve keyframes
+        /// <summary>
+        /// Creates a new editor animation curve using an existing animation curve as a basis.
+        /// </summary>
+        /// <param name="native">Animation curve to retrieve the keyframes from.</param>
+        /// <param name="tangentModes">A set of tangent modes for each keyframe. Should be the same size as the number
+        ///                            of keyframes in the provided animation. Can be null in which case all keyframes will
+        ///                            have tangents set to automatic.</param>
         internal EdAnimationCurve(AnimationCurve native, TangentMode[] tangentModes)
         {
             this.native = native;
@@ -80,11 +142,22 @@ namespace BansheeEditor
             return native.Evaluate(time, loop);
         }
 
+        /// <summary>
+        /// Adds a new keyframe to the animation curve. Keyframe will use the automatic tangent mode.
+        /// </summary>
+        /// <param name="time">Time at which to add the keyframe.</param>
+        /// <param name="value">Value of the keyframe.</param>
         internal void AddKeyframe(float time, float value)
         {
             AddKeyframe(time, value, TangentMode.Auto);
         }
 
+        /// <summary>
+        /// Adds a new keyframe to the animation curve.
+        /// </summary>
+        /// <param name="time">Time at which to add the keyframe.</param>
+        /// <param name="value">Value of the keyframe.</param>
+        /// <param name="tangentMode">Tangent mode of the keyframe.</param>
         internal void AddKeyframe(float time, float value, TangentMode tangentMode)
         {
             KeyFrame[] newKeyFrames = new KeyFrame[keyFrames.Length + 1];
@@ -123,6 +196,10 @@ namespace BansheeEditor
             keyFrames = newKeyFrames;
         }
 
+        /// <summary>
+        /// Removes a keyframe at the specified index.
+        /// </summary>
+        /// <param name="index">Index of the keyframe, referencing the <see cref="KeyFrames"/> array.</param>
         internal void RemoveKeyframe(int index)
         {
             if (index < 0 || index >= KeyFrames.Length)
@@ -145,7 +222,14 @@ namespace BansheeEditor
             keyFrames = newKeyFrames;
         }
 
-        // Updates key-frame value and returns new keyframe index
+        /// <summary>
+        /// Updates key-frame time and value. Since keyframes are ordered by time the index of the keyframe might change,
+        /// so a new index of the keyframe is returned by this method. 
+        /// </summary>
+        /// <param name="index">Index of the keyframe to update, referencing the <see cref="KeyFrames"/> array.</param>
+        /// <param name="time">Time to which to set the keyframe.</param>
+        /// <param name="value">Value of the keyframe.</param>
+        /// <returns>New index of the keyframe, referencing the <see cref="KeyFrames"/> array.</returns>
         internal int UpdateKeyframe(int index, float time, float value)
         {
             if (index < 0 || index >= keyFrames.Length)
@@ -198,6 +282,11 @@ namespace BansheeEditor
             return currentKeyIndex;
         }
 
+        /// <summary>
+        /// Changes the tangent mode of a keyframe at the specified index.
+        /// </summary>
+        /// <param name="index">Index of the keyframe to update, referencing the <see cref="KeyFrames"/> array.</param>
+        /// <param name="mode">New tangent mode of the keyframe.</param>
         internal void SetTangentMode(int index, TangentMode mode)
         {
             if (index < 0 || index >= tangentModes.Length)
@@ -206,6 +295,11 @@ namespace BansheeEditor
             tangentModes[index] = mode;
         }
 
+        /// <summary>
+        /// Converts a keyframe tangent (slope) value into a 2D normal vector.
+        /// </summary>
+        /// <param name="tangent">Keyframe tangent (slope).</param>
+        /// <returns>Normalized 2D vector pointing in the direction of the tangent.</returns>
         internal static Vector2 TangentToNormal(float tangent)
         {
             if(tangent == float.PositiveInfinity)
@@ -215,6 +309,11 @@ namespace BansheeEditor
             return Vector2.Normalize(normal);
         }
 
+        /// <summary>
+        /// Converts a 2D normal vector into a keyframe tangent (slope).
+        /// </summary>
+        /// <param name="normal">Normalized 2D vector pointing in the direction of the tangent.</param>
+        /// <returns>Keyframe tangent (slope).</returns>
         internal static float NormalToTangent(Vector2 normal)
         {
             // We know the X value must be one, use that to deduce pre-normalized length
@@ -224,6 +323,9 @@ namespace BansheeEditor
             return MathEx.Sqrt(length*length - 1) * MathEx.Sign(normal.y);
         }
 
+        /// <summary>
+        /// Applies the changes of the editor curve, to the actual underlying animation curve.
+        /// </summary>
         internal void Apply()
         {
             Array.Sort(keyFrames, (x, y) =>
@@ -235,6 +337,9 @@ namespace BansheeEditor
             native.KeyFrames = keyFrames;
         }
 
+        /// <summary>
+        /// Recalculates tangents for all keyframes using the keyframe values and set tangent modes.
+        /// </summary>
         private void UpdateTangents()
         {
             if (keyFrames.Length == 0)
@@ -379,6 +484,9 @@ namespace BansheeEditor
         }
     }
 
+    /// <summary>
+    /// Structure containing a reference to a keyframe as a curve index, and a keyframe index within that curve.
+    /// </summary>
     internal struct KeyframeRef
     {
         public KeyframeRef(int curveIdx, int keyIdx)
@@ -391,6 +499,9 @@ namespace BansheeEditor
         public int keyIdx;
     }
 
+    /// <summary>
+    /// Structure containing a reference to a keyframe tangent, as a keyframe reference and type of the tangent.
+    /// </summary>
     internal struct TangentRef
     {
         public TangentRef(KeyframeRef keyframeRef, TangentType type)
@@ -402,4 +513,6 @@ namespace BansheeEditor
         public KeyframeRef keyframeRef;
         public TangentType type;
     }
+
+    /** @} */
 }

+ 459 - 0
Source/MBansheeEditor/Windows/Animation/EditorAnimInfo.cs

@@ -0,0 +1,459 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+using System;
+using System.Collections.Generic;
+using System.Text;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /** @addtogroup AnimationEditor
+     *  @{
+     */
+
+
+    /// <summary>
+    /// A set of animation curves for a field of a certain type.
+    /// </summary>
+    internal struct FieldAnimCurves
+    {
+        public SerializableProperty.FieldType type;
+        public EdAnimationCurve[] curves;
+    }
+
+    /// <summary>
+    /// Stores tangent modes for an 3D vector animation curve (one mode for each keyframe).
+    /// </summary>
+    [SerializeObject]
+    internal class EditorVector3CurveTangents
+    {
+        public string name;
+        public TangentMode[] tangentsX;
+        public TangentMode[] tangentsY;
+        public TangentMode[] tangentsZ;
+    }
+
+    /// <summary>
+    /// Stores tangent modes for an float animation curve (one mode for each keyframe).
+    /// </summary>
+    [SerializeObject]
+    internal class EditorFloatCurveTangents
+    {
+        public string name;
+        public TangentMode[] tangents;
+    }
+
+    /// <summary>
+    /// Stores tangent information for all curves in an animation clip.
+    /// </summary>
+    [SerializeObject]
+    internal class EditorAnimClipTangents
+    {
+        public EditorVector3CurveTangents[] positionCurves;
+        public EditorVector3CurveTangents[] rotationCurves;
+        public EditorVector3CurveTangents[] scaleCurves;
+        public EditorFloatCurveTangents[] floatCurves;
+    }
+
+    /// <summary>
+    /// Stores animation clip data for clips that are currently being edited.
+    /// </summary>
+    internal class EditorAnimClipInfo
+    {
+        public AnimationClip clip;
+        public Dictionary<string, FieldAnimCurves> curves = new Dictionary<string, FieldAnimCurves>();
+        public AnimationEvent[] events = new AnimationEvent[0];
+
+        /// <summary>
+        /// Loads curve and event information from the provided clip, and creates a new instance of this object containing
+        /// the required data for editing the source clip in the animation editor.
+        /// </summary>
+        /// <param name="clip">Clip to load.</param>
+        /// <returns>Editor specific editable information about an animation clip.</returns>
+        public static EditorAnimClipInfo Create(AnimationClip clip)
+        {
+            EditorAnimClipInfo clipInfo = new EditorAnimClipInfo();
+            clipInfo.clip = clip;
+
+            AnimationCurves clipCurves = clip.Curves;
+            EditorAnimClipTangents editorCurveData = null;
+
+            string resourcePath = ProjectLibrary.GetPath(clip);
+            if (!string.IsNullOrEmpty(resourcePath))
+            {
+                LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
+                string clipName = PathEx.GetTail(resourcePath);
+
+                if (entry != null && entry.Type == LibraryEntryType.File)
+                {
+                    FileEntry fileEntry = (FileEntry)entry;
+                    ResourceMeta[] metas = fileEntry.ResourceMetas;
+
+                    for (int i = 0; i < metas.Length; i++)
+                    {
+                        if (clipName == metas[i].SubresourceName)
+                        {
+                            editorCurveData = metas[i].EditorData as EditorAnimClipTangents;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            if (editorCurveData == null)
+                editorCurveData = new EditorAnimClipTangents();
+
+            Action<NamedVector3Curve[], EditorVector3CurveTangents[], string> loadVector3Curve =
+                (curves, tangents, subPath) =>
+                {
+                    foreach (var curveEntry in curves)
+                    {
+                        TangentMode[] tangentsX = null;
+                        TangentMode[] tangentsY = null;
+                        TangentMode[] tangentsZ = null;
+                        foreach (var tangentEntry in tangents)
+                        {
+                            if (tangentEntry.name == curveEntry.Name)
+                            {
+                                tangentsX = tangentEntry.tangentsX;
+                                tangentsY = tangentEntry.tangentsY;
+                                tangentsZ = tangentEntry.tangentsZ;
+                                break;
+                            }
+                        }
+
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
+                        fieldCurves.type = SerializableProperty.FieldType.Vector3;
+                        fieldCurves.curves = new EdAnimationCurve[3];
+
+                        fieldCurves.curves[0] = new EdAnimationCurve(curveEntry.X, tangentsX);
+                        fieldCurves.curves[1] = new EdAnimationCurve(curveEntry.Y, tangentsY);
+                        fieldCurves.curves[2] = new EdAnimationCurve(curveEntry.Z, tangentsZ);
+
+                        string curvePath = curveEntry.Name.TrimEnd('/') + subPath;
+                        clipInfo.curves[curvePath] = fieldCurves;
+                    }
+                };
+
+            loadVector3Curve(clipCurves.PositionCurves, editorCurveData.positionCurves, "/Position");
+            loadVector3Curve(clipCurves.RotationCurves, editorCurveData.rotationCurves, "/Rotation");
+            loadVector3Curve(clipCurves.ScaleCurves, editorCurveData.scaleCurves, "/Scale");
+
+            // Find which individual float curves belong to the same field
+            Dictionary<string, Tuple<int, int, bool>[]> floatCurveMapping = new Dictionary<string, Tuple<int, int, bool>[]>();
+            {
+                int curveIdx = 0;
+                foreach (var curveEntry in clipCurves.FloatCurves)
+                {
+                    string path = curveEntry.Name;
+                    string pathNoSuffix = null;
+
+                    string pathSuffix;
+                    if (path.Length >= 2)
+                    {
+                        pathSuffix = path.Substring(path.Length - 2, 2);
+                        pathNoSuffix = path.Substring(0, path.Length - 2);
+                    }
+                    else
+                        pathSuffix = "";
+
+                    int tangentIdx = -1;
+                    int currentTangentIdx = 0;
+                    foreach (var tangentEntry in editorCurveData.floatCurves)
+                    {
+                        if (tangentEntry.name == curveEntry.Name)
+                        {
+                            tangentIdx = currentTangentIdx;
+                            break;
+                        }
+
+                        currentTangentIdx++;
+                    }
+
+                    Animation.PropertySuffixInfo suffixInfo;
+                    if (Animation.PropertySuffixInfos.TryGetValue(pathSuffix, out suffixInfo))
+                    {
+                        Tuple<int, int, bool>[] curveInfo;
+                        if (!floatCurveMapping.TryGetValue(pathNoSuffix, out curveInfo))
+                            curveInfo = new Tuple<int, int, bool>[4];
+
+                        curveInfo[suffixInfo.elementIdx] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
+                        floatCurveMapping[pathNoSuffix] = curveInfo;
+                    }
+                    else
+                    {
+                        Tuple<int, int, bool>[] curveInfo = new Tuple<int, int, bool>[4];
+                        curveInfo[0] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector);
+
+                        floatCurveMapping[path] = curveInfo;
+                    }
+
+                    curveIdx++;
+                }
+            }
+
+            foreach (var KVP in floatCurveMapping)
+            {
+                int numCurves = 0;
+                for (int i = 0; i < 4; i++)
+                {
+                    if (KVP.Value[i] == null)
+                        continue;
+
+                    numCurves++;
+                }
+
+                if (numCurves == 0)
+                    continue; // Invalid curve
+
+                FieldAnimCurves fieldCurves = new FieldAnimCurves();
+
+                // Deduce type (note that all single value types are assumed to be float even if their source type is int or bool)
+                if (numCurves == 1)
+                    fieldCurves.type = SerializableProperty.FieldType.Float;
+                else if (numCurves == 2)
+                    fieldCurves.type = SerializableProperty.FieldType.Vector2;
+                else if (numCurves == 3)
+                    fieldCurves.type = SerializableProperty.FieldType.Vector3;
+                else // 4 curves
+                {
+                    bool isVector = KVP.Value[0].Item3;
+                    if (isVector)
+                        fieldCurves.type = SerializableProperty.FieldType.Vector4;
+                    else
+                        fieldCurves.type = SerializableProperty.FieldType.Color;
+                }
+
+                fieldCurves.curves = new EdAnimationCurve[numCurves];
+
+                for (int i = 0; i < numCurves; i++)
+                {
+                    int curveIdx = KVP.Value[i].Item1;
+                    int tangentIdx = KVP.Value[i].Item2;
+
+                    TangentMode[] tangents = null;
+                    if (tangentIdx != -1)
+                        tangents = editorCurveData.floatCurves[tangentIdx].tangents;
+
+                    fieldCurves.curves[i] = new EdAnimationCurve(clipCurves.FloatCurves[curveIdx].Curve, tangents);
+                }
+
+                string curvePath = KVP.Key;
+                clipInfo.curves[curvePath] = fieldCurves;
+            }
+
+            // Add events
+            clipInfo.events = clip.Events;
+            return clipInfo;
+        }
+
+        /// <summary>
+        /// Checks is the specified animation clip is imported from an external file, or created within the editor.
+        /// </summary>
+        /// <param name="clip">Clip to check.</param>
+        /// <returns>True if the clip is imported from an external file (e.g. FBX file), or false if the clip is a native
+        ///          resource created within the editor.</returns>
+        public static bool IsClipImported(AnimationClip clip)
+        {
+            string resourcePath = ProjectLibrary.GetPath(clip);
+            return ProjectLibrary.IsSubresource(resourcePath);
+        }
+
+        /// <summary>
+        /// Saves the animation curves and events stored in this object, into the associated animation clip resource.
+        /// Relevant animation clip resource must already be created and exist in the project library.
+        /// </summary>
+        public void SaveToClip()
+        {
+            bool clipIsImported = IsClipImported(clip);
+
+            if (!clipIsImported)
+            {
+                List<NamedVector3Curve> positionCurves = new List<NamedVector3Curve>();
+                List<NamedVector3Curve> rotationCurves = new List<NamedVector3Curve>();
+                List<NamedVector3Curve> scaleCurves = new List<NamedVector3Curve>();
+                List<NamedFloatCurve> floatCurves = new List<NamedFloatCurve>();
+
+                List<EditorVector3CurveTangents> positionTangents = new List<EditorVector3CurveTangents>();
+                List<EditorVector3CurveTangents> rotationTangents = new List<EditorVector3CurveTangents>();
+                List<EditorVector3CurveTangents> scaleTangents = new List<EditorVector3CurveTangents>();
+                List<EditorFloatCurveTangents> floatTangents = new List<EditorFloatCurveTangents>();
+
+                foreach (var kvp in curves)
+                {
+                    string[] pathEntries = kvp.Key.Split('/');
+                    if (pathEntries.Length == 0)
+                        continue;
+
+                    string lastEntry = pathEntries[pathEntries.Length - 1];
+
+                    if (lastEntry == "Position" || lastEntry == "Rotation" || lastEntry == "Scale")
+                    {
+                        StringBuilder sb = new StringBuilder();
+                        for (int i = 0; i < pathEntries.Length - 2; i++)
+                            sb.Append(pathEntries[i] + "/");
+
+                        if (pathEntries.Length > 1)
+                            sb.Append(pathEntries[pathEntries.Length - 2]);
+
+                        string curvePath = sb.ToString();
+
+                        NamedVector3Curve curve = new NamedVector3Curve(curvePath,
+                            new AnimationCurve(kvp.Value.curves[0].KeyFrames),
+                            new AnimationCurve(kvp.Value.curves[1].KeyFrames),
+                            new AnimationCurve(kvp.Value.curves[2].KeyFrames));
+
+                        EditorVector3CurveTangents tangents = new EditorVector3CurveTangents();
+                        tangents.name = curvePath;
+                        tangents.tangentsX = kvp.Value.curves[0].TangentModes;
+                        tangents.tangentsY = kvp.Value.curves[1].TangentModes;
+                        tangents.tangentsZ = kvp.Value.curves[2].TangentModes;
+
+                        if (lastEntry == "Position")
+                        {
+                            positionCurves.Add(curve);
+                            positionTangents.Add(tangents);
+                        }
+                        else if (lastEntry == "Rotation")
+                        {
+                            rotationCurves.Add(curve);
+                            rotationTangents.Add(tangents);
+                        }
+                        else if (lastEntry == "Scale")
+                        {
+                            scaleCurves.Add(curve);
+                            scaleTangents.Add(tangents);
+                        }
+                    }
+                    else
+                    {
+                        Action<int, string> addCurve = (idx, subPath) =>
+                        {
+                            string path = kvp.Key + subPath;
+
+                            NamedFloatCurve curve = new NamedFloatCurve(path,
+                            new AnimationCurve(kvp.Value.curves[idx].KeyFrames));
+
+                            EditorFloatCurveTangents tangents = new EditorFloatCurveTangents();
+                            tangents.name = path;
+                            tangents.tangents = kvp.Value.curves[idx].TangentModes;
+
+                            floatCurves.Add(curve);
+                            floatTangents.Add(tangents);
+                        };
+
+                        switch (kvp.Value.type)
+                        {
+                            case SerializableProperty.FieldType.Vector2:
+                                addCurve(0, ".x");
+                                addCurve(1, ".y");
+                                break;
+                            case SerializableProperty.FieldType.Vector3:
+                                addCurve(0, ".x");
+                                addCurve(1, ".y");
+                                addCurve(2, ".z");
+                                break;
+                            case SerializableProperty.FieldType.Vector4:
+                                addCurve(0, ".x");
+                                addCurve(1, ".y");
+                                addCurve(2, ".z");
+                                addCurve(3, ".w");
+                                break;
+                            case SerializableProperty.FieldType.Color:
+                                addCurve(0, ".r");
+                                addCurve(1, ".g");
+                                addCurve(2, ".b");
+                                addCurve(3, ".a");
+                                break;
+                            case SerializableProperty.FieldType.Bool:
+                            case SerializableProperty.FieldType.Int:
+                            case SerializableProperty.FieldType.Float:
+                                addCurve(0, "");
+                                break;
+                        }
+                    }
+                }
+
+                AnimationCurves newClipCurves = new AnimationCurves();
+                newClipCurves.PositionCurves = positionCurves.ToArray();
+                newClipCurves.RotationCurves = rotationCurves.ToArray();
+                newClipCurves.ScaleCurves = scaleCurves.ToArray();
+                newClipCurves.FloatCurves = floatCurves.ToArray();
+
+                clip.Curves = newClipCurves;
+                clip.Events = events;
+
+                string resourcePath = ProjectLibrary.GetPath(clip);
+                ProjectLibrary.Save(clip);
+
+                // Save tangents for editor only use
+                LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
+                string clipName = PathEx.GetTail(resourcePath);
+
+                if (entry != null && entry.Type == LibraryEntryType.File)
+                {
+                    FileEntry fileEntry = (FileEntry)entry;
+                    ResourceMeta[] metas = fileEntry.ResourceMetas;
+
+                    for (int i = 0; i < metas.Length; i++)
+                    {
+                        if (clipName == metas[i].SubresourceName)
+                        {
+                            EditorAnimClipTangents newCurveData = new EditorAnimClipTangents();
+                            newCurveData.positionCurves = positionTangents.ToArray();
+                            newCurveData.rotationCurves = rotationTangents.ToArray();
+                            newCurveData.scaleCurves = scaleTangents.ToArray();
+                            newCurveData.floatCurves = floatTangents.ToArray();
+
+                            ProjectLibrary.SetEditorData(resourcePath, newCurveData);
+                            break;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                string resourcePath = ProjectLibrary.GetPath(clip);
+                LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath);
+
+                if (entry != null && entry.Type == LibraryEntryType.File)
+                {
+                    FileEntry fileEntry = (FileEntry)entry;
+                    MeshImportOptions meshImportOptions = (MeshImportOptions)fileEntry.Options;
+
+                    string clipName = PathEx.GetTail(resourcePath);
+
+                    List<ImportedAnimationEvents> newEvents = new List<ImportedAnimationEvents>();
+                    newEvents.AddRange(meshImportOptions.AnimationEvents);
+
+                    bool isExisting = false;
+                    for (int i = 0; i < newEvents.Count; i++)
+                    {
+                        if (newEvents[i].name == clipName)
+                        {
+                            newEvents[i].events = events;
+                            isExisting = true;
+                            break;
+                        }
+                    }
+
+                    if (!isExisting)
+                    {
+                        ImportedAnimationEvents newEntry = new ImportedAnimationEvents();
+                        newEntry.name = clipName;
+                        newEntry.events = events;
+
+                        newEvents.Add(newEntry);
+                    }
+
+                    meshImportOptions.AnimationEvents = newEvents.ToArray();
+
+                    ProjectLibrary.Reimport(resourcePath, meshImportOptions, true);
+                }
+            }
+        }
+    }
+
+    /** @} */
+}
+          

+ 47 - 73
Source/MBansheeEditor/Windows/Animation/GUIAnimEvents.cs

@@ -9,49 +9,52 @@ namespace BansheeEditor
      *  @{
      */
 
-    public class GUIAnimEvents
+    /// <summary>
+    /// Renders a list of animation events in a form of a timeline. User can set the range of the times to display,
+    /// as well as its physical dimensions.
+    /// </summary>
+    public class GUIAnimEvents : GUITimelineBase
     {
         private const int EVENT_HALF_WIDTH = 2;
 
-        private float rangeLength = 60.0f;
-        private float rangeOffset = 0.0f;
-        private int fps = 1;
-
-        private GUICanvas canvas;
-        private int width;
-        private int height;
-        private int drawableWidth;
-
         private AnimationEvent[] events = new AnimationEvent[0];
         private bool[] selectedEvents = new bool[0];
 
+        /// <summary>
+        /// Constructs a new events timeline and adds it to the specified layout.
+        /// </summary>
+        /// <param name="layout">Layout to add the events GUI to.</param>
+        /// <param name="width">Width of the GUI element in pixels.</param>
+        /// <param name="height">Height of the GUI element in pixels.</param>
         public GUIAnimEvents(GUILayout layout, int width, int height)
-        {
-            canvas = new GUICanvas();
-            layout.AddElement(canvas);
+            :base(layout, width, height)
+        { }
 
-            SetSize(width, height);
-        }
-
-        public bool FindEvent(Vector2I windowCoords, out int eventIdx)
+        /// <summary>
+        /// Attempts to find an event under the provided coordinates.
+        /// </summary>
+        /// <param name="pixelCoords">Coordinates relative to the layout the GUI element is on.</param>
+        /// <param name="eventIdx">Index of the event that was clicked on. Index references the events array as provided
+        ///                        to <see cref="SetEvents"/>. Only valid if method returns true.</param>
+        /// <returns>True if an event was found under the coordinates, false otherwise.</returns>
+        public bool FindEvent(Vector2I pixelCoords, out int eventIdx)
         {
             Rect2I bounds = canvas.Bounds;
 
-            if (windowCoords.x < (bounds.x + GUIGraphTime.PADDING) || windowCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
-                windowCoords.y < bounds.y || windowCoords.y >= (bounds.y + bounds.height))
+            if (pixelCoords.x < (bounds.x + PADDING) || pixelCoords.x >= (bounds.x + bounds.width - PADDING) ||
+                pixelCoords.y < bounds.y || pixelCoords.y >= (bounds.y + bounds.height))
             {
                 eventIdx = -1;
                 return false;
             }
 
-            Vector2I relativeCoords = windowCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
+            Vector2I relativeCoords = pixelCoords - new Vector2I(bounds.x, bounds.y);
             for (int i = 0; i < events.Length; i++)
             {
                 AnimationEvent evnt = events[i];
 
-                int xPos = (int)(((evnt.Time - rangeOffset) / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
-
-                if (relativeCoords.x >= (xPos - EVENT_HALF_WIDTH) || relativeCoords.y >= (xPos + EVENT_HALF_WIDTH))
+                int xPos = (int)(((evnt.Time - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
+                if (relativeCoords.x >= (xPos - EVENT_HALF_WIDTH) && relativeCoords.x <= (xPos + EVENT_HALF_WIDTH))
                 {
                     eventIdx = i;
                     return true;
@@ -61,33 +64,13 @@ namespace BansheeEditor
             eventIdx = -1;
             return false;
         }
-
-        public void SetSize(int width, int height)
-        {
-            this.width = width;
-            this.height = height;
-
-            canvas.SetWidth(width);
-            canvas.SetHeight(height);
-
-            drawableWidth = Math.Max(0, width - GUIGraphTime.PADDING * 2);
-        }
-
-        public void SetRange(float length)
-        {
-            rangeLength = Math.Max(0.0f, length);
-        }
-
-        public void SetOffset(float offset)
-        {
-            rangeOffset = offset;
-        }
-
-        public void SetFPS(int fps)
-        {
-            this.fps = Math.Max(1, fps);
-        }
-
+        
+        /// <summary>
+        /// Changes the set of displayed animation events.
+        /// </summary>
+        /// <param name="events">Events to display on the timeline.</param>
+        /// <param name="selected">Array of the same size as the <paramref name="events"/> array, determining which
+        ///                        events should be displayed as selected.</param>
         public void SetEvents(AnimationEvent[] events, bool[] selected)
         {
             int numEvents;
@@ -105,14 +88,19 @@ namespace BansheeEditor
                 Array.Copy(selected, selectedEvents, MathEx.Min(numEvents, selected.Length));
         }
 
+        /// <summary>
+        /// Draws a marker for a single event.
+        /// </summary>
+        /// <param name="t">Time to draw the marker at.</param>
+        /// <param name="selected">If true the marker will be drawn as selected.</param>
         private void DrawEventMarker(float t, bool selected)
         {
-            int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
+            int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
 
-            Vector2I a = new Vector2I(xPos - EVENT_HALF_WIDTH, height - 1);
-            Vector2I b = new Vector2I(xPos, 0);
+            Vector2I a = new Vector2I(xPos - EVENT_HALF_WIDTH, 0);
+            Vector2I b = new Vector2I(xPos + EVENT_HALF_WIDTH, 0);
             Vector2I c = new Vector2I(xPos + EVENT_HALF_WIDTH, height - 1);
-            Vector2I d = new Vector2I(xPos, 0);
+            Vector2I d = new Vector2I(xPos - EVENT_HALF_WIDTH, height - 1);
 
             // Draw square shape
             Vector2I[] linePoints = { a, b, c, d, a };
@@ -122,25 +110,9 @@ namespace BansheeEditor
             canvas.DrawTriangleStrip(trianglePoints, Color.White, 101);
             canvas.DrawPolyLine(linePoints, outerColor, 100);
         }
-
-        private float GetRange(bool padding = false)
-        {
-            float spf = 1.0f / fps;
-
-            float range = rangeLength;
-            if (padding)
-            {
-                float lengthPerPixel = rangeLength / drawableWidth;
-                range += lengthPerPixel * GUIGraphTime.PADDING;
-            }
-
-            return ((int)range / spf) * spf;
-        }
-
-        /// <summary>
-        /// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
-        /// </summary>
-        public void Rebuild()
+        
+        /// <inheritdoc/>
+        public override void Rebuild()
         {
             canvas.Clear();
 
@@ -160,6 +132,8 @@ namespace BansheeEditor
 
                 DrawEventMarker(t, selectedEvents[i]);
             }
+
+            DrawFrameMarker();
         }
     }
 

+ 29 - 130
Source/MBansheeEditor/Windows/Animation/GUIAnimFieldDisplay.cs

@@ -110,120 +110,6 @@ namespace BansheeEditor
             }
         }
 
-        public static SerializableProperty FindProperty(SceneObject root, string path)
-        {
-            if (string.IsNullOrEmpty(path) || root == null)
-                return null;
-
-            string trimmedPath = path.Trim('/');
-            string[] entries = trimmedPath.Split('/');
-
-            // Find scene object referenced by the path
-            SceneObject so = null;
-            int pathIdx = 0;
-            for (; pathIdx < entries.Length; pathIdx++)
-            {
-                string entry = entries[pathIdx];
-
-                if (string.IsNullOrEmpty(entry))
-                    continue;
-
-                // Not a scene object, break
-                if (entry[0] != '!')
-                    break;
-
-                if (so == null)
-                    so = root;
-                else
-                {
-                    string childName = entry.Substring(1, entry.Length - 1);
-                    so = so.FindChild(childName);
-
-                    if (so == null)
-                        break;
-                }
-            }
-
-            // Cannot find scene object with specified hierarchy & name
-            if (so == null)
-                return null;
-
-            if (pathIdx >= entries.Length)
-                return null;
-
-            // If path is referencing a component, find it
-            Component component = null;
-            {
-                string entry = entries[pathIdx];
-                if (entry[0] == ':')
-                {
-                    string componentName = entry.Substring(1, entry.Length - 1);
-
-                    Component[] components = so.GetComponents();
-                    component = Array.Find(components, x => x.GetType().Name == componentName);
-
-                    // Cannot find component with specified type
-                    if (component == null)
-                        return null;
-                }
-            }
-
-            // Look for a field within a component
-            if (component != null)
-            {
-                pathIdx++;
-                if (pathIdx >= entries.Length)
-                    return null;
-
-                SerializableObject componentObj = new SerializableObject(component);
-
-                StringBuilder pathBuilder = new StringBuilder();
-                for(; pathIdx < entries.Length; pathIdx++)
-                    pathBuilder.Append(entries[pathIdx] + "/");
-
-                return componentObj.FindProperty(pathBuilder.ToString());
-            }
-            else // Field is one of the builtin ones on the SceneObject itself
-            {
-                if ((pathIdx + 1) < entries.Length)
-                    return null;
-
-                string entry = entries[pathIdx];
-                if (entry == "Position")
-                {
-                    SerializableProperty property = new SerializableProperty(
-                        SerializableProperty.FieldType.Vector3,
-                        typeof(Vector3),
-                        () => so.LocalPosition, 
-                        (x) => so.LocalPosition = (Vector3)x);
-
-                    return property;
-                }
-                else if (entry == "Rotation")
-                {
-                    SerializableProperty property = new SerializableProperty(
-                        SerializableProperty.FieldType.Vector3,
-                        typeof(Vector3),
-                        () => so.LocalRotation.ToEuler(), 
-                        (x) => so.LocalRotation = Quaternion.FromEuler((Vector3)x));
-
-                    return property;
-                }
-                else if (entry == "Scale")
-                {
-                    SerializableProperty property = new SerializableProperty(
-                        SerializableProperty.FieldType.Vector3,
-                        typeof(Vector3),
-                        () => so.LocalScale, 
-                        (x) => so.LocalScale = (Vector3)x);
-
-                    return property;
-                }
-
-                return null;
-            }
-        }
-
         private void Rebuild()
         {
             scrollArea.Layout.Clear();
@@ -258,7 +144,12 @@ namespace BansheeEditor
             fields = new GUIAnimFieldEntry[paths.Count];
             for (int i = 0; i < paths.Count; i++)
             {
-                SerializableProperty property = FindProperty(root, paths[i]);
+                if (string.IsNullOrEmpty(paths[i]))
+                    continue;
+
+                string pathSuffix;
+                SerializableProperty property = Animation.FindProperty(root, paths[i], out pathSuffix);
+
                 if (property != null)
                 {
                     switch (property.Type)
@@ -395,7 +286,7 @@ namespace BansheeEditor
             string trimmedPath = path.Trim('/');
             GetNames(trimmedPath, shortName, out soName, out compName, out propertyPath);
 
-            if (soName == null || propertyPath == null)
+            if (propertyPath == null)
                 return "";
 
             if (shortName)
@@ -408,10 +299,20 @@ namespace BansheeEditor
                 else
                     truncatedPropPath = propertyPath;
 
-                if (compName != null)
-                    return soName + "(" + compName + ") - " + truncatedPropPath;
+                if (soName != null)
+                {
+                    if (compName != null)
+                        return soName + "(" + compName + ") - " + truncatedPropPath;
+                    else
+                        return soName + " - " + truncatedPropPath;
+                }
                 else
-                    return soName + " - " + truncatedPropPath;
+                {
+                    if (compName != null)
+                        return "(" + compName + ") - " + truncatedPropPath;
+                    else
+                        return truncatedPropPath;
+                }
             }
         }
 
@@ -421,6 +322,7 @@ namespace BansheeEditor
 
             // Find name of the last scene object in the path
             int pathIdx = 0;
+            soName = null;
             for (; pathIdx < entries.Length; pathIdx++)
             {
                 string entry = entries[pathIdx];
@@ -430,20 +332,17 @@ namespace BansheeEditor
 
                 // Not a scene object, break
                 if (entry[0] != '!')
-                    break;
-            }
-
-            if (pathIdx == 0)
-            {
-                soName = null;
-                compName = null;
-                propertyPath = null;
+                {
+                    if (pathIdx > 0)
+                    {
+                        string prevEntry = entries[pathIdx - 1];
+                        soName = prevEntry.Substring(1, prevEntry.Length - 1);
+                    }
 
-                return;
+                    break;
+                }
             }
 
-            soName = entries[pathIdx - 1].Substring(1, entries[pathIdx - 1].Length - 1);
-
             if (pathIdx >= entries.Length)
             {
                 compName = null;

+ 18 - 97
Source/MBansheeEditor/Windows/Animation/GUICurveDrawing.cs

@@ -14,7 +14,7 @@ namespace BansheeEditor
     /// Draws one or multiple curves over the specified physical area. User can specify horizontal and vertical range to
     /// display, as well as physical size of the GUI area.
     /// </summary>
-    internal class GUICurveDrawing
+    internal class GUICurveDrawing : GUITimelineBase
     {
         private const int LINE_SPLIT_WIDTH = 2;
         private const int TANGENT_LINE_DISTANCE = 30;
@@ -24,16 +24,9 @@ namespace BansheeEditor
         private EdAnimationCurve[] curves;
         private bool[][] selectedKeyframes;
 
-        private int width;
-        private int height;
-        private float xRange = 60.0f;
         private float yRange = 20.0f;
-        private Vector2 offset;
-        private int fps = 1;
-        private int markedFrameIdx = 0;
+        private float yOffset;
 
-        private int drawableWidth;
-        private GUICanvas canvas;
         private GUIGraphTicks tickHandler;
 
         /// <summary>
@@ -44,18 +37,12 @@ namespace BansheeEditor
         /// <param name="height">Height of the element in pixels.</param>
         /// <param name="curves">Initial set of curves to display. </param>
         public GUICurveDrawing(GUILayout layout, int width, int height, EdAnimationCurve[] curves)
+            :base(layout, width, height)
         {
-            canvas = new GUICanvas();
-            layout.AddElement(canvas);
-
             tickHandler = new GUIGraphTicks(GUITickStepType.Time);
-
             this.curves = curves;
             
-            SetSize(width, height);
             ClearSelectedKeyframes(); // Makes sure the array is initialized
-
-            Rebuild();
         }
 
         /// <summary>
@@ -66,23 +53,7 @@ namespace BansheeEditor
         {
             this.curves = curves;
         }
-
-        /// <summary>
-        /// Change the physical size of the GUI element.
-        /// </summary>
-        /// <param name="width">Width of the element in pixels.</param>
-        /// <param name="height">Height of the element in pixels.</param>
-        public void SetSize(int width, int height)
-        {
-            this.width = width;
-            this.height = height;
-
-            canvas.SetWidth(width);
-            canvas.SetHeight(height);
-
-            drawableWidth = Math.Max(0, width - GUIGraphTime.PADDING * 2);
-        }
-
+        
         /// <summary>
         /// Changes the visible range that the GUI element displays.
         /// </summary>
@@ -91,7 +62,7 @@ namespace BansheeEditor
         ///                      [-yRange * 0.5, yRange * 0.5]</param>
         public void SetRange(float xRange, float yRange)
         {
-            this.xRange = xRange;
+            SetRange(xRange);
             this.yRange = yRange;
         }
 
@@ -101,27 +72,10 @@ namespace BansheeEditor
         /// <param name="offset">Value to start the timeline values at.</param>
         public void SetOffset(Vector2 offset)
         {
-            this.offset = offset;
-        }
-
-        /// <summary>
-        /// Number of frames per second, used for frame selection and marking.
-        /// </summary>
-        /// <param name="fps">Number of prames per second.</param>
-        public void SetFPS(int fps)
-        {
-            this.fps = Math.Max(1, fps);
-        }
-
-        /// <summary>
-        /// Sets the frame at which to display the frame marker.
-        /// </summary>
-        /// <param name="frameIdx">Index of the frame to display the marker on, or -1 to clear the marker.</param>
-        public void SetMarkedFrame(int frameIdx)
-        {
-            markedFrameIdx = frameIdx;
+            SetOffset(offset.x);
+            yOffset = offset.y;
         }
-
+        
         /// <summary>
         /// Marks the specified key-frame as selected, changing the way it is displayed.
         /// </summary>
@@ -155,20 +109,6 @@ namespace BansheeEditor
             }
         }
 
-        /// <summary>
-        /// Returns time for a frame with the specified index. Depends on set range and FPS.
-        /// </summary>
-        /// <param name="frameIdx">Index of the frame (not a key-frame) to get the time for.</param>
-        /// <returns>Time of the frame with the provided index. </returns>
-        public float GetTimeForFrame(int frameIdx)
-        {
-            float range = GetRange();
-            int numFrames = (int)range * fps;
-            float timePerFrame = range / numFrames;
-
-            return frameIdx* timePerFrame;
-        }
-
         /// <summary>
         /// Attempts to find a keyframe under the provided coordinates.
         /// </summary>
@@ -281,7 +221,7 @@ namespace BansheeEditor
             Rect2I bounds = canvas.Bounds;
 
             // Check if outside of curve drawing bounds
-            if (pixelCoords.x < (bounds.x + GUIGraphTime.PADDING) || pixelCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
+            if (pixelCoords.x < (bounds.x + PADDING) || pixelCoords.x >= (bounds.x + bounds.width - PADDING) ||
                 pixelCoords.y < bounds.y || pixelCoords.y >= (bounds.y + bounds.height))
             {
                 curveCoords = new Vector2();
@@ -289,15 +229,15 @@ namespace BansheeEditor
             }
 
             // Find time and value of the place under the coordinates
-            Vector2I relativeCoords = pixelCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
+            Vector2I relativeCoords = pixelCoords - new Vector2I(bounds.x + PADDING, bounds.y);
 
             float lengthPerPixel = GetRange() / drawableWidth;
             float heightPerPixel = yRange / height;
 
-            float yOffset = yRange / 2.0f;
+            float centerOffset = yRange / 2.0f;
 
-            float t = offset.x + relativeCoords.x * lengthPerPixel;
-            float value = offset.y + yOffset - relativeCoords.y * heightPerPixel;
+            float t = rangeOffset + relativeCoords.x * lengthPerPixel;
+            float value = yOffset + centerOffset - relativeCoords.y * heightPerPixel;
 
             curveCoords = new Vector2();
             curveCoords.x = t;
@@ -315,10 +255,10 @@ namespace BansheeEditor
         {
             int heightOffset = height / 2; // So that y = 0 is at center of canvas
 
-            Vector2 relativeCurveCoords = curveCoords - offset;
+            Vector2 relativeCurveCoords = curveCoords - new Vector2(rangeOffset, yOffset);
 
             Vector2I pixelCoords = new Vector2I();
-            pixelCoords.x = (int)((relativeCurveCoords.x / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
+            pixelCoords.x = (int)((relativeCurveCoords.x / GetRange()) * drawableWidth) + PADDING;
             pixelCoords.y = heightOffset - (int)((relativeCurveCoords.y / yRange) * height);
 
             return pixelCoords;
@@ -332,7 +272,7 @@ namespace BansheeEditor
         /// <param name="onTop">Determines should the marker be drawn above or below the curve.</param>
         private void DrawFrameMarker(float t, Color color, bool onTop)
         {
-            int xPos = (int)(((t - offset.x) / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
+            int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
 
             Vector2I start = new Vector2I(xPos, 0);
             Vector2I end = new Vector2I(xPos, height);
@@ -472,36 +412,17 @@ namespace BansheeEditor
                 return mode.HasFlag(TangentMode.OutFree);
         }
 
-        /// <summary>
-        /// Returns the range of times displayed by the timeline rounded to the multiple of FPS.
-        /// </summary>
-        /// <param name="padding">If true, extra range will be included to cover the right-most padding.</param>
-        /// <returns>Time range rounded to a multiple of FPS.</returns>
-        private float GetRange(bool padding = false)
-        {
-            float spf = 1.0f / fps;
-
-            float range = xRange;
-            if (padding)
-            {
-                float lengthPerPixel = xRange / drawableWidth;
-                range += lengthPerPixel * GUIGraphTime.PADDING;
-            }
-
-            return ((int)range / spf) * spf;
-        }
-
         /// <summary>
         /// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
         /// </summary>
-        public void Rebuild()
+        public override void Rebuild()
         {
             canvas.Clear();
 
             if (curves == null)
                 return;
 
-            tickHandler.SetRange(offset.x, offset.x + GetRange(true), drawableWidth + GUIGraphTime.PADDING);
+            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + GUIGraphTime.PADDING);
 
             // Draw vertical frame markers
             int numTickLevels = tickHandler.NumLevels;

+ 552 - 30
Source/MBansheeEditor/Windows/Animation/GUICurveEditor.cs

@@ -10,14 +10,24 @@ namespace BansheeEditor
      *  @{
      */
 
+    /// <summary>
+    /// Displays a set of animation curves and events. Allows manipulation of both by adding, removing and modifying
+    /// curve keyframes, and animation events.
+    /// </summary>
     internal class GUICurveEditor
     {
+        /// <summary>
+        /// Information about currently selected set of keyframes for a specific curve.
+        /// </summary>
         class SelectedKeyframes
         {
             public int curveIdx;
             public List<int> keyIndices = new List<int>();
         }
 
+        /// <summary>
+        /// Information about a keyframe that is currently being dragged.
+        /// </summary>
         struct DraggedKeyframe
         {
             public DraggedKeyframe(int index, KeyFrame original)
@@ -30,20 +40,33 @@ namespace BansheeEditor
             public KeyFrame original;
         }
 
+        /// <summary>
+        /// Information about all keyframes of a specific curve that are currently being dragged.
+        /// </summary>
         class DraggedKeyframes
         {
             public int curveIdx;
             public List<DraggedKeyframe> keys = new List<DraggedKeyframe>();
         }
 
+        /// <summary>
+        /// Data about an animation event.
+        /// </summary>
+        class EventInfo
+        {
+            public AnimationEvent animEvent;
+            public bool selected;
+        }
+
         private const int TIMELINE_HEIGHT = 20;
         private const int EVENTS_HEIGHT = 10;
         private const int SIDEBAR_WIDTH = 30;
         private const int DRAG_START_DISTANCE = 3;
 
-        private EditorWindow window;
+        private AnimationWindow window;
         private GUILayout gui;
         private GUIPanel drawingPanel;
+        private GUIPanel eventsPanel;
 
         private GUIGraphTime guiTimeline;
         private GUIAnimEvents guiEvents;
@@ -52,9 +75,13 @@ namespace BansheeEditor
 
         private ContextMenu blankContextMenu;
         private ContextMenu keyframeContextMenu;
+        private ContextMenu blankEventContextMenu;
+        private ContextMenu eventContextMenu;
         private Vector2I contextClickPosition;
 
         private EdAnimationCurve[] curves = new EdAnimationCurve[0];
+        private bool disableCurveEdit = false;
+
         private float xRange = 60.0f;
         private float yRange = 10.0f;
         private Vector2 offset;
@@ -65,6 +92,8 @@ namespace BansheeEditor
         private int markedFrameIdx;
         private List<SelectedKeyframes> selectedKeyframes = new List<SelectedKeyframes>();
 
+        private List<EventInfo> events = new List<EventInfo>();
+
         private bool isPointerHeld;
         private bool isMousePressedOverKey;
         private bool isMousePressedOverTangent;
@@ -74,10 +103,30 @@ namespace BansheeEditor
         private Vector2I dragStart;
 
         /// <summary>
-        /// Triggers whenever user selects a new frame.
+        /// Triggers whenever user selects a new frame. Reports the index of the selected frame.
         /// </summary>
         public Action<int> OnFrameSelected;
 
+        /// <summary>
+        /// Triggered whenever a new animation event is added.
+        /// </summary>
+        public Action OnEventAdded;
+
+        /// <summary>
+        /// Triggered whenever values in an animation event change.
+        /// </summary>
+        public Action OnEventModified;
+
+        /// <summary>
+        /// Triggered whenever an animation event is deleted.
+        /// </summary>
+        public Action OnEventDeleted;
+
+        /// <summary>
+        /// Triggered whenever keyframe in a curve is modified (added, removed or edited).
+        /// </summary>
+        public Action OnCurveModified;
+
         /// <summary>
         /// The displayed range of the curve, where:
         ///   .x - Range of the horizontal area. Displayed area ranges from [0, x].
@@ -101,7 +150,7 @@ namespace BansheeEditor
         }
 
         /// <summary>
-        /// Returns the offset of the displayed curve values.
+        /// Determines how much to offset the displayed curve values.
         /// </summary>
         public Vector2 Offset
         {
@@ -135,7 +184,55 @@ namespace BansheeEditor
             get { return height; }
         }
 
-        public GUICurveEditor(EditorWindow window, GUILayout gui, int width, int height)
+        /// <summary>
+        /// Set to true if curves are not allowed to be edited.
+        /// </summary>
+        public bool DisableCurveEdit
+        {
+            set { disableCurveEdit = value; }
+        }
+
+        /// <summary>
+        /// Animation events displayed on the curve editor.
+        /// </summary>
+        public AnimationEvent[] Events
+        {
+            get
+            {
+                AnimationEvent[] animEvents = new AnimationEvent[events.Count];
+
+                // Note: Hidden dependency. Returned events must point to the same event class this object is using, so
+                // that any modifications made in this class will be visible in the returned values.
+                for (int i = 0; i < events.Count; i++)
+                    animEvents[i] = events[i].animEvent;
+
+                return animEvents;
+            }
+
+            set
+            {
+                events.Clear();
+
+                for (int i = 0; i < value.Length; i++)
+                {
+                    EventInfo eventInfo = new EventInfo();
+                    eventInfo.animEvent = value[i];
+
+                    events.Add(eventInfo);
+                }
+
+                UpdateEventsGUI();
+            }
+        }
+
+        /// <summary>
+        /// Creates a new curve editor GUI elements.
+        /// </summary>
+        /// <param name="window">Parent window of the GUI element.</param>
+        /// <param name="gui">GUI layout into which to place the GUI element.</param>
+        /// <param name="width">Width in pixels.</param>
+        /// <param name="height">Height in pixels.</param>
+        public GUICurveEditor(AnimationWindow window, GUILayout gui, int width, int height)
         {
             this.window = window;
             this.gui = gui;
@@ -146,8 +243,12 @@ namespace BansheeEditor
             blankContextMenu = new ContextMenu();
             blankContextMenu.AddItem("Add keyframe", AddKeyframeAtPosition);
 
+            blankEventContextMenu = new ContextMenu();
+            blankEventContextMenu.AddItem("Add event", AddEventAtPosition);
+
             keyframeContextMenu = new ContextMenu();
             keyframeContextMenu.AddItem("Delete", DeleteSelectedKeyframes);
+            keyframeContextMenu.AddItem("Delete", EditSelectedKeyframe);
             keyframeContextMenu.AddItem("Tangents/Auto", () => { ChangeSelectionTangentMode(TangentMode.Auto); });
             keyframeContextMenu.AddItem("Tangents/Free", () => { ChangeSelectionTangentMode(TangentMode.Free); });
             keyframeContextMenu.AddItem("Tangents/In/Auto", () => { ChangeSelectionTangentMode(TangentMode.InAuto); });
@@ -159,9 +260,13 @@ namespace BansheeEditor
             keyframeContextMenu.AddItem("Tangents/Out/Linear", () => { ChangeSelectionTangentMode(TangentMode.OutLinear); });
             keyframeContextMenu.AddItem("Tangents/Out/Step", () => { ChangeSelectionTangentMode(TangentMode.OutStep); });
 
+            eventContextMenu = new ContextMenu();
+            eventContextMenu.AddItem("Delete", DeleteSelectedEvents);
+            eventContextMenu.AddItem("Edit", EditSelectedEvent);
+
             guiTimeline = new GUIGraphTime(gui, width, TIMELINE_HEIGHT);
 
-            GUIPanel eventsPanel = gui.AddPanel();
+            eventsPanel = gui.AddPanel();
             eventsPanel.SetPosition(0, TIMELINE_HEIGHT);
             guiEvents = new GUIAnimEvents(eventsPanel, width, EVENTS_HEIGHT);
             
@@ -201,6 +306,13 @@ namespace BansheeEditor
             return guiCurveDrawing.CurveToPixelSpace(curveCoords);
         }
 
+        /// <summary>
+        /// Converts coordinates in window space (relative to the parent window origin) into coordinates in curve space.
+        /// </summary>
+        /// <param name="windowPos">Coordinates relative to parent editor window, in pixels.</param>
+        /// <param name="curveCoord">Curve coordinates within the range as specified by <see cref="Range"/>. Only
+        ///                          valid when function returns true.</param>
+        /// <returns>True if the coordinates are within the curve area, false otherwise.</returns>
         public bool WindowToCurveSpace(Vector2I windowPos, out Vector2 curveCoord)
         {
             Rect2I elementBounds = GUIUtility.CalculateBounds(gui, window.GUI);
@@ -212,6 +324,10 @@ namespace BansheeEditor
             return guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord);
         }
 
+        /// <summary>
+        /// Handles input. Should be called by the owning window whenever a pointer is pressed.
+        /// </summary>
+        /// <param name="ev">Object containing pointer press event information.</param>
         internal void OnPointerPressed(PointerEvent ev)
         {
             if (ev.IsUsed)
@@ -225,6 +341,9 @@ namespace BansheeEditor
             Rect2I drawingBounds = drawingPanel.Bounds;
             Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
 
+            Rect2I eventBounds = eventsPanel.Bounds;
+            Vector2I eventPos = pointerPos - new Vector2I(eventBounds.x, eventBounds.y);
+
             if (ev.Button == PointerButton.Left)
             {
                 Vector2 curveCoord;
@@ -258,6 +377,7 @@ namespace BansheeEditor
                     }
 
                     guiCurveDrawing.Rebuild();
+                    UpdateEventsGUI();
                 }
                 else
                 {
@@ -268,9 +388,20 @@ namespace BansheeEditor
                     else
                     {
                         int eventIdx;
-                        if (guiEvents.FindEvent(pointerPos, out eventIdx))
+                        if (guiEvents.FindEvent(eventPos, out eventIdx))
                         {
-                            // TODO - Select event
+                            if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
+                                ClearSelection();
+
+                            events[eventIdx].selected = true;
+                            UpdateEventsGUI();
+                        }
+                        else
+                        {
+                            ClearSelection();
+
+                            guiCurveDrawing.Rebuild();
+                            UpdateEventsGUI();
                         }
                     }
 
@@ -292,6 +423,9 @@ namespace BansheeEditor
                         ClearSelection();
 
                         blankContextMenu.Open(pointerPos, gui);
+
+                        guiCurveDrawing.Rebuild();
+                        UpdateEventsGUI();
                     }
                     else
                     {
@@ -302,14 +436,48 @@ namespace BansheeEditor
                             SelectKeyframe(keyframeRef);
 
                             guiCurveDrawing.Rebuild();
+                            UpdateEventsGUI();
                         }
 
                         keyframeContextMenu.Open(pointerPos, gui);
                     }
                 }
+                else if (guiEvents.GetFrame(eventPos) != -1) // Clicked over events bar
+                {
+                    contextClickPosition = eventPos;
+
+                    int eventIdx;
+                    if (!guiEvents.FindEvent(eventPos, out eventIdx))
+                    {
+                        ClearSelection();
+
+                        blankEventContextMenu.Open(pointerPos, gui);
+
+                        guiCurveDrawing.Rebuild();
+                        UpdateEventsGUI();
+                    }
+                    else
+                    {
+                        // If clicked outside of current selection, just select the one event
+                        if (!events[eventIdx].selected)
+                        {
+                            ClearSelection();
+                            events[eventIdx].selected = true;
+
+                            guiCurveDrawing.Rebuild();
+                            UpdateEventsGUI();
+                        }
+
+                        eventContextMenu.Open(pointerPos, gui);
+                    }
+                }
             }
         }
 
+        /// <summary>
+        /// Handles input. Should be called by the owning window whenever a pointer is moved.
+        /// </summary>
+        /// <param name="ev">Object containing pointer move event information.</param>
         internal void OnPointerMoved(PointerEvent ev)
         {
             if (ev.Button != PointerButton.Left)
@@ -332,7 +500,7 @@ namespace BansheeEditor
                         int distance = Vector2I.Distance(drawingPos, dragStart);
                         if (distance >= DRAG_START_DISTANCE)
                         {
-                            if (isMousePressedOverKey)
+                            if (isMousePressedOverKey && !disableCurveEdit)
                             {
                                 draggedKeyframes.Clear();
                                 foreach (var selectedEntry in selectedKeyframes)
@@ -357,7 +525,7 @@ namespace BansheeEditor
 
                     if (isDragInProgress)
                     {
-                        if (isMousePressedOverKey)
+                        if (isMousePressedOverKey && !disableCurveEdit)
                         {
                             Vector2 diff = Vector2.Zero;
 
@@ -400,9 +568,11 @@ namespace BansheeEditor
                                     SelectKeyframe(new KeyframeRef(draggedEntry.curveIdx, keyframe.index));
                             }
 
+                            OnCurveModified?.Invoke();
                             guiCurveDrawing.Rebuild();
+                            UpdateEventsGUI();
                         }
-                        else if (isMousePressedOverTangent)
+                        else if (isMousePressedOverTangent && !disableCurveEdit)
                         {
                             EdAnimationCurve curve = curves[draggedTangent.keyframeRef.curveIdx];
                             KeyFrame keyframe = curve.KeyFrames[draggedTangent.keyframeRef.keyIdx];
@@ -435,6 +605,7 @@ namespace BansheeEditor
                                 curve.KeyFrames[draggedTangent.keyframeRef.keyIdx] = keyframe;
                                 curve.Apply();
 
+                                OnCurveModified?.Invoke();
                                 guiCurveDrawing.Rebuild();
                             }
                         }
@@ -452,6 +623,10 @@ namespace BansheeEditor
             }
         }
 
+        /// <summary>
+        /// Handles input. Should be called by the owning window whenever a pointer is released.
+        /// </summary>
+        /// <param name="ev">Object containing pointer release event information.</param>
         internal void OnPointerReleased(PointerEvent ev)
         {
             isPointerHeld = false;
@@ -460,6 +635,10 @@ namespace BansheeEditor
             isMousePressedOverTangent = false;
         }
 
+        /// <summary>
+        /// Handles input. Should be called by the owning window whenever a button is released.
+        /// </summary>
+        /// <param name="ev">Object containing button release event information.</param>
         internal void OnButtonUp(ButtonEvent ev)
         {
             if(ev.Button == ButtonCode.Delete)
@@ -528,29 +707,79 @@ namespace BansheeEditor
             markedFrameIdx = frameIdx;
 
             guiTimeline.SetMarkedFrame(frameIdx);
+            guiEvents.SetMarkedFrame(frameIdx);
             guiCurveDrawing.SetMarkedFrame(frameIdx);
 
             Redraw();
         }
 
+        /// <summary>
+        /// Adds a new keyframe at the currently selected frame.
+        /// </summary>
         public void AddKeyFrameAtMarker()
         {
             ClearSelection();
 
-            foreach (var curve in curves)
+            if (!disableCurveEdit)
             {
-                float t = guiCurveDrawing.GetTimeForFrame(markedFrameIdx);
-                float value = curve.Evaluate(t);
+                foreach (var curve in curves)
+                {
+                    float t = guiCurveDrawing.GetTimeForFrame(markedFrameIdx);
+                    float value = curve.Evaluate(t);
 
-                curve.AddKeyframe(t, value);
-                curve.Apply();
+                    curve.AddKeyframe(t, value);
+                    curve.Apply();
+                }
             }
 
             // TODO - UNDOREDO
 
+            OnCurveModified?.Invoke();
+            guiCurveDrawing.Rebuild();
+            UpdateEventsGUI();
+        }
+
+        /// <summary>
+        /// Adds a new event at the currently selected event.
+        /// </summary>
+        public void AddEventAtMarker()
+        {
+            ClearSelection();
+
+            float eventTime = guiEvents.GetTimeForFrame(markedFrameIdx);
+            EventInfo eventInfo = new EventInfo();
+            eventInfo.animEvent = new AnimationEvent("", eventTime);
+            
+            events.Add(eventInfo); // TODO - UNDOREDO
+            OnEventAdded?.Invoke();
+
+            UpdateEventsGUI();
             guiCurveDrawing.Rebuild();
+
+            StartEventEdit(events.Count - 1);
         }
 
+        /// <summary>
+        /// Rebuilds GUI displaying the animation events list.
+        /// </summary>
+        private void UpdateEventsGUI()
+        {
+            AnimationEvent[] animEvents = new AnimationEvent[events.Count];
+            bool[] selected = new bool[events.Count];
+
+            for (int i = 0; i < events.Count; i++)
+            {
+                animEvents[i] = events[i].animEvent;
+                selected[i] = events[i].selected;
+            }
+
+            guiEvents.SetEvents(animEvents, selected);
+            guiEvents.Rebuild();
+        }
+
+        /// <summary>
+        /// Rebuilds the entire curve editor GUI.
+        /// </summary>
         public void Redraw()
         {
             guiCurveDrawing.Rebuild();
@@ -559,8 +788,16 @@ namespace BansheeEditor
             guiSidebar.Rebuild();
         }
         
+        /// <summary>
+        /// Changes the tangent mode for all currently selected keyframes.
+        /// </summary>
+        /// <param name="mode">Tangent mode to set. If only in or out tangent mode is provided, the mode for the opposite 
+        ///                    tangent will be kept as is.</param>
         private void ChangeSelectionTangentMode(TangentMode mode)
         {
+            if (disableCurveEdit)
+                return;
+
             foreach (var selectedEntry in selectedKeyframes)
             {
                 EdAnimationCurve curve = curves[selectedEntry.curveIdx];
@@ -601,9 +838,13 @@ namespace BansheeEditor
 
             // TODO - UNDOREDO
 
+            OnCurveModified?.Invoke();
             guiCurveDrawing.Rebuild();
         }
 
+        /// <summary>
+        /// Adds a new keyframe at the position the context menu was opened at.
+        /// </summary>
         private void AddKeyframeAtPosition()
         {
             Vector2 curveCoord;
@@ -611,52 +852,121 @@ namespace BansheeEditor
             {
                 ClearSelection();
 
-                foreach (var curve in curves)
+                if (!disableCurveEdit)
                 {
-                    float t = curveCoord.x;
-                    float value = curveCoord.y;
+                    foreach (var curve in curves)
+                    {
+                        float t = curveCoord.x;
+                        float value = curveCoord.y;
 
-                    curve.AddKeyframe(t, value);
-                    curve.Apply();
+                        curve.AddKeyframe(t, value);
+                        curve.Apply();
+                    }
                 }
 
                 // TODO - UNDOREDO
 
+                OnCurveModified?.Invoke();
                 guiCurveDrawing.Rebuild();
+                UpdateEventsGUI();
             }
         }
 
-        private void DeleteSelectedKeyframes()
+        /// <summary>
+        /// Adds a new event at the position the context menu was opened at.
+        /// </summary>
+        private void AddEventAtPosition()
         {
-            foreach (var selectedEntry in selectedKeyframes)
+            int frame = guiEvents.GetFrame(contextClickPosition);
+            if (frame != -1)
             {
-                EdAnimationCurve curve = curves[selectedEntry.curveIdx];
+                ClearSelection();
 
-                // Sort keys from highest to lowest so the indices don't change
-                selectedEntry.keyIndices.Sort((x, y) =>
+                float time = guiEvents.GetTimeForFrame(frame);
+
+                EventInfo eventInfo = new EventInfo();
+                eventInfo.animEvent = new AnimationEvent("", time);
+
+                events.Add(eventInfo); // TODO - UNDOREDO
+                OnEventAdded?.Invoke();
+
+                UpdateEventsGUI();
+                guiCurveDrawing.Rebuild();
+
+                StartEventEdit(events.Count - 1);
+            }
+        }
+
+        /// <summary>
+        /// Removes all currently selected keyframes from the curves.
+        /// </summary>
+        private void DeleteSelectedKeyframes()
+        {
+            if (!disableCurveEdit)
+            {
+                foreach (var selectedEntry in selectedKeyframes)
                 {
-                    return y.CompareTo(x);
-                });
+                    EdAnimationCurve curve = curves[selectedEntry.curveIdx];
 
-                foreach (var keyframeIdx in selectedEntry.keyIndices)
-                    curve.RemoveKeyframe(keyframeIdx);
+                    // Sort keys from highest to lowest so the indices don't change
+                    selectedEntry.keyIndices.Sort((x, y) =>
+                    {
+                        return y.CompareTo(x);
+                    });
 
-                curve.Apply();
+                    foreach (var keyframeIdx in selectedEntry.keyIndices)
+                        curve.RemoveKeyframe(keyframeIdx);
+
+                    curve.Apply();
+                }
             }
 
             // TODO - UNDOREDO
 
             ClearSelection();
 
+            OnCurveModified?.Invoke();
+            guiCurveDrawing.Rebuild();
+            UpdateEventsGUI();
+        }
+
+        /// <summary>
+        /// Deletes all currently selected events.
+        /// </summary>
+        private void DeleteSelectedEvents()
+        {
+            List<EventInfo> newEvents = new List<EventInfo>();
+            foreach (var entry in events)
+            {
+                if(!entry.selected)
+                    newEvents.Add(entry);
+            }
+
+            events = newEvents; // TODO - UNDOREDO
+            OnEventDeleted?.Invoke();
+
+            ClearSelection();
+
             guiCurveDrawing.Rebuild();
+            UpdateEventsGUI();
         }
 
+        /// <summary>
+        /// Unselects any selected keyframes and events.
+        /// </summary>
         private void ClearSelection()
         {
             guiCurveDrawing.ClearSelectedKeyframes();
             selectedKeyframes.Clear();
+
+            foreach (var entry in events)
+                entry.selected = false;
         }
 
+        /// <summary>
+        /// Adds the provided keyframe to the selection list (doesn't clear existing ones).
+        /// </summary>
+        /// <param name="keyframeRef">Keyframe to select.</param>
         private void SelectKeyframe(KeyframeRef keyframeRef)
         {
             guiCurveDrawing.SelectKeyframe(keyframeRef, true);
@@ -682,6 +992,11 @@ namespace BansheeEditor
             }
         }
 
+        /// <summary>
+        /// Checks is the provided keyframe currently selected.
+        /// </summary>
+        /// <param name="keyframeRef">Keyframe to check.</param>
+        /// <returns>True if selected, false otherwise.</returns>
         private bool IsSelected(KeyframeRef keyframeRef)
         {
             int curveIdx = selectedKeyframes.FindIndex(x =>
@@ -699,6 +1014,213 @@ namespace BansheeEditor
 
             return keyIdx != -1;
         }
+
+        /// <summary>
+        /// Opens the edit window for the currently selected keyframe.
+        /// </summary>
+        private void EditSelectedKeyframe()
+        {
+            if (disableCurveEdit)
+                return;
+
+            if (selectedKeyframes.Count == 0)
+                return;
+
+            EdAnimationCurve curve = curves[selectedKeyframes[0].curveIdx];
+            KeyFrame[] keyFrames = curve.KeyFrames;
+
+            int keyIndex = selectedKeyframes[0].keyIndices[0];
+            KeyFrame keyFrame = keyFrames[keyIndex];
+            Vector2I position = guiCurveDrawing.CurveToPixelSpace(new Vector2(keyFrame.time, keyFrame.value));
+
+            Rect2I drawingBounds = GUIUtility.CalculateBounds(drawingPanel, window.GUI);
+            position.x = MathEx.Clamp(position.x, 0, drawingBounds.width);
+            position.y = MathEx.Clamp(position.y, 0, drawingBounds.height);
+
+            Vector2I windowPos = position + new Vector2I(drawingBounds.x, drawingBounds.y);
+            
+            KeyframeEditWindow editWindow = DropDownWindow.Open<KeyframeEditWindow>(window, windowPos);
+            editWindow.Initialize(keyFrame, x =>
+            {
+                curve.UpdateKeyframe(keyIndex, x.time, x.value);
+                curve.Apply();
+                // TODO UNDOREDO
+
+                guiCurveDrawing.Rebuild();
+                OnCurveModified?.Invoke();
+            });
+        }
+
+        /// <summary>
+        /// Opens the edit window for the currently selected event.
+        /// </summary>
+        private void EditSelectedEvent()
+        {
+            for (int i = 0; i < events.Count; i++)
+            {
+                if (events[i].selected)
+                {
+                    StartEventEdit(i);
+                    break;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Opens the event edit window for the specified event.
+        /// </summary>
+        /// <param name="eventIdx">Event index to open the edit window for.</param>
+        private void StartEventEdit(int eventIdx)
+        {
+            AnimationEvent animEvent = events[eventIdx].animEvent;
+
+            Vector2I position = new Vector2I();
+            position.x = guiEvents.GetOffset(animEvent.Time);
+            position.y = EVENTS_HEIGHT/2;
+
+            Rect2I eventBounds = GUIUtility.CalculateBounds(eventsPanel, window.GUI);
+            Vector2I windowPos = position + new Vector2I(eventBounds.x, eventBounds.y);
+
+            SceneObject so = window.SelectedSO;
+            Component[] components = so.GetComponents();
+            string[] componentNames = new string[components.Length];
+            for (int i = 0; i < components.Length; i++)
+                componentNames[i] = components[i].GetType().Name;
+
+            EventEditWindow editWindow = DropDownWindow.Open<EventEditWindow>(window, windowPos);
+            editWindow.Initialize(animEvent, componentNames, () =>
+            {
+                UpdateEventsGUI();
+                OnEventModified?.Invoke();
+            });
+        }
+    }
+
+    /// <summary>
+    /// Drop down window that displays input boxes used for editing a keyframe.
+    /// </summary>
+    [DefaultSize(120, 80)]
+    internal class KeyframeEditWindow : DropDownWindow
+    {
+        /// <summary>
+        /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
+        /// use.
+        /// </summary>
+        /// <param name="keyFrame">Keyframe whose properties to edit.</param>
+        /// <param name="updateCallback">Callback triggered when event values change.</param>
+        internal void Initialize(KeyFrame keyFrame, Action<KeyFrame> updateCallback)
+        {
+            GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
+            timeField.Value = keyFrame.time;
+            timeField.OnChanged += x => { keyFrame.time = x; updateCallback(keyFrame); };
+
+            GUIFloatField valueField = new GUIFloatField(new LocEdString("Value"), 40, "");
+            valueField.Value = keyFrame.value;
+            valueField.OnChanged += x => { keyFrame.value = x; updateCallback(keyFrame); };
+
+            GUILayoutY vertLayout = GUI.AddLayoutY();
+
+            vertLayout.AddFlexibleSpace();
+            GUILayoutX horzLayout = vertLayout.AddLayoutX();
+            horzLayout.AddFlexibleSpace();
+            GUILayout contentLayout = horzLayout.AddLayoutY();
+            GUILayout timeLayout = contentLayout.AddLayoutX();
+            timeLayout.AddSpace(5);
+            timeLayout.AddElement(timeField);
+            timeLayout.AddFlexibleSpace();
+            GUILayout componentLayout = contentLayout.AddLayoutX();
+            componentLayout.AddSpace(5);
+            componentLayout.AddElement(valueField);
+            componentLayout.AddFlexibleSpace();
+            horzLayout.AddFlexibleSpace();
+            vertLayout.AddFlexibleSpace();
+        }
+    }
+
+    /// <summary>
+    /// Drop down window that displays input boxes used for editing an event.
+    /// </summary>
+    [DefaultSize(200, 80)]
+    internal class EventEditWindow : DropDownWindow
+    {
+        /// <summary>
+        /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
+        /// use.
+        /// </summary>
+        /// <param name="animEvent">Event whose properties to edit.</param>
+        /// <param name="componentNames">List of component names that the user can select from.</param>
+        /// <param name="updateCallback">Callback triggered when event values change.</param>
+        internal void Initialize(AnimationEvent animEvent, string[] componentNames, Action updateCallback)
+        {
+            int selectedIndex = -1;
+            if (!string.IsNullOrEmpty(animEvent.Name))
+            {
+                string[] nameEntries = animEvent.Name.Split('/');
+                if (nameEntries.Length > 1)
+                {
+                    string typeName = nameEntries[0];
+                    for (int i = 0; i < componentNames.Length; i++)
+                    {
+                        if (componentNames[i] == typeName)
+                        {
+                            selectedIndex = i;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
+            timeField.Value = animEvent.Time;
+            timeField.OnChanged += x => { animEvent.Time = x; updateCallback(); }; // TODO UNDOREDO  
+
+            GUIListBoxField componentField = new GUIListBoxField(componentNames, new LocEdString("Component"), 40);
+            if (selectedIndex != -1)
+                componentField.Index = selectedIndex;
+
+            componentField.OnSelectionChanged += x =>
+            {
+                string compName = "";
+                if (x != -1)
+                    compName = componentNames[x] + "/";
+
+                animEvent.Name = compName + x;
+                updateCallback();
+            };// TODO UNDOREDO 
+
+            GUITextField methodField = new GUITextField(new LocEdString("Method"), 40, false, "", GUIOption.FixedWidth(190));
+            methodField.Value = animEvent.Name;
+            methodField.OnChanged += x =>
+            {
+                string compName = "";
+                if(componentField.Index != -1)
+                    compName = componentNames[componentField.Index] + "/";
+
+                animEvent.Name = compName + x;
+                updateCallback();
+            }; // TODO UNDOREDO 
+
+            GUILayoutY vertLayout = GUI.AddLayoutY();
+
+            vertLayout.AddFlexibleSpace();
+            GUILayoutX horzLayout = vertLayout.AddLayoutX();
+            horzLayout.AddFlexibleSpace();
+            GUILayout contentLayout = horzLayout.AddLayoutY();
+            GUILayout timeLayout = contentLayout.AddLayoutX();
+            timeLayout.AddSpace(5);
+            timeLayout.AddElement(timeField);
+            timeLayout.AddFlexibleSpace();
+            GUILayout componentLayout = contentLayout.AddLayoutX();
+            componentLayout.AddSpace(5);
+            componentLayout.AddElement(componentField);
+            componentLayout.AddFlexibleSpace();
+            GUILayout methodLayout = contentLayout.AddLayoutX();
+            methodLayout.AddSpace(5);
+            methodLayout.AddElement(methodField);
+            methodLayout.AddFlexibleSpace();
+            horzLayout.AddFlexibleSpace();
+            vertLayout.AddFlexibleSpace();
+        }
     }
 
     /** @} */

+ 1 - 1
Source/MBansheeEditor/Windows/Animation/GUIFieldSelector.cs

@@ -294,7 +294,7 @@ namespace BansheeEditor
                 parent.children = new Element[3];
 
                 parent.children[0] = AddFieldRow(parent.childLayout, "Position", parent.so, null, parent.path + "/Position", SerializableProperty.FieldType.Vector3);
-                parent.children[1] = AddFieldRow(parent.childLayout, "Rotation", parent.so, null, parent.path + "/Rotation", SerializableProperty.FieldType.Vector4);
+                parent.children[1] = AddFieldRow(parent.childLayout, "Rotation", parent.so, null, parent.path + "/Rotation", SerializableProperty.FieldType.Vector3);
                 parent.children[2] = AddFieldRow(parent.childLayout, "Scale", parent.so, null, parent.path + "/Scale", SerializableProperty.FieldType.Vector3);
             }
         }

+ 7 - 146
Source/MBansheeEditor/Windows/Animation/GUIGraphTime.cs

@@ -13,24 +13,12 @@ namespace BansheeEditor
     /// Renders a timeline that may be used as a header for a graph display. User can set the range of the times to display,
     /// as well as its physical dimensions.
     /// </summary>
-    public class GUIGraphTime
+    public class GUIGraphTime : GUITimelineBase
     {
-        public const int PADDING = 30;
-
         private const float TICK_HEIGHT_PCT = 0.4f;
         private const int TEXT_PADDING = 2;
         
-        private int tickHeight;
-        private int drawableWidth;
-        private float rangeLength = 60.0f;
-        private float rangeOffset = 0.0f;
-
-        private GUICanvas canvas;
         private GUIGraphTicks tickHandler;
-        private int width;
-        private int height;
-        private int fps = 1;
-        private int markedFrameIdx = -1;
 
         /// <summary>
         /// Constructs a new timeline and adds it to the specified layout.
@@ -39,97 +27,9 @@ namespace BansheeEditor
         /// <param name="width">Width of the timeline in pixels.</param>
         /// <param name="height">Height of the timeline in pixels.</param>
         public GUIGraphTime(GUILayout layout, int width, int height)
+            :base(layout, width, height)
         {
-            canvas = new GUICanvas();
-            layout.AddElement(canvas);
-
             tickHandler = new GUIGraphTicks(GUITickStepType.Time);
-
-            SetSize(width, height);
-        }
-
-        /// <summary>
-        /// Uses the assigned FPS, range and physical size to calculate the frame that is under the provided coordinates.
-        /// </summary>
-        /// <param name="windowCoords">Coordinate relative to the window the GUI element is on.</param>
-        /// <returns>Frame that was clicked on, or -1 if the coordinates are outside of valid bounds. </returns>
-        public int GetFrame(Vector2I windowCoords)
-        {
-            Rect2I bounds = canvas.Bounds;
-
-            if (windowCoords.x < (bounds.x + PADDING) || windowCoords.x >= (bounds.x + bounds.width - PADDING) ||
-                windowCoords.y < bounds.y || windowCoords.y >= (bounds.y + bounds.height))
-            {
-                return -1;
-            }
-
-            Vector2I relativeCoords = windowCoords - new Vector2I(bounds.x + PADDING, bounds.y);
-
-            float lengthPerPixel = GetRange() / drawableWidth;
-            float time = rangeOffset + relativeCoords.x * lengthPerPixel;
-
-            return MathEx.RoundToInt(time * fps);
-        }
-
-        /// <summary>
-        /// Sets the frame at which to display the frame marker.
-        /// </summary>
-        /// <param name="frameIdx">Index of the frame to display the marker on, or -1 to clear the marker.</param>
-        public void SetMarkedFrame(int frameIdx)
-        {
-            markedFrameIdx = frameIdx;
-        }
-
-        /// <summary>
-        /// Sets the physical size onto which to draw the timeline.
-        /// </summary>
-        /// <param name="width">Width in pixels.</param>
-        /// <param name="height">Height in pixels.</param>
-        public void SetSize(int width, int height)
-        {
-            this.width = width;
-            this.height = height;
-
-            canvas.SetWidth(width);
-            canvas.SetHeight(height);
-
-            tickHeight = (int)(height * TICK_HEIGHT_PCT);
-            drawableWidth = Math.Max(0, width - PADDING * 2);
-
-            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + PADDING);
-        }
-
-        /// <summary>
-        /// Sets the range of values to display on the timeline.
-        /// </summary>
-        /// <param name="length">Amount of time to display, in seconds.</param>
-        public void SetRange(float length)
-        {
-            rangeLength = Math.Max(0.0f, length);
-
-            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + PADDING);
-        }
-
-        /// <summary>
-        /// Returns the offset at which the displayed timeline values start at.
-        /// </summary>
-        /// <param name="offset">Value to start the timeline values at.</param>
-        public void SetOffset(float offset)
-        {
-            rangeOffset = offset;
-
-            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + PADDING);
-        }
-
-        /// <summary>
-        /// Number of frames per second, used for frame selection and marking.
-        /// </summary>
-        /// <param name="fps">Number of prames per second.</param>
-        public void SetFPS(int fps)
-        {
-            this.fps = Math.Max(1, fps);
-
-            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + PADDING);
         }
         
         /// <summary>
@@ -172,6 +72,7 @@ namespace BansheeEditor
             int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
 
             // Draw tick
+            float tickHeight = (int)(height * TICK_HEIGHT_PCT);
             Vector2I start = new Vector2I(xPos, height - (int)(tickHeight * strength));
             Vector2I end = new Vector2I(xPos, height);
 
@@ -185,45 +86,11 @@ namespace BansheeEditor
                 DrawTime(xPos, t, displayAsMinutes);
         }
 
-        /// <summary>
-        /// Draws a vertical frame marker at the specified time.
-        /// </summary>
-        /// <param name="t">Time at which to draw the marker.</param>
-        private void DrawFrameMarker(float t)
-        {
-            int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
-
-            Vector2I start = new Vector2I(xPos, 0);
-            Vector2I end = new Vector2I(xPos, height);
-
-            canvas.DrawLine(start, end, Color.BansheeOrange);
-        }
-
-        /// <summary>
-        /// Returns the range of times displayed by the timeline rounded to the multiple of FPS.
-        /// </summary>
-        /// <param name="padding">If true, extra range will be included to cover the right-most padding.</param>
-        /// <returns>Time range rounded to a multiple of FPS.</returns>
-        private float GetRange(bool padding = false)
-        {
-            float spf = 1.0f / fps;
-
-            float range = rangeLength;
-            if (padding)
-            {
-                float lengthPerPixel = rangeLength / drawableWidth;
-                range += lengthPerPixel * PADDING;
-            }
-
-            return ((int)range / spf) * spf;
-        }
-
-        /// <summary>
-        /// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
-        /// </summary>
-        public void Rebuild()
+        /// <inheritdoc/>
+        public override void Rebuild()
         {
             canvas.Clear();
+            tickHandler.SetRange(rangeOffset, rangeOffset + GetRange(true), drawableWidth + PADDING);
 
             float range = GetRange();
 
@@ -244,13 +111,7 @@ namespace BansheeEditor
                 }
             }
 
-            if (markedFrameIdx != -1)
-            {
-                int numFrames = (int)range * fps;
-                float timePerFrame = range / numFrames;
-
-                DrawFrameMarker(markedFrameIdx*timePerFrame);
-            }
+            DrawFrameMarker();
         }
     }
 

+ 200 - 0
Source/MBansheeEditor/Windows/Animation/GUITimelineBase.cs

@@ -0,0 +1,200 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+using System;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /** @addtogroup AnimationEditor
+     *  @{
+     */
+
+    /// <summary>
+    /// Base class that can be implemented by objects needing to elements along draw a horizontal timeline.
+    /// </summary>
+    public class GUITimelineBase
+    {
+        public const int PADDING = 30;
+
+        protected int drawableWidth;
+        protected float rangeLength = 60.0f;
+        protected float rangeOffset = 0.0f;
+
+        protected GUICanvas canvas;
+        protected int width;
+        protected int height;
+        protected int fps = 1;
+        protected int markedFrameIdx = -1;
+
+        /// <summary>
+        /// Constructs a new timeline and adds it to the specified layout.
+        /// </summary>
+        /// <param name="layout">Layout to add the timeline GUI to.</param>
+        /// <param name="width">Width of the timeline in pixels.</param>
+        /// <param name="height">Height of the timeline in pixels.</param>
+        public GUITimelineBase(GUILayout layout, int width, int height)
+        {
+            canvas = new GUICanvas();
+            layout.AddElement(canvas);
+
+            SetSize(width, height);
+        }
+
+        /// <summary>
+        /// Uses the assigned FPS, range and physical size to calculate the frame that is under the provided coordinates.
+        /// </summary>
+        /// <param name="pixelCoords">Coordinate relative to the layout the GUI element is on.</param>
+        /// <returns>Frame that was clicked on, or -1 if the coordinates are outside of valid bounds. </returns>
+        public int GetFrame(Vector2I pixelCoords)
+        {
+            Rect2I bounds = canvas.Bounds;
+
+            if (pixelCoords.x < (bounds.x + PADDING) || pixelCoords.x >= (bounds.x + bounds.width - PADDING) ||
+                pixelCoords.y < bounds.y || pixelCoords.y >= (bounds.y + bounds.height))
+            {
+                return -1;
+            }
+
+            Vector2I relativeCoords = pixelCoords - new Vector2I(bounds.x + PADDING, bounds.y);
+
+            float lengthPerPixel = GetRange() / drawableWidth;
+            float time = rangeOffset + relativeCoords.x * lengthPerPixel;
+
+            return MathEx.RoundToInt(time * fps);
+        }
+
+        /// <summary>
+        /// Finds the pixel offset relative to the GUI element's origin, of the specified time.
+        /// </summary>
+        /// <param name="time">Time value to return the offset for.</param>
+        /// <returns>Offset in pixels relative to GUI element's origin.</returns>
+        public int GetOffset(float time)
+        {
+            return (int)(((time - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
+        }
+
+        /// <summary>
+        /// Returns time for a frame with the specified index. Depends on set range and FPS.
+        /// </summary>
+        /// <param name="frameIdx">Index of the frame (not a key-frame) to get the time for.</param>
+        /// <returns>Time of the frame with the provided index. </returns>
+        public float GetTimeForFrame(int frameIdx)
+        {
+            float range = GetRange();
+            int numFrames = (int)range * fps;
+            float timePerFrame = range / numFrames;
+
+            return frameIdx * timePerFrame;
+        }
+
+        /// <summary>
+        /// Sets the frame at which to display the frame marker.
+        /// </summary>
+        /// <param name="frameIdx">Index of the frame to display the marker on, or -1 to clear the marker.</param>
+        public void SetMarkedFrame(int frameIdx)
+        {
+            markedFrameIdx = frameIdx;
+        }
+
+        /// <summary>
+        /// Sets the physical size onto which to draw the timeline.
+        /// </summary>
+        /// <param name="width">Width in pixels.</param>
+        /// <param name="height">Height in pixels.</param>
+        public void SetSize(int width, int height)
+        {
+            this.width = width;
+            this.height = height;
+
+            canvas.SetWidth(width);
+            canvas.SetHeight(height);
+
+            drawableWidth = Math.Max(0, width - PADDING * 2);
+        }
+
+        /// <summary>
+        /// Sets the range of values to display on the timeline.
+        /// </summary>
+        /// <param name="length">Amount of time to display, in seconds.</param>
+        public void SetRange(float length)
+        {
+            rangeLength = Math.Max(0.0f, length);
+        }
+
+        /// <summary>
+        /// Returns the offset at which the displayed timeline values start at.
+        /// </summary>
+        /// <param name="offset">Value to start the timeline values at, in seconds.</param>
+        public void SetOffset(float offset)
+        {
+            rangeOffset = offset;
+        }
+
+        /// <summary>
+        /// Number of frames per second, used for frame selection and marking.
+        /// </summary>
+        /// <param name="fps">Number of prames per second.</param>
+        public void SetFPS(int fps)
+        {
+            this.fps = Math.Max(1, fps);
+        }
+
+        /// <summary>
+        /// Draws a vertical frame marker at the specified time.
+        /// </summary>
+        /// <param name="t">Time at which to draw the marker.</param>
+        private void DrawFrameMarker(float t)
+        {
+            int xPos = (int)(((t - rangeOffset) / GetRange()) * drawableWidth) + PADDING;
+
+            Vector2I start = new Vector2I(xPos, 0);
+            Vector2I end = new Vector2I(xPos, height);
+
+            canvas.DrawLine(start, end, Color.BansheeOrange);
+        }
+
+        /// <summary>
+        /// Returns the range of times displayed by the timeline rounded to the multiple of FPS.
+        /// </summary>
+        /// <param name="padding">If true, extra range will be included to cover the right-most padding.</param>
+        /// <returns>Time range rounded to a multiple of FPS.</returns>
+        protected float GetRange(bool padding = false)
+        {
+            float spf = 1.0f / fps;
+
+            float range = rangeLength;
+            if (padding)
+            {
+                float lengthPerPixel = rangeLength / drawableWidth;
+                range += lengthPerPixel * PADDING;
+            }
+
+            return ((int)range / spf) * spf;
+        }
+
+        /// <summary>
+        /// Draws the frame marker at the currently selected frame.
+        /// </summary>
+        protected void DrawFrameMarker()
+        {
+            if (markedFrameIdx != -1)
+            {
+                float range = GetRange();
+                int numFrames = (int)range * fps;
+                float timePerFrame = range / numFrames;
+
+                DrawFrameMarker(markedFrameIdx * timePerFrame);
+            }
+        }
+
+        /// <summary>
+        /// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
+        /// </summary>
+        public virtual void Rebuild()
+        {
+            canvas.Clear();
+        }
+    }
+
+    /** @} */
+}

+ 222 - 77
Source/MBansheeEditor/Windows/AnimationWindow.cs

@@ -2,6 +2,7 @@
 //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
 using System;
 using System.Collections.Generic;
+using System.Text;
 using BansheeEngine;
 
 namespace BansheeEditor
@@ -18,12 +19,19 @@ namespace BansheeEditor
     {
         private const int FIELD_DISPLAY_WIDTH = 200;
         private const int DRAG_START_DISTANCE = 3;
-        private const float DRAG_SCALE = 10.0f;
+        private const float DRAG_SCALE = 1.0f;
         private const float ZOOM_SCALE = 0.1f/120.0f; // One scroll step is usually 120 units, we want 1/10 of that
 
-        private bool isInitialized;
         private SceneObject selectedSO;
 
+        /// <summary>
+        /// Scene object for which are we currently changing the animation for.
+        /// </summary>
+        internal SceneObject SelectedSO
+        {
+            get { return selectedSO; }
+        }
+
         #region Overrides
 
         /// <summary>
@@ -44,17 +52,13 @@ namespace BansheeEditor
         private void OnInitialize()
         {
             Selection.OnSelectionChanged += OnSelectionChanged;
-            EditorInput.OnPointerPressed += OnPointerPressed;
-            EditorInput.OnPointerMoved += OnPointerMoved;
-            EditorInput.OnPointerReleased += OnPointerReleased;
-            EditorInput.OnButtonUp += OnButtonUp;
 
-            RebuildGUI();
+            UpdateSelectedSO(true);
         }
 
         private void OnEditorUpdate()
         {
-            if (!isInitialized)
+            if (selectedSO == null)
                 return;
 
             HandleDragAndZoomInput();
@@ -63,15 +67,19 @@ namespace BansheeEditor
         private void OnDestroy()
         {
             Selection.OnSelectionChanged -= OnSelectionChanged;
-            EditorInput.OnPointerPressed -= OnPointerPressed;
-            EditorInput.OnPointerMoved -= OnPointerMoved;
-            EditorInput.OnPointerReleased -= OnPointerReleased;
-            EditorInput.OnButtonUp -= OnButtonUp;
+
+            if (selectedSO != null)
+            {
+                EditorInput.OnPointerPressed -= OnPointerPressed;
+                EditorInput.OnPointerMoved -= OnPointerMoved;
+                EditorInput.OnPointerReleased -= OnPointerReleased;
+                EditorInput.OnButtonUp -= OnButtonUp;
+            }
         }
 
         protected override void WindowResized(int width, int height)
         {
-            if (!isInitialized)
+            if (selectedSO == null)
                 return;
 
             ResizeGUI(width, height);
@@ -110,15 +118,6 @@ namespace BansheeEditor
         private void RebuildGUI()
         {
             GUI.Clear();
-            selectedFields.Clear();
-            curves.Clear();
-            isInitialized = false;
-
-            if (selectedSO != Selection.SceneObject)
-            {
-                zoomAmount = 0.0f;
-                selectedSO = Selection.SceneObject;
-            }
 
             if (selectedSO == null)
             {
@@ -136,9 +135,6 @@ namespace BansheeEditor
                 return;
             }
 
-            // TODO - Retrieve Animation & AnimationClip from the selected object, fill curves dictionary
-            //  - If not available, show a button to create new animation clip
-
             // Top button row
             GUIContent playIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Play),
                 new LocEdString("Play"));
@@ -203,7 +199,7 @@ namespace BansheeEditor
 
             addEventButton.OnClick += () =>
             {
-                // TODO - Add event
+                guiCurveEditor.AddEventAtMarker();
             };
 
             optionsButton.OnClick += () =>
@@ -219,23 +215,90 @@ namespace BansheeEditor
 
             addPropertyBtn.OnClick += () =>
             {
-                Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition);
-                FieldSelectionWindow fieldSelection = DropDownWindow.Open<FieldSelectionWindow>(this, windowPos);
-                fieldSelection.OnFieldSelected += OnFieldAdded;
+                Action openPropertyWindow = () =>
+                {
+                    Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition);
+                    FieldSelectionWindow fieldSelection = DropDownWindow.Open<FieldSelectionWindow>(this, windowPos);
+                    fieldSelection.OnFieldSelected += OnFieldAdded;
+                };
+
+                if (clipInfo.clip == null)
+                {
+                    LocEdString title = new LocEdString("Warning");
+                    LocEdString message =
+                        new LocEdString("Selected object doesn't have an animation clip assigned. Would you like to create" +
+                                        " a new animation clip?");
+
+                    DialogBox.Open(title, message, DialogBox.Type.YesNoCancel, type =>
+                    {
+                        if (type == DialogBox.ResultType.Yes)
+                        {
+                            string[] clipSavePaths;
+                            if (BrowseDialog.OpenFile(ProjectLibrary.ResourceFolder, "", false, out clipSavePaths))
+                            {
+                                if (clipSavePaths.Length > 0)
+                                {
+                                    AnimationClip newClip = new AnimationClip();
+
+                                    ProjectLibrary.Create(newClip, clipSavePaths[0]);
+                                    LoadAnimClip(newClip);
+
+                                    Animation animation = selectedSO.GetComponent<Animation>();
+                                    if (animation == null)
+                                        animation = selectedSO.AddComponent<Animation>();
+
+                                    animation.DefaultClip = newClip;
+                                    EditorApplication.SetSceneDirty();
+
+                                    openPropertyWindow();
+                                }
+                            }
+                        }
+                    });
+                }
+                else
+                {
+                    if (IsClipImported(clipInfo.clip))
+                    {
+                        LocEdString title = new LocEdString("Warning");
+                        LocEdString message =
+                            new LocEdString("You cannot add/edit/remove curves from animation clips that" +
+                                            " are imported from an external file.");
+
+                        DialogBox.Open(title, message, DialogBox.Type.OK);
+                    }
+                    else
+                        openPropertyWindow();
+                }
             };
 
             delPropertyBtn.OnClick += () =>
             {
-                LocEdString title = new LocEdString("Warning");
-                LocEdString message = new LocEdString("Are you sure you want to remove all selected fields?");
+                if (clipInfo.clip == null)
+                    return;
 
-                DialogBox.Open(title, message, DialogBox.Type.YesNo, x =>
+                if (IsClipImported(clipInfo.clip))
                 {
-                    if (x == DialogBox.ResultType.Yes)
+                    LocEdString title = new LocEdString("Warning");
+                    LocEdString message =
+                        new LocEdString("You cannot add/edit/remove curves from animation clips that" +
+                                        " are imported from an external file.");
+
+                    DialogBox.Open(title, message, DialogBox.Type.OK);
+                }
+                else
+                {
+                    LocEdString title = new LocEdString("Warning");
+                    LocEdString message = new LocEdString("Are you sure you want to remove all selected fields?");
+
+                    DialogBox.Open(title, message, DialogBox.Type.YesNo, x =>
                     {
-                        RemoveSelectedFields();
-                    }
-                });
+                        if (x == DialogBox.ResultType.Yes)
+                        {
+                            RemoveSelectedFields();
+                        }
+                    });
+                }
             };
 
             GUILayout mainLayout = GUI.AddLayoutY();
@@ -290,6 +353,10 @@ namespace BansheeEditor
             Vector2I curveEditorSize = GetCurveEditorSize();
             guiCurveEditor = new GUICurveEditor(this, editorPanel, curveEditorSize.x, curveEditorSize.y);
             guiCurveEditor.OnFrameSelected += OnFrameSelected;
+            guiCurveEditor.OnEventAdded += OnEventsChanged;
+            guiCurveEditor.OnEventModified += EditorApplication.SetProjectDirty;
+            guiCurveEditor.OnEventDeleted += OnEventsChanged;
+            guiCurveEditor.OnCurveModified += EditorApplication.SetProjectDirty;
             guiCurveEditor.Redraw();
 
             horzScrollBar.SetWidth(curveEditorSize.x);
@@ -297,8 +364,6 @@ namespace BansheeEditor
 
             SetCurrentFrame(currentFrameIdx);
             UpdateScrollBarSize();
-
-            isInitialized = true;
         }
 
         private void ResizeGUI(int width, int height)
@@ -334,7 +399,7 @@ namespace BansheeEditor
 
                 Vector2 offset = guiCurveEditor.Offset;
                 offset.x = Math.Max(0.0f, offset.x + dragX);
-                offset.y += dragY;
+                offset.y -= dragY;
                 
                 guiCurveEditor.Offset = offset;
                 UpdateScrollBarSize();
@@ -468,19 +533,94 @@ namespace BansheeEditor
         }
         #endregion
 
-        #region Curve display
-        /// <summary>
-        /// A set of animation curves for a field of a certain type.
-        /// </summary>
-        private struct FieldCurves
+        #region Curve save/load
+        private EditorAnimClipInfo clipInfo;
+
+        private void LoadAnimClip(AnimationClip clip)
         {
-            public SerializableProperty.FieldType type;
-            public EdAnimationCurve[] curves;
+            EditorPersistentData persistentData = EditorApplication.PersistentData;
+
+            bool clipIsImported = IsClipImported(clip);
+            if (persistentData.dirtyAnimClips.TryGetValue(clip.UUID, out clipInfo))
+            {
+                // If an animation clip is imported, we don't care about it's cached curve values as they could have changed
+                // since last modification, so we re-load the clip. But we persist the events as those can only be set
+                // within the editor.
+                if (clipIsImported)
+                {
+                    EditorAnimClipInfo newClipInfo = EditorAnimClipInfo.Create(clip);
+                    newClipInfo.events = clipInfo.events;
+                }
+            }
+            else
+                clipInfo = EditorAnimClipInfo.Create(clip);
+
+            persistentData.dirtyAnimClips[clip.UUID] = clipInfo;
+
+            foreach (var curve in clipInfo.curves)
+                guiFieldDisplay.AddField(curve.Key);
+
+            guiCurveEditor.Events = clipInfo.events;
+            guiCurveEditor.DisableCurveEdit = clipIsImported;
+        }
+
+        private static bool IsClipImported(AnimationClip clip)
+        {
+            string resourcePath = ProjectLibrary.GetPath(clip);
+            return ProjectLibrary.IsSubresource(resourcePath);
+        }
+
+        private void UpdateSelectedSO(bool force)
+        {
+            SceneObject so = Selection.SceneObject;
+            if (selectedSO != so || force)
+            {
+                if (selectedSO != null && so == null)
+                {
+                    EditorInput.OnPointerPressed -= OnPointerPressed;
+                    EditorInput.OnPointerMoved -= OnPointerMoved;
+                    EditorInput.OnPointerReleased -= OnPointerReleased;
+                    EditorInput.OnButtonUp -= OnButtonUp;
+                }
+                else if (selectedSO == null && so != null)
+                {
+                    EditorInput.OnPointerPressed += OnPointerPressed;
+                    EditorInput.OnPointerMoved += OnPointerMoved;
+                    EditorInput.OnPointerReleased += OnPointerReleased;
+                    EditorInput.OnButtonUp += OnButtonUp;
+                }
+
+                zoomAmount = 0.0f;
+                selectedSO = so;
+                selectedFields.Clear();
+
+                RebuildGUI();
+
+                clipInfo = null;
+
+                // Load existing clip if one exists
+                if (selectedSO != null)
+                {
+                    Animation animation = selectedSO.GetComponent<Animation>();
+                    if (animation != null)
+                    {
+                        AnimationClip clip = animation.DefaultClip;
+                        if (clip != null)
+                            LoadAnimClip(clip);
+                    }
+                }
+
+                if(clipInfo == null)
+                    clipInfo = new EditorAnimClipInfo();
+            }
         }
 
+        #endregion
+
+        #region Curve display
+
         private int currentFrameIdx;
         private int fps = 1;
-        private Dictionary<string, FieldCurves> curves = new Dictionary<string, FieldCurves>();
 
         internal int FPS
         {
@@ -498,9 +638,10 @@ namespace BansheeEditor
             float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
 
             List<GUIAnimFieldPathValue> values = new List<GUIAnimFieldPathValue>();
-            foreach (var kvp in curves)
+            foreach (var kvp in clipInfo.curves)
             {
-                SerializableProperty property = GUIAnimFieldDisplay.FindProperty(selectedSO, kvp.Key);
+                string suffix;
+                SerializableProperty property = Animation.FindProperty(selectedSO, kvp.Key, out suffix);
                 if (property != null)
                 {
                     GUIAnimFieldPathValue fieldValue = new GUIAnimFieldPathValue();
@@ -623,7 +764,7 @@ namespace BansheeEditor
             {
                 case SerializableProperty.FieldType.Vector4:
                     {
-                        FieldCurves fieldCurves = new FieldCurves();
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
                         fieldCurves.type = type;
                         fieldCurves.curves = new EdAnimationCurve[4];
 
@@ -635,12 +776,12 @@ namespace BansheeEditor
                             selectedFields.Add(subFieldPath);
                         }
 
-                        curves[path] = fieldCurves;
+                        clipInfo.curves[path] = fieldCurves;
                     }
                     break;
                 case SerializableProperty.FieldType.Vector3:
                     {
-                        FieldCurves fieldCurves = new FieldCurves();
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
                         fieldCurves.type = type;
                         fieldCurves.curves = new EdAnimationCurve[3];
 
@@ -652,12 +793,12 @@ namespace BansheeEditor
                             selectedFields.Add(subFieldPath);
                         }
 
-                        curves[path] = fieldCurves;
+                        clipInfo.curves[path] = fieldCurves;
                     }
                     break;
                 case SerializableProperty.FieldType.Vector2:
                     {
-                        FieldCurves fieldCurves = new FieldCurves();
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
                         fieldCurves.type = type;
                         fieldCurves.curves = new EdAnimationCurve[2];
 
@@ -669,12 +810,12 @@ namespace BansheeEditor
                             selectedFields.Add(subFieldPath);
                         }
 
-                        curves[path] = fieldCurves;
+                        clipInfo.curves[path] = fieldCurves;
                     }
                     break;
                 case SerializableProperty.FieldType.Color:
                     {
-                        FieldCurves fieldCurves = new FieldCurves();
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
                         fieldCurves.type = type;
                         fieldCurves.curves = new EdAnimationCurve[4];
 
@@ -686,23 +827,24 @@ namespace BansheeEditor
                             selectedFields.Add(subFieldPath);
                         }
 
-                        curves[path] = fieldCurves;
+                        clipInfo.curves[path] = fieldCurves;
                     }
                     break;
                 default: // Primitive type
                     {
-                        FieldCurves fieldCurves = new FieldCurves();
+                        FieldAnimCurves fieldCurves = new FieldAnimCurves();
                         fieldCurves.type = type;
                         fieldCurves.curves = new EdAnimationCurve[1];
 
                         fieldCurves.curves[0] = new EdAnimationCurve();
                         selectedFields.Add(path);
 
-                        curves[path] = fieldCurves;
+                        clipInfo.curves[path] = fieldCurves;
                     }
                     break;
             }
 
+            EditorApplication.SetProjectDirty();
             UpdateDisplayedCurves();
         }
 
@@ -727,16 +869,17 @@ namespace BansheeEditor
             for (int i = 0; i < selectedFields.Count; i++)
             {
                 selectedFields.Remove(selectedFields[i]);
-                curves.Remove(GetSubPathParent(selectedFields[i]));
+                clipInfo.curves.Remove(GetSubPathParent(selectedFields[i]));
             }
 
             List<string> existingFields = new List<string>();
-            foreach (var KVP in curves)
+            foreach (var KVP in clipInfo.curves)
                 existingFields.Add(KVP.Key);
 
             guiFieldDisplay.SetFields(existingFields.ToArray());
 
             selectedFields.Clear();
+            EditorApplication.SetProjectDirty();
             UpdateDisplayedCurves();
         }
         #endregion
@@ -783,8 +926,8 @@ namespace BansheeEditor
                 subPathSuffix = path.Substring(index, path.Length - index);
             }
 
-            FieldCurves fieldCurves;
-            if (curves.TryGetValue(parentPath, out fieldCurves))
+            FieldAnimCurves fieldCurves;
+            if (clipInfo.curves.TryGetValue(parentPath, out fieldCurves))
             {
                 if (!string.IsNullOrEmpty(subPathSuffix))
                 {
@@ -852,9 +995,6 @@ namespace BansheeEditor
         #region Input callbacks
         private void OnPointerPressed(PointerEvent ev)
         {
-            if (!isInitialized)
-                return;
-
             guiCurveEditor.OnPointerPressed(ev);
 
             if (ev.button == PointerButton.Middle)
@@ -871,9 +1011,6 @@ namespace BansheeEditor
 
         private void OnPointerMoved(PointerEvent ev)
         {
-            if (!isInitialized)
-                return;
-
             guiCurveEditor.OnPointerMoved(ev);
 
             if (isButtonHeld)
@@ -909,17 +1046,11 @@ namespace BansheeEditor
             isButtonHeld = false;
             isDragInProgress = false;
 
-            if (!isInitialized)
-                return;
-
             guiCurveEditor.OnPointerReleased(ev);
         }
 
         private void OnButtonUp(ButtonEvent ev)
         {
-            if (!isInitialized)
-                return;
-
             guiCurveEditor.OnButtonUp(ev);
         }
         #endregion
@@ -927,7 +1058,15 @@ namespace BansheeEditor
         #region General callbacks
         private void OnFieldAdded(string path, SerializableProperty.FieldType type)
         {
-            AddNewField(path, type);
+            // Remove the root scene object from the path (we know which SO it is, no need to hardcode its name in the path)
+            string pathNoRoot = path.TrimStart('/');
+            int separatorIdx = pathNoRoot.IndexOf("/");
+            if (separatorIdx == -1 || (separatorIdx + 1) >= pathNoRoot.Length)
+                return;
+
+            pathNoRoot = pathNoRoot.Substring(separatorIdx + 1, pathNoRoot.Length - separatorIdx - 1);
+
+            AddNewField(pathNoRoot, type);
         }
 
         private void OnHorzScrollOrResize(float position, float size)
@@ -948,13 +1087,19 @@ namespace BansheeEditor
 
         private void OnSelectionChanged(SceneObject[] sceneObjects, string[] resourcePaths)
         {
-            RebuildGUI();
+            UpdateSelectedSO(false);
         }
 
         private void OnFrameSelected(int frameIdx)
         {
             SetCurrentFrame(frameIdx);
         }
+
+        private void OnEventsChanged()
+        {
+            clipInfo.events = guiCurveEditor.Events;
+            EditorApplication.SetProjectDirty();
+        }
         #endregion
     }
 

+ 10 - 5
Source/MBansheeEditor/Windows/Scene/SceneCamera.cs

@@ -55,7 +55,7 @@ namespace BansheeEditor
         private float currentSpeed;
         private Degree yaw;
         private Degree pitch;
-        private bool lastButtonState;
+        private bool lastHideCursorState;
         private Camera camera;
         private bool inputEnabled = true;
 
@@ -168,7 +168,7 @@ namespace BansheeEditor
                 bool isPanning = VirtualInput.IsButtonHeld(panBtn);
 
                 bool hideCursor = camActive || isPanning;
-                if (hideCursor != lastButtonState)
+                if (hideCursor != lastHideCursorState)
                 {
                     if (hideCursor)
                     {
@@ -188,7 +188,7 @@ namespace BansheeEditor
                         Cursor.ClipDisable();
                     }
 
-                    lastButtonState = hideCursor;
+                    lastHideCursorState = hideCursor;
                 }
 
                 float frameDelta = Time.FrameDelta;
@@ -261,8 +261,13 @@ namespace BansheeEditor
             }
             else
             {
-                Cursor.Show();
-                Cursor.ClipDisable();
+                if (lastHideCursorState)
+                {
+                    Cursor.Show();
+                    Cursor.ClipDisable();
+
+                    lastHideCursorState = false;
+                }
             }
 
             SceneWindow sceneWindow = EditorWindow.GetWindow<SceneWindow>();

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

@@ -1,7 +1,9 @@
 //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
 //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
 using System;
+using System.Collections.Generic;
 using System.Runtime.InteropServices;
+using System.Text;
 
 namespace BansheeEngine
 {
@@ -104,8 +106,27 @@ namespace BansheeEngine
     {
         private NativeAnimation _native;
 
-        [SerializeField]
-        private SerializableData serializableData = new SerializableData();
+        [SerializeField] private SerializableData serializableData = new SerializableData();
+
+        private FloatCurvePropertyInfo[] floatProperties;
+        private List<SceneObjectMappingInfo> mappingInfo = new List<SceneObjectMappingInfo>();
+        private AnimationClip primaryClip;
+
+        /// <summary>
+        /// Contains mapping for a suffix used by property paths used for curve identifiers, to their index and type.
+        /// </summary>
+        internal static readonly Dictionary<string, PropertySuffixInfo> PropertySuffixInfos = new Dictionary
+            <string, PropertySuffixInfo>
+        {
+            {".x", new PropertySuffixInfo(0, true)},
+            {".y", new PropertySuffixInfo(1, true)},
+            {".z", new PropertySuffixInfo(2, true)},
+            {".w", new PropertySuffixInfo(3, true)},
+            {".r", new PropertySuffixInfo(0, false)},
+            {".g", new PropertySuffixInfo(1, false)},
+            {".b", new PropertySuffixInfo(2, false)},
+            {".a", new PropertySuffixInfo(3, false)}
+        };
 
         /// <summary>
         /// Returns the non-component version of Animation that is wrapped by this component. 
@@ -185,7 +206,7 @@ namespace BansheeEngine
         /// <param name="clip">Clip to play.</param>
         public void Play(AnimationClip clip)
         {
-            if(_native != null)
+            if (_native != null)
                 _native.Play(clip);
         }
 
@@ -285,6 +306,201 @@ namespace BansheeEngine
             return false;
         }
 
+        /// <summary>
+        /// Searches the scene object hierarchy to find a property at the given path.
+        /// </summary>
+        /// <param name="root">Root scene object to which the path is relative to.</param>
+        /// <param name="path">Path to the property, where each element of the path is separated with "/". 
+        /// 
+        ///                    Path elements prefixed with "!" signify names of child scene objects (first one relative to 
+        ///                    <paramref name="root"/>. Name of the root element should not be included in the path. 
+        /// 
+        ///                    Path element prefixed with ":" signify names of components. If a path doesn't have a
+        ///                    component element, it is assumed the field is relative to the scene object itself (only 
+        ///                    "Translation", "Rotation" and "Scale fields are supported in such case). Only one component
+        ///                    path element per path is allowed.
+        /// 
+        ///                    Path entries with no prefix are considered regular script object fields. Each path must have
+        ///                    at least one such entry. Last field entry can optionally have a suffix separated from the
+        ///                    path name with ".". This suffix is not parsed internally, but will be returned as 
+        ///                    <paramref name="suffix"/>.
+        /// 
+        ///                    Path examples:
+        ///                     :MyComponent/myInt (path to myInt variable on a component attached to this object)
+        ///                     !childSO/:MyComponent/myInt (path to myInt variable on a child scene object)
+        ///                     !childSO/Translation (path to the scene object translation)
+        ///                     :MyComponent/myVector.z (path to the z component of myVector on this object)
+        ///                    </param>
+        /// <param name="suffix">Suffix of the last field entry, if it has any. Contains the suffix separator ".".</param>
+        /// <returns>If found, property object you can use for setting and getting the value from the property, otherwise 
+        ///          null.</returns>
+        internal static SerializableProperty FindProperty(SceneObject root, string path, out string suffix)
+        {
+            suffix = null;
+
+            if (string.IsNullOrEmpty(path) || root == null)
+                return null;
+
+            string trimmedPath = path.Trim('/');
+            string[] entries = trimmedPath.Split('/');
+
+            // Find scene object referenced by the path
+            SceneObject so = root;
+            int pathIdx = 0;
+            for (; pathIdx < entries.Length; pathIdx++)
+            {
+                string entry = entries[pathIdx];
+
+                if (string.IsNullOrEmpty(entry))
+                    continue;
+
+                // Not a scene object, break
+                if (entry[0] != '!')
+                    break;
+
+                string childName = entry.Substring(1, entry.Length - 1);
+                so = so.FindChild(childName);
+
+                if (so == null)
+                    break;
+            }
+
+            // Child scene object couldn't be found
+            if (so == null)
+                return null;
+
+            // Path too short, no field entry
+            if (pathIdx >= entries.Length)
+                return null;
+
+            // Check if path is referencing a component, and if so find it
+            Component component = null;
+            {
+                string entry = entries[pathIdx];
+                if (entry[0] == ':')
+                {
+                    string componentName = entry.Substring(1, entry.Length - 1);
+
+                    Component[] components = so.GetComponents();
+                    component = Array.Find(components, x => x.GetType().Name == componentName);
+
+                    // Cannot find component with specified type
+                    if (component == null)
+                        return null;
+                }
+            }
+
+            // Look for a field within a component
+            if (component != null)
+            {
+                pathIdx++;
+                if (pathIdx >= entries.Length)
+                    return null;
+
+                SerializableObject componentObj = new SerializableObject(component);
+
+                StringBuilder pathBuilder = new StringBuilder();
+                for (; pathIdx < entries.Length - 1; pathIdx++)
+                    pathBuilder.Append(entries[pathIdx] + "/");
+
+                // Check last path entry for suffix and remove it
+                int suffixIdx = entries[pathIdx].LastIndexOf(".");
+                if (suffixIdx != -1)
+                {
+                    string entryNoSuffix = entries[pathIdx].Substring(0, suffixIdx);
+                    suffix = entries[pathIdx].Substring(suffixIdx, entries[pathIdx].Length - suffixIdx);
+
+                    pathBuilder.Append(entryNoSuffix);
+                }
+                else
+                    pathBuilder.Append(entries[pathIdx]);
+
+                return componentObj.FindProperty(pathBuilder.ToString());
+            }
+            else // Field is one of the builtin ones on the SceneObject itself
+            {
+                if ((pathIdx + 1) < entries.Length)
+                    return null;
+
+                string entry = entries[pathIdx];
+                if (entry == "Position")
+                {
+                    SerializableProperty property = new SerializableProperty(
+                        SerializableProperty.FieldType.Vector3,
+                        typeof(Vector3),
+                        () => so.LocalPosition,
+                        (x) => so.LocalPosition = (Vector3) x);
+
+                    return property;
+                }
+                else if (entry == "Rotation")
+                {
+                    SerializableProperty property = new SerializableProperty(
+                        SerializableProperty.FieldType.Vector3,
+                        typeof(Vector3),
+                        () => so.LocalRotation.ToEuler(),
+                        (x) => so.LocalRotation = Quaternion.FromEuler((Vector3) x));
+
+                    return property;
+                }
+                else if (entry == "Scale")
+                {
+                    SerializableProperty property = new SerializableProperty(
+                        SerializableProperty.FieldType.Vector3,
+                        typeof(Vector3),
+                        () => so.LocalScale,
+                        (x) => so.LocalScale = (Vector3) x);
+
+                    return property;
+                }
+
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Searches the scene object hierarchy to find a child scene object using the provided path.
+        /// </summary>
+        /// <param name="root">Root scene object to which the path is relative to.</param>
+        /// <param name="path">Path to the property, where each element of the path is separated with "/". 
+        /// 
+        ///                    Path elements signify names of child scene objects (first one relative to 
+        ///                    <paramref name="root"/>. Name of the root element should not be included in the path.
+        ///                    Elements must be prefixed with "!" in order to match the path format of 
+        ///                    <see cref="FindProperty"/>.</param>
+        /// <returns>Child scene object if found, or null otherwise.</returns>
+        internal static SceneObject FindSceneObject(SceneObject root, string path)
+        {
+            if (string.IsNullOrEmpty(path) || root == null)
+                return null;
+
+            string trimmedPath = path.Trim('/');
+            string[] entries = trimmedPath.Split('/');
+
+            // Find scene object referenced by the path
+            SceneObject so = root;
+            int pathIdx = 0;
+            for (; pathIdx < entries.Length; pathIdx++)
+            {
+                string entry = entries[pathIdx];
+
+                if (string.IsNullOrEmpty(entry))
+                    continue;
+
+                // Not a scene object, break
+                if (entry[0] != '!')
+                    break;
+
+                string childName = entry.Substring(1, entry.Length - 1);
+                so = so.FindChild(childName);
+
+                if (so == null)
+                    break;
+            }
+
+            return so;
+        }
+
         /// <summary>
         /// Changes the state of a playing animation clip. If animation clip is not currently playing the state change is
         /// ignored.
@@ -297,6 +513,32 @@ namespace BansheeEngine
                 _native.SetState(clip, state);
         }
 
+        private void OnUpdate()
+        {
+            if (_native == null)
+                return;
+
+            AnimationClip newPrimaryClip = _native.GetClip(0);
+            if (newPrimaryClip != primaryClip)
+            {
+                RebuildFloatProperties(newPrimaryClip);
+                primaryClip = newPrimaryClip;
+
+                UpdateSceneObjectMapping();
+            }
+
+            // Apply values from generic float curves
+            if (floatProperties != null)
+            {
+                foreach (var entry in floatProperties)
+                {
+                    float curveValue;
+                    if (_native.GetGenericCurveValue(entry.curveIdx, out curveValue))
+                        entry.setter(curveValue);
+                }
+            }
+        }
+
         private void OnEnable()
         {
             RestoreNative();
@@ -330,6 +572,13 @@ namespace BansheeEngine
             if (serializableData.defaultClip != null)
                 _native.Play(serializableData.defaultClip);
 
+            primaryClip = _native.GetClip(0);
+            if (primaryClip != null)
+                RebuildFloatProperties(primaryClip);
+
+            SetBoneMappings();
+            UpdateSceneObjectMapping();
+
             Renderable renderable = SceneObject.GetComponent<Renderable>();
             if (renderable == null)
                 return;
@@ -358,6 +607,302 @@ namespace BansheeEngine
                 _native.Destroy();
                 _native = null;
             }
+
+            primaryClip = null;
+            mappingInfo.Clear();
+            floatProperties = null;
+        }
+
+        /// <summary>
+        /// Finds any curves that affect a transform of a specific scene object, and ensures that animation properly updates
+        /// those transforms. This does not include curves referencing bones.
+        /// </summary>
+        private void UpdateSceneObjectMapping()
+        {
+            List<SceneObjectMappingInfo> newMappingInfos = new List<SceneObjectMappingInfo>();
+            foreach(var entry in mappingInfo)
+            {
+                if (entry.isMappedToBone)
+                    newMappingInfos.Add(entry);
+                else
+                    _native.UnmapSceneObject(entry.sceneObject);
+            }
+
+            if (primaryClip != null)
+            {
+                SceneObject root = SceneObject;
+                AnimationCurves curves = primaryClip.Curves;
+                foreach (var curve in curves.PositionCurves)
+                {
+                    if (curve.Flags.HasFlag(AnimationCurveFlags.ImportedCurve))
+                        continue;
+
+                    SceneObject currentSO = FindSceneObject(root, curve.Name);
+
+                    bool found = false;
+                    for (int i = 0; i < newMappingInfos.Count; i++)
+                    {
+                        if (newMappingInfos[i].sceneObject == currentSO)
+                        {
+                            found = true;
+                            break;
+                        }
+                    }
+
+                    if (!found)
+                    {
+                        SceneObjectMappingInfo newMappingInfo = new SceneObjectMappingInfo();
+                        newMappingInfo.isMappedToBone = false;
+                        newMappingInfo.sceneObject = currentSO;
+
+                        newMappingInfos.Add(newMappingInfo);
+
+                        _native.MapCurveToSceneObject(curve.Name, currentSO);
+                    }
+                }
+            }
+
+            mappingInfo = newMappingInfos;
+        }
+
+        /// <summary>
+        /// Registers a new bone component, creating a new transform mapping from the bone name to the scene object
+        /// the component is attached to.
+        /// </summary>
+        /// <param name="bone">Bone component to register.</param>
+        internal void AddBone(Bone bone)
+        {
+            if (_native == null)
+                return;
+
+            SceneObject currentSO = bone.SceneObject;
+
+            SceneObjectMappingInfo newMapping = new SceneObjectMappingInfo();
+            newMapping.sceneObject = currentSO;
+            newMapping.isMappedToBone = true;
+            newMapping.bone = bone;
+
+            mappingInfo.Add(newMapping);
+            _native.MapCurveToSceneObject(bone.Name, newMapping.sceneObject);
+        }
+
+        /// <summary>
+        /// Unregisters a bone component, removing the bone -> scene object mapping.
+        /// </summary>
+        /// <param name="bone">Bone to unregister.</param>
+        internal void RemoveBone(Bone bone)
+        {
+            if (_native == null)
+                return;
+
+            SceneObject newSO = null;
+            for (int i = 0; i < mappingInfo.Count; i++)
+            {
+                if (mappingInfo[i].bone == bone)
+                {
+                    mappingInfo.RemoveAt(i);
+                    _native.UnmapSceneObject(mappingInfo[i].sceneObject);
+                    i--;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called whenever the bone the <see cref="Bone"/> component points to changed.
+        /// </summary>
+        /// <param name="bone">Bone component to modify.</param>
+        internal void NotifyBoneChanged(Bone bone)
+        {
+            if (_native == null)
+                return;
+
+            for (int i = 0; i < mappingInfo.Count; i++)
+            {
+                if (mappingInfo[i].bone == bone)
+                {
+                    _native.UnmapSceneObject(mappingInfo[i].sceneObject);
+                    _native.MapCurveToSceneObject(bone.Name, mappingInfo[i].sceneObject);
+                    break;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Finds any scene objects that are mapped to bone transforms. Such object's transforms will be affected by
+        /// skeleton bone animation.
+        /// </summary>
+        private void SetBoneMappings()
+        {
+            mappingInfo.Clear();
+
+            SceneObjectMappingInfo rootMapping = new SceneObjectMappingInfo();
+            rootMapping.sceneObject = SceneObject;
+            rootMapping.isMappedToBone = true;
+
+            mappingInfo.Add(rootMapping);
+            _native.MapCurveToSceneObject("", rootMapping.sceneObject);
+
+            Bone[] childBones = FindChildBones();
+            foreach (var entry in childBones)
+                AddBone(entry);
+        }
+
+        /// <summary>
+        /// Searches child scene objects for <see cref="Bone"/> components and returns any found ones.
+        /// </summary>
+        private Bone[] FindChildBones()
+        {
+            Stack<SceneObject> todo = new Stack<SceneObject>();
+            todo.Push(SceneObject);
+
+            List<Bone> bones = new List<Bone>();
+            while (todo.Count > 0)
+            {
+                SceneObject currentSO = todo.Pop();
+
+                Bone bone = currentSO.GetComponent<Bone>();
+                if (bone != null)
+                {
+                    bone.SetParent(this, true);
+                    bones.Add(bone);
+                }
+
+                int childCount = currentSO.GetNumChildren();
+                for (int i = 0; i < childCount; i++)
+                {
+                    SceneObject child = currentSO.GetChild(i);
+                    if (child.GetComponent<Animation>() != null)
+                        continue;
+
+                    todo.Push(child);
+                }
+            }
+
+            return bones.ToArray();
+        }
+
+        /// <summary>
+        /// Builds a list of properties that will be animated using float animation curves.
+        /// </summary>
+        /// <param name="clip">Clip to retrieve the float animation curves from.</param>
+        private void RebuildFloatProperties(AnimationClip clip)
+        {
+            if (clip == null)
+            {
+                floatProperties = null;
+                return;
+            }
+
+            AnimationCurves curves = clip.Curves;
+
+            List<FloatCurvePropertyInfo> newFloatProperties = new List<FloatCurvePropertyInfo>();
+            for (int i = 0; i < curves.FloatCurves.Length; i++)
+            {
+                string suffix;
+                SerializableProperty property = FindProperty(SceneObject, curves.FloatCurves[i].Name, out suffix);
+                if (property == null)
+                    continue;
+
+                int elementIdx = 0;
+                if (!string.IsNullOrEmpty(suffix))
+                {
+                    PropertySuffixInfo suffixInfo;
+                    if (PropertySuffixInfos.TryGetValue(suffix, out suffixInfo))
+                        elementIdx = suffixInfo.elementIdx;
+                }
+
+                Action<float> setter = null;
+
+                Type internalType = property.InternalType;
+                switch (property.Type)
+                {
+                    case SerializableProperty.FieldType.Vector2:
+                        if (internalType == typeof(Vector2))
+                        {
+                            setter = f =>
+                            {
+                                Vector2 value = property.GetValue<Vector2>();
+                                value[elementIdx] = f;
+                                property.SetValue(value);
+                            };
+                        }
+
+                        break;
+                    case SerializableProperty.FieldType.Vector3:
+                        if (internalType == typeof(Vector3))
+                        {
+                            setter = f =>
+                            {
+                                Vector3 value = property.GetValue<Vector3>();
+                                value[elementIdx] = f;
+                                property.SetValue(value);
+                            };
+                        }
+                        break;
+                    case SerializableProperty.FieldType.Vector4:
+                        if (internalType == typeof(Vector4))
+                        {
+                            setter = f =>
+                            {
+                                Vector4 value = property.GetValue<Vector4>();
+                                value[elementIdx] = f;
+                                property.SetValue(value);
+                            };
+                        }
+                        else if (internalType == typeof(Quaternion))
+                        {
+                            setter = f =>
+                            {
+                                Quaternion value = property.GetValue<Quaternion>();
+                                value[elementIdx] = f;
+                                property.SetValue(value);
+                            };
+                        }
+                        break;
+                    case SerializableProperty.FieldType.Color:
+                        if (internalType == typeof(Color))
+                        {
+                            setter = f =>
+                            {
+                                Color value = property.GetValue<Color>();
+                                value[elementIdx] = f;
+                                property.SetValue(value);
+                            };
+                        }
+                        break;
+                    case SerializableProperty.FieldType.Bool:
+                            setter = f =>
+                            {
+                                bool value = f > 0.0f;
+                                property.SetValue(value);
+                            };
+                        break;
+                    case SerializableProperty.FieldType.Int:
+                        setter = f =>
+                        {
+                            int value = (int)f;
+                            property.SetValue(value);
+                        };
+                        break;
+                    case SerializableProperty.FieldType.Float:
+                        setter = f =>
+                        {
+                            property.SetValue(f);
+                        };
+                        break;
+                }
+
+                if (setter == null)
+                    continue;
+
+                FloatCurvePropertyInfo propertyInfo = new FloatCurvePropertyInfo();
+                propertyInfo.curveIdx = i;
+                propertyInfo.setter = setter;
+
+                newFloatProperties.Add(propertyInfo);
+            }
+
+            floatProperties = newFloatProperties.ToArray();
         }
 
         /// <summary>
@@ -367,7 +912,26 @@ namespace BansheeEngine
         /// <param name="name">Name of the event.</param>
         private void EventTriggered(AnimationClip clip, string name)
         {
-            // TODO - Find a scene object, component and method based on the event name, and call it
+            // Event should be in format "ComponentType/MethodName"
+            if (string.IsNullOrEmpty(name))
+                return;
+
+            string[] nameEntries = name.Split('/');
+            if (nameEntries.Length != 2)
+                return;
+
+            string typeName = nameEntries[0];
+            string methodName = nameEntries[1];
+
+            Component[] components = SceneObject.GetComponents();
+            for (int i = 0; i < components.Length; i++)
+            {
+                if (components[i].GetType().Name == typeName)
+                {
+                    components[i].Invoke(methodName);
+                    break;
+                }
+            }
         }
 
         /// <summary>
@@ -380,6 +944,40 @@ namespace BansheeEngine
             public AnimWrapMode wrapMode = AnimWrapMode.Loop;
             public float speed = 1.0f;
         }
+
+        /// <summary>
+        /// Contains information about a property animated by a generic animation curve.
+        /// </summary>
+        private class FloatCurvePropertyInfo
+        {
+            public int curveIdx;
+            public Action<float> setter;
+        }
+
+        /// <summary>
+        /// Information about a suffix used in a property path.
+        /// </summary>
+        internal struct PropertySuffixInfo
+        {
+            public PropertySuffixInfo(int elementIdx, bool isVector)
+            {
+                this.elementIdx = elementIdx;
+                this.isVector = isVector;
+            }
+
+            public int elementIdx;
+            public bool isVector;
+        }
+
+        /// <summary>
+        /// Information about scene objects bound to a specific animation curve.
+        /// </summary>
+        internal struct SceneObjectMappingInfo
+        {
+            public SceneObject sceneObject;
+            public bool isMappedToBone;
+            public Bone bone;
+        }
     }
 
     /** @} */

+ 14 - 5
Source/MBansheeEngine/Animation/AnimationClip.cs

@@ -15,6 +15,18 @@ namespace BansheeEngine
     /// </summary>
     public class AnimationClip : Resource
     {
+        // Constructor for runtime use only (dummy parameter to differentiate from the normal constructor)
+        private AnimationClip(bool dummy)
+        { }
+
+        /// <summary>
+        /// Creates a new animation clip with no curves or events.
+        /// </summary>
+        public AnimationClip()
+        {
+            Internal_CreateInstance(this);
+        }
+
         /// <summary>
         /// Returns the length of the animation clip, in seconds.
         /// </summary>
@@ -41,11 +53,8 @@ namespace BansheeEngine
             set { Internal_SetAnimationEvents(mCachedPtr, value); }
         }
 
-        /// <summary>
-        /// Constructor for internal use by the runtime.
-        /// </summary>
-        private AnimationClip()
-        { }
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void Internal_CreateInstance(AnimationClip instance);
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern AnimationCurves Internal_GetAnimationCurves(IntPtr thisPtr);

+ 54 - 1
Source/MBansheeEngine/Animation/AnimationCurve.cs

@@ -30,6 +30,19 @@ namespace BansheeEngine
         public float time;
     }
 
+    /// <summary>
+    /// Flags that describe an <see cref="AnimationCurve"/>
+    /// </summary>
+    public enum AnimationCurveFlags // Note: Must match C++ enum AnimationCurveFlags
+    {
+        /// <summary>
+        /// If enabled, the curve was imported from an external file and not created within the engine. This will affect
+        /// how are animation results applied to scene objects (with imported animations it is assumed the curve is
+        /// animating bones and with in-engine curves it is assumed the curve is animating scene objects).
+        /// </summary>
+        ImportedCurve
+    }
+
     /// <summary>
     /// Animation spline represented by a set of keyframes, each representing an endpoint of a cubic hermite curve. The
     /// spline can be evaluated at any time, and uses caching to speed up multiple sequential evaluations.
@@ -90,6 +103,23 @@ namespace BansheeEngine
     /// </summary>
     public class NamedVector3Curve
     {
+        /// <summary>
+        /// Constructor for internal runtime use only.
+        /// </summary>
+        /// <param name="name">Name of the curve.</param>
+        /// <param name="flags">Flags that describe the animation curve, of type <see cref="AnimationCurveFlags"/>.</param>
+        /// <param name="x">Curve representing the x axis of the vector.</param>
+        /// <param name="y">Curve representing the y axis of the vector.</param>
+        /// <param name="z">Curve representing the z axis of the vector.</param>
+        private NamedVector3Curve(string name, int flags, AnimationCurve x, AnimationCurve y, AnimationCurve z)
+        {
+            Name = name;
+            Flags = (AnimationCurveFlags) flags;
+            X = x;
+            Y = y;
+            Z = z;
+        }
+
         /// <summary>
         /// Constructs a new named animation curve.
         /// </summary>
@@ -110,6 +140,11 @@ namespace BansheeEngine
         /// </summary>
         public string Name;
 
+        /// <summary>
+        /// Flags that describe the animation curve.
+        /// </summary>
+        public AnimationCurveFlags Flags;
+
         /// <summary>
         /// Animation curve for the x axis.
         /// </summary>
@@ -127,10 +162,23 @@ namespace BansheeEngine
     }
 
     /// <summary>
-    /// An animatio curve for a single floating point value paired with a name.
+    /// An animation curve for a single floating point value paired with a name.
     /// </summary>
     public class NamedFloatCurve
     {
+        /// <summary>
+        /// Constructor for internal runtime use only.
+        /// </summary>
+        /// <param name="name">Name of the curve.</param>
+        /// <param name="flags">Flags that describe the animation curve, of type <see cref="AnimationCurveFlags"/>.</param>
+        /// <param name="curve">Curve representing the floating point values.</param>
+        private NamedFloatCurve(string name, int flags, AnimationCurve curve)
+        {
+            Name = name;
+            Flags = (AnimationCurveFlags)flags;
+            Curve = curve;
+        }
+
         /// <summary>
         /// Constructs a new named animation curve.
         /// </summary>
@@ -147,6 +195,11 @@ namespace BansheeEngine
         /// </summary>
         public string Name;
 
+        /// <summary>
+        /// Flags that describe the animation curve.
+        /// </summary>
+        public AnimationCurveFlags Flags;
+
         /// <summary>
         /// Animation curve.
         /// </summary>

+ 125 - 0
Source/MBansheeEngine/Animation/Bone.cs

@@ -0,0 +1,125 @@
+//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+using System;
+
+namespace BansheeEngine
+{
+    /** @addtogroup Animation
+     *  @{
+     */
+
+    /// <summary>
+    /// Component that maps animation for specific bone also be applied to the <see cref="SceneObject"/> this component
+    /// is attached to. The component will attach to the first found parent <see cref="Animation"/> component.
+    /// </summary>
+    public class Bone : Component
+    {
+        [SerializeField]
+        private string name;
+
+        private Animation parent;
+
+        /// <summary>
+        /// Name of the bone to attach to.
+        /// </summary>
+        public string Name
+        {
+            get { return name; }
+            set
+            {
+                if (name != value)
+                {
+                    name = value;
+
+                    if (parent != null)
+                        parent.NotifyBoneChanged(this);
+                }
+            }
+        }
+
+        private void OnInitialize()
+        {
+            NotifyFlags = TransformChangedFlags.Transform | TransformChangedFlags.Parent;
+        }
+
+        private void OnEnable()
+        {
+            UpdateParentAnimation();
+        }
+
+        private void OnDisable()
+        {
+            if (parent != null)
+                parent.RemoveBone(this);
+
+            parent = null;
+        }
+
+        private void OnDestroy()
+        {
+            if (parent != null)
+                parent.RemoveBone(this);
+
+            parent = null;
+        }
+
+        private void OnTransformChanged(TransformChangedFlags flags)
+        {
+            if (!SceneObject.Active)
+                return;
+
+            if ((flags & TransformChangedFlags.Parent) != 0)
+                UpdateParentAnimation();
+        }
+
+        /// <summary>
+        /// Attempts to find the parent <see cref="Animation"/> component and registers itself with it.
+        /// </summary>
+        private void UpdateParentAnimation()
+        {
+            SceneObject currentSO = SceneObject;
+            while (currentSO != null)
+            {
+                Animation parent = currentSO.GetComponent<Animation>();
+                if (parent != null)
+                {
+                    if (currentSO.Active)
+                        SetParent(parent);
+                    else
+                        SetParent(null);
+
+                    return;
+                }
+
+                currentSO = currentSO.Parent;
+            }
+
+            SetParent(null);
+        }
+
+        /// <summary>
+        /// Changes the parent animation of this component.
+        /// </summary>
+        /// <param name="animation">New animation parent, can be null.</param>
+        /// <param name="isInternal">If true the bone will just be changed internally, but parent animation will not be
+        ///                          notified.</param>
+        internal void SetParent(Animation animation, bool isInternal = false)
+        {
+            if (animation == parent)
+                return;
+
+            if (!isInternal)
+            {
+                if (parent != null)
+                    parent.RemoveBone(this);
+
+                if (animation != null)
+                    animation.AddBone(this);
+            }
+
+            parent = animation;
+        }
+    }
+
+    /** @} */
+}

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

@@ -23,6 +23,11 @@ namespace BansheeEngine
             set { Internal_SetSpeed(mCachedPtr, value); }
         }
 
+        public int NumClips
+        {
+            get { return Internal_GetNumClips(mCachedPtr); }
+        }
+
         public Action<AnimationClip, string> OnEventTriggered;
 
         public void Play(AnimationClip clip)
@@ -83,6 +88,11 @@ namespace BansheeEngine
             return Internal_IsPlaying(mCachedPtr);
         }
 
+        public AnimationClip GetClip(int index)
+        {
+            return Internal_GetClip(mCachedPtr, index);
+        }
+
         public bool GetState(AnimationClip clip, out AnimationClipState state)
         {
             IntPtr clipPtr = IntPtr.Zero;
@@ -101,6 +111,27 @@ namespace BansheeEngine
             Internal_SetState(mCachedPtr, clipPtr, ref state);
         }
 
+        public bool GetGenericCurveValue(int curveIdx, out float value)
+        {
+            return Internal_GetGenericCurveValue(mCachedPtr, curveIdx, out value);
+        }
+
+        public void MapCurveToSceneObject(string curve, SceneObject sceneObject)
+        {
+            if (string.IsNullOrEmpty(curve) || sceneObject == null)
+                return;
+
+            Internal_MapCurveToSceneObject(mCachedPtr, curve, sceneObject.GetCachedPtr());
+        }
+
+        public void UnmapSceneObject(SceneObject sceneObject)
+        {
+            if (sceneObject == null)
+                return;
+
+            Internal_UnmapSceneObject(mCachedPtr, sceneObject.GetCachedPtr());
+        }
+
         internal NativeAnimation()
         {
             Internal_Create(this);
@@ -158,6 +189,21 @@ namespace BansheeEngine
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern void Internal_SetState(IntPtr thisPtr, IntPtr clipPtr, ref AnimationClipState state);
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern int Internal_GetNumClips(IntPtr thisPtr);
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern AnimationClip Internal_GetClip(IntPtr thisPtr, int idx);
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern bool Internal_GetGenericCurveValue(IntPtr thisPtr, int curveIdx, out float value);
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void Internal_MapCurveToSceneObject(IntPtr thisPtr, string curve, IntPtr sceneObjectPtr);
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void Internal_UnmapSceneObject(IntPtr thisPtr, IntPtr sceneObjectPtr);
     }
 
     /** @endcond */

+ 1 - 0
Source/MBansheeEngine/MBansheeEngine.csproj

@@ -47,6 +47,7 @@
     <Compile Include="Animation\AnimationClip.cs" />
     <Compile Include="Animation\AnimationCurve.cs" />
     <Compile Include="Animation\AnimationCurves.cs" />
+    <Compile Include="Animation\Bone.cs" />
     <Compile Include="Animation\Interop\NativeAnimation.cs" />
     <Compile Include="Audio\Audio.cs" />
     <Compile Include="Audio\AudioClip.cs" />

+ 1 - 1
Source/MBansheeEngine/Physics/Collider.cs

@@ -222,7 +222,7 @@ namespace BansheeEngine
         internal abstract NativeCollider CreateCollider();
 
         /// <summary>
-        /// Changes the rigidbody parent of the collider. Meant to be called from the Rigidbody itself. 
+        /// Changes the rigidbody parent of the collider.
         /// </summary>
         /// <param name="rigidbody">New rigidbody to assign as the parent to the collider.</param>
         /// <param name="isInternal">If true the rigidbody will just be changed internally, but parent rigidbody will not be

+ 13 - 1
Source/MBansheeEngine/Scene/Component.cs

@@ -72,7 +72,7 @@ namespace BansheeEngine
         /// <param name="box">Bounds in world space represented as an axis aligned bounding box.</param>
         /// <param name="sphere">Bounds in world space represented as a sphere.</param>
         /// <returns>True if the bounds have non-zero volume, false otherwise.</returns>
-        internal protected virtual bool CalculateBounds(out AABox box, out Sphere sphere)
+        protected internal virtual bool CalculateBounds(out AABox box, out Sphere sphere)
         {
             Vector3 pos = SceneObject.Position;
 
@@ -82,6 +82,15 @@ namespace BansheeEngine
             return false;
         }
 
+        /// <summary>
+        /// Calls a parameterless method with the specified name, on the component. 
+        /// </summary>
+        /// <param name="name">Name of the method to call.</param>
+        protected internal void Invoke(string name)
+        {
+            Internal_Invoke(mCachedPtr, name);
+        }
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern Component Internal_AddComponent(SceneObject parent, Type type);
 
@@ -106,6 +115,9 @@ namespace BansheeEngine
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_SetNotifyFlags(IntPtr nativeInstance, TransformChangedFlags flags);
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        internal static extern void Internal_Invoke(IntPtr nativeInstance, string name);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern void Internal_Destroy(IntPtr nativeInstance, bool immediate);
     }

+ 4 - 0
Source/SBansheeEngine/Include/BsManagedComponent.h

@@ -26,6 +26,9 @@ namespace BansheeEngine
 		/**	Returns managed component object instance. */
 		MonoObject* getManagedInstance() const { return mManagedInstance; }
 
+		/**	Returns managed class of the component. */
+		MonoClass* getClass() const { return mManagedClass; }
+
 		/**	Returns managed type of the component. */
 		MonoReflectionType* getRuntimeType() const { return mRuntimeType; }
 
@@ -90,6 +93,7 @@ namespace BansheeEngine
 		typedef void(__stdcall *OnTransformChangedThunkDef) (MonoObject*, TransformChangedFlags, MonoException**);
 
 		MonoObject* mManagedInstance;
+		MonoClass* mManagedClass;
 		MonoReflectionType* mRuntimeType;
 		uint32_t mManagedHandle;
 

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

@@ -54,6 +54,14 @@ namespace BansheeEngine
 		static bool internal_GetState(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, AnimationClipState* state);
 		static void internal_SetState(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, AnimationClipState* state);
 
+		static UINT32 internal_GetNumClips(ScriptAnimation* thisPtr);
+		static MonoObject* internal_GetClip(ScriptAnimation* thisPtr, UINT32 idx);
+
+		static void internal_MapCurveToSceneObject(ScriptAnimation* thisPtr, MonoString* curve, ScriptSceneObject* so);
+		static void internal_UnmapSceneObject(ScriptAnimation* thisPtr, ScriptSceneObject* so);
+
+		static bool internal_GetGenericCurveValue(ScriptAnimation* thisPtr, UINT32 curveIdx, float* value);
+
 		typedef void(__stdcall *OnEventTriggeredThunkDef) (MonoObject*, MonoObject*, MonoString*, MonoException**);
 		static OnEventTriggeredThunkDef sOnEventTriggeredThunk;
 	};

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

@@ -29,6 +29,7 @@ namespace BansheeEngine
 		/************************************************************************/
 		/* 								CLR HOOKS						   		*/
 		/************************************************************************/
+		static void internal_CreateInstance(MonoObject* instance);
 		static MonoObject* internal_GetAnimationCurves(ScriptAnimationClip* thisPtr);
 		static void internal_SetAnimationCurves(ScriptAnimationClip* thisPtr, MonoObject* curves);
 		static MonoArray* internal_GetAnimationEvents(ScriptAnimationClip* thisPtr);

+ 2 - 0
Source/SBansheeEngine/Include/BsScriptAnimationCurves.h

@@ -53,6 +53,7 @@ namespace BansheeEngine
 		/* 								CLR HOOKS						   		*/
 		/************************************************************************/
 		static MonoField* sNameField;
+		static MonoField* sFlagsField;
 		static MonoField* sXCurveField;
 		static MonoField* sYCurveField;
 		static MonoField* sZCurveField;
@@ -76,6 +77,7 @@ namespace BansheeEngine
 		/* 								CLR HOOKS						   		*/
 		/************************************************************************/
 		static MonoField* sNameField;
+		static MonoField* sFlagsField;
 		static MonoField* sCurveField;
 	};
 

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

@@ -64,6 +64,7 @@ namespace BansheeEngine
 		static MonoObject* internal_getSceneObject(ScriptComponent* nativeInstance);
 		static TransformChangedFlags internal_getNotifyFlags(ScriptComponent* nativeInstance);
 		static void internal_setNotifyFlags(ScriptComponent* nativeInstance, TransformChangedFlags flags);
+		static void internal_Invoke(ScriptComponent* nativeInstance, MonoString* name);
 		static void internal_destroy(ScriptComponent* nativeInstance, bool immediate);
 	};
 

+ 24 - 23
Source/SBansheeEngine/Source/BsManagedComponent.cpp

@@ -16,17 +16,17 @@
 namespace BansheeEngine
 {
 	ManagedComponent::ManagedComponent()
-		: mManagedInstance(nullptr), mRuntimeType(nullptr), mManagedHandle(0), mRunInEditor(false), mRequiresReset(true)
-		, mMissingType(false), mOnInitializedThunk(nullptr), mOnUpdateThunk(nullptr), mOnResetThunk(nullptr)
-		, mOnDestroyThunk(nullptr), mOnDisabledThunk(nullptr), mOnEnabledThunk(nullptr), mOnTransformChangedThunk(nullptr)
-		, mCalculateBoundsMethod(nullptr)
-	{ }
-
-	ManagedComponent::ManagedComponent(const HSceneObject& parent, MonoReflectionType* runtimeType)
-		: Component(parent), mManagedInstance(nullptr), mRuntimeType(runtimeType), mManagedHandle(0), mRunInEditor(false)
+		: mManagedInstance(nullptr), mManagedClass(nullptr), mRuntimeType(nullptr), mManagedHandle(0), mRunInEditor(false)
 		, mRequiresReset(true), mMissingType(false), mOnInitializedThunk(nullptr), mOnUpdateThunk(nullptr)
 		, mOnResetThunk(nullptr), mOnDestroyThunk(nullptr), mOnDisabledThunk(nullptr), mOnEnabledThunk(nullptr)
 		, mOnTransformChangedThunk(nullptr), mCalculateBoundsMethod(nullptr)
+	{ }
+
+	ManagedComponent::ManagedComponent(const HSceneObject& parent, MonoReflectionType* runtimeType)
+		: Component(parent), mManagedInstance(nullptr), mManagedClass(nullptr), mRuntimeType(runtimeType)
+		, mManagedHandle(0), mRunInEditor(false), mRequiresReset(true), mMissingType(false), mOnInitializedThunk(nullptr)
+		, mOnUpdateThunk(nullptr), mOnResetThunk(nullptr), mOnDestroyThunk(nullptr), mOnDisabledThunk(nullptr)
+		, mOnEnabledThunk(nullptr), mOnTransformChangedThunk(nullptr), mCalculateBoundsMethod(nullptr)
 	{
 		MonoUtil::getClassName(mRuntimeType, mNamespace, mTypeName);
 		setName(mTypeName);
@@ -84,6 +84,7 @@ namespace BansheeEngine
 				mManagedHandle = 0;
 			}
 
+			mManagedClass = nullptr;
 			mRuntimeType = nullptr;
 			mOnInitializedThunk = nullptr;
 			mOnUpdateThunk = nullptr;
@@ -132,7 +133,7 @@ namespace BansheeEngine
 		mFullTypeName = mNamespace + "." + mTypeName;
 		mManagedInstance = object;
 		
-		MonoClass* managedClass = nullptr;
+		mManagedClass = nullptr;
 		if (mManagedInstance != nullptr)
 		{
 			mManagedHandle = MonoUtil::newGCHandle(mManagedInstance);
@@ -140,7 +141,7 @@ namespace BansheeEngine
 			::MonoClass* monoClass = MonoUtil::getClass(object);
 			mRuntimeType = MonoUtil::getType(monoClass);
 
-			managedClass = MonoManager::instance().findClass(monoClass);
+			mManagedClass = MonoManager::instance().findClass(monoClass);
 		}
 
 		mOnInitializedThunk = nullptr;
@@ -152,69 +153,69 @@ namespace BansheeEngine
 		mOnTransformChangedThunk = nullptr;
 		mCalculateBoundsMethod = nullptr;
 
-		while(managedClass != nullptr)
+		while(mManagedClass != nullptr)
 		{
 			if (mOnInitializedThunk == nullptr)
 			{
-				MonoMethod* onInitializedMethod = managedClass->getMethod("OnInitialize", 0);
+				MonoMethod* onInitializedMethod = mManagedClass->getMethod("OnInitialize", 0);
 				if (onInitializedMethod != nullptr)
 					mOnInitializedThunk = (OnInitializedThunkDef)onInitializedMethod->getThunk();
 			}
 
 			if (mOnUpdateThunk == nullptr)
 			{
-				MonoMethod* onUpdateMethod = managedClass->getMethod("OnUpdate", 0);
+				MonoMethod* onUpdateMethod = mManagedClass->getMethod("OnUpdate", 0);
 				if (onUpdateMethod != nullptr)
 					mOnUpdateThunk = (OnUpdateThunkDef)onUpdateMethod->getThunk();
 			}
 
 			if (mOnResetThunk == nullptr)
 			{
-				MonoMethod* onResetMethod = managedClass->getMethod("OnReset", 0);
+				MonoMethod* onResetMethod = mManagedClass->getMethod("OnReset", 0);
 				if (onResetMethod != nullptr)
 					mOnResetThunk = (OnResetThunkDef)onResetMethod->getThunk();
 			}
 
 			if (mOnDestroyThunk == nullptr)
 			{
-				MonoMethod* onDestroyMethod = managedClass->getMethod("OnDestroy", 0);
+				MonoMethod* onDestroyMethod = mManagedClass->getMethod("OnDestroy", 0);
 				if (onDestroyMethod != nullptr)
 					mOnDestroyThunk = (OnDestroyedThunkDef)onDestroyMethod->getThunk();
 			}
 
 			if (mOnDisabledThunk == nullptr)
 			{
-				MonoMethod* onDisableMethod = managedClass->getMethod("OnDisable", 0);
+				MonoMethod* onDisableMethod = mManagedClass->getMethod("OnDisable", 0);
 				if (onDisableMethod != nullptr)
 					mOnDisabledThunk = (OnDisabledThunkDef)onDisableMethod->getThunk();
 			}
 
 			if (mOnEnabledThunk == nullptr)
 			{
-				MonoMethod* onEnableMethod = managedClass->getMethod("OnEnable", 0);
+				MonoMethod* onEnableMethod = mManagedClass->getMethod("OnEnable", 0);
 				if (onEnableMethod != nullptr)
 					mOnEnabledThunk = (OnInitializedThunkDef)onEnableMethod->getThunk();
 			}
 
 			if (mOnTransformChangedThunk == nullptr)
 			{
-				MonoMethod* onTransformChangedMethod = managedClass->getMethod("OnTransformChanged", 1);
+				MonoMethod* onTransformChangedMethod = mManagedClass->getMethod("OnTransformChanged", 1);
 				if (onTransformChangedMethod != nullptr)
 					mOnTransformChangedThunk = (OnTransformChangedThunkDef)onTransformChangedMethod->getThunk();
 			}
 
 			if(mCalculateBoundsMethod == nullptr)
-				mCalculateBoundsMethod = managedClass->getMethod("CalculateBounds", 2);
+				mCalculateBoundsMethod = mManagedClass->getMethod("CalculateBounds", 2);
 
 			// Search for methods on base class if there is one
-			MonoClass* baseClass = managedClass->getBaseClass();
+			MonoClass* baseClass = mManagedClass->getBaseClass();
 			if (baseClass != ScriptComponent::getMetaData()->scriptClass)
-				managedClass = baseClass;
+				mManagedClass = baseClass;
 			else
 				break;
 		}
 
-		if (managedClass != nullptr)
+		if (mManagedClass != nullptr)
 		{
 			MonoAssembly* bansheeEngineAssembly = MonoManager::instance().getAssembly(ENGINE_ASSEMBLY);
 			if (bansheeEngineAssembly == nullptr)
@@ -224,7 +225,7 @@ namespace BansheeEngine
 			if (runInEditorAttrib == nullptr)
 				BS_EXCEPT(InvalidStateException, "Cannot find RunInEditor managed class.");
 
-			mRunInEditor = managedClass->getAttribute(runInEditorAttrib) != nullptr;
+			mRunInEditor = mManagedClass->getAttribute(runInEditorAttrib) != nullptr;
 		}
 		else
 			mRunInEditor = false;

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

@@ -7,6 +7,7 @@
 #include "BsMonoClass.h"
 #include "BsMonoManager.h"
 #include "BsMonoUtil.h"
+#include "BsScriptSceneObject.h"
 #include "BsScriptResourceManager.h"
 #include "BsScriptAnimationClip.h"
 
@@ -47,6 +48,14 @@ namespace BansheeEngine
 		metaData.scriptClass->addInternalCall("Internal_GetState", &ScriptAnimation::internal_GetState);
 		metaData.scriptClass->addInternalCall("Internal_SetState", &ScriptAnimation::internal_SetState);
 
+		metaData.scriptClass->addInternalCall("Internal_GetNumClips", &ScriptAnimation::internal_GetNumClips);
+		metaData.scriptClass->addInternalCall("Internal_GetClip", &ScriptAnimation::internal_GetClip);
+
+		metaData.scriptClass->addInternalCall("Internal_MapCurveToSceneObject", &ScriptAnimation::internal_MapCurveToSceneObject);
+		metaData.scriptClass->addInternalCall("Internal_UnmapSceneObject", &ScriptAnimation::internal_UnmapSceneObject);
+
+		metaData.scriptClass->addInternalCall("Internal_GetGenericCurveValue", &ScriptAnimation::internal_GetGenericCurveValue);
+
 		sOnEventTriggeredThunk = (OnEventTriggeredThunkDef)metaData.scriptClass->getMethod("Internal_OnEventTriggered", 2)->getThunk();
 	}
 
@@ -135,6 +144,23 @@ namespace BansheeEngine
 		return thisPtr->getInternal()->isPlaying();
 	}
 
+	UINT32 ScriptAnimation::internal_GetNumClips(ScriptAnimation* thisPtr)
+	{
+		return thisPtr->getInternal()->getNumClips();
+	}
+
+	MonoObject* ScriptAnimation::internal_GetClip(ScriptAnimation* thisPtr, UINT32 idx)
+	{
+		HAnimationClip clip = thisPtr->getInternal()->getClip(idx);
+		if (!clip.isLoaded())
+			return nullptr;
+
+		ScriptAnimationClip* scriptClip;
+		ScriptResourceManager::instance().getScriptResource(clip, &scriptClip, true);
+
+		return scriptClip->getManagedInstance();
+	}
+
 	bool ScriptAnimation::internal_GetState(ScriptAnimation* thisPtr, ScriptAnimationClip* clip, AnimationClipState* state)
 	{
 		HAnimationClip nativeClip;
@@ -153,6 +179,25 @@ namespace BansheeEngine
 		thisPtr->getInternal()->setState(nativeClip, *state);
 	}
 
+	bool ScriptAnimation::internal_GetGenericCurveValue(ScriptAnimation* thisPtr, UINT32 curveIdx, float* value)
+	{
+		return thisPtr->getInternal()->getGenericCurveValue(curveIdx, *value);
+	}
+
+	void ScriptAnimation::internal_MapCurveToSceneObject(ScriptAnimation* thisPtr, MonoString* curve, ScriptSceneObject* so)
+	{
+		String curveName = MonoUtil::monoToString(curve);
+		HSceneObject soHandle = so->getNativeHandle();
+
+		thisPtr->getInternal()->mapCurveToSceneObject(curveName, soHandle);
+	}
+
+	void ScriptAnimation::internal_UnmapSceneObject(ScriptAnimation* thisPtr, ScriptSceneObject* so)
+	{
+		HSceneObject soHandle = so->getNativeHandle();
+		thisPtr->getInternal()->unmapSceneObject(soHandle);
+	}
+
 	MonoField* ScriptBlendClipInfo::clipField = nullptr;
 	MonoField* ScriptBlendClipInfo::positionField = nullptr;
 

+ 16 - 1
Source/SBansheeEngine/Source/BsScriptAnimationClip.cpp

@@ -4,6 +4,7 @@
 #include "BsScriptAnimationCurves.h"
 #include "BsScriptMeta.h"
 #include "BsMonoClass.h"
+#include "BsScriptResourceManager.h"
 
 namespace BansheeEngine
 {
@@ -15,6 +16,7 @@ namespace BansheeEngine
 
 	void ScriptAnimationClip::initRuntimeData()
 	{
+		metaData.scriptClass->addInternalCall("Internal_CreateInstance", &ScriptAnimationClip::internal_CreateInstance);
 		metaData.scriptClass->addInternalCall("Internal_GetAnimationCurves", &ScriptAnimationClip::internal_GetAnimationCurves);
 		metaData.scriptClass->addInternalCall("Internal_SetAnimationCurves", &ScriptAnimationClip::internal_SetAnimationCurves);
 		metaData.scriptClass->addInternalCall("Internal_GetAnimationEvents", &ScriptAnimationClip::internal_GetAnimationEvents);
@@ -24,7 +26,20 @@ namespace BansheeEngine
 
 	MonoObject* ScriptAnimationClip::createInstance()
 	{
-		return metaData.scriptClass->createInstance();
+		bool dummy = false;
+
+		void* params[1];
+		params[0] = &dummy;
+
+		return metaData.scriptClass->createInstance("bool", params);
+	}
+
+	void ScriptAnimationClip::internal_CreateInstance(MonoObject* instance)
+	{
+		HAnimationClip clip = AnimationClip::create();
+
+		ScriptAnimationClip* scriptInstance;
+		ScriptResourceManager::instance().createScriptResource(instance, clip, &scriptInstance);
 	}
 
 	MonoObject* ScriptAnimationClip::internal_GetAnimationCurves(ScriptAnimationClip* thisPtr)

+ 22 - 31
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);
@@ -172,6 +149,7 @@ namespace BansheeEngine
 	}
 
 	MonoField* ScriptNamedVector3Curve::sNameField = nullptr;
+	MonoField* ScriptNamedVector3Curve::sFlagsField = nullptr;
 	MonoField* ScriptNamedVector3Curve::sXCurveField = nullptr;
 	MonoField* ScriptNamedVector3Curve::sYCurveField = nullptr;
 	MonoField* ScriptNamedVector3Curve::sZCurveField = nullptr;
@@ -183,6 +161,7 @@ namespace BansheeEngine
 	void ScriptNamedVector3Curve::initRuntimeData()
 	{
 		sNameField = metaData.scriptClass->getField("Name");
+		sFlagsField = metaData.scriptClass->getField("Flags");
 		sXCurveField = metaData.scriptClass->getField("X");
 		sYCurveField = metaData.scriptClass->getField("Y");
 		sZCurveField = metaData.scriptClass->getField("Z");
@@ -197,6 +176,10 @@ namespace BansheeEngine
 
 		output.name = MonoUtil::monoToString(monoName);
 
+		UINT32 flags;
+		sFlagsField->getValue(instance, &flags);
+		output.flags = (AnimationCurveFlags)flags;
+
 		// Convert from three separate floating point curves, to a Vector3 curve
 		MonoObject* monoCurves[3];
 		sXCurveField->getValue(instance, &monoCurves[0]);
@@ -235,7 +218,6 @@ namespace BansheeEngine
 		}
 
 		// Populate keyframe values
-
 		Vector<TKeyframe<Vector3>> keyframeList(keyFrames.size());
 		for(auto& entry : keyFrames)
 		{
@@ -302,11 +284,14 @@ namespace BansheeEngine
 		MonoObject* monoYCurve = ScriptAnimationCurve::create(yCurve);
 		MonoObject* monoZCurve = ScriptAnimationCurve::create(zCurve);
 
-		void* params[4] = { monoString, monoXCurve, monoYCurve, monoZCurve };
-		return metaData.scriptClass->createInstance("string, AnimationCurve, AnimationCurve, AnimationCurve", params);
+		UINT32 flags = curve.flags;
+
+		void* params[5] = { monoString, &flags, monoXCurve, monoYCurve, monoZCurve };
+		return metaData.scriptClass->createInstance("string, int, AnimationCurve, AnimationCurve, AnimationCurve", params);
 	}
 
 	MonoField* ScriptNamedFloatCurve::sNameField = nullptr;
+	MonoField* ScriptNamedFloatCurve::sFlagsField = nullptr;
 	MonoField* ScriptNamedFloatCurve::sCurveField = nullptr;
 
 	ScriptNamedFloatCurve::ScriptNamedFloatCurve(MonoObject* instance)
@@ -316,6 +301,7 @@ namespace BansheeEngine
 	void ScriptNamedFloatCurve::initRuntimeData()
 	{
 		sNameField = metaData.scriptClass->getField("Name");
+		sFlagsField = metaData.scriptClass->getField("Flags");
 		sCurveField = metaData.scriptClass->getField("Curve");
 	}
 
@@ -328,6 +314,10 @@ namespace BansheeEngine
 
 		output.name = MonoUtil::monoToString(monoName);
 
+		UINT32 flags;
+		sFlagsField->getValue(instance, &flags);
+		output.flags = (AnimationCurveFlags)flags;
+
 		MonoObject* monoCurve = nullptr;
 		sCurveField->getValue(instance, &monoCurve);
 
@@ -345,7 +335,8 @@ namespace BansheeEngine
 		MonoString* monoString = MonoUtil::stringToMono(curve.name);
 		MonoObject* monoCurve = ScriptAnimationCurve::create(curve.curve);
 
-		void* params[2] = { monoString, monoCurve };
-		return metaData.scriptClass->createInstance("string, AnimationCurve", params);
+		UINT32 flags = curve.flags;
+		void* params[3] = { monoString, &flags, monoCurve };
+		return metaData.scriptClass->createInstance("string, int, AnimationCurve", params);
 	}
 }

+ 36 - 0
Source/SBansheeEngine/Source/BsScriptComponent.cpp

@@ -199,6 +199,42 @@ namespace BansheeEngine
 			nativeInstance->mManagedComponent->mNotifyFlags = flags;
 	}
 
+	void ScriptComponent::internal_Invoke(ScriptComponent* nativeInstance, MonoString* name)
+	{
+		HManagedComponent comp = nativeInstance->mManagedComponent;
+		if (checkIfDestroyed(nativeInstance->mManagedComponent))
+			return;
+
+		MonoObject* compObj = comp->getManagedInstance();
+		MonoClass* compClass = comp->getClass();
+
+		bool found = false;
+		String methodName = MonoUtil::monoToString(name);
+		while (compClass != nullptr)
+		{
+			MonoMethod* method = compClass->getMethod(methodName);
+			if (method != nullptr)
+			{
+				method->invoke(compObj, nullptr);
+				found = true;
+				break;
+			}
+
+			// Search for methods on base class if there is one
+			MonoClass* baseClass = compClass->getBaseClass();
+			if (baseClass != metaData.scriptClass)
+				compClass = baseClass;
+			else
+				break;
+		}
+
+		if (!found)
+		{
+			LOGWRN("Cannot find method \"" + methodName + "\" to invoke on component of type \"" + 
+				compClass->getTypeName() + "\".");
+		}
+	}
+
 	void ScriptComponent::internal_destroy(ScriptComponent* nativeInstance, bool immediate)
 	{
 		if (!checkIfDestroyed(nativeInstance->mManagedComponent))