Membuat shader rumput di mesin Unity


Tutorial ini akan menunjukkan kepada Anda bagaimana menulis shader geometris untuk menghasilkan bilah rumput dari bagian atas jala yang masuk dan menggunakan tessellation untuk mengontrol kepadatan rumput.

Artikel ini menjelaskan proses selangkah demi selangkah dari penulisan shader rumput di Unity. Shader menerima mesh yang masuk, dan dari setiap simpul mesh menghasilkan pisau rumput menggunakan shader geometris . Demi minat dan realisme, bilah rumput akan memiliki ukuran dan rotasi acak , dan mereka juga akan terpengaruh oleh angin . Untuk mengontrol kepadatan rumput, kami menggunakan tessellation untuk memisahkan jala yang masuk. Rumput akan dapat dilemparkan dan menerima bayangan.

Proyek yang sudah selesai diposting di akhir artikel. File shader yang dihasilkan berisi sejumlah besar komentar yang memudahkan pemahaman.

Persyaratan


Untuk menyelesaikan tutorial ini, Anda akan membutuhkan pengetahuan praktis tentang mesin Unity dan pemahaman awal tentang sintaks dan fungsionalitas shader.

Unduh konsep proyek (.zip) .

Mulai bekerja


Unduh konsep proyek dan buka di editor Unity. Buka adegan Main , dan kemudian buka shader Grass di editor kode Anda.

File ini berisi shader yang menghasilkan warna putih, serta beberapa fungsi yang akan kita gunakan dalam tutorial ini. Anda akan melihat bahwa fungsi-fungsi ini bersama dengan vertex shader termasuk dalam blok CGINCLUDE terletak di luar SubShader . Kode yang ditempatkan di blok ini akan secara otomatis dimasukkan dalam semua lintasan di shader; ini akan berguna nanti karena shader kami akan memiliki beberapa lintasan.

Kami akan mulai dengan menulis shader geometris yang menghasilkan segitiga dari setiap titik pada permukaan mesh kami.

1. Geometris Shaders


Pembagi geometris adalah bagian opsional dari pipa render. Mereka dieksekusi setelah shader vertex (atau shader tessellation jika tessellation digunakan) dan sebelum simpul diproses untuk shader fragmen.


Direct3D Graphics Pipeline 11. Perhatikan bahwa dalam diagram ini fragmen shader disebut pixel shader .

Pembagi geometris menerima satu primitif pada input dan dapat menghasilkan nol, satu atau banyak primitif. Kita akan mulai dengan menulis shader geometris yang menerima titik (atau titik ) pada input, dan yang memberi makan satu segitiga yang mewakili bilah rumput.

 // Add inside the CGINCLUDE block. struct geometryOutput { float4 pos : SV_POSITION; }; [maxvertexcount(3)] void geo(triangle float4 IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream) { } … // Add inside the SubShader Pass, just below the #pragma fragment frag line. #pragma geometry geo 

Kode di atas menyatakan shader geometris yang disebut geo dengan dua parameter. Yang pertama, triangle float4 IN[3] , melaporkan bahwa ia akan mengambil satu segitiga (terdiri dari tiga titik) sebagai input. Yang kedua, seperti TriangleStream , mengatur shader untuk menampilkan aliran segitiga sehingga setiap titik menggunakan struktur geometryOutput Output untuk mengirimkan datanya.

Kami mengatakan di atas bahwa shader akan menerima satu simpul dan menghasilkan bilah rumput. Lalu mengapa kita mendapatkan segitiga?
Mengambil sebagai input lebih murah. Ini bisa dilakukan sebagai berikut.

 void geo(point vertexOutput IN[1], inout TriangleStream<geometryOutput> triStream) 

Namun, karena mesh masuk kami (dalam hal ini GrassPlane10x10 , terletak di folder Mesh ) memiliki topologi segitiga , ini akan menyebabkan ketidakcocokan antara topologi mesh yang masuk dan input primitif yang diperlukan. Meskipun ini diizinkan dalam DirectX HLSL, itu tidak diizinkan di OpenGL , jadi kesalahan akan ditampilkan.

Selain itu, kami menambahkan parameter terakhir dalam tanda kurung di atas deklarasi fungsi: [maxvertexcount(3)] . Ia memberi tahu GPU bahwa kami akan menampilkan (tetapi tidak diharuskan melakukan) tidak lebih dari 3 simpul. Kami juga membuat SubShader menggunakan shader geometris dengan mendeklarasikannya di dalam Pass .

