Gamedev中的数学很简单。 矩阵和仿射变换

大家好! 我叫Grisha,是CGDevs的创始人。 今天,我想继续游戏开发中的数学主题。 在上一篇文章中 ,我们展示了在Unity项目中使用向量和积分的基本示例,现在让我们讨论矩阵和仿射变换。 如果您精通矩阵算术; 您知道什么是TRS以及如何使用它。 什么是Householder转换-您可能不会为自己找到任何新东西。 我们将在3D图形的背景下进行讨论。 如果您对此主题感兴趣-欢迎来到cat。



让我们从文章仿射转换的上下文中最重要的概念开始。 仿射变换本质上是通过将向量乘以特殊矩阵来变换坐标系(或空间)。 例如,诸如移动,旋转,缩放,反射之类的变换。仿射变换的主要属性是您停留在同一空间中(不可能从三维矢量中生成二维),并且如果线相交/平行/在转换前被交叉,那么此属性将在转换后保留。 此外,它们具有许多数学特性,需要了解组,集合和线性代数的理论,这使得使用它们更加容易。

TRS矩阵


计算机图形学中的第二个重要概念是TRS矩阵 。 使用它,您可以描述计算机图形处理中最常用的操作。 TRS矩阵是三个变换矩阵的组合。 位移矩阵(平移),每个轴上的旋转(旋转)和缩放比例(缩放)。
她看起来像这样。



其中:
移动t =新的Vector3(d,h,l)。
缩放 -s =新的Vector3(新的Vector3(a,e,i)。幅值,新的Vector3(b,f,j)。幅值,新的Vector3(c,g,k)。幅值);

旋转是以下形式的矩阵:



现在让我们更深入地了解Unity上下文。 首先,TRS矩阵是很方便的事情,但不应在任何地方使用。 由于简单指示单位中矢量的位置或相加会更快,但是在许多数学算法中,矩阵比矢量更方便很多倍。 Unity中的TRS功能主要在Matrix4x4类中实现,但从应用程序角度来看并不方便。 因为除了通过乘法应用矩阵外,它通常还可以存储有关对象方向的信息,并且对于某些变换,我希望不仅能够计算位置,而且还可以整体上更改对象的方向(例如,Unity中未实现的反射)

以下是局部坐标系的所有示例(将GameObject的原点视为对象所在的坐标的原点。如果对象是单位中层次结构的根,则原点为world(0,0,0))。

由于使用TRS矩阵,我们基本上可以描述物体在空间中的位置,因此需要将TRS分解为Unity的位置,旋转和比例的特定值。 为此,您可以为Matrix4x4类编写扩展方法。

获取位置,旋转和比例
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; } 


另外,为了方便工作,您可以实现几个Transform类扩展来在其中使用TRS。

扩展变换
 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); } 


在此方面,由于Unity中的矩阵在操作上非常差,因此实现了统一优势。 许多算法需要矩阵算术,即使在完全基本的运算中,例如在矩阵中加矩阵和将矩阵乘以标量,也无法在单元中实现矩阵算法。 此外,由于Unity3d中矢量实现的特殊性,因此您可以制作4x4矢量,但不能直接制作1x4,这也带来了许多不便。 由于我们将更多地讨论Householder的转换以进行反思,因此我们首先为此执行必要的操作。

标量的加/减和乘积很简单。 它看起来相当庞大,但是这里没有什么复杂的,因为算术很简单。

基本矩阵运算
 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) ); } 


但是为了反思,我们需要在特定情况下进行矩阵乘法运算。 将4x4的向量乘以1x4(转置)的乘积(转置)如果您熟悉矩阵数学,则知道通过此乘积需要查看维的极数,您将在输出处获得矩阵的维,即本例中为4x4。 有关如何将矩阵相乘的足够信息,因此我们将不作介绍。 这是一个特定案例的示例,它将在将来派上用场。

向量乘以转置
 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]) ); } 


户主转型


在寻找如何相对于任何轴反射对象时,我经常遇到建议在必要的方向上设置负比例。 在Unity上下文中,这是非常糟糕的建议,因为它破坏了引擎中的许多系统(butching,碰撞等)。在某些算法中,如果您需要对Vector3.up或Vector3.forward进行微不足道的反映,这将变成非常平凡的计算,但朝着任意方向发展。 开箱即用的单元中的反射方法尚未实现,因此我实施了Householder方法

Householder变换不仅在计算机图形学中使用,而且在这种情况下,它是一种线性变换,它相对于穿过“原点”的平面反映对象,并由平面的法线确定。 尽管它的公式很基本,但在许多资料中都对它进行了相当复杂且难以理解的描述。

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

其中H是变换矩阵,在我们的情况下, 是Matrix4x4.identity,并且n =新的Vector4(planeNormal.x,planeNormal.y,planeNormal.z,0)。 符号T表示换位,即乘以n *(n ^ T)后得到4x4矩阵。

实施的方法将派上用场,并且录音将非常紧凑。

户主转型
 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; } 


重要事项:应该对planeNormal进行标准化(这是逻辑上的),最后一个坐标n应该为0,这样就不会在方向上进行拉伸,因为它取决于向量n的长度。

现在为方便起见,我们在Unity中实现扩展方法进行转换

在局部坐标系中反映变换
 public static void LocalReflect(this Transform tr, Vector3 planeNormal) { var trs = tr.ExtractLocalTRS(); var reflected = trs.HouseholderReflection(planeNormal); tr.ApplyLocalTRS(reflected); } 


今天就这些了,如果这一系列文章将继续引起人们的兴趣,我还将介绍数学在游戏开发中的其他应用。 这次将没有项目,因为所有代码都放在了文章中,但是具有特定应用程序的项目将在下一篇文章中。 从图片中,您可以猜测下一篇文章的内容。



感谢您的关注!

Source: https://habr.com/ru/post/zh-CN432544/


All Articles