Utilisation des transformations 3D

Introduction

Si vous n’avez jamais fait de jeux 3D auparavant, travailler avec des rotations en trois dimensions peut être déroutant au début. Venant de la 2D, la façon naturelle de penser est du genre « Oh, c’est comme tourner en 2D, sauf que maintenant les rotations se produisent en X, Y et Z « .

Au premier abord, cela semble facile et pour les jeux simples, cette façon de penser peut même suffire. Malheureusement, c’est souvent incorrect.

Les angles en trois dimensions sont le plus souvent appelés « angles d’Euler ».

../../_images/transforms_euler.png

Les angles d’Euler ont été introduits par le mathématicien Leonhard Euler au début des années 1700.

../../_images/transforms_euler_himself.png

Cette façon de représenter les rotations 3D était révolutionnaire à l’époque, mais elle a plusieurs défauts lorsqu’elle est utilisée dans le développement de jeux (ce qui est à attendre d’un gars avec un chapeau amusant). L’idée de ce document est d’expliquer pourquoi, ainsi que de présenter les meilleures pratiques pour gérer les transformations lors de la programmation de jeux 3D.

Problèmes des angles d’Euler

Bien qu’il puisse sembler intuitif que chaque axe ait une rotation, la vérité est que ce n’est tout simplement pas pratique.

Ordre des axes

La raison principale est qu’il n’y a pas de façon unique de construire une orientation à partir des angles. Il n’existe pas de fonction mathématique standard qui prend tous les angles ensemble et produit une rotation 3D. La seule façon de produire une orientation à partir des angles est de faire tourner l’objet angle par angle, dans un ordre arbitraire.

Ceci pourrait être fait en tournant d’abord dans X, puis dans Y et ensuite dans Z. Alternativement, vous pouvez d’abord tourner en Y, puis en Z et enfin en X. Tout fonctionne, mais selon l’ordre, l’orientation finale de l’objet ne sera pas nécessairement la même. En effet, cela signifie qu’il y a plusieurs façons de construire une orientation à partir de 3 angles différents, selon l’ordre des rotations.

Voici une visualisation des axes de rotation (dans l’ordre X,Y,Z) dans un cardan (de Wikipedia). Comme vous pouvez le voir, l’orientation de chaque axe dépend de la rotation du précédent :

../../_images/transforms_gimbal.gif

Vous vous demandez peut-être comment cela vous affecte. Prenons un exemple pratique :

Imaginez que vous travaillez sur un contrôleur à la première personne (par exemple, un jeu FPS). Déplacer la souris vers la gauche et vers la droite contrôle votre angle de vue parallèle au sol, tout en le déplaçant vers le haut et vers le bas déplace la vue du joueur vers le haut et vers le bas.

Dans ce cas, pour obtenir l’effet désiré, la rotation doit d’abord être appliquée sur l’axe Y (dans ce cas, Godot utilise une orientation « Y-Up »), puis sur l’axe X.

../../_images/transforms_rotate1.gif

Si nous devions appliquer la rotation dans l’axe X d’abord, puis dans Y, l’effet serait undésirable :

../../_images/transforms_rotate2.gif

Selon le type de jeu ou d’effet désiré, l’ordre dans lequel vous voulez que les rotations des axes soient appliquées peut varier. Par conséquent, il ne suffit pas d’appliquer des rotations en X, Y et Z : il vous faut également un ordre de rotation.

Interpolation

Un autre problème avec l’utilisation des angles d’Euler est l’interpolation. Imaginez que vous voulez faire la transition entre deux caméras différentes ou entre deux positions ennemies (y compris les rotations). Une façon logique d’y parvenir est d’interpoler les angles d’une position à l’autre. On pourrait s’attendre à ce que ça ressemble à ça :

../../_images/transforms_interpolate1.gif

Mais cela n’a pas toujours l’effet escompté lorsque l’on utilise les angles :

../../_images/transforms_interpolate2.gif

La caméra a en fait tourné dans la direction opposée !