Shader geometris kami belum melakukan apa pun; untuk menggambar segitiga, tambahkan kode berikut di dalam geometri shader.

 geometryOutput o; o.pos = float4(0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(-0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(0, 1, 0, 1); triStream.Append(o); 


Ini memberikan hasil yang sangat aneh. Saat Anda memindahkan kamera, menjadi jelas bahwa segitiga ditampilkan di ruang layar . Ini logis: karena shader geometris dieksekusi segera sebelum memproses simpul, maka shader vertex menghilangkan tanggung jawab untuk simpul yang akan ditampilkan dalam ruang pemotongan . Kami akan mengubah kode kami untuk mencerminkan hal ini.

 // Update the return call in the vertex shader. //return UnityObjectToClipPos(vertex); return vertex; … // Update each assignment of o.pos in the geometry shader. o.pos = UnityObjectToClipPos(float4(0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(-0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(0, 1, 0, 1)); 


Sekarang segitiga kita ditampilkan dengan benar di dunia. Namun, tampaknya hanya satu yang dibuat. Faktanya, satu segitiga digambar untuk setiap verteks dari mesh kita, tetapi posisi yang ditetapkan untuk verteks dari segitiga adalah konstan - mereka tidak berubah untuk setiap verteks yang masuk. Oleh karena itu, semua segitiga terletak satu di atas yang lain.

Kami akan memperbaiki ini dengan membuat offset posisi titik keluar relatif terhadap titik masuk.

 // Add to the top of the geometry shader. float3 pos = IN[0]; … // Update each assignment of o.pos. o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); 


Mengapa beberapa simpul tidak membuat segitiga?

Meskipun kami telah menentukan bahwa primitif yang masuk akan berupa segitiga , sebilah rumput ditransmisikan hanya dari salah satu titik segitiga, membuang dua lainnya. Tentu saja, kita dapat mentransfer sebilah rumput dari ketiga titik yang masuk, tetapi ini akan mengarah pada fakta bahwa segitiga tetangga secara berlebihan menciptakan bilah rumput di atas satu sama lain.

Atau Anda dapat mengatasi masalah ini dengan mengambil jerat yang memiliki jenis titik Topologi sebagai jerat yang masuk dari shader geometris.

Segitiga sekarang digambar dengan benar, dan pangkalannya terletak di puncak yang memancarkannya. Sebelum melanjutkan, buat objek GrassPlane tidak aktif dalam adegan, dan buat objek GrassBall aktif . Kami ingin rumput menghasilkan dengan benar pada berbagai jenis permukaan, jadi penting untuk mengujinya pada jerat berbagai bentuk.


Sejauh ini, semua segitiga dipancarkan dalam satu arah, dan tidak keluar dari permukaan bola. Untuk mengatasi masalah ini, kita akan membuat bilah rumput di ruang bersinggungan .

2. Ruang singgung


Idealnya, kami ingin membuat bilah rumput dengan menetapkan lebar, tinggi, kelengkungan dan rotasi yang berbeda, tanpa memperhitungkan sudut permukaan dari mana bilah rumput dipancarkan. Sederhananya, kita mendefinisikan sebilah rumput di ruang lokal ke titik memancarkannya , dan kemudian mengubahnya sehingga lokal ke jala . Ruang ini disebut ruang singgung .


Dalam ruang singgung, sumbu X , Y, dan Z didefinisikan relatif terhadap normal dan posisi permukaan (dalam kasus kami, simpul).

Seperti ruang lainnya, kita dapat mendefinisikan ruang singgung titik dengan tiga vektor: kanan , maju dan atas . Dengan menggunakan vektor-vektor ini, kita dapat membuat matriks untuk memutar bilah rumput dari garis singgung ke ruang lokal.

Anda dapat mengakses vektor ke kanan dan atas dengan menambahkan data titik masukan baru.

 // Add to the CGINCLUDE block. struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; … // Modify the vertex shader. vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; } … // Modify the input for the geometry shader. Note that the SV_POSITION semantic is removed. void geo(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream) … // Modify the existing line declaring pos. float3 pos = IN[0].vertex; 

Vektor ketiga dapat dihitung dengan mengambil produk vektor antara dua lainnya. Produk vektor mengembalikan vektor tegak lurus ke dua vektor yang masuk.

 // Place in the geometry shader, below the line declaring float3 pos. float3 vNormal = IN[0].normal; float4 vTangent = IN[0].tangent; float3 vBinormal = cross(vNormal, vTangent) * vTangent.w; 

Mengapa hasil vektor dikalikan dengan koordinat garis singgung w?
Saat mengekspor mesh dari editor 3D, biasanya binormals (juga disebut garis singgung ke dua titik ) sudah disimpan dalam data mesh. Alih-alih mengimpor binormals ini, Unity hanya mengambil arah setiap binormal dan menugaskan mereka ke koordinat w tangen. Ini memungkinkan Anda untuk menghemat memori, sementara pada saat yang sama memberikan kemampuan untuk membuat ulang binormal yang benar. Diskusi terperinci tentang topik ini dapat ditemukan di sini .

Memiliki ketiga vektor, kita dapat membuat matriks untuk transformasi antara ruang singgung dan ruang lokal. Kami akan melipatgandakan setiap simpul bilah rumput dengan matriks ini sebelum meneruskannya ke UnityObjectToClipPos , yang mengharapkan simpul di ruang lokal.

 // Add below the lines declaring the three vectors. float3x3 tangentToLocal = float3x3( vTangent.x, vBinormal.x, vNormal.x, vTangent.y, vBinormal.y, vNormal.y, vTangent.z, vBinormal.z, vNormal.z ); 

Sebelum menggunakan matriks, kami mentransfer kode output titik ke fungsi agar tidak menulis baris kode yang sama berulang kali. Ini disebut prinsip KERING , atau jangan ulangi diri Anda sendiri .

 // Add to the CGINCLUDE block. geometryOutput VertexOutput(float3 pos) { geometryOutput o; o.pos = UnityObjectToClipPos(pos); return o; } … // Remove the following from the geometry shader. //geometryOutput o; //o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); //triStream.Append(o); // ...and replace it with the code below. triStream.Append(VertexOutput(pos + float3(0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(-0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(0, 1, 0))); 

Akhirnya, kita mengalikan simpul output dengan matriks tangentToLocal , menyelaraskan mereka dengan normal dari titik input mereka.

 triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 1, 0)))); 

