基于速度和网格计算减速。

9

我正在尝试计算基于requestFrame循环中已知速度(v)和需要行驶的距离的衰减或速度。我也知道每个帧的毫秒数(ms)。

因此,一个简单的衰减算法是:

velocity *= 0.9

这会平稳而漂亮地减速,但我希望动画元素在给定位置停止(对齐到网格)。那么我该如何精确计算减速?


我的回答不清楚吗? - MBo
有多少维?一个还是多个? - Daniele Ricci
6个回答

4

对于衰减系数 qn 步(基本时间间隔),距离是几何级数的和。

D = v0 * (1 - q ** n) / (1 - q)

我们可以使用简单的数值方法来找到给定的 Dv0n(后者是否已知)的 q。请注意,速度永远不会变为零,因此您可能需要使用一些阈值来停止。如果速度线性减小(恒定减速)而不是指数减小,则事情会更简单。

“n” 无法确定 - 没有任何 JS 环境能够保证在一个时间间隔内会发生多少次渲染。 - Gershom Maes

4

我必须承认,在您的场景中是否存在1、2或3个维度并不清楚,我将谈论线性运动;请记住,在多维环境中,速度、加速度和距离都是向量。

我将使用均匀加速度公式:

S = v0 * t + 1/2 * a * t ^ 2

vt = v0 + a * t

从您的问题中看来,加速度和时间应该是问题的输出。

最后一个不清楚的问题是,在您之前说“我知道我想要旅行的距离”,后来您需要移动到网格上结束,这两个句子似乎矛盾......我会在计算过程的开始处处理这种四舍五入的结束位置。

现在我们的问题具有以下输入:

  1. D(距离),已知
  2. v0(初始速度),已知
  3. vt(结束速度),已知:0
  4. dt(增量时间),已知:两个连续帧之间的时间,以秒为单位(而不是毫秒)

让我们开始根据加速度表示时间(第二个公式)

t =(vt- v0)/ a

vt = 0,因此

t = - v0 / a

让我们在第一个公式中替换它

S = - v0 ^ 2 / a + 1/2 * a (- v0 / a) ^ 2 = -(v0 ^ 2)/(2 * a)

从这里,我们可以找到加速度

a = -(v0 ^ 2)/(2 * S)

从第二个公式得出时间

t = - v0 / a

正如我们所说,在过程开始时,我们需要将距离舍入为与网格对齐的位置:

rD = roundDistanceToGrid(D);
a = - velocity * velocity / 2 / rD;
t = - velocity / a;

t 不是 dt 的整数倍。

从现在开始,直到经过的时间小于 t,每个帧只需执行以下操作:

velocity += a * dt;

在时间到达后的第一帧中,为了修正由于四舍五入造成的误差,请将速度设为零,并将物体准确地放置在网格上。


运用物理学,这才是真正的解决方案。 - duffymo
我认为这需要连续帧之间的时间是某个恒定值,但事实并非如此。 - Gershom Maes

2
简短回答:a = e**(-v0*dt/d),其中d是你的距离,a是衰减常量,dt是每帧的时间,v0是初始速度。
为什么?所给的算法是指数衰减。如果您想以这种方式进行操作,则不能使用此答案中的均匀加速方程。 在每个帧n处隐式表达式v[n] = v[n-1] * a(例如,当a = 0.9和v[0] = 1.0时)可以明确地写为v = v0*a**(n)。或者更好地用时间t表示为v = v0*a**(t/dt),其中dt = 1/fps(每秒帧数),t = n*dt(n = 1, 2, 3, ....)。 请注意:这永远不会是0!但是,行进物体仍会行进一定距离。 行进距离d是该函数的积分:d = v0*dt * a**t / ln(a)。经过一段时间后,物体将接近于-v0*dt/ln(a)。解出a得到上述结果。
注:这是一个解析结果,您的数字结果将在其附近。

2
这更多是一个软件工程问题,而不是数学/物理问题。数学/物理问题相当简单。这里的难点在于处理浏览器变化的帧/刻度率。对于由不同持续时间的离散时间步骤引发的问题,数学/物理实际上并不适用。
以下是解决此问题的一些代码;请注意,您可以单击“destabilize”以在非常不稳定的帧/刻度率下查看其工作(您将在实现中看到此时模拟延迟是真实的!)
最好点击“全屏”按钮:

