沿着贝塞尔曲线在两个行星之间移动飞船,但缺少一些加速方程


48

好的,我已经在math.stackechange.com上发布了这个,但是没有得到任何答案:(

首先是我的问题的图片,随后进行描述:

替代文字

因此,我已经设定了所有要点和价值观。

船舶开始时P1S=0.27 Degrees每game嗒作动绕左行星运动,到达时按照贝塞尔Point A曲线弯曲直到到达Point D,然后P2S=0.42 Degrees每game嗒作动绕右行星航行。两者的区别在于S,行星绕行星的移动速度相同。

到目前为止一切顺利,我已经启动并运行了,现在是我的问题。

S P1S P2相差很大时,船在到达目的地时会在两种速度之间跳跃,这看起来很糟糕。所以,我需要加速的船Point A,并Point DS P1S P2

我所缺少的是紫色,它们是:

  • 一种计算滴答声的方法,其中考虑了加速度,船舶沿着贝塞尔曲线移动。

  • 再次考虑加速度,一种基于T在贝塞尔曲线上找到位置的方法。

ATM I通过计算点之间的距离来计算贝塞尔N曲线的长度。因此,我认为我需要的是一种缩放比例的方法,该方法T需要根据加速度将其放入贝塞尔曲线计算中。


2
在弄清楚这一点上做得很好。我建议您发布您的发现作为对问题的答案。
bummzack 2010年

Answers:


83

好的,我一切正常,花了很长时间,所以我将在这里发布我的详细解决方案。
注意:所有代码示例均在JavaScript中。

因此,让我们将问题分解为基本部分:

  1. 您需要计算0..1贝塞尔曲线的长度以及之间的点

  2. 现在,您需要调整比例,T以将船从一种速度加速到另一种速度

正确设置贝塞尔曲线

找到一些绘制Bezier曲线的代码很容易,尽管有很多不同的方法,其中一种是DeCasteljau算法,但您也可以将等式用于三次Bézier曲线:

// Part of a class, a, b, c, d are the four control points of the curve
x: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
           + 3 * ((1 - t) * (1 - t)) * t * this.b.x
           + 3 * (1 - t) * (t * t) * this.c.x
           + (t * t * t) * this.d.x;
},

y: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
           + 3 * ((1 - t) * (1 - t)) * t * this.b.y
           + 3 * (1 - t) * (t * t) * this.c.y
           + (t * t * t) * this.d.y;
}

有了这个,一个现在可以通过调用绘制贝塞尔曲线x,并yt从范围0 to 1,让我们一起来看看:

替代文字

呃...这不是真正的平均分配,是吗?
由于贝塞尔曲线的性质,上的点0...1具有不同的arc lenghts,因此靠近起点和终点的线段要比靠近曲线中间的线段长。

将T均匀地映射到曲线上AKA弧长参数化

那么该怎么办?那么简单来说,我们需要一个函数来我们的地图T上的t曲线,让我们T 0.25的结果t那是在25%曲线的长度。

我们该怎么做?好吧,我们是Google ...但是事实证明,该术语不是可搜索的,有时您会点击此PDF。哪一种读物确实很棒,但是如果您已经忘记了在学校学到的所有数学知识(或者您只是不喜欢那些数学符号),那将毫无用处。

现在怎么办?好吧,去Google再读一些(阅读:6个小时),您终于找到了关于该主题的精彩文章(包括精美的图片!^ _ ^“):http :
//www.planetclegg.com/projects/WarpingTextToSplines.html

做实际的代码

如果您很久以前就已经失去了数学知识(并且您设法跳过了很棒的文章链接),尽管您无法拒绝下载PDF ,那么您现在可能会认为:“上帝,数百行代码和大量CPU”

不,不会。因为我们做所有程序员都会做的事,所以在数学方面:
我们只是作弊。

弧长参数化,惰性方法

面对现实吧,我们不需要游戏中无止境的精度,对吗?因此,除非您在美国国家航空航天局工作并计划向人们发送火星,否则您将不需要0.000001 pixel完美的解决方案。