gambar

Ini lebih seperti yang kita butuhkan, tetapi tidak tepat. Masalahnya di sini adalah bahwa pada awalnya kita menetapkan arah "atas" (atas) dari sumbu Y ; Namun, dalam ruang singgung, arah naik biasanya terletak di sepanjang sumbu Z. Sekarang kita akan melakukan perubahan ini.

 // Modify the position of the third vertex being emitted. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)))); 


3. Penampilan rumput


Untuk membuat segitiga lebih mirip bilah rumput, Anda perlu menambahkan warna dan variasi. Kami mulai dengan menambahkan gradien turun dari atas bilah rumput.

Gradien warna 3.1


Tujuan kami adalah memungkinkan seniman untuk mengatur dua warna - atas dan bawah, dan untuk menyisipkan di antara dua warna ini ia ujung ke pangkal pisau rumput. Warna-warna ini sudah didefinisikan dalam file shader sebagai _TopColor dan _BottomColor . Untuk pengambilan sampel yang tepat, Anda harus meneruskan koordinat UV ke shader fragmen.

 // Add to the geometryOutput struct. float2 uv : TEXCOORD0; … // Modify the VertexOutput function signature. geometryOutput VertexOutput(float3 pos, float2 uv) … // Add to VertexOutput, just below the line assigning o.pos. o.uv = uv; … // Modify the existing lines in the geometry shader. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)), float2(0.5, 1))); 

Kami menciptakan koordinat UV untuk bilah rumput dalam bentuk segitiga, dua simpul dasar yang terletak di kiri bawah dan kanan, dan ujung atas terletak di tengah di atas.


Koordinat UV dari tiga simpul bilah rumput. Meskipun kami mengecat bilah rumput dengan gradien sederhana, pengaturan tekstur yang serupa memungkinkan Anda untuk melapisi tekstur.

Sekarang kita dapat mencicipi warna atas dan bawah dalam shader fragmen dengan UV dan kemudian interpolasi dengan lerp . Kita juga perlu memodifikasi parameter fragmen shader, menjadikan geometryOutput sebagai input, dan bukan hanya posisi float4 .

 // Modify the function signature of the fragment shader. float4 frag (geometryOutput i, fixed facing : VFACE) : SV_Target … // Replace the existing return call. return float4(1, 1, 1, 1); return lerp(_BottomColor, _TopColor, i.uv.y); 


3.2 Arah sudu acak


Untuk menciptakan variabilitas dan memberikan tampilan yang lebih alami pada rumput, kami akan membuat setiap helai rumput terlihat secara acak. Untuk melakukan ini, kita perlu membuat matriks rotasi yang memutar bilah rumput jumlah acak di sekitar sumbu atas .

Ada dua fungsi dalam file shader yang akan membantu kami melakukan ini: rand , yang menghasilkan angka acak dari input tiga dimensi, dan AngleAxis3x3 , yang menerima sudut (dalam radian ) dan mengembalikan matriks yang memutar nilai ini di sekitar sumbu yang ditentukan. Fungsi terakhir bekerja persis sama dengan fungsi C # Quaternion.AngleAxis (hanya AngleAxis3x3 mengembalikan matriks, bukan angka empat).

Fungsi rand mengembalikan angka dalam kisaran 0 ... 1; kita kalikan dengan 2 Pi untuk mendapatkan rentang nilai sudut penuh.

 // Add below the line declaring the tangentToLocal matrix. float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1)); 

Kami menggunakan posisi pos masuk sebagai seed untuk rotasi acak. Karena ini, setiap helai rumput akan memiliki rotasi sendiri, konstan di setiap bingkai.

Rotasi dapat diterapkan pada bilah rumput dengan mengalikannya dengan matriks tangentToLocal dibuat. Perhatikan bahwa perkalian matriks tidak komutatif ; urutan operan itu penting .

 // Add below the line declaring facingRotationMatrix. float3x3 transformationMatrix = mul(tangentToLocal, facingRotationMatrix); … // Replace the multiplication matrix operand with our new transformationMatrix. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, 1)), float2(0.5, 1))); 


3.3 Tekuk ke depan secara acak


Jika semua bilah rumput sejajar sempurna, mereka akan tampak sama. Ini mungkin cocok untuk rumput yang terawat baik, misalnya, di halaman yang dipangkas, tetapi di alam rumput tidak tumbuh seperti itu. Kami akan membuat matriks baru untuk memutar rumput di sepanjang sumbu X , serta properti untuk mengontrol rotasi ini.

 // Add as a new property. _BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2 … // Add to the CGINCLUDE block. float _BendRotationRandom; … // Add to the geometry shader, below the line declaring facingRotationMatrix. float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0)); 

