Дві речі важливі для того, щоб рух виглядав плавним, перше - це очевидно, що те, що ви надаєте, має відповідати очікуваному стану в момент, коли кадр представлений користувачеві, друге - потрібно представити користувачеві кадри. через відносно фіксований інтервал. Представляючи кадр у T + 10ms, потім інший у T + 30ms, потім інший у T + 40ms, користувачеві з’являться користувачі, які оцінюють, навіть якщо те, що насправді показано на той час, є правильним відповідно до моделювання.
Здається, у вашої основної петлі не існує жодного механізму решітки, щоб переконатися, що ви здійснюватиметесь лише через рівні проміжки часу. Тому іноді ви можете робити 3 оновлення між рендерами, іноді ви можете робити 4. В основному ваш цикл буде відображатися якомога частіше, як тільки ви змоделюєте достатньо часу, щоб просунути стан моделювання перед поточним часом, потім надайте цю державу. Але будь-яка змінність часу, необхідного для оновлення чи візуалізації, і інтервал між кадрами також буде різнитися. У вас є фіксований часовий крок для моделювання, але мінливий часовий крок для візуалізації.
Можливо, вам потрібно зачекати безпосередньо перед вашим візуалізацією, що гарантує вам лише коли-небудь розпочати візуалізацію на початку інтервалу візуалізації. В ідеалі це повинно бути адаптивним: якщо для оновлення / візуалізації вам знадобилося занадто багато часу, і початок інтервалу вже минув, вам слід зробити візуалізацію негайно, але також збільшити довжину інтервалу, поки ви не зможете послідовно візуалізувати та оновити і все-таки дістатися до наступне візуалізація до закінчення інтервалу. Якщо у вас вистачає часу на запас, ви можете повільно скоротити інтервал (тобто збільшити частоту кадрів), щоб знову зробити швидше.
Але, і ось ось запускає, якщо ви не візуалізуєте кадр одразу після виявлення того, що стан моделювання було оновлено до "зараз", тоді ви введете тимчасове псевдонім. Кадр, що подається користувачеві, подається трохи не в той час, і сам по собі буде відчувати себе заїканням.
Це причина "часткового кроку", який ви побачите у згаданих статтях. Це там з поважної причини, і це тому, що, якщо ви не зафіксуєте часовий крок фізики до якогось фіксованого інтеграла, кратного вашого фіксованого часового кроку візуалізації, ви просто не можете представити кадри в потрібний час. Ви в кінцевому підсумку або представляєте їх занадто рано, або занадто пізно. Єдиний спосіб отримати фіксовану швидкість візуалізації та все-таки представити щось фізично правильне - це визнати, що в момент, коли інтервал візуалізації настає, ви, швидше за все, будете посередині між двома вашими фіксованими часовими кроками фізики. Але це не означає, що об'єкти змінюються під час надання, просто, що візуалізація повинна тимчасово встановити, де розташовані об'єкти, щоб він міг відображати їх десь між тим, де вони були раніше, і де вони знаходяться після оновлення. Це важливо - ніколи не змінюйте світову державу для надання, лише оновлення повинні змінювати світову державу.
Отже, щоб помістити його в цикл псевдокодів, я думаю, вам потрібно щось більше, як:
InitialiseWorldState();
previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval
subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame
while (true)
{
frameStart = ActualTime();
//Render the world state as if it was some proportion
// between previousTime and currentTime
// E.g. if subFrameProportion is 0.5, previousTime is 0.1 and
// currentTime is 0.2, then we actually want to render the state
// as it would be at time 0.15. We'd do that by interpolating
// between movingObject.previousPosition and movingObject.currentPosition
// with a lerp parameter of 0.5
Render(subFrameProportion);
//Check we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
}
expectedFrameEnd = frameStart + renderInterval;
//Loop until it's time to render the next frame
while (ActualTime() < expectedFrameEnd)
{
//step the simulation forward until it has moved just beyond the frame end
if (previousTime < expectedFrameEnd) &&
currentTime >= expectedFrameEnd)
{
previousTime = currentTime;
Update();
currentTime += fixedTimeStep;
//After the update, all objects will be in the position they should be for
// currentTime, **but** they also need to remember where they were before,
// so that the rendering can draw them somewhere between previousTime and
// currentTime
//Check again we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
expectedFrameEnd = frameStart + renderInterval
}
}
else
{
//We've brought the simulation to just after the next time
// we expect to render, so we just want to wait.
// Ideally sleep or spin in a tight loop while waiting.
timeTillFrameEnd = expectedFrameEnd - ActualTime();
sleep(timeTillFrameEnd);
}
}
//How far between update timesteps (i.e. previousTime and currentTime)
// will we be at the end of the frame when we start the next render?
subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}
Для цього всі об’єкти, що оновлюються, повинні зберегти знання про те, де вони були раніше та де вони зараз, щоб візуалізація могла використовувати його знання про те, де знаходиться об'єкт.
class MovingObject
{
Vector velocity;
Vector previousPosition;
Vector currentPosition;
Initialise(startPosition, startVelocity)
{
currentPosition = startPosition; // position at time 0
velocity = startVelocity;
//ignore previousPosition because we should never render before time 0
}
Update()
{
previousPosition = currentPosition;
currentPosition += velocity * fixedTimeStep;
}
Render(subFrameProportion)
{
Vector actualPosition =
Lerp(previousPosition, currentPosition, subFrameProportion);
RenderAt(actualPosition);
}
}
І давайте викладемо часову шкалу в мілісекунди, кажучи, що для відтворення потрібно 3 мс, оновлення займає 1 мс, час кроку оновлення встановлено на 5 мс, а часовий крок візуалізації починається (і залишається) на 16 мс [60 Гц].
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
R0 U5 U10 U15 U20 W16 R16 U25 U30 U35 W32 R32
- Спочатку ми ініціалізуємо час 0 (так currentTime = 0)
- Ми надаємо пропорцію 1,0 (100% поточного часу), яка буде малювати світ у 0
- Коли це закінчиться, фактичний час становить 3, і ми не очікуємо, що кадр закінчиться до 16, тому нам потрібно запустити деякі оновлення
- T + 3: ми оновлюємо від 0 до 5 (тому згодом currentTime = 5, previousTime = 0)
- T + 4: ще до кінця кадру, тому ми оновлюємо від 5 до 10
- T + 5: ще до кінця кадру, тому ми оновлюємо від 10 до 15
- T + 6: ще до кінця кадру, тому ми оновлюємо з 15 до 20
- T + 7: ще до кінця кадру, але currentTime знаходиться лише поза межами кадру. Ми не хочемо більше імітувати, тому що це може виштовхнути нас поза часом, у який ми хочемо подати наступний. Замість цього ми спокійно чекаємо наступного інтервалу візуалізації (16)
- T + 16: Настав час повторити. previousTime - 15, currentTime - 20. Отже, якщо ми хочемо відобразити на T + 16, ми перебуваємо за 1 мс шляху через довгий часовий крок 5 мс. Таким чином, ми проходимо 20% шляху через кадр (пропорція = 0,2). Коли ми візуалізуємо, ми малюємо об'єкти на 20% шляху між їх попередньою позицією та поточною позицією.
- Цикл поверніть на 3. і продовжуйте нескінченно.
Тут є ще один нюанс щодо імітації занадто далеко заздалегідь, тобто користувацькі вкладки можуть бути ігноровані, хоча вони відбувалися ще до того, як кадр був фактично наданий, але не хвилюйтеся з цього приводу, поки ви не будете впевнені, що цикл плавно імітує.