Есть две вещи, которые очень важны для придания движению гладкости, во-первых, очевидно, что то, что вы отображаете, должно соответствовать ожидаемому состоянию во время представления кадра пользователю, во-вторых, вам нужно представить кадры пользователю. с относительно фиксированным интервалом. Представление кадра в момент времени T + 10 мс, затем другого в момент времени T + 30 мс, а затем другого в момент времени T + 40 мс, покажется пользователю, что он судит, даже если то, что фактически показано для тех времен, является правильным в соответствии с моделированием.
В вашем основном цикле, похоже, отсутствует какой-либо механизм стробирования, который бы гарантировал, что вы выполняете рендеринг только через регулярные интервалы. Так что иногда вы можете делать 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% currentTime), которая будет рисовать мир во время 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. и продолжайте до бесконечности.
Здесь есть еще один нюанс, связанный с симуляцией слишком заблаговременно, то есть пользовательские входы могут быть проигнорированы, даже если они произошли до того, как кадр был фактически визуализирован, но не беспокойтесь об этом, пока не убедитесь, что цикл симулируется плавно.