Sekali lagi kami menggunakan posisi bilah rumput sebagai benih acak, kali ini dengan menyapunya untuk membuat benih yang unik. Kami juga akan mengalikan UNITY_PI dengan 0,5 ; ini akan memberi kita interval acak 0 ... 90 derajat.

Kami sekali lagi menerapkan matriks ini melalui rotasi, mengalikan semuanya dalam urutan yang benar.

 // Modify the existing line. float3x3 transformationMatrix = mul(mul(tangentToLocal, facingRotationMatrix), bendRotationMatrix); 


3.4 Lebar dan tinggi


Sedangkan ukuran bilah rumput terbatas pada lebar 1 unit dan tinggi 1 unit. Kami akan menambahkan properti untuk mengontrol ukuran, serta properti untuk menambahkan variasi acak.

 // Add as new properties. _BladeWidth("Blade Width", Float) = 0.05 _BladeWidthRandom("Blade Width Random", Float) = 0.02 _BladeHeight("Blade Height", Float) = 0.5 _BladeHeightRandom("Blade Height Random", Float) = 0.3 … // Add to the CGINCLUDE block. float _BladeHeight; float _BladeHeightRandom; float _BladeWidth; float _BladeWidthRandom; … // Add to the geometry shader, above the triStream.Append calls. float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight; float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth; … // Modify the existing positions with our new height and width. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-width, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1))); 


Segitiga sekarang jauh lebih mirip bilah rumput, tetapi juga terlalu sedikit. Tidak ada cukup puncak di jala yang masuk untuk menciptakan kesan bidang yang terlalu padat.

Salah satu solusinya adalah membuat mesh baru yang lebih padat, baik menggunakan C # atau dalam editor 3D. Ini akan berhasil, tetapi tidak akan memungkinkan kita untuk mengontrol kepadatan rumput secara dinamis. Sebagai gantinya, kami akan membagi mesh yang masuk menggunakan tessellation .

4. Tessellation


Tessellation adalah tahap opsional dari render pipeline, dilakukan setelah vertex shader dan sebelum geometric shader (jika ada). Tugasnya adalah untuk membagi satu permukaan yang masuk ke banyak primitif. Tessellation diimplementasikan dalam dua langkah yang dapat diprogram: hull dan domain shaders.

Untuk shader permukaan, Unity memiliki implementasi tessellation bawaan . Namun, karena kita tidak menggunakan shader permukaan, kita harus mengimplementasikan shell dan domain shader kita sendiri. Pada artikel ini, saya tidak akan membahas implementasi tessellation secara rinci, dan kami hanya menggunakan file CustomTessellation.cginc ada. File ini diadaptasi dari artikel Coding Catlike , yang merupakan sumber informasi yang sangat baik tentang implementasi tessellation di Unity.

Jika kita memasukkan objek TessellationExample dalam adegan, kita akan melihat bahwa itu sudah memiliki materi yang mengimplementasikan tessellation. Mengubah properti Tessellation Uniform menunjukkan efek subdivisi.


Kami menerapkan tessellation di shader rumput untuk mengontrol kepadatan pesawat, dan oleh karena itu untuk mengontrol jumlah bilah rumput yang dihasilkan. Pertama, Anda perlu menambahkan file CustomTessellation.cginc . Kami akan merujuknya dengan jalur relatifnya ke shader.

 // Add inside the CGINCLUDE block, below the other #include statements. #include "Shaders/CustomTessellation.cginc" 

Jika Anda membuka CustomTessellation.cginc , Anda akan melihat bahwa vertexOutput dan vertexOutput , serta vertex shaders, sudah ditentukan di dalamnya. Tidak perlu mendefinisikan ulang mereka di shader rumput kami; mereka dapat dihapus.

 /*struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; }*/ 

Perhatikan bahwa vert vertex shader di CustomTessellation.cginc hanya meneruskan input langsung ke tahap tessellation; fungsi vertexOutput , yang disebut di dalam domain shader, mengambil tugas untuk menciptakan struktur vertexOutput .

Sekarang kita bisa menambahkan shell dan domain shader ke shader rumput. Kami juga akan menambahkan properti _TessellationUniform baru untuk mengontrol ukuran unit - variabel yang sesuai dengan properti ini telah dideklarasikan di CustomTessellation.cginc .

 // Add as a new property. _TessellationUniform("Tessellation Uniform", Range(1, 64)) = 1 … // Add below the other #pragma statements in the SubShader Pass. #pragma hull hull #pragma domain domain 

Sekarang mengubah properti Tessellation Uniform memungkinkan kita untuk mengontrol kepadatan rumput. Saya menemukan bahwa hasil yang baik diperoleh dengan nilai 5 .


5. Angin


Kami menerapkan angin dengan mengambil sampel tekstur distorsi . Tekstur ini akan terlihat seperti peta normal , hanya di dalamnya hanya akan ada dua bukan tiga saluran. Kami akan menggunakan dua saluran ini sebagai arah angin sepanjang X dan Y.


