Bagaimana interpolasi benar-benar berfungsi untuk memuluskan gerakan suatu objek?

10

Saya telah mengajukan beberapa pertanyaan serupa selama 8 bulan terakhir atau lebih tanpa sukacita, jadi saya akan membuat pertanyaan lebih umum.

Saya memiliki game Android yaitu OpenGL ES 2.0. di dalamnya saya memiliki Loop Game berikut:

Loop saya berfungsi pada prinsip langkah waktu tetap (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();   

Intergrasi saya berfungsi seperti ini:

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

Sekarang, semuanya berjalan cukup seperti yang saya inginkan. Saya dapat menentukan bahwa saya ingin objek bergerak melintasi jarak tertentu (lebar layar katakan) dalam 2,5 detik dan itu akan melakukan hal itu. Juga karena lompatan frame yang saya izinkan dalam loop game saya, saya bisa melakukan ini pada hampir semua perangkat dan itu akan selalu memakan waktu 2,5 detik.

Masalah

Namun, masalahnya adalah bahwa ketika membuat bingkai melompati, grafik gagap. Sangat menyebalkan. Jika saya menghapus kemampuan untuk melompati frame, maka semuanya lancar seperti yang Anda inginkan, tetapi akan berjalan pada kecepatan yang berbeda pada perangkat yang berbeda. Jadi itu bukan pilihan.

Saya masih tidak yakin mengapa frame dilewati, tetapi saya ingin menunjukkan bahwa ini tidak ada hubungannya dengan kinerja yang buruk , saya telah mengambil kode kembali ke 1 sprite kecil dan tidak ada logika (terlepas dari logika yang diperlukan untuk pindahkan sprite) dan saya masih mendapatkan bingkai yang dilewati. Dan ini ada di tablet Google Nexus 10 (dan seperti yang disebutkan di atas, saya perlu melewatkan bingkai untuk menjaga kecepatan konsisten di semua perangkat).

Jadi, satu-satunya pilihan lain yang saya miliki adalah menggunakan interpolasi (atau ekstrapolasi), saya sudah membaca setiap artikel yang ada di luar sana tetapi tidak ada yang benar-benar membantu saya untuk memahami cara kerjanya dan semua upaya implementasi saya gagal.

Dengan menggunakan satu metode, saya bisa membuat semuanya bergerak dengan lancar tetapi tidak bisa dilakukan karena mengacaukan benturan saya. Saya dapat meramalkan masalah yang sama dengan metode serupa karena interpolasi diteruskan ke (dan ditindaklanjuti dalam) metode rendering - pada waktu render. Jadi jika Collision memperbaiki posisi (karakter sekarang berdiri tepat di sebelah dinding), maka penyaji dapat mengubah posisi itu dan menggambarnya di dinding.

Jadi saya benar-benar bingung. Orang-orang mengatakan bahwa Anda tidak boleh mengubah posisi objek dari dalam metode rendering, tetapi semua contoh online menunjukkan ini.

Jadi saya meminta dorongan ke arah yang benar, tolong jangan menautkan ke artikel loop game populer (deWitters, Perbaiki cap waktu Anda, dll) karena saya sudah membaca ini beberapa kali . Saya tidak meminta siapa pun untuk menulis kode saya untuk saya. Jelaskan secara sederhana bagaimana Interpolasi sebenarnya bekerja dengan beberapa contoh. Saya kemudian akan pergi dan mencoba untuk mengintegrasikan ide-ide apa pun ke dalam kode saya dan akan mengajukan pertanyaan yang lebih spesifik jika perlu lebih jauh ke depan. (Saya yakin ini adalah masalah yang banyak orang perjuangkan).

sunting

Beberapa informasi tambahan - variabel yang digunakan dalam loop game.

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
sumber
Dan alasan untuk downvote adalah ...................?
BungleBonce
1
Tidak mungkin mengatakannya sesekali. Tampaknya ini memiliki semua pertanyaan yang bagus ketika mencoba untuk memecahkan masalah. Cuplikan kode ringkas, penjelasan tentang apa yang telah Anda coba, coba penelitian, dan jelaskan apa masalah Anda dan apa yang perlu Anda ketahui.
Jesse Dorsey
Saya bukan downvote Anda, tapi tolong jelaskan satu bagian. Anda mengatakan gambarnya gagap saat bingkai dilewati. Itu tampak seperti pernyataan yang jelas (sebuah frame terlewatkan, sepertinya sebuah frame terlewatkan). Jadi bisakah Anda menjelaskan skipping yang lebih baik? Apakah sesuatu yang aneh terjadi? Jika tidak, ini mungkin masalah yang tidak dapat diselesaikan, karena Anda tidak bisa mendapatkan gerakan yang lancar jika framerate turun.
Seth Battin
Terima kasih, Noctrine, itu benar-benar membuatku jengkel ketika orang-orang downvote tanpa meninggalkan penjelasan. @SethBattin, maaf, ya tentu saja, Anda benar, skipping frame yang menyebabkan jerkiness, namun, interpolasi semacam itu harus menyelesaikan masalah ini, seperti yang saya katakan di atas, saya sudah beberapa (tetapi terbatas) sukses. Jika saya salah, maka saya kira pertanyaannya adalah, bagaimana saya bisa menjalankannya dengan lancar pada kecepatan yang sama di berbagai perangkat?
BungleBonce
4
Bacalah kembali dokumen-dokumen itu dengan hati-hati. Mereka tidak benar-benar mengubah lokasi objek dalam metode rendering. Mereka hanya memodifikasi lokasi semu metode berdasarkan posisi terakhirnya dan posisi saat ini berdasarkan berapa banyak waktu telah berlalu.
AttackingHobo

Jawaban:

5

Ada dua hal penting untuk membuat gerakan tampak mulus, yang pertama jelas bahwa apa yang Anda render harus mencocokkan dengan kondisi yang diharapkan pada saat bingkai disajikan kepada pengguna, yang kedua adalah Anda perlu menampilkan bingkai kepada pengguna pada interval yang relatif tetap. Menyajikan bingkai pada T + 10ms, lalu yang lain pada T + 30ms, lalu yang lain pada T + 40ms, akan tampak bagi pengguna untuk menilai, bahkan jika apa yang sebenarnya ditunjukkan untuk waktu itu adalah benar sesuai dengan simulasi.

Loop utama Anda tampaknya tidak memiliki mekanisme gating untuk memastikan bahwa Anda hanya membuat secara berkala. Jadi kadang-kadang Anda mungkin melakukan 3 pembaruan antara render, kadang-kadang Anda mungkin 4. Pada dasarnya loop Anda akan merender sesering mungkin, segera setelah Anda mensimulasikan cukup waktu untuk mendorong keadaan simulasi di depan waktu saat ini, Anda akan lalu berikan status itu. Tetapi setiap variabilitas dalam berapa lama waktu yang dibutuhkan untuk memperbarui atau membuat, dan interval antara frame akan bervariasi juga. Anda memiliki stempel waktu tetap untuk simulasi Anda, tetapi stempel waktu variabel untuk rendering Anda.

Apa yang mungkin Anda butuhkan adalah menunggu tepat sebelum render Anda, yang memastikan bahwa Anda hanya akan mulai render pada awal interval render. Idealnya harus adaptif: jika Anda telah terlalu lama memperbarui / membuat dan awal interval telah berlalu, Anda harus merender segera, tetapi juga meningkatkan panjang interval, sampai Anda dapat secara konsisten membuat dan memperbarui dan masih dapat render berikutnya sebelum interval selesai. Jika Anda memiliki banyak waktu luang, maka Anda dapat secara perlahan mengurangi interval (yaitu meningkatkan frame rate) untuk membuat lebih cepat lagi.

Tapi, dan inilah kickernya, jika Anda tidak membuat frame segera setelah mendeteksi bahwa keadaan simulasi telah diperbarui menjadi "sekarang", maka Anda memperkenalkan alias sementara. Bingkai yang disajikan kepada pengguna disajikan pada waktu yang sedikit salah, dan itu sendiri akan terasa seperti gagap.

Ini adalah alasan untuk "timestep parsial" yang akan Anda lihat disebutkan dalam artikel yang telah Anda baca. Itu ada di sana untuk alasan yang bagus, dan itu karena kecuali jika Anda memperbaiki catatan waktu fisika Anda ke beberapa kelipatan terpisahkan tetap dari catatan waktu pembuatan render tetap Anda, Anda tidak bisa menampilkan bingkai pada waktu yang tepat. Anda akhirnya mempresentasikannya terlalu dini, atau terlalu terlambat. Satu-satunya cara untuk mendapatkan tingkat rendering yang tetap dan masih menyajikan sesuatu yang benar secara fisik, adalah dengan menerima bahwa pada saat interval rendering muncul, Anda kemungkinan besar akan berada di tengah-tengah antara dua catatan waktu fisika tetap Anda. Tetapi itu tidak berarti bahwa objek-objek tersebut dimodifikasi selama rendering, Hanya saja rendering harus menetapkan sementara di mana objek berada sehingga dapat membuat mereka di suatu tempat di antara di mana mereka sebelumnya dan di mana mereka setelah pembaruan. Itu penting - jangan pernah mengubah negara dunia untuk rendering, hanya pembaruan yang harus mengubah negara dunia.

Jadi untuk memasukkannya ke dalam loop pseudocode, saya pikir Anda membutuhkan sesuatu yang lebih seperti:

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);
}

