Godot 向量数学

godot的坐标系

二维平面上的任何位置都可以用一对数字来表示。然而,我们也可以将位置 (4, 3) 看作是从 (0, 0) 点或 原点 出发的 偏移 。画一个箭头从原点指向点:


关于向量,需要考虑的一个重要点是它们仅表示 相对 方向和大小。没有一个向量的位置的概念。以下两个向量相同:


向量表示方向和幅度。仅表示幅值的值称为 标量。

gdscript

创建向量

$Node2D.position = Vector2(400, 300)

# create a vector with coordinates (2, 5)
var a = Vector2(2, 5)
# create a vector and assign x and y manually
var b = Vector2()
b.x = 3
b.y = 1

相加

var c = a + b # (2, 5) + (3, 1) = (5, 6)
加法 a + bb + a结果是一样的
我们也可以通过在第一个向量的末尾加上第二个向量来直观地看到这一点:

向量x标量

var c = a * 2  # (2, 5) * 2 = (4, 10)
var d = b / 3  # (3, 6) / 3 = (1, 2)

移动/加法

向量可以表示具有大小和方向的任何量。典型的示例有:位置、速度、加速度和力。在这幅图像中,在第一步的飞船有一个位置向量为(1,3)和一个速度向量为(2,1)。速度向量表示船舶每一步移动的距离。通过将速度加到当前位置,我们可以求出步骤2的位置。

朝向/减法

在这个场景中,您有一个坦克,希望它的炮塔指向机器人。从机器人的位置减去坦克的位置就得到了从坦克指向机器人的向量。


要找到从 A 指向 B 的向量,请使用 B - A。

单位向量 / 归一化

一个 大小 为 1 的向量称为 单位向量 。它们有时也被称为 方向向量 或 法线 。当您需要跟踪一个方向时,单位向量是有用的。
归一化 向量意味着在保持其方向的同时将其长度减少到 1 。这是通过将每个分量除以其大小来完成的:
原理代码

var a = Vector2(2, 4)
var m = sqrt(a.x*a.x + a.y*a.y)  # get magnitude "m" using the Pythagorean theorem
a.x /= m
a.y /= m

api一行搞定

a = a.normalized()

因为归一化需要除以向量的长度,所以不能对长度为“0”的向量进行归一化。试图这样做会导致错误。

反射

单位向量的一种常见用法是表示 法线 。法向量是垂直于曲面的单位向量,定义了曲面的方向。它们通常用于照明、碰撞和涉及表面的其他操作。
假设我们有一个移动的球,我们想从墙上或其他物体上弹回来:



因为这是一个水平曲面,所以曲面法线的值为 (0, -1) 。当球碰撞时,我们取它的剩余运动(当它撞到表面时剩余的量),用法线反射它。在Godot中, Vector2 类有一个 bounce() 方法来处理这个问题。上图有一个使用 KinematicBody2D 的GDScript示例:

var collision = move_and_collide(velocity * delta)
if collision:
    var reflect = collision.remainder.bounce(collision.normal)
    velocity = velocity.bounce(collision.normal)
    move_and_collide(reflect)

点乘

点乘 是向量数学中最重要的概念之一,但经常被误解。点乘是对两个向量的操作,它返回一个 标量。与同时包含大小和方向的向量不同,标量值只有大小。




内置api

var c = a.dot(b)
var d = b.dot(a)  # these are equivalent

当与单位向量一起使用时,点积是最有用的,使得第一个公式减少到仅有 cosθ。这意味着我们可以使用点积来告诉我们关于两个向量之间的角度的一些信息:



当使用单位向量,结果总是会在 -1 (180°) 和 1 (0°) 之间。

面向问题
我们可以利用这个事实来检测一个物体是否面向另一个物体。在下图中,游戏角色 P 试图避开丧尸 A 和 B 。假设一个丧尸的视野是 180° , 他们能看到游戏角色吗?

var AP = (P - A).normalized()
if AP.dot(fA) > 0:
    print("A sees P!")

叉乘

叉乘获得的依然是一个向量,垂直于两个向量构成的平面,叉乘的标量值也是两个向量的围成平行四边形的面积

实现

var c = Vector3()
c.x = (a.y * b.z) - (a.z * b.y)
c.y = (a.z * b.x) - (a.x * b.z)
c.z = (a.x * b.y) - (a.y * b.x)

api

