دليل ضغط الرسوم المتحركة الهيكل العظمي


ستكون هذه المقالة نظرة عامة موجزة عن كيفية تنفيذ مخطط ضغط متحرك بسيط وبعض المفاهيم ذات الصلة. أنا لست خبيرًا في هذا الأمر بأي حال من الأحوال ، ولكن هناك القليل جدًا من المعلومات حول هذا الموضوع ، وهي مجزأة تمامًا. إذا كنت تريد قراءة مقالات أعمق حول هذا الموضوع ، فأوصيك بالانتقال إلى الروابط التالية:


قبل أن نبدأ ، من الجدير إعطاء مقدمة موجزة للرسوم المتحركة الهيكلية وبعض مفاهيمها الأساسية.

أساسيات الرسوم المتحركة والضغط


الرسوم المتحركة للهيكل العظمي موضوع بسيط جدًا ، إذا نسيت عن السلخ. لدينا مفهوم الهيكل العظمي الذي يحتوي على تحولات عظام الشخصية. يتم تخزين هذه التحولات العظمية في شكل هرمي. في الواقع ، يتم تخزينها كدلتا بين موقعها العالمي وموقف الوالدين. المصطلحات هنا مربكة ، لأنه في محرك اللعبة ، يُطلق على المحلي اسم مساحة النموذج / الشخصية ، والعالم هو الفضاء العالمي. في مصطلحات الرسوم المتحركة ، يطلق على المحلية اسم مساحة عظم الوالد ، والعالمية إما مساحة الشخصية أو الفضاء العالمي ، اعتمادًا على ما إذا كانت هناك حركة لعظم الجذر ؛ ولكن دعونا لا نقلق بشأن هذا كثيرا. الشيء المهم هو أن التحولات العظمية يتم تخزينها محليًا بالنسبة لآبائهم. هذا له العديد من المزايا ، وخاصة عند الخلط (المزج):إذا كان اختلاط الموقفين عالميًا ، فسيتم تقريبهما خطيًا في الموضع ، مما سيؤدي إلى زيادة ونقص العظام وتشوه الشخصية.وإذا كنت تستخدم الدلتا ، فسيتم الخلط من فرق إلى آخر ، لذلك إذا كان تحويل دلتا لعظمة واحدة بين وضعيتين هو نفسه ، فإن طول العظم يبقى ثابتًا. أعتقد أنه من الأسهل (ولكن ليس دقيقًا تمامًا) أن نأخذها بهذه الطريقة: يؤدي استخدام الدلتا إلى حركة "كروية" لمواضع العظام أثناء الخلط ، ويؤدي خلط التحولات العالمية إلى حركة خطية لمواقف العظام.

الرسوم المتحركة للهيكل العظمي هي مجرد قائمة مرتبة من الإطارات الرئيسية بمعدل إطارات ثابت (عادة). الإطار الرئيسي هو وضع الهيكل العظمي. إذا أردنا الحصول على وضعية بين الإطارات الأساسية ، فإننا نأخذ عينات من الإطارات الرئيسية ونمزج بينهما ، باستخدام جزء الوقت بينهما كوزن المزيج. توضح الصورة أدناه رسمًا متحركًا تم إنشاؤه بسرعة 30 إطارًا في الثانية. تحتوي الرسوم المتحركة على ما مجموعه 5 إطارات ونحتاج إلى الحصول على الوضع 0.52 ثانية بعد البداية. لذلك ، نحتاج إلى اختبار الوضع في الإطار 1 والوضع في الإطار 2 ، ثم نمزج بينهما بوزن مزيج يبلغ حوالي 57٪.


مثال على الرسوم المتحركة لخمسة إطارات وطلب لوقفة في وقت إطار متوسط

بعد الحصول على المعلومات المذكورة أعلاه والاعتقاد بأن الذاكرة ليست مشكلة بالنسبة لنا ، فإن الحفظ التسلسلي للوضع سيكون الطريقة المثالية لتخزين الرسوم المتحركة ، كما هو موضح أدناه:


تخزين بيانات الرسوم المتحركة بسيط

