Panduan Kompresi Animasi Kerangka


Artikel ini akan menjadi tinjauan singkat tentang bagaimana menerapkan skema kompresi animasi sederhana dan beberapa konsep terkait. Saya sama sekali tidak ahli dalam hal ini, tetapi ada sangat sedikit informasi tentang topik ini, dan itu cukup terfragmentasi. Jika Anda ingin membaca artikel yang lebih dalam tentang topik ini, maka saya sarankan Anda pergi ke tautan berikut:


Sebelum kita mulai, ada baiknya memberikan pengantar singkat tentang animasi kerangka dan beberapa konsep dasarnya.

Dasar-dasar Animasi dan Kompresi


Animasi kerangka adalah topik yang cukup sederhana, jika Anda lupa menguliti. Kami memiliki konsep kerangka yang berisi transformasi tulang karakter. Transformasi tulang ini disimpan dalam format hierarkis; pada kenyataannya, mereka disimpan sebagai delta antara posisi global mereka dan posisi orang tua. Terminologi di sini membingungkan, karena dalam mesin permainan lokal sering disebut model / karakter ruang, dan global adalah ruang dunia. Dalam terminologi animasi, lokal disebut sebagai ruang induk tulang, dan global adalah ruang karakter atau ruang dunia, tergantung pada apakah ada pergerakan tulang akar; tapi jangan khawatir tentang itu. Yang penting adalah bahwa transformasi tulang disimpan secara lokal relatif terhadap orang tua mereka. Ini memiliki banyak keuntungan, dan terutama saat pencampuran (blending):jika pencampuran kedua posisi bersifat global, maka keduanya akan diinterpolasi secara linear dalam posisi tersebut, yang akan mengarah pada peningkatan dan penurunan tulang dan deformasi karakter.Dan jika Anda menggunakan delta, pencampuran dilakukan dari satu perbedaan ke yang lain, jadi jika transformasi delta untuk satu tulang antara dua pose adalah sama, maka panjang tulang tetap konstan. Saya pikir ini paling mudah (tapi tidak sepenuhnya akurat) untuk mengambilnya dengan cara ini: penggunaan delta mengarah pada gerakan "bola" posisi tulang selama pencampuran, dan pencampuran transformasi global mengarah ke gerakan linear posisi tulang.

Animasi kerangka hanyalah daftar kerangka kunci yang teratur dengan laju bingkai konstan (biasanya). Bingkai kuncinya adalah pose kerangka. Jika kami ingin berpose di antara kerangka kunci, kami mencicipi kerangka kunci kedua dan mencampur di antara mereka, menggunakan sebagian kecil dari waktu di antara mereka sebagai berat campuran. Gambar di bawah ini menunjukkan animasi yang dibuat pada 30fps. Animasi ini memiliki total 5 frame dan kita perlu mendapatkan pose 0,52 s setelah memulai. Oleh karena itu, kita perlu mengambil sampel pose dalam bingkai 1 dan pose dalam bingkai 2, dan kemudian mencampurnya dengan berat campuran sekitar 57%.


Contoh animasi 5 bingkai dan permintaan pose pada waktu frame menengah.

Setelah informasi di atas dan mengingat bahwa memori bukan masalah bagi kami, penghematan urutan pose akan menjadi cara ideal untuk menyimpan animasi, seperti yang ditunjukkan di bawah ini:


Penyimpanan data animasi sederhana

Mengapa ini sempurna? Sampling keyframe apa saja datang ke operasi memcpy sederhana. Mengambil sampel pose perantara membutuhkan dua operasi memcpy dan satu operasi mixing. Dari sudut pandang cache, kami menyalin menggunakan memcpy dua blok data secara berurutan, yaitu, setelah menyalin frame pertama, salah satu cache sudah memiliki frame kedua. Anda dapat mengatakan: tunggu, ketika kita melakukan pencampuran, kita perlu mencampur semua tulang; Bagaimana jika kebanyakan dari mereka tidak berubah di antara frame? Bukankah lebih baik menyimpan tulang sebagai catatan dan hanya mencampur transformasi yang berubah? Nah, jika ini diterapkan, maka sedikit lebih banyak cache yang meleset berpotensi terjadi ketika membaca catatan individual, dan kemudian Anda perlu melacak konversi mana yang perlu Anda campur, dan seterusnya ... Mencampur mungkin tampak seperti pekerjaan yang menghabiskan banyak waktu,tetapi pada dasarnya itu adalah aplikasi dari satu instruksi ke dua blok memori yang sudah ada dalam cache. Selain itu, kode pencampuran relatif sederhana, seringkali hanya satu set instruksi SIMD tanpa bercabang, dan prosesor modern akan memprosesnya dalam beberapa saat.

