محاكاة فيزيائية لمئات الآلاف من الجسيمات على Unity + DOTS

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

صورة

والنتيجة هي على النحو التالي:

حيوية


من أجل تحقيق ذلك ، تحتاج إلى حل المشاكل التالية:

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

الفيزياء


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

تقليل المسافة إلى وضع البداية


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

المسافة من المؤشر


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

التوهين


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

نتيجة


ونتيجة لذلك ، نحصل على صيغة القوة التي تعمل على الجسيم في لحظة اعتباطية:

Fs=Ca(xx0)Cr||xxr||2xxr||xxr||Cdv,


أين x,v- الموقع الحالي وسرعة الجسيمات ، x0- موقع البداية xr- موضع المؤشر ، Ca,Cr,Cd- معاملات الجاذبية والتنافر والتوهين على التوالي.

رسوم متحركة أخرى


سلوك البرمجة


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

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

إن جوهر كتابة برنامج داخل ECS هو فصل الشفرة إلى مكونات ( مكونات ) تصف الحالة والأنظمة ( الأنظمة ) التي تصف سلوك وتفاعل المكونات والكيانات - الأشياء التي تحتوي على مجموعة من المكونات.

مكونات


سنحتاج إلى المكونات التالية:

//    ( )
public struct AttractorPosData : IComponentData
{
   public float2 Value;
}

//  
public struct VelocityData : IComponentData
{
   public float2 Value;
} 

//  
public struct AccelerationData : IComponentData
{
   public float2 Value;
}

//   
public struct ParticleData : IComponentData 
{
    public Color color;
    public Matrix4x4 matrix;
}

مع المكونات ، كل شيء بسيط للغاية ، انتقل إلى الأنظمة:

//   
[UpdateBefore(typeof(MoveSystem))]
public class VelocityUpdateSystem : SystemBase
{
    protected override void OnUpdate()
    {
        var time = Time.DeltaTime;
        Entities.ForEach((ref VelocityData velocity, 
                in AccelerationData acceleration) =>
            {
                velocity.Value += time * acceleration.Value;
            })
            .ScheduleParallel();
    }
}

//   
public class MoveSystem : SystemBase
{
    protected override void OnUpdate()
    {
        var time = Time.DeltaTime;
        Entities.ForEach((ref Translation t, in VelocityData velocity) =>
            {
                t.Value.xy += time * velocity.Value;
            })
            .ScheduleParallel();
    }
}

يمكنك التحكم في تسلسل الأنظمة باستخدام السمات

[UpdateBeforeAttribute]
[UpdateAfterAttribute]

سيكون أكبر نظام يصف السلوك هو نظام تحديث التسارع ، الذي يعمل وفقًا للصيغة المشتقة :

[UpdateBefore(typeof(VelocityUpdateSystem))]
public class AccelerationSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float2 repulsorPos = Globals.Repulsor.Position;

        //   
        var attractionPower = Globals.SettingsHolder.SettingsModel.Attraction;
        //  
        var repulsionPower = Globals.SettingsHolder.SettingsModel.Repulsion;
        //  
        var dampingPower = Globals.SettingsHolder.SettingsModel.Damping;
        Entities.ForEach((
                ref AccelerationData acceleration, 
                in AttractorPosData attractorPos, 
                in Translation t,
                in VelocityData velocity) =>
            {
                //  
                var attraction = (attractorPos.Value - t.Value.xy) * attractionPower;

                var distSqr = math.distancesq(repulsorPos, t.Value.xy);
                //  
                var repulsion = -math.normalizesafe(repulsorPos - t.Value.xy) 
                    / distSqr * repulsionPower;

                //  
                var damping = -velocity.Value * dampingPower;

                acceleration.Value = attraction + repulsion + damping;
            })
            .ScheduleParallel();
    }
}

يجعل


على الرغم من العدد الضخم المحتمل للجسيمات التي يجب عرضها ، فبالنسبة للجميع ، يمكنك استخدام نفس الشبكة المربعة من مضلعين ، بالإضافة إلى نفس المادة ، مما سيجعل من الممكن عرضها جميعًا في مكالمة سحب واحدة ، إن لم يكن لمرة واحدة " لكن ": جميع الجسيمات ، بشكل عام ، لها ألوان مختلفة ، وإلا فإن الصورة الناتجة ستكون مملة للغاية.

يمكن لشادر Unity shader القياسي "Sprites / افتراضي" استخدام GPU Instancing لتحسين عرض الكائنات مع نقوش وألوان غير متساوية ، ودمجها في مكالمة سحب واحدة ، ولكن في هذه الحالة ، يجب تعيين ارتباط إلى الملمس واللون لكل كائن محدد من برنامج نصي ، والذي من ECS ليس لدينا وصول.