لماذا هذا مثالي؟ يأتي أخذ عينات لأي إطار رئيسي إلى عملية memcpy بسيطة. يتطلب أخذ عينات للوضع المتوسط ​​عمليتين memcpy وعملية خلط واحدة. من وجهة نظر ذاكرة التخزين المؤقت ، نقوم بنسخ باستخدام كتلتي بيانات memcpy بالترتيب ، أي بعد نسخ الإطار الأول ، سيكون لدى أحد ذاكرة التخزين المؤقت بالفعل إطار ثانٍ. يمكنك أن تقول: انتظر ، عندما نقوم بالخلط ، نحتاج إلى مزج جميع العظام ؛ ماذا لو لم يتغير معظمها بين الإطارات؟ ألن يكون من الأفضل تخزين العظام كسجلات وخلط التحولات المتغيرة فقط؟ حسنًا ، إذا تم تحقيق ذلك ، فمن المحتمل حدوث المزيد من الأخطاء في ذاكرة التخزين المؤقت عند قراءة السجلات الفردية ، وبعد ذلك ستحتاج إلى تتبع التحويلات التي تحتاج إلى مزجها ، وما إلى ذلك ... قد يبدو المزج الكثير من العمل الذي يستغرق وقتًا طويلاً ،ولكن في جوهره هو تطبيق تعليمة واحدة على كتلتين من الذاكرة الموجودة بالفعل في ذاكرة التخزين المؤقت. بالإضافة إلى ذلك ، فإن كود الخلط بسيط نسبيًا ، وغالبًا ما يكون مجرد مجموعة من تعليمات SIMD دون التفرع ، وسيعالجها المعالج الحديث في غضون لحظات.

تكمن المشكلة في هذا النهج في أنه يأخذ مساحة كبيرة جدًا من الذاكرة ، خاصة في الألعاب حيث تكون الشروط التالية صحيحة بالنسبة لـ 95٪ من البيانات.

  • للعظام طول ثابت
    • الشخصيات في معظم الألعاب لا تمدد العظام ، وبالتالي ، في نفس الرسوم المتحركة ، تكون سجلات التحولات ثابتة.
  • نحن عادة لا نقيس العظام.
    • نادرًا ما يتم استخدام المقياس في الرسوم المتحركة للعبة. يتم استخدامه بنشاط في الأفلام و VFX ، ولكن القليل جدًا في الألعاب. حتى عند استخدامها ، يتم استخدام نفس المقياس عادة.
    • في الواقع ، في معظم الرسوم المتحركة التي قمت بإنشائها في وقت التشغيل ، استفدت من هذه الحقيقة واحتفظت بتحول العظام بالكامل في 8 متغيرات عائمة: 4 لتدوير الرباعي ، 3 للتحريك و 1 للقياس. هذا يقلل بشكل كبير من حجم الوضعية في وقت التشغيل ، مما يوفر إنتاجية متزايدة عند المزج والنسخ.

مع وضع كل هذا في الاعتبار ، إذا نظرت إلى تنسيق البيانات الأصلي ، يمكنك أن ترى مدى عدم كفاءة الذاكرة التي تنفقها. نكرر قيم الإزاحة والقياس لكل عظمة ، حتى لو لم تتغير. والوضع يخرج عن السيطرة بسرعة. عادة ، يقوم الرسوم المتحركة بإنشاء رسوم متحركة بتردد 30 إطارًا في الثانية ، وفي الألعاب على مستوى AAA ، عادة ما يكون للشخص حوالي 100 عظمة. بناءً على هذا الكم من المعلومات وتنسيق 8 تعويم ، نحتاج إلى حوالي 3 كيلوبايت لكل وضع و 94 كيلوبايت في الثانية من الرسوم المتحركة نتيجة لذلك. تتراكم القيم بسرعة ويمكن أن تسد جميع الذاكرة بسهولة على بعض الأنظمة الأساسية.

فلنتحدث عن الضغط ؛ عند محاولة ضغط البيانات ، هناك عدة جوانب يجب وضعها في الاعتبار:

  • نسبة الضغط
    • كم تمكنا من تقليل حجم الذاكرة المشغولة
  • جودة
    • مقدار المعلومات التي فقدناها من بيانات المصدر
  • معدل الضغط
    • .

