Browse Source

Proper full matrix interpolation by de- and recomposition.

Michael 7 years ago
parent
commit
ddfaa3fa3e

+ 2 - 0
Build/CMakeLists.txt

@@ -586,6 +586,8 @@ endif(NOT BUILD_FRAMEWORK)
 
 
     # Build and install sample shell library
     # Build and install sample shell library
     add_library(shell STATIC ${shell_SRC_FILES} ${shell_HDR_FILES})
     add_library(shell STATIC ${shell_SRC_FILES} ${shell_HDR_FILES})
+	set_property(TARGET shell PROPERTY CXX_STANDARD 17)
+	set_property(TARGET shell PROPERTY CXX_STANDARD_REQUIRED ON)
     if (APPLE)
     if (APPLE)
     	# We only support i386 for the samples as it still uses Carbon
     	# We only support i386 for the samples as it still uses Carbon
     	set_target_properties(shell PROPERTIES OSX_ARCHITECTURES "i386;")
     	set_target_properties(shell PROPERTIES OSX_ARCHITECTURES "i386;")

+ 11 - 0
Include/Rocket/Core/Matrix4.h

@@ -353,6 +353,10 @@ class Matrix4
 		/// @return true, if the inversion succeeded.
 		/// @return true, if the inversion succeeded.
 		bool Invert() noexcept;
 		bool Invert() noexcept;
 
 
+		/// Inverts this matrix in place, if possible.
+		/// @return true, if the inversion succeeded.
+		float Determinant() const noexcept;
+
 		/// Returns the negation of this matrix.
 		/// Returns the negation of this matrix.
 		/// @return The negation of this matrix.
 		/// @return The negation of this matrix.
 		ThisType operator-() const noexcept;
 		ThisType operator-() const noexcept;
@@ -376,6 +380,9 @@ class Matrix4
 		/// @return This matrix, post-operation.
 		/// @return This matrix, post-operation.
 		const ThisType& operator/=(Component other) noexcept;
 		const ThisType& operator/=(Component other) noexcept;
 
 
+		inline const VectorType& operator[](size_t i) const noexcept { return vectors[i]; }
+		inline VectorType& operator[](size_t i) noexcept { return vectors[i]; }
+
 		/// Returns the sum of this matrix and another.
 		/// Returns the sum of this matrix and another.
 		/// @param[in] other The matrix to add this to.
 		/// @param[in] other The matrix to add this to.
 		/// @return The sum of the two matrices.
 		/// @return The sum of the two matrices.
@@ -486,6 +493,10 @@ class Matrix4
 		static ThisType Skew (Component angle_x, Component angle_y) noexcept;
 		static ThisType Skew (Component angle_x, Component angle_y) noexcept;
 		static ThisType SkewX (Component angle) noexcept;
 		static ThisType SkewX (Component angle) noexcept;
 		static ThisType SkewY (Component angle) noexcept;
 		static ThisType SkewY (Component angle) noexcept;
+
+		static ThisType Compose(const Vector3< Component >& translation, const Vector3< Component >& scale,
+			const Vector3< Component >& skew, const Vector4< Component >& perspective, const Vector4< Component >& quaternion) noexcept;
+
 };
 };
 }
 }
 }
 }

+ 99 - 0
Include/Rocket/Core/Matrix4.inl

@@ -309,6 +309,51 @@ bool Matrix4< Component, Storage >::Invert() noexcept
 	return true;
 	return true;
 }
 }
 
 
+
+
+
+template<typename Component, class Storage>
+inline float Rocket::Core::Matrix4<Component, Storage>::Determinant() const noexcept
+{
+	const Component *src = data();
+	float diag[4]; // Diagonal elements of the matrix inverse (see Invert)
+
+	diag[0] = src[5] * src[10] * src[15] -
+		src[5] * src[11] * src[14] -
+		src[9] * src[6] * src[15] +
+		src[9] * src[7] * src[14] +
+		src[13] * src[6] * src[11] -
+		src[13] * src[7] * src[10];
+
+	diag[1] = -src[4] * src[10] * src[15] +
+		src[4] * src[11] * src[14] +
+		src[8] * src[6] * src[15] -
+		src[8] * src[7] * src[14] -
+		src[12] * src[6] * src[11] +
+		src[12] * src[7] * src[10];
+
+	diag[2] = src[4] * src[9] * src[15] -
+		src[4] * src[11] * src[13] -
+		src[8] * src[5] * src[15] +
+		src[8] * src[7] * src[13] +
+		src[12] * src[5] * src[11] -
+		src[12] * src[7] * src[9];
+
+	diag[3] = -src[4] * src[9] * src[14] +
+		src[4] * src[10] * src[13] +
+		src[8] * src[5] * src[14] -
+		src[8] * src[6] * src[13] -
+		src[12] * src[5] * src[10] +
+		src[12] * src[6] * src[9];
+
+	float det = src[0] * diag[0] + \
+		src[1] * diag[1] + \
+		src[2] * diag[2] + \
+		src[3] * diag[3];
+
+	return det;
+}
+
 // Returns the negation of this matrix.
 // Returns the negation of this matrix.
 template< typename Component, class Storage >
 template< typename Component, class Storage >
 typename Matrix4< Component, Storage >::ThisType Matrix4< Component, Storage >::operator-() const noexcept
 typename Matrix4< Component, Storage >::ThisType Matrix4< Component, Storage >::operator-() const noexcept
