Как интерполировать между двумя игровыми состояниями?

24

Каков наилучший шаблон для создания системы, в которой все объекты должны быть интерполированы между двумя состояниями обновления?

Обновление всегда будет запускаться с одинаковой частотой, но я хочу иметь возможность рендеринга на любом FPS. Таким образом, рендеринг будет максимально плавным, независимо от количества кадров в секунду, будь то ниже или выше частоты обновления.

Я хотел бы обновить 1 кадр в будущем интерполировать из текущего кадра в будущий кадр. Этот ответ имеет ссылку, которая говорит о том, как это сделать:

Полуфиксируемый или полностью фиксированный временной шаг?

Изменить: Как я мог также использовать последнюю и текущую скорость в интерполяции? Например, при простой линейной интерполяции он будет перемещаться с одинаковой скоростью между позициями. Мне нужен способ, чтобы он интерполировал положение между двумя точками, но принимал во внимание скорость в каждой точке для интерполяции. Это было бы полезно для низкоскоростных симуляций, таких как эффекты частиц.

AttackingHobo
источник
2
тики логические тики? Итак, ваше обновление fps <рендеринг fps?
Коммунистическая утка
Я изменил термин. Но да, логика тикает. И нет, я хочу полностью освободить рендеринг от обновления, чтобы игра могла рендериться с частотой 120 Гц или 22,8 Гц, и обновление все равно будет работать с той же скоростью, если пользователь отвечает системным требованиям.
AttackingHobo
это может быть очень сложно, так как при рендеринге все позиции вашего объекта должны оставаться неподвижными (изменение их во время процесса рендеринга может вызвать некоторое неопределенное поведение)
Ali1S232
Интерполяция будет рассчитывать состояние за время между 2 уже рассчитанными кадрами обновления. Разве это не вопрос об экстраполяции, вычислении состояния на некоторое время после последнего кадра обновления? Поскольку следующее обновление еще даже не заключено в капсулу.
Майк Земдер
Я думаю, что если у него есть только один поток обновления / рендеринга, то не может произойти повторное обновление только позиции рендеринга. Вы просто отправляете позиции в GPU и затем обновляете.
zacharmarz

Ответы:

22

Вы хотите разделить ставки обновления (логический тик) и отрисовки (рендеринг тиков).

Ваши обновления будут отображать положение всех объектов в мире, которые будут нарисованы.

Здесь я расскажу о двух разных возможностях: запрошенная вами, экстраполяция, а также другой метод, интерполяция.

1.

Экстраполяция - это то, где мы вычисляем (прогнозируемую) позицию объекта в следующем кадре, а затем интерполируем между текущей позицией объектов и позицией, в которой объект будет находиться в следующем кадре.

Для этого каждый объект должен быть связан с velocityи position. Чтобы найти позицию, в которой будет находиться объект в следующем кадре, мы просто добавляем velocity * draw_timestepтекущую позицию объекта, чтобы найти прогнозируемую позицию следующего кадра. draw_timestepколичество времени, прошедшее с предыдущего тика рендеринга (он же предыдущий вызов отрисовки).

Если вы оставите это здесь, вы обнаружите, что объекты «мерцают», когда их прогнозируемое положение не соответствует фактическому положению в следующем кадре. Чтобы убрать мерцание, вы можете сохранить прогнозируемую позицию и сделать рывок между ранее прогнозируемой позицией и новой прогнозируемой позицией на каждом шаге отрисовки, используя время, прошедшее с момента предыдущего обновления, в качестве фактора скачка. Это по-прежнему будет приводить к плохому поведению, когда быстро движущиеся объекты внезапно меняют местоположение, и вы можете решить этот особый случай. Все сказанное в этом параграфе является причиной, по которой вы не хотите использовать экстраполяцию.

2.

