Menulis shader di Unity. GrabPass, PerRendererData

Hai Saya ingin berbagi pengalaman saya menulis shader di Unity. Mari kita mulai dengan shader Displacement / Refraction dalam 2D, pertimbangkan fungsionalitas yang digunakan untuk menulisnya (GrabPass, PerRendererData), dan perhatikan juga masalah yang akan muncul.

Informasi ini berguna bagi mereka yang memiliki gagasan umum tentang shader dan mencoba membuatnya, tetapi tidak terbiasa dengan kemampuan yang disediakan Unity, dan tidak tahu sisi mana yang harus didekati. Coba lihat, mungkin pengalaman saya akan membantu Anda mengetahuinya.



Inilah hasil yang ingin kita capai.

gambar

Persiapan


Pertama, buat shader yang hanya akan menggambar sprite yang ditentukan. Dia akan menjadi dasar kita untuk manipulasi lebih lanjut. Sesuatu akan ditambahkan ke dalamnya, sesuatu yang sebaliknya akan dihapus. Ini akan berbeda dari standar "Sprite-Default" dengan tidak adanya beberapa tag dan tindakan yang tidak akan mempengaruhi hasil.

Kode shader untuk rendering sprite
Shader "Displacement/Displacement_Wave" { Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Cull Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; fixed4 _Color; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return texColor; } ENDCG } } } 

Sprite untuk ditampilkan
Latar belakang sebenarnya transparan, sengaja digelapkan.

gambar

Benda kerja yang dihasilkan.

gambar

Grabpass


Sekarang tugas kita adalah membuat perubahan pada gambar saat ini di layar, dan untuk ini kita perlu mendapatkan gambar. Dan bagian GrabPass akan membantu kita dengan ini. Bagian ini akan menangkap gambar layar dalam tekstur _GrabTexture . Tekstur hanya akan berisi apa yang digambar sebelum objek kita menggunakan shader ini akan ditampilkan.

Selain tekstur itu sendiri, kita perlu koordinat pemindaian untuk mendapatkan warna piksel darinya. Untuk melakukan ini, tambahkan koordinat tekstur tambahan ke data shader fragmen. Koordinat ini tidak dinormalisasi (nilai tidak berada dalam kisaran 0 hingga 1) dan menggambarkan posisi titik dalam ruang kamera (proyeksi).

 struct v2f { float4 vertex : SV_POSITION; float2 uv : float4 color : COLOR; float4 grabPos : TEXCOORD1; }; 

Dan di vertex shader mengisinya.

 o.grabPos = ComputeGrabScreenPos (o.vertex); 

Untuk mendapatkan warna dari _GrabTexture , kita dapat menggunakan metode berikut jika kita menggunakan koordinat yang tidak dinormalisasi

 tex2Dproj(_GrabTexture, i.grabPos) 

Tetapi kami akan menggunakan metode yang berbeda dan menormalkan koordinat kami sendiri, menggunakan pembagian perspektif, mis. membagi semua yang lain menjadi komponen-w.

 tex2D(_GrabTexture, i.grabPos.xy/i.grabPos.w) 

komponen w
Pembagian menjadi komponen-w hanya diperlukan ketika menggunakan perspektif, dalam proyeksi ortografi, itu akan selalu menjadi 1. Bahkan, w menyimpan nilai jarak, arahkan ke kamera. Tapi itu bukan kedalaman - z , nilainya harus berkisar antara 0 hingga 1. Bekerja dengan kedalaman layak untuk topik yang terpisah, jadi kami akan kembali ke shader kami.

Pembagian perspektif juga dapat dilakukan dalam vertex shader, dan data yang sudah disiapkan dapat ditransfer ke fragmen shader.

 v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); o.grabPos = ComputeScreenPos (o.vertex); o.grabPos /= o.grabPos.w; return o; } 

Tambahkan fragmen shader, masing-masing.

 fixed4 frag (v2f i) : SV_Target { fixed4 = grabColor = tex2d(_GrabTexture, i.grabPos.xy); fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return grabColor; } 

Matikan mode pencampuran yang ditentukan, karena sekarang kami menerapkan blending mode kami di dalam fragmen shader.

 //Blend SrcAlpha OneMinusSrcAlpha Blend Off 

Dan lihat hasil GrabPass .

gambar

Sepertinya tidak ada yang terjadi, tetapi tidak. Untuk kejelasan, kami memperkenalkan sedikit perubahan, untuk ini kami akan menambahkan nilai variabel ke koordinat tekstur. Agar kita dapat memodifikasi variabel, tambahkan properti _DisplacementPower baru.

 Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) _DisplacementPower ("Displacement Power" , Float) = 0 } SubShader { Pass { ... float _DisplacementPower; ... } } 

Dan lagi, buat perubahan pada shader fragmen.

 fixed4 grabColor = tex2d(_GrabTexture, i.grabPos.xy + _DisplaccementPower); 

Op hop dan hasil! Gambar dengan bergeser.