@@ -629,6 +674,60 @@ Matrix4< Component, Storage > Matrix4< Component, Storage >::SkewY(Component ang
 	return Skew(0, angle);
 	return Skew(0, angle);
 }
 }
 
 
+template<typename Component, class Storage>
+Matrix4< Component, Storage > Rocket::Core::Matrix4<Component, Storage>::Compose(const Vector3<Component>& translation,
+	const Vector3<Component>& scale, const Vector3<Component>& skew, const Vector4<Component>& perspective,
+	const Vector4<Component>& quaternion) noexcept
+{
+	ThisType matrix = ThisType::Identity();
+
+	for (int i = 0; i < 4; i++)
+		matrix[i][3] = perspective[i];
+
+	for (int i = 0; i < 4; i++)
+		for (int j = 0; j < 3; j++)
+			matrix[3][i] += translation[j] * matrix[j][i];
+	
+	float x = quaternion.x;
+	float y = quaternion.y;
+	float z = quaternion.z;
+	float w = quaternion.w;
+
+	ThisType rotation = Matrix4< Component, Storage >::FromRows(
+		VectorType(1.f - 2.f * (y*y + z * z), 2.f*(x*y - z * w), 2.f*(x*z + y * w), 0.f),
+		VectorType(2.f * (x * y + z * w), 1.f - 2.f * (x * x + z * z), 2.f * (y * z - x * w), 0.f),
+		VectorType(2.f * (x * z - y * w), 2.f * (y * z + x * w), 1.f - 2.f * (x * x + y * y), 0.f),
+		VectorType(0, 0, 0, 1)
+	);
+
+	matrix *= rotation;
+
+	ThisType temp = ThisType::Identity();
+	if(skew[2])
+	{
+		temp[2][1] = skew[2];
+		matrix *= temp;
+	}
+	if (skew[1])
+	{
+		temp[2][1] = 0;
+		temp[2][0] = skew[1];
+		matrix *= temp;
+	}
+	if (skew[0])
+	{
+		temp[2][0] = 0;
+		temp[1][0] = skew[0];
+		matrix *= temp;
+	}
+
+	for (int i = 0; i < 3; i++)
+		for (int j = 0; j < 4; j++)
+			matrix[i][j] *= scale[i];
+
+	return matrix;
+}
+
 template< typename Component, class Storage >
 template< typename Component, class Storage >
 template< typename _Component >
 template< typename _Component >
 struct Matrix4< Component, Storage >::VectorMultiplier< _Component, RowMajorStorage< _Component > >
 struct Matrix4< Component, Storage >::VectorMultiplier< _Component, RowMajorStorage< _Component > >

+ 1 - 1
Include/Rocket/Core/Transform.h

@@ -62,7 +62,7 @@ public:
 
 
 	/// Helper function to create a Property with TransformRef from list of primitives
 	/// Helper function to create a Property with TransformRef from list of primitives
 	static Property MakeProperty(std::vector<Transforms::Primitive> primitives);
 	static Property MakeProperty(std::vector<Transforms::Primitive> primitives);
-
+	
 	/// Copy constructor
 	/// Copy constructor
 	Transform(const Transform& other);
 	Transform(const Transform& other);
 
 

+ 12 - 2
Include/Rocket/Core/TransformPrimitive.h

@@ -237,6 +237,17 @@ struct Perspective : public UnresolvedPrimitive< 1 >
 	Perspective(const NumericValue* values) noexcept : UnresolvedPrimitive(values) { }
 	Perspective(const NumericValue* values) noexcept : UnresolvedPrimitive(values) { }
 };
 };
 
 
+struct DecomposedMatrix4
+{
+	Vector4f perspective;
+	Vector4f quaternion;
+	Vector3f translation;
+	Vector3f scale;
+	Vector3f skew;
+
+	bool Decompose(const Matrix4f& m);
+};
+
 
 
 using PrimitiveVariant = std::variant<
 using PrimitiveVariant = std::variant<
 	Matrix2D, Matrix3D,
 	Matrix2D, Matrix3D,
@@ -244,7 +255,7 @@ using PrimitiveVariant = std::variant<
 	ScaleX, ScaleY, ScaleZ, Scale2D, Scale3D,
 	ScaleX, ScaleY, ScaleZ, Scale2D, Scale3D,
 	RotateX, RotateY, RotateZ, Rotate2D, Rotate3D,
 	RotateX, RotateY, RotateZ, Rotate2D, Rotate3D,
 	SkewX, SkewY, Skew2D,
 	SkewX, SkewY, Skew2D,