أنا مهتم بشكل أساسي بالجودة والسرعة ، وأقل قلقا بشأن الذاكرة. بالإضافة إلى ذلك ، أعمل مع الرسوم المتحركة للألعاب ، ويمكنني الاستفادة من حقيقة أنه في الواقع ، لتقليل الحمل على الذاكرة ، لا يتعين علينا استخدام الإزاحة والقياس في البيانات. ونتيجة لذلك ، يمكننا تجنب انخفاض الجودة الناتج عن انخفاض عدد الإطارات والحلول الأخرى مع الخسائر.

من المهم للغاية ملاحظة أنه لا يجب الاستهانة بتأثير ضغط الرسوم المتحركة على الأداء: في أحد مشروعاتي السابقة ، انخفض معدل أخذ العينات بنحو 35٪ ، وكانت هناك أيضًا بعض مشاكل الجودة.

عندما نبدأ العمل بضغط بيانات الرسوم المتحركة ، هناك مجالان رئيسيان مهمان يجب مراعاتهما:

  • مدى السرعة التي يمكننا بها ضغط العناصر الفردية للمعلومات في إطار رئيسي (رباعيات ، عائم ، وما إلى ذلك).
  • كيف يمكننا ضغط تسلسل الإطارات الرئيسية لإزالة المعلومات الزائدة.

فصل البيانات


يمكن اختزال كل هذا القسم تقريبًا إلى مبدأ واحد: تشويه البيانات.

إن التمييز هو طريقة صعبة للقول إننا نريد تحويل قيمة من فاصل مستمر إلى مجموعة منفصلة من القيم.

تعويم التمييز


عندما يتعلق الأمر بأخذ عينات للقيم العائمة ، فإننا نسعى جاهدين لأخذ هذه القيمة العائمة وتمثيلها كعدد صحيح باستخدام وحدات بت أقل. الحيلة هي أن العدد الصحيح قد لا يمثل في الواقع رقم المصدر ، ولكن القيمة في فاصل منفصل معين إلى فاصل مستمر. عادة ما يتم استخدام نهج بسيط للغاية. لأخذ قيمة ، نحتاج أولاً إلى فاصل للقيمة الأصلية ؛ بعد تلقي هذا الفاصل الزمني ، نقوم بتطبيع القيمة الأولية لهذا الفاصل الزمني. ثم يتم ضرب هذه القيمة المقيسة في القيمة القصوى الممكنة لحجم الإخراج المطلوب بالبتات. أي أنه بالنسبة إلى 16 بتة نقوم بضرب القيمة في 65535. ثم يتم تقريب القيمة الناتجة إلى أقرب عدد صحيح وتخزينها. يظهر هذا بوضوح في الصورة:


مثال على أخذ عينات من عوامة 32 بت إلى عدد صحيح 16 بت غير موقّع

للحصول على القيمة الأصلية مرة أخرى ، نقوم ببساطة بتنفيذ العمليات بالترتيب العكسي. من المهم أن نلاحظ هنا أننا بحاجة إلى تسجيل الفاصل الأولي للقيمة في مكان ما ؛ وإلا ، فلن نتمكن من فك ترميز قيمة العينة. يحدد عدد البتات في قيمة العينة حجم الخطوة في الفاصل الزمني المعياري ، وبالتالي حجم الخطوة في الفاصل الزمني الأصلي: ستكون القيمة التي تم فك تشفيرها مضاعفة لحجم الخطوة هذا ، مما يسمح لنا بسهولة حساب الحد الأقصى للخطأ الذي يحدث بسبب عملية أخذ العينات ، حتى نتمكن من تحديد عدد البتات المطلوبة لتطبيقنا.

لن أعطي أمثلة على التعليمات البرمجية المصدر ، لأن هناك مكتبة مريحة وبسيطة إلى حد ما لأداء عمليات أخذ العينات الأساسية ، وهو مصدر جيد لهذا الموضوع: https://github.com/r-lyeh-archived/quant (أود أن أقول أنه لا يجب عليك استخدام وظيفة التمييع qu quention ، ولكن المزيد عن هذا لاحقًا).

