🤖 Cinématique robotique · sur automate (PLC)

Chapitre 02
Transformations rigides & orientation

Objectifs du chapitre

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 :

iTj = [ R   t ;   0T   1 ] = [ r11 r12 r13 tx ; r21 r22 r23 ty ; r31 r32 r33 tz ; 0   0   0   1 ]

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 :

p'̂ = iTj · p̂   ⇔    p'3×1 = R · p3×1 + t

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 :

0T2 = 0T1 · 1T2    et, en généralisant,    0Tn = 0T1 · 1T2 · … · n-1Tn

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 :

T⁻¹ = [ RT   −RTt ;   0T   1 ]

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 θ :

Rz(θ) = [ c   −s   0 ; s   c   0 ; 0   0   1 ]
Ry(θ) = [ c   0   s ; 0   1   0 ; −s   0   c ]
Rx(θ) = [ 1   0   0 ; 0   c   −s ; 0   s   c ]

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 :

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ésentationNb de nombresAvantagesInconvé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 :

R(φ, θ, ψ) = Rz(ψ) · Ry(θ) · Rx(φ)

En développant le produit (avec cφ = cos φ, sφ = sin φ, etc.), on obtient explicitement :

R = [ cψcθ   cψsθsφ−sψcφ   cψsθcφ+sψsφ ; sψcθ   sψsθsφ+cψcφ   sψsθcφ−cψsφ ; −sθ   cθsφ   cθcφ ]

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 :

ψ = atan2(r21, r11)   (yaw)
θ = atan2( −r31, √(r11² + r21²) )   (pitch)
φ = atan2(r32, r33)   (roll)

(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

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