Sebelum mencicipi tekstur angin, kita perlu membuat koordinat UV. Alih-alih menggunakan koordinat tekstur yang ditetapkan untuk mesh, kami menerapkan posisi titik masuk. Berkat ini, jika ada beberapa jerat rumput di dunia, ilusi akan dibuat bahwa mereka semua adalah bagian dari sistem angin yang sama. Kami juga menggunakan _Time shader untuk menggulir tekstur angin di sepanjang permukaan rumput.

 // Add as new properties. _WindDistortionMap("Wind Distortion Map", 2D) = "white" {} _WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0) … // Add to the CGINCLUDE block. sampler2D _WindDistortionMap; float4 _WindDistortionMap_ST; float2 _WindFrequency; … // Add to the geometry shader, just above the line declaring the transformationMatrix. float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y; 

Kami menerapkan skala dan offset _WindDistortionMap ke posisi, dan kemudian menggesernya ke _Time.y , ditingkatkan ke _WindFrequency . Sekarang kita akan menggunakan UVs ini untuk mencicipi tekstur dan membuat properti untuk mengontrol kekuatan angin.

 // Add as a new property. _WindStrength("Wind Strength", Float) = 1 … // Add to the CGINCLUDE block. float _WindStrength; … // Add below the line declaring float2 uv. float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength; 

Perhatikan bahwa kami skala nilai sampel dari tekstur dari interval 0 ... 1 ke interval -1 ... 1. Selanjutnya, kita dapat membuat vektor dinormalisasi yang menunjukkan arah angin.

 // Add below the line declaring float2 windSample. float3 wind = normalize(float3(windSample.x, windSample.y, 0)); 

Sekarang kita dapat membuat matriks untuk memutar vektor ini dan melipatgandakannya dengan Matriks transformationMatrix kita.

 // Add below the line declaring float3 wind. float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind); … // Modify the existing line. float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix); 

Akhirnya, kami mentransfer tekstur Wind (terletak di akar proyek) ke bidang Peta Distorsi Angin dari materi rumput di editor Unity. Kami juga mengatur parameter Ubin tekstur ke 0.01, 0.01 .


Jika rumput tidak menjiwai di jendela Adegan , lalu klik tombol Toggle skybox, kabut, dan berbagai efek lainnya untuk mengaktifkan bahan animasi.

Dari kejauhan, rumput terlihat benar, tetapi jika kita perhatikan dengan cermat bilah rumput, kita melihat bahwa seluruh bilah rumput berputar, itulah sebabnya alas tidak lagi menempel ke tanah.


Pangkal bilah rumput tidak lagi melekat pada tanah, tetapi berpotongan dengan itu (ditunjukkan dengan warna merah ), dan menggantung di atas bidang tanah (ditunjukkan oleh garis hijau ).

Kami akan memperbaikinya dengan mendefinisikan matriks transformasi kedua, yang hanya berlaku untuk dua simpul basis. Dalam matriks ini tidak akan disertakan matriks windRotationdan bendRotationMatrix, berkat yang dasar melekat pada permukaan rumput.

 // Add below the line declaring float3x3 transformationMatrix. float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix); … // Modify the existing lines outputting the base vertex positions. triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0))); 

6. Lengkungan bilah rumput


Sekarang setiap helai rumput ditentukan oleh satu segitiga. Pada jarak yang jauh, ini bukan masalah, tetapi di dekat bilah rumput mereka terlihat sangat kaku dan geometris, bukan organik dan bersemangat. Kami akan memperbaikinya dengan membuat bilah rumput dari beberapa segitiga dan menekuknya di sepanjang kurva .

Setiap helai rumput akan dibagi menjadi beberapa segmen . Setiap segmen akan memiliki bentuk persegi panjang dan terdiri dari dua segitiga, dengan pengecualian segmen atas - itu akan menjadi satu segitiga yang menunjukkan ujung bilah rumput.

Sejauh ini, kami hanya menggambar tiga simpul, membuat segitiga tunggal. Lalu, bagaimana, jika ada lebih banyak simpul, apakah geometri shader tahu mana yang akan bergabung dan membentuk segitiga? Jawabannya ada dalam struktur datastrip segitiga . Tiga simpul pertama bergabung dan membentuk segitiga, dan setiap simpul baru membentuk segitiga dengan dua sebelumnya.


Bilah rumput yang terbagi lagi, direpresentasikan sebagai strip segitiga dan menciptakan satu simpul pada satu waktu. Setelah tiga simpul pertama, setiap simpul baru membentuk segitiga baru dengan dua simpul sebelumnya.

Ini tidak hanya lebih efisien dalam hal penggunaan memori, tetapi juga memungkinkan Anda untuk membuat urutan segitiga dengan mudah dan cepat dalam kode Anda. Jika kita ingin membuat beberapa garis segitiga, kita bisa memanggil RestartStrip untuk TriangleStreamfungsi tersebut . Sebelum kita mulai menggambar lebih banyak simpul dari geometri shader, kita perlu meningkatkannya . Kami akan menggunakan desain untuk memungkinkan penulis shader mengontrol jumlah segmen dan menghitung jumlah simpul yang ditampilkan dari itu.

maxvertexcount#define

 // Add to the CGINCLUDE block. #define BLADE_SEGMENTS 3 … // Modify the existing line defining the maxvertexcount. [maxvertexcount(BLADE_SEGMENTS * 2 + 1)] 