let elem = document.querySelector('.model');
let rangeElem = document.querySelector('.range');
let fpsElem = document.querySelector('.fps');
let destabilizeElem = document.querySelector('.destabilize');

destabilizeElem.addEventListener('click', evt => {
  destabilizeElem.classList.toggle('active');
  evt.stopPropagation();
  evt.preventDefault();
});

let model = {
  pos: [ 0, 0 ],
  vel: [ 0, 0 ],
  startPos: [ 0, 0 ],
  range: 100
};
let reset = ({ startMs, range, vel, ang=0 }) => {
  
  // Start again with `range` representing how far the model
  // should travel and `vel` representing its initial speed.
  // We will calculate `velMult` to be a value multiplied
  // against `vel` each frame, such that the model will
  // asymptotically reach a distance of `range`
    
  let [ velX, velY ] = [ Math.sin(ang) * vel, Math.cos(ang) * vel ];
  
  // Note the box-shadow on `rangeElem` is 2px wide, so to
  // see the exact range it represents we should subtract
  // half that amount. This way the middle of the border
  // truly represents a distance of `range`!
  rangeElem.style.width = rangeElem.style.height = `${(range - 1) << 1}px`;
  rangeElem.style.marginLeft = rangeElem.style.marginTop = `-${range - 1}px`;
  elem.transform = 'translate(0, 0)';

  model.pos = [ 0, 0 ];
  model.vel = [ velX, velY ];
  model.startPos = [ 0, 0 ];
  model.range = range;
  
};

let ms = performance.now();
let frame = () => {
  
  let prevFrame = ms;
  let dms = (ms = performance.now()) - prevFrame;
  let dt = dms * 0.001;
  
  elem.style.transform = `translate(${model.pos[0]}px, ${model.pos[1]}px)`;
  
  // Now `velMult` is different every frame:
  let velMag = Math.hypot(...model.vel);
  let dx = model.pos[0] - model.startPos[0];
  let dy = model.pos[1] - model.startPos[1];
  let rangeRemaining = model.range - Math.hypot(dx, dy);
  let velMult = 1 - Math.max(0, Math.min(1, dt * velMag / rangeRemaining));
  
  model.pos[0] += model.vel[0] * dt;
  model.pos[1] += model.vel[1] * dt;
  model.vel[0] *= velMult;
  model.vel[1] *= velMult;
  
  fpsElem.textContent = `dms: ${dms.toFixed(2)}`;
  
  // Reset once the velocity has multiplied nearly to 0
  if (velMag < 0.05) {
    reset({
      startMs: ms,
      
      // Note that without `Math.round` results will be *visually* inaccurate
      // This is simply a result of css truncating floats in some cases
      range: Math.round(50 + Math.random() * 300),
      vel: 600 + Math.random() * 1200,
      ang: Math.random() * 2 * Math.PI
    });
  }
    
};
(async () => {
  while (true) {
    await new Promise(r => window.requestAnimationFrame(r));
    if (destabilizeElem.classList.contains('active')) {
      await new Promise(r => setTimeout(r, Math.round(Math.random() * 100)));
    }
    frame();
  }
})();
html, body {
  position: absolute;
  left: 0; right: 0; top: 0; bottom: 0;
  overflow: hidden;
}
.origin {
  position: absolute;
  overflow: visible;
  left: 50%; top: 50%;
}
.model {
  position: absolute;
  width: 30px; height: 30px;
  margin-left: -15px; margin-top: -15px;
  border-radius: 100%;
  box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.8);
}
.model::before {
  content: ''; position: absolute; display: block;
  left: 50%; top: 50%;
  width: 4px; height: 4px; margin-left: -2px; margin-top: -2px;
  border-radius: 100%;
  box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.8);
}
.range {
  position: absolute;
  width: 100px; height: 100px;
  margin-left: -50px; margin-top: -50px;
  border-radius: 100%;
  box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.5);
}
.fps {
  position: absolute;
  right: 0; bottom: 0;
  height: 20px; line-height: 20px;
  white-space: nowrap; overflow: hidden;
  padding: 10px;
  font-family: monospace;
  background-color: rgba(0, 0, 0, 0.1);
}
.destabilize {
  position: absolute;
  right: 0; bottom: 45px;
  height: 20px; line-height: 20px;
  white-space: nowrap; overflow: hidden;
  padding: 10px;
  font-family: monospace;
  box-shadow: inset 0 0 0 4px rgba(0, 0, 0, 0.1);
  cursor: pointer;
}
.destabilize.active { box-shadow: inset 0 0 0 4px rgba(255, 130, 0, 0.9); }
<div class="origin">
  <div class="model"></div>
  <div class="range"></div>