Setelah perubahan berhasil, Anda dapat melanjutkan ke distorsi yang lebih kompleks. Kami menggunakan tekstur yang disiapkan sebelumnya yang akan menyimpan gaya perpindahan pada titik yang ditentukan. Warna merah untuk nilai offset pada sumbu x, dan hijau pada sumbu y.

Tekstur digunakan untuk distorsi



Mari kita mulai. Tambahkan properti baru untuk menyimpan tekstur.

 _DisplacementTex ("Displacement Texture", 2D) = "white" {} 

Dan sebuah variabel.

 sampler2D _DisplacementTex; 

Dalam fragmen shader kita mendapatkan nilai offset dari tekstur dan menambahkannya ke koordinat tekstur.

 fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); 

Sekarang, mengubah nilai-nilai parameter _DisplacementPower , kita tidak hanya menggeser gambar asli, tetapi mendistorsi itu.



Hamparan


Sekarang di layar hanya ada distorsi ruang, dan sprite, yang kami tunjukkan di awal, tidak ada. Kembalikan ke tempatnya. Untuk melakukan ini, kita akan menggunakan campuran warna yang sulit. Ambil sesuatu yang lain, seperti blending mode overlay. Formulanya adalah sebagai berikut:



di mana S adalah gambar asli, C adalah korektif, yaitu, sprite kami, R adalah hasilnya.

Transfer formula ini ke shader kami.

 fixed4 color = grabColor < 0.5 ? 2*grabColor*texColor : 1-2*(1-texColor)*(1-grabColor); 

Penggunaan operator bersyarat dalam shader adalah topik yang agak membingungkan. Banyak hal tergantung pada platform dan grafik API yang digunakan. Dalam beberapa kasus, pernyataan bersyarat tidak akan mempengaruhi kinerja. Tapi selalu layak untuk mundur. Operator bersyarat dapat diganti menggunakan matematika dan metode yang tersedia. Kami menggunakan konstruksi berikut

 c = step ( y, x); r = c * a + (1 - c) * b; 

Fungsi langkah
Fungsi langkah akan mengembalikan 1 jika x lebih besar dari atau sama dengan y . Dan 0 jika x kurang dari y .

Misalnya, jika x = 1, dan y = 0,5, maka hasil c akan menjadi 1. Dan ekspresi berikut akan terlihat seperti
r = 1 * a + 0 * b
Karena mengalikan dengan 0 memberi 0, maka hasilnya akan menjadi nilai a .
Kalau tidak, jika c adalah 0,
r = 0 * a + 1 * b
Dan hasil akhirnya adalah b .

Tulis ulang warna untuk mode overlay .

 fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); 

Pastikan untuk mempertimbangkan transparansi sprite. Untuk melakukan ini, kita akan menggunakan interpolasi linier antara dua warna.

 color = lerp(grabColor, color ,texColor.a); 

Kode shader fragmen penuh.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

Dan hasil dari pekerjaan kami.



Fitur GrabPass


Disebutkan di atas bahwa pass GrabPass {} menangkap konten layar menjadi tekstur _GrabTexture . Pada saat yang sama, setiap kali bagian ini dipanggil, isi tekstur akan diperbarui.
Pembaruan konstan dapat dihindari dengan menentukan nama tekstur di mana konten layar akan ditangkap.
 GrabPass{"_DisplacementGrabTexture"} 

Sekarang konten dari tekstur akan diperbarui hanya pada panggilan pertama dari pass GrabPass per frame. Ini menghemat sumber daya jika ada banyak objek menggunakan GrabPass {} . Tetapi jika dua objek tumpang tindih, maka artefak akan terlihat, karena kedua objek akan menggunakan gambar yang sama.

Menggunakan GrabPass {"_ DisplacementGrabTexture"}.



Menggunakan GrabPass {}.



Animasi


Sekarang saatnya untuk menghidupkan efek kita. Kami ingin mengurangi kekuatan distorsi saat gelombang ledakan tumbuh, mensimulasikan kepunahannya. Untuk melakukan ini, kita perlu mengubah properti material.

Script untuk animasi
 public class Wave : MonoBehaviour { private float _elapsedTime; private SpriteRenderer _renderer; public float Duration; [Space] public AnimationCurve ScaleProgress; public Vector3 ScalePower; [Space] public AnimationCurve PropertyProgress; public float PropertyPower; [Space] public AnimationCurve AlphaProgress; private void Start() { _renderer = GetComponent<SpriteRenderer>(); } private void OnEnable() { _elapsedTime = 0f; } void Update() { if (_elapsedTime < Duration) { var progress = _elapsedTime / Duration; var scale = ScaleProgress.Evaluate(progress) * ScalePower; var property = PropertyProgress.Evaluate(progress) * PropertyPower; var alpha = AlphaProgress.Evaluate(progress); transform.localScale = scale; _renderer.material.SetFloat("_DisplacementPower", property); var color = _renderer.color; color.a = alpha; _renderer.color = color; _elapsedTime += Time.deltaTime; } else { _elapsedTime = 0; } } } 

Dan pengaturannya


Hasil dari animasi.



