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.

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 spriteShader "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 ditampilkanLatar belakang sebenarnya transparan, sengaja digelapkan.

Benda kerja yang dihasilkan.

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 wPembagian 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.
Dan lihat hasil
GrabPass .

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)
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 langkahFungsi 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; } } }
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() { ...
Sekarang, untuk mengubah properti, kami akan memperbarui
MaterialPropertyBlock objek kami tanpa membuat salinan materi.
Tentang SpriteRendererMari 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.