ضغط رباعي


ضغط Quaternion هو موضوع مدروس جيدًا ، لذلك لن أكرر ما شرحه الآخرون بشكل أفضل. في ما يلي رابط إلى منشور ضغط لقطة يوفر أفضل وصف لهذا الموضوع: https://gafferongames.com/post/snapshot_compression/ .

ومع ذلك ، لدي ما أقوله حول هذا الموضوع. تشير مشاركات bitsquid ، التي تتحدث عن ضغط الرباعي ، إلى ضغط الرباعية إلى 32 بت باستخدام حوالي 10 بت من البيانات لكل مكون رباعي. هذا هو بالضبط ما يفعله Quant ، لأنه يعتمد على المنشورات ذات bitsquid. في رأيي ، مثل هذا الضغط كبير جدًا وفي اختباراتي تسبب في اهتزاز قوي. ربما استخدم المؤلفون تسلسلات هرمية أقل عمقًا للشخصية ، ولكن إذا قمت بضرب أكثر من 15 فصلاً في أمثلة الرسوم المتحركة الخاصة بي ، فقد تبين أن الخطأ المجمع خطير للغاية. في رأيي، المطلق الحد الأدنى من الدقة 48 بت لكل من أربعة.

تقليص الحجم بسبب أخذ العينات


قبل أن نبدأ في التفكير في طرق الضغط المختلفة وترتيب السجلات ، دعنا نرى نوع الضغط الذي نحصل عليه إذا قمنا ببساطة بتطبيق التقسيم في الدائرة الأصلية. سنستخدم نفس المثال كما كان من قبل (هيكل عظمي من 100 عظمة) ، لذلك إذا استخدمنا 48 بت (3 × 16 بت) لكل ربع ، و 48 بت (3 × 16) للتحرك و 16 بت للقياس ، ثم في المجموع للتحويل نحتاج 14 بايت بدلاً من 32 بايت. هذا هو 43.75 ٪ من الحجم الأصلي. أي لمدة ثانية واحدة من الرسوم المتحركة بتردد 30 إطارًا في الثانية ، قمنا بتقليل الصوت من حوالي 94 كيلوبايت إلى حوالي 41 كيلوبايت.

هذا ليس سيئًا على الإطلاق ، فالتمرد عملية منخفضة التكلفة نسبيًا ، لذلك لن يؤثر ذلك على وقت تفريغها كثيرًا. لقد وجدنا نقطة انطلاق جيدة للبداية ، وفي بعض الحالات سيكون هذا كافيًا لتنفيذ الرسوم المتحركة ضمن ميزانية الموارد وضمان جودة وأداء ممتازين.

ضغط التسجيل


يصبح كل شيء معقدًا للغاية هنا ، خاصة عندما يبدأ المطورون في تجربة تقنيات مثل تقليل الإطار الرئيسي ، وتركيب المنحنى ، وما إلى ذلك. أيضًا في هذه المرحلة ، بدأنا حقًا في تقليل جودة الرسوم المتحركة.

في جميع هذه القرارات تقريبًا ، يُفترض أن خصائص كل عظم (الدوران والإزاحة والقياس) يتم تخزينها كسجل منفصل. لذلك ، يمكننا قلب الدائرة ، كما أظهرتها سابقًا:


حفظ بيانات العظام كسجلات

هنا نقوم ببساطة بحفظ جميع السجلات بالتسلسل ، ولكن يمكننا أيضًا تجميع جميع سجلات التدوير وعمليات الإزاحة والمقاييس. الفكرة الأساسية هي أننا ننتقل من تخزين البيانات من كل وقفة إلى تخزين السجلات.

