Simulasi fisik ratusan ribu partikel pada Unity + DOTS

Pada satu titik, ketika berkeliaran di hamparan World Wide Web di salah satu situs, saya menemukan elemen JS interaktif - gambar yang terdiri dari partikel yang terbang terpisah ketika kursor mouse mendekat. Ada keinginan untuk memprogram simulasi perilaku seperti itu pada Unity dan memeriksa kinerja apa yang bisa ditekan keluar dari mesin. Saya berbagi dengan Anda solusi teknis dan pengamatan saya diterima selama proses implementasi.

gambar

Hasilnya adalah sebagai berikut:

Animasi


Untuk mencapainya, Anda perlu memecahkan masalah berikut:

  • menggambarkan fisika partikel;
  • memastikan kecepatan optimal perhitungan posisi pada setiap frame;
  • pilih cara untuk dengan cepat menggambar sejumlah besar partikel di layar.

Fisika


Untuk menggambarkan perilaku partikel yang diinginkan, kita perlu merumuskan hanya tiga prinsip: keinginan untuk mengurangi jarak ke titik awal, keinginan untuk "terbang menjauh" dari kursor mouse dan redaman gerakan. Kita tidak membutuhkan interaksi fisik dan formula yang tepat, kita hanya perlu prinsip-prinsip umum, bergerak sesuai dengan mana partikel akan berperilaku dengan cara yang disengaja. Untuk kesederhanaan, massa dikecualikan dari semua rumus - kami setuju untuk menganggapnya sebagai tunggal.

Mengurangi jarak ke posisi awal


Agar partikel berusaha untuk kembali ke posisi semula, hukum Hooke cocok untuk kita : gaya yang diarahkan ke posisi awal akan sebanding secara linear dengan jarak ke sana. Partikel itu terbang dua kali lebih jauh dari posisi awal - dua kali lebih kuat itu "menarik" ke belakang, semuanya sederhana.

Jarak dari kursor


Agar menarik, partikel-partikel itu entah bagaimana harus berinteraksi dengan kursor, membiarkan diri mereka "mendorong" dari posisi awal. Saya mengambil gravitasi dengan tanda yang berlawanan sebagai dasar untuk interaksi ini : partikel akan ditolak oleh kekuatan yang berbanding terbalik dengan kuadrat jarak antara posisi kursor mouse dan posisi saat ini dari partikel dan diarahkan sepanjang vektor dari kursor ke partikel.F=βˆ’C/r2dimana C- konstanta tertentu yang mengatur interaksi.

Pelemahan


Jika kita membatasi diri pada dua formula sebelumnya, partikel setelah mengatur amplitudo awal akan terombang-ambing selamanya, karena tidak ada tempat untuk kehilangan energi dalam kerangka model seperti itu. Kami akan mensimulasikan pelemahan sebagai kekuatan resistensi viskos media, yang, menurut hukum Stokes , secara linear sebanding dengan kecepatan partikel dan diarahkan dalam arah yang berlawanan dengan gerakan. Kami puas dengan pembatasan yang diberlakukan pada penerapan formula ini, karena dalam hal ini kami tidak tertarik pada keakuratan absolut dari interaksi fisik, tetapi hanya pada prinsipnya.

Hasil


Sebagai hasilnya, kami mendapatkan formula gaya yang bekerja pada partikel pada saat yang tidak tentu:

Fβ†’s=Caβˆ—(xβ†’βˆ’xβ†’0)βˆ’Cr||xβ†’βˆ’xβ†’r||2βˆ—xβ†’βˆ’xβ†’r||xβ†’βˆ’xβ†’r||βˆ’Cdβˆ—vβ†’,


Dimana x,v- Posisi saat ini dan kecepatan partikel, x0- posisi awal xr- posisi kursor, Ca,Cr,Cd- koefisien tarik-menarik, tolakan dan redaman, masing-masing.

Animasi lain


Perilaku pemrograman


Karena dalam model kami, partikel-partikelnya tidak saling tergantung satu sama lain, dan perilakunya ditentukan semata-mata oleh karakteristik dan konstanta umumnya, perhitungan keadaan yang diperbarui ideal untuk paralelisasi. Mempertimbangkan minat lama saya pada Unity DOTS , saya memutuskan untuk menerapkan teknologi ini untuk menyelesaikan masalah yang dirumuskan.

DOTS adalah pengembangan mesin Unity yang relatif baru, sebuah tumpukan teknologi yang berfokus pada penulisan kode multi-thread berkinerja tinggi. Di HabrΓ© ada terjemahan dari artikel pengantar di DOTS. Berkat penggunaan ECS , pola arsitektural Sistem Pekerjaan dan kompiler Burst, DOTS memungkinkan untuk menulis kode multi-utas yang cepat, sekaligus mengurangi kemungkinan bidikan di kaki.

Inti dari penulisan suatu program dalam ECS adalah untuk memisahkan kode menjadi komponen ( Komponen ) yang menggambarkan keadaan, sistem ( Sistem ) yang menggambarkan perilaku dan interaksi komponen, dan Entitas - objek yang berisi sekumpulan komponen.