Agar hal ini berhasil, semua objek yang diperbarui perlu mempertahankan pengetahuan di mana mereka sebelumnya dan di mana mereka sekarang, sehingga rendering dapat menggunakan pengetahuan tentang di mana objek itu berada.

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);
    }
}

Dan mari kita paparkan timeline dalam milidetik, mengatakan rendering membutuhkan 3ms untuk menyelesaikan, memperbarui butuh 1ms, langkah waktu pembaruan Anda ditetapkan menjadi 5ms, dan catatan waktu render Anda mulai (dan tetap) pada 60ms [60Hz].

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. Pertama kita inisialisasi pada waktu 0 (jadi currentTime = 0)
  2. Kami membuat dengan proporsi 1,0 (100% currentTime), yang akan menarik dunia pada waktu 0
  3. Ketika itu selesai, waktu sebenarnya adalah 3, dan kami tidak berharap frame berakhir hingga 16, jadi kami perlu menjalankan beberapa pembaruan
  4. T + 3: Kami memperbarui dari 0 hingga 5 (jadi setelah ini currentTime = 5, priorTime = 0)
  5. T + 4: masih sebelum frame berakhir, jadi kami memperbarui dari 5 hingga 10
  6. T + 5: masih sebelum frame berakhir, jadi kami memperbarui dari 10 hingga 15
  7. T + 6: masih sebelum frame berakhir, jadi kami memperbarui dari 15 hingga 20
  8. T + 7: masih sebelum akhir bingkai, tetapi currentTime hanya di luar ujung bingkai. Kami tidak ingin mensimulasikan lebih jauh karena hal itu akan mendorong kami melampaui waktu yang kami inginkan selanjutnya. Sebaliknya, kita menunggu dengan tenang untuk interval render berikutnya (16)
  9. T + 16: Saatnya untuk membuat lagi. priorTime adalah 15, currentTime adalah 20. Jadi jika kita ingin merender pada T + 16, kita 1ms dari jalan melalui timestep panjang 5ms. Jadi kita adalah 20% dari jalan melalui bingkai (proporsi = 0,2). Ketika kita membuat, kita menggambar objek 20% dari jalan antara posisi mereka sebelumnya dan posisi mereka saat ini.
  10. Kembali ke 3. dan lanjutkan tanpa batas.