بعد القيام بذلك ، يمكننا استخدام طرق أخرى لتقليل الذاكرة المشغولة. الأول هو البدء في إسقاط الإطارات. ملحوظة: لا يتطلب هذا تنسيق سجل ويمكن تطبيق هذه الطريقة في المخطط السابق. تعمل هذه الطريقة ، لكنها تؤدي إلى فقدان الحركات الصغيرة في الرسوم المتحركة ، لأننا نتجاهل معظم البيانات. تم استخدام هذه التقنية بنشاط على PS3 ، وأحيانًا اضطررنا إلى الانحناء إلى ترددات أخذ عينات منخفضة بجنون ، على سبيل المثال ، حتى 7 إطارات في الثانية (عادةً للرسوم المتحركة غير المهمة جدًا). لدي ذكريات سيئة من هذا ، كمبرمج للرسوم المتحركة أرى بوضوح التفاصيل الضائعة والتعبير ، ولكن إذا نظرت من وجهة نظر مبرمج النظام ، يمكننا القول أن الرسوم المتحركة هي "تقريبًا" هي نفسها ، لأنه بشكل عام يتم الحفاظ على الحركة ، ولكن في نفس الوقت حفظ الكثير من الذاكرة.

دعونا نتجاهل هذا النهج (في رأيي ، إنه مدمر للغاية) وننظر في الخيارات الممكنة الأخرى. نهج شائع آخر هو إنشاء منحنى لكل سجل وإجراء تقليل الإطارات الرئيسية على المنحنى ، أي إزالة الإطارات الرئيسية المكررة. من وجهة نظر الرسوم المتحركة للعبة ، مع هذا النهج ، يتم ضغط تسجيلات الحركة والقياس بشكل مثالي ، وأحيانًا يتم تقليلها إلى إطار رئيسي واحد. هذا الحل غير مدمر ، ولكنه يتطلب تفريغ ، لأنه في كل مرة نحتاج فيها إلى التحول ، علينا أن نحسب المنحنى ، لأنه لم يعد بإمكاننا الانتقال إلى البيانات الموجودة في الذاكرة. يمكن تحسين الوضع قليلاً إذا قمت بحساب الرسوم المتحركة في اتجاه واحد فقط.وتخزين حالة العينة لكل رسم متحرك لكل عظمة (أي مكان الحصول على حساب المنحنى) ، ولكن عليك أن تدفع مقابل ذلك مع زيادة في الذاكرة وزيادة كبيرة في تعقيد الشفرة. في أنظمة الرسوم المتحركة الحديثة ، غالبًا ما لا نلعب الرسوم المتحركة من البداية إلى النهاية. غالبًا في حالات الإزاحة في وقت معين ، يقومون بتحويلات إلى رسوم متحركة جديدة بفضل أشياء مثل المزج المتزامن أو مطابقة الطور. غالبًا ما نختبر أوضاعًا فردية ولكن غير متتالية لتنفيذ أشياء مثل الخلط بهدف / النظر إلى كائن ، وغالبًا ما يتم تشغيل الرسوم المتحركة بالترتيب العكسي. لذلك ، لا أوصي باستخدام هذا الحل ، فهو ببساطة لا يستحق الإزعاج الناجم عن التعقيد والأخطاء المحتملة.

هناك أيضًا مفهوم ليس فقط حذف المفاتيح المتطابقة على المنحنيات ، ولكن أيضًا تحديد عتبة يتم فيها حذف المفاتيح المماثلة ؛ هذا يؤدي إلى حقيقة أن الرسوم المتحركة تصبح أكثر تلاشيًا ، على غرار طريقة إسقاط الإطارات ، لأن النتيجة النهائية هي نفسها من حيث البيانات. غالبًا ما يتم استخدام مخططات ضغط الرسوم المتحركة ، حيث يتم تعيين معلمات الضغط لكل سجل ، ويتم تعذيب الرسوم المتحركة باستمرار بهذه القيم ، في محاولة للحفاظ على الجودة وتقليل الحجم في نفس الوقت. هذا سير عمل مؤلم ومجهد ، ولكن من الضروري إذا كنت تعمل مع الذاكرة المحدودة للأجيال الأكبر سنا من وحدات التحكم. لحسن الحظ ، لدينا اليوم ميزانية ذاكرة كبيرة ولا نحتاج إلى مثل هذه الأشياء الرهيبة.

تم الكشف عن جميع هذه الجوانب في منشورات Riot / BitSquid و Nicholas (انظر الروابط في بداية مقالتي). لن أتحدث عنها بالتفصيل. بدلاً من ذلك ، سأتحدث عما قررته بشأن ضغط السجلات