Masalah dengan pendekatan ini adalah dibutuhkan sejumlah besar memori, terutama di game di mana kondisi berikut berlaku untuk 95% dari data.

  • Tulang memiliki panjang yang konstan
    • Karakter di sebagian besar permainan tidak meregangkan tulang, oleh karena itu, dalam animasi yang sama, catatan transformasi konstan.
  • Kami biasanya tidak mengukur tulang.
    • Skala jarang digunakan dalam animasi game. Ini cukup aktif digunakan dalam film dan VFX, tetapi sangat sedikit dalam game. Bahkan ketika digunakan, skala yang sama biasanya digunakan.
    • Bahkan, di sebagian besar animasi yang saya buat saat runtime, saya mengambil keuntungan dari fakta ini dan menjaga seluruh transformasi tulang dalam 8 variabel float: 4 untuk memutar angka empat, 3 untuk bergerak dan 1 untuk skala. Ini secara signifikan mengurangi ukuran pose pada saat run time, memberikan peningkatan produktivitas saat pencampuran dan penyalinan.

Dengan semua ini dalam pikiran, jika Anda melihat format data asli, Anda dapat melihat betapa tidak efisiennya menghabiskan memori. Kami menduplikasi nilai perpindahan dan skala masing-masing tulang, bahkan jika mereka tidak berubah. Dan situasinya cepat lepas kendali. Biasanya, animator membuat animasi pada frekuensi 30fps, dan di game level AAA, karakter biasanya memiliki sekitar 100 tulang. Berdasarkan jumlah informasi ini dan format 8 float, kita membutuhkan sekitar 3 KB per pose dan 94 KB per detik animasi sebagai hasilnya. Nilai cepat menumpuk dan pada beberapa platform dapat dengan mudah menyumbat semua memori.

Jadi mari kita bicara tentang kompresi; Saat mencoba mengompres data, ada beberapa aspek yang perlu dipertimbangkan:

  • Rasio kompresi
    • Berapa banyak yang kami kelola untuk mengurangi jumlah memori yang digunakan
  • Kualitas
    • Berapa banyak informasi yang kami kehilangan dari sumber data
  • Tingkat kompresi
    • .

Saya terutama mementingkan kualitas dan kecepatan, dan kurang memperhatikan memori. Selain itu, saya bekerja dengan animasi game, dan saya dapat mengambil keuntungan dari kenyataan bahwa pada kenyataannya, untuk mengurangi beban pada memori, kita tidak harus menggunakan perpindahan dan skala dalam data. Karena ini, kita dapat menghindari penurunan kualitas yang disebabkan oleh penurunan jumlah bingkai dan solusi lain dengan kerugian.

Penting juga untuk dicatat bahwa Anda tidak boleh meremehkan efek kompresi animasi terhadap kinerja: di salah satu proyek saya sebelumnya, laju pengambilan sampel menurun sekitar 35%, dan ada juga beberapa masalah kualitas.

Saat kami mulai bekerja dengan kompresi data animasi, ada dua bidang penting utama yang perlu dipertimbangkan:

  • Seberapa cepat kita dapat mengompres elemen-elemen informasi individual dalam bingkai kunci (angka empat, float, dll.).
  • Bagaimana kita bisa mengompresi urutan frame kunci untuk menghapus informasi yang berlebihan.

Diskritisasi data


Hampir semua bagian ini dapat direduksi menjadi satu prinsip: diskritkan data.

Diskretisasi adalah cara yang sulit untuk mengatakan bahwa kami ingin mengonversi nilai dari interval kontinu ke serangkaian nilai terpisah.

Float Diskretisasi


