Tugas terakhir saya dalam grafik teknis / rendering adalah menemukan solusi yang baik untuk rendering air. Secara khusus, rendering jet air tipis dan bergerak cepat berdasarkan partikel. Selama seminggu terakhir, saya memikirkan hasil yang baik, jadi saya akan menulis artikel tentang ini.
Saya tidak terlalu suka pendekatan kubus voxelized / marching ketika memberikan air (lihat, misalnya, memberikan simulasi cairan di Blender). Ketika volume air berada pada skala yang sama dengan kisi-kisi yang digunakan untuk rendering, gerakannya terasa terpisah. Masalah ini dapat diselesaikan dengan meningkatkan resolusi grid, tetapi untuk jet tipis jarak yang relatif jauh secara real time itu tidak praktis karena sangat mempengaruhi waktu eksekusi dan memori yang ditempati. (Ada preseden untuk menggunakan struktur voxel yang jarang untuk memperbaiki situasi. Tapi saya tidak yakin seberapa baik ini bekerja untuk sistem dinamis. Juga, ini bukan tingkat kesulitan yang ingin saya kerjakan.)
Alternatif pertama yang saya jelajahi adalah Screen Space Meshes Müller. Mereka menggunakan rendering partikel air ke dalam buffer kedalaman, menghaluskannya, mengenali fragmen yang terhubung dari kedalaman yang sama, dan membangun mesh dari hasilnya menggunakan marching square. Saat ini, metode ini mungkin menjadi
lebih dapat diterapkan daripada tahun 2007 (karena sekarang kita dapat membuat mesh dalam compute shader), tetapi masih terkait dengan tingkat kerumitan dan biaya yang lebih besar daripada yang saya inginkan.
Pada akhirnya, saya menemukan presentasi Simon Green dengan GDC 2010, Screen Space Fluid Rendering For Games. Ini dimulai dengan cara yang persis sama dengan Screen Space Meshes: dengan merender partikel ke dalam buffer kedalaman dan menghaluskannya. Tetapi alih-alih membangun mesh, buffer yang dihasilkan digunakan untuk menaungi dan menyusun cairan dalam adegan utama (dengan secara eksplisit merekam kedalaman). Saya memutuskan untuk mengimplementasikan sistem seperti itu.
Persiapan
Beberapa proyek Unity sebelumnya mengajarkan saya untuk tidak berurusan dengan keterbatasan rendering mesin. Oleh karena itu, buffer cairan dirender oleh kamera kedua dengan kedalaman bidang yang lebih dangkal sehingga membuatnya di depan adegan utama. Setiap sistem fluida ada pada lapisan rendering yang terpisah; ruang utama tidak termasuk lapisan air, dan ruang kedua hanya menghasilkan air. Kedua kamera adalah anak-anak dari objek kosong untuk memastikan orientasi relatif mereka.
Skema seperti itu berarti bahwa saya dapat membuat hampir semua hal dalam lapisan cair, dan akan terlihat seperti yang saya harapkan. Dalam konteks adegan demo saya, ini berarti bahwa beberapa jet dan percikan dari sub-emitter dapat bergabung bersama. Selain itu, ini akan memungkinkan pencampuran sistem air lainnya, misalnya, volume berdasarkan bidang ketinggian, yang kemudian dapat dibuat sama. (Saya belum menguji ini.)
Sumber air dalam adegan saya adalah sistem partikel standar. Faktanya, tidak ada simulasi fluida yang dilakukan. Ini, pada gilirannya, berarti bahwa partikel tidak saling tumpang tindih dengan cara yang sepenuhnya fisik, tetapi hasil akhirnya tampaknya dapat diterima dalam praktiknya.
Render buffer cairan
Langkah pertama dalam teknik ini adalah membuat buffer fluida dasar. Ini adalah penyangga di luar layar yang berisi (pada tahap implementasi saya saat ini) sebagai berikut: lebar fluida, vektor gerak di ruang layar dan nilai noise. Selain itu, kami membuat buffer kedalaman dengan secara eksplisit merekam kedalaman dari shader fragmen untuk mengubah setiap segi empat dari partikel menjadi "bola" bola (well, sebenarnya elips).
Perhitungan kedalaman dan lebar cukup sederhana:
frag_out o; float3 N; N.xy = i.uv*2.0 - 1.0; float r2 = dot(N.xy, N.xy); if (r2 > 1.0) discard; Nz = sqrt(1.0 - r2); float4 pixel_pos = float4(i.view_pos + N * i.size, 1.0); float4 clip_pos = mul(UNITY_MATRIX_P, pixel_pos); float depth = clip_pos.z / clip_pos.w; o.depth = depth; float thick = Nz * i.size * 2;
(Tentu saja, perhitungan kedalaman dapat disederhanakan; dari posisi klip kita hanya perlu z dan w.)
Beberapa saat kemudian, kita akan kembali ke shader fragmen untuk vektor gerakan dan derau.
Kegembiraan dimulai dari vertex shader, dan di sinilah saya menyimpang dari teknik Green. Tujuan proyek ini adalah membuat pancaran air berkecepatan tinggi; itu dapat diwujudkan dengan bantuan partikel bola, tetapi sejumlah besar dari mereka akan diperlukan untuk membuat jet kontinu. Sebagai gantinya, saya akan meregangkan segi empat partikel berdasarkan kecepatannya, yang pada gilirannya meregangkan bola-bola kedalaman, membuat mereka tidak berbentuk bola, tetapi berbentuk bulat panjang. (Karena perhitungan kedalaman didasarkan pada UV, yang tidak berubah, semuanya hanya berfungsi.)
Pengguna Unity yang berpengalaman mungkin bertanya-tanya mengapa saya tidak menggunakan mode Built-in Billboard yang tersedia di sistem partikel Unity. Membentang Billboard melakukan peregangan tanpa syarat sepanjang vektor kecepatan di ruang dunia. Dalam kasus umum, ini sangat cocok, tetapi mengarah ke masalah yang sangat nyata ketika vektor kecepatan diarahkan bersama dengan vektor kamera yang menghadap ke depan (atau sangat dekat dengannya). Billboard membentang di layar, yang membuat sifat dua dimensinya sangat terlihat.
Sebagai gantinya, saya menggunakan papan iklan yang diarahkan ke kamera dan memproyeksikan vektor kecepatan ke bidang partikel, menggunakannya untuk meregangkan segi empat. Jika vektor kecepatan tegak lurus terhadap bidang (diarahkan ke layar atau menjauh darinya), maka partikel tetap tidak terentang dan bulat, sebagaimana mestinya, dan ketika dimiringkan, partikel direntangkan ke arah ini, yang merupakan apa yang kita butuhkan.
Mari kita tinggalkan penjelasan panjang, berikut ini fungsi yang cukup sederhana:
float3 ComputeStretchedVertex(float3 p_world, float3 c_world, float3 vdir_world, float stretch_amount) { float3 center_offset = p_world - c_world; float3 stretch_offset = dot(center_offset, vdir_world) * vdir_world; return p_world + stretch_offset * lerp(0.25f, 3.0f, stretch_amount); }
Untuk menghitung vektor gerak ruang layar, kami menghitung dua set posisi vektor:
float3 vp1 = ComputeStretchedVertex( vertex_wp, center_wp, velocity_dir_w, rand); float3 vp0 = ComputeStretchedVertex( vertex_wp - velocity_w * unity_DeltaTime.x, center_wp - velocity_w * unity_DeltaTime.x, velocity_dir_w, rand); o.motion_0 = mul(_LastVP, float4(vp0, 1.0)); o.motion_1 = mul(_CurrVP, float4(vp1, 1.0));
Perhatikan bahwa karena kita menghitung vektor gerak di bagian utama dan bukan di bagian vektor kecepatan, Unity tidak memberi kita proyeksi arus sebelumnya atau tidak terdistorsi dari pandangan. Untuk memperbaikinya, saya menambahkan skrip sederhana ke sistem partikel yang sesuai:
public class ScreenspaceLiquidRenderer : MonoBehaviour { public Camera LiquidCamera; private ParticleSystemRenderer m_ParticleRenderer; private bool m_First; private Matrix4x4 m_PreviousVP; void Start() { m_ParticleRenderer = GetComponent(); m_First = true; } void OnWillRenderObject() { Matrix4x4 current_vp = LiquidCamera.nonJitteredProjectionMatrix * LiquidCamera.worldToCameraMatrix; if (m_First) { m_PreviousVP = current_vp; m_First = false; } m_ParticleRenderer.material.SetMatrix("_LastVP", GL.GetGPUProjectionMatrix(m_PreviousVP, true)); m_ParticleRenderer.material.SetMatrix("_CurrVP", GL.GetGPUProjectionMatrix(current_vp, true)); m_PreviousVP = current_vp; } }
Saya cache matriks sebelumnya secara manual karena Camera.previousViewProjectionMatrix memberikan hasil yang salah.
¯ \ _ (ツ) _ / ¯
(Juga, metode ini melanggar rendering rendering; mungkin lebih bijaksana untuk menetapkan konstanta matriks global dalam praktik daripada menggunakannya untuk setiap materi.)
Mari kita kembali ke shader fragmen: kita menggunakan posisi yang diproyeksikan untuk menghitung vektor gerak ruang layar:
float3 hp0 = i.motion_0.xyz / i.motion_0.w; float3 hp1 = i.motion_1.xyz / i.motion_1.w; float2 vp0 = (hp0.xy + 1) / 2; float2 vp1 = (hp1.xy + 1) / 2; #if UNITY_UV_STARTS_AT_TOP vp0.y = 1.0 - vp0.y; vp1.y = 1.0 - vp1.y; #endif float2 vel = vp1 - vp0;
(Perhitungan vektor gerakan hampir tidak berubah diambil dari
https://github.com/keijiro/ParticleMotionVector/blob/master/Assets/ParticleMotionVector/Shaders/Motion.cginc )
Akhirnya, nilai terakhir dalam buffer cairan adalah noise. Saya menggunakan angka acak yang stabil untuk setiap partikel untuk memilih satu dari empat suara (dikemas dalam satu tekstur tunggal). Kemudian diskalakan dengan kecepatan dan kesatuan minus ukuran partikel (oleh karena itu, partikel cepat dan kecil lebih berisik). Nilai noise ini digunakan dalam shading pass untuk mendistorsi normals dan menambahkan lapisan busa. Karya Green menggunakan noise putih tiga saluran, tetapi karya yang lebih baru (Screen Space Fluid Rendering with Curvature Flow) mengusulkan untuk menggunakan noise Perlin. Saya menggunakan noise Voronoi / noise sel dengan skala yang berbeda:
Mencampur Masalah (dan Penanganan Masalah)
Dan di sini masalah pertama implementasi saya muncul. Untuk perhitungan yang benar dari ketebalan partikel dicampur secara aditif. Karena pencampuran mempengaruhi semua keluaran, ini berarti vektor noise dan gerakan juga tercampur secara aditif. Aditif suara cukup cocok untuk kita, tetapi bukan vektor aditif, dan jika kamu membiarkannya apa adanya, kamu akan mendapatkan waktu anti-aliasing (TAA) dan motion blur yang menjijikkan. Untuk mengatasi masalah ini, saat merender penyangga fluida, saya cukup mengalikan vektor gerakan dengan ketebalan dan membaginya dengan ketebalan total dalam lintasan bayangan. Ini memberi kita vektor gerakan rata-rata tertimbang untuk semua partikel yang tumpang tindih; tidak cukup apa yang kita butuhkan (artefak aneh dibuat ketika beberapa jet berpotongan), tetapi cukup dapat diterima.
Masalah yang lebih kompleks adalah kedalaman; Untuk rendering kedalaman yang tepat, kita harus memiliki rekaman kedalaman dan pemeriksaan kedalaman yang aktif. Ini dapat menyebabkan masalah jika partikel tidak diurutkan (karena perbedaan dalam urutan rendering dapat menyebabkan output partikel yang tumpang tindih oleh orang lain menjadi terpotong). Karena itu, kami memesan sistem partikel Unity untuk mengurutkan partikel berdasarkan kedalaman, dan kemudian kami menyilangkan jari dan harapan kami. sistem itu juga akan menyajikan secara mendalam. Kami akan * memiliki * kasus sistem yang tumpang tindih (misalnya, persimpangan dua jet partikel) yang tidak diproses dengan benar, yang akan menyebabkan ketebalan yang lebih kecil. Tapi ini tidak terlalu sering terjadi, dan tidak terlalu memengaruhi penampilan.
Kemungkinan besar, pendekatan yang benar adalah membuat kedalaman dan penyangga warna benar-benar terpisah; pengembalian untuk ini adalah rendering dua-pass. Ada baiknya mengeksplorasi masalah ini ketika mengatur sistem.
Kedalaman penghalusan
Akhirnya, hal terpenting dalam teknik Hijau. Kami membuat sekelompok bola bundar ke dalam buffer kedalaman, tetapi pada kenyataannya, air tidak terdiri dari "bola". Jadi sekarang kita ambil perkiraan ini dan mengaburkannya agar lebih seperti permukaan cairan.
Pendekatan naif adalah dengan hanya menerapkan kedalaman noise Gaussian ke seluruh buffer. Ini menciptakan hasil yang aneh - itu menghaluskan titik jauh lebih dari yang dekat, dan mengaburkan tepi siluet. Sebagai gantinya, kita dapat mengubah radius blur secara mendalam, dan menggunakan blur dua sisi untuk menghemat tepinya.
Hanya satu masalah muncul di sini: perubahan seperti itu membuat blur tidak bisa dibedakan. Blur bersama dapat dilakukan dalam dua lintasan: blur secara horizontal, dan kemudian secara vertikal. Pengaburan yang tidak bisa dibedakan dilakukan dalam satu pass. Perbedaan ini penting karena skala blur yang dibagi secara linear (O (w) + O (h)), dan skala blur yang tidak dibagi secara tepat (O (w * h)). Pengaburan dalam skala besar dan tidak dibagi dengan cepat menjadi tidak berlaku dalam praktik.
Sebagai orang dewasa, pengembang yang bertanggung jawab, kita dapat membuat langkah yang jelas: tutup mata, berpura-pura bahwa kebisingan dua arah * dibagikan *, dan masih menerapkannya dengan lorong horizontal dan vertikal yang terpisah.
Green dalam presentasinya menunjukkan bahwa meskipun pendekatan ini
menciptakan artefak dalam hasil yang dihasilkan (terutama ketika merekonstruksi normals), tahap naungan menyembunyikannya dengan baik. Ketika bekerja dengan aliran air yang lebih sempit yang saya buat, artefak ini bahkan kurang terlihat dan tidak terlalu mempengaruhi hasilnya.
Shading
Kami akhirnya selesai bekerja dengan buffer cairan. Sekarang mari kita beralih ke bagian kedua dari efek: naungan dan penggabungan gambar utama.
Di sini kita menemukan banyak pembatasan rendering Persatuan. Saya memutuskan untuk menyinari air hanya dengan cahaya matahari dan skybox; Mendukung sumber pencahayaan tambahan memerlukan beberapa lintasan (ini boros!) Atau membangun struktur pencarian pencahayaan di sisi GPU (mahal dan agak rumit). Selain itu, karena Unity tidak menyediakan akses ke peta bayangan, dan lampu arah menggunakan bayangan ruang layar (berdasarkan buffer kedalaman yang dihasilkan oleh geometri buram), kami tidak memiliki akses ke informasi tentang bayangan dari sumber cahaya matahari. Anda dapat melampirkan buffer perintah ke sumber cahaya matahari untuk membuat peta bayangan ruang layar khusus untuk air, tetapi sejauh ini saya belum melakukannya.
Tahap terakhir naungan dikontrol melalui skrip, dan menggunakan buffer perintah untuk mengirim panggilan draw. Ini
diperlukan karena tekstur vektor gerak (digunakan untuk anti-aliasing sementara (TAA) dan blur gerak) tidak dapat digunakan untuk rendering langsung menggunakan Graphics.SetRenderTarget (). Dalam skrip yang dilampirkan ke kamera utama, kami menulis yang berikut ini:
void Start() {
Buffer warna dan vektor gerakan tidak dapat dirender secara bersamaan dengan MRT (target multi render). Saya tidak dapat menemukan alasannya. Selain itu, mereka membutuhkan pengikatan ke buffer kedalaman yang berbeda. Untungnya, kami menulis kedalaman untuk
kedua buffer kedalaman ini, jadi memproyeksikan ulang anti-aliasing sementara berfungsi dengan baik (oh, senang bekerja dengan mesin "kotak hitam").
Di setiap bingkai, kami membuang render gabungan dari OnPostRender ():
RenderTexture GenerateRefractionTexture() { RenderTexture result = RenderTexture.GetTemporary(m_MainCamera.activeTexture.descriptor); Graphics.Blit(m_MainCamera.activeTexture, result); return result; } void OnPostRender() { if (ScreenspaceLiquidCamera && ScreenspaceLiquidCamera.IsReady()) { RenderTexture refraction_texture = GenerateRefractionTexture(); m_Mat.SetTexture("_MainTex", ScreenspaceLiquidCamera.GetColorBuffer()); m_Mat.SetVector("_MainTex_TexelSize", ScreenspaceLiquidCamera.GetTexelSize()); m_Mat.SetTexture("_LiquidRefractTexture", refraction_texture); m_Mat.SetTexture("_MainDepth", ScreenspaceLiquidCamera.GetDepthBuffer()); m_Mat.SetMatrix("_DepthViewFromClip", ScreenspaceLiquidCamera.GetProjection().inverse); if (SunLight) { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(-SunLight.transform.forward)); m_Mat.SetColor("_SunColor", SunLight.color * SunLight.intensity); } else { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(new Vector3(0, 1, 0))); m_Mat.SetColor("_SunColor", Color.white); } m_Mat.SetTexture("_ReflectionProbe", ReflectionProbe.defaultTexture); m_Mat.SetVector("_ReflectionProbe_HDR", ReflectionProbe.defaultTextureHDRDecodeValues); Graphics.ExecuteCommandBuffer(m_CommandBuffer); RenderTexture.ReleaseTemporary(refraction_texture); } }
Dan di sinilah partisipasi CPU berakhir, kemudian hanya shader pergi.
Mari kita mulai dengan berjalannya vektor gerakan. Beginilah keseluruhan bentuk shader:
#include "UnityCG.cginc" sampler2D _MainDepth; sampler2D _MainTex; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_P, v.vertex); o.uv = v.uv; return o; } struct frag_out { float4 color : SV_Target; float depth : SV_Depth; }; frag_out frag(v2f i) { frag_out o; float4 fluid = tex2D(_MainTex, i.uv); if (fluid.a == 0) discard; o.depth = tex2D(_MainDepth, i.uv).r; float2 vel = fluid.gb / fluid.a; o.color = float4(vel, 0, 1); return o; }
Kecepatan dalam ruang layar disimpan dalam saluran hijau dan biru dari buffer fluida. Karena kami meningkatkan kecepatan dengan ketebalan saat merender buffer, kami membagi lagi ketebalan total (yang terletak di saluran alpha) untuk mendapatkan kecepatan rata-rata tertimbang.
Perlu dicatat bahwa ketika bekerja dengan volume air yang besar, metode lain untuk memproses buffer kecepatan mungkin diperlukan. Karena kita membuat tanpa pencampuran, vektor gerakan untuk semua yang ada di
belakang air hilang, menghancurkan TAA dan mengaburkan gerakan dari benda-benda ini. Saat bekerja dengan aliran air yang tipis, ini bukan masalah, tapi itu bisa mengganggu saat bekerja dengan kolam atau danau ketika kita membutuhkan TAA atau benda buram gerak agar terlihat jelas melalui permukaan.
Lebih menarik adalah lulus naungan utama. Prioritas pertama kami setelah penutupan dengan ketebalan cairan adalah merekonstruksi posisi dan ruang tampilan normal (ruang tampilan).
float3 ViewPosition(float2 uv) { float clip_z = tex2D(_MainDepth, uv).r; float clip_x = uv.x * 2.0 - 1.0; float clip_y = 1.0 - uv.y * 2.0; float4 clip_p = float4(clip_x, clip_y, clip_z, 1.0); float4 view_p = mul(_DepthViewFromClip, clip_p); return (view_p.xyz / view_p.w); } float3 ReconstructNormal(float2 uv, float3 vp11) { float3 vp12 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, 1)); float3 vp10 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, -1)); float3 vp21 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(1, 0)); float3 vp01 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(-1, 0)); float3 dvpdx0 = vp11 - vp12; float3 dvpdx1 = vp10 - vp11; float3 dvpdy0 = vp11 - vp21; float3 dvpdy1 = vp01 - vp11;
Ini adalah cara yang mahal untuk merekonstruksi posisi ruang tampilan: kami mengambil posisi di ruang klip dan melakukan operasi proyeksi terbalik.
Setelah kami mendapatkan cara untuk merekonstruksi posisi, normalnya lebih sederhana: kami menghitung posisi titik-titik tetangga di buffer kedalaman dan membangun basis garis singgung dari mereka. Untuk bekerja dengan tepi siluet, kami mengambil sampel di kedua arah dan memilih titik terdekat dengan ruang tampilan untuk merekonstruksi yang normal. Metode ini bekerja sangat baik dan menyebabkan masalah hanya pada benda yang sangat tipis.
Ini berarti bahwa kami melakukan lima operasi proyeksi mundur terpisah per piksel (untuk titik saat ini dan empat yang berdekatan). Ada cara yang lebih murah, tetapi posting ini sudah terlalu lama, jadi saya akan meninggalkannya untuk nanti.
Normal yang dihasilkan adalah:
Saya mendistorsi normal yang dihitung ini menggunakan turunan dari nilai kebisingan dari buffer fluida, diskalakan oleh parameter gaya dan dinormalisasi dengan membaginya dengan ketebalan jet (untuk alasan yang sama seperti untuk kecepatan):
N.xy += NoiseDerivatives(i.uv, fluid.r) * (_NoiseStrength / fluid.a); N = normalize(N);
Kami akhirnya dapat melanjutkan dengan naungan itu sendiri. Naungan air terdiri dari tiga bagian utama: refleksi specular, refraksi specular dan busa.
Refleksi adalah GGX standar yang diambil seluruhnya dari standar Unity shader. (Dengan satu koreksi, F0 2% yang benar digunakan untuk air.)
Dengan pembiasan, semuanya jadi lebih menarik. Pembiasan yang benar membutuhkan raytracing (atau raymarching untuk hasil perkiraan). Untungnya, pembiasan kurang intuitif untuk mata daripada refleksi, dan karena itu hasil yang salah tidak begitu terlihat. Oleh karena itu, kami menggeser sampel UV untuk tekstur bias dengan normals x dan y, diskalakan oleh parameter tebal dan gaya:
float aspect = _MainTex_TexelSize.y * _MainTex_TexelSize.z; float2 refract_uv = (i.grab_pos.xy + N.xy * float2(1, -aspect) * fluid.a * _RefractionMultiplier) / i.grab_pos.w; float4 refract_color = tex2D(_LiquidRefractTexture, refract_uv);
(Perhatikan bahwa koreksi korelasi digunakan; ini
opsional - setelah semua, itu hanya perkiraan, tetapi menambahkannya cukup sederhana.)
Cahaya yang dibiaskan ini melewati cairan, sehingga sebagian diserap:
float3 water_color = _AbsorptionColor.rgb * _AbsorptionIntensity; refract_color.rgb *= exp(-water_color * fluid.a);
Perhatikan bahwa _AbsorptionColor ditentukan dengan kebalikan dari cara yang diharapkan: nilai dari masing-masing saluran menunjukkan jumlah cahaya yang
diserap daripada cahaya yang ditransmisikan. Oleh karena itu, _AbsorptionColor dengan nilai (1, 0, 0) tidak memberikan warna merah, tetapi warna pirus (teal).
Refleksi dan refraksi dicampur menggunakan koefisien Fresnel:
float spec_blend = lerp(0.02, 1.0, pow(1.0 - ldoth, 5)); float4 clear_color = lerp(refract_color, spec, spec_blend);
Sampai saat itu, kami bermain sesuai aturan (kebanyakan) dan menggunakan naungan fisik.
Dia cukup baik, tetapi dia memiliki masalah dengan air. Agak sulit dilihat:
Untuk memperbaikinya, mari tambahkan busa.
Busa muncul ketika air bergolak dan udara bercampur dengan air untuk membentuk gelembung. Gelembung semacam itu menciptakan semua jenis variasi refleksi dan pembiasan, yang memberi semua air rasa pencahayaan yang menyebar. Saya akan memodelkan perilaku ini dengan cahaya sekitar yang dibungkus:
float3 foam_color = _SunColor * saturate((dot(N, L)*0.25f + 0.25f));
Ini ditambahkan ke warna akhir menggunakan faktor khusus, tergantung pada kebisingan cairan dan koefisien Fresnel melunak:
float foam_blend = saturate(fluid.r * _NoiseStrength) * lerp(0.05f, 0.5f, pow(1.0f - ndotv, 3)); clear_color.rgb += foam_color * saturate(foam_blend);
Pencahayaan ambient yang dibungkus dinormalisasi untuk menghemat energi sehingga dapat digunakan sebagai perkiraan difusi. Pencampuran warna busa lebih terlihat. Ini merupakan pelanggaran yang cukup jelas terhadap hukum konservasi energi.
Namun secara umum, semuanya terlihat bagus dan membuat aliran lebih terlihat:
Pekerjaan dan perbaikan lebih lanjut
Dalam sistem yang dibuat, banyak yang bisa diperbaiki.
- Menggunakan banyak warna. Saat ini, penyerapan dihitung hanya pada tahap terakhir naungan dan menggunakan warna dan kecerahan konstan untuk semua cairan pada layar. Dukungan untuk warna yang berbeda dimungkinkan, tetapi membutuhkan buffer warna kedua dan solusi integral penyerapan untuk setiap partikel dalam proses rendering buffer fluida dasar. Ini berpotensi menjadi operasi yang mahal.
- Cakupan penuh. Memiliki akses ke struktur pencarian pencahayaan di sisi GPU (baik dibangun dengan tangan, atau berkat ikatan dengan pipa render Unity HD yang baru), kami dapat menerangi air dengan baik dengan sejumlah sumber cahaya dan menciptakan pencahayaan sekitar yang tepat.
- Perbaikan refraksi. Dengan tekstur mip buram dari tekstur latar belakang, kita dapat mensimulasikan dengan lebih baik refraksi untuk permukaan kasar. Dalam praktiknya, ini tidak terlalu berguna untuk semprotan cairan kecil, tetapi mungkin berguna untuk volume yang lebih besar.
Jika saya memiliki kesempatan, saya akan meningkatkan sistem ini hingga kehilangan denyut nadi, tetapi saat ini dapat disebut lengkap.