Як реально працює інтерполяція, щоб згладити рух об'єкта?


10

Я задавав кілька подібних питань протягом останніх 8 місяців або близько того, не маючи справжньої радості, тому я збираюся зробити це питання більш загальним.

У мене є Android-гра, яка є OpenGL ES 2.0. в ній у мене є наступний цикл гри:

Мій цикл працює за принципом встановленого крокового часу (dt = 1 / ticksPerSecond )

loops=0;

    while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){

        updateLogic(dt);
        nextGameTick+=skipTicks;
        timeCorrection += (1000d/ticksPerSecond) % 1;
        nextGameTick+=timeCorrection;
        timeCorrection %=1;
        loops++;

    }

    render();   

Моя інтеграція працює так:

sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;

Зараз все працює майже так, як хотілося б. Я можу вказати, що я хотів би, щоб об’єкт перемістився через певну відстань (скажімо ширину екрану) за 2,5 секунди, і він зробить саме це. Крім того, через пропуск кадрів, який я дозволяю в своєму ігровому циклі, я можу це зробити на майже будь-якому пристрої, і це займе 2,5 секунди.

Проблема

Однак проблема полягає в тому, що коли кадр візуалізації пропускає, графічне заїкання. Це надзвичайно дратує. Якщо я знімаю можливість пропускати кадри, то все гладко, як вам подобається, але буде працювати з різною швидкістю на різних пристроях. Тож це не варіант.

Я все ще не впевнений, чому кадр пропускає, але я хотів би зазначити, що це нічого спільного з низькою продуктивністю , я взяв код назад до 1 крихітного спрайту і ніякої логіки (крім логіки, необхідної для перемістити спрайт), і я все одно отримую пропущені кадри. І це на планшеті Google Nexus 10 (і як уже згадувалося вище, мені потрібно пропускати кадр, щоб у будь-якому разі підтримувати швидкість, що відповідає всім пристроям).

Отже, єдиний інший варіант, який я маю - це використовувати інтерполяцію (або екстраполяцію), я читав там кожну статтю, але жодна насправді не допомогла мені зрозуміти, як вона працює, і всі мої спроби реалізації не змогли.

Використовуючи один метод, я зміг змусити речі рухатися плавно, але це було непрацездатним, оскільки це зіпсувало моє зіткнення. Я можу передбачити ту саму проблему з будь-яким подібним методом, оскільки інтерполяція передається (і діє в межах) методу візуалізації - під час надання. Отже, якщо зіткнення виправить положення (персонаж зараз стоїть прямо біля стіни), то рендерінг може змінити своє положення і намалювати його в стіні.

Тож я справді розгублений. Люди сказали, що ніколи не слід змінювати позицію об'єкта в межах методу візуалізації, але всі приклади в Інтернеті це показують.

Тож я прошу проштовхнутись у правильному напрямку, будь ласка, не посилайтеся на популярні статті з циклу ігор (deWitters, Виправте свій часовий крок тощо), як я читав ці кілька разів . Я не прошу нікого писати для мене свій код. Просто поясніть, будь ласка, простими словами, як Інтерполяція насправді працює на деяких прикладах. Тоді я спробую інтегрувати будь-які ідеї у свій код і задам більш конкретні запитання, якщо потрібно - будьте далі вниз. (Я впевнений, що це проблема, з якою багато людей борються).

редагувати

Деякі додаткові відомості - змінні, що використовуються в ігровому циклі.

private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;                         
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take        
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;

А причиною знищення є ...................?
BungleBonce

1
Іноді сказати неможливо. Схоже, все, що має бути хорошим питанням, намагаючись вирішити проблему. Короткий фрагмент коду, пояснення того, що ви спробували, спроби дослідження та чітке пояснення, у чому полягає ваша проблема, і що вам потрібно знати.
Джессі Дорсі

Я не був вашим голосом, але уточніть, будь ласка, одну частину. Ви кажете, що графічне заїкання при пропуску кадру. Це здається очевидним твердженням (кадр пропущений, схоже, кадр пропущений). Тож чи можете ви краще пояснити пропуск? Чи трапляється щось дивніше? Якщо ні, то це може бути нерозв'язною проблемою, оскільки ви не можете отримати плавний рух, якщо кадр провалюється.
Сет Беттін