Ketika datang ke pengambilan sampel nilai float, kami berusaha untuk mengambil nilai float dan mewakilinya sebagai integer menggunakan bit lebih sedikit. Kuncinya adalah bilangan bulat mungkin tidak benar-benar mewakili nomor sumber, tetapi nilai dalam interval diskrit dipetakan ke interval kontinu. Biasanya pendekatan yang sangat sederhana digunakan. Untuk mencicipi suatu nilai, pertama-tama kita perlu interval untuk nilai aslinya; Setelah menerima interval ini, kami menormalkan nilai awal untuk interval ini. Kemudian nilai normal ini dikalikan dengan nilai maksimum yang mungkin untuk ukuran output yang diinginkan yang diberikan dalam bit. Yaitu, untuk 16 bit kita mengalikan nilainya dengan 65535. Kemudian nilai yang dihasilkan dibulatkan ke bilangan bulat terdekat dan disimpan. Ini jelas ditunjukkan pada gambar:


Contoh pengambilan sampel float 32-bit ke integer 16-bit yang tidak ditandatangani

Untuk mendapatkan nilai asli lagi, kami cukup melakukan operasi dalam urutan terbalik. Penting untuk dicatat di sini bahwa kita perlu merekam suatu interval awal dari suatu nilai; jika tidak, kami tidak akan dapat men-decode nilai sampel. Jumlah bit dalam nilai sampel menentukan ukuran langkah dalam interval yang dinormalisasi, dan oleh karena itu ukuran langkah dalam interval asli: nilai yang didekodekan akan menjadi kelipatan dari ukuran langkah ini, yang memungkinkan kita untuk dengan mudah menghitung kesalahan maksimum yang terjadi karena proses pengambilan sampel, sehingga kita dapat menentukan jumlah bit diperlukan untuk aplikasi kita.

Saya tidak akan memberikan contoh kode sumber, karena ada perpustakaan yang cukup nyaman dan sederhana untuk melakukan operasi pengambilan sampel dasar, yang merupakan sumber yang baik tentang topik ini: https://github.com/r-lyeh-archived/quant (saya akan mengatakan Anda tidak boleh menggunakan fungsi diskretisasi angka empat, tetapi lebih lanjut tentang ini nanti).

Kompresi Quaternion


Kompresi Quaternion adalah topik yang dipelajari dengan baik, jadi saya tidak akan mengulangi apa yang orang lain jelaskan dengan lebih baik. Berikut ini tautan ke pos kompresi snapshot yang memberikan deskripsi terbaik tentang topik ini: https://gafferongames.com/post/snapshot_compression/ .

Namun, saya memiliki sesuatu untuk dikatakan pada topik. Posting bitsquid, yang berbicara tentang kompresi angka empat, menyarankan mengompresi angka empat menjadi 32 bit menggunakan sekitar 10 bit data untuk setiap komponen angka empat. Inilah yang dilakukan Quant, karena didasarkan pada posting bitsquid. Menurut pendapat saya, kompresi seperti itu terlalu hebat dan dalam pengujian saya itu menyebabkan getaran yang kuat. Mungkin penulis menggunakan hierarki karakter yang kurang dalam, tetapi jika Anda mengalikan 15 plus angka empat dari contoh animasi saya, kesalahan gabungan tersebut ternyata cukup serius. Menurut pendapat saya, akurasi minimum absolut adalah 48 bit per angka empat.

Perampingan karena pengambilan sampel


Sebelum kita mulai mempertimbangkan berbagai metode kompresi dan pengaturan catatan, mari kita lihat jenis kompresi apa yang kita dapatkan jika kita menerapkan diskretisasi di sirkuit asli. Kami akan menggunakan contoh yang sama seperti sebelumnya (kerangka 100 tulang), jadi jika Anda menggunakan 48 bit (3 x 16 bit) per angka empat, 48 bit (3 × 16) untuk bergerak dan 16 bit untuk skala, maka total untuk konversi kita membutuhkan 14 byte, bukan 32 byte. Ini adalah 43,75% dari ukuran aslinya. Artinya, untuk 1 detik animasi dengan frekuensi 30FPS, kami mengurangi volume dari sekitar 94 KB menjadi sekitar 41 KB.

Ini tidak buruk sama sekali, diskritisasi adalah operasi yang relatif murah, sehingga ini tidak akan terlalu banyak mempengaruhi waktu pembongkaran. Kami menemukan titik awal yang baik untuk memulai, dan dalam beberapa kasus ini bahkan akan cukup untuk menerapkan animasi dalam anggaran sumber daya dan memastikan kualitas dan kinerja yang sangat baik.

