Buat efek distribusi warna di Unity


Efek ini terinspirasi oleh episode Powerpuff Girls . Saya ingin membuat efek penyebaran warna di dunia hitam dan putih, tetapi untuk menerapkannya dalam koordinat ruang dunia , untuk melihat bagaimana warna melukis objek , dan tidak hanya menyebar datar di layar, seperti dalam kartun.

Saya menciptakan efek dalam Pipa Render Ringan yang baru dari mesin Unity, contoh bawaan dari pipa Pipa Rendering Scriptable. Semua konsep berlaku untuk saluran pipa lain, tetapi beberapa fungsi atau matriks bawaan mungkin memiliki nama yang berbeda. Saya juga menggunakan tumpukan pemrosesan pasca baru, tetapi dalam tutorial saya akan menghilangkan deskripsi rinci pengaturannya, karena dijelaskan dengan sangat baik dalam manual lain, misalnya, dalam video ini .



Efek post-processing dalam skala abu-abu


Hanya untuk referensi, seperti inilah pemandangan tanpa efek pasca pemrosesan.


Untuk efek ini, saya menggunakan paket Pasca Pemrosesan Unity 2018 yang baru, yang dapat diunduh dari manajer paket. Jika Anda tidak tahu cara menggunakannya, maka saya merekomendasikan tutorial ini .

Saya menulis efek saya sendiri dengan memperluas kelas PostProcessingEffectSettings dan PostProcessEffectRenderer yang ditulis dalam C #, kode sumbernya dapat dilihat di sini . Sebenarnya, saya tidak melakukan sesuatu yang sangat menarik dengan efek ini pada sisi CPU (dalam kode C #) kecuali bahwa saya menambahkan sekelompok properti umum ke Inspektur, jadi saya tidak akan menjelaskan bagaimana melakukan ini dalam tutorial. Saya harap kode saya berbicara sendiri.

Mari kita beralih ke kode shader dan mulai dengan efek grayscale. Dalam tutorial, kami tidak akan memodifikasi file shaderlab, struktur input, dan vertex shader, sehingga Anda dapat melihat kode sumbernya di sini . Sebagai gantinya, kami akan mengurus shader fragmen.

Untuk mengonversi warna menjadi skala abu-abu, kami mengurangi nilai setiap piksel menjadi nilai luminance yang menjelaskan kecerahannya . Ini dapat dilakukan dengan mengambil produk skalar dari nilai warna tekstur kamera dan vektor tertimbang , yang menggambarkan kontribusi setiap saluran warna terhadap kecerahan warna secara keseluruhan.

Mengapa kami menggunakan produk skalar? Jangan lupa bahwa produk skalar dihitung sebagai berikut:

dot(a, b) = a x * b x + a y * b y + a z * b z

Dalam hal ini, kami mengalikan setiap saluran dari nilai warna berdasarkan berat . Lalu kami menambahkan produk-produk ini untuk mengurangi mereka ke nilai skalar tunggal. Ketika warna RGB memiliki nilai yang sama di saluran R, G, dan B, warnanya berubah menjadi abu-abu.

Seperti apa bentuk kode shader:

 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 

Jika shader dasar dikonfigurasikan dengan benar, maka efek post-processing akan mewarnai seluruh layar dalam skala abu-abu.




Rendering efek warna di ruang dunia


Karena ini adalah efek pasca pemrosesan, kami tidak memiliki informasi tentang geometri adegan di vertex shader. Pada tahap pasca pemrosesan, satu-satunya informasi yang kami miliki adalah gambar yang diberikan oleh kamera dan ruang koordinat terpotong untuk pengambilan sampelnya. Namun, kami ingin efek pewarnaan menyebar ke seluruh objek, seolah-olah itu terjadi di dunia, dan tidak hanya di layar datar.

Untuk menggambar efek ini dalam geometri pemandangan, kita membutuhkan koordinat ruang dunia dari setiap piksel. Untuk berpindah dari koordinat ruang koordinat terpotong ke koordinat ruang dunia , kita perlu melakukan transformasi ruang koordinat .