-	Perspective>;
+	Perspective, DecomposedMatrix4>;
 
 
 
 
 /**
 /**
@@ -270,7 +281,6 @@ struct Primitive
 	// Promote units to basic types which can be interpolated, that is, convert 'length -> pixel' for unresolved primitives.
 	// Promote units to basic types which can be interpolated, that is, convert 'length -> pixel' for unresolved primitives.
 	bool ResolveUnits(Element& e) noexcept;
 	bool ResolveUnits(Element& e) noexcept;
 
 
-
 	static bool TryConvertToMatchingGenericType(Primitive& p0, Primitive& p1) noexcept;
 	static bool TryConvertToMatchingGenericType(Primitive& p0, Primitive& p1) noexcept;
 
 
 	bool InterpolateWith(const Primitive& other, float alpha) noexcept;
 	bool InterpolateWith(const Primitive& other, float alpha) noexcept;

+ 1 - 8
Include/Rocket/Core/Types.h

@@ -70,6 +70,7 @@ typedef unsigned __int64 uint64_t;
 #include "Matrix4.h"
 #include "Matrix4.h"
 #include "String.h"
 #include "String.h"
 #include "Reference.h"
 #include "Reference.h"
+#include "Transform.h"
 
 
 namespace Rocket {
 namespace Rocket {
 namespace Core {
 namespace Core {
@@ -105,17 +106,9 @@ typedef std::set< String > PropertyNameList;
 typedef std::set< String > AttributeNameList;
 typedef std::set< String > AttributeNameList;
 typedef Dictionary ElementAttributes;
 typedef Dictionary ElementAttributes;
 typedef std::vector< ElementAnimation > ElementAnimationList;
 typedef std::vector< ElementAnimation > ElementAnimationList;
-}
-}
-
-#include <Rocket/Core/Transform.h>
-
-namespace Rocket {
-namespace Core {
 
 
 // Reference types
 // Reference types
 typedef SharedReference< Transform > TransformRef;
 typedef SharedReference< Transform > TransformRef;
-
 }
 }
 }
 }
 
 

+ 53 - 7
Samples/basic/animation/data/animation.rml

@@ -3,12 +3,17 @@
 	<link type="text/template" href="../../../assets/window.rml"/>
 	<link type="text/template" href="../../../assets/window.rml"/>
 	<title>Animation Sample</title>
 	<title>Animation Sample</title>
 	<style>
 	<style>
-		body
+		body.window
 		{
 		{
-			width: 350px;
-			height: 300px;
+			max-width: 2000px;
+			max-height: 2000px;
+			width: 1400px;
+			height: 750px;
+			perspective: 3000px;
+		}
+		button {
+			margin-top: 50px;
 		}
 		}
-
 		div#title_bar div#icon
 		div#title_bar div#icon
 		{
 		{
 			display: none;
 			display: none;
@@ -22,22 +27,63 @@
 		
 		
 		#start_game 
 		#start_game 
 		{
 		{
-			opacity: 0.5;
+			opacity: 0.8;
 			transform: rotate(-350) translateX(100) scale(1.2);
 			transform: rotate(-350) translateX(100) scale(1.2);
 			transform-origin: 30% 80% 0;
 			transform-origin: 30% 80% 0;
 		}
 		}
 
 
-		#high_score {
+		#high_scores {
 			margin-left: -100px;
 			margin-left: -100px;
 		}
 		}
+		#exit {
+			transform: rotate(45deg);
+		}
+		
+		#transform_tests div.container
+		{
+			margin-top: 15px;
+			width: 100%;
+			height: 200px;
+			background-color: #ae8484aa;
+		}
+		#transform_tests div.plain
+		{
+			font-size: 1.2em;
+			padding: 10px;
+			margin: auto;
+			width: 130px;
+			height: 70px;
+			background-color: #c66;
+		}
+		#transform_tests div.plain:hover { background-color: #ddb700; }
+		#generic {
+			transform: translateX(100) rotateZ(90);
+		}
+		#combine {
+			transform: rotate(45deg);
+		}
+		#decomposition {
+			/* The scale(1.0) should force a full matrix recomposition when interpolating,
+			   then, the information about multiple turns get lost and it only turns 45deg. */
+			transform: rotate(45deg) scale(1.0);
+		}
 	</style>
 	</style>
 </head>
 </head>
 
 
 <body template="window">
 <body template="window">
+<div style="width: 45%; height: 80%; position: absolute;">
 	<button id="start_game">Start Game</button><br />
 	<button id="start_game">Start Game</button><br />
-	<button id="high_score" onkeydown="hello">High Scores</button><br />
+	<button id="high_scores" onkeydown="hello">High Scores</button><br />
 	<button id="options">Options</button><br />
 	<button id="options">Options</button><br />
 	<button id="help">Help</button><br />
 	<button id="help">Help</button><br />
 	<button id="exit" onclick="exit">Exit</button>
 	<button id="exit" onclick="exit">Exit</button>
+</div>
+<div style="width: 35%; height: 80%;position: absolute; left: 60%;" id="transform_tests">
+	<div style="font-size: 1.5em; text-align: left;">Test transform animations</div>
+	<div class="container"><div class="plain" id="generic">Generic form conversion.</div></div>
+	<div class="container"><div class="plain" id="combine">Match different transform primitive sizes</div></div>
+	<div class="container"><div class="plain" id="decomposition">Force full matrix decomposition</div></div>
+</div>
+
 </body>
 </body>
 </rml>
 </rml>

+ 48 - 25
Samples/basic/animation/src/main.cpp

@@ -37,8 +37,8 @@
 
 
 
 
 // Animations TODO:
 // Animations TODO:
-//  - Proper interpolation of full transform matrices (split into translate/rotate/skew/scale).
-//  - Better error reporting when submitting invalid animations, check validity on add. Remove animation if invalid.
+//  - Some primitives should be converted to DecomposedMatrix4 when adding key: Matrix3d, Matrix2d, Perspective.
+//  - Update transform animations / resolve keys again when parent box size changes.
 //  - RCSS support? Both @keyframes and transition, maybe.
 //  - RCSS support? Both @keyframes and transition, maybe.
 //  - Profiling
 //  - Profiling
 //  - [offtopic] Improve performance of transform parser (hashtable)
 //  - [offtopic] Improve performance of transform parser (hashtable)