قد يكون الحل هو أسلوب Graphics.DrawMeshInstanced، والذي يسمح لك برسم شبكة واحدة عدة مرات في مكالمة سحب واحدة مع معلمات مواد مختلفة ، باستخدام نفس GPU Instancing. تبدو هكذا:

[UpdateAfter(typeof(TrsMatrixCalculationSystem))]
public class SpriteSheetRenderer : SystemBase
{
    // DrawMeshInstanced     1023    
    private const int BatchCount = 1023;
    //   
    private readonly List<Matrix4x4> _matrixList = new List<Matrix4x4>(BatchCount);
    //  
    private readonly List<Vector4> _colorList = new List<Vector4>(BatchCount);

    protected override void OnUpdate()
    {
        var materialPropertyBlock = new MaterialPropertyBlock();
        var quadMesh = Globals.Quad;
        var material = Globals.ParticleMaterial;

        //        
        //   
        var shaderPropertyId = Shader.PropertyToID("_Color");

        var entityQuery = GetEntityQuery(typeof(ParticleData));
        //   ,   ParticleData
        var animationData = 
                   entityQuery.ToComponentDataArray<ParticleData>(Allocator.TempJob);
        var layer = LayerMask.NameToLayer("Particles");

        for (int meshCount = 0; 
              meshCount < animationData.Length; 
              meshCount += BatchCount)
        {
            var batchSize = math.min(BatchCount, animationData.Length - meshCount);
            _matrixList.Clear();
            _colorList.Clear();

            for (var i = meshCount; i < meshCount + batchSize; i++)
            {
                _matrixList.Add(animationData[i].matrix);
                _colorList.Add(animationData[i].color);
            }

            materialPropertyBlock.SetVectorArray(shaderPropertyId, _colorList);
            Graphics.DrawMeshInstanced(
                quadMesh,
                0,
                material,
                _matrixList,
                materialPropertyBlock,
                ShadowCastingMode.Off, false, layer);
        }

        animationData.Dispose();
    }
}

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

[UpdateAfter(typeof(MoveSystem))]
public class TrsMatrixCalculationSystem : SystemBase
{
    protected override void OnUpdate()
    {
        //   scale   
        var scale = Globals.SettingsHolder.SettingsModel.ParticlesScale;
        Entities.ForEach((ref ParticleData data, in Translation translation) =>
            {
                data.matrix = Matrix4x4.TRS(translation.Value, Quaternion.identity,
                    new Vector3(scale, scale, scale));
            })
            .ScheduleParallel();
    }
}

الرسوم المتحركة 46000 الجسيمات


أداء


حان الوقت للحديث عن سبب ذلك (بالإضافة إلى الرضا الجمالي بالطبع). في Unity ، في الوقت الحالي ، هناك فرصة للاختيار من بين اثنين من خلفية البرمجة النصية (تطبيقات CLR ): Mono القديم الجيد والحل الأكثر حداثة من مطوري Unity - IL2CPP .

قارن أداء التصميم لهذين التنفيذين:
عدد الجسيمات على الشاشةمتوسط ​​معدل عرض الإطارات ، أحاديمتوسط ​​معدل عرض الإطارات ، IL2CPP
50،000128255
10000066130
200،0003157
مواصفات الكمبيوتر:
QHD 2560x1440
Intel Core i5-9600K
16GB RAM
MSI GeForce RTX 2080 SUPER VENTUS XS OC 8.0 GB


160،000 جزيئات

ملحوظة أنه في تجربة الفراغ الكروي هذه ، يتفوق IL2CPP على أداء Mono حوالي مرتين.

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

Unity Editor


Build من

الملاحظ أن معظم الوقت يقضي في عملية تحويل اللون من اللون إلى Vector4 والإضافة إلى القائمة. (). يمكننا بسهولة التخلص من الأول - سنؤجل التحول في لحظة تكوين الجسيمات ، لأن اللون لا يتغير في العملية:

// 
public struct ParticleData : IComponentData
{
    public Color color;
    public Matrix4x4 matrix;
}
// 
public struct ParticleData : IComponentData
{
    public Vector4 color;
    public Matrix4x4 matrix;
}

سمح لنا هذا التغيير بالتخلص تمامًا من عملية التحويل المكلفة:

عدد الإطارات في الثانية لـ 200.000 جزيئة بعد زيادتها إلى 61. من

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

استنتاج


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

ملاحظة


يمكن للجميع التعرف على المشروع في مستودع github الخاص بي ، ولكن كيفية تثبيت البنية كخلفية لسطح المكتب في Wallpaper Engine : link .

All Articles