Biasanya, untuk berpindah dari satu ruang koordinat ke yang lain, diperlukan sebuah matriks yang mendefinisikan transformasi dari ruang koordinat A ke ruang B. Untuk beralih dari A ke B, kita mengalikan vektor dalam ruang koordinat A dengan matriks transformasi ini. Dalam kasus kami, kami akan melakukan transisi berikut: ruang koordinat terpotong (ruang klip) -> ruang tampilan (ruang tampilan) -> ruang dunia (ruang dunia) . Artinya, kita membutuhkan matriks clip-to-view-space dan matriks view-to-world-space yang disediakan Unity.

Namun, Koordinat kesatuan dari ruang koordinat terpotong tidak memiliki nilai z yang menentukan kedalaman piksel, atau jarak ke kamera. Kita membutuhkan nilai ini untuk bergerak dari ruang koordinat terpotong ke ruang spesies. Mari kita mulai dengan ini!

Mendapatkan nilai buffer kedalaman


Jika pipa rendering diaktifkan, maka ia menggambar tekstur di viewport yang menyimpan nilai z dalam struktur yang disebut buffer kedalaman . Kami dapat mencicipi buffer ini untuk mendapatkan nilai z yang hilang dari ruang koordinat kami dari koordinat terpotong!

Pertama, pastikan bahwa buffer kedalaman benar-benar diberikan dengan mengklik bagian "Tambahkan Data Tambahan" dari kamera di Inspektur dan memeriksa bahwa kotak "Membutuhkan Tekstur Kedalaman" dicentang. Pastikan juga bahwa opsi Allow MSAA diaktifkan untuk kamera. Saya tidak tahu mengapa efek ini perlu diperiksa, tetapi itu benar. Jika buffer kedalaman digambar, maka di frame debugger Anda akan melihat tahap "Depth Prepass" .

Buat sampler _CameraDepthTexture dalam file hlsl

 TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 

Sekarang mari kita menulis fungsi GetWorldFromViewPosition dan untuk saat ini kita akan menggunakannya untuk memeriksa buffer kedalaman . (Nanti kita akan mengembangkannya untuk mendapatkan posisi di dunia.)

 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 

Di shader fragmen, gambarkan nilai sampel tekstur kedalaman.

 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 

Ini adalah hasil saya terlihat ketika hanya ada satu dataran berbukit di tempat kejadian (saya mematikan semua pohon untuk lebih menyederhanakan pengujian nilai-nilai ruang dunia). Hasil Anda harus terlihat serupa. Nilai hitam dan putih menggambarkan jarak dari geometri ke kamera.


Berikut adalah beberapa langkah yang dapat Anda ambil jika Anda mengalami masalah:

  • Pastikan kamera telah mengaktifkan rendering dalam.
  • Pastikan kamera telah mengaktifkan MSAA.
  • Coba ganti bidang dekat dan jauh dari kamera.
  • Pastikan bahwa objek yang Anda harapkan untuk melihat di buffer kedalaman menggunakan shader dengan pass kedalaman. Ini memastikan bahwa objek menarik ke buffer kedalaman. Semua shader standar di LWRP melakukan ini.

Mendapatkan nilai dalam ruang dunia


Sekarang kita memiliki semua informasi yang diperlukan untuk ruang koordinat terpotong , mari kita beralih ke ruang spesies , dan kemudian ke ruang dunia .

Perhatikan bahwa matriks transformasi yang diperlukan untuk operasi ini sudah ada di perpustakaan SRP. Namun, mereka terkandung dalam pustaka C # engine Unity, jadi saya memasukkannya ke shader di fungsi Render dari skrip ColorSpreadRenderer :

 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 

Sekarang mari kita memperluas fungsi GetWorldFromViewPosition kami.

Pertama, kita perlu mendapatkan posisi di viewport dengan mengalikan posisi dalam ruang koordinat terpotong oleh InverseProjectionMatrix . Kita juga perlu melakukan beberapa sihir voodoo dengan posisi layar, yang terkait dengan bagaimana Unity menyimpan posisinya di ruang koordinat terpotong.

Akhirnya, kita dapat melipatgandakan posisi di viewport dengan ViewToWorldMatrix untuk mendapatkan posisi di ruang dunia .

 float3 GetWorldFromViewPosition (VertexOutput i) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

