Transformations basiques

A propos du code

Le code dans cet article a été écrit avec Code::Blocks et la SDL 2.
Vous pouvez trouver ici un guide pour installer ces logiciels.
Bien qu'il soit basé sur la SDL, je n'utilise pas ses fonctions directement. J'ai écrit une petite bibliothèque
avec quelques fonctions basiques pour faciliter la compréhension et la portabilité dans un autre langage.
Vous pouvez en apprendre plus sur cette bibliothèque ici.

Cet article utilise la fonction de ligne qu'on a ajouté dans l'algorithme de ligne de Bresenham.
Et la classe Vec2f qu'on a ajoutée dans vecteurs.

Formes

Dans cet article, je vais parler des transformations basiques en 2D: la translation, l'homothétie et la rotation.
Mais pour les visualiser clairement, on va devoir tracer une forme simple.
J'ai choisi un triangle centré autour de l'origine.


Comme on l'a dit auparavant, un point peut être vu comme un vecteur. Donc on va utiliser la classe Vec2f que j'ai
introduite dans les 2 derniers articles.
On va voir qu'appliquer une transformation à notre triangle revient à l'appliquer à chacun de ses points.

Mais d'abord dessinons notre triangle. Dans le code il est défini comme un tableau:
		// define the shape
		Vec2f shape[] =
		{
			{-20, 20}, {-20, -20}, {30, 20}
		};
				
Et on le dessine comme si l'origine était au centre de l'écran
		void drawShape(Vec2f* points, int nbPoints, Color color)
		{
			for (int i = 0; i < nbPoints; i++)
			{
				int next = (i + 1) % nbPoints;
				gfx.line(points[i].x + SCREEN_WIDTH  / 2,
				         points[i].y + SCREEN_HEIGHT / 2,
				         points[next].x + SCREEN_WIDTH  / 2,
				         points[next].y + SCREEN_HEIGHT / 2,
				         color);
			}
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Et voici le résultat:

Translation

La translation est la transformation la plus simple.
On a simplement besoin d'ajouter le vecteur de translation à chaque point.


On n'a pas besoin de créer une fonction spéciale dans Vec2f, on va utiliser l'opérateur d'addition pour ajouter le
vecteur à chaque point de la forme
		void translateShape(Vec2f* points, int nbPoints, Vec2f transVector)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i] += transVector;
		}
				