Awalnya, kami menetapkan jumlah segmen menjadi 3 dan memperbarui maxvertexcountuntuk menghitung jumlah simpul berdasarkan jumlah segmen.

Untuk membuat bilah rumput yang tersegmentasi, kami menggunakan siklus for. Setiap iterasi dari loop akan menambahkan dua simpul : kiri dan kanan . Setelah menyelesaikan ujungnya, kami menambahkan simpul terakhir di ujung bilah rumput.

Sebelum kita melakukan ini, akan berguna untuk memindahkan bagian posisi komputasi dari simpul bilah kode ke dalam fungsi, karena kita akan menggunakan kode ini beberapa kali di dalam dan di luar loop. Tambahkan CGINCLUDEberikut ini ke blok :

 geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix) { float3 tangentPoint = float3(width, 0, height); float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint); return VertexOutput(localPosition, uv); } 

Fungsi ini melakukan tugas yang sama karena ia melewati argumen yang sebelumnya kami lewati VertexOutputuntuk menghasilkan simpul dari bilah rumput. Memperoleh posisi, tinggi dan lebar, itu benar mengubah simpul menggunakan matriks yang ditransmisikan dan memberikannya koordinat UV. Kami akan memperbarui kode yang ada agar fungsi berfungsi dengan benar.

 // Update the existing code outputting the vertices. triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

Fungsi mulai bekerja dengan benar, dan kami siap untuk memindahkan kode generasi vertex ke dalam loop for. Tambahkan yang float widthberikut di bawah baris :

 for (int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; } 

Kami mengumumkan siklus yang akan dijalankan sekali untuk setiap bilah segmen rumput. Di dalam loop, tambahkan variabel t. Variabel ini akan menyimpan nilai dalam rentang 0 ... 1, yang menunjukkan seberapa jauh kita telah bergerak di sepanjang bilah rumput. Kami menggunakan nilai ini untuk menghitung lebar dan tinggi segmen di setiap iterasi loop.

 // Add below the line declaring float t. float segmentHeight = height * t; float segmentWidth = width * (1 - t); 

Saat bergerak ke atas rumput, tingginya meningkat dan lebarnya berkurang. Sekarang kita dapat menambahkan panggilan ke loop GenerateGrassVertexuntuk menambahkan simpul ke aliran segitiga. Kami juga akan menambahkan satu panggilan di GenerateGrassVertexluar loop untuk membuat ujung bilah rumput.

 // Add below the line declaring float segmentWidth. float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix; triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, float2(1, t), transformMatrix)); … // Add just below the loop to insert the vertex at the tip of the blade. triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); … // Remove the existing calls to triStream.Append. //triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

Lihatlah garis dengan deklarasi float3x3 transformMatrix- di sini kita memilih salah satu dari dua matriks transformasi: kita ambil transformationMatrixFacinguntuk simpul dari basis dan transformationMatrixuntuk semua yang lain.


Bilah rumput sekarang dibagi menjadi banyak segmen, tetapi permukaan bilahnya masih datar - segitiga baru belum terlibat. Kami akan menambahkan rumput kelengkungan, menggeser posisi vertex dari Y . Pertama, kita perlu memodifikasi fungsi GenerateGrassVertexsehingga mendapat offset di Y , yang akan kita panggil forward.

 // Update the function signature of GenerateGrassVertex. geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix) … // Modify the Y coordinate assignment of tangentPoint. float3 tangentPoint = float3(width, forward, height); 

Untuk menghitung perpindahan setiap simpul, kami mengganti pownilai ke dalam fungsi t. Setelah tnaik ke daya, pengaruhnya pada perpindahan ke depan akan menjadi nonlinear dan mengubah bilah rumput menjadi kurva.

 // Add as new properties. _BladeForward("Blade Forward Amount", Float) = 0.38 _BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2 … // Add to the CGINCLUDE block. float _BladeForward; float _BladeCurve; … // Add inside the geometry shader, below the line declaring float width. float forward = rand(pos.yyz) * _BladeForward; … // Add inside the loop, below the line declaring segmentWidth. float segmentForward = pow(t, _BladeCurve) * forward; … // Modify the GenerateGrassVertex calls inside the loop. triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix)); … // Modify the GenerateGrassVertex calls outside the loop. triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix)); 

Ini adalah potongan kode yang cukup besar, tetapi semua pekerjaan dilakukan mirip dengan apa yang dilakukan untuk lebar dan tinggi bilah rumput. Dengan nilai yang lebih rendah _BladeForwarddan _BladeCurvekami mendapatkan halaman yang tertata rapi, dan nilai yang lebih besar akan memberikan efek sebaliknya.


7. Pencahayaan dan bayangan


Sebagai langkah terakhir untuk menyelesaikan shader, kami akan menambahkan kemampuan untuk melemparkan dan menerima bayangan. Kami juga akan menambahkan pencahayaan sederhana dari sumber utama cahaya directional.

7.1 Bayangan Casting


Untuk memberikan bayangan di Unity, Anda perlu menambahkan pass kedua ke shader. Bagian ini akan digunakan oleh sumber cahaya yang menciptakan bayangan di tempat kejadian untuk membuat kedalaman rumput ke dalam peta bayangan mereka . Ini berarti bahwa shader geometris harus diluncurkan di lorong bayangan, sehingga bilah rumput dapat melemparkan bayangan.

