GPU منضم. كيفية نقل كل شيء إلى بطاقة الفيديو وأكثر من ذلك بقليل. الرسوم المتحركة

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

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

Oooo ، مرة أخرى الرسوم المتحركة. حول هذا مائة مرة مكتوبة بالفعل ووصفها. ما هو معقد جدا؟ نقوم بتعبئة مصفوفة العظام في المخزن المؤقت / الملمس ، ونستخدمها للتسلق في تظليل قمة الرأس. تم توضيح ذلك مرة أخرى في GPU Gems 3 (الفصل 2. عرض حشد متحرك) . ونفذت في توحيد عرض تقديمي حديث. هل من الممكن بطريقة أخرى؟

تكنوديما من الوحدة


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

في معركة واسعة النطاق ، يقاتل جيشان ، يتألف كل منهما من نوع واحد من الوحدات. هياكل عظمية على اليسار ، فرسان على اليمين. مجموعة متنوعة تتكون كل وحدة من 3 لوود (~ 300 ، ~ 1000 ، ~ 4000 رأسًا لكل منهما) ، وتؤثر عظامان فقط على الرأس. يتكون نظام الرسوم المتحركة من 7 رسوم متحركة لكل نوع من أنواع الوحدات (أذكر أنه يوجد بالفعل 2 منهم). لا تمتزج الرسوم المتحركة ، ولكنها تتحول بشكل منفرد من التعليمات البرمجية البسيطة التي يتم تنفيذها في job'ax ، والتي يتم التأكيد عليها في العرض التقديمي. لا يوجد جهاز الدولة. عندما يكون لدينا نوعان من الشبكات ، يمكنك جذب الحشد بأكمله في مكالمات مسحوبة برسمين. الرسوم المتحركة للهيكل العظمي ، كما كتبت بالفعل ، تعتمد على التكنولوجيا الموصوفة في عام 2009.
مبتكر؟ هم ... طفرة؟ أم ... مناسبة للألعاب الحديثة؟ حسنا ، ربما ، نسبة FPS إلى عدد الوحدات تباهى.

العيوب الرئيسية لهذا النهج (ما قبل المصفوفة في القوام):

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

GPAS


GPU نظام الرسوم المتحركة بالطاقة. الاسم جاء للتو.
كان لنظام الرسوم المتحركة الجديد العديد من المتطلبات:

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

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

دعونا نلقي نظرة على مثال للرسوم المتحركة في عارض الرسوم المتحركة:



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

موقف - Vector3 ، رباعي - Vector4 ، مقياس - Vector3. سيكون لبنية الإطار الرئيسي شيء واحد مشترك (للتبسيط) ، لذلك نحن بحاجة إلى 4 تعويم لتناسب أي من الأنواع المذكورة أعلاه. نحتاج أيضًا إلى InTangent و OutTangent للاستيفاء الصحيح بين الإطارات الأساسية وفقًا للانحناء. نعم ، والوقت الطبيعي لا ينسى:

struct KeyFrame { float4 v; float4 inTan, outTan; float time; }; 

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

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

الآن مثيرة للاهتمام. GPU الرسوم المتحركة الهيكل العظمي.

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

كل إطار مفترض ، بصرف النظر عن الحجم الذي ينتمي إليه (ترجمة ، تدوير ، مقياس) ، يتم تحريفه بنفس الطريقة تمامًا (البحث عن طريق البحث الخطي ، سامحني Knuth):

 void InterpolateKeyFrame(inout float4 rv, int startIdx, int endIdx, float t) { for (int i = startIdx; i < endIdx; ++i) { KeyFrame k0 = keyFrames[i + 0]; KeyFrame k1 = keyFrames[i + 1]; float lerpFactor = (t - k0.time) / (k1.time - k0.time); if (lerpFactor < 0 || lerpFactor > 1) continue; rv = CurveInterpoate(k0, k1, lerpFactor); break; } } 

المنحنى هو منحنى بيزيير مكعب ، وبالتالي فإن وظيفة الاستيفاء تكون على النحو التالي:

 float4 CurveInterpoate(KeyFrame v0, KeyFrame v1, float t) { float dt = v1.time - v0.time; float4 m0 = v0.outTan * dt; float4 m1 = v1.inTan * dt; float t2 = t * t; float t3 = t2 * t; float a = 2 * t3 - 3 * t2 + 1; float b = t3 - 2 * t2 + t; float c = t3 - t2; float d = -2 * t3 + 3 * t2; float4 rv = a * v0.v + b * m0 + c * m1 + d * v1.v; return rv; } 

