我正在尝试基于我知道速度(v)和我要行驶的距离这一事实来计算requestFrame循环中的衰减或速度。我也知道每一帧的毫秒数。
所以一个简单的衰减算法是:
velocity *= 0.9
这会平滑且平稳地降低速度。但是我希望动画元素停止在给定位置(捕捉到网格)。那么如何准确计算出减速度?
答案 0 :(得分:4)
我必须承认不清楚在您的场景中是否有 1、2 或 3 个维度,我将谈论线性运动;请记住,在多维环境中,速度、加速度和距离是向量。
我会使用匀加速公式:
S = v0 * t + 1/2 * a * t ^ 2
vt = v0 + a * t
从你的问题看来,加速度和时间应该是问题的输出。
在你的问题中最后一件不清楚的是,在你说“我知道我想要旅行的距离”之前,你需要在网格上结束运动,这两句话似乎是相反的......我会处理这个在计算过程开始时舍入结束位置。
现在我们的问题有以下输入:
让我们开始用加速度的函数来表达时间(第二个公式)
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;
在时间过去后的第一帧上,要修复由于舍入而导致的错误,请将速度设置为零并将对象准确放置在网格上。
答案 1 :(得分:1)
对于衰减系数q
和n
步长(基本时间间隔),距离是几何级数的总和
D = v0 * (1-q**n) / (1-q)
我们可以使用简单的数值方法为给定的q
,D
和v0
找到n
(后者是已知的吗?)。
还要注意,速度永远不会为零,因此您可能必须使用一些阈值来停止。如果速度线性减小(恒定减速度)而不是指数减小,那么情况就更简单了。
答案 2 :(得分: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
:
我们知道 dv = -v0
因为最终速度是 0。
所以t = dt = -v0/a
将 t
代入第一个方程并求解 a
得到:
a = -0.5 * v0*v0 / (s1 - s0)
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>
答案 3 :(得分:1)
这更像是一个软件工程问题,而不是一个数学/物理问题。数学/物理非常简单。这里的难点是处理浏览器变化的帧/滴答率。对于由变化持续时间的离散时间步长提出的问题,数学/物理学不会太实用。
这是一些解决问题的代码;请注意,您可以单击“不稳定”以查看它在非常不稳定的帧/滴答率下的工作情况(您将在实现中看到这种延迟模拟是真实的!)
最好点击“整页”按钮:
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
增加 velocity
,然后 velocity
乘以某个 brakingFactor
,并且有一些要实现的目标 distance
,我们知道:
brakingFactor = 1 - constantSecsPerStep * initialVelocity / distance
这当然只有在 constantSecsPerStep
始终不变的情况下才有效。对于不同的 secsPerStep
,我使用了这个公式:
updatedBrakingFactor = 1 - durationOfCurrentTick * currentVelocity / remainingDistance
听起来您想要我称之为“纯粹”的解决方案,其中没有明确的“议程”来确定减速对象将捕捉到的位置(不应存在诸如“预期目的地”之类的数据)。不幸的是,我声称至少必须有一些数据来建立这个议程,并且该模型没有经历一些任意的运动。 updatedBrakingFactor
公式需要知道 remainingDistance
,而不是初始距离。需要有数据来推导(在代码中我决定存储模型的“开始位置”,但也可以使用“开始时间”)。
请注意,在数学上,模型的速度永远不会完全变为 0
- 因此需要启发式方法来近似模型何时“到达”。我选择等待瞬时速度低于某个小阈值。
答案 4 :(得分:1)
简短回答: a = e**(-v0*dt/d)
,其中 d
是您的距离,a
是衰减常数,dt
每帧时间和 {{ 1}} 初始速度。
为什么?
您给出的算法是指数衰减。如果要这样做,就不能使用this answer中的匀加速方程。
每帧 v0
处的隐式公式 v[n] = v[n-1] * a
(例如 a=0.9 和 v[0] = 1.0)可以明确写为 n
。或者在时间 v = v0*a**(n)
为 t
方面更好,其中 v = v0*a**(t/dt)
(每秒帧数)和 dt = 1/fps
(t = n*dt
)。
注意:这永远不会是 0!然而,行进对象仍然行进了有限的距离。
行进的距离 n = 1, 2, 3, ....
是该函数的积分:d
。一段时间后,对象将接近d = v0*dt * a**t / ln(a)
。求解 -v0*dt/ln(a)
给出了上述结果。
注意:这是一个分析结果,您的数字结果将接近该结果。
答案 5 :(得分: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
是整数时才会发生这种情况。否则物体会移动一段额外的距离,如果速度变为负值,你必须确保停止运动,你可以在最后一步修改速度来解决这个问题。
数学细节可以在my answer to that question中找到。