var c = a.cross(b)

在叉乘中,顺序很重要。a.cross(b) 和 b.cross(a) 的结果不一样。得到的向量指向相反的方向。

法线的计算

叉乘的一种常用方法是在三维空间中求平面或曲面的表面法向量。如果我们有三角形 ABC 我们可以用向量减法找到两条边 AB 和 AC。通过叉乘, AB x AC 得到一个垂直于这两个向量的向量:表面法向量。
下面是一个计算三角形法线的函数:

func get_triangle_normal(a, b, c):
    # find the surface normal given 3 vertices
    var side1 = b - a
    var side2 = c - a
    var normal = side1.cross(side2)
    return normal

平面

点乘对于单位向量还有一个有趣的性质。想象垂直于这个向量(通过原点)经过一个平面。平面将整个空间划分为正(在平面上)和负(在平面下),(与普遍的看法相反)您也可以在2D中使用它们的数学:

垂直于表面的单位向量(因此,它们描述了表面的方向)称为 单位法向量 。不过,它们通常只是缩写为 法线 。法线出现在平面、3D几何中(以确定每个面或顶点的侧边),等等。一个 法线 就是一个 单位向量,但是由于它的使用,它被称为 法线 。(就像我们说坐标(0,0)就是原点一样!).

它就像看起来那样简单。平面经过原点,它的表面垂直于单位向量(或 法线 )。指向向量的一边是正半空间,而另一边是负半空间。在3维空间中,这完全相同,除了平面是一个无限的表面(想象一张无限伸展的平坦纸张,它固定在原点)而不是直线。


点到平面的距离

现在,我们很清楚了平面是什么,让我们再回到点乘上。 单位向量 和任何 空间点 之间的点乘(是的,这次我们在向量和位置之间进行点乘),将返回 从点到平面的距离 :

var distance = normal.dot(point)

但不仅仅是绝对距离,如果点在负半空间中,距离也是负的,这使我们能够知道点在平面的哪一侧。


不在原点

我知道您在想什么!到目前为止,这还不错,但 真正的 平面在空间中无处不在,而不仅仅是通过原点的平面。您想要真正的 平面 ,您 现在 就想行动起来。

记住,平面不仅把空间分成两半,而且它们还有 极性 。这意味着有可能有完全重叠的平面,但是它们的负半空间和正半空间是相反的。

记住这一点,让我们将整个平面描述为 法线 N 和 距原点的距离 标量 D 。因此,我们的平面将由N和D表示,例如:


这将拉伸(调整大小)法线向量并使其接触平面。这个数学可能看起来很疑惑,但实际上比看起来简单得多。如果我们想再说一遍,从点到平面的距离,我们也会这样做,但是要调整距离:

var distance = N.dot(point) - D

api

var distance = plane.distance_to(point)

向量没有位置的概念,它需要-D来获得有效的距离

翻转平面的极性可以通过同时对N和D取负来完成。这将导致平面处于相同的位置,但是具有反转的负半空间和正半空间:

N = -N
D = -D

api

var inverted_plane = -plane

进阶

在二维空间中构造平面

平面显然不是从哪儿冒出来的,所以必须构造。在2D中构造它们很简单,这可以从法线(单位向量)和点,或者用2维空间中的两个点来完成。

针对法线和点的情况,大部分工作已经完成,因为当法线已经计算出来时,只需从法线和点的点乘得到D。

var N = normal
var D = normal.dot(point)

对于空间中的两个点,实际上会有两个平面同时经过它们,它们共享相同的空间,但是法线方向相反。为了从这两个点计算面的法线,必须首先获得方向向量,然后将向任何一边旋转90°:

# calculate vector from a to b
var dvec = (point_b - point_a).normalized()
# rotate 90 degrees
var normal = Vector2(dvec.y, -dvec.x)
# or alternatively
# var normal = Vector2(-dvec.y, dvec.x)
# depending the desired side of the normal

其余的与前面的示例相同,point_a或point_b都可以工作,因为它们在相同的平面中:

var N = normal
var D = normal.dot(point_a)
# this works the same
# var D = normal.dot(point_b)

当法线为单位向量,其实就是将点a或点b乘以cos他们和法线的夹角,就得出了d这个距离值了

平面的一些示例

这里有一个简单的示例,说明平面的用途。假设您有一个凸多边形。例如,矩形、梯形、三角形或任何没有向内弯曲的多边形。