تم حساب الموقف المحلي (TRS) من العظام. بعد ذلك ، مع تظليل حساب منفصل ، نمزج جميع الرسوم المتحركة اللازمة لهذا العظم. للقيام بذلك ، لدينا مخزن مؤقت مع مؤشرات الرسوم المتحركة والأوزان لكل الرسوم المتحركة في المزيج النهائي. نحصل على هذه المعلومات من جهاز الدولة. يتم حل وضع BlendTree داخل BlendTree على النحو التالي. على سبيل المثال ، هناك شجرة:



سيكون وزن BlendTree Walk 0.35 ، Run - 0.65. وفقًا لذلك ، يجب تحديد الموضع النهائي للعظام من خلال 4 رسوم متحركة: Walk1 و Walk2 و Run1 و Run2. سيكون لأوزانهم قيم (0.35 * 0.92 ، 0.35 * 0.08 ، 0.65 * 0.92 ، 0.65 * 0.08) = (0.322 ، 0.028 ، 0.598 ، 0.052) ، على التوالي. تجدر الإشارة إلى أن مجموع الأوزان يجب أن يكون دائمًا مساويًا لواحد ، أو يتم توفير الأخطاء السحرية.

"قلب" وظيفة المزج:

 float bw = animDef.blendWeight; BoneXForm boneToBlend = animatedBones[srcBoneIndex]; float4 q = boneToBlend.quat; float3 t = boneToBlend.translate; float3 s = boneToBlend.scale; if (dot(resultBone.quat, q) < 0) q = -q; resultBone.translate += t * bw; resultBone.quat += q * bw; resultBone.scale += s * bw; 

يمكنك الآن ترجمة مصفوفة التحول. إيقاف. حول التسلسل الهرمي للعظام منسي تماما.
استنادًا إلى البيانات من الهيكل العظمي ، نقوم ببناء مجموعة من المؤشرات ، حيث تحتوي الخلية التي بها مؤشر العظام على مؤشر الأصل. في الجذر ، اكتب -1.

مثال:



 float4x4 animMat = IdentityMatrix(); float4x4 mat = initialPoses[boneId]; while (boneId >= 0) { BoneXForm b = blendedBones[boneId]; float4x4 xform = MakeTransformMatrix(b.translate, b.quat, b.scale); animMat = mul(animMat, xform); boneId = bonesHierarchyIndices[boneId]; } mat = mul(mat, animMat); resultSkeletons[id] = mat; 

هنا ، من حيث المبدأ ، جميع النقاط الرئيسية لتقديم ومزج الرسوم المتحركة.

GPSM


GPU مدعوم آلة الدولة (هل تفكر في ذلك الحق). سيعمل نظام الرسوم المتحركة الموضح أعلاه بشكل مثالي مع Unity Animation State Machine ، ولكن بعد ذلك ستكون كل الجهود غير مجدية. مع إمكانية حساب عشرات (إن لم يكن المئات) الآلاف من الرسوم المتحركة لكل إطار ، لن تقوم UnityAnimator بسحب الآلاف من أجهزة الحالة العاملة في وقت واحد. هم ...
ما هي آلة الدولة في الوحدة؟ هذا هو نظام مغلق من الحالات والانتقالات ، والتي يتم التحكم فيها بواسطة خصائص رقمية بسيطة. تعمل كل آلة دولة بشكل مستقل عن بعضها البعض ، لنفس مجموعة بيانات الإدخال. انتظر لحظة هذه هي مهمة مثالية لجرافيك وحساب تظليل!

مرحلة الخبز

أولاً ، نحتاج إلى جمع جميع بيانات أجهزة الحالة ووضعها في بنية ملائمة لمعالج GPU. وهذا: الدول (الدول) ، والتحولات (التحولات) والمعلمات (المعلمات).
يتم وضع جميع هذه البيانات في المخازن المؤقتة الخطية ، والتي تتناولها الفهارس.
يعتبر كل مؤشر ترابط حساب الجهاز حالته. يوفر AnimatorController واجهة لجميع هياكل آلة الحالة الداخلية اللازمة.