Интерполяция - это место, где мы храним состояние двух последних обновлений и интерполируем их между собой на основе текущего количества времени, прошедшего с момента последнего обновления. В этой настройке каждый объект должен иметь связанный positionи previous_position. В этом случае наш рисунок будет представлять в худшем случае один тик обновления за текущим игровым состоянием, а в лучшем случае в том же состоянии, что и текущий тик обновления.


На мой взгляд, вы, вероятно, захотите интерполяцию, как я ее описал, так как это легче реализовать из двух, и можно нарисовать крошечную долю секунды (например, 1/60 секунды) за текущим обновленным состоянием.


Редактировать:

В случае, если вышеприведенного недостаточно для выполнения реализации, вот пример того, как выполнить метод интерполяции, который я описал. Я не буду освещать экстраполяцию, потому что не могу придумать ни одного реального сценария, в котором вы бы предпочли его.

Когда вы создаете нарисованный объект, он будет хранить свойства, необходимые для рисования (т. Е. Информацию о состоянии, необходимую для его рисования).

Для этого примера мы будем хранить положение и вращение. Вы также можете сохранить другие свойства, такие как координаты цвета или текстуры (например, если прокручивается текстура).

Чтобы предотвратить изменение данных во время их отрисовки потоком рендеринга (т. Е. Расположение одного объекта во время отрисовки потока рендеринга, но все остальные еще не обновлены), нам необходимо реализовать некоторый тип двойной буферизации.

Объект хранит две его копии previous_state. Я положу их в массив и буду ссылаться на них как previous_state[0]и previous_state[1]. Точно так же нужно две копии этого current_state.

Чтобы отслеживать, какая копия двойного буфера используется, мы храним переменную state_index, которая доступна как для обновления, так и для потока отрисовки.

Поток обновления сначала вычисляет все свойства объекта, используя его собственные данные (любые структуры данных, которые вы хотите). Затем он копирует current_state[state_index]в previous_state[state_index]и копирует новые данные , относящиеся к рисованию, positionи rotationв current_state[state_index]. Затем это делает state_index = 1 - state_index, чтобы перевернуть текущую используемую копию двойного буфера.

Все в вышеприведенном абзаце должно быть сделано с снятой блокировкой current_state. Обновление и отрисовка потоков снимают эту блокировку. Блокировка снимается только на время копирования информации о состоянии, что быстро.

В потоке рендеринга вы затем выполняете линейную интерполяцию по положению и повороту следующим образом:

current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)

Где elapsed- количество времени, прошедшее в потоке рендеринга с момента последнего обновления, и update_tick_lengthколичество времени, которое ваша фиксированная частота обновления занимает на такт (например, при 20FPS обновлениях update_tick_length = 0.05).

Если вы не знаете, что это за Lerpфункция, то посмотрите статью в Википедии на эту тему: Линейная интерполяция . Однако, если вы не знаете, что такое lerping, то вы, вероятно, не готовы реализовать разделенное обновление / рисование с интерполированным рисунком.

Ольховский
источник
1
+1 то же самое должно быть сделано для ориентаций / вращений и всех других состояний, которые меняются со временем, то есть, как анимация материала в системах частиц и т. Д.
Maik Semder
1
Хороший вопрос, Майк, я просто использовал положение в качестве примера. Вам необходимо сохранить «скорость» любого свойства, которое вы хотите экстраполировать (т. Е. Скорость изменения во времени этого свойства), если вы хотите использовать экстраполяцию. В конце концов, я действительно не могу вспомнить ситуацию, когда экстраполяция лучше, чем интерполяция, я включил ее только потому, что вопрос аскера потребовал ее. Я использую интерполяцию. При интерполяции нам нужно сохранять текущие и предыдущие результаты обновления любых свойств для интерполяции, как вы сказали.
Ольховский
Это переформулировка проблемы и различие между интерполяцией и экстраполяцией; это не ответ.
1
В моем примере я сохранил положение и вращение в состоянии. Вы можете просто хранить скорость (или скорость) в состоянии. Затем вы точно так же перебираете скорость ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Вы можете сделать это с любым номером, который хотите сохранить в штате. Lerping просто дает вам значение между двумя значениями, учитывая коэффициент lerp.
Ольховский
1
Для интерполяции углового движения рекомендуется использовать slerp вместо lerp. Проще всего было бы хранить кватернионы обоих состояний и проскальзывать между ними. В противном случае те же правила применяются для угловой скорости и углового ускорения. У вас есть тест-кейс для скелетной анимации?
Майк Земдер
-2

