Les mathématiques dans Gamedev sont simples. Matrices et transformations affines

Bonjour à tous! Je m'appelle Grisha et je suis le fondateur de CGDevs. Aujourd'hui, je veux continuer le sujet des mathématiques dans le jeu dev. Dans un article précédent , nous avons montré des exemples de base d'utilisation de vecteurs et d'intégrales dans des projets Unity, et parlons maintenant des matrices et des transformations affines. Si vous connaissez bien l'arithmétique matricielle; Vous savez ce qu'est TRS et comment travailler avec lui; qu'est-ce que la transformation des chefs de ménage - vous ne trouverez probablement rien de nouveau pour vous. Nous parlerons dans le contexte des graphismes 3D. Si vous êtes intéressé par ce sujet - bienvenue au chat.



Commençons par l'un des concepts les plus importants dans le contexte de l'article - les transformations affines . Les transformations affines sont, par essence, la transformation du système de coordonnées (ou espace) en multipliant le vecteur par une matrice spéciale. Par exemple, des transformations telles que le mouvement, la rotation, la mise à l'échelle, la réflexion, etc. Les principales propriétés des transformations affines sont que vous restez dans le même espace (il est impossible de faire en deux dimensions à partir d'un vecteur en trois dimensions) et que si les lignes se croisent / sont parallèles / ont été croisés avant la conversion, cette propriété sera conservée après la conversion. De plus, ils ont beaucoup de propriétés mathématiques qui nécessitent une connaissance de la théorie des groupes, des ensembles et de l'algèbre linéaire, ce qui facilite le travail avec eux.

Matrice TRS


Le deuxième concept important en infographie est la matrice TRS . En l'utilisant, vous pouvez décrire les opérations les plus courantes utilisées lors de l'utilisation d'images de synthèse. Une matrice TRS est une composition de trois matrices de transformation. Matrices de déplacement (translation), rotation sur chaque axe (rotation) et mise à l'échelle (échelle).
Elle ressemble à ça.



Où:
Le mouvement est t = nouveau Vector3 (d, h, l).
Mise à l'échelle - s = nouveau Vector3 (nouveau Vector3 (a, e, i) .magnitude, nouveau Vector3 (b, f, j) .magnitude, nouveau Vector3 (c, g, k) .magnitude);

Une rotation est une matrice de la forme:



Allons maintenant un peu plus loin dans le contexte Unity. Pour commencer, la matrice TRS est une chose très pratique, mais elle ne doit pas être utilisée partout. Étant donné qu'une simple indication de la position ou de l'ajout de vecteurs dans une unité fonctionnera plus rapidement, mais dans de nombreux algorithmes mathématiques, les matrices sont beaucoup plus pratiques que les vecteurs. La fonctionnalité TRS dans Unity est largement implémentée dans la classe Matrix4x4 , mais elle n'est pas pratique du point de vue de l'application. Puisque, en plus d'appliquer la matrice par multiplication, elle peut généralement stocker des informations sur l'orientation de l'objet, et pour certaines transformations, je voudrais pouvoir calculer non seulement la position, mais aussi changer l'orientation de l'objet dans son ensemble (par exemple, une réflexion qui n'est pas implémentée dans Unity)

