平面上非相交圆盘运动的路径生成

15

我在寻找什么

我有300个或更少半径相等的圆盘放置在平面上。在时间0,每个圆盘都在一个位置。在时间1,每个圆盘都可能在不同的位置上。我想生成每个圆盘在0到1之间的二维路径,使得圆盘之间不相交,路径相对高效(短)且弯曲度较小(例如,直线比蛇形线更可取)。

  • 通常而言,计算时间较短比解决方案的准确性更重要。(例如,有一些交叉是可以接受的,我不一定需要最优解)
  • 但是,圆盘不应该穿过彼此,停止或突然减速,或者突然改变方向——越“平滑”越好。唯一的例外是时间0和1。
  • 路径可以以采样形式或分段线性方式(或更好)表示——我不担心通过样条线实现真正光滑的路径。(如果需要,我可以近似)

我尝试了什么

您可以看到我的最佳尝试演示(通过Javascript + WebGL)。请注意,由于涉及计算,旧电脑加载速度会很慢。它似乎在Windows的Firefox / Chrome / IE11中工作。

在此演示中,我将每个圆盘表示为3D中的“弹性带”(即,每个圆盘在每个时间点上都有一个位置),并运行简单的游戏式物理引擎来解决约束,并将每个时刻的每个点视为具有到前/后一时刻的弹簧的质量。 (在这种情况下,“时间”只是第三个维度。)

实际上,这对小N(<20)非常有效,但在常见的测试用例中(例如,从圆形排列的盘子开始,将每个盘子移动到圆形上的相反点),由于约束和弹性传播缓慢,它无法生成令人信服的路径。(例如,如果我将时间分成100个离散级别,则弹性带中的张力每个模拟周期仅传播一级)这使得好的解决方案需要许多(>10000)迭代,并且这对我的应用程序来说非常缓慢。它也无法合理地解决许多N>40的情况,但这可能仅是因为我不能合理地运行足够的迭代次数。

我还尝试了什么

我的初始尝试是一个山岭爬行者,它从逐渐变异的直线路径开始。测量比当前最佳解决方案更好的解决方案将替换当前最佳解决方案。更好的测量结果来自交集的数量(即,完全重叠的测量结果比仅仅擦过的测量结果更差)以及路径长度(较短的路径更好)。

这产生了一些出乎意料的好结果,但不可靠,很可能经常陷入局部最小值。对于N>20非常缓慢。我尝试了一些技术(模拟退火,遗传算法等),以尝试解决局部最小值问题,但我从未获得太大的成功。

我正在尝试的

我正在优化“弹性带”模型,以便张力和约束在时间维度中更快地传播。在许多情况下,这将节省大量所需的迭代次数,但是在高度受限制的情况下(例如,许多圆盘试图穿过同一位置),仍然需要不可行的迭代次数。我不是解决约束或更快地传播弹簧的专家(我已经尝试阅读了一些关于不可伸缩布料模拟的论文,但我还没有弄清楚它们是否适用),因此我会对是否有好的方法感兴趣。

桌子上的想法

Spektre实现了一种非常快速的RTS风格单位移动算法,效果非常好。 它快速而优雅,但是它会遇到RTS运动风格的问题:突然的方向变化,单位可能会停止以解决碰撞。 另外,单位不会同时到达其目的地,这本质上是一个突然的停止。 这可能是一种良好的启发式方法,可以使平滑路径成为可行的路径,之后可以在时间上重新采样路径并运行“平滑”算法(就像我的演示中使用的算法一样)。
Ashkan Kzme建议问题可能与网络流相关。 似乎最小费用流问题可以工作,只要空间和时间能够以合理的方式离散化,并且运行时间可以保持下降。 这里的优点是它是一组经过充分研究的问题,但是突然的速度变化仍然是一个问题,因此可能需要某种“平滑”后处理步骤。我目前遇到的障碍是决定时空网络表示法,不能导致圆盘互相传送。
Jay Kominek发布了一个答案,使用非线性优化器来优化二次Bezier曲线,取得了一些有希望的结果。

