diff --git a/CMakeLists.txt b/CMakeLists.txt
index 14cea49b702abb8715aeb8e8938ab01d6771d175..f7942870da7ecf50291e955e4aba9d23b02597e1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -105,6 +105,17 @@ set(GFX3_SUPPORT_SOURCES
 
 
 # Executables
+add_executable(demo_GMR01
+	${GFX2_SUPPORT_SOURCES}
+	${PROJECT_SOURCE_DIR}/src/demo_GMR01.cpp
+)
+target_link_libraries(demo_GMR01
+	${OPENGL_LIBRARIES}
+	${GLEW_LIBRARIES}
+	${GLFW_LIB}
+	${ARMADILLO_LIBRARIES}
+)
+
 add_executable(demo_infHorLQR01_glsl
 	${GFX3_SUPPORT_SOURCES}
 	${PROJECT_SOURCE_DIR}/src/demo_infHorLQR01_glsl.cpp
@@ -165,21 +176,21 @@ target_link_libraries(demo_MPC_velocity01
 	${GLFW_LIB}
 )
 
-add_executable(demo_online_gmm01
+add_executable(demo_online_GMM01
 	${GL2_SUPPORT_SOURCES}
-	${PROJECT_SOURCE_DIR}/src/demo_online_gmm01.cpp
+	${PROJECT_SOURCE_DIR}/src/demo_online_GMM01.cpp
 )
-target_link_libraries(demo_online_gmm01
+target_link_libraries(demo_online_GMM01
 	${OPENGL_LIBRARIES}
 	${ARMADILLO_LIBRARIES}
 	${GLFW_LIB}
 )
 
-add_executable(demo_online_hsmm01
+add_executable(demo_online_HSMM01
 	${GL2_SUPPORT_SOURCES}
-	${PROJECT_SOURCE_DIR}/src/demo_online_hsmm01.cpp
+	${PROJECT_SOURCE_DIR}/src/demo_online_HSMM01.cpp
 )
-target_link_libraries(demo_online_hsmm01
+target_link_libraries(demo_online_HSMM01
 	${OPENGL_LIBRARIES}
 	${ARMADILLO_LIBRARIES}
 	${GLFW_LIB}
@@ -239,10 +250,10 @@ target_link_libraries(demo_Riemannian_pose_batchLQR01
 	${GLFW_LIB}
 )
 
-add_executable(demo_Riemannian_pose_gmm01
-	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_pose_gmm01.cpp
+add_executable(demo_Riemannian_pose_GMM01
+	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_pose_GMM01.cpp
 )
-target_link_libraries(demo_Riemannian_pose_gmm01
+target_link_libraries(demo_Riemannian_pose_GMM01
 	${ARMADILLO_LIBRARIES}
 )
 
@@ -256,10 +267,10 @@ target_link_libraries(demo_Riemannian_pose_infHorLQR01
 	${GLFW_LIB}
 )
 
-add_executable(demo_Riemannian_pose_tpgmm01
-	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_pose_tpgmm01.cpp
+add_executable(demo_Riemannian_pose_TPGMM01
+	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_pose_TPGMM01.cpp
 )
-target_link_libraries(demo_Riemannian_pose_tpgmm01
+target_link_libraries(demo_Riemannian_pose_TPGMM01
 	${ARMADILLO_LIBRARIES}
 )
 
@@ -273,21 +284,21 @@ target_link_libraries(demo_Riemannian_quat_infHorLQR01
 	${GLFW_LIB}
 )
 
-add_executable(demo_Riemannian_quat_tpgmm01
+add_executable(demo_Riemannian_quat_TPGMM01
 	${GL2_SUPPORT_SOURCES}
-	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_quat_tpgmm01.cpp
+	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_quat_TPGMM01.cpp
 )
-target_link_libraries(demo_Riemannian_quat_tpgmm01
+target_link_libraries(demo_Riemannian_quat_TPGMM01
 	${ARMADILLO_LIBRARIES}
 	${OPENGL_LIBRARIES}
 	${GLFW_LIB}
 )
 
-add_executable(demo_Riemannian_sphere_gmm01
+add_executable(demo_Riemannian_sphere_GMM01
 	${GFX2_SUPPORT_SOURCES}
-	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_sphere_gmm01.cpp
+	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_sphere_GMM01.cpp
 )
-target_link_libraries(demo_Riemannian_sphere_gmm01
+target_link_libraries(demo_Riemannian_sphere_GMM01
 	${OPENGL_LIBRARIES}
 	${ARMADILLO_LIBRARIES}
 	${GLEW_LIBRARIES}
@@ -316,11 +327,11 @@ target_link_libraries(demo_Riemannian_sphere_product01
 	${GLFW_LIB}
 )
 
-add_executable(demo_Riemannian_sphere_tpgmm01
+add_executable(demo_Riemannian_sphere_TPGMM01
 	${GFX2_SUPPORT_SOURCES}
-	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_sphere_tpgmm01.cpp
+	${PROJECT_SOURCE_DIR}/src/demo_Riemannian_sphere_TPGMM01.cpp
 )
-target_link_libraries(demo_Riemannian_sphere_tpgmm01
+target_link_libraries(demo_Riemannian_sphere_TPGMM01
 	${OPENGL_LIBRARIES}
 	${ARMADILLO_LIBRARIES}
 	${GLEW_LIBRARIES}
@@ -337,3 +348,14 @@ target_link_libraries(demo_TPbatchLQR01
 	${GLFW_LIB}
 	${ARMADILLO_LIBRARIES}
 )
+
+add_executable(demo_TPGMMProduct01
+		${GFX2_SUPPORT_SOURCES}
+		${PROJECT_SOURCE_DIR}/src/demo_TPGMMProduct01.cpp
+		)
+target_link_libraries(demo_TPGMMProduct01
+		${OPENGL_LIBRARIES}
+		${GLEW_LIBRARIES}
+		${GLFW_LIB}
+		${ARMADILLO_LIBRARIES}
+		)
diff --git a/README.md b/README.md
index c37e84e15da6857cdad5ed6ef96c8db4f920828c..8a9f9d8203b17dec6b1ae6ef4611ee5c6b1e58bb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# PbDlib-cpp
+# PbDlib-cpp-sandbox
 
-PbDlib-cpp is a set of tools in C++ combining statistical learning, optimal control and differential geometry for programming-by-demonstration applications.
+PbDlib-cpp-sandbox is a set of tools in C++ combining statistical learning, optimal control and differential geometry for programming-by-demonstration applications.
 Other versions of the library are available at http://www.idiap.ch/software/pbdlib/ (in Matlab, C++ and Python).
 
 
@@ -63,6 +63,12 @@ See [https://gitlab.idiap.ch/rli/pbdlib-cpp/blob/master/examples.md](https://git
 
 ### Gallery
 
+demo\_GMR01
+
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_GMR01.gif)
+
+***
+
 demo\_MPC\_batch01
 
 ![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_MPC_batch01.gif)
@@ -87,9 +93,9 @@ demo\_MPC\_velocity01
 
 ***
 
-demo\_online\_gmm01
+demo\_online\_GMM01
 
-![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_online_gmm01.gif)
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_online_GMM01.gif)
 
 ***
 
@@ -129,15 +135,15 @@ demo\_Riemannian\_quat\_infHorLQR01
 
 ***
 
-demo\_Riemannian\_quat\_tpgmm01
+demo\_Riemannian\_quat\_TPGMM01
 
-![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_quat_tpgmm01.gif)
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_quat_TPGMM01.gif)
 
 ***
 
-demo\_Riemannian\_sphere\_gmm01
+demo\_Riemannian\_sphere\_GMM01
 
-![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_sphere_gmm01.gif)
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_sphere_GMM01.gif)
 
 ***
 
@@ -153,12 +159,18 @@ demo\_Riemannian\_sphere\_product01
 
 ***
 
-demo\_Riemannian\_sphere\_tpgmm01
+demo\_Riemannian\_sphere\_TPGMM01
 
-![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_sphere_tpgmm01.gif)
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_Riemannian_sphere_TPGMM01.gif)
 
 ***
 
 demo\_TPbatchLQR01
 
 ![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_TPbatchLQR01.gif)
+
+***
+
+demo\_TPGMMProduct01
+
+![](https://gitlab.idiap.ch/rli/pbdlib-cpp/raw/master/images/demo_TPGMMProduct01.gif)
diff --git a/examples.md b/examples.md
index 426067a769093220708f1bf7f1567d4f035038b1..7d0ac36ea7e64b0c61a69e939539d5574cb141cb 100644
--- a/examples.md
+++ b/examples.md
@@ -153,23 +153,25 @@ This project will build a number of executables, as listed in the table below.
 | Filename | ref. | Description |
 |----------|------|-------------|
 | demo\_infHorLQR01\_glsl | [1] | Discrete infinite horizon linear quadratic regulation |
+| demo\_GMR01 | [1] | Gaussian mixture model (GMM) and time-based Gaussian mixture regression (GMR) used for reproduction |
 | demo\_MPC\_batch01 | [1,8] | Model predictive control (MPC) with batch linear quadratic tracking (LQT) formulation |
 | demo\_MPC\_iterative01 | [1,8] | Model predictive control (MPC) with iterative linear quadratic tracking (LQT) formulation |
 | demo\_MPC\_semitied01 | [5,8] | MPC with semi-tied covariances |
 | demo\_MPC\_velocity01 | [1,8] | MPC with an objective including velocity tracking |
-| demo\_online\_gmm01 | [6] | Online GMM learning and LQR-based trajectory generation |
-| demo\_online\_hsmm01 | [2,7] | Online HSMM learning and sampling with LQR-based trajectory generation |
+| demo\_online\_GMM01 | [6] | Online GMM learning and LQR-based trajectory generation |
+| demo\_online\_HSMM01 | [2,7] | Online HSMM learning and sampling with LQR-based trajectory generation |
 | demo\_proMP01 | [] | Conditioning on trajectory distributions with ProMP |
 | demo\_Riemannian\_cov\_GMR01 | [10] | GMR with time as input and covariance data as output by relying on Riemannian manifold |
 | demo\_Riemannian\_cov\_interp02 | [4] | Covariance interpolation on Riemannian manifold from a GMM with augmented covariances |
 | demo\_Riemannian\_pose\_batchLQR01 | [] | Linear quadratic regulation of pose by relying on Riemannian manifold and batch LQR (MPC without constraints) |
-| demo\_Riemannian\_pose\_gmm01 | [] | GMM for 3D position and unit quaternion data by relying on Riemannian manifold |
+| demo\_Riemannian\_pose\_GMM01 | [] | GMM for 3D position and unit quaternion data by relying on Riemannian manifold |
 | demo\_Riemannian\_pose\_infHorLQR01 | [] | Linear quadratic regulation of pose by relying on Riemannian manifold and infinite-horizon LQR |
-| demo\_Riemannian\_pose\_tpgmm01 | [] | TP-GMM of pose (R3 x S3) with two frames |
+| demo\_Riemannian\_pose\_TPGMM01 | [] | TP-GMM of pose (R3 x S3) with two frames |
 | demo\_Riemannian\_quat\_infHorLQR01 | [3] | Linear quadratic regulation on hypersphere (orientation as unit quaternions) by relying on Riemannian manifold and infinite-horizon LQR |
-| demo\_Riemannian\_quat\_tpgmm01 | [] | TP-GMM on S3 (unit quaternion) with two frames |
-| demo\_Riemannian\_sphere\_gmm01 | [3] | GMM for data on a sphere by relying on Riemannian manifold |
+| demo\_Riemannian\_quat\_TPGMM01 | [] | TP-GMM on S3 (unit quaternion) with two frames |
+| demo\_Riemannian\_sphere\_GMM01 | [3] | GMM for data on a sphere by relying on Riemannian manifold |
 | demo\_Riemannian\_sphere\_infHorLQR01 | [3] | Linear quadratic regulation on a sphere by relying on Riemannian manifold and infinite-horizon LQR |
 | demo\_Riemannian\_sphere\_product01 | [] | Gaussian product on sphere |
-| demo\_Riemannian\_sphere\_tpgmm01 | [3] | TP-GMM for data on a sphere by relying on Riemannian manifold (with single frame) |
+| demo\_Riemannian\_sphere\_TPGMM01 | [3] | TP-GMM for data on a sphere by relying on Riemannian manifold (with single frame) |
 | demo\_TPbatchLQR01 | [1] | Linear quadratic control (unconstrained linear MPC) acting in multiple frames, which is equivalent to the fusion of Gaussian controllers |
+| demo\_TPGMMProduct01 | [] | Product of Gaussians for a Task-Parametrized GMM |
diff --git a/images/demo_GMR01.gif b/images/demo_GMR01.gif
new file mode 100644
index 0000000000000000000000000000000000000000..04a140c5fa7cf06c8b24337cfe36d30229bd0375
Binary files /dev/null and b/images/demo_GMR01.gif differ
diff --git a/images/demo_Riemannian_quat_tpgmm01.gif b/images/demo_Riemannian_quat_TPGMM01.gif
similarity index 100%
rename from images/demo_Riemannian_quat_tpgmm01.gif
rename to images/demo_Riemannian_quat_TPGMM01.gif
diff --git a/images/demo_Riemannian_sphere_gmm01.gif b/images/demo_Riemannian_sphere_GMM01.gif
similarity index 100%
rename from images/demo_Riemannian_sphere_gmm01.gif
rename to images/demo_Riemannian_sphere_GMM01.gif
diff --git a/images/demo_Riemannian_sphere_tpgmm01.gif b/images/demo_Riemannian_sphere_TPGMM01.gif
similarity index 100%
rename from images/demo_Riemannian_sphere_tpgmm01.gif
rename to images/demo_Riemannian_sphere_TPGMM01.gif
diff --git a/images/demo_TPGMMProduct01.gif b/images/demo_TPGMMProduct01.gif
new file mode 100644
index 0000000000000000000000000000000000000000..163e82c4db08d73b9953c947693e5ceb0a07312c
Binary files /dev/null and b/images/demo_TPGMMProduct01.gif differ
diff --git a/images/demo_online_gmm01.gif b/images/demo_online_GMM01.gif
similarity index 100%
rename from images/demo_online_gmm01.gif
rename to images/demo_online_GMM01.gif
diff --git a/include/gfx2.h b/include/gfx2.h
index 8f72291dca00e61df1f160d49038f32ef6e4b995..c4960423d749700967cade511b6318a2878070aa 100644
--- a/include/gfx2.h
+++ b/include/gfx2.h
@@ -265,6 +265,7 @@ namespace gfx2
 
 		// Other
 		bool			lightning_enabled;
+		bool			use_one_minus_src_alpha_blending;
 	};
 
 	//-------------------------------------------------------------------------
@@ -298,7 +299,8 @@ namespace gfx2
 	model_t create_line(const arma::fvec& color, const arma::mat& points,
 						const arma::fvec& position = arma::zeros<arma::fvec>(3),
 						const arma::fmat& rotation = arma::eye<arma::fmat>(4, 4),
-						transforms_t* parent_transforms = 0);
+						transforms_t* parent_transforms = 0,
+						bool line_strip = true);
 
 	//-------------------------------------------------------------------------
 	// Create a line mesh, colored (no lightning), from an array containing the
@@ -307,7 +309,8 @@ namespace gfx2
 	model_t create_line(const arma::fvec& color, const std::vector<arma::vec>& points,
 						const arma::fvec& position = arma::zeros<arma::fvec>(3),
 						const arma::fmat& rotation = arma::eye<arma::fmat>(4, 4),
-						transforms_t* parent_transforms = 0);
+						transforms_t* parent_transforms = 0,
+						bool line_strip = true);
 
 	//-------------------------------------------------------------------------
 	// Create a mesh, colored (no lightning), from a matrix containing the
@@ -318,6 +321,24 @@ namespace gfx2
 						const arma::fmat& rotation = arma::eye<arma::fmat>(4, 4),
 						transforms_t* parent_transforms = 0);
 
+	//-------------------------------------------------------------------------
+	// Create a mesh representing a gaussian, colored (no lightning)
+	//-------------------------------------------------------------------------
+	model_t create_gaussian_background(const arma::fvec& color, const arma::vec& mu,
+									   const arma::mat& sigma,
+									   const arma::fvec& position = arma::zeros<arma::fvec>(3),
+									   const arma::fmat& rotation = arma::eye<arma::fmat>(4, 4),
+									   transforms_t* parent_transforms = 0);
+
+	//-------------------------------------------------------------------------
+	// Create a line mesh representing a gaussian border, colored (no lightning)
+	//-------------------------------------------------------------------------
+	model_t create_gaussian_border(const arma::fvec& color, const arma::vec& mu,
+								   const arma::mat& sigma,
+								   const arma::fvec& position = arma::zeros<arma::fvec>(3),
+								   const arma::fmat& rotation = arma::eye<arma::fmat>(4, 4),
+								   transforms_t* parent_transforms = 0);
+
 	//-------------------------------------------------------------------------
 	// Release the OpenGL resources used by the model
 	//-------------------------------------------------------------------------
@@ -414,4 +435,31 @@ namespace gfx2
 	bool intersects(const ray_t& ray, const arma::fvec& center, float radius,
 					arma::fvec &result);
 
+
+	/******************************** OTHERS *********************************/
+
+	//-------------------------------------------------------------------------
+	// Returns the vertices needed to create a mesh representing the background
+	// a gaussian
+	//
+	// The result is a matrix of shape (2, nb_points * 3)
+	//-------------------------------------------------------------------------
+	arma::mat get_gaussian_background_vertices(const arma::vec& mu, const arma::mat& sigma,
+											   int nb_points = 60);
+
+	//-------------------------------------------------------------------------
+	// Returns the vertices needed to create a line representing the border of
+	// a gaussian
+	//
+	// If line_strip is true:
+	//  - The result is a matrix of shape (2, nb_points)
+	//  - The rendering mode must be GL_LINE_STRIP
+	//
+	// If line_strip is false:
+	//  - The result is a matrix of shape (2, nb_points * 2)
+	//  - The rendering mode must be GL_LINES
+	//-------------------------------------------------------------------------
+	arma::mat get_gaussian_border_vertices(const arma::vec& mu, const arma::mat& sigma,
+										   int nb_points = 60, bool line_strip = true);
+
 };
diff --git a/src/demo_GMR01.cpp b/src/demo_GMR01.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1dda2827899402d0090c94548a6052030b8f4518
--- /dev/null
+++ b/src/demo_GMR01.cpp
@@ -0,0 +1,1026 @@
+/*
+ * demo_GMR01.cpp
+ *
+ * Gaussian mixture model (GMM) with time-based Gaussian mixture regression
+ * (GMR) used for reproduction.
+ *
+ * @article{Calinon16JIST,
+ *	 author="Calinon, S.",
+ *	 title="A Tutorial on Task-Parameterized Movement Learning and Retrieval",
+ *	 journal="Intelligent Service Robotics",
+ *	 publisher="Springer Berlin Heidelberg",
+ *	 doi="10.1007/s11370-015-0187-9",
+ *	 year="2016",
+ *	 volume="9",
+ *	 number="1",
+ *	 pages="1--29"
+ * }
+ *
+ * Authors: Sylvain Calinon, Philip Abbet
+ */
+
+
+#include <stdio.h>
+#include <armadillo>
+#include <mvn.h>
+
+#include <gfx2.h>
+#include <gfx_ui.h>
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <imgui_impl_glfw_gl2.h>
+#include <imgui_internal.h>
+#include <window_utils.h>
+
+using namespace arma;
+
+
+/***************************** ALGORITHM SECTION *****************************/
+
+typedef std::vector<vec> vector_list_t;
+typedef std::vector<mat> matrix_list_t;
+
+
+//-----------------------------------------------------------------------------
+// Contains all the parameters used by the algorithm. Some of them are
+// modifiable through the UI, others are hard-coded.
+//-----------------------------------------------------------------------------
+struct parameters_t {
+	int	  nb_states;		// Number of components in the GMM
+	int	  nb_data;			// Number of datapoints in a trajectory
+	float dt;				// Time step (without rescaling, large values such
+							// as 1 has the advantage of creating clusers based
+							// on position information)
+};
+
+
+//-----------------------------------------------------------------------------
+// Model trained using the algorithm
+//-----------------------------------------------------------------------------
+struct model_t {
+	parameters_t  parameters; // Parameters used to train the model
+
+	int			  nb_var;	 // Number of variables [t,x1,x2]
+	vector_list_t mu;
+	matrix_list_t sigma;
+	vec           priors;
+};
+
+
+//-----------------------------------------------
+
+arma::vec gaussPDF(mat Data, colvec Mu, mat Sigma) {
+
+	int nbVar = Data.n_rows;
+	int nbData = Data.n_cols;
+	Data = Data.t() - repmat(Mu.t(), nbData, 1);
+
+	vec prob = sum((Data * inv(Sigma)) % Data, 1);
+
+	prob = exp(-0.5 * prob) / sqrt(pow((2 * datum::pi), nbVar) * det(Sigma) + 2.2251E-308);
+
+	return prob;
+}
+
+//-----------------------------------------------------------------------------
+// Training of the model
+//-----------------------------------------------------------------------------
+void learn(const matrix_list_t& demos, model_t &model) {
+
+	model.nb_var = 3;
+
+	// Initialization of Gaussian Mixture Model (GMM) parameters by clustering 
+	// the data into equal bins based on the first variable (time steps).
+	const float diag_reg_fact = 1e-4f;
+
+	vec timing_sep = linspace<vec>(
+		demos[0](0, 0), demos[0](0, demos[0].n_cols - 1), model.parameters.nb_states + 1
+	);
+
+	model.mu.clear();
+	model.sigma.clear();
+	model.priors = vec(model.parameters.nb_states);
+
+	mat data(model.nb_var, model.parameters.nb_data * demos.size());
+	for (unsigned int m = 0; m < demos.size(); ++m) {
+		data(span::all, span(m * model.parameters.nb_data,
+							 (m + 1) * model.parameters.nb_data - 1)) =
+			demos[m];
+	}
+
+	for (unsigned int i = 0; i < model.parameters.nb_states; ++i) {
+		uvec idtmp = find( (data(0, span::all) >= timing_sep(i)) &&
+						   (data(0, span::all) < timing_sep(i + 1)) );
+
+		model.priors(i) = idtmp.size();
+		model.mu.push_back(mean(data.cols(idtmp), 1));
+
+		mat sigma = cov(data.cols(idtmp).t());
+
+		// Optional regularization term to avoid numerical instability
+		sigma = sigma + eye(model.nb_var, model.nb_var) * diag_reg_fact;
+
+		model.sigma.push_back(sigma);
+	}
+
+	model.priors = model.priors / sum(model.priors);
+
+
+	// Training of a Gaussian mixture model (GMM) with an expectation-maximization
+	// (EM) algorithm
+	const int nb_max_steps = 100;			// Maximum number of iterations allowed
+	const int nb_min_steps = 5;				// Minimum number of iterations allowed
+	const double max_diff_log_likelihood = 1e-4;	// Likelihood increase threshold
+													// to stop the algorithm
+
+	std::vector<double> log_likelihoods;
+
+	for (int iter = 0; iter < nb_max_steps; ++iter) {
+
+		// E-step
+		mat L = ones(model.parameters.nb_states, data.n_cols);
+
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			L(i, span::all) = model.priors(i) * mat(gaussPDF(data, model.mu[i], model.sigma[i])).t();
+		}
+
+		mat gamma = L / repmat(sum(L, 0) + DBL_MIN, model.parameters.nb_states, 1);
+		mat gamma2 = gamma / repmat(sum(gamma, 1), 1, data.n_cols);
+
+
+		// M-step
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+
+			// Update priors
+			model.priors(i) = sum(gamma(i, span::all)) / data.n_cols;
+
+			// Update mu
+			model.mu[i] = data * gamma2(i, span::all).t();
+
+			// Update sigma
+			mat data_tmp = data - repmat(model.mu[i], 1, data.n_cols);
+			model.sigma[i] = data * diagmat(gamma2(i, span::all)) * data_tmp.t() +
+							 eye(data.n_rows, data.n_rows) * diag_reg_fact;
+		}
+
+		// Compute average log-likelihood
+		log_likelihoods.push_back(vec(sum(log(sum(L, 0)), 1))[0] / data.n_cols);
+
+		// Stop the algorithm if EM converged (small change of log-likelihood)
+		if (iter >= nb_min_steps) {
+			if (log_likelihoods[iter] - log_likelihoods[iter - 1] < max_diff_log_likelihood)
+				break;
+		}
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Gaussian mixture regression (GMR)
+//-----------------------------------------------------------------------------
+void compute_GMR(const model_t& model, const vec& time_steps, mat &points,
+				 matrix_list_t &sigma_out) {
+
+	const int in = 0;
+	const span out(1, model.nb_var - 1);
+
+	const int nb_data = time_steps.n_elem;
+	const int nb_var_out = out.b - out.a + 1;
+
+	const float diag_reg_fact = 1e-8f;
+
+	mat mu_tmp = zeros(nb_var_out, model.parameters.nb_states);
+	points = zeros(nb_var_out, nb_data);
+	sigma_out.clear();
+
+	mat H(model.parameters.nb_states, nb_data);
+
+	for (int t = 0; t < nb_data; ++t) {
+
+		// Compute activation weight
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			mat time_step(1, 1);
+			time_step(0, 0) = time_steps(t);
+
+			vec mu(1);
+			mu(0) = model.mu[i](in);
+
+			mat sigma(1, 1);
+			sigma(0, 0) = model.sigma[i](in, in);
+
+			H(i, t) = model.priors(i) * gaussPDF(time_step, mu, sigma)[0];
+		}
+
+		H(span::all, t) = H(span::all, t) / sum(H(span::all, t) + DBL_MIN);
+
+		// Compute conditional means
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			mu_tmp(span::all, i) = model.mu[i](out) +
+								   model.sigma[i](out, in) / model.sigma[i](in, in) *
+									   (time_steps(t) * model.mu[i](in));
+
+			points(span::all, t) = points(span::all, t) + H(i, t) * mu_tmp(span::all, i);
+		}
+
+		// Compute conditional covariances
+		mat sigma = zeros(nb_var_out, nb_var_out);
+
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			mat sigma_tmp = model.sigma[i](out, out) -
+							model.sigma[i](out, in) / model.sigma[i](in, in) *
+								model.sigma[i](in, out);
+
+			sigma = sigma + H(i, t) * (sigma_tmp + mu_tmp(span::all, i) * mu_tmp(span::all, i).t());
+		}
+
+		sigma = sigma - points(span::all, t) * points(span::all, t).t() +
+				eye(nb_var_out, nb_var_out) * diag_reg_fact;
+
+		sigma_out.push_back(sigma);
+	}
+}
+
+
+/****************************** HELPER FUNCTIONS *****************************/
+
+static void error_callback(int error, const char* description) {
+	fprintf(stderr, "Error %d: %s\n", error, description);
+}
+
+
+//-----------------------------------------------------------------------------
+// Contains all the informations about a viewport
+//-----------------------------------------------------------------------------
+struct viewport_t {
+	int x;
+	int y;
+	int width;
+	int height;
+
+	// Projection matrix parameters
+	arma::vec projection_top_left;
+	arma::vec projection_bottom_right;
+	double projection_near;
+	double projection_far;
+};
+
+
+//-----------------------------------------------------------------------------
+// Helper function to setup a viewport
+//-----------------------------------------------------------------------------
+void setup_viewport(viewport_t* viewport, int x, int y, int width, int height,
+					double near = -1.0, double far = 1.0) {
+
+	viewport->x = x;
+	viewport->y = y;
+	viewport->width = width;
+	viewport->height = height;
+	viewport->projection_top_left = vec({ (double) -width / 2,
+										  (double) height / 2 });
+	viewport->projection_bottom_right = vec({ (double) width / 2,
+											  (double) -height / 2 });
+	viewport->projection_near = near;
+	viewport->projection_far = far;
+}
+
+
+//-----------------------------------------------------------------------------
+// Converts some coordinates from UI-space to OpenGL-space, taking the
+// coordinates of a viewport into account
+//-----------------------------------------------------------------------------
+arma::vec ui2fb(const arma::vec& coords, const gfx2::window_size_t& window_size,
+				const viewport_t& viewport) {
+	arma::vec result = coords;
+
+	// ui -> viewport
+	result(0) = coords(0) * (float) window_size.fb_width / (float) window_size.win_width - viewport.x;
+	result(1) = (window_size.win_height - coords(1)) *
+				(float) window_size.fb_height / (float) window_size.win_height - viewport.y;
+
+	// viewport -> fb
+	result(0) = result(0) - (float) viewport.width * 0.5f;
+	result(1) = result(1) - (float) viewport.height * 0.5f;
+
+	return result;
+}
+
+
+//-----------------------------------------------------------------------------
+// Converts some coordinates from OpenGL-space to UI-space, taking the
+// coordinates of a viewport into account
+//-----------------------------------------------------------------------------
+arma::vec fb2ui(const arma::vec& coords, const gfx2::window_size_t& window_size,
+				const viewport_t& viewport) {
+	arma::vec result = coords;
+
+	// fb -> viewport
+	result(0) = coords(0) + (float) viewport.width * 0.5f;
+	result(1) = coords(1) + (float) viewport.height * 0.5f;
+
+	// viewport -> ui
+	result(0) = (result(0) + viewport.x) * (float) window_size.win_width / (float) window_size.fb_width;
+
+	result(1) = window_size.win_height - (result(1) + viewport.y) * (float) window_size.win_height / (float) window_size.fb_height;
+
+	return result;
+}
+
+
+//-----------------------------------------------------------------------------
+// Colors of the displayed lines and gaussians
+//-----------------------------------------------------------------------------
+const mat COLORS({
+	{ 0.0,	0.0,  1.0  },
+	{ 0.0,	0.5,  0.0  },
+	{ 1.0,	0.0,  0.0  },
+	{ 0.0,	0.75, 0.75 },
+	{ 0.75, 0.0,  0.75 },
+	{ 0.75, 0.75, 0.0  },
+	{ 0.25, 0.25, 0.25 },
+	{ 0.0,	0.0,  1.0  },
+	{ 0.0,	0.5,  0.0  },
+	{ 1.0,	0.0,  0.0  },
+});
+
+
+//-----------------------------------------------------------------------------
+// Create a demonstration (with a length of 'timestamps.size()') from a
+// trajectory (of any length)
+//-----------------------------------------------------------------------------
+mat sample_trajectory(const vector_list_t& trajectory, const vec& time_steps) {
+
+	// Resampling of the trajectory
+	vec x(trajectory.size());
+	vec y(trajectory.size());
+	vec x2(trajectory.size());
+	vec y2(trajectory.size());
+
+	for (size_t i = 0; i < trajectory.size(); ++i) {
+		x(i) = trajectory[i](0);
+		y(i) = trajectory[i](1);
+	}
+
+	vec from_indices = linspace<vec>(0, trajectory.size() - 1, trajectory.size());
+	vec to_indices = linspace<vec>(0, trajectory.size() - 1, time_steps.size());
+
+	interp1(from_indices, x, to_indices, x2, "*linear");
+	interp1(from_indices, y, to_indices, y2, "*linear");
+
+	// Create the demonstration
+	mat demo(3, time_steps.size());
+	for (int i = 0; i < time_steps.size(); ++i) {
+		demo(0, i) = time_steps[i];
+		demo(1, i) = x2[i];
+		demo(2, i) = y2[i];
+	}
+
+	return demo;
+}
+
+
+//-----------------------------------------------------------------------------
+// Contains all the needed infos about the state of the application (values of
+// the parameters modifiable via the UI, which action the user is currently
+// doing, ...)
+//-----------------------------------------------------------------------------
+struct gui_state_t {
+	// Indicates if the user is currently drawing a new demonstration
+	bool is_drawing_demonstration;
+
+	// Indicates if the parameters dialog is displayed
+	bool is_parameters_dialog_displayed;
+
+	// Indicates if the parameters were modified through the UI
+	bool are_parameters_modified;
+
+	// Indicates if the reproductions must be recomputed
+	bool must_recompute_GMR;
+
+	// Parameters modifiable via the UI (they correspond to the ones declared
+	// in parameters_t)
+	int parameter_nb_states;
+	int parameter_nb_data;
+};
+
+
+//-----------------------------------------------------------------------------
+// Render the "demonstrations & model" viewport
+//-----------------------------------------------------------------------------
+void draw_demos_viewport(const viewport_t& viewport,
+						 const vector_list_t& current_trajectory,
+						 const matrix_list_t& demonstrations,
+						 const model_t& model) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.7f, 0.7f, 0.7f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+
+	// Draw the GMM states
+	if (!model.mu.empty()) {
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			glClear(GL_DEPTH_BUFFER_BIT);
+
+			gfx2::draw_gaussian(conv_to<fvec>::from(COLORS.row(i % 10).t()),
+								model.mu[i](span(1, model.nb_var - 1), span::all),
+								model.sigma[i](span(1, model.nb_var - 1),
+											   span(1, model.nb_var - 1))
+			);
+		}
+
+		glClear(GL_DEPTH_BUFFER_BIT);
+	}
+
+	// Draw the currently created demonstration (if any)
+	if (current_trajectory.size() > 1)
+		gfx2::draw_line(arma::fvec({0.33f, 0.97f, 0.33f}), current_trajectory);
+
+	// Draw the demonstrations
+	int color_index = 0;
+	for (size_t i = 0; i < demonstrations.size(); ++i) {
+		arma::mat datapoints = demonstrations[i](span(1, 2), span::all);
+
+		arma::fvec color = arma::conv_to<arma::fvec>::from(COLORS.row(color_index));
+
+		gfx2::draw_line(color, datapoints);
+
+		++color_index;
+		if (color_index >= demonstrations.size())
+			color_index = 0;
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Render a "reproduction" viewport
+//-----------------------------------------------------------------------------
+void draw_GMR_viewport(const viewport_t& viewport, const mat& points,
+					   const std::vector<gfx2::model_t>& models) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+
+	if (!models.empty()) {
+		for (int i = 0; i < models.size(); ++i) {
+			glClear(GL_DEPTH_BUFFER_BIT);
+			gfx2::draw(models[i]);
+		}
+
+		glClear(GL_DEPTH_BUFFER_BIT);
+
+		gfx2::draw_line(arma::fvec({0.0f, 0.0f, 0.0f}), points);
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Returns the dimensions that a plot should have inside the provided viewport
+//-----------------------------------------------------------------------------
+ivec get_plot_dimensions(const viewport_t& viewport) {
+
+	const int MARGIN = 50;
+
+	ivec result(2);
+	result(0) = viewport.width - 2 * MARGIN;
+	result(1) = viewport.height - 2 * MARGIN;
+
+	return result;
+}
+
+
+//-----------------------------------------------------------------------------
+// Render a "timeline" viewport
+//-----------------------------------------------------------------------------
+void draw_timeline_viewport(const gfx2::window_size_t& window_size,
+							const viewport_t& viewport,
+							const matrix_list_t& demonstrations,
+							const model_t& model,
+							const mat& GMR_points, matrix_list_t GMR_sigma,
+							unsigned int dimension) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+
+	ivec plot_dimensions = get_plot_dimensions(viewport);
+
+	ivec plot_top_left({ -plot_dimensions(0) / 2, plot_dimensions(1) / 2 });
+	ivec plot_bottom_right({ plot_dimensions(0) / 2, -plot_dimensions(1) / 2 });
+
+	// Axis labels
+	ui::begin("Text");
+
+	vec coords = fb2ui(vec({ -20.0, double(-viewport.height / 2 + 45) }),
+					   window_size, viewport);
+	ui::text(ImVec2(coords(0), coords(1)), "t", ImVec4(0,0,0,1));
+
+	std::stringstream label;
+	label << "x" << dimension;
+
+	coords = fb2ui(vec({ double(-viewport.width / 2) + 10, -20.0 }),
+				   window_size, viewport);
+	ui::text(ImVec2(coords(0), coords(1)), label.str(), ImVec4(0,0,0,1));
+
+	ui::end();
+
+	// Draw the axes
+	gfx2::draw_line(fvec({0.0f, 0.0f, 0.0f}),
+					mat({ { double(plot_top_left(0)), double(plot_bottom_right(0)) },
+						  { double(plot_bottom_right(1)), double(plot_bottom_right(1)) }
+						})
+	);
+
+	gfx2::draw_line(fvec({0.0f, 0.0f, 0.0f}),
+					mat({ { double(plot_top_left(0)), double(plot_top_left(0)) },
+						  { double(plot_bottom_right(1)), double(plot_top_left(1)) }
+						})
+	);
+
+	// Check if there is something to display
+	if (demonstrations.empty())
+		return;
+
+	// Draw the GMR
+	double scale_x = (double) plot_dimensions(0) / demonstrations[0](0, demonstrations[0].n_cols - 1);
+	double scale_y = (double) plot_dimensions(1) / viewport.height;
+
+	mat top_vertices(2, GMR_points.n_cols);
+	mat bottom_vertices(2, GMR_points.n_cols);
+
+	for (int j = 0; j < GMR_points.n_cols; ++j) {
+		top_vertices(0, j) = demonstrations[0](0, j) * scale_x - plot_dimensions(0) / 2;
+		top_vertices(1, j) = (GMR_points(dimension - 1, j) +
+							 sqrt(GMR_sigma[j](dimension - 1, dimension - 1))) * scale_y;
+
+		bottom_vertices(0, j) = top_vertices(0, j);
+		bottom_vertices(1, j) = (GMR_points(dimension - 1, j) -
+								sqrt(GMR_sigma[j](dimension - 1, dimension - 1))) * scale_y;
+	}
+
+	mat gmr_points(2, (GMR_points.n_cols - 1) * 6);
+
+	for (int j = 0; j < GMR_points.n_cols - 1; ++j) {
+		gmr_points(span::all, j * 6 + 0) = top_vertices(span::all, j);
+		gmr_points(span::all, j * 6 + 1) = bottom_vertices(span::all, j);
+		gmr_points(span::all, j * 6 + 2) = top_vertices(span::all, j + 1);
+
+		gmr_points(span::all, j * 6 + 3) = top_vertices(span::all, j + 1);
+		gmr_points(span::all, j * 6 + 4) = bottom_vertices(span::all, j);
+		gmr_points(span::all, j * 6 + 5) = bottom_vertices(span::all, j + 1);
+	}
+
+	gfx2::model_t gmr_model = gfx2::create_mesh(fvec({ 0.0f, 0.8f, 0.0f, 0.05f }), gmr_points);
+	gmr_model.use_one_minus_src_alpha_blending = true;
+	gfx2::draw(gmr_model);
+
+	// Draw the GMM states
+	for (int i = 0; i < model.parameters.nb_states; ++i) {
+		glClear(GL_DEPTH_BUFFER_BIT);
+
+		vec mu = model.mu[i].elem(uvec({ 0, dimension }));
+		mu(0) = mu(0) * scale_x - plot_dimensions(0) / 2;
+		mu(1) *= scale_y;
+
+		mat sigma = model.sigma[i].submat(uvec({ 0, dimension }), uvec({ 0, dimension }));
+		sigma(0, 0) = sigma(0, 0) * (scale_x * scale_x);
+		sigma(0, 1) = sigma(0, 1) * scale_x;
+		sigma(1, 0) = sigma(1, 0) * scale_x;
+
+		gfx2::draw_gaussian(conv_to<fvec>::from(COLORS.row(i % 10).t()), mu, sigma);
+	}
+
+	glClear(GL_DEPTH_BUFFER_BIT);
+
+	// Draw the demonstrations
+	int color_index = 0;
+	for (size_t i = 0; i < demonstrations.size(); ++i) {
+		arma::mat datapoints = demonstrations[i].rows(uvec({ 0, dimension }));
+
+		datapoints(0, span::all) = datapoints(0, span::all) * scale_x - plot_dimensions(0) / 2;
+		datapoints(1, span::all) *= scale_y;
+
+		arma::fvec color = arma::conv_to<arma::fvec>::from(COLORS.row(color_index));
+
+		gfx2::draw_line(color, datapoints);
+
+		++color_index;
+		if (color_index >= demonstrations.size())
+			color_index = 0;
+	}
+
+	// Draw the GRM result
+	mat points(2, GMR_points.n_cols);
+	points(0, span::all) = demonstrations[0](0, span::all);
+	points(1, span::all) = GMR_points(dimension - 1, span::all);
+
+	points(0, span::all) = points(0, span::all) * scale_x - plot_dimensions(0) / 2;
+	points(1, span::all) *= scale_y;
+
+	gfx2::draw_line(arma::fvec({0.0f, 0.0f, 0.0f}), points);
+}
+
+
+/******************************* MAIN FUNCTION *******************************/
+
+int main(int argc, char **argv) {
+	arma_rng::set_seed_random();
+
+	// Model
+	model_t model;
+
+	// Parameters
+	model.parameters.nb_states = 6;
+	model.parameters.nb_data   = 200;
+	model.parameters.dt		   = 0.001f;
+
+
+	// Take 4k screens into account (framebuffer size != window size)
+	gfx2::window_size_t window_size;
+	window_size.win_width = 800;
+	window_size.win_height = 800;
+	window_size.fb_width = -1;	// Will be known later
+	window_size.fb_height = -1;
+	int viewport_width = 0;
+	int viewport_height = 0;
+
+
+	// Initialise GLFW
+	glfwSetErrorCallback(error_callback);
+
+	if (!glfwInit())
+		return -1;
+
+	glfwWindowHint(GLFW_SAMPLES, 4);
+	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
+	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
+
+	// Open a window and create its OpenGL context
+	GLFWwindow* window = create_window_at_optimal_size(
+		"Demo Conditioning on trajectory distributions",
+		window_size.win_width, window_size.win_height
+	);
+
+	glfwMakeContextCurrent(window);
+
+
+	// Setup GLSL
+	gfx2::init();
+	glEnable(GL_SCISSOR_TEST);
+	glEnable(GL_DEPTH_TEST);
+	glEnable(GL_CULL_FACE);
+	glEnable(GL_LINE_SMOOTH);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+	// Setup ImGui
+	ImGui::CreateContext();
+	ImGui_ImplGlfwGL2_Init(window, true);
+
+
+	// Viewports
+	viewport_t viewport_demos;
+	viewport_t viewport_GMR;
+	viewport_t viewport_x1;
+	viewport_t viewport_x2;
+
+
+	// GUI state
+	gui_state_t gui_state;
+	gui_state.is_drawing_demonstration = false;
+	gui_state.is_parameters_dialog_displayed = false;
+	gui_state.are_parameters_modified = true;
+	gui_state.must_recompute_GMR = false;
+	gui_state.parameter_nb_states = model.parameters.nb_states;
+	gui_state.parameter_nb_data = model.parameters.nb_data;
+
+
+	// List of demonstrations and GMr results
+	vec time_steps;
+	matrix_list_t demos;
+	mat GMR_points;
+	matrix_list_t GMR_sigma;
+	std::vector<gfx2::model_t> GMR_models;
+
+
+	// Main loop
+	vector_list_t current_trajectory;
+	std::vector<vector_list_t> original_trajectories;
+
+	while (!glfwWindowShouldClose(window)) {
+		glfwPollEvents();
+
+		// Detect when the window was resized
+		if ((ImGui::GetIO().DisplaySize.x != window_size.win_width) ||
+			(ImGui::GetIO().DisplaySize.y != window_size.win_height)) {
+
+			window_size.win_width = ImGui::GetIO().DisplaySize.x;
+			window_size.win_height = ImGui::GetIO().DisplaySize.y;
+
+			glfwGetFramebufferSize(window, &window_size.fb_width, &window_size.fb_height);
+
+			viewport_width = window_size.fb_width / 2 - 1;
+			viewport_height = window_size.fb_height / 2 - 1;
+
+			// Update all the viewports
+			setup_viewport(&viewport_demos, 0, window_size.fb_height - viewport_height,
+						   viewport_width, viewport_height);
+
+			setup_viewport(&viewport_GMR, window_size.fb_width - viewport_width,
+						   window_size.fb_height - viewport_height,
+						   viewport_width, viewport_height);
+
+			setup_viewport(&viewport_x1, 0, 0, viewport_width, viewport_height);
+
+			setup_viewport(&viewport_x2, window_size.fb_width - viewport_width, 0,
+						   viewport_width, viewport_height);
+		}
+
+
+		// If the parameters changed, learn the model again
+		if (gui_state.are_parameters_modified) {
+
+			if (time_steps.size() != gui_state.parameter_nb_data) {
+				demos.clear();
+
+				time_steps = linspace<vec>(
+					0, gui_state.parameter_nb_data - 1, gui_state.parameter_nb_data
+				) * model.parameters.dt;
+
+				for (size_t i = 0; i < original_trajectories.size(); ++i) {
+					demos.push_back(sample_trajectory(original_trajectories[i],
+													  time_steps)
+					);
+				}
+			}
+
+			model.parameters.nb_data = gui_state.parameter_nb_data;
+			model.parameters.nb_states = gui_state.parameter_nb_states;
+
+			if (!demos.empty()) {
+				learn(demos, model);
+				gui_state.must_recompute_GMR = true;
+			}
+
+			gui_state.are_parameters_modified = false;
+		}
+
+
+		// Recompute the GMR (if necessary)
+		if (gui_state.must_recompute_GMR) {
+
+			compute_GMR(model, time_steps, GMR_points, GMR_sigma);
+
+			// Create one big mesh for the GMR viewport (for performance reasons)
+			for (int i = 0; i < GMR_models.size(); ++i)
+				gfx2::destroy(GMR_models[i]);
+			
+			GMR_models.clear();
+
+			const int NB_POINTS = 60;
+
+			mat vertices(2, NB_POINTS * 3 * GMR_sigma.size());
+			mat lines(2, NB_POINTS * 2 * GMR_sigma.size());
+
+			for (int j = 0; j < GMR_sigma.size(); ++j) {
+
+				mat v = gfx2::get_gaussian_background_vertices(GMR_points(span::all, j),
+															   GMR_sigma[j], NB_POINTS);
+
+				vertices(span::all, span(j * NB_POINTS * 3, (j + 1) * NB_POINTS * 3 - 1)) = v;
+
+				mat p = gfx2::get_gaussian_border_vertices(GMR_points(span::all, j),
+														   GMR_sigma[j], NB_POINTS, false);
+
+				lines(span::all, span(j * NB_POINTS * 2, (j + 1) * NB_POINTS * 2 - 1)) = p;
+			}
+
+			GMR_models.push_back(
+				gfx2::create_mesh(fvec({ 0.0f, 0.8f, 0.0f, 0.1f }), vertices)
+			);
+
+			GMR_models[0].use_one_minus_src_alpha_blending = true;
+
+			GMR_models.push_back(
+				gfx2::create_line(fvec({ 0.0f, 0.4f, 0.0f, 0.1f }), lines,
+								  arma::zeros<arma::fvec>(3),
+								  arma::eye<arma::fmat>(4, 4), 0, false)
+			);
+
+			gui_state.must_recompute_GMR = false;
+		}
+
+
+		// Start the rendering
+		ImGui_ImplGlfwGL2_NewFrame();
+
+		glViewport(0, 0, window_size.fb_width, window_size.fb_height);
+		glScissor(0, 0, window_size.fb_width, window_size.fb_height);
+		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+
+		draw_demos_viewport(viewport_demos, current_trajectory, demos, model);
+
+		draw_GMR_viewport(viewport_GMR, GMR_points, GMR_models);
+
+		draw_timeline_viewport(window_size, viewport_x1, demos, model, GMR_points, GMR_sigma, 1);
+
+		draw_timeline_viewport(window_size, viewport_x2, demos, model, GMR_points, GMR_sigma, 2);
+
+
+		// Window: Demonstrations & model
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 2, 36));
+		ImGui::SetNextWindowPos(ImVec2(0, 0));
+		ImGui::Begin("Demonstrations & model", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("Demonstrations & model		");
+		ImGui::SameLine();
+
+		if (ImGui::Button("Clear")) {
+			original_trajectories.clear();
+			demos.clear();
+			GMR_points = mat();
+			GMR_sigma.clear();
+			GMR_models.clear();
+			model.mu.clear();
+			model.sigma.clear();
+		}
+
+		ImGui::SameLine();
+		ImGui::Text("	  ");
+		ImGui::SameLine();
+
+		if (ImGui::Button("Parameters"))
+			gui_state.is_parameters_dialog_displayed = true;
+
+		ImGui::End();
+
+
+		// Window: GMR
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 2, 36));
+		ImGui::SetNextWindowPos(ImVec2(window_size.win_width - window_size.win_width / 2, 0));
+		ImGui::Begin("GMR", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("GMR");
+
+		ImGui::End();
+
+
+		// Window: Timeline x1
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 2, 36));
+		ImGui::SetNextWindowPos(ImVec2(0, window_size.win_height / 2));
+		ImGui::Begin("Timeline: x1", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("Timeline: x1");
+
+		ImGui::End();
+
+
+		// Window: Timeline x2
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 2, 36));
+		ImGui::SetNextWindowPos(ImVec2(window_size.win_width - window_size.win_width / 2,
+									   window_size.win_height / 2));
+		ImGui::Begin("Timeline: x2", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("Timeline: x2");
+
+		ImGui::End();
+
+
+		// Window: Parameters
+		ImGui::SetNextWindowSize(ImVec2(440, 106));
+		ImGui::SetNextWindowPos(ImVec2((window_size.win_width - 440) / 2, (window_size.win_height - 106) / 2));
+		ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 255));
+
+		if (gui_state.is_parameters_dialog_displayed)
+			ImGui::OpenPopup("Parameters");
+
+		if (ImGui::BeginPopupModal("Parameters", NULL,
+								   ImGuiWindowFlags_NoResize |
+								   ImGuiWindowFlags_NoSavedSettings)) {
+
+			ImGui::SliderInt("Nb states", &gui_state.parameter_nb_states, 2, 20);
+			ImGui::SliderInt("Nb data", &gui_state.parameter_nb_data, 100, 300);
+
+			if (ImGui::Button("Close")) {
+				ImGui::CloseCurrentPopup();
+				gui_state.is_parameters_dialog_displayed = false;
+				gui_state.are_parameters_modified = true;
+			}
+
+			ImGui::EndPopup();
+		}
+
+		ImGui::PopStyleColor();
+
+
+		// GUI rendering
+		ImGui::Render();
+		ImGui_ImplGlfwGL2_RenderDrawData(ImGui::GetDrawData());
+
+		// Swap buffers
+		glfwSwapBuffers(window);
+
+		// Keyboard input
+		if (ImGui::IsKeyPressed(GLFW_KEY_ESCAPE))
+			break;
+
+
+		if (!gui_state.is_drawing_demonstration) {
+			// Left click: start a new demonstration (only if not on the UI and in the
+			// demonstrations viewport)
+			if (ImGui::IsMouseClicked(GLFW_MOUSE_BUTTON_1)) {
+				double mouse_x, mouse_y;
+				glfwGetCursorPos(window, &mouse_x, &mouse_y);
+
+				if ((mouse_x <= window_size.win_width / 2) &&
+					(mouse_y > 36) && (mouse_y <= window_size.win_height / 2))
+				{
+					gui_state.is_drawing_demonstration = true;
+
+					vec coords = ui2fb({ mouse_x, mouse_y }, window_size, viewport_demos);
+					current_trajectory.push_back(coords);
+				}
+			}
+		} else {
+			double mouse_x, mouse_y;
+			glfwGetCursorPos(window, &mouse_x, &mouse_y);
+
+			vec coords = ui2fb({ mouse_x, mouse_y }, window_size, viewport_demos);
+
+			vec last_point = current_trajectory[current_trajectory.size() - 1];
+			vec diff = abs(coords - last_point);
+
+			if ((diff(0) > 1e-6) && (diff(1) > 1e-6))
+				current_trajectory.push_back(coords);
+
+			// Left mouse button release: end the demonstration creation
+			if (!ImGui::IsMouseDown(GLFW_MOUSE_BUTTON_1)) {
+				gui_state.is_drawing_demonstration = false;
+
+				if (current_trajectory.size() > 1) {
+					demos.push_back(sample_trajectory(current_trajectory, time_steps));
+
+					original_trajectories.push_back(current_trajectory);
+
+					learn(demos, model);
+
+					gui_state.must_recompute_GMR = true;
+				}
+
+				current_trajectory.clear();
+			}
+		}
+	}
+
+
+	// Cleanup
+	ImGui_ImplGlfwGL2_Shutdown();
+	glfwTerminate();
+
+	return 0;
+}
diff --git a/src/demo_Riemannian_pose_gmm01.cpp b/src/demo_Riemannian_pose_GMM01.cpp
similarity index 100%
rename from src/demo_Riemannian_pose_gmm01.cpp
rename to src/demo_Riemannian_pose_GMM01.cpp
diff --git a/src/demo_Riemannian_pose_tpgmm01.cpp b/src/demo_Riemannian_pose_TPGMM01.cpp
similarity index 100%
rename from src/demo_Riemannian_pose_tpgmm01.cpp
rename to src/demo_Riemannian_pose_TPGMM01.cpp
diff --git a/src/demo_Riemannian_quat_tpgmm01.cpp b/src/demo_Riemannian_quat_TPGMM01.cpp
similarity index 100%
rename from src/demo_Riemannian_quat_tpgmm01.cpp
rename to src/demo_Riemannian_quat_TPGMM01.cpp
diff --git a/src/demo_Riemannian_sphere_gmm01.cpp b/src/demo_Riemannian_sphere_GMM01.cpp
similarity index 100%
rename from src/demo_Riemannian_sphere_gmm01.cpp
rename to src/demo_Riemannian_sphere_GMM01.cpp
diff --git a/src/demo_Riemannian_sphere_tpgmm01.cpp b/src/demo_Riemannian_sphere_TPGMM01.cpp
similarity index 100%
rename from src/demo_Riemannian_sphere_tpgmm01.cpp
rename to src/demo_Riemannian_sphere_TPGMM01.cpp
diff --git a/src/demo_TPGMMProduct01.cpp b/src/demo_TPGMMProduct01.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..49274e7e7a0eb005de40d009118ff51cfab7feda
--- /dev/null
+++ b/src/demo_TPGMMProduct01.cpp
@@ -0,0 +1,1543 @@
+/*
+ * demo_TPGMMProduct01.cpp
+ *
+ * Product of Gaussians for a Task-Parametrized GMM
+ *
+ * @article{Calinon16JIST,
+ *   author="Calinon, S.",
+ *   title="A Tutorial on Task-Parameterized Movement Learning and Retrieval",
+ *   journal="Intelligent Service Robotics",
+ *   publisher="Springer Berlin Heidelberg",
+ *   doi="10.1007/s11370-015-0187-9",
+ *   year="2016",
+ *   volume="9",
+ *   number="1",
+ *   pages="1--29"
+ * }
+ *
+ * Authors: Sylvain Calinon, Philip Abbet, Andras Kupcsik
+ */
+
+
+#include <stdio.h>
+#include <float.h>
+#include <armadillo>
+
+#include <gfx2.h>
+#include <gfx_ui.h>
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <imgui_impl_glfw_gl2.h>
+#include <window_utils.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui_internal.h>
+
+using namespace arma;
+
+
+/***************************** ALGORITHM SECTION *****************************/
+
+typedef std::vector<vec> vector_list_t;
+typedef std::vector<mat> matrix_list_t;
+
+
+//-----------------------------------------------------------------------------
+// Contains all the parameters used by the algorithm. Some of them are
+// modifiable through the UI, others are hard-coded.
+//-----------------------------------------------------------------------------
+struct parameters_t {
+	int	   nb_states;		// Number of components in the GMM
+	int	   nb_frames;		// Number of candidate frames of reference
+	int	   nb_deriv;		// Number of static and dynamic features
+	int	   nb_data;			// Number of datapoints in a trajectory <==== not used for product of Gaussians!!!
+	float  dt;				// Time step duration					<==== not used for product of Gaussians!!!
+};
+
+//-----------------------------------------------------------------------------
+// Model trained using the algorithm
+//-----------------------------------------------------------------------------
+struct model_t {
+	parameters_t			   parameters;	// Parameters used to train the model
+
+	// These lists contain one element per GMM state and per frame (access them
+	// by doing: mu[state][frame])
+	std::vector<vector_list_t> mu;
+	std::vector<matrix_list_t> sigma;
+
+	int						   nb_var;
+	mat						   pix;
+	vec						   priors;
+};
+
+
+//-----------------------------------------------------------------------------
+// Represents a coordinate system, aka a reference frame
+//-----------------------------------------------------------------------------
+struct coordinate_system_t {
+
+	coordinate_system_t(const arma::vec& position, const arma::mat& orientation,
+						const parameters_t& parameters) {
+
+		this->position = zeros(2 + (parameters.nb_deriv - 1) * 2);
+		this->position(span(0, 1)) = position(span(0, 1));
+
+		this->orientation = kron(eye(parameters.nb_deriv, parameters.nb_deriv),
+								 orientation(span(0, 1), span(0, 1)));
+	}
+
+	vec position;
+	mat orientation;
+};
+
+
+//-----------------------------------------------------------------------------
+// Represents a list of coordinate systems
+//-----------------------------------------------------------------------------
+typedef std::vector<coordinate_system_t> coordinate_system_list_t;
+
+
+//-----------------------------------------------------------------------------
+// Contains all the needed informations about a demonstration, like:
+//	 - its reference frames
+//	 - the trajectory originally drawn by the user
+//	 - the resampled trajectory
+//	 - the trajectory expressed in each reference frame
+//-----------------------------------------------------------------------------
+class Demonstration
+{
+public:
+	Demonstration(coordinate_system_list_t coordinate_systems,
+				  const std::vector<arma::vec>& points,
+				  const parameters_t& parameters)
+			: coordinate_systems(coordinate_systems)
+	{
+		points_original = mat(2, points.size());
+
+		for (size_t i = 0; i < points.size(); ++i) {
+			points_original(0, i) = points[i](0);
+			points_original(1, i) = points[i](1);
+		}
+
+		update(parameters);
+	}
+
+
+	//-------------------------------------------------------------------------
+	// Resample the trajectory and recompute it in each reference frame
+	// according to the provided parameters
+	//-------------------------------------------------------------------------
+	void update(const parameters_t& parameters)
+	{
+		// Resampling of the trajectory
+		arma::vec x = points_original.row(0).t();
+		arma::vec y = points_original.row(1).t();
+		arma::vec x2(parameters.nb_data);
+		arma::vec y2(parameters.nb_data);
+
+		arma::vec from_indices = arma::linspace<arma::vec>(0, points_original.n_cols - 1, points_original.n_cols);
+		arma::vec to_indices = arma::linspace<arma::vec>(0, points_original.n_cols - 1, parameters.nb_data);
+
+		interp1(from_indices, x, to_indices, x2, "*linear");
+		interp1(from_indices, y, to_indices, y2, "*linear");
+
+		points = mat(2 * parameters.nb_deriv, parameters.nb_data);
+		points(span(0), span::all) = x2.t();
+		points(span(1), span::all) = y2.t();
+
+		// Compute the derivatives
+		mat D = (diagmat(ones(1, parameters.nb_data - 1), -1) -
+				 eye(parameters.nb_data, parameters.nb_data)) / parameters.dt;
+
+		D(parameters.nb_data - 1, parameters.nb_data - 1) = 0.0;
+
+		points(span(2, 3), span::all) = points(span(0, 1), span::all) * pow(D, 1);
+
+		// Compute the points in each coordinate system
+		points_in_coordinate_systems.clear();
+
+		for (int m = 0; m < coordinate_systems.size(); ++m) {
+			points_in_coordinate_systems.push_back(
+				pinv(coordinate_systems[m].orientation) *
+				(points - repmat(coordinate_systems[m].position, 1, parameters.nb_data))
+			);
+		}
+	}
+
+
+	//-------------------------------------------------------------------------
+	// Returns the coordinates of a point in a specific reference frame of
+	// the demonstration
+	//-------------------------------------------------------------------------
+	arma::vec convert_to_coordinate_system(const arma::vec& point, int frame) const {
+		vec original_point = zeros(points.n_rows);
+		original_point(span(0, 1)) = point(span(0, 1));
+
+		vec result = pinv(coordinate_systems[frame].orientation) *
+					 (original_point - coordinate_systems[frame].position);
+
+		return result(span(0, 1));
+	}
+
+
+public:
+	coordinate_system_list_t coordinate_systems;
+	arma::mat				 points_original;
+	arma::mat				 points;
+	matrix_list_t			 points_in_coordinate_systems;
+};
+
+
+//-----------------------------------------------------------------------------
+// Represents a list of demonstrations
+//-----------------------------------------------------------------------------
+typedef std::vector<Demonstration> demonstration_list_t;
+
+
+//-----------------------------------------------------------------------------
+// Computes the factorial of a number
+//-----------------------------------------------------------------------------
+int factorial(int n) {
+	return (n == 1 || n == 0) ? 1 : factorial(n - 1) * n;
+}
+
+
+//-----------------------------------------------------------------------------
+// Return the likelihood of datapoint(s) to be generated by a Gaussian
+// parameterized by center and covariance
+//-----------------------------------------------------------------------------
+arma::vec gaussPDF(const mat& data, colvec mu, mat sigma) {
+
+	int nb_var = data.n_rows;
+	int nb_data = data.n_cols;
+
+	mat data2 = data.t() - repmat(mu.t(), nb_data, 1);
+
+	vec prob = sum((data2 * inv(sigma)) % data2, 1);
+
+	prob = exp(-0.5 * prob) / sqrt(pow((2 * datum::pi), nb_var) * det(sigma) + DBL_MIN);
+
+	return prob;
+}
+
+
+//-----------------------------------------------------------------------------
+// Initialization of Gaussian Mixture Model (GMM) parameters by clustering an
+// ordered dataset into equal bins
+//-----------------------------------------------------------------------------
+void init_tensorGMM_kbins(const demonstration_list_t& demos,
+						  model_t &model) {
+
+	model.priors.resize(model.parameters.nb_states);
+	model.mu.clear();
+	model.sigma.clear();
+
+	model.nb_var = demos[0].points_in_coordinate_systems[0].n_rows;
+
+
+	// Initialize bins
+	uvec t_sep = linspace<uvec>(0, model.parameters.nb_data - 1,
+								model.parameters.nb_states + 1);
+
+	struct bin_t {
+		mat data;
+		vec mu;
+		mat sigma;
+	};
+
+	std::vector<bin_t> bins;
+	for (int i = 0; i < model.parameters.nb_states; ++i) {
+		bin_t bin;
+		bin.data = zeros(model.nb_var * model.parameters.nb_frames,
+						 demos.size() * (t_sep(i + 1) - t_sep(i) + 1));
+
+		bins.push_back(bin);
+	}
+
+
+	// Split each demonstration in K equal bins
+	for (int n = 0; n < demos.size(); ++n) {
+
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			int bin_size = t_sep(i + 1) - t_sep(i) + 1;
+
+			for (int m = 0; m < model.parameters.nb_frames; ++m) {
+				bins[i].data(span(m * model.nb_var, (m + 1) * model.nb_var - 1),
+							 span(n * bin_size, (n + 1) * bin_size - 1)) =
+						demos[n].points_in_coordinate_systems[m](span::all, span(t_sep(i), t_sep(i + 1)));
+			}
+		}
+	}
+
+
+	// Calculate statistics on bin data
+	for (int i = 0; i < model.parameters.nb_states; ++i) {
+		bins[i].mu = mean(bins[i].data, 1);
+		bins[i].sigma = cov(bins[i].data.t());
+		model.priors(i) = bins[i].data.n_elem;
+	}
+
+
+	// Reshape GMM into a tensor
+	for (int i = 0; i < model.parameters.nb_states; ++i) {
+		model.mu.push_back(vector_list_t());
+		model.sigma.push_back(matrix_list_t());
+	}
+
+	for (int m = 0; m < model.parameters.nb_frames; ++m) {
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			model.mu[i].push_back(bins[i].mu(span(m * model.nb_var, (m + 1) * model.nb_var - 1)));
+
+			model.sigma[i].push_back(bins[i].sigma(span(m * model.nb_var, (m + 1) * model.nb_var - 1),
+												   span(m * model.nb_var, (m + 1) * model.nb_var - 1)));
+		}
+	}
+
+	model.priors /= sum(model.priors);
+}
+
+
+//-----------------------------------------------------------------------------
+// Training of a task-parameterized Gaussian mixture model (GMM) with an
+// expectation-maximization (EM) algorithm.
+//
+// The approach allows the modulation of the centers and covariance matrices of
+// the Gaussians with respect to external parameters represented in the form of
+// candidate coordinate systems.
+//-----------------------------------------------------------------------------
+void train_EM_tensorGMM(const demonstration_list_t& demos,
+						model_t &model) {
+
+	const int nb_max_steps = 100;			// Maximum number of iterations allowed
+	const int nb_min_steps = 5;				// Minimum number of iterations allowed
+	const double max_diff_log_likelihood = 1e-5;	// Likelihood increase threshold
+	// to stop the algorithm
+	const double diag_reg_fact = 1e-2;		// Regularization term is optional
+
+
+	cube data(model.nb_var, model.parameters.nb_frames,
+			  demos.size() * model.parameters.nb_data);
+
+	for (int n = 0; n < demos.size(); ++n) {
+		for (int m = 0; m < model.parameters.nb_frames; ++m) {
+			data(span::all, span(m), span(n * model.parameters.nb_data,
+										  (n + 1) * model.parameters.nb_data - 1)) =
+					demos[n].points_in_coordinate_systems[m];
+		}
+	}
+
+
+	std::vector<double> log_likelihoods;
+
+	for (int iter = 0; iter < nb_max_steps; ++iter) {
+
+		// E-step
+		mat L = ones(model.parameters.nb_states, data.n_slices);
+
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			for (int m = 0; m < model.parameters.nb_frames; ++m) {
+
+				// Matricization/flattening of tensor
+				mat data_mat(data(span::all, span(m), span::all));
+
+				vec gamma0 = gaussPDF(data_mat, model.mu[i][m], model.sigma[i][m]);
+
+				L(i, span::all) = L(i, span::all) % gamma0.t();
+			}
+
+			L(i, span::all) =  L(i, span::all) * model.priors(i);
+		}
+
+		mat gamma = L / repmat(sum(L, 0) + DBL_MIN, L.n_rows, 1);
+		mat gamma2 = gamma / repmat(sum(gamma, 1), 1, data.n_slices);
+
+		model.pix = gamma2;
+
+
+		// M-step
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+
+			// Update priors
+			model.priors(i) = sum(gamma(i, span::all)) / data.n_slices;
+
+			for (int m = 0; m < model.parameters.nb_frames; ++m) {
+
+				// Matricization/flattening of tensor
+				mat data_mat(data(span::all, span(m), span::all));
+
+				// Update mu
+				model.mu[i][m] = data_mat * gamma2(i, span::all).t();
+
+				// Update sigma
+				mat data_tmp = data_mat - repmat(model.mu[i][m], 1, data.n_slices);
+				model.sigma[i][m] = data_tmp * diagmat(gamma2(i, span::all)) * data_tmp.t() +
+									eye(data_tmp.n_rows, data_tmp.n_rows) * diag_reg_fact;
+			}
+		}
+
+		// Compute average log-likelihood
+		log_likelihoods.push_back(vec(sum(log(sum(L, 0)), 1))[0] / data.n_slices);
+
+		// Stop the algorithm if EM converged (small change of log-likelihood)
+		if (iter >= nb_min_steps) {
+			if (log_likelihoods[iter] - log_likelihoods[iter - 1] < max_diff_log_likelihood)
+				break;
+		}
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Training of the model
+//-----------------------------------------------------------------------------
+void learn(const demonstration_list_t& demos, model_t &model) {
+
+	init_tensorGMM_kbins(demos, model);
+	train_EM_tensorGMM(demos, model);
+
+}
+
+
+/*************************** DEMONSTRATION SECTION ***************************/
+
+static void error_callback(int error, const char* description) {
+	fprintf(stderr, "Error %d: %s\n", error, description);
+}
+
+
+//-----------------------------------------------------------------------------
+// Contains all the informations about a viewport
+//-----------------------------------------------------------------------------
+struct viewport_t {
+	int x;
+	int y;
+	int width;
+	int height;
+
+	// Projection matrix parameters
+	arma::vec projection_top_left;
+	arma::vec projection_bottom_right;
+	double projection_near;
+	double projection_far;
+
+	// View matrix parameters
+	arma::fvec view;
+};
+
+
+//-----------------------------------------------------------------------------
+// Helper function to setup a viewport
+//-----------------------------------------------------------------------------
+void setup_viewport(viewport_t* viewport, int x, int y, int width, int height,
+					double near = -1.0, double far = 1.0,
+					const fvec& view_transforms = zeros<fvec>(3)) {
+
+	viewport->x = x;
+	viewport->y = y;
+	viewport->width = width;
+	viewport->height = height;
+	viewport->projection_top_left = vec({ (double) -width / 2,
+										  (double) height / 2 });
+	viewport->projection_bottom_right = vec({ (double) width / 2,
+											  (double) -height / 2 });
+	viewport->projection_near = near;
+	viewport->projection_far = far;
+	viewport->view = view_transforms;
+}
+
+
+//-----------------------------------------------------------------------------
+// Converts some coordinates from UI-space to OpenGL-space, taking the
+// coordinates of a viewport into account
+//-----------------------------------------------------------------------------
+arma::vec ui2fb(const arma::vec& coords, const gfx2::window_size_t& window_size,
+				const viewport_t& viewport) {
+	arma::vec result = coords;
+
+	// ui -> viewport
+	result(0) = coords(0) * (float) window_size.fb_width / (float) window_size.win_width - viewport.x;
+	result(1) = (window_size.win_height - coords(1)) *
+				(float) window_size.fb_height / (float) window_size.win_height - viewport.y;
+
+	// viewport -> fb
+	result(0) = result(0) - (float) viewport.width * 0.5f;
+	result(1) = result(1) - (float) viewport.height * 0.5f;
+
+	return result;
+}
+
+
+//-----------------------------------------------------------------------------
+// Converts some coordinates from OpenGL-space to UI-space, taking the
+// coordinates of a viewport into account
+//-----------------------------------------------------------------------------
+arma::vec fb2ui(const arma::vec& coords, const gfx2::window_size_t& window_size,
+				const viewport_t& viewport) {
+	arma::vec result = coords;
+
+	// fb -> viewport
+	result(0) = result(0) + (float) viewport.width * 0.5f;
+	result(1) = result(1) + (float) viewport.height * 0.5f;
+
+	// viewport -> ui
+	result(0) = (result(0) + viewport.x) * (float) window_size.win_width /
+				(float) window_size.fb_width;
+
+	result(1) = window_size.win_height - (result(1) + viewport.y) *
+										 (float) window_size.win_height / (float) window_size.fb_height;
+
+	return result;
+}
+
+
+//-----------------------------------------------------------------------------
+// Colors used by the movable handlers
+//-----------------------------------------------------------------------------
+const arma::fmat HANDLER_COLORS({
+	{ 0.0, 0.0,  0.0  },
+	{ 0.0, 0.0,  1.0  },
+	{ 0.0, 0.5,  0.0  },
+	{ 1.0, 0.0,  0.0  },
+	{ 0.0, 0.75, 0.75 },
+});
+
+
+//-----------------------------------------------------------------------------
+// Colors used by the fixed handlers
+//-----------------------------------------------------------------------------
+const arma::fmat HANDLER_FIXED_COLORS({
+	{ 0.4, 0.4,  0.4  },
+	{ 0.4, 0.4,  1.0  },
+	{ 0.2, 0.5,  0.2  },
+	{ 1.0, 0.4,  0.4  },
+	{ 0.3, 0.75, 0.75 },
+});
+
+
+//-----------------------------------------------------------------------------
+// Colors of the displayed lines and gaussians
+//-----------------------------------------------------------------------------
+const arma::mat COLORS({
+	{ 0.0,  0.0,  1.0  },
+	{ 0.0,  0.5,  0.0  },
+	{ 1.0,  0.0,  0.0  },
+	{ 0.0,  0.75, 0.75 },
+	{ 0.75, 0.0,  0.75 },
+	{ 0.75, 0.75, 0.0  },
+	{ 0.25, 0.25, 0.25 },
+	{ 0.0,  0.0,  1.0  },
+	{ 0.0,  0.5,  0.0  },
+	{ 1.0,  0.0,  0.0  },
+});
+
+
+//-----------------------------------------------------------------------------
+// An user-movable UI widget representing a coordinate system
+//
+// Can be "fixed", so the user can't move it anymore
+//-----------------------------------------------------------------------------
+class Handler
+{
+public:
+	Handler(const viewport_t* viewport, const ImVec2& position, const ImVec2& y,
+			int index)
+			: viewport(viewport), hovered(false), fixed(false), moved(false), index(index)
+	{
+		ui_position = position;
+		ui_y = y;
+
+		fvec color = HANDLER_COLORS.row(index).t();
+
+		models[0] = gfx2::create_rectangle(color, 25.0f, 5.0f);
+		models[0].transforms.parent = &transforms;
+		models[0].transforms.rotation = gfx2::rotate(arma::fvec({0.0f, 0.0f, 1.0f}),
+													 gfx2::deg2rad(90.0f));
+
+		models[1] = gfx2::create_rectangle(color, 60.0f, 5.0f);
+		models[1].transforms.parent = &transforms;
+		models[1].transforms.position(0) = 30.0f;
+		models[1].transforms.position(1) = -10.0f;
+
+		models[2] = gfx2::create_rectangle(color, 60.0f, 5.0f);
+		models[2].transforms.parent = &transforms;
+		models[2].transforms.position(0) = 30.0f;
+		models[2].transforms.position(1) = 10.0f;
+	}
+
+
+	void update(const gfx2::window_size_t& window_size)
+	{
+		transforms.position.rows(0, 1) = arma::conv_to<arma::fvec>::from(
+				ui2fb(ui_position, window_size, *viewport)
+		);
+
+		arma::vec delta = ui_y / arma::norm(arma::vec(ui_y));
+
+		transforms.rotation(0, 0) = -delta(0);
+		transforms.rotation(1, 0) = delta(1);
+		transforms.rotation(0, 1) = -delta(1);
+		transforms.rotation(1, 1) = -delta(0);
+	}
+
+
+	void update()
+	{
+		transforms.position(0) = ui_position.x;
+		transforms.position(1) = ui_position.y;
+
+		arma::vec delta = ui_y / arma::norm(arma::vec(ui_y));
+
+		transforms.rotation(0, 0) = -delta(0);
+		transforms.rotation(1, 0) = delta(1);
+		transforms.rotation(0, 1) = -delta(1);
+		transforms.rotation(1, 1) = -delta(0);
+	}
+
+
+	void viewport_resized(const gfx2::window_size_t& window_size)
+	{
+		ui_position = fb2ui(arma::conv_to<arma::vec>::from(transforms.position.rows(0, 1)),
+							window_size, *viewport);
+	}
+
+
+	void fix()
+	{
+		fixed = true;
+
+		for (int i = 0; i < 3; ++i)
+			models[i].diffuse_color = HANDLER_FIXED_COLORS.row(index).t();
+	}
+
+
+	bool draw(const gfx2::window_size_t& window_size)
+	{
+		if (!fixed)
+		{
+			std::stringstream id;
+			id << "handler" << (uint64_t) this;
+
+			ImGuiWindow* window = ImGui::GetCurrentWindow();
+			if (window->SkipItems)
+				return false;
+
+			ImGui::PushID(id.str().c_str());
+
+			// Position
+			ImVec2 current_position = ui_position;
+
+			ui_position = ui::dragger(0, ui_position, false, ui::config.draggerSize );
+
+			moved = moved || (ui_position.x != current_position.x) || (ui_position.y != current_position.y);
+
+			hovered = ImGui::IsItemHovered();
+
+			// y-axis
+			ImVec2 py = ui_position + ui_y;
+			current_position = ui_y;
+
+			py = ui::dragger(1, py, false, ui::config.draggerSize * 0.7);
+			ui_y = py - ui_position;
+
+			moved = moved || ((ui_y.x != current_position.x) || (ui_y.y != current_position.y));
+
+			hovered = ImGui::IsItemHovered() || hovered;
+
+			window->DrawList->AddLine(ui_position, py, ui::config.lineColor);
+
+			ImGui::PopID();
+
+			update(window_size);
+		}
+
+		return hovered;
+	}
+
+
+	void draw_anchor() const
+	{
+		for (int i = 0; i < 3; ++i)
+			gfx2::draw(models[i]);
+	}
+
+
+public:
+	const viewport_t* viewport;
+	int index;
+
+	ImVec2 ui_position;
+	ImVec2 ui_y;
+
+	gfx2::transforms_t transforms;
+	gfx2::model_t models[3];
+
+	bool hovered;
+	bool fixed;
+	bool moved;
+};
+
+
+//-----------------------------------------------------------------------------
+// Represents a list of handlers
+//-----------------------------------------------------------------------------
+typedef std::vector<Handler*> handler_list_t;
+
+
+//-----------------------------------------------------------------------------
+// Contains all the needed infos about the state of the application (values of
+// the parameters modifiable via the UI, which action the user is currently
+// doing, ...)
+//-----------------------------------------------------------------------------
+struct gui_state_t {
+	// Indicates if the user can draw a new demonstration
+	bool can_draw_demonstration;
+
+	// Indicates if the user is currently drawing a new demonstration
+	bool is_drawing_demonstration;
+
+	// Indicates if the parameters dialog is displayed
+	bool is_parameters_dialog_displayed;
+
+	// Indicates if the parameters were modified through the UI
+	bool are_parameters_modified;
+
+	// Parameters modifiable via the UI (they correspond to the ones declared
+	//in parameters_t)
+	int parameter_nb_states;
+	int parameter_nb_frames;
+	int parameter_nb_data;
+};
+
+
+//-----------------------------------------------------------------------------
+// Create a handler at a random position (within the given boundaries)
+//-----------------------------------------------------------------------------
+Handler* create_random_handler(const viewport_t* viewport, int index,
+							   int min_x, int min_y, int max_x, int max_y) {
+	return new Handler(viewport,
+					   ImVec2((randu() * 0.8f + 0.1f) * (max_x - min_x) + min_x,
+							  (randu() * 0.5f + 0.1f) * (max_y - min_y) + min_y),
+					   ImVec2((randu() - 0.5) * 10, randu() * -10 - 10),
+					   index);
+}
+
+
+//-----------------------------------------------------------------------------
+// Create handlers to be used for a new demonstration at random positions
+//-----------------------------------------------------------------------------
+void create_new_demonstration_handlers(const viewport_t& viewport,
+									   const gfx2::window_size_t& window_size,
+									   int nb_frames,
+									   handler_list_t &handlers) {
+
+	for (size_t i = 0; i < handlers.size(); ++i)
+		delete handlers[i];
+
+	handlers.clear();
+
+	handlers.push_back(
+			new Handler(&viewport, ImVec2(window_size.win_width / 6,
+										  window_size.win_height  - 50),
+						ImVec2(0, 30), 0)
+	);
+
+	for (int n = 1; n < nb_frames; ++n) {
+		handlers.push_back(
+			create_random_handler(&viewport, n, 10, 20,
+								  window_size.win_width / 3 - 10,
+								  window_size.win_height / 2 - 20)
+		);
+	};
+}
+
+
+//-----------------------------------------------------------------------------
+// Create handlers to be used for a new reproduction at random positions
+//-----------------------------------------------------------------------------
+void create_reproduction_handlers(const viewport_t& viewport,
+								  const gfx2::window_size_t& window_size,
+								  int nb_frames,
+								  handler_list_t &handlers) {
+
+	for (size_t i = 0; i < handlers.size(); ++i)
+		delete handlers[i];
+
+	handlers.clear();
+
+	handlers.push_back(
+		new Handler(&viewport, ImVec2(window_size.win_width / 2,
+									  window_size.win_height - 50),
+					ImVec2(0, 30), 0)
+	);
+
+	for (int n = 1; n < nb_frames; ++n) {
+		handlers.push_back(
+			create_random_handler(&viewport, n,
+								  window_size.win_width / 3 + 20,
+								  window_size.win_height / 2 + 20,
+								  window_size.win_width * 2 / 3 - 20,
+								  window_size.win_height - 20)
+		);
+	};
+}
+
+
+//-----------------------------------------------------------------------------
+// Extract a list of coordinate systems from a list of handlers
+//-----------------------------------------------------------------------------
+void convert(const handler_list_t& from, coordinate_system_list_t &to,
+			 const parameters_t& parameters) {
+
+	to.clear();
+
+	for (int n = 0; n < from.size(); ++n) {
+		to.push_back(
+			coordinate_system_t(arma::conv_to<arma::vec>::from(from[n]->transforms.position),
+								arma::conv_to<arma::mat>::from(from[n]->transforms.rotation),
+								parameters)
+		);
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Extract a list of coordinate systems from a list of demonstrations
+//-----------------------------------------------------------------------------
+void convert(const demonstration_list_t& from, std::vector<coordinate_system_list_t> &to) {
+
+	to.clear();
+
+	for (int n = 0; n < from.size(); ++n) {
+		to.push_back(from[n].coordinate_systems);
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Compute a view matrix centered on the given reference frame across all the
+// demonstrations
+//-----------------------------------------------------------------------------
+arma::fvec compute_centered_view_matrix(const demonstration_list_t& demonstrations,
+										int frame) {
+	vec top_left({-30, 0});
+	vec bottom_right({0, 0});
+
+	for (auto iter = demonstrations.begin(); iter != demonstrations.end(); ++iter) {
+		top_left(0) = fmin(top_left(0), iter->points_in_coordinate_systems[frame](0, span::all).min());
+		top_left(1) = fmax(top_left(1), iter->points_in_coordinate_systems[frame](1, span::all).max());
+
+		bottom_right(0) = fmax(bottom_right(0), iter->points_in_coordinate_systems[frame](0, span::all).max());
+		bottom_right(1) = fmin(bottom_right(1), iter->points_in_coordinate_systems[frame](1, span::all).min());
+	}
+
+	vec center = (bottom_right - top_left) / 2 + top_left;
+
+	return fvec({ (float) -center(0), (float) -center(1), 0.0f });
+}
+
+
+//-----------------------------------------------------------------------------
+// Render the "demonstrations" viewport
+//-----------------------------------------------------------------------------
+void draw_demos_viewport(const viewport_t& viewport,
+						 const std::vector<arma::vec>& current_trajectory,
+						 const demonstration_list_t& demonstrations,
+						 const std::vector<handler_list_t> fixed_demonstration_handlers,
+						 const handler_list_t current_demonstration_handlers) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.7f, 0.7f, 0.7f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+	glTranslatef(viewport.view(0), viewport.view(1), viewport.view(2));
+
+	// Draw the currently created demonstration (if any)
+	if (current_trajectory.size() > 1)
+		gfx2::draw_line(arma::fvec({0.33f, 0.97f, 0.33f}), current_trajectory);
+
+	// Draw the demonstrations
+	int color_index = 0;
+	for (auto iter = demonstrations.begin(); iter != demonstrations.end(); ++iter) {
+		arma::mat datapoints = iter->points(span(0, 1), span::all);
+
+		arma::fvec color = arma::conv_to<arma::fvec>::from(COLORS.row(color_index));
+
+		gfx2::draw_line(color, datapoints);
+
+		++color_index;
+		if (color_index >= demonstrations.size())
+			color_index = 0;
+	}
+
+	// Draw the handlers
+	for (size_t n = 0; n < fixed_demonstration_handlers.size(); ++n) {
+		for (size_t i = 0; i < fixed_demonstration_handlers[n].size(); ++i)
+			fixed_demonstration_handlers[n][i]->draw_anchor();
+	}
+
+	for (size_t i = 0; i < current_demonstration_handlers.size(); ++i)
+		current_demonstration_handlers[i]->draw_anchor();
+}
+
+
+//-----------------------------------------------------------------------------
+// Render a "reproduction" viewport
+//-----------------------------------------------------------------------------
+void draw_reproductions_viewport(const viewport_t& viewport,
+								 const std::vector<handler_list_t>& handlers,
+								 const matrix_list_t& reproductions) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+	glTranslatef(viewport.view(0), viewport.view(1), viewport.view(2));
+
+	 // Draw the handlers
+	for (size_t n = 0; n < handlers.size(); ++n) {
+		for (size_t i = 0; i < handlers[n].size(); ++i)
+			handlers[n][i]->draw_anchor();
+	}
+}
+
+
+//-----------------------------------------------------------------------------
+// Render a "reproduction" viewport
+//-----------------------------------------------------------------------------
+void draw_reproductions_viewport(const viewport_t& viewport,
+								 const handler_list_t& handlers,
+								 const matrix_list_t& reproductions) {
+
+	std::vector<handler_list_t> handler_list;
+	handler_list.push_back(handlers);
+
+	draw_reproductions_viewport(viewport, handler_list, reproductions);
+}
+
+
+//-----------------------------------------------------------------------------
+// Render a "GMMs" viewport
+//-----------------------------------------------------------------------------
+void draw_GMMs_viewport(const viewport_t& viewport,
+								 const handler_list_t& handlers, model_t model) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+	glTranslatef(viewport.view(0), viewport.view(1), viewport.view(2));
+
+
+	std::vector<handler_list_t> handler_list;
+	handler_list.push_back(handlers);
+
+	// Draw the handlers
+	for (size_t n = 0; n < handler_list.size(); ++n) {
+		for (size_t i = 0; i < handler_list[n].size(); ++i)
+			handler_list[n][i]->draw_anchor();
+	}
+
+	// Draw the GMM states
+	if (model.mu.size() > 0 ) {
+		for (int f = 0; f < model.parameters.nb_frames; f++) {
+
+			for (int i = 0; i < model.parameters.nb_states; ++i) {
+				glClear(GL_DEPTH_BUFFER_BIT);
+				gfx2::draw_gaussian(conv_to<fvec>::from(COLORS.row(f).t()),
+									handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1) * model.mu[i][f].subvec(0, 1) + handler_list[0][f]->transforms.position.subvec(0, 1),
+									handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1) * model.sigma[i][f].submat(0, 0, 1, 1) * handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1).t());
+			}
+		}
+	}
+
+
+}
+
+//-----------------------------------------------------------------------------
+// Render a "GMMs" viewport
+//-----------------------------------------------------------------------------
+void draw_product_viewport(const viewport_t& viewport,
+						const handler_list_t& handlers, model_t model) {
+
+	glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+	glScissor(viewport.x, viewport.y, viewport.width, viewport.height);
+	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glMatrixMode(GL_PROJECTION);
+	glLoadIdentity();
+	glOrtho(viewport.projection_top_left(0), viewport.projection_bottom_right(0),
+			viewport.projection_bottom_right(1), viewport.projection_top_left(1),
+			viewport.projection_near, viewport.projection_far);
+
+	glMatrixMode(GL_MODELVIEW);
+	glLoadIdentity();
+	glTranslatef(viewport.view(0), viewport.view(1), viewport.view(2));
+
+
+	std::vector<handler_list_t> handler_list;
+	handler_list.push_back(handlers);
+
+	// Draw the handlers
+	for (size_t n = 0; n < handler_list.size(); ++n) {
+		for (size_t i = 0; i < handler_list[n].size(); ++i)
+			handler_list[n][i]->draw_anchor();
+	}
+
+	mat black = {0.0, 0.0, 0.0};
+
+	// Draw the GMM states
+	if (model.mu.size() > 0 ) {
+		for (int i = 0; i < model.parameters.nb_states; ++i) {
+			vec mu = zeros(2, 1);
+			mat cov = zeros(2, 2);
+			for (int f = 0; f < model.parameters.nb_frames; f++) {
+				vec mutmp = handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1) * model.mu[i][f].subvec(0, 1) + handler_list[0][f]->transforms.position.subvec(0, 1);
+				mat covtmp = handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1) * model.sigma[i][f].submat(0, 0, 1, 1) * handler_list[0][f]->transforms.rotation.submat(0, 0, 1, 1).t();
+				mat icovtmp = arma::inv(covtmp);
+				cov = cov + icovtmp;
+				mu = mu + icovtmp * mutmp;
+
+			}
+
+			cov = arma::inv(cov);
+			mu = cov * mu;
+
+			glClear(GL_DEPTH_BUFFER_BIT);
+			gfx2::draw_gaussian(conv_to<fvec>::from(black.row(0).t()), mu, cov);
+		}
+	}
+
+
+}
+
+
+/******************************* MAIN FUNCTION *******************************/
+
+int main(int argc, char **argv) {
+	arma_rng::set_seed_random();
+
+	// Model
+	model_t model;
+
+	// Parameters
+	model.parameters.nb_states	= 4;
+	model.parameters.nb_frames	= 2;
+	model.parameters.nb_deriv	= 2;
+	model.parameters.nb_data	= 200;
+	model.parameters.dt			= 0.1f;
+
+	// Take 4k screens into account (framebuffer size != window size)
+	gfx2::window_size_t window_size;
+	window_size.win_width = 1200;
+	window_size.win_height = 400;
+	window_size.fb_width = -1;	// Will be known later
+	window_size.fb_height = -1;
+	int viewport_width = 0;
+	int viewport_height = 0;
+
+
+	// Initialise GLFW
+	glfwSetErrorCallback(error_callback);
+
+	if (!glfwInit())
+		return -1;
+
+	glfwWindowHint(GLFW_SAMPLES, 4);
+	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
+	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
+
+	// Open a window and create its OpenGL context
+	GLFWwindow* window = create_window_at_optimal_size(
+			"Demo - Gaussian Product for TPGMM",
+			window_size.win_width, window_size.win_height
+	);
+
+	glfwMakeContextCurrent(window);
+
+	// Setup GLSL
+	gfx2::init();
+	glEnable(GL_SCISSOR_TEST);
+	glEnable(GL_DEPTH_TEST);
+	glEnable(GL_CULL_FACE);
+	glEnable(GL_LINE_SMOOTH);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+	// Setup ImGui
+	ImGui::CreateContext();
+	ImGui_ImplGlfwGL2_Init(window, true);
+
+
+	// Viewports
+	viewport_t viewport_demos;
+	viewport_t viewport_product;
+	viewport_t viewport_GMMs;
+
+	// GUI state
+	gui_state_t gui_state;
+	gui_state.can_draw_demonstration = true;
+	gui_state.is_drawing_demonstration = false;
+	gui_state.is_parameters_dialog_displayed = false;
+	gui_state.are_parameters_modified = false;
+	gui_state.parameter_nb_data = model.parameters.nb_data;
+	gui_state.parameter_nb_states = model.parameters.nb_states;
+	gui_state.parameter_nb_frames = model.parameters.nb_frames;
+
+
+	// Initial handlers
+	std::vector<handler_list_t> fixed_demonstration_handlers;
+	handler_list_t current_demonstration_handlers;
+	handler_list_t reproduction_handlers;
+
+	// create_new_demonstration_handlers(viewport_demos, window_size,
+	// 								  model.parameters.nb_frames,
+	// 								  current_demonstration_handlers
+	// );
+	//
+	// create_reproduction_handlers(viewport_product, window_size,
+	// 							 model.parameters.nb_frames,
+	// 							 reproduction_handlers);
+
+
+	// List of demonstrations and reproductions
+	demonstration_list_t demos;
+
+	// Main loop
+	std::vector<arma::vec> current_trajectory;
+
+	while (!glfwWindowShouldClose(window)) {
+		glfwPollEvents();
+
+		// Detect when the window was resized
+		if ((ImGui::GetIO().DisplaySize.x != window_size.win_width) ||
+			(ImGui::GetIO().DisplaySize.y != window_size.win_height)) {
+
+			bool first = (window_size.win_width == -1) || (window_size.fb_width == -1);
+
+			window_size.win_width = ImGui::GetIO().DisplaySize.x;
+			window_size.win_height = ImGui::GetIO().DisplaySize.y;
+
+			glfwGetFramebufferSize(window, &window_size.fb_width, &window_size.fb_height);
+
+			viewport_width = window_size.fb_width / 3 - 1;
+			viewport_height = window_size.fb_height;
+
+			// Update all the viewports
+			setup_viewport(&viewport_demos, 0, window_size.fb_height - viewport_height,
+						   viewport_width, viewport_height);
+
+			setup_viewport(&viewport_product, viewport_width + 2,
+						   window_size.fb_height - viewport_height,
+						   viewport_width, viewport_height);
+
+			setup_viewport(&viewport_GMMs, window_size.fb_width - viewport_width,
+						   window_size.fb_height - viewport_height,
+						   viewport_width, viewport_height);
+
+
+			// Update all the handlers
+			if (!first) {
+				for (size_t i = 0; i < current_demonstration_handlers.size(); ++i)
+					current_demonstration_handlers[i]->viewport_resized(window_size);
+
+				for (size_t i = 0; i < reproduction_handlers.size(); ++i)
+					reproduction_handlers[i]->viewport_resized(window_size);
+			}
+
+			// At the very first frame: load initial data from files (can't be done
+			// outside the loop because we need to know the size of the OpenGL front
+			// buffer)
+			//
+			// Note: The loaded data was recorded in another demo with 3x2 viewports,
+			// so appropriate scaling must be applied
+			else if ((window_size.win_width != -1) && (window_size.fb_width != -1)) {
+				cube loaded_trajectories;
+				mat loaded_frames;
+				loaded_trajectories.load("data/data_tpbatch_lqr_trajectories.txt");
+				loaded_frames.load("data/data_tpbatch_lqr_frames.txt");
+
+				for (int n = 0; n < loaded_frames.n_cols / 2; ++n) {
+					Handler* handler1 = new Handler(
+						&viewport_demos,
+						ImVec2(loaded_frames(0, n * 2) * window_size.win_width,
+							   loaded_frames(1, n * 2) * window_size.win_height * 2),
+						ImVec2(loaded_frames(2, n * 2) * window_size.win_width,
+							   loaded_frames(3, n * 2) * window_size.win_height * 2),
+						0
+					);
+					handler1->update(window_size);
+					handler1->fix();
+
+					Handler* handler2 = new Handler(
+						&viewport_demos,
+						ImVec2(loaded_frames(0, n * 2 + 1) * window_size.win_width,
+							   loaded_frames(1, n * 2 + 1) * window_size.win_height * 2),
+						ImVec2(loaded_frames(2, n * 2 + 1) * window_size.win_width,
+							   loaded_frames(3, n * 2 + 1) * window_size.win_height * 2),
+						1
+					);
+					handler2->update(window_size);
+					handler2->fix();
+
+					handler_list_t handlers;
+					handlers.push_back(handler1);
+					handlers.push_back(handler2);
+
+					fixed_demonstration_handlers.push_back(handlers);
+				}
+
+				Handler* handler_reproduction_1 = new Handler(
+					&viewport_product,
+					fixed_demonstration_handlers[0][0]->ui_position +
+					ImVec2(window_size.win_width / 3, 0),
+					fixed_demonstration_handlers[0][0]->ui_y,
+					0
+				);
+				handler_reproduction_1->update(window_size);
+				reproduction_handlers.push_back(handler_reproduction_1);
+
+				Handler* handler_reproduction_2 = new Handler(
+					&viewport_product,
+					fixed_demonstration_handlers[0][1]->ui_position +
+					ImVec2(window_size.win_width / 3, 0),
+					fixed_demonstration_handlers[0][1]->ui_y,
+					1
+				);
+				handler_reproduction_2->update(window_size);
+				reproduction_handlers.push_back(handler_reproduction_2);
+
+				for (int n = 0; n < loaded_trajectories.n_slices; ++n) {
+					vector_list_t trajectory;
+					for (int i = 0; i < loaded_trajectories.n_cols; ++i) {
+						mat t = loaded_trajectories(span::all, span(i), span(n));
+						t(0, span::all) *= window_size.fb_width;
+						t(1, span::all) *= window_size.fb_height * 2;
+						trajectory.push_back(t);
+					}
+
+					coordinate_system_list_t coordinate_systems;
+					convert(fixed_demonstration_handlers[n], coordinate_systems, model.parameters);
+
+					Demonstration demonstration(coordinate_systems, trajectory, model.parameters);
+					demos.push_back(demonstration);
+				}
+
+				// Initial learning from the loaded data
+				learn(demos, model);
+			}
+		}
+
+
+		// If the parameters changed, recompute
+		if (gui_state.are_parameters_modified) {
+
+			// If the number of frames changed, clear everything
+			if (model.parameters.nb_frames != gui_state.parameter_nb_frames) {
+				demos.clear();
+				model.mu.clear();
+				model.sigma.clear();
+
+				for (size_t i = 0; i < current_demonstration_handlers.size(); ++i)
+					delete current_demonstration_handlers[i];
+
+				for (size_t n = 0; n < fixed_demonstration_handlers.size(); ++n) {
+					for (size_t i = 0; i < fixed_demonstration_handlers[n].size(); ++i)
+						delete fixed_demonstration_handlers[n][i];
+				}
+
+				for (size_t i = 0; i < reproduction_handlers.size(); ++i)
+					delete reproduction_handlers[i];
+
+				current_demonstration_handlers.clear();
+				fixed_demonstration_handlers.clear();
+				reproduction_handlers.clear();
+
+
+				create_new_demonstration_handlers(viewport_demos, window_size,
+												  gui_state.parameter_nb_frames,
+												  current_demonstration_handlers
+				);
+
+				create_reproduction_handlers(viewport_product, window_size,
+											 gui_state.parameter_nb_frames,
+											 reproduction_handlers);
+
+				model.parameters.nb_frames = gui_state.parameter_nb_frames;
+
+				gui_state.can_draw_demonstration = true;
+			}
+
+			// If the number of states changed, recompute the model
+			if (model.parameters.nb_states != gui_state.parameter_nb_states) {
+
+				model.parameters.nb_states = gui_state.parameter_nb_states;
+
+				for (auto iter = demos.begin(); iter != demos.end(); ++iter)
+					iter->update(model.parameters);
+
+				if (!demos.empty()) {
+					learn(demos, model);
+				}
+			}
+
+			gui_state.are_parameters_modified = false;
+		}
+
+
+		// Start the rendering
+		ImGui_ImplGlfwGL2_NewFrame();
+
+		glViewport(0, 0, window_size.fb_width, window_size.fb_height);
+		glScissor(0, 0, window_size.fb_width, window_size.fb_height);
+		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+
+		draw_demos_viewport(viewport_demos, current_trajectory, demos,
+							fixed_demonstration_handlers,
+							current_demonstration_handlers);
+
+		draw_product_viewport(viewport_product, reproduction_handlers, model);
+
+		draw_GMMs_viewport(viewport_GMMs, reproduction_handlers, model);
+
+
+		// Draw the UI controls of the handlers
+		ui::begin("handlers");
+
+		bool hovering_ui = false;
+
+		for (size_t i = 0; i < reproduction_handlers.size(); ++i) {
+			hovering_ui = reproduction_handlers[i]->draw(window_size) || hovering_ui;
+		}
+
+		for (size_t i = 0; i < current_demonstration_handlers.size(); ++i)
+			hovering_ui = current_demonstration_handlers[i]->draw(window_size) || hovering_ui;
+
+		ui::end();
+
+
+		// Window: Demonstrations
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 3, 36));
+		ImGui::SetNextWindowPos(ImVec2(0, 0));
+		ImGui::Begin("Demonstrations", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("Demonstrations      ");
+		ImGui::SameLine();
+
+		if (!gui_state.can_draw_demonstration) {
+			if (ImGui::Button("Add")) {
+				create_new_demonstration_handlers(viewport_demos, window_size,
+												  gui_state.parameter_nb_frames,
+												  current_demonstration_handlers
+				);
+
+				gui_state.can_draw_demonstration = true;
+			}
+		}
+
+		ImGui::SameLine();
+
+		if (ImGui::Button("Clear")) {
+			demos.clear();
+			model.mu.clear();
+			model.sigma.clear();
+
+			for (size_t i = 0; i < current_demonstration_handlers.size(); ++i)
+				delete current_demonstration_handlers[i];
+
+			for (size_t n = 0; n < fixed_demonstration_handlers.size(); ++n) {
+				for (size_t i = 0; i < fixed_demonstration_handlers[n].size(); ++i)
+					delete fixed_demonstration_handlers[n][i];
+			}
+
+			current_demonstration_handlers.clear();
+			fixed_demonstration_handlers.clear();
+
+			create_new_demonstration_handlers(viewport_demos, window_size,
+											  model.parameters.nb_frames,
+											  current_demonstration_handlers
+			);
+
+			gui_state.can_draw_demonstration = true;
+		}
+
+		ImGui::SameLine();
+		ImGui::Text("    ");
+		ImGui::SameLine();
+
+		if (ImGui::Button("Parameters"))
+			gui_state.is_parameters_dialog_displayed = true;
+
+		hovering_ui = ImGui::IsWindowHovered() || hovering_ui;
+
+		ImGui::End();
+
+		// Window: Product
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 3, 36));
+		ImGui::SetNextWindowPos(ImVec2(window_size.win_width / 3, 0));
+		ImGui::Begin("Product", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("Product");
+
+		hovering_ui = ImGui::IsWindowHovered() || hovering_ui;
+
+		ImGui::End();
+
+		// Window: GMMs in global coordinates
+		ImGui::SetNextWindowSize(ImVec2(window_size.win_width / 3, 36));
+		ImGui::SetNextWindowPos(ImVec2(window_size.win_width * 2 / 3, 0));
+		ImGui::Begin("GMMs", NULL,
+					 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
+					 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
+					 ImGuiWindowFlags_NoTitleBar
+		);
+
+		ImGui::Text("GMMs");
+
+		hovering_ui = ImGui::IsWindowHovered() || hovering_ui;
+
+		ImGui::End();
+
+
+		// Window: Parameters
+		ImGui::SetNextWindowSize(ImVec2(600, 100));
+		ImGui::SetNextWindowPos(ImVec2((window_size.win_width - 600) / 2, (window_size.win_height - 100) / 2));
+		ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 255));
+
+		if (gui_state.is_parameters_dialog_displayed)
+			ImGui::OpenPopup("Parameters");
+
+		if (ImGui::BeginPopupModal("Parameters", NULL,
+								   ImGuiWindowFlags_NoResize |
+								   ImGuiWindowFlags_NoSavedSettings)) {
+
+			ImGui::SliderInt("Nb states", &gui_state.parameter_nb_states, 1, 10);
+			ImGui::SliderInt("Nb frames", &gui_state.parameter_nb_frames, 2, 5);
+
+			if (ImGui::Button("Close")) {
+				ImGui::CloseCurrentPopup();
+				gui_state.is_parameters_dialog_displayed = false;
+				gui_state.are_parameters_modified = true;
+			}
+
+			ImGui::EndPopup();
+
+			hovering_ui = true;
+		}
+
+		ImGui::PopStyleColor();
+
+
+		// GUI rendering
+		ImGui::Render();
+		ImGui_ImplGlfwGL2_RenderDrawData(ImGui::GetDrawData());
+
+		// Swap buffers
+		glfwSwapBuffers(window);
+
+		// Keyboard input
+		if (ImGui::IsKeyPressed(GLFW_KEY_ESCAPE))
+			break;
+
+
+		// Left click: start a new demonstration (only if not on the UI and in the
+		// demonstrations viewport)
+		if (!gui_state.is_drawing_demonstration) {
+			if (ImGui::IsMouseClicked(GLFW_MOUSE_BUTTON_1) && gui_state.can_draw_demonstration) {
+				double mouse_x, mouse_y;
+				glfwGetCursorPos(window, &mouse_x, &mouse_y);
+
+				if (!hovering_ui && (mouse_x <= window_size.win_width / 3))
+				{
+					gui_state.is_drawing_demonstration = true;
+
+					vec coords = ui2fb({ mouse_x, mouse_y }, window_size, viewport_demos);
+					current_trajectory.push_back(coords);
+				}
+			}
+		} else {
+			double mouse_x, mouse_y;
+			glfwGetCursorPos(window, &mouse_x, &mouse_y);
+
+			vec coords = ui2fb({ mouse_x, mouse_y }, window_size, viewport_demos);
+
+			vec last_point = current_trajectory[current_trajectory.size() - 1];
+			vec diff = abs(coords - last_point);
+
+			if ((diff(0) > 1e-6) && (diff(1) > 1e-6))
+				current_trajectory.push_back(coords);
+
+			// Left mouse button release: end the demonstration creation
+			if (!ImGui::IsMouseDown(GLFW_MOUSE_BUTTON_1)) {
+				gui_state.is_drawing_demonstration = false;
+
+				if (current_trajectory.size() > 1) {
+
+					coordinate_system_list_t coordinate_systems;
+					convert(current_demonstration_handlers, coordinate_systems, model.parameters);
+
+					Demonstration demonstration(coordinate_systems, current_trajectory,
+												model.parameters);
+
+					demos.push_back(demonstration);
+
+					for (int i = 0; i < current_demonstration_handlers.size(); ++i)
+						current_demonstration_handlers[i]->fix();
+
+					fixed_demonstration_handlers.push_back(current_demonstration_handlers);
+					current_demonstration_handlers.clear();
+
+					learn(demos, model);
+
+					gui_state.can_draw_demonstration = false;
+				}
+
+				current_trajectory.clear();
+			}
+		}
+
+		ImGui::CaptureMouseFromApp();
+	}
+
+
+	// Cleanup
+	ImGui_ImplGlfwGL2_Shutdown();
+	glfwTerminate();
+
+	return 0;
+}
diff --git a/src/demo_online_gmm01.cpp b/src/demo_online_GMM01.cpp
similarity index 100%
rename from src/demo_online_gmm01.cpp
rename to src/demo_online_GMM01.cpp
diff --git a/src/demo_online_hsmm01.cpp b/src/demo_online_HSMM01.cpp
similarity index 100%
rename from src/demo_online_hsmm01.cpp
rename to src/demo_online_HSMM01.cpp
diff --git a/src/utils/gfx2.cpp b/src/utils/gfx2.cpp
index f9a1ee086a6c0dad55ada251dc7ef078a4895d64..b8cfbc302c0f7a6136c853aea16ed47e432e093b 100644
--- a/src/utils/gfx2.cpp
+++ b/src/utils/gfx2.cpp
@@ -284,6 +284,7 @@ model_t create_rectangle(const arma::fvec& color, float width, float height,
 
 	model.mode = GL_TRIANGLES;
 	model.lightning_enabled = false;
+	model.use_one_minus_src_alpha_blending = false;
 
 	// Position & rotation
 	model.transforms.position = position;
@@ -338,6 +339,7 @@ model_t create_square(const arma::fvec& color, float size, const arma::fvec& pos
 
 	model.mode = GL_TRIANGLES;
 	model.lightning_enabled = false;
+	model.use_one_minus_src_alpha_blending = false;
 
 	// Position & rotation
 	model.transforms.position = position;
@@ -391,6 +393,7 @@ model_t create_sphere(float radius, const arma::fvec& position,
 
 	model.mode = GL_TRIANGLES;
 	model.lightning_enabled = true;
+	model.use_one_minus_src_alpha_blending = false;
 
 	// Position & rotation
 	model.transforms.position = position;
@@ -475,12 +478,13 @@ model_t create_sphere(float radius, const arma::fvec& position,
 
 model_t create_line(const arma::fvec& color, const arma::mat& points,
 					const arma::fvec& position, const arma::fmat& rotation,
-					transforms_t* parent_transforms)
+					transforms_t* parent_transforms, bool line_strip)
 {
 	model_t model = { 0 };
 
-	model.mode = GL_LINE_STRIP;
+	model.mode = (line_strip ? GL_LINE_STRIP : GL_LINES);
 	model.lightning_enabled = false;
+	model.use_one_minus_src_alpha_blending = false;
 
 	// Position & rotation
 	model.transforms.position = position;
@@ -516,14 +520,14 @@ model_t create_line(const arma::fvec& color, const arma::mat& points,
 
 model_t create_line(const arma::fvec& color, const std::vector<arma::vec>& points,
 					const arma::fvec& position, const arma::fmat& rotation,
-					transforms_t* parent_transforms)
+					transforms_t* parent_transforms, bool line_strip)
 {
 	arma::mat points_mat(points[0].n_rows, points.size());
 
 	for (size_t i = 0; i < points.size(); ++i)
 		points_mat.col(i) = points[i];
 
-	return create_line(color, points_mat, position, rotation, parent_transforms);
+	return create_line(color, points_mat, position, rotation, parent_transforms, line_strip);
 }
 
 //-----------------------------------------------
@@ -536,6 +540,7 @@ model_t create_mesh(const arma::fvec& color, const arma::mat& vertices,
 
 	model.mode = GL_TRIANGLES;
 	model.lightning_enabled = false;
+	model.use_one_minus_src_alpha_blending = false;
 
 	// Position & rotation
 	model.transforms.position = position;
@@ -569,6 +574,71 @@ model_t create_mesh(const arma::fvec& color, const arma::mat& vertices,
 
 //-----------------------------------------------
 
+model_t create_gaussian_background(const arma::fvec& color, const arma::vec& mu,
+								   const arma::mat& sigma,
+								   const arma::fvec& position, const arma::fmat& rotation,
+								   transforms_t* parent_transforms)
+{
+	const int NB_POINTS = 60;
+
+	arma::mat pts0 = arma::join_cols(arma::cos(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, NB_POINTS)),
+									 arma::sin(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, NB_POINTS))
+	);
+
+	arma::vec eigval(2);
+	arma::mat eigvec(2, 2);
+	eig_sym(eigval, eigvec, sigma(arma::span(0, 1), arma::span(0, 1)));
+
+	arma::mat R = eigvec * diagmat(sqrt(eigval));
+
+	arma::mat pts = R * pts0 + arma::repmat(mu(arma::span(0, 1)), 1, NB_POINTS);
+
+	arma::mat vertices(2, NB_POINTS * 3);
+
+	for (int i = 0; i < NB_POINTS - 1; ++i)
+	{
+		vertices(arma::span::all, i * 3) = mu(arma::span(0, 1));
+		vertices(arma::span::all, i * 3 + 1) = pts(arma::span::all, i + 1);
+		vertices(arma::span::all, i * 3 + 2) = pts(arma::span::all, i);
+	}
+
+	vertices(arma::span::all, (NB_POINTS - 1) * 3) = mu(arma::span(0, 1));
+	vertices(arma::span::all, (NB_POINTS - 1) * 3 + 1) = pts(arma::span::all, 0);
+	vertices(arma::span::all, (NB_POINTS - 1) * 3 + 2) = pts(arma::span::all, NB_POINTS - 1);
+
+	model_t model = create_mesh(color, vertices, position, rotation, parent_transforms);
+
+	model.use_one_minus_src_alpha_blending = true;
+
+	return model;
+}
+
+//-----------------------------------------------
+
+model_t create_gaussian_border(const arma::fvec& color, const arma::vec& mu,
+							   const arma::mat& sigma,
+							   const arma::fvec& position, const arma::fmat& rotation,
+							   transforms_t* parent_transforms)
+{
+	const int NB_POINTS = 60;
+
+	arma::mat pts0 = arma::join_cols(arma::cos(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, NB_POINTS)),
+									 arma::sin(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, NB_POINTS))
+	);
+
+	arma::vec eigval(2);
+	arma::mat eigvec(2, 2);
+	eig_sym(eigval, eigvec, sigma(arma::span(0, 1), arma::span(0, 1)));
+
+	arma::mat R = eigvec * diagmat(sqrt(eigval));
+
+	arma::mat pts = R * pts0 + arma::repmat(mu(arma::span(0, 1)), 1, NB_POINTS);
+
+	return create_line(color, pts, position, rotation, parent_transforms);
+}
+
+//-----------------------------------------------
+
 void destroy(const model_t& model)
 {
 	if (model.vertex_buffer)
@@ -636,9 +706,44 @@ bool draw(const model_t& model, const light_list_t& lights)
 	glPushMatrix();
 	glMultMatrixf(model_matrix.memptr());
 
+	GLboolean is_blending_enabled = glIsEnabled(GL_BLEND);
+	GLint blend_src_rgb, blend_src_alpha, blend_dst_rgb, blend_dst_alpha;
+
+	if (model.use_one_minus_src_alpha_blending)
+	{
+		if (is_blending_enabled)
+		{
+			glGetIntegerv(GL_BLEND_SRC_RGB, &blend_src_rgb);
+			glGetIntegerv(GL_BLEND_SRC_ALPHA, &blend_src_alpha);
+			glGetIntegerv(GL_BLEND_DST_RGB, &blend_dst_rgb);
+			glGetIntegerv(GL_BLEND_DST_ALPHA, &blend_dst_alpha);
+		}
+		else
+		{
+			glEnable(GL_BLEND);
+		}
+
+		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	}
+
 	// Draw the mesh
 	glDrawArrays(model.mode, 0, model.nb_vertices);
 
+	if (model.use_one_minus_src_alpha_blending)
+	{
+		if (is_blending_enabled)
+		{
+			glBlendFunc(GL_SRC_COLOR, blend_src_rgb);
+			glBlendFunc(GL_SRC_ALPHA, blend_src_alpha);
+			glBlendFunc(GL_DST_COLOR, blend_dst_rgb);
+			glBlendFunc(GL_DST_ALPHA, blend_dst_alpha);
+		}
+		else
+		{
+			glDisable(GL_BLEND);
+		}
+	}
+
 	glPopMatrix();
 
 	// Cleanup
@@ -828,4 +933,72 @@ bool intersects(const ray_t& ray, const arma::fvec& center, float radius,
 	return true;
 }
 
+
+/********************************** OTHERS ***********************************/
+
+arma::mat get_gaussian_background_vertices(const arma::vec& mu, const arma::mat& sigma,
+									 	   int nb_points)
+{
+	arma::mat pts0 = arma::join_cols(arma::cos(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, nb_points)),
+									 arma::sin(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, nb_points))
+	);
+
+	arma::vec eigval(2);
+	arma::mat eigvec(2, 2);
+	eig_sym(eigval, eigvec, sigma(arma::span(0, 1), arma::span(0, 1)));
+
+	arma::mat R = eigvec * diagmat(sqrt(eigval));
+
+	arma::mat pts = R * pts0 + arma::repmat(mu(arma::span(0, 1)), 1, nb_points);
+
+	arma::mat vertices(2, nb_points * 3);
+
+	for (int i = 0; i < nb_points - 1; ++i)
+	{
+		vertices(arma::span::all, i * 3) = mu(arma::span(0, 1));
+		vertices(arma::span::all, i * 3 + 1) = pts(arma::span::all, i + 1);
+		vertices(arma::span::all, i * 3 + 2) = pts(arma::span::all, i);
+	}
+
+	vertices(arma::span::all, (nb_points - 1) * 3) = mu(arma::span(0, 1));
+	vertices(arma::span::all, (nb_points - 1) * 3 + 1) = pts(arma::span::all, 0);
+	vertices(arma::span::all, (nb_points - 1) * 3 + 2) = pts(arma::span::all, nb_points - 1);
+
+	return vertices;
+}
+
+//-----------------------------------------------
+
+arma::mat get_gaussian_border_vertices(const arma::vec& mu, const arma::mat& sigma,
+								 	   int nb_points, bool line_strip)
+{
+	arma::mat pts0 = arma::join_cols(arma::cos(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, nb_points)),
+									 arma::sin(arma::linspace<arma::rowvec>(0, 2 * arma::datum::pi, nb_points))
+	);
+
+	arma::vec eigval(2);
+	arma::mat eigvec(2, 2);
+	eig_sym(eigval, eigvec, sigma(arma::span(0, 1), arma::span(0, 1)));
+
+	arma::mat R = eigvec * diagmat(sqrt(eigval));
+
+	arma::mat pts = R * pts0 + arma::repmat(mu(arma::span(0, 1)), 1, nb_points);
+
+	if (line_strip)
+		return pts;
+
+	arma::mat vertices(2, nb_points * 2);
+
+	for (int i = 0; i < nb_points - 1; ++i)
+	{
+		vertices(arma::span::all, i * 2) = pts(arma::span::all, i);;
+		vertices(arma::span::all, i * 2 + 1) = pts(arma::span::all, i + 1);
+	}
+
+	vertices(arma::span::all, (nb_points - 1) * 2) = pts(arma::span::all, 0);;
+	vertices(arma::span::all, (nb_points - 1) * 2 + 1) = pts(arma::span::all, nb_points - 1);
+
+	return vertices;
+}
+
 }