Komponen


Kami membutuhkan komponen-komponen berikut:

//    ( )
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;
}

Dengan komponen, semuanya cukup sederhana, buka sistem:

//   
[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();
    }
}

Anda dapat mengontrol urutan sistem menggunakan atribut

[UpdateBeforeAttribute]
[UpdateAfterAttribute]

Sistem terbesar yang menggambarkan perilaku akan menjadi sistem pembaruan akselerasi, bertindak sesuai dengan rumus yang diturunkan :

[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();
    }
}

Memberikan


Terlepas dari jumlah partikel yang berpotensi besar yang perlu ditampilkan, untuk semua Anda dapat menggunakan mesh persegi yang sama dari dua poligon, serta bahan yang sama, yang memungkinkan untuk membuat semuanya dalam satu panggilan menarik, jika bukan untuk satu ” tapi ”: semua partikel, pada umumnya, memiliki warna yang berbeda, jika tidak gambar yang dihasilkan akan sangat membosankan.

Unity shader standar "Sprite / Default" dapat menggunakan GPU Instancing untuk mengoptimalkan rendering objek dengan sprite dan warna yang tidak sama, menggabungkannya menjadi satu panggilan draw, tetapi dalam kasus ini, tautan ke tekstur dan warna untuk setiap objek tertentu harus diatur dari skrip, ke mana dari ECS kami tidak memiliki akses.

Solusinya mungkin metode Graphics.DrawMeshInstanced, yang memungkinkan Anda untuk menggambar satu jala beberapa kali dalam satu panggilan undian dengan parameter material berbeda, menggunakan GPU Instancing yang sama. Ini terlihat seperti ini:

[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();
    }
}

Untuk membuat kelompok objek menggunakan metode ini, Anda perlu mengumpulkan array matriks transisi dan properti material yang seharusnya bervariasi. Sistem rendering, seperti yang Anda lihat, dijalankan setelah TrsMatrixCalculationSystem - sistem tambahan yang menghitung matriks transisi untuk setiap partikel, yang terlihat sangat sederhana:

[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();
    }
}

Animasi 46.000 partikel


Performa


Sudah waktunya untuk berbicara tentang mengapa (selain, tentu saja, kepuasan estetika) ini semua terserah. Di Unity, saat ini, ada peluang untuk memilih dari dua Scripting Backend (implementasi CLR ): Mono tua yang baik dan solusi yang lebih modern dari pengembang Unity - IL2CPP .

Bandingkan kinerja build untuk dua implementasi runtime ini:
Jumlah partikel di layarFrame rate rata-rata, MonoFrame rate rata-rata, IL2CPP
50.000128255
100.00066130
200.0003157
Spesifikasi PC:
QHD 2560x1440
Intel Core i5-9600K
16GB RAM
MSI GeForce RTX 2080 SUPER VENTUS XS OC 8.0 GB


160.000 partikel

Terlihat bahwa, dalam eksperimen vakum bulat ini, IL2CPP mengungguli Mono sekitar dua kali.

Menurut profiler, secara harfiah semua waktu yang dihabiskan pada frame dihabiskan untuk sistem rendering, perhitungan sistem lain praktis "gratis":

Unity Editor


Build.

Terlihat bahwa sebagian besar waktu dihabiskan untuk proses mengubah warna dari Color ke Vector4 dan menambahkan ke dalam daftar. Tambahkan (). Kita dapat dengan mudah menyingkirkan yang pertama - kita akan menunda transformasi pada saat pembuatan partikel, karena warna tidak berubah dalam proses:

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

Perubahan ini memungkinkan kami untuk sepenuhnya menghilangkan operasi konversi yang mahal:

Jumlah frame per detik untuk 200.000 partikel setelah meningkat menjadi 61.

Adalah mungkin untuk mengoptimalkan pengisian daftar, misalnya, untuk menyimpan semua array warna, menghasilkan satu kali, tetapi solusi ini tidak tampak elegan bagi saya, oleh karena itu Saya akan dengan senang hati memberikan saran yang membangun dalam komentar.

Kesimpulan


ECS secara umum dan Unity DOTS khususnya adalah alat yang hebat untuk skenario dan kelas tugas tertentu. Mereka memenuhi peran mereka dalam pemrosesan efisien sejumlah besar data dan memungkinkan Anda untuk membuat simulasi, yang jika tidak ada mereka akan membutuhkan lebih banyak usaha. Namun, Anda tidak boleh menganggap DOTS sebagai "peluru perak" dan melempar batu ke pengembang yang mematuhi konsep tradisional Kesatuan dalam proyek baru - DOTS bukan untuk semua orang dan bukan untuk setiap tugas.

PS


Semua orang bisa berkenalan dengan proyek di repositori github saya , tetapi cara menginstal build sebagai wallpaper desktop di Wallpaper Engine : tautan .

All Articles