那么我们如何映射Tt?它很简单,仅包含3个步骤:

  1. N使用该位置计算曲线上的点t,并将该位置的arc-length(即曲线的长度)存储到数组中

  2. 要映射Tt,请先将T曲线的总长度乘以得到u,然后在长度数组中搜索小于的最大值的索引u

  3. 如果命中准确,则返回该索引处的数组值除以N,如果未在找到的点与下一个点之间插入一点,则再次将其除以N并返回。

就这样!现在,让我们看一下完整的代码:

function Bezier(a, b, c, d) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;

    this.len = 100;
    this.arcLengths = new Array(this.len + 1);
    this.arcLengths[0] = 0;

    var ox = this.x(0), oy = this.y(0), clen = 0;
    for(var i = 1; i <= this.len; i += 1) {
        var x = this.x(i * 0.05), y = this.y(i * 0.05);
        var dx = ox - x, dy = oy - y;        
        clen += Math.sqrt(dx * dx + dy * dy);
        this.arcLengths[i] = clen;
        ox = x, oy = y;
    }
    this.length = clen;    
}

这将初始化我们的新曲线并计算arg-lenghts,它还将最后的长度存储为total length曲线的,这里的关键因素this.len就是我们的N。映射越高,越精确,因为上面图片中的大小曲线100 points似乎就足够了,如果您只需要一个很好的长度估计,那么像25这样的事情已经可以完成,因为我们只需要1个像素例如,但是您的映射会不太精确,导致T映射到时分布不均t

Bezier.prototype = {
    map: function(u) {
        var targetLength = u * this.arcLengths[this.len];
        var low = 0, high = this.len, index = 0;
        while (low < high) {
            index = low + (((high - low) / 2) | 0);
            if (this.arcLengths[index] < targetLength) {
                low = index + 1;

            } else {
                high = index;
            }
        }
        if (this.arcLengths[index] > targetLength) {
            index--;
        }

        var lengthBefore = this.arcLengths[index];
        if (lengthBefore === targetLength) {
            return index / this.len;

        } else {
            return (index + (targetLength - lengthBefore) / (this.arcLengths[index + 1] - lengthBefore)) / this.len;
        }
    },

    mx: function (u) {
        return this.x(this.map(u));
    },

    my: function (u) {
        return this.y(this.map(u));
    },

实际的映射代码,首先我们binary search对存储的长度进行简单查找,找到小于的最大长度targetLength,然后返回或进行插值并返回。

    x: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
               + 3 * ((1 - t) * (1 - t)) * t * this.b.x
               + 3 * (1 - t) * (t * t) * this.c.x
               + (t * t * t) * this.d.x;
    },

    y: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
               + 3 * ((1 - t) * (1 - t)) * t * this.b.y
               + 3 * (1 - t) * (t * t) * this.c.y
               + (t * t * t) * this.d.y;
    }
};

再次t在曲线上计算。

取得成果的时间

替代文字

到现在为止mxmy您将T在曲线上均匀分布:)

不是那么难吗?再次证明,简单(尽管不是完美的解决方案)足以满足游戏需求。

如果您想查看完整的代码,可以使用Gist:https
//gist.github.com/670236

最后,加速船只

因此,现在剩下的就是通过映射T我们随后用来t在曲线上找到位置的位置来加速船只沿其路径行驶。

首先,我们需要两个运动方程,即ut + 1/2at²(v - u) / t

在实际代码中,如下所示:

startSpeed = getStartingSpeedInPixels() // Note: pixels
endSpeed = getFinalSpeedInPixels() // Note: pixels
acceleration = (endSpeed - startSpeed) // since we scale to 0...1 we can leave out the division by 1 here
position = 0.5 * acceleration * t * t + startSpeed * t;

然后我们将其缩小为0...1

maxPosition = 0.5 * acceleration + startSpeed;
newT = 1 / maxPosition * position;

到了那里,船只现在正在沿着这条小路平稳行驶。

万一它不起作用...

当您阅读本文时,一切都很好,但我最初在加速部分遇到了一些问题,当向Gamedev聊天室中的某人解释该问题时,我发现了我思考的最后一个错误。

如果您还没有忘记原始问题中的图片,我s在那儿提到,结果s就是速度(以度为单位),但船只沿路径以像素为单位移动,而我已经忘记了这一事实。因此,在这种情况下,我需要做的是将以度为单位的位移转换为以像素为单位的位移,事实证明这很简单:

function rotationToMovement(planetSize, rotationSpeed) {
    var r = shipAngle * Math.PI / 180;
    var rr = (shipAngle + rotationSpeed) * Math.PI / 180;
    var orbit = planetSize + shipOrbit;
    var dx = Math.cos(r) * orbit - Math.cos(rr) * orbit;
    var dy = Math.sin(r) * orbit - Math.sin(rr) * orbit;
    return Math.sqrt(dx * dx + dy * dy);
};

如此而已!谢谢阅读 ;)


7
这将需要一段时间才能消化。但是,哇,您自己的问题的惊人答案。
AttackingHobo 2010年

7
我注册了一个帐户只是为了支持这个答案
没人

有一些要点我的朋友。像魅力一样工作。问答均已批准。
杰斯

2
“ i”乘以0.05,而“ len”设置为100。这将“ t”映射为“ 0-5”而不是“ 0-1”。
邪恶活动

1
@EvilActivity是的,我也看到了,他的原始长度必须为20,然后忘记将0.05更改为0.01。因此,最好有一个动态的“ len”(适应于真实的弧长,或者甚至完全等于它),然后以“ 1 / len”来计算“步长”。我发现这很奇怪,这些年来没有其他人提出过!
比尔·科西亚斯

4

问题在于,一艘船不会自然地遵循这一轨迹。因此,即使它运行良好,它仍然看起来不正确。

如果要模拟行星之间的平滑过渡,我建议实际对其进行建模。方程非常简单,因为您只有两个很大的力:重力和推力。

您只需要设置常量:P1,P2的质量,船

每打一次游戏(时间:t),您就要做3件事

  1. 计算船上p1和船上p2的重力,将所得矢量加到推力矢量上。

  2. 根据第1步中的新加速度来计算新速度

  3. 根据您的新速度移动船

可能看起来需要做很多工作,但是可以用十几行代码完成,而且看起来很自然。

如果您需要物理方面的帮助,请告诉我。


我可能会考虑测试一下,是否可以在需要以下功能的情况下提供一种方法t:)
Ivo Wetzel 2010年

-但在游戏编程中,请勿将t用作变量。您基本上已经处于参数化情况,因为您只是在计算飞船的新dx和dy。这是一个使两个行星(在Flash中)旋转的示例 aharrisbooks.net/flash/fg2r12/twoPlanets.html-这与Python相同:aharrisbooks.net/pythonGame/ch09/twoPlanets.py
两次

2

我找到了一篇出色的文章,以用javascript编写的代码示例解释了此问题的可能解决方案。它通过将t值“推向”正确的方向来工作。

取而代之的是,我们可以利用以下事实:任何点分布的平均支腿长度d_avg与均匀分布的点将产生的支腿长度几乎相同(这种相似性随n的增加而增加)。如果我们计算实际腿长d与平均腿长d_avg之间的差d_err,则可以轻推与每个点相对应的时间参数t以减小该差。

这个问题已经有了很多很酷的答案,但是我发现这个解决方案值得关注。


1

感谢您提供出色的页面,描述了您如何解决此问题。我做了一个与您有些不同的事情,因为我深深地受内存限制:我不构建数组,也不必通过二进制搜索在数组中寻找正确的“段”。这是因为我一直都知道我正在从贝塞尔曲线的一端移到另一端:因此,我只记得“当前”段,并且如果我看到我将超出该段的范围来计算下一个位置,然后根据行驶方向计算下一个(或上一个)分段。这对于我的应用程序来说效果很好。我唯一需要解决的故障是,在某些曲线上,分段的大小是如此之小,以至于我下一个要指出的图是-在罕见的情况下-比当前分段要早一个分段,因此,不仅仅是到

不知道这是否合情合理,但这无疑对我有所帮助。


0

这种建模很奇怪,并且会产生奇怪的不合逻辑的结果。特别是如果起始行星的速度确实很慢。

用推力对船舶建模。

当船只在起始行星的最后轨道上时,以全推力加速。

当飞船到达一定距离时,请使用反向推力将飞船减速至目标行星的轨道速度。

编辑:当一个节点即将离开轨道时,立即进行整个模拟。要么发送所有数据,要么间隔发送一些运动矢量,然后在它们之间进行内插。