Karena shader geometris ditulis di dalam blok CGINCLUDE, kita dapat menggunakannya dalam lintasan file apa pun. Buat pass kedua yang akan menggunakan shader yang sama dengan yang pertama, dengan pengecualian shader fragmen - kita akan mendefinisikan yang baru di mana kita akan menulis makro yang memproses output.

 // Add below the existing Pass. Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma geometry geo #pragma fragment frag #pragma hull hull #pragma domain domain #pragma target 4.6 #pragma multi_compile_shadowcaster float4 frag(geometryOutput i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } 

Selain membuat shader fragmen baru, ada beberapa perbedaan penting dalam bagian ini. Label LightModepenting ShadowCaster, bukan ForwardBase- ini memberitahu Unity bahwa bagian ini harus digunakan untuk membuat objek menjadi peta bayangan. Ada juga arahan preprosesor di sini multi_compile_shadowcaster. Ini memastikan bahwa shader mengkompilasi semua opsi yang diperlukan untuk membuat bayangan.

Jadikan objek game Fence aktif di TKP; jadi kami mendapatkan permukaan di mana bilah rumput bisa memberi bayangan.


7.2 Mendapatkan Bayangan


Setelah Unity merender peta bayangan dari sudut pandang bayangan yang menciptakan sumber cahaya, ia meluncurkan bagian yang "mengumpulkan" bayangan ke dalam tekstur ruang layar . Untuk mencicipi tekstur ini, kita perlu menghitung posisi simpul di ruang layar dan mentransfernya ke shader fragmen.

 // Add to the geometryOutput struct. unityShadowCoord4 _ShadowCoord : TEXCOORD1; … // Add to the VertexOutput function, just above the return call. o._ShadowCoord = ComputeScreenPos(o.pos); 

Dalam fragmen shader dari bagian itu, ForwardBasekita bisa menggunakan makro untuk mendapatkan nilai yang floatmenunjukkan apakah permukaan dalam bayangan atau tidak. Nilai ini berada dalam kisaran 0 ... 1, di mana 0 adalah naungan penuh, 1 adalah penerangan penuh.

Mengapa koordinat UV dari ruang layar disebut _ShadowCoord? Ini tidak sesuai dengan konvensi penamaan sebelumnya.
Unity ( ). SHADOW_ATTENUATION . Autolight.cginc , , .

 #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) 

- , .

 // Add to the ForwardBase pass's fragment shader, replacing the existing return call. return SHADOW_ATTENUATION(i); //return lerp(_BottomColor, _TopColor, i.uv.y); 

Akhirnya, kita perlu membuat shader dikonfigurasi dengan benar untuk menerima bayangan. Untuk melakukan ini, kami akan menambahkan ForwardBasearahan preprocessor ke pass sehingga mengkompilasi semua opsi shader yang diperlukan.

 // Add to the ForwardBase pass's preprocessor directives, below #pragma target 4.6. #pragma multi_compile_fwdbase 


Setelah mendekatkan kamera, kita bisa melihat artefak di permukaan bilah rumput; mereka disebabkan oleh fakta bahwa bilah rumput individu membayangi diri mereka sendiri. Kita dapat memperbaikinya dengan menerapkan pergeseran linier atau memindahkan posisi simpul di ruang pemotongan sedikit jauh dari layar. Kami akan menggunakan makro Unity untuk ini dan memasukkannya ke dalam desain #ifsehingga operasi hanya dilakukan di jalur bayangan.

 // Add at the end of the VertexOutput function, just above the return call. #if UNITY_PASS_SHADOWCASTER // Applying the bias prevents artifacts from appearing on the surface. o.pos = UnityApplyLinearShadowBias(o.pos); #endif 


Setelah menerapkan pergantian bayangan linier, artefak bayangan dalam bentuk garis-garis menghilang dari permukaan segitiga.

Mengapa ada artefak di sepanjang tepi bilah rumput yang teduh?

(multisample anti-aliasing MSAA ) Unity , . , .

— , , Unity . ( ); Unity .

7.3 Pencahayaan


Kami akan menerapkan pencahayaan menggunakan algoritma perhitungan pencahayaan tersebar sangat sederhana dan umum.


... di mana N adalah normal ke permukaan, L adalah arah yang dinormalisasi dari sumber utama pencahayaan terarah, dan I adalah pencahayaan yang dihitung. Dalam tutorial ini kami tidak akan menerapkan pencahayaan tidak langsung.

Saat ini, normals tidak ditugaskan untuk simpul dari bilah rumput. Seperti halnya posisi vertex, pertama-tama kita menghitung normals dalam ruang singgung dan kemudian mengubahnya menjadi lokal.

Ketika Angka Kelengkungan Pisau adalah 1 , semua bilah rumput di ruang singgung diarahkan dalam satu arah: tepat di seberang sumbu Y. Sebagai langkah pertama dari solusi kami, kami menghitung yang normal, dengan asumsi tidak ada lengkungan.

 // Add to the GenerateGrassVertex function, belowing the line declaring tangentPoint. float3 tangentNormal = float3(0, -1, 0); float3 localNormal = mul(transformMatrix, tangentNormal); 

