Olá pessoal! Meu nome é Grisha e sou o fundador da CGDevs. Hoje eu quero continuar o tópico de matemática no game dev. Em um
artigo anterior , mostramos exemplos básicos de uso de vetores e integrais em projetos do Unity, e agora vamos falar sobre matrizes e transformações afins. Se você é bem versado em aritmética matricial; Você sabe o que é TRS e como trabalhar com ele; qual é a transformação do chefe de família - você provavelmente não encontrará nada de novo para si mesmo. Falaremos no contexto de gráficos 3D. Se você está interessado neste tópico - bem-vindo ao gato.

Vamos começar com um dos conceitos mais importantes no contexto do artigo -
transformações afins .
Transformações afins são, em essência, a transformação do sistema de coordenadas (ou espaço) multiplicando o vetor por uma matriz especial. Por exemplo, transformações como movimento, rotação, escala, reflexão, etc. As principais propriedades das transformações afins são que você fica no mesmo espaço (é impossível fazer bidimensional a partir de um vetor tridimensional) e que se as linhas se cruzam / são paralelas / foram cruzados antes da conversão, essa propriedade será preservada após a conversão. Além disso, eles têm muitas propriedades matemáticas que requerem conhecimento da teoria de grupos, conjuntos e álgebra linear, o que facilita o trabalho com eles.
Matriz TRS
O segundo conceito importante em computação gráfica é a
matriz TRS . Usando-o, você pode descrever as operações mais comuns usadas ao trabalhar com gráficos de computador.
Uma matriz TRS é uma composição de três matrizes de transformação. Matrizes de translação (translação), rotação em cada eixo (rotação) e escala (escala).
Ela é assim.

Onde:
O movimento é
t = novo Vetor3 (d, h, l).
Dimensionamento -
s = novo Vetor3 (novo Vetor3 (a, e, i) .magnitude, novo Vetor3 (b, f, j) .magnitude, novo Vetor3 (c, g, k) .magnitude);
Uma rotação é uma matriz do formulário:

Agora vamos aprofundar um pouco mais o contexto do Unity. Para começar, a matriz TRS é uma coisa muito conveniente, mas não deve ser usada em qualquer lugar. Como uma simples indicação da posição ou adição de vetores em uma unidade funcionará mais rapidamente, mas em muitos algoritmos matemáticos, as matrizes são muitas vezes mais convenientes que os vetores. A funcionalidade TRS no Unity é amplamente implementada na classe
Matrix4x4 , mas não é conveniente do ponto de vista da aplicação. Como, além de aplicar a matriz por multiplicação, geralmente é possível armazenar informações sobre a orientação do objeto e, para algumas transformações, eu gostaria de poder calcular não apenas a posição, mas também alterar a orientação do objeto como um todo (por exemplo, reflexão que não é implementada no Unity)
Todos os exemplos são dados abaixo para o sistema de coordenadas local (a origem do GameObject é considerada a origem das coordenadas, dentro da qual o objeto está localizado. Se o objeto é a raiz da hierarquia na unidade, a origem é o mundo (0,0,0)).
Como usando a matriz TRS, podemos basicamente descrever a posição de um objeto no espaço, precisamos de uma decomposição do TRS para os valores específicos de posição, rotação e escala do Unity. Para fazer isso, você pode escrever métodos de extensão para a classe Matrix4x4
Obtendo posição, rotação e escalapublic 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; }
Além disso, para um trabalho conveniente, você pode implementar algumas extensões de classe Transform para trabalhar com o TRS.
Transformação de expansão 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); }
Nisso, as vantagens da unidade terminam, uma vez que as matrizes na unidade são muito pobres em operação. Muitos algoritmos requerem aritmética matricial, que não é implementada na unidade, mesmo em operações completamente básicas, como adicionar matrizes e multiplicar matrizes por um escalar. Além disso, devido à peculiaridade da implementação de vetores no Unity3d, também existem vários inconvenientes associados ao fato de você poder criar um vetor 4x4, mas não o 1x4. Como falaremos mais sobre a transformação do chefe de família para reflexões, primeiro implementamos as operações necessárias para isso.
Adicionar / subtrair e multiplicar por um escalar é simples. Parece bastante volumoso, mas não há nada complicado aqui, pois a aritmética é simples.
Operações básicas da matriz 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) ); }
Mas, para reflexão, precisamos da operação de multiplicação de matrizes em um caso particular. Multiplicação de um vetor de dimensão 4x4 por 1x4 (transposto) Se você conhece a matemática de matrizes, sabe que, com essa multiplicação, precisa olhar para os números extremos da dimensão e obterá a dimensão da matriz na saída, ou seja, neste caso 4x4. Há informações suficientes sobre como as matrizes são multiplicadas, portanto não iremos pintar isso. Aqui está um exemplo de um caso específico que será útil no futuro.
Multiplique um vetor por uma transposta 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]) ); }
Transformação familiar
Em busca de como refletir um objeto em relação a qualquer eixo, costumo encontrar conselhos para colocar uma escala negativa na direção necessária. Esse é um péssimo conselho no contexto do Unity, pois quebra muitos sistemas no mecanismo (cortes, colisões etc.) Em alguns algoritmos, isso se transforma em cálculos não triviais se você precisar refletir de maneira não trivial em relação ao Vector3.up ou Vector3.forward, mas em uma direção arbitrária. O método de reflexão em uma unidade pronta para uso não é implementado, então eu implementei
o método Household .
A transformação de chefe de família é usada não apenas em computação gráfica, mas neste contexto é uma transformação linear que reflete um objeto em relação a um plano que passa pela “origem” e é determinado pelo normal ao plano. Em muitas fontes, é descrito de maneira bastante complicada e incompreensível, embora sua fórmula seja elementar.
H = I-2 * n * (n ^ T)Onde
H é a matriz de transformação,
eu , no nosso caso, é Matrix4x4.identity e
n = new Vector4 (planeNormal.x, planeNormal.y, planeNormal.z, 0). O símbolo T significa transposição, ou seja, após multiplicar
n * (n ^ T) , obtemos uma matriz 4x4.
Os métodos implementados serão úteis e a gravação será muito compacta.
Transformação familiar 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 deve ser normalizado (o que é lógico) e a última coordenada n deve ser 0, para que não haja efeito de alongamento na direção, pois depende do comprimento do vetor n.
Agora, por conveniência no Unity, implementamos o método de extensão para transformação
Transformação de reflexão no 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); }
Isso é tudo por hoje, se esta série de artigos continuar interessante, também revelarei outras aplicações da matemática no desenvolvimento de jogos. Desta vez, não haverá projeto, pois todo o código é colocado no artigo, mas o projeto com aplicativo específico estará no próximo artigo. Na imagem, você pode adivinhar o que será o próximo artigo.

Obrigado pela atenção!