Дякую, Ноктрін, це просто дуже дратує мене, коли люди зволікають, не залишаючи пояснень. @SethBattin, вибачте, так, звичайно, ви маєте рацію, пропуск кадрів викликає хиткість, однак, інтерполяція якась така повинна вирішити, як я вже говорив вище, я мав певний (але обмежений) успіх. Якщо я помиляюся, то, гадаю, питання виникне, як я можу змусити його працювати безперебійно з однаковою швидкістю на різних пристроях?
BungleBonce

4
Ретельно перечитайте ці документи. Вони фактично не змінюють розташування об'єкта в методі візуалізації. Вони лише змінюють видиме місце методу, виходячи з його останньої позиції та поточної позиції залежно від того, скільки часу пройшло.
AttackingHobo

Відповіді:


5

Дві речі важливі для того, щоб рух виглядав плавним, перше - це очевидно, що те, що ви надаєте, має відповідати очікуваному стану в момент, коли кадр представлений користувачеві, друге - потрібно представити користувачеві кадри. через відносно фіксований інтервал. Представляючи кадр у 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
  1. Спочатку ми ініціалізуємо час 0 (так currentTime = 0)
  2. Ми надаємо пропорцію 1,0 (100% поточного часу), яка буде малювати світ у 0
  3. Коли це закінчиться, фактичний час становить 3, і ми не очікуємо, що кадр закінчиться до 16, тому нам потрібно запустити деякі оновлення
  4. T + 3: ми оновлюємо від 0 до 5 (тому згодом currentTime = 5, previousTime = 0)
  5. T + 4: ще до кінця кадру, тому ми оновлюємо від 5 до 10
  6. T + 5: ще до кінця кадру, тому ми оновлюємо від 10 до 15
  7. T + 6: ще до кінця кадру, тому ми оновлюємо з 15 до 20
  8. T + 7: ще до кінця кадру, але currentTime знаходиться лише поза межами кадру. Ми не хочемо більше імітувати, тому що це може виштовхнути нас поза часом, у який ми хочемо подати наступний. Замість цього ми спокійно чекаємо наступного інтервалу візуалізації (16)
  9. T + 16: Настав час повторити. previousTime - 15, currentTime - 20. Отже, якщо ми хочемо відобразити на T + 16, ми перебуваємо за 1 мс шляху через довгий часовий крок 5 мс. Таким чином, ми проходимо 20% шляху через кадр (пропорція = 0,2). Коли ми візуалізуємо, ми малюємо об'єкти на 20% шляху між їх попередньою позицією та поточною позицією.
  10. Цикл поверніть на 3. і продовжуйте нескінченно.

Тут є ще один нюанс щодо імітації занадто далеко заздалегідь, тобто користувацькі вкладки можуть бути ігноровані, хоча вони відбувалися ще до того, як кадр був фактично наданий, але не хвилюйтеся з цього приводу, поки ви не будете впевнені, що цикл плавно імітує.


Примітка: псевдокод є слабким двома способами. По-перше, це не сприймає випадок спіралі смерті (для оновлення потрібно більше часу, ніж fixTimeStep, тобто моделювання все більше відстає, фактично нескінченна петля), по-друге, рендерІнтервал ніколи не скорочується. На практиці ви хочете негайно збільшити інтерфейс рендерінгу, але потім з часом скорочуйте його поступово, як можна краще, до певного допуску до фактичного кадрового часу. Інакше одне погане / тривале оновлення назавжди осідає вас із низькою частотою кадрів.
MrCranky

Дякую за це @MrCranky, дійсно, я протягом багатьох років борюся за те, як "обмежити" візуалізацію в моєму циклі! Просто не міг розібратися, як це зробити, і задумався, чи може це бути одним із питань. Я правильно прочитаю це, і спробую запропонувати ваші пропозиції. Повідомлю про це! Ще раз дякую :-)
BungleBonce