Il y a plusieurs raisons à cela :

  • Les rotations ne correspondent pas linéairement à l’orientation, donc les interpoler n’aboutit pas toujours au chemin le plus court (passer de 270` à 0` degrés n'est pas la même chose que passer de ``270 à 360, même si les angles sont équivalents).
  • Le blocage de cardan est en jeu (le premier et le dernier axe de rotation s’alignent, ce qui entraîne une perte de liberté). Voir la page Wikipedia sur le blocage de cardan pour une explication détaillée de ce problème.

Dîtes non aux angles d’Euler

Le résultat de tout ceci est que vous ne devriez pas utiliser la propriété rotation des nœuds :ref:`class_Spatial” de Godot pour les jeux. C’est là pour être utilisé principalement dans l’éditeur, pour la cohérence avec le moteur 2D, et pour des rotations simples (généralement un seul axe, ou même deux dans des cas limités). Même si vous êtes tenté, ne l’utilisez pas.

Il existe une meilleure façon de résoudre vos problèmes de rotation.

Introduction aux transformations

Godot utilise le type de données Transform pour les orientations. Chaque nœud class_Spatial” contient une propriété ``transform` qui est relative à la transformation du parent, si le parent est un type dérivé de spatial.

Il est également possible d’accéder à la transformation des coordonnées du monde via la propriété global_transform.

Une transformation a un Basis (transform.basis sub-property), qui se compose de trois vecteurs Vector3. Ceux-ci sont accessibles via la propriété transform.basis et peuvent être accédés directement par transform.basis.x, transform.basis.y, et transform.basis.z. Chaque vecteur pointe dans la direction dans laquelle son axe a été tourné, de sorte qu’ils décrivent efficacement la rotation totale du nœud. L’échelle (tant qu’elle est uniforme) peut également être déduite de la longueur des axes. Une base peut aussi être interprétée comme une matrice 3x3 et utilisée comme transform.basis[x][y].

Une base par défaut (non modifiée) s’apparente à :

var basis = Basis()
# Contains the following default values:
basis.x = Vector3(1, 0, 0) # Vector pointing along the X axis
basis.y = Vector3(0, 1, 0) # Vector pointing along the Y axis
basis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultBasis = new Basis();
GD.Print(defaultBasis); // prints: ((0, 0, 0), (0, 0, 0), (0, 0, 0))

// Instead we can use the Identity property.
var identityBasis = Basis.Identity;
GD.Print(identityBasis.x); // prints: (1, 0, 0)
GD.Print(identityBasis.y); // prints: (0, 1, 0)
GD.Print(identityBasis.z); // prints: (0, 0, 1)

// The Identity basis is equivalent to:
var basis = new Basis(Vector3.Right, Vector3.Up, Vector3.Back);
GD.Print(basis); // prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))

C’est aussi un analogue d’une matrice d’identité 3 x 3.

Selon la convention OpenGL, X est l’axe Droite, Y est l’axe Haut et Z est l’axe Avant.

Ensemble avec la base, une transformation a aussi une origine. Il s’agit d’un Vector3 spécifiant à quelle distance de l’origine réelle (0, 0, 0, 0)` cette transformation est. Combinant la base avec l”origine, une transformation représente efficacement une translation, une rotation et une mise à l’échelle dans l’espace.

../../_images/transforms_camera.png

Une façon de visualiser une transformation est de regarder le gadget 3D d’un objet en mode « local space ».

../../_images/transforms_local_space.png

Les flèches du gadget indiquent les axes X, Y et Z (en rouge, vert et bleu respectivement) de la base, tandis que le centre du gadget est à l’origine de l’objet.

../../_images/transforms_gizmo.png

Pour plus d’informations sur les mathématiques des vecteurs et des transformations, veuillez lire les tutoriels Vector math.

Manipuler les transformations

Bien sûr, les transformations ne sont pas aussi simple à manipuler comme angles et ont leurs propres problèmes.

Il est possible de faire tourner une transformation, soit en multipliant sa base par une autre (c’est ce qu’on appelle l’accumulation), soit en utilisant les méthodes de rotation.

# Rotate the transform about the X axis
transform.basis = Basis(Vector3(1, 0, 0), PI) * transform.basis
# shortened
transform.basis = transform.basis.rotated(Vector3(1, 0, 0), PI)
// rotate the transform about the X axis
transform.basis = new Basis(Vector3.Right, Mathf.Pi) * transform.basis;
// shortened
transform.basis = transform.basis.Rotated(Vector3.Right, Mathf.Pi);

Une méthode dans Spatial simplifie cela :

# Rotate the transform in X axis
rotate(Vector3(1, 0, 0), PI)
# shortened
rotate_x(PI)
// Rotate the transform about the X axis
Rotate(Vector3.Right, Mathf.Pi);
// shortened
RotateX(Mathf.Pi);

Ceci permet de faire pivoter le nœud par rapport au nœud parent.

Pour faire pivoter par rapport à l’espace de l’objet (la propre transformation du nœud), utilisez ce qui suit :

# Rotate locally
rotate_object_local(Vector3(1, 0, 0), PI)
// Rotate locally
RotateObjectLocal(Vector3.Right, Mathf.Pi);

Erreurs de précision

Effectuer des opérations successives sur les transformations entraînera une perte de précision due à une erreur à virgule flottante. Cela signifie que l’échelle de chaque axe peut ne plus être exactement « 1,0 » et qu’ils peuvent ne plus être exactement à « 90 » degrés les uns des autres.

Si une transformation est tournée à chaque image, elle finira par se déformer avec le temps. C’est inévitable.

Il y a deux façons différentes de gérer cela. La première est de orthonormaliser la transformation après un certain temps (peut-être une fois par image si vous la modifiez à chaque image) :

transform = transform.orthonormalized()
transform = transform.Orthonormalized();

Tous les axes auront à nouveau une longueur de 1,0 et seront à 90 degrés les uns des autres. Cependant, toute mise à l’échelle appliquée à la transformation sera perdue.

Il est recommandé de ne pas mettre à l’échelle les nœuds qui vont être manipulés ; mettez plutôt à l’échelle leurs nœuds enfants (tels que MeshInstance). Si vous devez absolument mettre à l’échelle le nœud, réappliquez-la à la fin :

transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);

Obtenir l’information

Vous pensez peut-être qu’à ce stade : « D’accord, mais comment obtenir des angles à partir d’une transformation ? » Encore une fois, la réponse est : vous ne le faites pas. Vous devez faire de votre mieux pour arrêter de penser en angles.

Imaginez que vous avez besoin de tirer une balle dans la direction faisant face à votre joueur . Utilisez simplement l’axe avant (communément Z ou -Z).

bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED
bullet.Transform = transform;
bullet.LinearVelocity = transform.basis.z * BulletSpeed;

L’ennemi regarde-t-il le joueur ? Utilisez le produit scalaire pour cela (voir le tutoriel Vector math pour une explication du produit scalaire) :

# Get the direction vector from player to enemy
var direction = enemy.transform.origin - player.transform.origin
if direction.dot(enemy.transform.basis.z) > 0:
    enemy.im_watching_you(player)
// Get the direction vector from player to enemy
Vector3 direction = enemy.Transform.origin - player.Transform.origin;
if (direction.Dot(enemy.Transform.basis.z) > 0)
{
    enemy.ImWatchingYou(player);
}

Déplacement latéral à gauche :

# Remember that +X is right
if Input.is_action_pressed("strafe_left"):
    translate_object_local(-transform.basis.x)
// Remember that +X is right
if (Input.IsActionPressed("strafe_left"))
{
    TranslateObjectLocal(-Transform.basis.x);
}

Sauter :

# Keep in mind Y is up-axis
if Input.is_action_just_pressed("jump"):
    velocity.y = JUMP_SPEED

velocity = move_and_slide(velocity)
// Keep in mind Y is up-axis
if (Input.IsActionJustPressed("jump"))
    velocity.y = JumpSpeed;

velocity = MoveAndSlide(velocity);

Tous les comportements communs et logique peuvent être faits avec juste des vecteurs.

Informations de réglage

Il y a, bien sûr, des cas où vous voulez régler l’information sur une transformation. Imaginez une vue à la première personne ou une caméra orbitale. Celles-ci sont certainement faites en utilisant des angles, parce que vous voulez que les transformations se produisent dans un ordre spécifique.

Pour de tels cas, gardez les angles et les rotations à l’extérieur de la transformation et réglez-les à chaque image. N’essayez pas de les récupérer et de les réutiliser car la transformation n’est pas destinée à être utilisée de cette façon.

Exemple de regarder les alentours, en vue FPS :

# accumulators
var rot_x = 0
var rot_y = 0

func _input(event):
    if event is InputEventMouseMotion and event.button_mask & 1:
        # modify accumulated mouse rotation
        rot_x += event.relative.x * LOOKAROUND_SPEED
        rot_y += event.relative.y * LOOKAROUND_SPEED
        transform.basis = Basis() # reset rotation
        rotate_object_local(Vector3(0, 1, 0), rot_x) # first rotate in Y
        rotate_object_local(Vector3(1, 0, 0), rot_y) # then rotate in X
// accumulators
private float _rotationX = 0f;
private float _rotationY = 0f;

public override void _Input(InputEvent @event)
{
    if (mouseMotion is InputEventMouseMotion mouseMotion)
    {
        // modify accumulated mouse rotation
        _rotationX += mouseMotion.Relative.x * LookAroundSpeed;
        _rotationY += mouseMotion.Relative.y * LookAroundSpeed;

        // reset rotation
        Transform transform = Transform;
        transform.basis = Basis.Identity;
        Transform = transform;

        RotateObjectLocal(Vector3.Up, _rotationX); // first rotate about Y
        RotateObjectLocal(Vector3.Right, _rotationY); // then rotate about X
    }
}

Comme vous pouvez le voir, dans de tels cas, il est encore plus simple de garder la rotation à l’extérieur, puis d’utiliser la transformation comme orientation finale.

Interpoler avec des quaternions

L’interpolation entre deux transformations peut se faire efficacement avec des quaternions. Plus d’informations sur le fonctionnement des quaternions peuvent être trouvées à d’autres endroits sur Internet. Pour l’usage pratique, il suffit de comprendre que leur utilisation principale est de faire une interpolation de la trajectoire la plus proche possible. Si vous avez deux rotations, un quaternion permettra l’interpolation entre elles en utilisant l’axe le plus proche.

Convertir une rotation en quaternion est très simple.

# Convert basis to quaternion, keep in mind scale is lost
var a = Quat(transform.basis)
var b = Quat(transform2.basis)
# Interpolate using spherical-linear interpolation (SLERP).
var c = a.slerp(b,0.5) # find halfway point between a and b
# Apply back
transform.basis = Basis(c)
// Convert basis to quaternion, keep in mind scale is lost
var a = transform.basis.Quat();
var b = transform.basis.Quat();
// Interpolate using spherical-linear interpolation (SLERP).
var c = a.Slerp(b, 0.5f); // find halfway point between a and b
// Apply back
transform.basis = new Basis(c);

La référence de type: ref: class_Quat contient plus d’informations sur le type de données (elle peut également faire de l’accumulation de transformée, la transformation des points,, etc., bien que cela soit utilisé moins souvent). Si vous interpolez ou appliquez plusieurs fois des opérations sur des quaternions, n’oubliez pas qu’ils doivent éventuellement être normalisés, sinon ils risquent également de présenter des erreurs de précision numérique.

Les quaternions sont utiles pour les interpolations caméra/chemin/etc. car le résultat sera toujours correct et lisse.

Les transformations sont vos amies

Pour la plupart des débutants, s’habituer à travailler avec des transformations peut prendre un certain temps. Cependant, une fois que vous vous y serez habitué, vous apprécierez leur simplicité et leur puissance.

N’hésitez pas à demander de l’aide à ce sujet dans n’importe laquelle des  » communautés en ligne  » de Godot <https://godotengine.org/community>`_ et, une fois que vous aurez suffisamment confiance en vous, s’il vous plaît aider les autres !