嗯,你希望使用哪种运行时? - Jay Kominek
@Jay Kominek,最好能够以“实时”速度 - 用户修改开始/结束配置后,计算出的路径可以可视化。稍微宽容一些,如果我可以在不到半秒钟的时间内生成一个解决方案,我可以接受。话虽如此,我会尽力而为 - 可能不可能在半秒钟内合理地实现任何这些目标。 - Kaganar
我添加了一个网络演示,以供参考。 (请参见“我尝试过的内容”部分。) - Kaganar
1
@Kaganar 或许我没有完全理解你的问题,但你能否使用最大独立路径算法来解决,这需要使用网络流算法。如果你在模拟图中找到足够的独立路径,那么就可以了,当然前提是你的模型中存在明确的路径。 - Ashkan Kazemi
@Ashkan Kzme,我在寻找名为“maximum separate paths”的算法或主题方面遇到了一些问题。您能否澄清您的想法? - Kaganar
显示剩余13条评论
4个回答

7

我为了好玩尝试了一下,以下是结果:

算法:

  1. 处理每个圆盘
  2. 将速度设置为 constant*destination_vector
    • 乘法常数 a
    • 然后将速度限制在常数 v 之内
  3. 测试新迭代位置是否与其他圆盘冲突
  4. 如果有冲突,则将速度旋转一个角度步长 ang
  5. 循环直到找到自由方向或完整覆盖圆周
  6. 如果没有找到自由方向,则标记圆盘为卡住状态

    这是圆形到反向圆形路径的样子:

    example1

    这是随机到随机路径的样子:

    example2

    卡住的圆盘为黄色(在这些情况下没有)而不动的圆盘已经到达目的地。 如果没有路径,例如如果圆盘已经在另一个圆盘的目的地上,也会卡住。为了避免这种情况,您还需要更改发生碰撞的圆盘... 您可以调整 ang,a,v 常数以使其外观不同,并且还可以尝试随机旋转角度方向以避免涡流/漩涡运动

以下是我使用的源代码(C ++):

//---------------------------------------------------------------------------
const int    discs =23;     // number of discs
const double disc_r=5;      // disc radius
const double disc_dd=4.0*disc_r*disc_r;
struct _disc
    {
    double x,y,vx,vy;       // actual position
    double x1,y1;           // destination
    bool _stuck;            // is currently stuck?
    };
_disc disc[discs];          // discs array
//---------------------------------------------------------------------------
void disc_generate0(double x,double y,double r)     // circle position to inverse circle destination
    {
    int i;
    _disc *p;
    double a,da;
    for (p=disc,a=0,da=2.0*M_PI/double(discs),i=0;i<discs;a+=da,i++,p++)
        {
        p->x =x+(r*cos(a));
        p->y =y+(r*sin(a));
        p->x1=x-(r*cos(a));
        p->y1=y-(r*sin(a));
        p->vx=0.0;
        p->vy=0.0;
        p->_stuck=false;
        }
    }
//---------------------------------------------------------------------------
void disc_generate1(double x,double y,double r)     // random position to random destination
    {
    int i,j;
    _disc *p,*q;
    double a,da;
    Randomize();
    for (p=disc,a=0,da=2.0*M_PI/double(discs),i=0;i<discs;a+=da,i++,p++)
        {
        for (j=-1;j<0;)
            {
            p->x=x+(2.0*Random(r))-r;
            p->y=y+(2.0*Random(r))-r;
            for (q=disc,j=0;j<discs;j++,q++)
             if (i!=j)
              if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
               { j=-1; break; }
            }
        for (j=-1;j<0;)
            {
            p->x1=x+(2.0*Random(r))-r;
            p->y1=y+(2.0*Random(r))-r;
            for (q=disc,j=0;j<discs;j++,q++)
             if (i!=j)
              if (((q->x1-p->x1)*(q->x1-p->x1))+((q->y1-p->y1)*(q->y1-p->y1))<disc_dd)
               { j=-1; break; }
            }
        p->vx=0.0;
        p->vy=0.0;
        p->_stuck=false;
        }
    }