Mari kita lakukan pemeriksaan untuk memastikan bahwa posisi di ruang global sudah benar. Untuk melakukan ini, saya menulis shader yang hanya mengembalikan posisi objek di ruang dunia ; ini adalah perhitungan yang cukup sederhana berdasarkan shader biasa, kebenarannya dapat dipercaya. Matikan efek pasca-pemrosesan dan ambil tangkapan layar dari shader uji ini untuk ruang dunia . Setelah saya menerapkan shader ke permukaan bumi dalam adegan terlihat seperti ini:


(Perhatikan bahwa nilai-nilai di ruang dunia jauh lebih besar dari 1,0, jadi jangan khawatir bahwa warna-warna ini masuk akal; sebagai gantinya, pastikan saja hasilnya sama untuk jawaban "benar" dan "dihitung"). Selanjutnya, mari kita kembali ke tes objek adalah bahan biasa (dan bukan bahan uji ruang dunia), dan kemudian nyalakan efek pasca-pemrosesan lagi. Hasil saya terlihat seperti ini:


Ini benar-benar mirip dengan uji shader yang saya tulis, yaitu, perhitungan ruang dunia kemungkinan besar benar!

Menggambar lingkaran di ruang dunia


Sekarang kita memiliki posisi di ruang dunia , kita dapat menggambar lingkaran warna di TKP! Kita perlu mengatur radius di mana efeknya akan menggambar warna. Di luar, efeknya akan membuat gambar dalam skala abu-abu. Untuk mengaturnya, Anda perlu menyesuaikan nilai untuk radius efek ( _MaxSize ) dan pusat lingkaran (_Center). Saya mengatur nilai-nilai ini di kelas C # ColorSpread sehingga mereka terlihat di inspektur. Mari perluas shader fragmen kami dengan memaksanya untuk memeriksa apakah piksel saat ini ada di dalam radius lingkaran :

 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

Akhirnya, kita bisa menggambar warna berdasarkan apakah itu berada dalam radius di ruang dunia . Seperti inilah efek dasarnya!




Menambahkan Efek Khusus


Saya akan melihat beberapa teknik lagi yang digunakan untuk membuat warna menyebar di tanah. Ada banyak lagi untuk efek penuh, tetapi tutorial sudah menjadi terlalu besar, jadi kita akan membatasi diri kita pada yang paling penting.

Animasi Lingkaran Pembesaran


Kami ingin efeknya menyebar ke seluruh dunia, yaitu, seolah tumbuh. Untuk melakukan ini, Anda perlu mengubah jari - jari tergantung pada waktu.

_StartTime menunjukkan waktu di mana lingkaran harus mulai tumbuh. Dalam proyek saya, saya menggunakan skrip tambahan yang memungkinkan Anda mengklik di mana saja di layar untuk memulai pertumbuhan lingkaran; dalam hal ini, waktu mulai sama dengan waktu mouse diklik.

_GrowthSpeed ​​mengatur kecepatan meningkatkan lingkaran.

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

Kita juga perlu memperbarui pemeriksaan jarak untuk membandingkan jarak saat ini dengan meningkatnya radius efek , dan bukan dengan _MaxSize.

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

Seperti apa hasilnya nanti:


Menambah radius noise


Saya ingin efeknya lebih seperti buram cat, bukan hanya lingkaran yang tumbuh. Untuk melakukan ini, mari kita tambahkan noise ke radius efek sehingga distribusinya tidak merata.

Pertama, kita perlu sampel tekstur di ruang dunia . Koordinat UV dari i.screenPos terletak di ruang layar , dan jika kami sampel berdasarkan pada mereka, bentuk efeknya akan bergerak dengan kamera; jadi mari kita gunakan koordinat di ruang dunia . Saya menambahkan parameter _NoiseTexScale untuk mengontrol skala sampel tekstur noise , karena koordinat di ruang dunia cukup besar.

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

Sekarang mari kita sampel tekstur noise dan tambahkan nilai ini ke radius efek. Saya menggunakan skala _NoiseSize untuk kontrol lebih besar atas ukuran noise.

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

Beginilah hasilnya setelah beberapa penyesuaian:




Kesimpulannya


Anda dapat mengikuti pembaruan tutorial di Twitter saya, dan di Twitch saya menghabiskan stream coding! (Juga, saya mengalirkan game dari waktu ke waktu, jadi jangan heran jika Anda melihat saya duduk di piyama dan bermain Kingdom Hearts 3.)

Ucapan Terima Kasih:

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


All Articles