Perrendererdata


Perhatikan garis di bawah ini.

 _renderer.material.SetFloat("_DisplacementPower", property); 

Di sini kita tidak hanya mengubah salah satu sifat material, tetapi membuat salinan materi sumber (hanya pada panggilan pertama dari metode ini) dan sudah bekerja dengannya. Ini adalah opsi yang cukup berhasil, tetapi jika ada lebih dari satu objek di panggung, misalnya, seribu, maka membuat banyak salinan tidak akan menghasilkan sesuatu yang baik. Ada opsi yang lebih baik - ini adalah menggunakan atribut [PerRendererData] dalam shader, dan objek MaterialPropertyBlock dalam skrip.

Untuk melakukan ini, tambahkan atribut ke properti _DisplacementPower di shader.

 [PerRendererData] _DisplacementPower ("Displacement Power" , Range(-.1,.1)) = 0 

Setelah itu, properti tidak akan lagi ditampilkan di inspektur, karena Sekarang adalah individual untuk setiap objek, yang akan mengatur nilai.



Kami kembali ke skrip dan mengubahnya.

 private MaterialPropertyBlock _propertyBlock; private void Start() { _renderer = GetComponent<SpriteRenderer>(); _propertyBlock = new MaterialPropertyBlock(); } void Update() { ... //_renderer.material.SetFloat("_DisplacementPower", property); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat("_DisplacementPower", property); _renderer.SetPropertyBlock(_propertyBlock); ... } 

Sekarang, untuk mengubah properti, kami akan memperbarui MaterialPropertyBlock objek kami tanpa membuat salinan materi.

Tentang SpriteRenderer
Mari kita lihat baris ini di shader.

 [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} 

SpriteRenderer bekerja serupa dengan sprite. Ini menetapkan properti _MainTex ke nilainya menggunakan MaterialPropertyBlock . Oleh karena itu, di inspektur, properti _MainTex tidak ditampilkan untuk materi, dan dalam komponen SpriteRenderer kami menentukan tekstur yang kami butuhkan. Pada saat yang sama, ada banyak sprite yang berbeda di atas panggung, tetapi hanya satu materi yang akan digunakan untuk rendering mereka (jika Anda tidak mengubahnya sendiri).

Fitur PerRendererData


Anda bisa mendapatkan MaterialPropertyBlock dari hampir semua komponen yang terkait dengan render. Misalnya, SpriteRenderer , ParticleRenderer , MeshRenderer , dan komponen Renderer lainnya. Tapi selalu ada pengecualian, ini adalah CanvasRenderer . Tidak mungkin untuk mendapatkan dan mengubah properti menggunakan metode ini. Oleh karena itu, jika Anda menulis game 2D menggunakan komponen UI, Anda akan menghadapi masalah ini saat menulis shader.

Rotasi


Efek tidak menyenangkan terjadi ketika gambar diputar. Pada contoh gelombang bulat, ini terutama terlihat.

Gelombang yang tepat saat berputar (90 derajat) memberikan distorsi lain.



Merah menunjukkan vektor yang diperoleh dari titik yang sama dalam tekstur, tetapi dengan rotasi tekstur yang berbeda. Nilai offset tetap sama dan tidak memperhitungkan rotasi.

Untuk mengatasi masalah ini, kita akan menggunakan matriks transformasi unity_ObjectToWorld . Ini akan membantu menghitung ulang vektor kami dari koordinat lokal ke koordinat dunia.

 float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld, offset); 

Tetapi matriks juga berisi data tentang skala objek, jadi ketika menunjukkan kekuatan distorsi, kita harus memperhitungkan skala objek itu sendiri.

 _propertyBlock.SetFloat("_DisplacementPower", property/transform.localScale.x); 

Gelombang kanan juga diputar 90 derajat, tetapi distorsi sekarang dihitung dengan benar.



Klip


Tekstur kami memiliki piksel transparan yang cukup (terutama jika kami menggunakan jenis Mesh persegi ). Shader memprosesnya, yang dalam hal ini tidak masuk akal. Karena itu, kami akan mencoba mengurangi jumlah perhitungan yang tidak perlu. Kami dapat mengganggu pemrosesan piksel transparan menggunakan metode klip (x) . Jika parameter yang diteruskan ke kurang dari nol, maka shader akan berakhir. Tetapi karena nilai alfa tidak boleh kurang dari 0, kami akan mengurangi nilai kecil darinya. Itu juga dapat dimasukkan ke dalam properti ( Potongan ) dan digunakan untuk memotong bagian transparan dari gambar. Dalam hal ini, kita tidak perlu parameter terpisah, jadi kita hanya akan menggunakan angka 0,01 .

Kode shader fragmen penuh.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy * 2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld,offset); fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; clip(texColor.a - 0.01); fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * 2 * grabColor * texColor + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

PS: Kode sumber untuk shader dan skrip adalah tautan ke git . Proyek ini juga memiliki generator tekstur kecil untuk distorsi. Kristal dengan alas diambil dari aset - 2D Game Kit.

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


All Articles