Эта проблема требует от вас думать о ваших определениях начала и конца немного по-другому. Начинающие программисты часто думают об изменении позиции в кадре, и это хороший способ начать с самого начала. Ради моего ответа, давайте рассмотрим одномерный ответ.

Допустим, у вас есть обезьяна в положении x. Теперь у вас также есть «addX», к которому вы добавляете позицию обезьяны для каждого кадра на основе клавиатуры или какого-либо другого элемента управления. Это будет работать до тех пор, пока у вас есть гарантированная частота кадров. Допустим, ваш x равен 100, а addX равен 10. После 10 кадров ваш x + = addX должен накапливаться до 200.

Теперь вместо addX, когда у вас есть переменная частота кадров, вы должны думать о скорости и ускорении. Я проведу вас через всю эту арифметику, но она очень проста. Что мы хотим знать, так это то, как далеко вы хотите путешествовать за миллисекунду (1/1000 секунды)

Если вы снимаете со скоростью 30 кадров в секунду, то ваш velX должен составлять 1/3 секунды (10 кадров из последнего примера со скоростью 30 кадров в секунду), и вы знаете, что хотите пройти 100 x за это время, поэтому установите для velX значение 100 дистанций / 10 кадров в секунду или 10 дистанций на кадр. В миллисекундах это работает до 1 расстояния x за 3,3 миллисекунды или до 0,3 'x' за миллисекунду.

Теперь, каждый раз, когда вы обновляете, все, что вам нужно сделать, это выяснить, сколько времени прошло. Независимо от того, прошло ли 33 мс (1/30 секунды) или что-то еще, вы просто умножаете расстояние 0,3 на количество пройденных миллисекунд. Это означает, что вам нужен таймер, который дает вам мс (миллисекунду) точность, но большинство таймеров дают вам это. Просто сделайте что-то вроде этого:

var beginTime = getTimeInMillisecond ()

... позже ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = время

... теперь используйте этот elapsedTime для расчета всех ваших расстояний.

Микки
источник
1
У него нет переменной частоты обновления. У него фиксированная частота обновления. Честно говоря, я действительно не знаю, что вы пытаетесь сделать здесь: /
Ольховский
1
??? -1. В этом все дело, у меня гарантированная частота обновления, но переменная скорость рендеринга, и я хочу, чтобы она была плавной, без заиканий.
AttackingHobo
Переменные скорости обновления плохо работают с сетевыми играми, конкурентными играми, системами воспроизведения или чем-то еще, что зависит от детерминированности игрового процесса.
AttackingHobo
1
Фиксированное обновление также позволяет легко интегрировать псевдо-трение. Например, если вы хотите умножить свою скорость на 0,9 каждого кадра, как вы узнаете, на сколько нужно умножаться, если у вас быстрый или медленный кадр? Иногда предпочтительнее фиксированное обновление - практически во всех физических симуляциях используется фиксированная частота обновления.
Ольховский
2
Если бы я использовал переменную частоту кадров и установил сложное начальное состояние с множеством объектов, отражающихся друг от друга, нет никакой гарантии, что он будет симулировать точно так же. Фактически, он, скорее всего, будет симулировать немного по-разному каждый раз, с небольшими различиями в начале, смешиваясь в течение короткого времени в совершенно разные состояния между каждым прогоном симуляции.
AttackingHobo