Et dans notre boucle principale, on va faire une copie de notre forme, appliquer la transformation, puis la
dessiner:
		while (sys.isQuitRequested() == false)
		{
			gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

			drawShape(shape, 3, Color(0, 255, 255));

			// copy shape
			Vec2f shape2[3];

			for (int i = 0; i < 3; i++)
				shape2[i] = shape[i];

			// translate and draw
			translateShape(shape2, 3, Vec2f(150, 75));
			drawShape(shape2, 3, Color(255, 0, 0));

			gfx.render();
			sys.wait(20);
			sys.processEvents();
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				


Homothétie

Pour étirer notre forme on doit multiplier ses coordonnées par un float.


La classe Vec2f nous permet de multiplier un vecteur par un simple float. Mais si on veut avoir différentes tailles
suivant x et y on a besoin d'écrire une nouvelle fonction:
		void Vec2f::scale(const Vec2f& s)
		{
			x *= s.x;
			y *= s.y;
		}
				
Maintenant, si on modifie notre boucle principale pour utiliser cette fonction sur la copie de notre forme
		void scaleShape(Vec2f* points, int nbPoints, Vec2f scaleVector)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i].scale(scaleVector);
		}

		[...]

		while (sys.isQuitRequested() == false)
		{
			gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

			drawShape(shape, 3, Color(0, 255, 255));

			// copy shape
			Vec2f shape2[3];

			for (int i = 0; i < 3; i++)
				shape2[i] = shape[i];

			// scale and draw
			scaleShape(shape2, 3, Vec2f(6, 3));
			drawShape(shape2, 3, Color(255, 0, 0));

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
On obtient ça:


On peut aussi utiliser des valeurs négatives pour avoir une symétrie horizontale ou verticale
		// scale and draw
		scaleShape(shape2, 3, Vec2f(-6, 3));
		drawShape(shape2, 3, Color(255, 0, 0));

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				


Ou des valeurs négatives sur les 2 axes pour avoir une symétrie centrale
		// scale and draw
		scaleShape(shape2, 3, Vec2f(-6, -3));
		drawShape(shape2, 3, Color(255, 0, 0));

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				


Rotation

La rotation est un peu plus compliquée, mais pas tellement.
Dans l'atircle à propos du cercle, on a déjà vu comment calculer l'image d'un point qui est sur l'axe horizontal.


Mais comment transforme-t-on un point qui peut être n'importe où dans le plan ?
On va décomposer ses coordonnées en une partie horizontale et une partie verticale:


On sait déja comment faire tourner la partie horizontale mais quelle est l'image de la verticale ?


Visuellement, en observant la plus grande et la plus petite coordonnée, et en mettant ça en relation avec l'image
de la partie horizontale, on peut deviner les coordonnées de ce point:


Donc les équation pour faire tourner notre point sont:
		newX = x * cos(angle) - y * sin(angle);
		newY = x * sin(angle) + y * cos(angle);
				
On peut ajouter ça comme fonction dans notre classe Vec2f:
		void Vec2f::rotate(const float angle)
		{
			float co = cos(angle);
			float si = sin(angle);

			float newX = x * co - y * si;
			float newY = x * si + y * co;

			x = newX;
			y = newY;
		}
				
Maintenant testons ça avec notre forme:
		void rotateShape(Vec2f* points, int nbPoints, float angle)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i].rotate(angle);
		}

		[...]

		while (sys.isQuitRequested() == false)
		{
			gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

			drawShape(shape, 3, Color(0, 255, 255));

			// copy shape
			Vec2f shape2[3];

			for (int i = 0; i < 3; i++)
				shape2[i] = shape[i];

			// rotate and draw
			rotateShape(shape2, 3, DEG_TO_RAD(30));
			drawShape(shape2, 3, Color(255, 0, 0));

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Et voici ce qu'on voit:


Remarquez que le triangle tourne dans le sens des aiguilles d'une montre, parce qu'à l'écran la coordonnée y
augmente vers le bas, alors que les mathématiciens la font habituellement augmenter vers le haut.

Combiner les transformations

Quand vous combinez 2 transformations, l'ordre dans lequel vous les appliquez est important.
Par exemple combinons une translation et une homothétie.
Dans un cas on va faire l'homothétie d'abord puis la translation et dans l'autre cas, on fera le contraire.
		while (sys.isQuitRequested() == false)
		{
			gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

			drawShape(shape, 3, Color(0, 255, 255));

			// copy shape
			Vec2f shape2[3];

			for (int i = 0; i < 3; i++)
				shape2[i] = shape[i];

			// transform and draw
			scaleShape(shape2, 3, Vec2f(-2, 1));
			translateShape(shape2, 3, Vec2f(100, 50));
			drawShape(shape2, 3, Color(0, 255, 0));

			// copy shape
			Vec2f shape3[3];

			for (int i = 0; i < 3; i++)
				shape3[i] = shape[i];

			// transform and draw
			translateShape(shape3, 3, Vec2f(100, 50));
			scaleShape(shape3, 3, Vec2f(-2, 1));
			drawShape(shape3, 3, Color(255, 0, 0));

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Et voila ce qu'on obtient:


Pour le triangle vert, on l'a d'abord redimensionné...


...puis on l'a translaté:


Le rouge a d'abord été translaté...


...puis redimensionné.


Si on combine une rotation et une translation on obtient différents résultats aussi.
		float angle = 0;

		// wait until we quit
		while (sys.isQuitRequested() == false)
		{
			gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

			drawShape(shape, 3, Color(0, 255, 255));

			// copy shape
			Vec2f shape2[3];

			for (int i = 0; i < 3; i++)
				shape2[i] = shape[i];

			// transform and draw
			rotateShape(shape2, 3, DEG_TO_RAD(angle));
			translateShape(shape2, 3, Vec2f(150, 0));
			drawShape(shape2, 3, Color(0, 255, 0));

			// copy shape
			Vec2f shape3[3];

			for (int i = 0; i < 3; i++)
				shape3[i] = shape[i];

			// transform and draw
			translateShape(shape3, 3, Vec2f(150, 0));
			rotateShape(shape3, 3, DEG_TO_RAD(angle));
			drawShape(shape3, 3, Color(255, 0, 0));

			angle += 1;

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				


Dans ce programme animé, le triangle vert tourne sur lui-même pendant que le rouge tourne autour du centre de
l'écran.

Changer le centre de rotation

On peut combiner une rotation et des translations pour changer le centre autour duquel la forme tourne.
Une rotation est toujours appliquée autour de l'origine du plan. Alors dans le cas de notre triangle il se trouve à
l'intérieur de celui-ci comme on peut le voir sur la première image de cet article.

Maintenant supposons qu'on veuille que le triangle tourne autour de son point qui est à l'angle droit (c'est-à-dire
les coordonnées du premier point du tableau).
On peut faire ça en translatant d'abord le triangle pour que ce point se retrouve à l'origine.


Ensuite on fait tourner le triangle.


Et enfin on le re-translate en arrière.


Avec ce code vous verrez la différence entre les 2 centres de rotation.
		void drawShapeLeft(Vec2f* points, int nbPoints, Color color)
		{
			for (int i = 0; i < nbPoints; i++)
			{
				int next = (i + 1) % nbPoints;
				gfx.line(points[i].x + SCREEN_WIDTH  / 3,
				         points[i].y + SCREEN_HEIGHT / 2,
				         points[next].x + SCREEN_WIDTH  / 3,
				         points[next].y + SCREEN_HEIGHT / 2,
				         color);
			}
		}

		void drawShapeRight(Vec2f* points, int nbPoints, Color color)
		{
			for (int i = 0; i < nbPoints; i++)
			{
				int next = (i + 1) % nbPoints;
				gfx.line(points[i].x + SCREEN_WIDTH  * 2 / 3,
				         points[i].y + SCREEN_HEIGHT / 2,
				         points[next].x + SCREEN_WIDTH  * 2 / 3,
				         points[next].y + SCREEN_HEIGHT / 2,
				         color);
			}
		}

		void translateShape(Vec2f* points, int nbPoints, Vec2f transVector)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i] += transVector;
		}

		void scaleShape(Vec2f* points, int nbPoints, Vec2f scaleVector)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i].scale(scaleVector);
		}

		void rotateShape(Vec2f* points, int nbPoints, float angle)
		{
			for (int i = 0; i < nbPoints; i++)
				points[i].rotate(angle);
		}

		int main(int argc, char* argv[])
		{
			// init the window
			gfx.init("2D Transforms 9", SCREEN_WIDTH, SCREEN_HEIGHT);
			gfx.init2D();

			// define the shape
			Vec2f shape[] =
			{
				{-20, 20}, {-20, -20}, {30, 20}
			};

			float angle = 0;

			// wait until we quit
			while (sys.isQuitRequested() == false)
			{
				gfx.clearScreen(Color(0, 0, 0, SDL_ALPHA_OPAQUE));

				drawShapeLeft(shape, 3, Color(0, 255, 255));
				drawShapeRight(shape, 3, Color(0, 255, 255));

				// copy shape
				Vec2f shape2[3];

				for (int i = 0; i < 3; i++)
					shape2[i] = shape[i];

				// transform and draw
				rotateShape(shape2, 3, DEG_TO_RAD(angle));
				drawShapeLeft(shape2, 3, Color(0, 255, 0));

				// copy shape
				Vec2f shape3[3];

				for (int i = 0; i < 3; i++)
					shape3[i] = shape[i];

				// transform and draw
				translateShape(shape3, 3, -shape[0]);
				rotateShape(shape3, 3, DEG_TO_RAD(angle));
				translateShape(shape3, 3, shape[0]);
				drawShapeRight(shape3, 3, Color(255, 0, 0));

				angle += 1;

				gfx.render();
				sys.wait(20);
				sys.processEvents();
			}

			gfx.quit();

			return EXIT_SUCCESS;
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				


Liens

Vidéo des programmes de cet article