@@ -57,40 +57,60 @@ public:
 				document->GetElementById("title")->SetInnerRML(title);
 				document->GetElementById("title")->SetInnerRML(title);
 				document->SetProperty("left", Property(position.x, Property::PX));
 				document->SetProperty("left", Property(position.x, Property::PX));
 				document->SetProperty("top", Property(position.y, Property::PX));
 				document->SetProperty("top", Property(position.y, Property::PX));
-				//document->Animate("opacity", Property(0.6f, Property::NUMBER), 0.5f, -1, true);
+				//document->Animate("opacity", Property(0.6f, Property::NUMBER), 0.5f, Tween{}, -1, true);
 			}
 			}
 
 
-
+			// Button fun
 			{
 			{
 				auto el = document->GetElementById("start_game");
 				auto el = document->GetElementById("start_game");
 				PropertyDictionary pd;
 				PropertyDictionary pd;
-				StyleSheetSpecification::ParsePropertyDeclaration(pd, "transform", "rotate(10)");
+				StyleSheetSpecification::ParsePropertyDeclaration(pd, "transform", "rotate(10) translateX(100)");
 				auto p = pd.GetProperty("transform");
 				auto p = pd.GetProperty("transform");
 				el->Animate("transform", *p, 1.8f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 				el->Animate("transform", *p, 1.8f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 
 
 				auto pp = Transform::MakeProperty({ Transforms::Scale2D{3.f} });
 				auto pp = Transform::MakeProperty({ Transforms::Scale2D{3.f} });
 				el->Animate("transform", pp, 1.3f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 				el->Animate("transform", pp, 1.3f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 			}
 			}
-
-			//{
-			//	auto el = document->GetElementById("high_score");
-			//	el->Animate("margin-left", Property(0.f, Property::PX), 0.3f, 10, true);
-			//	el->Animate("margin-left", Property(100.f, Property::PX), 0.6f);
-			//}
-
+			{
+				auto el = document->GetElementById("high_scores");
+				el->Animate("margin-left", Property(0.f, Property::PX), 0.3f, Tween{ Tween::Sine, Tween::In }, 10, true, 1.f);
+				el->Animate("margin-left", Property(100.f, Property::PX), 3.0f, Tween{ Tween::Circular, Tween::Out });
+			}
+			{
+				auto el = document->GetElementById("options");
+				el->Animate("image-color", Property(Colourb(128, 255, 255, 255), Property::COLOUR), 0.3f, Tween{}, -1, false);
+				el->Animate("image-color", Property(Colourb(128, 128, 255, 255), Property::COLOUR), 0.3f);
+				el->Animate("image-color", Property(Colourb(0, 128, 128, 255), Property::COLOUR), 0.3f);
+				el->Animate("image-color", Property(Colourb(64, 128, 255, 0), Property::COLOUR), 0.9f);
+				el->Animate("image-color", Property(Colourb(255, 255, 255, 255), Property::COLOUR), 0.3f);
+			}
+			{
+				auto el = document->GetElementById("help");
+				el->Animate("margin-left", Property(100.f, Property::PX), 1.0f, Tween{ Tween::Quadratic, Tween::InOut }, -1, true);
+			}
 			{
 			{
 				auto el = document->GetElementById("exit");
 				auto el = document->GetElementById("exit");
-				el->Animate("margin-left", Property(100.f, Property::PX), 1.0f, Tween{}, -1, true);
+				PropertyDictionary pd;
+				StyleSheetSpecification::ParsePropertyDeclaration(pd, "transform", "translate(200px, 200px) rotate(1215deg)");
+				el->Animate("transform", *pd.GetProperty("transform"), 3.f, Tween{ Tween::Bounce, Tween::Out }, -1);
 			}
 			}
 
 
-			//{
-			//	auto el = document->GetElementById("help");
-			//	el->Animate("image-color", Property(Colourb(128, 255, 255, 255), Property::COLOUR), 0.3f, -1, false);
-			//	el->Animate("image-color", Property(Colourb(128, 128, 255, 255), Property::COLOUR), 0.3f);
-			//	el->Animate("image-color", Property(Colourb(0, 128, 128, 255), Property::COLOUR), 0.3f);
-			//	el->Animate("image-color", Property(Colourb(64, 128, 255, 0), Property::COLOUR), 0.9f);
-			//	el->Animate("image-color", Property(Colourb(255, 255, 255, 255), Property::COLOUR), 0.3f);
-			//}
+			// Transform tests
+			{
+				auto el = document->GetElementById("generic");
+				auto p = Transform::MakeProperty({ Transforms::TranslateY{50, Property::PX} , Transforms::RotateX{90, Property::DEG}});
+				el->Animate("transform", p, 1.3f, Tween{}, -1, true);
+			}
+			{
+				auto el = document->GetElementById("combine");
+				auto p = Transform::MakeProperty({ Transforms::Translate2D{50, 50, Property::PX}, Transforms::Rotate2D(1215) });
+				el->Animate("transform", p, 8.0f, Tween{}, -1, true);
+			}
+			{
+				auto el = document->GetElementById("decomposition");
+				auto p = Transform::MakeProperty({ Transforms::Translate2D{50, 50, Property::PX}, Transforms::Rotate2D(1215) });
+				el->Animate("transform", p, 8.0f, Tween{}, -1, true);
+			}
 
 
 			document->Show();
 			document->Show();
 		}
 		}
@@ -256,12 +276,15 @@ int main(int ROCKET_UNUSED_PARAMETER(argc), char** ROCKET_UNUSED_PARAMETER(argv)
 	ROCKET_UNUSED(argv);
 	ROCKET_UNUSED(argv);
 #endif
 #endif
 
 
+	const int width = 1800;
+	const int height = 1000;
+
 	ShellRenderInterfaceOpenGL opengl_renderer;
 	ShellRenderInterfaceOpenGL opengl_renderer;
 	shell_renderer = &opengl_renderer;
 	shell_renderer = &opengl_renderer;
 
 
 	// Generic OS initialisation, creates a window and attaches OpenGL.
 	// Generic OS initialisation, creates a window and attaches OpenGL.
 	if (!Shell::Initialise("../../Samples/") ||
 	if (!Shell::Initialise("../../Samples/") ||
-		!Shell::OpenWindow("Animation Sample", shell_renderer, 1024, 768, true))
+		!Shell::OpenWindow("Animation Sample", shell_renderer, width, height, true))
 	{
 	{
 		Shell::Shutdown();
 		Shell::Shutdown();
 		return -1;
 		return -1;
@@ -269,7 +292,7 @@ int main(int ROCKET_UNUSED_PARAMETER(argc), char** ROCKET_UNUSED_PARAMETER(argv)
 
 
 	// Rocket initialisation.
 	// Rocket initialisation.
 	Rocket::Core::SetRenderInterface(&opengl_renderer);
 	Rocket::Core::SetRenderInterface(&opengl_renderer);
-	opengl_renderer.SetViewport(1024,768);
+	opengl_renderer.SetViewport(width, height);
 
 
 	ShellSystemInterface system_interface;
 	ShellSystemInterface system_interface;
 	Rocket::Core::SetSystemInterface(&system_interface);
 	Rocket::Core::SetSystemInterface(&system_interface);
@@ -277,7 +300,7 @@ int main(int ROCKET_UNUSED_PARAMETER(argc), char** ROCKET_UNUSED_PARAMETER(argv)
 	Rocket::Core::Initialise();
 	Rocket::Core::Initialise();
 
 
 	// Create the main Rocket context and set it on the shell's input layer.
 	// Create the main Rocket context and set it on the shell's input layer.
-	context = Rocket::Core::CreateContext("main", Rocket::Core::Vector2i(1024, 768));
+	context = Rocket::Core::CreateContext("main", Rocket::Core::Vector2i(width, height));
 	if (context == NULL)
 	if (context == NULL)
 	{
 	{
 		Rocket::Core::Shutdown();
 		Rocket::Core::Shutdown();
@@ -298,7 +321,7 @@ int main(int ROCKET_UNUSED_PARAMETER(argc), char** ROCKET_UNUSED_PARAMETER(argv)
 
 
 	Shell::LoadFonts("assets/");
 	Shell::LoadFonts("assets/");
 
 
-	window = new DemoWindow("Animation sample", Rocket::Core::Vector2f(81, 200), context);
+	window = new DemoWindow("Animation sample", Rocket::Core::Vector2f(81, 100), context);
 	window->GetDocument()->AddEventListener("keydown", new Event("hello"));
 	window->GetDocument()->AddEventListener("keydown", new Event("hello"));
 	window->GetDocument()->AddEventListener("keyup", new Event("hello"));
 	window->GetDocument()->AddEventListener("keyup", new Event("hello"));
 
 

+ 69 - 24
Source/Core/ElementAnimation.cpp

@@ -122,6 +122,28 @@ static Variant InterpolateValues(const Variant & v0, const Variant & v1, float a
 	return v0;
 	return v0;
 }
 }
 
 
+bool CombineAndDecompose(Transform& t, Element& e)
+{
+	Matrix4f m = Matrix4f::Identity();
+
+	for (auto& primitive : t.GetPrimitives())
+	{
+		Matrix4f m_primitive;
+		if (primitive.ResolveTransform(m_primitive, e))
+			m *= m_primitive;
+	}
+
+	Transforms::DecomposedMatrix4 decomposed;
+
+	if (!decomposed.Decompose(m))
+		return false;
+
+	t.ClearPrimitives();
+	t.AddPrimitive(decomposed);
+
+	return true;
+}
+
 
 
 
 
 enum class PrepareTransformResult { Unchanged = 0, ChangedT0 = 1, ChangedT1 = 2, ChangedT0andT1 = 3, Invalid = 4 };
 enum class PrepareTransformResult { Unchanged = 0, ChangedT0 = 1, ChangedT1 = 2, ChangedT0andT1 = 3, Invalid = 4 };
@@ -130,9 +152,8 @@ static PrepareTransformResult PrepareTransformPair(Transform& t0, Transform& t1,
 {
 {
 	using namespace Transforms;
 	using namespace Transforms;
 
 
-	// Insert missing primitives into transform
-	// See e.g. https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms for inspiration
-
+	// Insert or modify primitives such that the two transforms match exactly in both number of and types of primitives.
+	// Based largely on https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
 
 
 	auto& prims0 = t0.GetPrimitives();
 	auto& prims0 = t0.GetPrimitives();
 	auto& prims1 = t1.GetPrimitives();
 	auto& prims1 = t1.GetPrimitives();
@@ -140,21 +161,28 @@ static PrepareTransformResult PrepareTransformPair(Transform& t0, Transform& t1,
 	// Check for trivial case where they contain the same primitives
 	// Check for trivial case where they contain the same primitives
 	if (prims0.size() == prims1.size())
 	if (prims0.size() == prims1.size())
 	{
 	{
+		PrepareTransformResult result = PrepareTransformResult::Unchanged;
 		bool same_primitives = true;
 		bool same_primitives = true;
 		for (size_t i = 0; i < prims0.size(); i++)
 		for (size_t i = 0; i < prims0.size(); i++)
 		{
 		{
-			if (prims0[i].primitive.index() != prims1[i].primitive.index())
+			auto p0_type = prims0[i].primitive.index();
+			auto p1_type = prims1[i].primitive.index();
+			if (p0_type != p1_type)
 			{
 			{
 				// They are not the same, but see if we can convert them to their more generic form
 				// They are not the same, but see if we can convert them to their more generic form
-				if(!Primitive::TryConvertToMatchingGenericType(prims0[i], prims1[i]))
+				if (!Primitive::TryConvertToMatchingGenericType(prims0[i], prims1[i]))
 				{
 				{
 					same_primitives = false;
 					same_primitives = false;
 					break;
 					break;
 				}
 				}
+				if (prims0[i].primitive.index() != p0_type)
+					(int&)result |= (int)PrepareTransformResult::ChangedT0;
+				if (prims1[i].primitive.index() != p1_type)
+					(int&)result |= (int)PrepareTransformResult::ChangedT1;
 			}
 			}
 		}
 		}
 		if (same_primitives)
 		if (same_primitives)
-			return PrepareTransformResult::Unchanged;
+			return result;
 	}
 	}
 
 
 	if (prims0.size() != prims1.size())
 	if (prims0.size() != prims1.size())
@@ -246,43 +274,60 @@ static PrepareTransformResult PrepareTransformPair(Transform& t0, Transform& t1,
 
 
 
 
 	// If we get here, things get tricky. Need to do full matrix interpolation.
 	// If we get here, things get tricky. Need to do full matrix interpolation.
-	// We resolve the full transform here. This is not entirely correct if the elements box size changes
-	// during the animation. Ideally, we would resolve it during each iteration.
-	// For performance: We could also consider breaking up the transforms into their interpolating primitives (translate, rotate, skew, scale) here,
-	// instead of doing this every animation tick.
-	for(Transform* t : {&t0, &t1})
+	// In short, we decompose here the Transforms into translation, rotation, scale, skew and perspective components. 
+	// Then, during update, interpolate these components and combine into a new transform matrix.
+	if constexpr(true)
+	{
+		if (!CombineAndDecompose(t0, element))
+			return PrepareTransformResult::Invalid;
+		if (!CombineAndDecompose(t1, element))
+			return PrepareTransformResult::Invalid;
+	}
+	else
 	{
 	{
-		Matrix4f transform_value = Matrix4f::Identity();
-		for (const auto& primitive : t->GetPrimitives())
+		// Bad "flat" matrix interpolation
+		for (Transform* t : { &t0, &t1 })
 		{
 		{
-			Matrix4f m;
-			if (primitive.ResolveTransform(m, element))
-				transform_value *= m;
+			Matrix4f transform_value = Matrix4f::Identity();
+			for (const auto& primitive : t->GetPrimitives())
+			{
+				Matrix4f m;
+				if (primitive.ResolveTransform(m, element))
+					transform_value *= m;
+			}
+			t->ClearPrimitives();
+			t->AddPrimitive({ Matrix3D{transform_value} });
 		}
 		}
-		t->ClearPrimitives();
-		t->AddPrimitive({ Matrix3D{transform_value} });
 	}
 	}
 
 
 	return PrepareTransformResult::ChangedT0andT1;
 	return PrepareTransformResult::ChangedT0andT1;
 }
 }
 
 
 
 
-static bool PrepareTransforms(std::vector<AnimationKey>& keys, Element& element)
+static bool PrepareTransforms(std::vector<AnimationKey>& keys, Element& element, int start_index)
 {
 {
-	for (int i = 1; i < (int)keys.size();)
+	int count_iterations = -1;
+	const int max_iterations = 3 * (int)keys.size();
+	if (start_index < 1) start_index = 1;
+
+	// For each pair of keys, match the transform primitives such that they can be interpolated during animation update
+	for (int i = start_index; i < (int)keys.size() && count_iterations < max_iterations; count_iterations++)
 	{
 	{
 		auto& ref0 = keys[i - 1].value.Get<TransformRef>();
 		auto& ref0 = keys[i - 1].value.Get<TransformRef>();
 		auto& ref1 = keys[i].value.Get<TransformRef>();
 		auto& ref1 = keys[i].value.Get<TransformRef>();
 
 
 		auto result = PrepareTransformPair(*ref0, *ref1, element);
 		auto result = PrepareTransformPair(*ref0, *ref1, element);
 
 
+		if (result == PrepareTransformResult::Invalid)
+			return false;
+
 		bool changed_t0 = (result == PrepareTransformResult::ChangedT0 || result == PrepareTransformResult::ChangedT0andT1);
 		bool changed_t0 = (result == PrepareTransformResult::ChangedT0 || result == PrepareTransformResult::ChangedT0andT1);
 		if (changed_t0 && i > 1)
 		if (changed_t0 && i > 1)
 			--i;
 			--i;
 		else
 		else
 			++i;
 			++i;
 	}
 	}
-	return true;
+	return (count_iterations < max_iterations);
 }
 }
 
 
 
 
@@ -337,7 +382,7 @@ bool ElementAnimation::AddKey(float time, const Property & property, Element& el
 		}
 		}
 
 
 		if (result)
 		if (result)
-			result = PrepareTransforms(keys, element);
+			result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 	}
 	}
 
 
 	if (!result)
 	if (!result)
@@ -350,8 +395,6 @@ Property ElementAnimation::UpdateAndGetProperty(float world_time)
 {
 {
 	Property result;
 	Property result;
 
 
-	//Log::Message(Log::LT_INFO, "Animation it = %d,  t_it = %f, rev = %d,  dt = %f", current_iteration, time_since_iteration_start, (int)reverse_direction, time - last_update_time);
-
 	if (animation_complete || !valid || world_time - last_update_world_time <= 0.0f)
 	if (animation_complete || !valid || world_time - last_update_world_time <= 0.0f)
 		return result;
 		return result;
 
 
@@ -413,6 +456,8 @@ Property ElementAnimation::UpdateAndGetProperty(float world_time)
 
 
 		if (t1 - t0 > eps)
 		if (t1 - t0 > eps)
 			alpha = (t - t0) / (t1 - t0);
 			alpha = (t - t0) / (t1 - t0);
+
+		alpha = Math::Clamp(alpha, 0.0f, 1.0f);
 	}
 	}
 
 
 	alpha = keys[key1].tween(alpha);
 	alpha = keys[key1].tween(alpha);

+ 163 - 15
Source/Core/TransformPrimitive.cpp

@@ -245,6 +245,11 @@ struct ResolveTransformVisitor
 		return true;
 		return true;
 	}
 	}
 
 
+	bool operator()(const DecomposedMatrix4& p)
+	{
+		m = Matrix4f::Compose(p.translation, p.scale, p.skew, p.perspective, p.quaternion);
+		return true;
+	}
 	bool operator()(const Perspective& p)
 	bool operator()(const Perspective& p)
 	{
 	{
 		return false;
 		return false;
@@ -254,6 +259,31 @@ struct ResolveTransformVisitor
 
 
 
 
 
 
+
+
+bool Primitive::ResolveTransform(Matrix4f & m, Element & e) const noexcept
+{
+	ResolveTransformVisitor visitor{ m, e };
+
+	bool result = std::visit(visitor, primitive);
+
+	return result;
+}
+
+bool Primitive::ResolvePerspective(float & p, Element & e) const noexcept
+{
+	bool result = false;
+
+	if (const Perspective* perspective = std::get_if<Perspective>(&primitive))
+	{
+		p = perspective->values[0].ResolveDepth(e);
+		result = true;
+	}
+
+	return result;
+}
+
+
 struct SetIdentityVisitor
 struct SetIdentityVisitor
 {
 {
 	template <size_t N>
 	template <size_t N>
@@ -298,6 +328,14 @@ struct SetIdentityVisitor
 	{
 	{
 		p.values[0] = p.values[1] = p.values[2] = 1;
 		p.values[0] = p.values[1] = p.values[2] = 1;
 	}
 	}
+	void operator()(DecomposedMatrix4& p)
+	{
+		p.perspective = Vector4f(0, 0, 0, 1);
+		p.quaternion = Vector4f(0, 0, 0, 1);
+		p.translation = Vector3f(0, 0, 0);
+		p.scale = Vector3f(1, 1, 1);
+		p.skew = Vector3f(0, 0, 0);
+	}
 };
 };
 
 
 
 
@@ -306,31 +344,30 @@ void Primitive::SetIdentity() noexcept
 	std::visit(SetIdentityVisitor{}, primitive);
 	std::visit(SetIdentityVisitor{}, primitive);
 }
 }
 
 
-
-bool Primitive::ResolveTransform(Matrix4f & m, Element & e) const noexcept
+// Interpolate two quaternions a, b with the factor alpha
+static Vector4f QuaternionSlerp(const Vector4f& a, const Vector4f& b, float alpha)
 {
 {
-	ResolveTransformVisitor visitor{ m, e };
+	using namespace Math;
 
 
-	bool result = std::visit(visitor, primitive);
+	float dot = a.DotProduct(b);
+	dot = Clamp(dot, -1.f, 1.f);
+	
+	if (dot == 1)
+		return a;
 
 
-	return result;
-}
+	float theta = ACos(dot);
+	float w = Sin(alpha * theta) / SquareRoot(1.f - dot * dot);
+	float a_scale = Cos(alpha*theta) - dot * w;
 
 
-bool Primitive::ResolvePerspective(float & p, Element & e) const noexcept
-{
-	bool result = false;
-
-	if (const Perspective* perspective = std::get_if<Perspective>(&primitive))
+	Vector4f result;
+	for (int i = 0; i < 4; i++)
 	{
 	{
-		p = perspective->values[0].ResolveDepth(e);
-		result = true;
+		result[i] = a[i] * a_scale + b[i] * w;
 	}
 	}
 
 
 	return result;
 	return result;
 }
 }
 
 
-
-
 struct InterpolateVisitor
 struct InterpolateVisitor
 {
 {
 	const PrimitiveVariant& other_variant;
 	const PrimitiveVariant& other_variant;
@@ -360,6 +397,15 @@ struct InterpolateVisitor
 	//  // Also, Matrix2d, Perspective, and conditionally Rotate3d get interpolated in this way
 	//  // Also, Matrix2d, Perspective, and conditionally Rotate3d get interpolated in this way
 	//}
 	//}
 
 
+	void Interpolate(DecomposedMatrix4& p0, const DecomposedMatrix4& p1)
+	{
+		p0.perspective = p0.perspective * (1.0f - alpha) + p1.perspective * alpha;
+		p0.quaternion = QuaternionSlerp(p0.quaternion, p1.quaternion, alpha);
+		p0.translation = p0.translation * (1.0f - alpha) + p1.translation * alpha;
+		p0.scale = p0.scale* (1.0f - alpha) + p1.scale* alpha;
+		p0.skew = p0.skew* (1.0f - alpha) + p1.skew* alpha;
+	}
+
 	template <typename T>
 	template <typename T>
 	void operator()(T& p0)
 	void operator()(T& p0)
 	{
 	{
@@ -474,6 +520,10 @@ struct ResolveUnitsVisitor
 		// No conversion needed for resolved transforms
 		// No conversion needed for resolved transforms
 		return true;
 		return true;
 	}
 	}
+	bool operator()(DecomposedMatrix4& p)
+	{
+		return true;
+	}
 	bool operator()(Perspective& p)
 	bool operator()(Perspective& p)
 	{
 	{
 		// Perspective is special and not used for transform animations, ignore.
 		// Perspective is special and not used for transform animations, ignore.
@@ -488,7 +538,105 @@ bool Primitive::ResolveUnits(Element & e) noexcept
 }
 }
 
 
 
 
+static Vector3f Combine(const Vector3f& a, const Vector3f& b, float a_scale, float b_scale)
+{
+	Vector3f result;
+	result.x = a_scale * a.x + b_scale * b.x;
+	result.y = a_scale * a.y + b_scale * b.y;
+	result.z = a_scale * a.z + b_scale * b.z;
+	return result;
+}
+
+
+
+bool DecomposedMatrix4::Decompose(const Matrix4f & m)
+{
+	// Follows the procedure given in https://drafts.csswg.org/css-transforms-2/#interpolation-of-3d-matrices
+
+	if (m[3][3] == 0)
+		return false;
+
+	// Perspective matrix
+	Matrix4f p = m;
 
 
+	for (int i = 0; i < 3; i++)
+		p[i][3] = 0;
+	p[3][3] = 1;
+
+	if (p.Determinant() == 0)
+		return false;
+
+	if (m[0][3] != 0 || m[1][3] != 0 || m[2][3] != 0)
+	{
+		auto rhs = m.GetColumn(3);
+		Matrix4f p_inv = p;
+		if (!p_inv.Invert())
+			return false;
+		auto& p_inv_trans = p.Transpose();
+		perspective = p_inv_trans * rhs;
+	}
+	else
+	{
+		perspective[0] = perspective[1] = perspective[2] = 0;
+		perspective[3] = 1;
+	}
+
+	for (int i = 0; i < 3; i++)
+		translation[i] = m[3][i];
+
+	Vector3f row[3];
+	for (int i = 0; i < 3; i++)
+	{
+		row[i][0] = m[i][0];
+		row[i][1] = m[i][1];
+		row[i][2] = m[i][2];
+	}
+
+	scale[0] = row[0].Magnitude();
+	row[0] = row[0].Normalise();
+
+	skew[0] = row[0].DotProduct(row[1]);
+	row[1] = Combine(row[1], row[0], 1, -skew[0]);
+
+	scale[1] = row[1].Magnitude();
+	row[1] = row[1].Normalise();
+	skew[0] /= scale[1];
+
+	skew[1] = row[0].DotProduct(row[2]);
+	row[2] = Combine(row[2], row[0], 1, -skew[1]);
+	skew[2] = row[1].DotProduct(row[2]);
+	row[2] = Combine(row[2], row[1], 1, -skew[2]);
+
+	scale[2] = row[2].Magnitude();
+	row[2] = row[2].Normalise();
+	skew[1] /= scale[2];
+	skew[2] /= scale[2];
+
+	// Check if we need to flip coordinate system
+	auto pdum3 = row[1].CrossProduct(row[2]);
+	if (row[0].DotProduct(pdum3) < 0.0f)
+	{
+		for (int i = 0; i < 3; i++)
+		{
+			scale[i] *= -1.f;
+			row[i] *= -1.f;
+		}
+	}
+
+	quaternion[0] = 0.5f * Math::SquareRoot(Math::Max(1.f + row[0][0] - row[1][1] - row[2][2], 0.0f));
+	quaternion[1] = 0.5f * Math::SquareRoot(Math::Max(1.f - row[0][0] + row[1][1] - row[2][2], 0.0f));
+	quaternion[2] = 0.5f * Math::SquareRoot(Math::Max(1.f - row[0][0] - row[1][1] + row[2][2], 0.0f));
+	quaternion[3] = 0.5f * Math::SquareRoot(Math::Max(1.f + row[0][0] + row[1][1] + row[2][2], 0.0f));
+
+	if (row[2][1] > row[1][2])
+		quaternion[0] *= -1.f;
+	if (row[0][2] > row[2][0])
+		quaternion[1] *= -1.f;
+	if (row[1][0] > row[0][1])
+		quaternion[2] *= -1.f;
+
+	return true;
+}
 
 
 }
 }
 }
 }