Дякую @MrCranky, добре, я прочитав і перечитав вашу відповідь, але не можу її зрозуміти :-( Я намагався її реалізувати, але це просто дало мені порожній екран. Дуже бореться з цим. Попередній кадр та currentFrame я припускаю стосується попередніх і поточних позицій моїх рухомих об'єктів? Крім того, як щодо рядка "currentFrame = Update ();" - я не отримую цей рядок, чи означає це оновлення виклику (); тому що я не бачу, де ще я закликаю оновлення? Або це просто означає встановити currentFrame (позицію) на нове значення? Ще раз дякую за допомогу !!
BungleBonce

Так, ефективно. Причина, по якій я ставлю в попередньомуFrame та currentFrame як значення повернення з Update and InitialiseWorldState, полягає в тому, що для того, щоб візуалізація намалювала світ, оскільки він проходить між двома фіксованими кроками оновлення, вам потрібно мати не тільки поточне положення кожного об'єкт, який ви хочете намалювати, а також їх попередні позиції. Ви можете змусити кожен об'єкт зберегти обидва значення внутрішньо, що стає непростим.
MrCranky

Але також можливо (але набагато складніше) архітектуру речей, щоб вся державна інформація, необхідна для представлення поточного стану світу в той час, зберігалася під одним об'єктом. Концептуально це набагато чіткіше, коли пояснюється, яка інформація є в системі, оскільки ви можете ставитися до стану кадру як до чогось, що виробляється кроком оновлення, а збереження попереднього кадру навколо - це лише збереження ще одного з цих об'єктів стану кадру. Однак я можу переписати відповідь, щоб бути трохи більше, як ви насправді, ймовірно, реалізуєте її.
MrCranky

3

Те, що всі вам казали, це правильно. Ніколи не оновлюйте позицію імітації спрайта в логіці візуалізації.

Подумайте про це так, ваш спрайт має 2 положення; де моделювання говорить, що він є останнім оновленням моделювання, і де спрайт надається. Вони є двома абсолютно різними координатами.

Спрайт виводиться в його екстрапольованому положенні. Екстрапольована позиція обчислюється кожен кадр візуалізації, який використовується для візуалізації спрайта, а потім викидається. Це все, що там є.

Крім цього, ви, здається, добре розумієте. Сподіваюсь, це допомагає.


Відмінно @WilliamMorrison - дякую за підтвердження цього, я ніколи не був впевнений на 100%, що це було так, тепер я думаю, що я на шляху до того, щоб я працював певною мірою - ура!
BungleBonce

Цікаво просто @WilliamMorrison, використовуючи ці викидні координати, як можна пом'якшити проблему того, як спрайти малюють "вбудовані" або "трохи вище" інших об'єктів - очевидний приклад, як тверді об'єкти у 2d грі. Чи доведеться вам запустити і ваш код зіткнення під час візуалізації?
BungleBonce

У своїх іграх так, саме цим я і займаюся. Будь ласка, краще, ніж я, не роби цього, це не найкраще рішення. Це ускладнює код візуалізації з логікою, яку він не повинен використовувати, і витратить процесор на надмірне виявлення зіткнень. Краще інтерполювати між другою та останньою позицією та поточною позицією. Це вирішує проблему, оскільки ви не екстраполюєтеся на погану позицію, але ускладнюєте те, що ви робите один крок позаду моделювання. Я люблю чути вашу думку, який підхід ви і ваш досвід.
Вільям Моррісон,

Так, вирішити це складно. Я поставив окреме запитання щодо цього тут gamedev.stackexchange.com/questions/83230/…, якщо ви хочете слідкувати за цим чи щось сприяти. Тепер, що ви запропонували у своєму коментарі, я це вже не роблю? (Інтерполяція між попереднім та поточним кадром)?
BungleBonce

Не зовсім. Ви насправді екстраполюєте зараз. Ви берете найсучасніші дані за допомогою моделювання та екстраполюєте, як виглядають ці дані після дробових часових кроків. Я пропоную вам інтерполювати між останньою позицією моделювання та поточною позицією імітації шляхом дробових часових кроків для візуалізації замість цього. Візуалізація буде відставати від моделювання на 1 крок. Це гарантує, що ви ніколи не надаватимете об’єкт у стані, коли симуляція не підтвердилась (тобто снаряд не з’явиться у стіні, якщо моделювання не вдасться)
Вільям Моррісон,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.