Tous les exemples sont donnés ci-dessous pour le système de coordonnées local (l'origine du GameObject est considérée comme l'origine des coordonnées, à l'intérieur de laquelle se trouve l'objet. Si l'objet est la racine de la hiérarchie dans l'unité, alors l'origine est le monde (0,0,0)).

Étant donné que l'utilisation de la matrice TRS vous permet de décrire la position d'un objet dans l'espace, nous avons besoin d'une décomposition de TRS en valeurs spécifiques de position, de rotation et d'échelle pour Unity. Pour ce faire, vous pouvez écrire des méthodes d'extension pour la classe Matrix4x4

Obtenir la position, la rotation et l'échelle
public static Vector3 ExtractPosition(this Matrix4x4 matrix) { Vector3 position; position.x = matrix.m03; position.y = matrix.m13; position.z = matrix.m23; return position; } public static Quaternion ExtractRotation(this Matrix4x4 matrix) { Vector3 forward; forward.x = matrix.m02; forward.y = matrix.m12; forward.z = matrix.m22; Vector3 upwards; upwards.x = matrix.m01; upwards.y = matrix.m11; upwards.z = matrix.m21; return Quaternion.LookRotation(forward, upwards); } public static Vector3 ExtractScale(this Matrix4x4 matrix) { Vector3 scale; scale.x = new Vector4(matrix.m00, matrix.m10, matrix.m20, matrix.m30).magnitude; scale.y = new Vector4(matrix.m01, matrix.m11, matrix.m21, matrix.m31).magnitude; scale.z = new Vector4(matrix.m02, matrix.m12, matrix.m22, matrix.m32).magnitude; return scale; } 


De plus, pour un travail pratique, vous pouvez implémenter quelques extensions de classe Transform pour y travailler avec TRS.

Transformation d'expansion
 public static void ApplyLocalTRS(this Transform tr, Matrix4x4 trs) { tr.localPosition = trs.ExtractPosition(); tr.localRotation = trs.ExtractRotation(); tr.localScale = trs.ExtractScale(); } public static Matrix4x4 ExtractLocalTRS(this Transform tr) { return Matrix4x4.TRS(tr.localPosition, tr.localRotation, tr.localScale); } 


Sur ce point, les avantages de l'unité s'arrêtent, car les matrices dans Unity sont très pauvres en fonctionnement. De nombreux algorithmes nécessitent une arithmétique matricielle, qui n'est pas implémentée dans l'unité même dans des opérations complètement basiques, telles que l'ajout de matrices et la multiplication de matrices par un scalaire. De plus, en raison de la particularité de l'implémentation de vecteurs dans Unity3d, il existe également un certain nombre d'inconvénients associés au fait que vous pouvez créer un vecteur 4x4, mais ne pouvez pas faire 1x4 hors de la boîte. Puisque nous parlerons davantage de la transformation du Householder pour les réflexions, nous mettons d'abord en place les opérations nécessaires pour cela.

Ajouter / soustraire et multiplier par un scalaire est simple. Cela semble plutôt volumineux, mais il n'y a rien de compliqué ici, car l'arithmétique est simple.

Opérations matricielles de base
 public static Matrix4x4 MutiplyByNumber(this Matrix4x4 matrix, float number) { return new Matrix4x4( new Vector4(matrix.m00 * number, matrix.m10 * number, matrix.m20 * number, matrix.m30 * number), new Vector4(matrix.m01 * number, matrix.m11 * number, matrix.m21 * number, matrix.m31 * number), new Vector4(matrix.m02 * number, matrix.m12 * number, matrix.m22 * number, matrix.m32 * number), new Vector4(matrix.m03 * number, matrix.m13 * number, matrix.m23 * number, matrix.m33 * number) ); } public static Matrix4x4 DivideByNumber(this Matrix4x4 matrix, float number) { return new Matrix4x4( new Vector4(matrix.m00 / number, matrix.m10 / number, matrix.m20 / number, matrix.m30 / number), new Vector4(matrix.m01 / number, matrix.m11 / number, matrix.m21 / number, matrix.m31 / number), new Vector4(matrix.m02 / number, matrix.m12 / number, matrix.m22 / number, matrix.m32 / number), new Vector4(matrix.m03 / number, matrix.m13 / number, matrix.m23 / number, matrix.m33 / number) ); } public static Matrix4x4 Plus(this Matrix4x4 matrix, Matrix4x4 matrixToAdding) { return new Matrix4x4( new Vector4(matrix.m00 + matrixToAdding.m00, matrix.m10 + matrixToAdding.m10, matrix.m20 + matrixToAdding.m20, matrix.m30 + matrixToAdding.m30), new Vector4(matrix.m01 + matrixToAdding.m01, matrix.m11 + matrixToAdding.m11, matrix.m21 + matrixToAdding.m21, matrix.m31 + matrixToAdding.m31), new Vector4(matrix.m02 + matrixToAdding.m02, matrix.m12 + matrixToAdding.m12, matrix.m22 + matrixToAdding.m22, matrix.m32 + matrixToAdding.m32), new Vector4(matrix.m03 + matrixToAdding.m03, matrix.m13 + matrixToAdding.m13, matrix.m23 + matrixToAdding.m23, matrix.m33 + matrixToAdding.m33) ); } public static Matrix4x4 Minus(this Matrix4x4 matrix, Matrix4x4 matrixToMinus) { return new Matrix4x4( new Vector4(matrix.m00 - matrixToMinus.m00, matrix.m10 - matrixToMinus.m10, matrix.m20 - matrixToMinus.m20, matrix.m30 - matrixToMinus.m30), new Vector4(matrix.m01 - matrixToMinus.m01, matrix.m11 - matrixToMinus.m11, matrix.m21 - matrixToMinus.m21, matrix.m31 - matrixToMinus.m31), new Vector4(matrix.m02 - matrixToMinus.m02, matrix.m12 - matrixToMinus.m12, matrix.m22 - matrixToMinus.m22, matrix.m32 - matrixToMinus.m32), new Vector4(matrix.m03 - matrixToMinus.m03, matrix.m13 - matrixToMinus.m13, matrix.m23 - matrixToMinus.m23, matrix.m33 - matrixToMinus.m33) ); } 


Mais pour la réflexion, nous avons besoin de l'opération de multiplication matricielle dans un cas particulier particulier. Multiplication d'un vecteur de dimension 4x4 par 1x4 (transposé) Si vous êtes familier avec les mathématiques matricielles, alors vous savez qu'avec cette multiplication vous devez regarder les nombres extrêmes de la dimension, et vous obtiendrez la dimension de la matrice en sortie, c'est-à-dire dans ce cas 4x4. Il y a suffisamment d'informations sur la façon dont les matrices sont multipliées, nous ne peindrons donc pas cela. Voici un exemple d'un cas spécifique qui sera utile à l'avenir.

Multipliez un vecteur par un transposé
 public static Matrix4x4 MultiplyVectorsTransposed(Vector4 vector, Vector4 transposeVector) { float[] vectorPoints = new[] {vector.x, vector.y, vector.z, vector.w}, transposedVectorPoints = new[] {transposeVector.x, transposeVector.y, transposeVector.z, transposeVector.w}; int matrixDimension = vectorPoints.Length; float[] values = new float[matrixDimension * matrixDimension]; for (int i = 0; i < matrixDimension; i++) { for (int j = 0; j < matrixDimension; j++) { values[i + j * matrixDimension] = vectorPoints[i] * transposedVectorPoints[j]; } } return new Matrix4x4( new Vector4(values[0], values[1], values[2], values[3]), new Vector4(values[4], values[5], values[6], values[7]), new Vector4(values[8], values[9], values[10], values[11]), new Vector4(values[12], values[13], values[14], values[15]) ); } 


Transformation du chef de ménage


À la recherche de la façon de refléter un objet par rapport à n'importe quel axe, je rencontre souvent des conseils pour mettre une échelle négative dans la direction nécessaire. C'est un très mauvais conseil dans le contexte d'Unity, car il casse beaucoup de systèmes dans le moteur (butching, collisions, etc.) Dans certains algorithmes, cela se transforme en calculs assez banals si vous devez réfléchir de manière non triviale par rapport à Vector3.up ou Vector3.forward, mais dans une direction arbitraire. La méthode de réflexion dans une unité prête à l'emploi n'est pas implémentée, j'ai donc implémenté la méthode Householder .

La transformation Householder est utilisée non seulement en infographie, mais dans ce contexte, c'est une transformation linéaire qui reflète un objet par rapport à un plan qui passe par «l'origine» et est déterminée par la normale au plan. Dans de nombreuses sources, il est décrit de manière assez compliquée et incompréhensible, bien que sa formule soit élémentaire.

H = I-2 * n * (n ^ T)

H est la matrice de transformation, I dans notre cas est Matrix4x4.identity, et n = nouveau Vector4 (planeNormal.x, planeNormal.y, planeNormal.z, 0). Le symbole T signifie transposition, c'est-à-dire qu'après multiplication de n * (n ^ T), nous obtenons une matrice 4x4.

Les méthodes mises en œuvre seront utiles et l'enregistrement sera très compact.

Transformation du chef de ménage
 public static Matrix4x4 HouseholderReflection(this Matrix4x4 matrix4X4, Vector3 planeNormal) { planeNormal.Normalize(); Vector4 planeNormal4 = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, 0); Matrix4x4 householderMatrix = Matrix4x4.identity.Minus( MultiplyVectorsTransposed(planeNormal4, planeNormal4).MutiplyByNumber(2)); return householderMatrix * matrix4X4; } 


Important: planeNormal doit être normalisé (ce qui est logique), et la dernière coordonnée n doit être 0, afin qu'il n'y ait aucun effet d'étirement dans la direction, car cela dépend de la longueur du vecteur n.

Maintenant, pour plus de commodité dans Unity, nous implémentons la méthode d'extension pour la transformation

Réflexion de la transformation dans le système de coordonnées local
 public static void LocalReflect(this Transform tr, Vector3 planeNormal) { var trs = tr.ExtractLocalTRS(); var reflected = trs.HouseholderReflection(planeNormal); tr.ApplyLocalTRS(reflected); } 


C'est tout pour aujourd'hui, si cette série d'articles continue d'être intéressante, je dévoilerai également d'autres applications des mathématiques au développement de jeux. Cette fois, il n'y aura pas de projet, car tout le code est placé dans l'article, mais le projet avec une application spécifique sera dans l'article suivant. Sur la photo, vous pouvez deviner de quoi parlera le prochain article.



Merci de votre attention!

Source: https://habr.com/ru/post/fr432544/


All Articles