对多边形的每个部分,我们计算出经过该部分的平面。一旦我们有了平面的列表,我们就可以做些分类的事情,例如检查一个点是否在多边形内部。

我们遍历所有平面,如果我们能找到使得点到平面的距离
为正的平面,那么点在多边形之外。如果我们不能,那么这一点就在多边形内部。



如果有任何一个面/边和点点距离是正,那么点就不在多边形内

var inside = true
for p in planes:
    # check if distance to plane is positive
    if (p.distance_to(point) > 0):
        inside = false
        break # with one that fails, it's enough

很酷,是吧?但这可以变得更好!稍加努力,类似的逻辑将让我们知道两个凸多边形是否重叠。这叫做分离轴定理(或SAT),大多数物理引擎都用这个来检测碰撞。

对于一个点,仅仅检查一个平面是否返回正距离就足以判断该点是否在外面。对于一个多边形,我们必须找到一个平面,使得另一个多边形上的所有点到它的距离为正。这种可以用A平面对B点进行检查,然后用B平面对A点进行检查:


var overlapping = true

for p in planes_of_A:
    var all_out = true
    for v in points_of_B:
        if (p.distance_to(v) < 0):
            all_out = false
            break

    if (all_out):
        # 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
    for p in planes_of_B:
        var all_out = true
        for v in points_of_A:
            if (p.distance_to(v) < 0):
                all_out = false
                break

        if (all_out):
            overlapping = false
            break

if (overlapping):
    print("Polygons Collided!")

正如您所看到的,平面是非常有用的,然而这只是冰山一角。您可能想知道非凸多边形会发生什么。这通常只是通过将凹多边形分割成较小的凸多边形来处理,或者使用诸如BSP(现在使用得不多)之类的技术。

三维碰撞检测

还记得把2D中的凸形转换成2D平面阵列对碰撞检测有用吗?您可以检测一个点是否在任何凸形状内,或者两个2D凸形状是否重叠。

嗯,这在3D中也适用,如果两个3D多面体形状碰撞,您将无法找到分离平面。如果发现一个分离平面,那么形状肯定不会发生碰撞。

要得到分离平面意味着多边形A的所有顶点都在平面的一侧,而多边形B的所有顶点都在另一侧。该平面始终是多边形A或多边形B的面向平面之一。

然而在3D中,这种方法存在一个问题,因为在某些情况下可能找不到分离平面。下面就是这种情况的一个示例:



为了避免这种情况,一些额外的平面需要作为分隔器被测试,这些平面是多边形A的边和多边形B的边的叉乘


var final_pos = Vector2(x.dot(new_pos), y.dot(new_pos))
那么我们所拥有的是…等一下,这是船的设计位置!

差值

var t = 0.0

func _physics_process(delta):
    t += delta

    $Monkey.transform = $Position1.transform.interpolate_with($Position2.transform, t)
const FOLLOW_SPEED = 4.0

func _physics_process(delta):
    var mouse_pos = get_local_mouse_position()

    $Sprite.position = $Sprite.position.linear_interpolate(mouse_pos, delta * FOLLOW_SPEED)
image

贝塞尔曲线

func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float):
    var q0 = p0.linear_interpolate(p1, t)
    var q1 = p1.linear_interpolate(p2, t)
image

三次曲线

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
    var q0 = p0.linear_interpolate(p1, t)
    var q1 = p1.linear_interpolate(p2, t)
    var q2 = p2.linear_interpolate(p3, t)

    var r0 = q0.linear_interpolate(q1, t)
    var r1 = q1.linear_interpolate(q2, t)

    var s = r0.linear_interpolate(r1, t)
    return s
image

曲线

var t = 0.0

func _process(delta):
    t += delta
    position = _cubic_bezier(p0, p1, p2, p3, t)
image
var t = 0.0

func _process(delta):
    t += delta
    position = curve.interpolate_baked(t * curve.get_baked_length(), true)
image

https://docs.godotengine.org/zh_CN/latest/tutorials/math/vector_math.html#doc-vector-math
https://docs.godotengine.org/zh_CN/latest/tutorials/math/vectors_advanced.html
https://docs.godotengine.org/zh_CN/latest/tutorials/math/matrices_and_transforms.html

https://docs.godotengine.org/zh_CN/latest/tutorials/math/interpolation.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容