游戏开发中的进阶向量数学
日期: 2020-12-12 分类: 个人收藏 422次阅读
飞机
点积具有带有单位向量的另一个有趣的属性。想象一下,垂直于该矢量(并通过原点)的平面通过了一个平面。平面将整个空间分为正数(在平面上)和负数(在平面下),并且(与流行的看法相反),您还可以在2D中使用其数学运算:
垂直于曲面的单位向量(因此,它们描述了曲面的方向)称为单位法向向量。虽然,通常他们只是简称为法线。法线出现在飞机,3D几何(以确定其中每一个面或顶点板壁)等。通常 是一个单位矢量,但它被称为正常 ,因为它的用法。(就像我们将(0,0)称为原点)。
看起来很简单。平面经过原点,并且其表面垂直于单位矢量(或法线)。指向向量的一侧为正半空间,而另一侧为负半空间。在3D中,这是完全相同的,除了平面是一个无限的表面(想象一个可以定向并固定到原点的无限的平纸)而不是一条线。
到飞机的距离
现在很清楚飞机是什么,让我们回到点积。单位矢量和空间中任何点之间的点积 (是的,这次我们进行矢量和位置之间的点积),返回从点到平面的距离:
var distance = normal.Dot(point);
但是不仅是绝对距离,如果点在负半空间中,则距离也将为负:
这使我们能够知道一个点在平面的哪一侧。
远离原点
我知道你在想什么!到目前为止,这还不错,但是真实的飞机在空间中无处不在,不仅经过原点。你想真正的飞机的行动,你想它现在。
请记住,平面不仅将空间分成两部分,而且还具有极性。这意味着可以有完全重叠的平面,但是它们的负半空间和正半空间会互换。
考虑到这一点,让我们将整个平面描述为法线 N和距原点标量D的 距离。因此,我们的平面由N和D表示。例如:
对于3D数学,Godot提供了Plane 内置类型来处理。
基本上,N和D可以表示空间中的任何平面,无论是2D还是3D(取决于N的维数),并且两者的数学公式相同。与以前相同,但是D是从原点到平面的距离,沿N方向行进。例如,假设您想到达飞机上的某个点,您将执行以下操作:
var pointInPlane = N * D;
这将拉伸(调整大小)法线向量并使之接触平面。这个数学运算看起来似乎很混乱,但是实际上比看起来要简单得多。如果我们想再次说出点到平面的距离,我们可以做同样的事情,只是要调整距离:
var distance = N.Dot(point) - D;
使用内置函数的相同操作:
var distance = plane.DistanceTo(point);
这将再次返回正或负距离。
可以通过使N和D都为负值来翻转平面的极性。这将导致平面处于相同的位置,但是具有负半角和正半角的反转:
N = -N;
D = -D;
当然,Godot也可以在Plane中实现此运算符,因此请执行以下操作:
var invertedPlane = -plane;
将按预期工作。
因此,请记住,飞机就是这样,它的主要实际用途是计算到它的距离。那么,为什么计算点到平面的距离有用呢?这非常有用!让我们看一些简单的例子。
以2D方式构建平面
平面显然不会从任何地方冒出来,因此必须进行构建。以2D方式构建它们很容易,可以从法线(单位矢量)和一个点,也可以从空间中的两个点完成。
对于法线和点,由于已经计算了法线,因此大部分工作都已完成,因此只需根据法线和点的点积计算D。
var N = normal;
var D = normal.Dot(point);
对于空间中的两个点,实际上有两个平面穿过它们,它们共享相同的空间,但法线指向相反的方向。要从两点计算法线,必须首先获取方向矢量,然后将其向任一侧旋转90°度:
// Calculate vector from `a` to `b`.
var dvec = (pointB - pointA).Normalized();
// Rotate 90 degrees.
var normal = new Vector2(dvec.y, -dvec.x);
// Alternatively (depending the desired side of the normal):
// var normal = new Vector2(-dvec.y, dvec.x);
其余部分与前面的示例相同,因为point_a或point_b都在同一平面上,所以它们都可以工作:
var N = normal;
var D = normal.Dot(pointA);
// this works the same
// var D = normal.Dot(pointB);
在3D模式下执行相同的操作会稍微复杂一些,将在后面进行详细说明。
飞机的一些例子
这是平面有用的简单示例。假设您有一个凸 多边形。例如,矩形,梯形,三角形或没有面向内弯曲的任何多边形。
对于多边形的每个片段,我们都会计算经过该片段的平面。一旦有了平面列表,我们就可以做整齐的事情,例如检查点是否在多边形内。
我们遍历所有平面,如果可以找到到该点的距离为正的平面,则该点在多边形之外。如果我们做不到,那么重点就在里面。
代码应该是这样的:
var inside = true;
foreach (var p in planes)
{
// check if distance to plane is positive
if (p.DistanceTo(point) > 0)
{
inside = false;
break; // with one that fails, it's enough
}
}
太酷了吧?但这会变得更好!稍加努力,当两个凸多边形也重叠时,类似的逻辑就会让我们知道。这称为分离轴定理(或SAT),大多数物理引擎都使用它来检测碰撞。
对于一个点,仅检查飞机是否返回正距离就足以确定该点是否在外面。对于另一个多边形,我们必须找到一个平面,在该平面上所有 其他多边形点都将 返回一个正距离。该检查是使用A的平面相对于B的点进行的,然后使用B的平面相对于A的点进行的:
代码应该是这样的:
var overlapping = true;
foreach (Plane plane in planesOfA)
{
var allOut = true;
foreach (Vector3 point in pointsOfB)
{
if (plane.DistanceTo(point) < 0)
{
allOut = false;
break;
}
}
if (allOut)
{
// a separating plane was found
// do not continue testing
overlapping = false;
break;
}
}
if (overlapping)
{
// only do this check if no separating plane
// was found in planes of A
foreach (Plane plane in planesOfB)
{
var allOut = true;
foreach (Vector3 point in pointsOfA)
{
if (plane.DistanceTo(point) < 0)
{
allOut = false;
break;
}
}
if (allOut)
{
overlapping = false;
break;
}
}
}
if (overlapping)
{
GD.Print("Polygons Collided!");
}
如您所见,飞机非常有用,这是冰山一角。您可能想知道非凸多边形会发生什么。通常可以通过将凹面多边形拆分为较小的凸面多边形,或使用诸如BSP(如今已不多使用)之类的技术来处理。
3D碰撞检测
这是另外一个奖励,是对耐心和遵守本篇教程的奖励。这是另一个智慧。这可能不是直接用例(Godot已经很好地进行了碰撞检测),但是几乎所有物理引擎和碰撞检测库都在使用它:)
还记得将2D中的凸形转换为2D平面数组对于碰撞检测很有用吗?您可以检测点是否在任何凸形形状内,或者两个2D凸形形状是否重叠。
好吧,这也适用于3D,如果两个3D多面体形状发生碰撞,您将无法找到分离平面。如果找到分离平面,则形状绝对不会碰撞。
要稍微刷新一点,一个分离平面意味着多边形A的所有顶点都在该平面的一侧,而多边形B的所有顶点都在另一侧。该平面始终是面A或面B的端面之一。
但是在3D中,这种方法存在问题,因为在某些情况下可能找不到分离平面。这是这种情况的一个示例:
为了避免这种情况,需要测试一些额外的平面作为分隔符,这些平面是面A的边与面B的边之间的叉积。
所以最终的算法是这样的:
var overlapping = true;
foreach (Plane plane in planesOfA)
{
var allOut = true;
foreach (Vector3 point in pointsOfB)
{
if (plane.DistanceTo(point) < 0)
{
allOut = false;
break;
}
}
if (allOut)
{
// a separating plane was found
// do not continue testing
overlapping = false;
break;
}
}
if (overlapping)
{
// only do this check if no separating plane
// was found in planes of A
foreach (Plane plane in planesOfB)
{
var allOut = true;
foreach (Vector3 point in pointsOfA)
{
if (plane.DistanceTo(point) < 0)
{
allOut = false;
break;
}
}
if (allOut)
{
overlapping = false;
break;
}
}
}
if (overlapping)
{
foreach (Vector3 edgeA in edgesOfA)
{
foreach (Vector3 edgeB in edgesOfB)
{
var normal = edgeA.Cross(edgeB);
if (normal.Length() == 0)
{
continue;
}
var maxA = float.MinValue; // tiny number
var minA = float.MaxValue; // huge number
// we are using the dot product directly
// so we can map a maximum and minimum range
// for each polygon, then check if they
// overlap.
foreach (Vector3 point in pointsOfA)
{
var distance = normal.Dot(point);
maxA = Mathf.Max(maxA, distance);
minA = Mathf.Min(minA, distance);
}
var maxB = float.MinValue; // tiny number
var minB = float.MaxValue; // huge number
foreach (Vector3 point in pointsOfB)
{
var distance = normal.Dot(point);
maxB = Mathf.Max(maxB, distance);
minB = Mathf.Min(minB, distance);
}
if (minA > maxB || minB > maxA)
{
// not overlapping!
overlapping = false;
break;
}
}
if (!overlapping)
{
break;
}
}
}
if (overlapping)
{
GD.Print("Polygons Collided!");
}
更多信息
有关在Godot中使用向量数学的更多信息,请参见以下文章:
矩阵与变换
如果您需要其他说明,请查看3Blue1Brown的精彩视频系列“线性代数的本质”:https://www.youtube.com/watch?v=fNk_zzaMoSs&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
精华推荐