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

الوحدة قدمت مؤخرا ECS. في عملية الدراسة ، أصبحت مهتمة بكيفية جعل الرسوم المتحركة و ECS أصدقاء. وفي عملية البحث ، صادفت أسلوبًا مثيرًا للاهتمام استخدمه شباب NORDVEUS في العرض التوضيحي لتقرير Unite Austin 2017.
توحيد أوستن 2017 - معركة ضخمة في الكون Spellsouls.


يحتوي التقرير على العديد من الحلول المثيرة للاهتمام ، ولكن اليوم سنناقش الحفاظ على الرسوم المتحركة الهيكلية في نسيج بهدف تطبيقه.


لماذا هذه الصعوبات ، تسأل؟


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



لقد كنت مهتمًا جدًا بهذا النهج وقررت معرفة ذلك بمزيد من التفصيل ، وبما أنني لم أجد مقالات حول هذا الموضوع ، فقد دخلت في الشفرة. في عملية دراسة المشكلة ، وُلدت هذه المقالة ورؤيتي لحل هذه المشكلة.


لذلك دعونا نقطع الفيل إلى قطع:



  • الحصول على مفاتيح الرسوم المتحركة من مقاطع
  • حفظ البيانات إلى نسيج
  • شبكة (شبكة) إعداد
  • تظليل
  • وضع كل ذلك معا


الحصول على مفاتيح الرسوم المتحركة من مقاطع الرسوم المتحركة


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


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


var boneMatrices = new Matrix4x4[Mathf.CeilToInt(frameRate * clip.length), renderer.bones.Length]; 

في المثال التالي ، قمنا بتغيير إطارات مقطع واحد تلو الآخر وحفظ المصفوفات:


 //       for (var frameIndex = 0; frameIndex < totalFramesInClip; ++frameIndex) { //  : 0 -  , 1 - . var normalizedTime = (float) frameIndex / totalFramesInClip; //     animationState.normalizedTime = normalizedTime; animation.Sample(); //     for (var boneIndex = 0; j < renderer.bones.Length; boneIndex++) { //         var matrix = renderer.bones[boneIndex].localToWorldMatrix * renderer.sharedMesh.bindposes[boneIndex]; //   boneMatrices[i, j] = matrix; } } 

المصفوفات هي 4 في 4 ، ولكن الصف الأخير يبدو دائمًا مثل (0 ، 0 ، 0 ، 1). لذلك ، لغرض التحسين الطفيف ، يمكن تخطيها. وهذا بدوره سوف يقلل من تكلفة نقل البيانات بين المعالج وبطاقة الفيديو.


 a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 0 0 0 1 

حفظ البيانات إلى نسيج