//---------------------------------------------------------------------------
void disc_iterate(double dt)                    // iterate positions
    {
    int i,j,k;
    _disc *p,*q;
    double v=25.0,a=10.0,x,y;
    const double ang=10.0*M_PI/180.0,ca=cos(ang),sa=sin(ang);
    const int n=double(2.0*M_PI/ang);
    for (p=disc,i=0;i<discs;i++,p++)
        {
        p->vx=a*(p->x1-p->x); if (p->vx>+v) p->vx=+v; if (p->vx<-v) p->vx=-v;
        p->vy=a*(p->y1-p->y); if (p->vy>+v) p->vy=+v; if (p->vy<-v) p->vy=-v;
        x=p->x; p->x+=(p->vx*dt);
        y=p->y; p->y+=(p->vy*dt);
        p->_stuck=false;
        for (k=0,q=disc,j=0;j<discs;j++,q++)
         if (i!=j)
          if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
            {
            k++; if (k>=n) { p->x=x; p->y=y; p->_stuck=true; break; }
            p->x=+(p->vx*ca)+(p->vy*sa); p->vx=p->x;
            p->y=-(p->vx*sa)+(p->vy*ca); p->vy=p->y;
            p->x=x+(p->vx*dt);
            p->y=y+(p->vy*dt);
            j=-1; q=disc-1;
            }
        }
    }
//---------------------------------------------------------------------------

使用方法很简单:

  1. 使用你要放置圆盘的平面的中心和半径调用 generate0/1
  2. 调用 iterate(dt 是经过的时间,以秒为单位)
  3. 绘制场景

如果你想改成使用 t=<0,1>

  1. 循环迭代直到所有圆盘到达目标位置或超时
  2. 记住每个圆盘速度变化的列表 需要位置或速度向量以及发生变化的时间
  3. 循环结束后将所有圆盘列表重新缩放到 <0,1> 范围内
  4. 渲染/动画显示重新缩放的列表

[注]

我的测试是实时运行的,但我没有应用 <0,1> 范围,并且圆盘数量也不多。因此您需要测试一下这是否足够快。

加速的方法:

  • 增加角度步长
  • 旋转后针对最后相撞的圆盘进行碰撞检测,只有在空闲时才检测其余圆盘……
  • 将圆盘分成(重叠的)区域,分别处理每个区域
  • 我认为某些场景方法可以加速,例如定期创建场地图以更好地确定避免障碍的方向

[编辑1] 一些调整以避免在障碍物周围无限振荡

对于更多的圆盘,有些圆盘会被卡在已经停止的圆盘周围弹跳。为了避免这种情况,只需偶尔改变 ang 步骤方向,就可以得到以下结果:

exampe3

你可以看到结束前的振荡弹跳

这是修改后的源代码:

