Chapitre 02
Transformations rigides & orientation
Objectifs du chapitre
- Maîtriser la matrice de transformation homogène 4×4 et la transformation d'un point.
- Composer des repères par produit matriciel et comprendre la non-commutativité.
- Calculer l'inverse d'une transformation rigide sans inversion générale coûteuse.
- Écrire les rotations élémentaires Rx, Ry, Rz et distinguer pré- vs post-multiplication.
- Comparer les représentations de l'orientation (matrice, RPY/Euler, axe-angle, quaternion) et leurs pièges.
- Implémenter en Structured Text les conversions RPY ↔ matrice et la ré-orthonormalisation.
1. La matrice de transformation homogène 4×4
Une transformation rigide combine une rotation et une translation entre deux repères. En coordonnées homogènes, un point p = (x, y, z) devient (x, y, z, 1)T, et la transformation prend la forme compacte d'une matrice 4×4 :
Le bloc R ∈ SO(3) (rotation 3×3, orthogonale, RTR = I, det R = +1) porte l'orientation ; le vecteur t ∈ ℝ³ porte la position. La dernière ligne (0 0 0 1) est constante : elle rend le produit associatif et l'inverse propre. Transformer un point s'écrit alors comme un simple produit matrice-vecteur :
La partie rotation tourne le vecteur ; la partie translation le décale. Le 1 homogène « active » la translation — un vecteur direction (dernière coordonnée 0) subit la rotation mais pas la translation, ce qui est exactement le comportement voulu pour un axe ou une vitesse.
1.1 Composition en chaîne
L'intérêt majeur des coordonnées homogènes : la composition de repères se fait par produit matriciel. Si 0T1 place le repère 1 dans le repère 0 et 1T2 le repère 2 dans le repère 1, alors :
Les indices s'« annihilent » deux à deux (…1·1…) : c'est un excellent garde-fou pour vérifier l'ordre d'une chaîne. Cette dernière équation est la cinématique directe (chapitres 4-5), écrite de la base vers l'effecteur.
1.2 Non-commutativité
Le produit matriciel n'est pas commutatif : A · B ≠ B · A en général. Géométriquement, « tourner puis translater » ne donne pas la même pose que « translater puis tourner ». C'est la source de bugs la plus fréquente en manipulation de repères — le code compile, les dimensions collent, mais le robot va au mauvais endroit.
L'ordre est porteur de sens. Une transformation, c'est d'abord une rotation
puis une translation exprimée dans ce repère : inverser l'ordre change le résultat.
La chaîne s'écrit toujours de gauche à droite, base → effecteur,
0Tn = 0T1 · … · n-1Tn.
Dans Mat4Mul(A, B, C) (chapitre 1), A est le facteur de gauche :
écrire Mat4Mul(B, A, C) compose dans le mauvais sens et donne une pose fausse.
2. Inverse d'une transformation rigide
On a souvent besoin de renverser une transformation : si iTj place j dans i, alors jTi = (iTj)⁻¹ place i dans j. Inverser une matrice 4×4 quelconque coûte cher (élimination de Gauss, ≈ 60-80 opérations, sans parler des risques numériques). Mais une transformation rigide a une structure particulière : sa partie rotation est orthogonale, donc son inverse est sa transposée (R⁻¹ = RT). On en déduit la formule fermée :
La vérification est immédiate : T⁻¹ · T a pour bloc rotation RTR = I et pour translation RTt − RTt = 0. On ne calcule donc aucune inversion : juste une transposition (recopie d'indices) et un produit matrice-vecteur de 3 lignes.
Ne jamais inverser une transformation rigide « à la main » par Gauss. La formule T⁻¹ = [RT −RTt] est exacte, ≈ 10 fois moins coûteuse, et surtout numériquement stable : une transposition ne peut pas diverger ni introduire de division par un pivot minuscule. Sur automate, où chaque microseconde du cycle compte et où l'on veut du déterminisme, c'est la seule bonne façon de faire.
// Inverse d'une transformation RIGIDE : Tinv := T^-1 = [ R^T | -R^T t ].
// Suppose que T a bien la structure [R t ; 0 0 0 1] (R orthogonale).
// Tinv doit être DISTINCTE de T.
FUNCTION Mat4InvRigid : BOOL
VAR_IN_OUT
T : T_MAT4; // entrée : transformation rigide
Tinv : T_MAT4; // sortie : son inverse
END_VAR
VAR
i, j : INT;
t : ARRAY[0..2] OF LREAL; // translation d'origine (colonne 3)
END_VAR
// 1) Transposer le bloc rotation 3x3 : Rinv[i,j] := R[j,i].
FOR i := 0 TO 2 DO
FOR j := 0 TO 2 DO
Tinv[i, j] := T[j, i];
END_FOR
END_FOR
// 2) Nouvelle translation : t' := -R^T * t.
// On lit d'abord t (colonne 3) puis on projette sur les lignes de R^T.
FOR i := 0 TO 2 DO
t[i] := T[i, 3];
END_FOR
FOR i := 0 TO 2 DO
Tinv[i, 3] := -( Tinv[i, 0] * t[0]
+ Tinv[i, 1] * t[1]
+ Tinv[i, 2] * t[2] );
END_FOR
// 3) Dernière ligne homogène.
Tinv[3, 0] := 0.0; Tinv[3, 1] := 0.0; Tinv[3, 2] := 0.0; Tinv[3, 3] := 1.0;
Mat4InvRigid := TRUE;
END_FUNCTION
3. Rotations élémentaires
Les rotations autour des trois axes principaux sont les briques de toute orientation. Avec la convention main droite (angle θ positif = sens trigonométrique vu depuis l'extrémité positive de l'axe), en notant c = cos θ, s = sin θ :
Notez le signe inversé du sin dans Ry : le −s est en bas à gauche et le +s en haut à droite, l'exact opposé de Rx et Rz. Ce n'est pas une coquille : c'est la conséquence de l'orientation cyclique des axes (x → y → z → x). Ry tourne dans le plan (z, x) — l'ordre est « à rebours » du couple (x, y) —, d'où le renversement. C'est l'erreur la plus classique quand on écrit ces matrices de mémoire.
4. Composition d'orientations : pré- vs post-multiplication
Quand on enchaîne deux rotations, le résultat dépend du repère par rapport auquel chaque rotation est exprimée. Deux conventions coexistent :
- Post-multiplication (R · R') : la seconde rotation R' est exprimée dans le repère courant (déjà tourné). On multiplie à droite. C'est la logique d'un enchaînement de liaisons : chaque axe tourne dans le repère du segment précédent.
- Pré-multiplication (R' · R) : la seconde rotation est exprimée dans le repère fixe (le monde). On multiplie à gauche.
Autrement dit : une rotation exprimée dans le repère courant se post-multiplie, une rotation exprimée dans le repère fixe se pré-multiplie. Les deux lectures d'une même séquence sont valides mais donnent des matrices différentes — sauf cas particulier commutatif (rotations autour du même axe).
« Repère courant » ou « repère fixe » ? Décidez, puis tenez-vous-y. La séquence RPY R = Rz(ψ)·Ry(θ)·Rx(φ) se lit de deux façons équivalentes : soit comme trois rotations autour des axes courants appliquées dans l'ordre z, y, x, soit comme trois rotations autour des axes fixes appliquées dans l'ordre inverse x, y, z. Mélanger les deux interprétations dans le même calcul produit une orientation fausse, difficile à débusquer car « proche » de la bonne. Écrivez la convention en commentaire au-dessus de chaque produit.
5. Représentations de l'orientation
Une orientation de SO(3) a 3 degrés de liberté, mais on peut la coder de plusieurs façons, chacune avec ses compromis. Aucune n'est parfaite — le choix dépend de l'usage (calcul interne, interface humaine, interpolation).
| Représentation | Nb de nombres | Avantages | Inconvénients |
|---|---|---|---|
| Matrice de rotation | 9 (6 contraintes) | robuste, pas de singularité, composition = produit direct | redondante ; dérive numérique au fil des produits → ré-orthonormaliser |
| Angles RPY / Euler ZYX | 3 | compacts, intuitifs à lire (roll/pitch/yaw) | blocage de cardan (gimbal lock) à θ = ±90° ; convention ambiguë |
| Axe-angle | 4 (axe unitaire + angle), ou 3 (vecteur rotation) | géométrique, minimal (vecteur rotation) ; lien direct avec la vitesse angulaire | singularité à θ = 0 (axe indéfini) ; composition non triviale |
| Quaternion unitaire | 4 (1 contrainte : norme = 1) | pas de singularité, léger, interpolation propre (slerp), composition rapide | moins intuitif à lire ; signe double (q et −q = même rotation) |
Matrice en interne, RPY/quaternion aux interfaces. Sur automate, la cinématique enchaîne des produits de matrices : garder l'orientation sous forme de matrice évite des conversions à chaque étape et compose directement avec les transformations homogènes. Aux frontières — affichage IHM, consigne opérateur, échange avec un robot du commerce — on convertit vers du RPY (lisible par un humain) ou du quaternion (échange numérique sûr, sans blocage de cardan). En clair : on calcule en matrices, on communique en RPY ou quaternions.
6. Conversions RPY (Euler ZYX)
La convention RPY (roll-pitch-yaw) décrit l'orientation par trois angles : roll φ autour de x, pitch θ autour de y, yaw ψ autour de z. La matrice correspondante, en repère fixe (donc appliquée dans l'ordre x, y, z et écrite en pré-multipliant), est :
En développant le produit (avec cφ = cos φ, sφ = sin φ, etc.), on obtient explicitement :
Pour l'extraction inverse (retrouver les angles depuis une matrice R connue), on utilise atan2, qui gère les quatre quadrants et le signe correctement — jamais asin seul, qui perd de l'information. En lisant les bons coefficients :
(indices à partir de 1 dans les formules ; en ST, où les tableaux commencent à 0,
r21 devient R[1,0], etc.)
Blocage de cardan (gimbal lock) à θ = ±90°. Quand le pitch atteint ±90°, on a cθ = 0 : r11 et r21 s'annulent tous les deux, et atan2(0, 0) est indéfini. Roll et yaw ne sont plus séparables (deux axes s'alignent, un degré de liberté est perdu dans la paramétrisation). Il faut détecter ce cas (|r31| ≈ 1) et choisir une convention : par exemple fixer ψ = 0 et reporter toute la rotation restante sur φ. C'est précisément la singularité que le quaternion évite.
7. Implémentation en Structured Text
On réutilise les types et briques du chapitre 1 (T_MAT3, T_MAT4,
Mat4Identity, Mat4Mul). Les rotations élémentaires renvoient une
T_MAT4 (bloc rotation + identité ailleurs) pour se composer directement avec les
transformations homogènes. On passe le résultat en VAR_IN_OUT pour éviter toute copie.
Un seul SIN et un seul COS par matrice. Calculez
c := COS(theta) et s := SIN(theta) une fois dans une variable locale,
puis réutilisez-les : c'est plus rapide (les fonctions trigonométriques sont coûteuses) et
garantit que le même angle produit exactement les mêmes valeurs partout dans la matrice. Si
votre runtime fournit un SINCOS combiné, préférez-le. Et toujours en
LREAL, angles en radians.
// ---- Rotations élémentaires en 4x4 (bloc rotation + identité homogène) ----
// Rz(theta) : rotation autour de z. c en (0,0)(1,1), -s en (0,1), +s en (1,0).
FUNCTION RotZ : BOOL
VAR_IN_OUT
M : T_MAT4;
END_VAR
VAR_INPUT
theta : LREAL; // rad
END_VAR
VAR
c, s : LREAL;
END_VAR
c := COS(theta);
s := SIN(theta);
Mat4Identity(M := M); // met tout à I, on écrase le bloc 3x3 utile
M[0,0] := c; M[0,1] := -s;
M[1,0] := s; M[1,1] := c;
RotZ := TRUE;
END_FUNCTION
// Ry(theta) : ATTENTION au signe inversé du sin (-s en (2,0), +s en (0,2)).
FUNCTION RotY : BOOL
VAR_IN_OUT
M : T_MAT4;
END_VAR
VAR_INPUT
theta : LREAL;
END_VAR
VAR
c, s : LREAL;
END_VAR
c := COS(theta);
s := SIN(theta);
Mat4Identity(M := M);
M[0,0] := c; M[0,2] := s;
M[2,0] := -s; M[2,2] := c;
RotY := TRUE;
END_FUNCTION
// Rx(theta) : rotation autour de x. -s en (1,2), +s en (2,1).
FUNCTION RotX : BOOL
VAR_IN_OUT
M : T_MAT4;
END_VAR
VAR_INPUT
theta : LREAL;
END_VAR
VAR
c, s : LREAL;
END_VAR
c := COS(theta);
s := SIN(theta);
Mat4Identity(M := M);
M[1,1] := c; M[1,2] := -s;
M[2,1] := s; M[2,2] := c;
RotX := TRUE;
END_FUNCTION
La construction d'une matrice depuis les angles RPY se fait par la composition
Rz(ψ)·Ry(θ)·Rx(φ). On enchaîne
deux produits via Mat4Mul, en veillant à des matrices temporaires distinctes :
// R := Rz(psi) * Ry(theta) * Rx(phi) (RPY, repère fixe / Euler ZYX)
// Écrit une transformation homogène 4x4 (translation nulle) dans M.
FUNCTION RpyToMat : BOOL
VAR_IN_OUT
M : T_MAT4; // sortie
END_VAR
VAR_INPUT
phi : LREAL; // roll (autour de x), rad
theta : LREAL; // pitch (autour de y), rad
psi : LREAL; // yaw (autour de z), rad
END_VAR
VAR
Rz, Ry, Rx, tmp : T_MAT4;
END_VAR
RotZ(M := Rz, theta := psi);
RotY(M := Ry, theta := theta);
RotX(M := Rx, theta := phi);
Mat4Mul(A := Ry, B := Rx, C := tmp); // tmp := Ry * Rx
Mat4Mul(A := Rz, B := tmp, C := M); // M := Rz * (Ry * Rx)
RpyToMat := TRUE;
END_FUNCTION
L'extraction inverse applique les trois atan2, avec détection du
cas dégénéré θ = ±90°. La fonction ATAN2(y, x) et
SQRT sont standard IEC 61131-3.
// Extrait les angles RPY (phi, theta, psi) d'une matrice de rotation.
// Convention Euler ZYX. Gère le blocage de cardan à theta = +/- 90°.
FUNCTION MatToRpy : BOOL
VAR_IN_OUT
M : T_MAT4; // entrée : rotation (bloc 3x3 utilisé)
END_VAR
VAR_OUTPUT
phi : LREAL; // roll
theta : LREAL; // pitch
psi : LREAL; // yaw
END_VAR
VAR
cy : LREAL; // sqrt(r11^2 + r21^2) = |cos(theta)|
END_VAR
// r11 = M[0,0], r21 = M[1,0], r31 = M[2,0], r32 = M[2,1], r33 = M[2,2].
cy := SQRT(M[0,0]*M[0,0] + M[1,0]*M[1,0]);
IF cy > 1E-9 THEN
// Cas régulier.
psi := ATAN2(M[1,0], M[0,0]); // atan2(r21, r11)
theta := ATAN2(-M[2,0], cy); // atan2(-r31, cy)
phi := ATAN2(M[2,1], M[2,2]); // atan2(r32, r33)
ELSE
// Blocage de cardan : cos(theta) ≈ 0, theta = +/- 90°.
// Roll et yaw non séparables : on fixe psi = 0 par convention.
psi := 0.0;
theta := ATAN2(-M[2,0], cy); // +/- PI/2 selon le signe de r31
phi := ATAN2(-M[1,2], M[1,1]); // report de la rotation sur phi
END_IF
MatToRpy := TRUE;
END_FUNCTION
8. Pièges à connaître
- Ordre des angles d'Euler. ZYX (RPY) est répandu, mais chaque constructeur a sa convention : ZYZ, XYZ, angles fixes ou courants, degrés ou radians… Un jeu d'angles n'a de sens qu'avec sa convention. Vérifiez la documentation du robot cible avant tout échange, sous peine d'orientation fausse mais « plausible ».
- Dérive de l'orthonormalité de R. À force de multiplier des matrices en flottant, RTR s'éloigne lentement de I : les colonnes cessent d'être exactement unitaires et orthogonales. Sur une longue chaîne ou une intégration répétée, il faut ré-orthonormaliser périodiquement (voir exercice 4).
-
Radians, toujours.
SIN,COS,ATAN2travaillent en radians. Convertissez aux frontières (rad := deg * PI / 180.0) et raisonnez en SI au cœur du calcul, comme fixé au chapitre 1.
Exercices
Exercice 1 — RotZ en 4×4 et vérification à π
En réutilisant RotZ, construisez Rz(π)
et appliquez-la au point (1, 0, 0). On doit obtenir
(−1, 0, 0) (aux erreurs d'arrondi près). Vérifiez-le en ST.
Voir la solution
PROGRAM P_TestRotZPi
VAR
R : T_MAT4;
px, py, pz : LREAL;
qx, qy, qz : LREAL;
ok : BOOL := TRUE;
PI : LREAL := 3.14159265358979;
END_VAR
// Point de départ (1, 0, 0).
px := 1.0; py := 0.0; pz := 0.0;
RotZ(M := R, theta := PI);
// q := R * p (partie 3x3 ; translation nulle ici).
qx := R[0,0]*px + R[0,1]*py + R[0,2]*pz;
qy := R[1,0]*px + R[1,1]*py + R[1,2]*pz;
qz := R[2,0]*px + R[2,1]*py + R[2,2]*pz;
IF ABS(qx + 1.0) > 1E-9 THEN ok := FALSE; END_IF // attendu -1
IF ABS(qy) > 1E-9 THEN ok := FALSE; END_IF // attendu 0
IF ABS(qz) > 1E-9 THEN ok := FALSE; END_IF // attendu 0
// ok = TRUE si Rz(PI) envoie (1,0,0) sur (-1,0,0)
À θ = π : c = −1,
s ≈ 0, donc Rz
vaut diag(−1, −1, 1) et retourne le point dans le plan
xy. On compare avec une tolérance car
SIN(π) ne vaut pas exactement 0 en flottant.
Exercice 2 — Composer deux rotations
Montrez numériquement que Rz(90°)·Rx(90°) et Rx(90°)·Rz(90°) diffèrent (non-commutativité). Construisez les deux produits et comparez-les coefficient par coefficient.
Voir la solution
PROGRAM P_TestNonCommute
VAR
Rz, Rx, AB, BA : T_MAT4;
i, j : INT;
identiques : BOOL := TRUE;
HALFPI : LREAL := 1.5707963267949; // PI/2
END_VAR
RotZ(M := Rz, theta := HALFPI);
RotX(M := Rx, theta := HALFPI);
Mat4Mul(A := Rz, B := Rx, C := AB); // AB := Rz * Rx
Mat4Mul(A := Rx, B := Rz, C := BA); // BA := Rx * Rz
FOR i := 0 TO 3 DO
FOR j := 0 TO 3 DO
IF ABS(AB[i,j] - BA[i,j]) > 1E-9 THEN
identiques := FALSE; // au moins un coefficient diffère
END_IF
END_FOR
END_FOR
// identiques = FALSE : les deux ordres donnent des rotations différentes
Le produit matriciel n'étant pas commutatif, identiques ressort
FALSE. Concrètement, tourner d'abord autour de z
puis de x ne place pas le repère au même endroit que l'ordre
inverse : c'est exactement le piège de la section 1.2, ici rendu tangible.
Exercice 3 — Extraire les RPY d'une matrice connue
Construisez R avec RpyToMat pour
φ = 0,1, θ = 0,2,
ψ = 0,3 rad, puis ré-extrayez les angles avec
MatToRpy. Vérifiez qu'on retombe sur les angles de départ (aller-retour).
Voir la solution
PROGRAM P_TestRpyAllerRetour
VAR
R : T_MAT4;
phi0, the0, psi0 : LREAL;
phi1, the1, psi1 : LREAL;
ok : BOOL := TRUE;
END_VAR
phi0 := 0.1; the0 := 0.2; psi0 := 0.3;
RpyToMat(M := R, phi := phi0, theta := the0, psi := psi0);
MatToRpy(M := R, phi => phi1, theta => the1, psi => psi1);
IF ABS(phi1 - phi0) > 1E-9 THEN ok := FALSE; END_IF
IF ABS(the1 - the0) > 1E-9 THEN ok := FALSE; END_IF
IF ABS(psi1 - psi0) > 1E-9 THEN ok := FALSE; END_IF
// ok = TRUE : la conversion RPY -> R -> RPY est l'identité (hors singularité)
L'aller-retour est exact tant qu'on reste loin du blocage de cardan
(|θ| ≠ 90°). Testez ensuite
θ = π/2 : MatToRpy bascule dans la branche
dégénérée, fixe ψ = 0 et reporte la rotation sur
φ — les angles rendus diffèrent alors des angles d'entrée,
bien que la matrice soit la même. C'est attendu : plusieurs triplets RPY décrivent
la même orientation au point singulier.
Exercice 4 — Ré-orthonormaliser une matrice de rotation dérivée
Après de nombreux produits, R n'est plus tout à fait
orthonormale. Écrivez une fonction ReOrthoNorm qui la corrige par
Gram-Schmidt sur les colonnes : normaliser la 1re colonne, rendre
la 2e orthogonale à la 1re puis la normaliser, et reconstruire la 3e
par produit vectoriel des deux premières.
Voir la solution
// Ré-orthonormalise le bloc rotation 3x3 de M par Gram-Schmidt sur colonnes.
// Corrige la dérive numérique accumulée au fil des produits de matrices.
FUNCTION ReOrthoNorm : BOOL
VAR_IN_OUT
M : T_MAT4;
END_VAR
VAR
cx, cy, cz : T_VEC3; // colonnes 0, 1, 2 de R
dot, n : LREAL;
i : INT;
END_VAR
// Lecture des colonnes.
FOR i := 0 TO 2 DO
cx[i] := M[i,0]; cy[i] := M[i,1]; cz[i] := M[i,2];
END_FOR
// 1) Normaliser cx.
n := SQRT(cx[0]*cx[0] + cx[1]*cx[1] + cx[2]*cx[2]);
FOR i := 0 TO 2 DO cx[i] := cx[i] / n; END_FOR
// 2) Rendre cy orthogonale à cx (cy := cy - (cy·cx) cx), puis normaliser.
dot := cy[0]*cx[0] + cy[1]*cx[1] + cy[2]*cx[2];
FOR i := 0 TO 2 DO cy[i] := cy[i] - dot * cx[i]; END_FOR
n := SQRT(cy[0]*cy[0] + cy[1]*cy[1] + cy[2]*cy[2]);
FOR i := 0 TO 2 DO cy[i] := cy[i] / n; END_FOR
// 3) cz := cx × cy (produit vectoriel -> repère direct garanti).
cz[0] := cx[1]*cy[2] - cx[2]*cy[1];
cz[1] := cx[2]*cy[0] - cx[0]*cy[2];
cz[2] := cx[0]*cy[1] - cx[1]*cy[0];
// Réécriture des colonnes dans M.
FOR i := 0 TO 2 DO
M[i,0] := cx[i]; M[i,1] := cy[i]; M[i,2] := cz[i];
END_FOR
ReOrthoNorm := TRUE;
END_FUNCTION
Reconstruire la 3e colonne par produit vectoriel
cx × cy garantit à la fois
l'orthogonalité et le caractère direct (det R = +1) —
on ne peut pas retomber sur une réflexion. À appeler périodiquement (par exemple tous les N
cycles) sur les matrices intégrées longtemps, jamais dans la boucle chaude sans nécessité :
les SQRT ont un coût.
Récapitulatif
- Une transformation rigide s'écrit iTj = [R t ; 0 1] ; transformer un point : p' = R·p + t ; composer : 0Tn = 0T1 · … · n-1Tn — non commutatif.
- Inverse d'une transformation rigide : T⁻¹ = [RT −RTt ; 0 1] — pas d'inversion générale, rapide et stable (
Mat4InvRigid). - Rotations élémentaires Rx, Ry, Rz : attention au signe inversé du sin dans Ry.
- Composition : post-multiplier pour une rotation dans le repère courant, pré-multiplier pour le repère fixe.
- Représentations : matrice (robuste, dérive → ré-orthonormaliser), RPY (intuitif, gimbal lock à θ=±90°), axe-angle, quaternion (sans singularité). Sur PLC : matrice en interne, RPY/quaternion aux interfaces.
- Conversions : R = Rz(ψ)·Ry(θ)·Rx(φ) ; extraction par atan2 (
RpyToMat/MatToRpy), avec traitement du cas dégénéré. - Pièges : convention d'Euler dépendante du constructeur, dérive de l'orthonormalité, radians partout.