如何基于前一点的直线距离高效求解贝塞尔曲线上的下一个点
贝塞尔曲线上小球保持等直线距离的优化方案
我需要让贝塞尔(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
相关产品推荐
相关产品推荐