void disc_iterate(double dt)                    // iterate positions
    {
    int i,j,k;
    static int cnt=0;
    _disc *p,*q;
    double v=25.0,a=10.0,x,y;
    const double ang=10.0*M_PI/180.0,ca=cos(ang),sa=sin(ang);
    const int n=double(2.0*M_PI/ang);
    // process discs
    for (p=disc,i=0;i<discs;i++,p++)
        {
        // compute and limit speed
        p->vx=a*(p->x1-p->x); if (p->vx>+v) p->vx=+v; if (p->vx<-v) p->vx=-v;
        p->vy=a*(p->y1-p->y); if (p->vy>+v) p->vy=+v; if (p->vy<-v) p->vy=-v;
        // stroe old and compute new position
        x=p->x; p->x+=(p->vx*dt);
        y=p->y; p->y+=(p->vy*dt);
        p->_stuck=false;
        // test if coliding
        for (k=0,q=disc,j=0;j<discs;j++,q++)
         if (i!=j)
          if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
            {
            k++; if (k>=n) { p->x=x; p->y=y; p->_stuck=true; break; }   // if full circle covered? stop
            if (int(cnt&128))   // change the rotation direction every 128 iterations
                {
                // rotate +ang
                p->x=+(p->vx*ca)+(p->vy*sa); p->vx=p->x;
                p->y=-(p->vx*sa)+(p->vy*ca); p->vy=p->y;
                }
            else{
                //rotate -ang
                p->x=+(p->vx*ca)-(p->vy*sa); p->vx=p->x;
                p->y=+(p->vx*sa)+(p->vy*ca); p->vy=p->y;
                }
            // update new position and test from the start again
            p->x=x+(p->vx*dt);
            p->y=y+(p->vy*dt);
            j=-1; q=disc-1;
            }
        }
    cnt++;
    }

@Kaganar 完成编辑,附注:常量设置为视图 256x256 像素和圆盘半径 = 5 像素。 - Spektre
这个有很多优点:实际的代码,快速,易于实现。这遵循了Strilanc的建议——看看RTS游戏如何处理单位移动。对于RTS游戏来说,单位完全停止并不一定是不可取的,到达时间差异很大。然而,这些属性在这个问题上并不适用,因为速度和方向往往会突然改变。话虽如此,可以运行该算法,然后将我的演示中的优化步骤应用于帮助平滑路径。我已编辑问题以反映这个答案。 - Kaganar

4

这并不完美,但我最好的想法是沿着二次贝塞尔曲线移动光盘。这意味着每个光盘只有2个自由变量,您需要为其找到值。

此时,您可以将误差函数“插入”非线性优化器。您愿意等待的时间越长,解决方案就越好,就光盘避免彼此碰撞而言。

只有一个实际命中:

enter image description here

不必显示命中次数,实际上光盘开始重叠:

enter image description here

我已经制作了一个完整的示例,但关键是要最小化的误差函数,我在这里重复一下:

double errorf(unsigned n, const double *pts, double *grad,
              void *data)
{
  problem_t *setup = (problem_t *)data;
  double error = 0.0;

  for(int step=0; step<setup->steps; step++) {
    double t = (1.0+step) / (1.0+setup->steps);
    for(int i=0; i<setup->N; i++)
      quadbezier(&setup->starts[2*i],
                 &pts[2*i],
                 &setup->stops[2*i],
                 t,
                 &setup->scratch[2*i]);

    for(int i=0; i<setup->N; i++)
      for(int j=i+1; j<setup->N; j++) {
        double d = distance(&setup->scratch[2*i],
                            &setup->scratch[2*j]);
        d /= RADIUS;
        error += (1.0/d) * (1.0/d);
      }
  }

  return error / setup->steps;
}

忽略ngraddatasetup描述了被优化的具体问题,圆盘数量以及它们的起点和终点。 quadbezier进行Bezier曲线插值,并将其答案放入->scratch中。我们在路径上检查->steps个点,并测量每个步骤时圆盘彼此之间的距离。为使优化问题更加平滑,当圆盘开始接触时,它不会有一个硬性开关,而只是尽可能让它们彼此保持远离。

完全可编译的代码、Makefile以及一些Python代码可以将一堆二次Bezier曲线转换为一系列图像,可在https://github.com/jkominek/discs获取。