الهياكل الرئيسية لآلة الدولة:

 struct State { float speed; int firstTransition; int numTransitions; int animDefId; }; struct Transition { float exitTime; float duration; int sourceStateId; int targetStateId; int firstCondition; int endCondition; uint properties; }; struct StateData { int id; float timeInState; float animationLoop; }; struct TransitionData { int id; float timeInTransition; }; struct CurrentState { StateData srcState, dstState; TransitionData transition; }; struct AnimationDef { uint animId; int nextAnimInTree; int parameterIdx; float lengthInSec; uint numBones; uint loop; }; struct ParameterDef { float2 line0ab, line1ab; int runtimeParamId; int nextParameterId; }; struct Condition { int checkMode; int runtimeParamIndex; float referenceValue; }; 

  • تحتوي الحالة على السرعة التي يتم بها تشغيل الحالة ، ومؤشرات شروط الانتقال إلى الآخرين وفقًا لماكينة الحالة.
  • يحتوي الانتقال على مؤشرات الحالة "من" و "إلى". وقت الانتقال ، ووقت الخروج ، ورابط لمجموعة من الشروط لإدخال هذه الحالة.
  • CurrentState عبارة عن كتلة بيانات وقت التشغيل تحتوي على بيانات عن الحالة الحالية لجهاز الحالة.
  • يحتوي AnimationDef على وصف للرسوم المتحركة مع روابط للآخرين المتعلقة به بواسطة BlendTree.
  • ParameterDef هو وصف للمعلمة التي تتحكم في سلوك أجهزة الحالة. Line0ab و Line1ab هي معاملات معادلة الخطوط لتحديد وزن الحركة بواسطة قيمة المعلمة. من هنا:


  • الشرط - تحديد الشرط لمقارنة قيمة وقت تشغيل المعلمة والقيمة المرجعية.

مرحلة التشغيل

يمكن عرض الدورة الرئيسية لكل آلة حالة باستخدام الخوارزمية التالية:



هناك 4 أنواع من المعلمات في Unity animator: float و int و bool و trigger (وهو bool). سوف نقدم كل منهم كما تطفو. عند إعداد الشروط ، من الممكن اختيار واحد من ستة أنواع للمقارنة. إذا == يساوي. IfNot == NotEqual. لذلك سوف نستخدم فقط 4. يتم تمرير مؤشر المشغل إلى حقل checkMode لهيكل الشرط.

 for (int i = t.firstCondition; i < t.endCondition; ++i) { Condition c = allConditions[i]; float paramValue = runtimeParameters[c.runtimeParamIndex]; switch (c.checkMode) { case 3: if (paramValue < c.referenceValue) return false; case 4: if (paramValue > c.referenceValue) return false; case 6: if (abs(paramValue - c.referenceValue) > 0.001f) return false; case 7: if (abs(paramValue - c.referenceValue) < 0.001f) return false; } } return true; 

لبدء الانتقال ، يجب أن تكون جميع الشروط صحيحة. تسميات الحالة الغريبة هي فقط (int) AnimatorConditionMode. منطق المقاطعة هو منطق صعب لمقاطعته واستعادة التحولات.

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

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

بطبيعة الحال ، فإن القياس الكامل مع Unity Animator لم ينجح. مبادئ العمل الداخلية ، إذا لم تكن موصوفة في الوثائق ، كان لا بد من عكسها وجعل التماثلية. بعض الوظائف لم تكتمل بعد (قد لا تكون). على سبيل المثال ، يدعم BlendType في BlendTree 1D فقط. لجعل أنواع أخرى ، من حيث المبدأ ، ليست صعبة ، الآن فقط ليس من الضروري. لا توجد أحداث للرسوم المتحركة ، نظرًا لأنه من الضروري إجراء إعادة قراءة باستخدام وحدة معالجة الرسومات ، وستكون القراءة "الصحيحة" عدة إطارات ، وهو أمر غير مقبول دائمًا. لكن هذا ممكن أيضا.

تقديم