问题是,这全部基于刻度,没有中间位置。这是一款网络多人游戏,在完整游戏中发送600多艘战舰的所有位置将杀死所有网络。只有事件会传送tickOffset,其余事件是根据当前世界滴答和偏移量来计算的。
伊沃·韦策尔

我编辑了我的回复。
AttackingHobo 2010年

0

如果我理解正确,那么您的问题就太局限了。

我相信您希望太空飞船在某个时间t内沿着轨道之间的指定路径行进,并且还希望它在同一时间t内从速度s1加速到速度s2。不幸的是,您通常无法找到同时满足这两个约束的加速度。

您必须稍微放松一下问题才能使其解决。


2
那怎么放松呢?我能想象的是修改插入贝塞尔曲线路径中的T。我需要以某种方式对其进行缩放,以使其首先缓慢地增长到0.5,然后更快地增长到1。因此,船从原始速度减速到弯道中间的固定速度,然后再次从该速度加速到最后的速度曲线?
伊沃·韦策尔

1
我认为,如果飞船从其原始速度加速到转移中点附近的某个位置,然后减速到新的轨道,它将看起来更加现实。
加雷斯·里斯

不过,我仍然坚持如何将加速度插入整件事中,我需要以某种方式修改T:/
Ivo Wetzel 2010年


-1

可接受的解决方案存在的问题

作为贝塞尔曲线的指数函数,我们期望曲线不同区域的前进速度不同。

因为Ivo的解在这些初始指数样本之间线性插值,所以误差将严重偏向那些增量最大的(通常是三次)曲线的末端/中间。因此,除非像他所建议的那样大大提高采样率,否则错误对于在给定的情况下是显而易见的,并且在某个缩放级别上将始终是显而易见的,即,该算法固有的偏差是固有的。不适用于例如缩放可能不受限制的基于矢量的图形。NN

通过指导抽样来抵消偏差

另一种解决方案是在抵消Bezier函数产生的自然偏差之后线性重新映射distancet

假设这是我们理想中想要的:

curve length = 10

t      distance
0.2    2
0.4    4
0.6    6
0.8    8
1.0    10

但这是我们从Bezier位置函数得到的结果:

t      distance
0.2    0.12
0.4    1.22
0.6    2.45
0.8    5.81
1.0    10.00

通过查看N采样,我们可以看到距离增量最大的地方,并在两个相邻距离之间的中间重新采样(“拆分”),增加N1。例如,在处分裂t=0.9(在最大增量的中间),我们可以得到:

0.8    5.81
0.9    7.39
1.0    10.00

我们对下一个最大距离间隔重复此过程,直到整个集合中任意两个距离之间的最大增量小于某个值为止minDistanceDelta,更具体地说,小于与epsilon我们要映射到的特定距离的距离t。然后,我们可以将所需t步骤线性映射到相应distance的。这将生成一个哈希表/映射,您可以廉价地访问该哈希表/映射,并且可以在运行时无偏差地在其之间进行取值。

随着集合的增加N,重复此操作的成本会增加,因此理想情况下应将其作为预处理进行。每次N增加时,将两个新的结果间隔添加到intervals集合中,同时删除它们替换的旧的单个间隔。这是您用来寻找下一个最大间隔以一分为二的结构。保持intervals按距离排序使事情变得容易,因为您可以从末端弹出下一个工作项,然后拆分等。

我们最终得到了我们理想的东西:

epsilon: 0.01

t            distance
0.200417     2.00417
0.3998132    3.9998132
0.600703     6.00703
0.800001     8.00001
0.9995309    9.995309

由于我们的每一步走的猜测,我们不会得到确切的确切距离24等我们想要的,而是通过重复迭代这些得到足够接近所需的距离值,这样你就可以映射到您的t公平准确的步骤,消除由于偏到几乎相等的采样。

然后t=0.5,您可以像Ivo在其答案中那样进行检索,例如,通过在(3.99981326.00703)上两个最接近的值之间进行插值。

结论

在大多数情况下,Ivo的解决方案将很好地工作,但是对于必须不惜一切代价避免产生偏差的情况,请确保将您的distances分散得尽可能均匀,然后线性映射到t

请注意,拆分可以随机进行,而不是每次都拆分中间部分,例如,我们可能已将第一个示例间隔拆分为t=0.827而不是t=0.9

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.