Rekam kompresi


Semuanya menjadi sangat rumit di sini, terutama ketika pengembang mulai mencoba teknik seperti mengurangi bingkai kunci, pemasangan kurva, dll. Juga pada tahap ini kami benar-benar mulai mengurangi kualitas animasi.

Dalam hampir semua keputusan seperti itu, diasumsikan bahwa karakteristik setiap tulang (rotasi, perpindahan, dan skala) disimpan sebagai catatan terpisah. Karena itu, kita dapat membalik sirkuit, seperti yang saya tunjukkan sebelumnya:


Menyimpan data tulang sebagai catatan

Di sini kita hanya menyimpan semua catatan secara berurutan, tetapi juga dapat mengelompokkan semua catatan rotasi, perpindahan, dan skala. Ide dasarnya adalah bahwa kita beralih dari menyimpan data dari setiap pose ke menyimpan catatan.

Setelah melakukan ini, kita dapat menggunakan cara lain untuk mengurangi memori yang digunakan. Yang pertama adalah mulai menjatuhkan frame. Catatan: ini tidak memerlukan format rekaman dan metode ini dapat diterapkan dalam skema sebelumnya. Metode ini berfungsi, tetapi menyebabkan hilangnya gerakan kecil dalam animasi, karena kami membuang sebagian besar data. Teknik ini secara aktif digunakan pada PS3, dan kadang-kadang kami harus membungkuk ke frekuensi pengambilan sampel yang sangat rendah, misalnya, hingga 7 frame per detik (biasanya untuk animasi yang tidak terlalu penting). Saya masih memiliki kenangan buruk dari ini, sebagai seorang programmer animasi saya dengan jelas melihat rincian yang hilang dan ekspresif, tetapi jika Anda melihat dari sudut pandang programmer sistem, kita dapat mengatakan bahwa animasi itu "hampir" sama, karena secara umum gerakan itu tetap ada, tetapi pada saat yang sama kita menghemat banyak memori.

Mari kita hilangkan pendekatan ini (menurut saya, itu terlalu merusak) dan mempertimbangkan opsi lain yang mungkin. Pendekatan populer lainnya adalah membuat kurva untuk setiap catatan dan melakukan pengurangan bingkai kunci pada kurva, mis. menghapus duplikat keyframe. Dari sudut pandang animasi game, dengan pendekatan ini, rekaman gerakan dan skala dikompresi dengan sempurna, kadang-kadang dikurangi menjadi satu kerangka kunci. Solusi ini tidak merusak, tetapi membutuhkan pembongkaran, karena setiap kali kita perlu mendapatkan transformasi, kita harus menghitung kurva, karena kita tidak bisa lagi hanya pergi ke data dalam memori. Situasi dapat ditingkatkan sedikit jika Anda menghitung animasi hanya dalam satu arah.dan menyimpan status sampler dari setiap animasi untuk setiap tulang (yaitu dari mana mendapatkan perhitungan kurva), tetapi Anda harus membayar untuk ini dengan peningkatan memori dan peningkatan signifikan dalam kompleksitas kode. Dalam sistem animasi modern, kita sering tidak memainkan animasi dari awal hingga akhir. Seringkali pada offset waktu tertentu, mereka membuat transisi ke animasi baru berkat hal-hal seperti pencampuran yang disinkronkan atau pencocokan fase. Seringkali kita mengambil sampel secara individu tetapi tidak secara berurutan untuk mengimplementasikan hal-hal seperti mencampurkan membidik / melihat suatu objek, dan seringkali animasi dimainkan dalam urutan terbalik. Oleh karena itu, saya tidak merekomendasikan menggunakan solusi seperti itu, hanya saja tidak sebanding dengan kerumitan yang disebabkan oleh kompleksitas dan potensi bug.