يتم تقديم الوحدة من خلال instancing. وفقًا لـ SV_InstanceID ، في تظليل قمة الرأس ، نحصل على مصفوفة جميع العظام التي تؤثر على قمة الرأس ، ونحولها. لا شيء على الإطلاق غير عادي:

 float4 ApplySkin(float3 v, uint vertexID, uint instanceID) { BoneInfoPacked bip = boneInfos[vertexID]; BoneInfo bi = UnpackBoneInfo(bip); SkeletonInstance skelInst = skeletonInstances[instanceID]; int bonesOffset = skelInst.boneOffset; float4x4 animMat = 0; for (int i = 0; i < 4; ++i) { float bw = bi.boneWeights[i]; if (bw > 0) { uint boneId = bi.boneIDs[i]; float4x4 boneMat = boneMatrices[boneId + bonesOffset]; animMat += boneMat * bw; } } float4 rv = float4(v, 1); rv = mul(rv, animMat); return rv; } 

النتائج


هل هذه المزرعة تعمل بسرعة؟ من الواضح أنه أبطأ من أخذ عينات النسيج باستخدام المصفوفات ، ولكن لا يزال بإمكاني إظهار بعض الأرقام (GTX 970).

فيما يلي 50،000 جهاز دولة:



هنا 280،000 عظام متحركة:



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

سكرتير خاص أريد رمي حجر في حديقة مطوري Unite TechDemo. لديهم عدد كبير من النماذج المتماثلة للأطلال والجسور على خشبة المسرح ، ولم يحسنوا تقديمها بأية طريقة. بدلا من ذلك ، حاولوا من خلال وضع علامة "ثابت". الآن فقط ، في فهارس 16 بت لا يمكنك حشر الكثير من الأشكال الهندسية (ثلاث مرات هاها ، 2017) ولم يجتمع أي شيء ، لأن النماذج متعددة الأضلاع. أضع "تمكين Instancing" لجميع التظليل ، ولم يتم تحديد "ثابت". لم يكن هناك أي دعم ملموس ، لكن ، لعنة ، فأنت تقوم بعمل تجريبي تقني من أجل كل FPS. لا يمكنك حماقة من هذا القبيل.

كان
 *** Summary *** Draw calls: 2553 Dispatch calls: 0 API calls: 8378 Index/vertex bind calls: 2992 Constant bind calls: 648 Sampler bind calls: 395 Resource bind calls: 805 Shader set calls: 682 Blend set calls: 230 Depth/stencil set calls: 92 Rasterization set calls: 238 Resource update calls: 1017 Output set calls: 74 API:Draw/Dispatch call ratio: 3.28163 298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB. Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32) 216 Buffers - 180.11 MB total 17.54 MB IBs 159.81 MB VBs. 1528.06 MB - Grand total GPU buffer + texture load. *** Draw Statistics *** Total calls: 2553, instanced: 2, indirect: 2 Instance counts: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: >=15: ******************************************************************************************************************************** (2) 


أصبح
 *** Summary *** Draw calls: 1474 Dispatch calls: 0 API calls: 11106 Index/vertex bind calls: 3647 Constant bind calls: 1039 Sampler bind calls: 348 Resource bind calls: 718 Shader set calls: 686 Blend set calls: 230 Depth/stencil set calls: 110 Rasterization set calls: 258 Resource update calls: 1904 Output set calls: 74 API:Draw/Dispatch call ratio: 7.5346 298 Textures - 1041.01 MB (1039.95 MB over 32x32), 42 RTs - 306.94 MB. Avg. tex dimension: 1811.77x1810.21 (2016.63x2038.98 over 32x32) 427 Buffers - 93.30 MB total 9.81 MB IBs 80.51 MB VBs. 1441.25 MB - Grand total GPU buffer + texture load. *** Draw Statistics *** Total calls: 1474, instanced: 391, indirect: 2 Instance counts: 1: 2: ******************************************************************************************************************************** (104) 3: ************************************************* (40) 4: ********************** (18) 5: ****************************** (25) 6: ********************************************************************************************* (76) 7: *********************************** (29) 8: ************************************************** (41) 9: ********* (8) 10: ************** (12) 11: 12: ****** (5) 13: ******* (6) 14: ** (2) >=15: ****************************** (25) 


PPS في جميع الأوقات ، كانت الألعاب مرتبطة بشكل أساسي بوحدة المعالجة المركزية ، أي وحدة المعالجة المركزية لم تواكب GPU. الكثير من المنطق والفيزياء. بنقل جزء من منطق اللعبة من وحدة المعالجة المركزية إلى وحدة معالجة الرسومات ، نقوم بإفراغ الأول ونحمّل الثاني ، أي جعل وضع GPU ملزمة على الأرجح. ومن هنا كان عنوان المقال.

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


All Articles