</div>
<div class="destabilize">Destabilize</div>
<div class="fps"></div>

这里的技巧是实时地根据帧速率调整制动。
在一个离散模型中,每隔secsPerStep后,position会增加velocityvelocity再乘以一定的brakingFactor,并且存在某个目标distance需要达到,则我们知道:
brakingFactor = 1 - constantSecsPerStep * initialVelocity / distance

当然,这仅适用于constantSecsPerStep始终保持恒定的情况。对于变化的secsPerStep,我使用了以下公式:
updatedBrakingFactor = 1 - durationOfCurrentTick * currentVelocity / remainingDistance

听起来你想要一个我称之为“纯净”的解决方案,其中没有明确的“议程”来确定减速物体将捕捉到的位置(不应存在任何数据,例如“预期目的地”)。不幸的是,我认为必须有一些数据来建立这个议程,并且该模型不会经历任意移动。更新的updatedBrakingFactor公式需要了解remainingDistance而不是初始距离。需要有数据来推导这一点(在代码中,我决定存储模型的“起始位置”,但也可以使用“起始时间”)。
请注意,在数学上,模型的速度永远不会完全变为0 - 因此需要一种启发式方法来近似模型何时“到达”。我选择等待瞬时速度降至某个小阈值。

1

简短回答:

d = 99  // Distance  
v = 11  // Velocity

// Negative acceleration is deceleration:
acceleration = -0.5 * v * v / (d - 0.5 * v)

推导:

从具有恒定加速度的运动方程开始:

s1 = s0 + v0 + a*t*t/2

加速度方程:

a = dv/dt (change in velocity over change in time)

并解出a:

  1. 我们知道dv = -v0,因为最终速度为0。

  2. 所以t = dt = -v0/a

  3. t代入第一个方程式,并解出a

    a = -0.5 * v0*v0 / (s1 - s0)

  4. s1 - s0就是行驶距离d。由于某种原因,我必须从d中减去一半的速度才能得到正确的结果....


模拟证明: 您可以尝试在下面的模拟器中输入不同的速度和距离。

  • 请注意,最终位置会有一点误差,因为方程假设持续运动(时间步长非常小),但requestFrame将导致相对较大的时间步长。
  • 出于同样的原因,加速度必须仅在运动开始时计算并保存。我曾尝试在每帧重新计算加速度,但由于达到最终位置时舍入/模拟误差过大,因此无法实现。

function run() {
  console.log('Simulating:')
   
  d = getNumber('d')  // Distance
  v = getNumber('v')  // Velocity

  p = 0  // Position
  a = -0.5 * v * v / (d - 0.5 * v) // Acceleration
  
  log = [{p, v, a}]
  
  while (v > 0) {
    p = p + v;
    d = d - p;
    v = v + a;
    data = {p, v, a};
    console.log(data) // For StackOverflow console
    log.push(data)
  }
  console.table(log); // For browser dev console
}

function getNumber(id) {
  return Number(document.getElementById(id).value)
}
<div>Distance <input id=d value=10 /></div>
<div>Velocity <input id=v value=1 /></div>
<div><button onclick='run()'>Run Simulation (Open dev console first to get full data in a nicely formatted table)</button></div>


0

您可以使用您提出的公式velocity *= r来实现您的目标。然而,理论上,它将需要无限的时间才能使您的物体行驶您想要的距离d,因为速度永远不会真正达到零,使用连续乘法。实际上,它将在达到您的值可以考虑大于零的最低值之后达到零,但这也需要很长时间。
要获得所需的值r,从速度V0开始,并假设您的帧的时间间隔为ms,可以计算出值r

r = 1 - V0 * ms / D;

还有另一种选择,即通过每帧减少一个常量值dv来降低速度,可以计算出该值:

dv = ms * Math.pow(V0, 2) / (2 * D - ms * V0);

第二种情况下的行驶距离可能并不总是D,只有当值2 * D / ms / V0为整数时才会发生。否则,物体将行驶额外的距离,并且您必须确保在速度变为负数时停止运动,您可以在最后一步对速度进行修改以解决此问题。

数学细节可以在我的回答中找到。


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