You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于前一点的直线距离高效求解贝塞尔曲线上的下一个点

贝塞尔曲线上小球保持等直线距离的优化方案

我需要让贝塞尔(Bézier)曲线上的小球之间保持相同的直线距离。目前我采用的是简单遍历曲线上可能位置的方案:每次迭代增加固定步长,直到找到距离大于等于要求值的位置。该方案可正常运行,但当小球数量较多时会占用大量计算资源。

原始遍历方案实现代码如下:

class TestScene extends Phaser.Scene {
  create() {
    // prettier-ignore
    const curve = new Phaser.Curves.QuadraticBezier(
      [
        55, 310,
        40, 0,
        250, 310
      ]
    );

    const graphicsLayer = this.add.graphics({
      lineStyle: {
        width: 2,
        color: 0x0000ff
      }
    });

    curve.draw(graphicsLayer);

    const balls = this.initBalls(6);

    this.curve = curve;
    this.balls = balls;
  }

  initBalls(quantity) {
    const balls = [];

    for (let i = 0; i < quantity; i++) {
      const ball = this.add.circle(0, 0, 12, 0x00ff00);
      ball.t = 0;

      balls.push(ball);
    }

    return balls;
  }

  calcNextBallT(previous) {
    const ballDiameter = previous.radius * 2;
    const curve = this.curve;
    const curveLen = curve.getLength();
    const previousPos = new Phaser.Math.Vector2(previous);
    const previousT = previous.t;

    const startingT = (1 / curveLen) * ballDiameter + previousT;

    const step = 1 / 1000;

    let nextT = startingT;
    let nextPos;
    let currentDistance = 0;
    while (nextT >= 0 && nextT <= 1) {
      nextPos = curve.getPointAt(nextT);
      currentDistance = previousPos.distance(nextPos);
      if (currentDistance >= ballDiameter) {
        break;
      }
      nextT += step;
    }

    return nextT;
  }

  update() {
    const {
      curve,
      balls
    } = this;
    if (!curve || !balls) {
      return;
    }

    const speed = 1;
    const curveLen = curve.getLength();
    const step = (1 / curveLen) * speed;

    balls.forEach((ball, index, array) => {
      let nextT = ball.t;

      if (index === 0) {
        nextT += step;
        if (nextT > 1) {
          nextT = 0;
        }
      } else {
        const previous = array[index - 1];
        nextT = this.calcNextBallT(previous);
      }

      ball.t = nextT;
      ball.copyPosition(curve.getPointAt(nextT));
    });
  }
}

const game = new Phaser.Game({
  width: 320,
  height: 320,
  powerPreference: 'low-power',
  audio: {
    noAudio: true
  },
  scene: [TestScene]
});
<script src="https://unpkg.com/phaser@3.55.2/dist/phaser.min.js"></script>

我认为可能存在数学或算法层面的更优解,类似求大圆与曲线的交点,但据我了解这种方法不具备可行性。请问有没有更好的方法,可以基于与前一点的直线距离确定贝塞尔曲线上的下一个点?

方案实测更新

经实测,二分查找(Binary Search)方案稳定性极佳,即使曲线形状和小球尺寸发生变化,平均也仅需5-10次迭代即可得到结果。优化后的实现代码如下:

class TestScene extends Phaser.Scene {
  init() {
    this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
      gameObject.setPosition(dragX, dragY);
    });

    this.iters = [];
  }

  create() {
    // prettier-ignore
    const p0 = this.add.circle(55, 310, 4, 0xffff00),
          p1 = this.add.circle(40, 5, 4, 0xffff00),
          p2 = this.add.circle(250, 310, 4, 0xffff00);

    const curve = new Phaser.Curves.QuadraticBezier(p0, p1, p2);

    const graphicsLayer = this.add.graphics({
      lineStyle: {
        width: 2,
        color: 0x0000ff
      }
    });
    curve.draw(graphicsLayer);

    const curveDragHandler = () => {
      curve.updateArcLengths();
      graphicsLayer.clear();
      curve.draw(graphicsLayer);
    };

    [p0, p1, p2].forEach((p) => {
      p.setDepth(10).setInteractive();
      this.input.setDraggable(p);
      p.on('drag', curveDragHandler);
    });

    const balls = this.initBalls(3, 50);

    const text = this.add.text(0, 0, '', {
      color: 'yellow'
    });

    this.curve = curve;
    this.balls = balls;
    this.text = text;
  }

  initBalls(quantity, diameter) {
    const radius = diameter / 2;
    const balls = [];

    for (let i = 0; i < quantity; i++) {
      const ball = this.add.circle(0, 0, radius, 0x00ff00);
      ball.t = 0;

      balls.push(ball);
    }

    return balls;
  }

  calcNextBallT(previous) {
    const ballDiameter = previous.radius * 2;
    const curve = this.curve;
    const previousPos = new Phaser.Math.Vector2(previous);
    const previousT = previous.t;

    let nextT = 1;
    let lowT = previousT;
    let highT = 1;
    let iter = 1;
    const skip = previousPos.distance(curve.getEndPoint()) <= ballDiameter;
    while (lowT <= highT && !skip) {
      nextT = lowT + (highT - lowT) / 2;
      const nextPos = curve.getPointAt(nextT);
      const currentDistance = previousPos.distance(nextPos);

      if (fuzzySame(currentDistance, ballDiameter)) {
        break;
      }

      if (currentDistance > ballDiameter) {
        highT = nextT;
      } else {
        lowT = nextT;
      }

      iter++;
    }

    if (!skip) {
      this.iters.push(iter);
    }

    return nextT;
  }

  update() {
    const {
      curve,
      balls
    } = this;
    if (!curve || !balls) {
      return;
    }

    const speed = 1;
    const curveLen = curve.getLength();
    const step = (1 / curveLen) * speed;

    balls.forEach((ball, index, array) => {
      let nextT = ball.t;

      if (index === 0) {
        nextT += step;
        if (nextT > 1) {
          const average = findAverage(this.iters).toFixed(2);
          const maximum = findMaximum(this.iters);
          this.text.setText(`Average: ${average}\nMaximum: ${maximum}`);
          this.iters = [];
          nextT = 0;
        }
      } else {
        const previous = array[index - 1];
        nextT = this.calcNextBallT(previous);
      }

      ball.t = nextT;
      ball.copyPosition(curve.getPointAt(nextT));
    });
  }
}

const fuzzySame = (num1, num2) => {
  const tolerance = 0.2;
  return num1 >= num2 - tolerance && num1 <= num2 + tolerance;
};

const findAverage = (array) => {
  const len = array.length;
  let total = 0;
  array.forEach((num) => (total += num));
  return total / len;
};

const findMaximum = (array) => {
  return [...array].sort((a, b) => b - a)[0];
};

const game = new Phaser.Game({
  width: 320,
  height: 320,
  powerPreference: 'low-power',
  audio: {
    noAudio: true
  },
  scene: [TestScene]
});
<script src="https://unpkg.com/phaser@3.55.2/dist/phaser.min.js"></script>

内容的提问来源于stack exchange,提问作者Egor Tyurin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 14:39:05