在大量点上性能有些缓慢,但有许多改进选项。

  1. 如果用户只是对起点和终点进行微小的调整,则在每次调整后,使用先前的解作为新的起点,在后台重新运行优化。修复接近的解应该比每次从头开始重新创建更快。
  2. 并行化所有点上的 n^2 循环。
  3. 检查其他优化算法是否可以更好地处理这些数据。现在它会先进行全局优化,然后进行局部优化。有些算法已经“知道”如何做到这一点,并且可能更聪明。
  4. 如果您能够免费或接近计算梯度函数,我相信这样做是值得的,并切换到可以利用梯度信息的算法。即使梯度不便宜,这也可能是值得的。
  5. 用一个子优化替换整个步骤,找到两个圆盘最靠近的位置的 t,然后使用该距离作为误差。计算该子优化的梯度应该更容易。
  6. 更好的中间点数据结构,这样您就不必为相距很远的圆盘执行大量不必要的距离计算。
  7. 可能还有更多?

看起来很有趣,但你的例子中碰撞/相交了这些圆盘。 - Spektre
只需要给它一小部分秒数,而不是几毫秒,它就能在一次命中中找到第一个问题的解决方案。第二个例子中,圆盘开始重叠,这是演示它完全处理退化输入而不会出现奇怪或特殊情况的证明。这保证了平滑性,无需任何后处理步骤(这也可能要尝试避免引入交叉点)。 - Jay Kominek
到目前为止,这可能是我遇到的最有趣的方法,因为它在某种程度上满足了我所有的偏好。我不确定我会选择贝塞尔曲线,但总体方法仍然适用。 - Kaganar

3
这种问题的常规解决方案是使用所谓的“热图”(或“影响图”)。对于场中的每个点,您都要计算一个“热值”。圆盘朝高值移动,远离低值。热图非常适合您的问题类型,因为它们非常简单易用,却可以生成复杂的类似人工智能的行为。
例如,想象只有两个圆盘。如果您的热图规则是等辐射线的,则圆盘将相互靠近,然后再次分开,来回振荡。如果您的规则随机强度在不同的辐射线上变化,则行为将是混乱的。您还可以使规则依赖于速度,此时圆盘将加速和减速移动。
一般来说,热图规则应该使接近圆盘的区域变得“更热”。距离圆盘太近或太远的地方会变得“更冷”。通过改变这个最佳距离,您可以确定圆盘聚集在一起的距离。
以下是一些示例代码的文章,展示如何使用热图:

http://haufler.org/2012/05/26/beating-the-scribd-ai-challenge-implementing-traits-through-heuristics-part-1/

http://www.gamedev.net/page/resources/_/technical/artificial-intelligence/the-core-mechanics-of-influence-mapping-r2799

游戏AI专家,第二卷,热力图章节。

0

我还没有足够的声望来评论,所以对于这个非答案我很抱歉。 但是关于实时战略游戏(RTS)方面,通常会使用A*算法进行路径寻找。你坚持使用基于物理的模型有什么原因吗?

其次,你提供的链接中的尝试运行相当顺利,但在中间的加速度部分表现出了我最初的想法。由于你的模型将其视为橡皮筋,它基本上在寻找旋转的最短路径以达到目标位置。

如果你不担心物理方法,我建议尝试以下方法: 尝试直接朝目标移动。如果发生碰撞,它应该尝试顺时针绕过最近的碰撞,直到位于从当前位置到目标位置的向量的垂直方向上。

假设我们做一个测试案例,在盒子的顶部有5个连续的物体,底部也有5个连续的物体,它们会直接朝彼此移动,直到发生碰撞。整个顶部一行将向右滑动,直到它们掉落到底部一行的边缘上,而底部一行则向左移动并浮在顶部一行的边缘上。(想象一下威士忌和水杯的魔术把戏开始时的样子)

由于运动不是由弹簧中储存的势能决定的,这会在旋转过程中加速物体,因此您可以完全控制模拟过程中速度的变化。

在像上面那样的圆形测试中,如果所有盘片都以相同的速度初始化,则整个团块将向中心移动,碰撞并作为一个单位扭曲约四分之一圈,然后它们将分开并朝着各自的目标前进。

如果时间轻微随机化,我认为您会得到所需的行为。

希望这可以帮到您。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接