Ada nuansa lain di sini tentang simulasi terlalu jauh sebelumnya, yang berarti input pengguna mungkin diabaikan meskipun terjadi sebelum bingkai benar-benar dibuat, tetapi jangan khawatir tentang hal itu sampai Anda yakin bahwa loop tersebut mensimulasikan dengan lancar.

MrCranky
sumber
NB: kodesemu lemah dalam dua cara. Pertama tidak menangkap kasus death-spiral (butuh waktu lebih lama dari fixTimeStep untuk Pembaruan, yang berarti simulasi semakin jauh di belakang, efektif loop tak terbatas), kedua renderInterval tidak pernah dipersingkat lagi. Dalam praktiknya Anda ingin segera meningkatkan renderInterval, tapi kemudian seiring waktu, persingkat secara bertahap sebaik mungkin dalam batas waktu bingkai aktual. Kalau tidak, satu pembaruan buruk / lama akan membebani Anda dengan framerate rendah selamanya.
MrCranky
Terima kasih atas @MrCranky ini, memang, saya telah berjuang selama bertahun-tahun tentang cara 'membatasi' rendering di loop saya! Hanya tidak tahu bagaimana melakukannya dan bertanya-tanya apakah itu mungkin salah satu masalahnya. Saya akan membaca dengan baik dan mencoba saran Anda, akan melaporkannya kembali! Terima kasih lagi :-)
BungleBonce
Terima kasih @MrCranky, OK, saya sudah membaca & membaca kembali jawaban Anda tetapi saya tidak dapat memahaminya :-( Saya mencoba menerapkannya tetapi itu hanya memberi saya layar kosong. Benar-benar berjuang dengan ini .Frame sebelumnya dan sekarangFrame saya berasumsi berkaitan dengan posisi sebelumnya dan saat ini dari objek bergerak saya? Juga, bagaimana dengan baris "currentFrame = Perbarui ();" - Saya tidak mendapatkan baris ini, apakah ini berarti pembaruan panggilan (); karena saya tidak bisa melihat di mana lagi saya memanggil pembaruan? Atau apakah itu hanya berarti mengatur currentFrame (posisi) ke nilai baru itu? Terima kasih lagi atas bantuan Anda !!
BungleBonce
Ya, secara efektif. Alasan mengapa saya memasukkanFrame dan currentFrame sebagai nilai pengembalian dari Pembaruan dan InitialiseWorldState adalah karena untuk memungkinkan rendering menggambar dunia karena merupakan bagian dari dua langkah pembaruan tetap, Anda harus memiliki tidak hanya posisi saat ini dari setiap objek yang ingin Anda gambar, tetapi juga posisi mereka sebelumnya. Anda bisa meminta setiap objek menyimpan kedua nilai secara internal, yang semakin sulit.
MrCranky
Tetapi juga memungkinkan (tetapi jauh lebih sulit) untuk merancang berbagai hal sehingga semua informasi keadaan yang diperlukan untuk mewakili keadaan dunia saat ini pada saat T disimpan di bawah satu objek. Secara konseptual, ini jauh lebih bersih ketika menjelaskan informasi apa yang ada di dalam sistem karena Anda dapat memperlakukan frame state sebagai sesuatu yang dihasilkan oleh langkah pembaruan, dan menjaga frame sebelumnya hanya tentang mempertahankan satu lagi objek frame state tersebut. Namun saya mungkin menulis ulang jawabannya menjadi sedikit lebih seperti Anda mungkin benar-benar mengimplementasikannya.
MrCranky
3

Apa yang semua orang katakan adalah benar. Jangan pernah memperbarui posisi simulasi sprite Anda dalam logika render Anda.

Pikirkan seperti ini, sprite Anda memiliki 2 posisi; di mana simulasi mengatakan dia pada pembaruan simulasi terakhir, dan di mana sprite diberikan. Mereka adalah dua koordinat yang sangat berbeda.

Sprite diberikan pada posisi ekstrapolasi. Posisi ekstrapolasi dihitung setiap frame render, digunakan untuk membuat sprite, lalu dibuang. Hanya itu yang ada untuk itu.

Selain itu, Anda tampaknya memiliki pemahaman yang baik. Semoga ini membantu.

William Morrison
sumber
Excellent @WilliamMorrison - terima kasih telah mengkonfirmasi ini, saya tidak pernah benar-benar yakin 100% bahwa ini adalah masalahnya, saya sekarang berpikir saya sedang dalam perjalanan untuk membuat ini bekerja sampai derajat tertentu - tepuk tangan!
BungleBonce
Hanya ingin tahu @ WilliamMorrison, menggunakan koordinat yang dibuang ini, bagaimana seseorang mengurangi masalah sprite yang sedang menggambar objek lain yang 'tertanam' atau 'tepat di atas' - contoh yang jelas, menjadi benda padat dalam permainan 2d. Apakah Anda harus menjalankan kode tumbukan Anda pada waktu render juga?
BungleBonce
Dalam gim saya ya, itulah yang saya lakukan. Tolong lebih baik dari saya, jangan lakukan itu, itu bukan solusi terbaik. Ini merumitkan kode render dengan logika yang seharusnya tidak digunakan, dan akan membuang cpu pada deteksi tabrakan yang berlebihan. Lebih baik interpolasi antara posisi kedua ke posisi terakhir dan saat ini. Ini menyelesaikan masalah karena Anda tidak melakukan ekstrapolasi ke posisi yang buruk, tetapi mempersulit hal-hal saat Anda merender satu langkah di belakang simulasi. Saya senang mendengar pendapat Anda, pendekatan mana yang Anda ambil, dan pengalaman Anda.
William Morrison
Ya itu masalah sulit untuk diselesaikan. Saya telah mengajukan pertanyaan terpisah mengenai hal ini di sini gamedev.stackexchange.com/questions/83230/... jika Anda ingin mengawasinya atau berkontribusi sesuatu. Sekarang, apa yang Anda sarankan dalam komentar Anda, apakah saya belum melakukan ini? (Interpolasi antara frame sebelumnya dan saat ini)?
BungleBonce
Tidak terlalu. Anda sebenarnya memperkirakan sekarang. Anda mengambil data terbaru dari simulasi, dan memperkirakan seperti apa data itu setelah timesteps fraksional. Saya menyarankan Anda menginterpolasi antara posisi simulasi terakhir, dan posisi simulasi saat ini dengan timesteps fraksional untuk rendering sebagai gantinya. Rendering akan berada di belakang simulasi dengan 1 timestep. Ini memastikan Anda tidak akan pernah membuat objek dalam keadaan simulasi tidak valid (mis. Proyektil tidak akan muncul di dinding kecuali simulasi gagal.)
William Morrison