... قررت ... عدم ضغط السجلات.

قبل البدء في التلويح ، دعني أشرح ...

عندما أحفظ البيانات في السجلات ، أقوم بتخزين بيانات التدوير لجميع الإطارات. عندما يتعلق الأمر بالحركة والقياس ، فأنا أتتبع ما إذا كانت الحركة والقياس ثابتًا أثناء الضغط ، وإذا كان الأمر كذلك ، فأنا أحفظ قيمة واحدة فقط لكل سجل. بمعنى ، إذا تحرك السجل على طول X ، ولكن ليس على طول Y و Z ، فعندئذٍ أحفظ جميع قيم نقل السجل على طول X ، ولكن فقط قيمة واحدة لنقل السجل على طول Y و Z.

ينشأ هذا الموقف بالنسبة لمعظم العظام في حوالي 95٪ من الرسوم المتحركة لدينا ، لذلك في النهاية يمكننا تقليل الذاكرة المشغولة بشكل كبير ، دون فقدان الجودة على الإطلاق. يتطلب هذا العمل من وجهة نظر إنشاء المحتوى (DCC): لا نريد أن يكون للعظام حركات طفيفة وتكبير في سير عمل إنشاء الرسوم المتحركة ، ولكن هذه الفائدة تستحق التكلفة الإضافية.

في مثال الرسوم المتحركة لدينا ، هناك سجلان فقط مع نقل ولا توجد سجلات ذات مقياس. ثم لمدة ثانية واحدة من الرسوم المتحركة ، ينخفض ​​حجم البيانات من 41 كيلوبايت إلى 18.6 كيلوبايت (أي ما يصل إلى 20٪ من حجم البيانات الأصلية). يصبح الوضع أفضل عندما تزيد مدة الرسوم المتحركة ، ولا ننفق الموارد إلا على تسجيل المنعطفات والحركات الديناميكية ، وتبقى تكاليف التسجيلات الثابتة ثابتة ، مما يوفر المزيد في الرسوم المتحركة الطويلة. وليس علينا أن نختبر فقدان الجودة الناتج عن أخذ العينات.

مع وضع كل هذه المعلومات في الاعتبار ، يبدو مخطط البيانات النهائي كما يلي:


مثال على مخطط بيانات الرسوم المتحركة المضغوط (3 إطارات لكل سجل)

بالإضافة إلى ذلك ، أحفظ الإزاحة في كتلة البيانات لبدء بيانات كل عظمة. هذا ضروري لأنه في بعض الأحيان نحتاج إلى أخذ عينات من البيانات لعظم واحد فقط دون قراءة الوضعية بأكملها. هذا يوفر لنا طريقة سريعة للوصول مباشرة إلى بيانات التسجيل.

بالإضافة إلى بيانات الرسوم المتحركة المخزنة في كتلة ذاكرة واحدة ، لدي أيضًا خيارات ضغط لكل سجل:


مثال لمعلمات الضغط للسجلات من محرك Kruger الخاص بي

تخزن هذه المعلمات جميع البيانات التي أحتاجها لفك ترميز القيم المعينة لكل سجل. كما أنهم يراقبون البيانات الثابتة للسجلات حتى أعرف كيفية التعامل مع البيانات المضغوطة عندما أعثر على سجل ثابت عند أخذ العينات.

يمكنك أيضًا ملاحظة أن التقدير لكل سجل هو فردي: أثناء الضغط ، أتتبع القيم الدنيا والقصوى لكل خاصية (على سبيل المثال ، الانتقال على طول X) لكل سجل للتأكد من أن البيانات يتم فصلها خلال الحد الأدنى / الحد الأقصى للفاصل والحفاظ على أقصى قدر من الدقة. لا أعتقد أنه من الممكن بشكل عام إنشاء فترات زمنية عامة لأخذ العينات دون تدمير بياناتك (عندما تكون القيم خارج الفترة الزمنية) ودون ارتكاب أخطاء كبيرة.

مع ذلك ، إليك ملخصًا موجزًا ​​لمحاولاتي الغبية لتطبيق ضغط الرسوم المتحركة: في النهاية ، أستخدم الضغط تقريبًا.

All Articles