tangentNormal, didefinisikan secara langsung berlawanan dengan sumbu Y , ditransformasikan oleh matriks yang sama yang kami gunakan untuk mengkonversi titik singgung ke ruang lokal. Sekarang kita bisa meneruskannya ke suatu fungsi VertexOutput, dan kemudian ke suatu struktur geometryOutput.

 // Modify the return call in GenerateGrassVertex. return VertexOutput(localPosition, uv, localNormal); … // Add to the geometryOutput struct. float3 normal : NORMAL; … // Modify the existing function signature. geometryOutput VertexOutput(float3 pos, float2 uv, float3 normal) … // Add to the VertexOutput function to pass the normal through to the fragment shader. o.normal = UnityObjectToWorldNormal(normal); 

Perhatikan bahwa sebelum kesimpulan, kita mengubah yang normal menjadi ruang dunia ; Unity menyampaikan kepada shaders arah sumber utama cahaya terarah di ruang dunia, sehingga transformasi ini diperlukan.

Sekarang kita dapat memvisualisasikan normals dalam fragmen shader ForwardBaseuntuk memeriksa hasil pekerjaan kita.

 // Add to the ForwardBase fragment shader. float3 normal = facing > 0 ? i.normal : -i.normal; return float4(normal * 0.5 + 0.5, 1); // Remove the existing return call. //return SHADOW_ATTENUATION(i); 

Karena Cullnilai diberikan pada shader kami Off, kedua sisi bilah rumput diberikan. Agar normal diarahkan pada arah yang benar, kami menggunakan parameter tambahan VFACEyang kami tambahkan ke shader fragmen.

Argumen fixed facingakan mengembalikan angka positif jika kita menampilkan permukaan depan, dan angka negatif jika sebaliknya. Kami menggunakan ini dalam kode di atas untuk membalik normal jika perlu.


Ketika Jumlah Kelengkungan Blade lebih besar dari 1, posisi garis singgung Z dari setiap simpul akan digeser dengan jumlah yang forwardditeruskan ke fungsi GenerateGrassVertex. Kami akan menggunakan nilai ini untuk secara proporsional skala sumbu Z dari normal.

 // Modify the existing line in GenerateGrassVertex. float3 tangentNormal = normalize(float3(0, -1, forward)); 

Terakhir, tambahkan kode ke shader fragmen untuk menggabungkan bayangan, pencahayaan terarah, dan pencahayaan sekitar. Saya merekomendasikan mempelajari informasi yang lebih terperinci tentang penerapan pencahayaan khusus dalam shader di tutorial saya tentang toon shader .

 // Add to the ForwardBase fragment shader, below the line declaring float3 normal. float shadow = SHADOW_ATTENUATION(i); float NdotL = saturate(saturate(dot(normal, _WorldSpaceLightPos0)) + _TranslucentGain) * shadow; float3 ambient = ShadeSH9(float4(normal, 1)); float4 lightIntensity = NdotL * _LightColor0 + float4(ambient, 1); float4 col = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y); return col; // Remove the existing return call. //return float4(normal * 0.5 + 0.5, 1); 


Kesimpulan


Dalam tutorial ini, rumput mencakup area kecil 10x10 unit. Agar shader dapat menutup ruang terbuka besar dengan tetap mempertahankan kinerja tinggi, optimisasi harus diperkenalkan. Anda dapat menerapkan tessellation berdasarkan jarak sehingga bilah rumput lebih sedikit dihasilkan dari kamera. Selain itu, untuk jarak yang jauh, bukannya bilah rumput individu, kelompok bilah rumput dapat ditarik menggunakan quadrangle tunggal dengan tekstur yang ditumpangkan.


Tekstur rumput termasuk dalam paket Aset Standar mesin Unity. Banyak bilah rumput digambar pada satu segi empat, yang mengurangi jumlah segitiga dalam adegan.

Meskipun secara alami kita tidak dapat menggunakan pembagi geometris dengan pembungkus permukaan, untuk meningkatkan atau memperluas fungsi pencahayaan dan naungan, jika Anda perlu menggunakan model pencahayaan Unity standar, Anda dapat mempelajari repositori GitHub ini , yang menunjukkan solusi untuk masalah ini dengan menunda render dan pengisian G-buffer secara manual.

Kode sumber Shader di repositori GitHub

Penambahan: kerja sama


Tanpa interoperabilitas, efek grafis mungkin tampak statis atau tidak bernyawa bagi pemain. Tutorial ini sudah sangat panjang, jadi saya tidak menambahkan bagian tentang interaksi objek dunia dengan rumput.

Implementasi herbal interaktif yang naif akan mengandung dua komponen: sesuatu di dunia game yang dapat mengirimkan data ke shader untuk memberi tahu bagian mana dari rumput yang sedang berinteraksi dengan, dan kode dalam shader untuk menafsirkan data ini.

Contoh bagaimana ini dapat diterapkan dengan air ditunjukkan di sini . Dapat diadaptasi untuk bekerja dengan rumput; alih-alih menggambar riak di tempat karakter berada, Anda dapat membalik bilah rumput ke bawah untuk mensimulasikan efek langkah.

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


All Articles