Matemáticas en Gamedev es simple. Matrices y transformaciones afines

Hola a todos! Mi nombre es Grisha y soy el fundador de CGDevs. Hoy quiero continuar con el tema de las matemáticas en el desarrollo de juegos. En un artículo anterior , mostramos ejemplos básicos del uso de vectores e integrales en proyectos de Unity, y ahora hablemos de matrices y transformaciones afines. Si está bien versado en aritmética matricial; Sabes qué es TRS y cómo trabajar con él; ¿Cuál es la transformación del jefe de familia? Probablemente no encontrará nada nuevo para usted. Hablaremos en el contexto de los gráficos en 3D. Si está interesado en este tema, bienvenido a cat.



Comencemos con uno de los conceptos más importantes en el contexto del artículo: las transformaciones afines . Las transformaciones afines son, en esencia, la transformación del sistema de coordenadas (o espacio) multiplicando el vector por una matriz especial. Por ejemplo, transformaciones como movimiento, rotación, escala, reflexión, etc. Las principales propiedades de las transformaciones afines son que permaneces en el mismo espacio (es imposible hacer dos dimensiones a partir de un vector tridimensional) y que si las líneas se intersectaran / fueran paralelas / se cruzaron antes de la conversión, esta propiedad se conservará después de la conversión. Además, tienen muchas propiedades matemáticas que requieren el conocimiento de la teoría de grupos, conjuntos y álgebra lineal, lo que facilita el trabajo con ellos.

Matriz TRS


El segundo concepto importante en gráficos por computadora es la matriz TRS . Al usarlo, puede describir las operaciones más comunes que se usan al trabajar con gráficos de computadora. Una matriz TRS es una composición de tres matrices de transformación. Matrices de desplazamiento (Traslación), rotación en cada eje (Rotación) y escala (Escala).
Ella se ve así.



Donde:
El movimiento es t = nuevo Vector3 (d, h, l).
Escala - s = nuevo Vector3 (nuevo Vector3 (a, e, i) .magnitud, nuevo Vector3 (b, f, j) .magnitud, nuevo Vector3 (c, g, k) .magnitud);

Una rotación es una matriz de la forma:



Ahora profundicemos un poco más en el contexto de Unity. Para empezar, la matriz TRS es algo muy conveniente, pero no debe usarse en todas partes. Dado que una simple indicación de la posición o la adición de vectores en una unidad funcionará más rápido, pero en muchos algoritmos matemáticos, las matrices son muchas veces más convenientes que los vectores. La funcionalidad TRS en Unity se implementa en gran medida en la clase Matrix4x4 , pero no es conveniente desde el punto de vista de la aplicación. Dado que, además de aplicar la matriz a través de la multiplicación, generalmente puede almacenar información sobre la orientación del objeto, y para algunas transformaciones, me gustaría poder calcular no solo la posición, sino también cambiar la orientación del objeto en su conjunto (por ejemplo, la reflexión que no se implementa en Unity)

Todos los ejemplos se dan a continuación para el sistema de coordenadas local (el origen del GameObject se considera el origen de las coordenadas, dentro de las cuales se encuentra el objeto. Si el objeto es la raíz de la jerarquía en la unidad, entonces el origen es mundo (0,0,0)).

Dado que al usar la matriz TRS, básicamente puede describir la posición de un objeto en el espacio, necesitamos una descomposición de TRS a los valores específicos de posición, rotación y escala de Unity. Para hacer esto, puede escribir métodos de extensión para la clase Matrix4x4

Obtener posición, rotación y escala
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; } 


Además, para un trabajo conveniente, puede implementar un par de extensiones de clase Transformar para trabajar con TRS en él.

Transformación de expansión
 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); } 


En esto, las ventajas de la unidad terminan, ya que las matrices en la Unidad son muy pobres en operación. Muchos algoritmos requieren aritmética matricial, que no se implementa en la unidad incluso en operaciones completamente básicas, como agregar matrices y multiplicar matrices por un escalar. Además, debido a la peculiaridad de la implementación de vectores en Unity3d, también hay una serie de inconvenientes asociados con el hecho de que puede hacer un vector 4x4, pero no puede hacer 1x4 fuera de la caja. Dado que hablaremos más sobre la transformación del Jefe de familia para las reflexiones, primero implementamos las operaciones necesarias para esto.

Sumar / restar y multiplicar por un escalar es simple. Parece bastante voluminoso, pero no hay nada complicado aquí, ya que la aritmética es simple.

Operaciones matriciales básicas
 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) ); } 


Pero para reflexionar, necesitamos la operación de multiplicación de matrices en un caso particular en particular. Multiplicación de un vector de dimensión 4x4 por 1x4 (transpuesto) Si está familiarizado con las matemáticas de matriz, entonces sabe que con esta multiplicación necesita mirar los números extremos de la dimensión, y obtendrá la dimensión de la matriz en la salida, es decir, en este caso 4x4. Hay suficiente información sobre cómo se multiplican las matrices, por lo que no pintaremos esto. Aquí hay un ejemplo de un caso específico que será útil en el futuro.

Multiplicar un vector por una transpuesta
 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]) ); } 


Transformación del hogar


En busca de cómo reflejar un objeto en relación con cualquier eje, a menudo encuentro consejos para colocar una escala negativa en la dirección necesaria. Este es un consejo muy malo en el contexto de Unity, ya que rompe muchos sistemas en el motor (butching, colisiones, etc.) En algunos algoritmos, esto se convierte en cálculos bastante triviales si necesita reflejar no trivialmente con respecto a Vector3.up o Vector3.forward, pero en una dirección arbitraria. El método de reflexión en una unidad fuera de la caja en sí no está implementado, por lo que implementé el método Householder .

La transformación Householder se usa no solo en gráficos de computadora, sino que en este contexto es una transformación lineal que refleja un objeto relativo a un plano que pasa a través del "origen" y está determinado por la normalidad al plano. En muchas fuentes, se describe de manera bastante complicada e incomprensible, aunque su fórmula es elemental.

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

Donde H es la matriz de transformación, I en nuestro caso es Matrix4x4.identity, y n = new Vector4 (planeNormal.x, planeNormal.y, planeNormal.z, 0). El símbolo T significa transposición, es decir, después de multiplicar n * (n ^ T) obtenemos una matriz 4x4.

Los métodos implementados serán útiles y la grabación será muy compacta.

Transformación del hogar
 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; } 


Importante: planeNormal debería normalizarse (lo cual es lógico), y la última coordenada n debería ser 0, de modo que no haya ningún efecto de estiramiento en la dirección, ya que depende de la longitud del vector n.

Ahora, por conveniencia en Unity, implementamos el método de extensión para la transformación

Reflexión de la transformación en el sistema de coordenadas local.
 public static void LocalReflect(this Transform tr, Vector3 planeNormal) { var trs = tr.ExtractLocalTRS(); var reflected = trs.HouseholderReflection(planeNormal); tr.ApplyLocalTRS(reflected); } 


Eso es todo por hoy, si esta serie de artículos seguirá siendo interesante, también revelaré otras aplicaciones de las matemáticas en el desarrollo de juegos. Esta vez no habrá proyecto, ya que todo el código se coloca en el artículo, pero el proyecto con la aplicación específica estará en el próximo artículo. De la imagen puedes adivinar de qué se tratará el próximo artículo.



Gracias por su atencion!

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


All Articles