لحساب حجم النسيج ، نقوم بضرب إجمالي عدد الإطارات في جميع مقاطع الرسوم المتحركة بعدد العظام وعدد الصفوف في المصفوفة (اتفقنا على حفظ الصفوف الثلاثة الأولى).


 var dataSize = numberOfBones * numberOfKeyFrames * MATRIX_ROWS_COUNT); //      var size = NextPowerOfTwo((int) Math.Sqrt(dataSize)); var texture = new Texture2D(size, size, TextureFormat.RGBAFloat, false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Point, anisoLevel = 0 }; 

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


 Clip0[Frame0[Bone0[row0,row1,row2]...BoneN[row0,row1,row2].]...FramM[bone0[row0,row1,row2]...ClipK[...] 

ما يلي هو رمز الحفظ:


 var textureColor = new Color[texture.width * texture.height]; var clipOffset = 0; for (var clipIndex = 0; clipIndex < sampledBoneMatrices.Count; clipIndex++) { var framesCount = sampledBoneMatrices[clipIndex].GetLength(0); for (var keyframeIndex = 0; keyframeIndex < framesCount; keyframeIndex++) { var frameOffset = keyframeIndex * numberOfBones * 3; for (var boneIndex = 0; boneIndex < numberOfBones; boneIndex++) { var index = clipOffset + frameOffset + boneIndex * 3; var matrix = sampledBoneMatrices[clipIndex][keyframeIndex, boneIndex]; textureColor[index + 0] = matrix.GetRow(0); textureColor[index + 1] = matrix.GetRow(1); textureColor[index + 2] = matrix.GetRow(2); } } } texture.SetPixels(textureColor); texture.Apply(false, false); 

شبكة (شبكة) إعداد


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


 var boneWeights = mesh.boneWeights; var boneIds = new List<Vector4>(mesh.vertexCount); var boneInfluences = new List<Vector4>(mesh.vertexCount); for (var i = 0; i < mesh.vertexCount; i++) { boneIds.Add(new Vector4(bw.boneIndex0, bw.boneIndex1, bw.boneIndex2, bw.boneIndex3); boneInfluences.Add(new Vector4(bw.weight0, bw.weight1, bw.weight2, bw.weight3)); } mesh.SetUVs(1, boneIds); mesh.SetUVs(2, boneInfluences); 

تظليل


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


 // frameOffset = clipOffset + frameIndex * clipLength * 3 -     CPU // boneIndex -      ,   UV1 int index = frameOffset + boneIndex * 3; 

إذن ، حصلنا على فهرس الصف الأول من المصفوفة ، وسيكون فهرس الصفين الثاني والثالث هو +1 ، +2 ، على التوالي. يبقى ترجمة الفهرس أحادي البعد إلى الإحداثيات الطبيعية للنسيج ولهذا نحتاج إلى حجم الملمس.


 inline float4 IndexToUV(int index, float2 size) { return float4(((float)((int)(index % size.x)) + 0.5) / size.x, ((float)((int)(index / size.x)) + 0.5) / size.y, 0, 0); } 

بعد طرح الصفوف ، نجمع المصفوفة دون أن ننسى الصف الأخير ، الذي يساوي دائمًا (0 ، 0 ، 0 ، 1).


 float4 row0 = tex2Dlod(frameOffset, IndexToUV(index + 0, animationTextureSize)); float4 row1 = tex2Dlod(frameOffset, IndexToUV(index + 1, animationTextureSize)); float4 row2 = tex2Dlod(frameOffset, IndexToUV(index + 2, animationTextureSize)); float4 row3 = float4(0, 0, 0, 1); return float4x4(row0, row1, row2, row3); 

في الوقت نفسه ، يمكن أن تؤثر عدة عظام على قمة واحدة مرة واحدة. المصفوفة الناتجة ستكون مجموع المصفوفات التي تؤثر على الرأس مضروبة في وزن تأثيرها.


 float4x4 m0 = CreateMatrix(frameOffset, bones.x) * boneInfluences.x; float4x4 m1 = CreateMatrix(frameOffset, bones.y) * boneInfluences.y; float4x4 m2 = CreateMatrix(frameOffset, bones.z) * boneInfluences.z; float4x4 m3 = CreateMatrix(frameOffset, bones.w) * boneInfluences.w; return m0 + m1 + m2 + m3; 

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


وضع كل ذلك معا


لعرض الكائنات ، سوف نستخدم Graphics.DrawMeshInstancedIndirect ، حيث سننقل الشبكة والمواد المعدة. أيضًا ، في المادة ، يجب أن نمرّر الملمس مع الرسوم المتحركة إلى حجم الملمس ومجموعة مع مؤشرات إلى الإطار لكل كائن في الوقت الحالي. كمعلومات إضافية ، فإننا نعبر موضع كل كائن ودورانه. يمكن العثور على كيفية تغيير الموضع والدوران على جانب التظليل في [مقالة] .


في طريقة التحديث ، قم بزيادة الوقت المنقضي من بداية الحركة على Time.deltaTime.


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


 var offset = clipStart + frameIndex * bonesCount * 3.0f 

من المحتمل أن يكون كل ذلك قد قام بتمرير جميع البيانات إلى التظليل ، ونحن نسمي Graphics.DrawMeshInstancedIndirect باستخدام الشبكة والمواد المعدة.


النتائج


أظهر اختبار هذه التقنية على جهاز مزود ببطاقة رسومات 1050 زيادة في الأداء بنحو 2 مرة.


صورة

الرسوم المتحركة من 4000 كائن من نفس النوع على وحدة المعالجة المركزية


صورة

الرسوم المتحركة من 8000 الكائنات من نفس النوع على GPU


في الوقت نفسه ، يُظهر اختبار هذا المشهد على جهاز macbook pro 15 باستخدام بطاقة رسومات مدمجة النتيجة العكسية. GPU يفقد بلا خجل (حوالي 2-3 مرات) ، وهذا ليس مستغربا.


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


مراجع




[رمز مشروع جيثب]

شكرا لاهتمامكم


ملاحظة: أنا جديد على الوحدة ولا أعرف كل التفاصيل الدقيقة ، قد تحتوي المقالة على معلومات غير دقيقة. آمل أن أصلحهم بمساعدتكم وفهم الموضوع بشكل أفضل.


Source: https://habr.com/ru/post/ar452416/


All Articles