Ada juga konsep tidak hanya menghapus kunci identik pada kurva, tetapi juga menentukan ambang di mana kunci yang sama dihapus; ini mengarah pada fakta bahwa animasi menjadi lebih pudar, mirip dengan metode menjatuhkan frame, karena hasil akhirnya sama dalam hal data. Skema kompresi animasi sering digunakan, di mana parameter kompresi ditetapkan untuk setiap catatan, dan animator terus-menerus tersiksa dengan nilai-nilai ini, berusaha mempertahankan kualitas dan mengurangi ukuran pada saat yang sama. Ini adalah alur kerja yang menyakitkan dan menegangkan, tetapi perlu jika Anda bekerja dengan memori terbatas dari generasi konsol yang lebih tua. Untungnya, hari ini kami memiliki anggaran memori yang besar dan kami tidak perlu hal-hal buruk seperti itu.

Semua aspek ini diungkapkan dalam tulisan Riot / BitSquid dan Nicholas (lihat tautan di awal artikel saya). Saya tidak akan membicarakannya secara rinci. Sebaliknya, saya akan berbicara tentang apa yang saya putuskan tentang mengompresi catatan ...

Saya ... memutuskan untuk tidak mengompresi catatan.

Sebelum Anda mulai melambaikan tangan, izinkan saya menjelaskan ...

Ketika saya menyimpan data dalam catatan, saya menyimpan data rotasi untuk semua frame. Ketika datang ke gerakan dan skala, saya melacak apakah gerakan dan skala statis selama kompresi, dan jika demikian, saya hanya menyimpan satu nilai per catatan. Yaitu, jika catatan bergerak sepanjang X, tetapi tidak sepanjang Y dan Z, maka saya menyimpan semua nilai memindahkan catatan sepanjang X, tetapi hanya satu nilai memindahkan catatan sepanjang Y dan Z.

Situasi ini muncul untuk sebagian besar tulang di sekitar 95% dari animasi kita, jadi pada akhirnya kita dapat secara signifikan mengurangi memori yang ditempati, benar-benar tanpa kehilangan kualitas. Ini membutuhkan kerja dari sudut pandang pembuatan konten (DCC): kami tidak ingin tulang memiliki sedikit gerakan dan memperbesar alur kerja pembuatan animasi, tetapi manfaat seperti itu sepadan dengan biaya tambahan.

Dalam contoh animasi kami, hanya ada dua catatan dengan bergerak dan tidak ada catatan dengan skala. Kemudian untuk 1 detik animasi, volume data berkurang dari 41 KB menjadi 18,6 KB (yaitu, hingga 20% dari volume data asli). Situasi menjadi lebih baik ketika durasi animasi meningkat, kita hanya menghabiskan sumber daya untuk merekam giliran dan gerakan dinamis, dan biaya rekaman statis tetap konstan, yang menghemat lebih banyak dalam animasi yang panjang. Dan kita tidak harus mengalami kehilangan kualitas yang disebabkan oleh pengambilan sampel.

Dengan semua informasi ini dalam pikiran, skema data akhir saya terlihat seperti ini:


Contoh skema data animasi terkompresi (3 frame per record)

Selain itu, saya menyimpan offset di blok data untuk memulai data setiap tulang. Ini perlu karena kadang-kadang kita perlu sampel data hanya untuk satu tulang tanpa membaca seluruh pose. Ini memberi kami cara cepat untuk langsung mengakses data rekaman.

Selain data animasi yang disimpan dalam satu blok memori, saya juga memiliki opsi kompresi untuk setiap catatan:


Contoh parameter kompresi untuk catatan dari mesin Kruger saya

. Parameter ini menyimpan semua data yang saya perlukan untuk mendekode nilai sampel dari setiap catatan. Mereka juga memantau catatan statis sehingga saya tahu bagaimana menangani data terkompresi ketika saya menemukan catatan statis saat pengambilan sampel.

Anda juga dapat melihat bahwa diskritisasi untuk setiap record adalah individual: selama kompresi, saya melacak nilai minimum dan maksimum dari setiap karakteristik (misalnya, bergerak di sepanjang X) dari setiap record untuk memastikan bahwa data tersebut diskritisasi dalam interval minimum / maksimum dan menjaga akurasi maksimum. Saya tidak berpikir bahwa secara umum dimungkinkan untuk membuat interval pengambilan sampel global tanpa merusak data Anda (ketika nilainya berada di luar interval) dan tanpa membuat kesalahan signifikan.

Bagaimanapun, inilah ringkasan singkat dari upaya bodoh saya untuk menerapkan kompresi animasi: pada akhirnya, saya hampir menggunakan kompresi.

All Articles