Bayangan 2D pada Bidang Jarak yang Ditandatangani

Sekarang kita tahu dasar-dasar menggabungkan fungsi jarak yang ditandatangani, Anda dapat menggunakannya untuk membuat hal-hal keren. Dalam tutorial ini, kita akan menggunakannya untuk membuat bayangan dua dimensi yang lembut. Jika Anda belum membaca tutorial sebelumnya tentang bidang jarak yang ditandatangani (SDF), maka saya sangat menyarankan Anda mempelajarinya, dimulai dengan tutorial tentang cara membuat bentuk sederhana .


[GIF menghasilkan artefak tambahan selama rekompresi.]

Konfigurasi dasar


Saya membuat konfigurasi sederhana dengan sebuah ruangan, menggunakan teknik yang dijelaskan dalam tutorial sebelumnya. Sebelumnya, saya tidak menyebutkan bahwa saya menggunakan fungsi abs untuk vector2 untuk mencerminkan posisi relatif terhadap sumbu x dan y, dan juga bahwa saya membalikkan jarak gambar untuk menukar bagian dalam dan luar.

Kami akan menyalin file 2D_SDF.cginc dari tutorial sebelumnya ke dalam satu folder dengan shader, yang akan kami tulis dalam tutorial ini.

 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); return dist; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Jika kami masih menggunakan teknik visualisasi dari tutorial sebelumnya, angkanya akan terlihat seperti ini:


Bayangan sederhana


Untuk membuat bayangan yang tajam, kita berkeliling ruang dari posisi sampel ke posisi sumber cahaya. Jika kita menemukan objek di jalan, kita memutuskan bahwa pixel harus diarsir, dan jika kita sampai ke sumber tanpa hambatan, kita mengatakan bahwa itu tidak diarsir.

Kami mulai dengan menghitung parameter dasar balok. Kami sudah memiliki titik awal (posisi piksel yang kami render) dan titik target (posisi sumber cahaya) untuk balok. Kami membutuhkan panjang dan arah yang dinormalisasi. Arah dapat diperoleh dengan mengurangi awal dari akhir dan menormalkan hasilnya. Panjangnya dapat diperoleh dengan mengurangi posisi dan meneruskan nilai ke metode length .

 float traceShadow(float2 position, float2 lightPosition){ float direction = normalise(lightPosition - position); float distance = length(lightPosition - position); } 

Kemudian kita mengulangi ray dalam loop. Kami akan mengatur iterasi loop dalam deklarasi define, dan ini akan memungkinkan kami untuk mengkonfigurasi jumlah iterasi maksimum nanti, dan juga memungkinkan kompiler untuk sedikit mengoptimalkan shader dengan memperluas loop.

Dalam loop, kita membutuhkan posisi kita sekarang, jadi kita mendeklarasikannya di luar loop dengan nilai awal 0. Dalam loop, kita dapat menghitung posisi sampel dengan menambahkan balok maju dikalikan dengan arah balok dengan posisi dasar. Kemudian kita mencicipi fungsi jarak yang ditandatangani di posisi yang baru saja dihitung.

 // outside of function #define SAMPLES 32 // in shadow function float rayDistance = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(pos + direction * rayDistance); //do other stuff and move the ray further } 

Kemudian kita periksa untuk melihat apakah kita sudah pada titik di mana kita dapat menghentikan siklus. Jika jarak adegan fungsi jarak dengan tanda dekat dengan 1, maka kita dapat mengasumsikan bahwa balok diblokir oleh gambar dan mengembalikan 0. Jika balok menyebar lebih jauh dari jarak ke sumber cahaya, kita dapat mengasumsikan bahwa kita mencapai sumber tanpa tabrakan dan mengembalikan nilai 1.

Jika pengembalian tidak berhasil, maka Anda perlu menghitung posisi sampel selanjutnya. Ini dilakukan dengan menambahkan jarak dalam adegan kemajuan balok. Alasan untuk ini adalah bahwa jarak dalam adegan memberi kita jarak ke gambar terdekat, jadi jika kita menambahkan nilai ini ke balok, kita mungkin tidak akan bisa memancarkan balok lebih jauh dari angka terdekat, atau bahkan di luar itu, yang akan mengarah pada aliran bayangan.

Jika kami tidak menemukan apa pun dan tidak mencapai sumber cahaya pada saat stok sampel selesai (siklus berakhir), kami juga harus mengembalikan nilainya. Karena ini terutama terjadi di sebelah bentuk, sesaat sebelum piksel masih dianggap teduh, di sini kami menggunakan nilai balik 0.

 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return 1; } rayProgress = rayProgress + sceneDist; } return 0; } 

Untuk menggunakan fungsi ini, kami menyebutnya dalam fungsi fragmen dengan posisi piksel dan posisi sumber cahaya. Lalu kami mengalikan hasilnya dengan warna apa saja untuk mencampurnya dengan warna sumber cahaya.

Saya juga menggunakan teknik yang dijelaskan dalam tutorial pertama tentang bidang jarak dengan tanda untuk memvisualisasikan geometri. Lalu saya baru saja menambahkan lipatan dan geometri. Di sini kita cukup menggunakan operasi penjumlahan, dan tidak melakukan interpolasi linier atau tindakan serupa, karena bentuknya hitam di mana-mana di mana bentuknya tidak, dan bayangannya hitam di mana pun bentuknya.

fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz;

 float2 lightPos; sincos(_Time.y, lightPos.x /*sine of time*/, lightPos.y /*cosine of time*/); float shadows = traceShadows(position, lightPos); float3 light = shadows * float3(.6, .6, 1); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light; return float4(col, 1); } 


Bayangan lembut


Mulai dari bayangan yang keras ini menjadi lebih lembut dan lebih realistis cukup mudah. Dalam hal ini, shader tidak menjadi mahal secara komputasi.

Pertama, kita cukup mendapatkan jarak ke objek pemandangan terdekat untuk setiap sampel yang kita bypass, dan pilih yang terdekat. Lalu tempat kami dulu mengembalikan 1, akan mungkin untuk mengembalikan jarak ke angka terdekat. Agar kecerahan bayangan tidak terlalu tinggi dan tidak mengarah pada penciptaan warna aneh, kami akan meneruskannya melalui metode saturate , yang membatasi ke interval dari 0 hingga 1. Kami mendapatkan minimum antara gambar terdekat saat ini dan yang berikutnya setelah memeriksa apakah berkas sumber cahaya telah mencapai distribusi. jika tidak, kita dapat mengambil sampel yang melampaui sumber cahaya dan mendapatkan artefak aneh.

 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 


Hal pertama yang kita perhatikan setelah ini adalah "gigi" aneh di bayang-bayang. Mereka muncul karena jarak dari tempat kejadian ke sumber cahaya kurang dari 1. Saya mencoba untuk menangkal ini dengan berbagai cara, tetapi tidak dapat menemukan solusi. Sebaliknya, kita bisa menerapkan ketajaman bayangan. Ketajaman akan menjadi parameter lain dalam fungsi bayangan. Dalam lingkaran, kita mengalikan jarak dalam adegan dengan ketajaman, dan kemudian dengan ketajaman 2, bagian abu-abu yang lembut dan abu-abu dari bayangan akan menjadi setengahnya. Saat menggunakan ketajaman, sumber cahaya dapat berasal dari gambar pada jarak minimal 1 dibagi dengan ketajaman, jika tidak, artefak akan muncul. Karena itu, jika Anda menggunakan ketajaman 20, maka jaraknya harus setidaknya 0,05 unit.

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, hardness * sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 

 //in fragment function float shadows = traceShadows(position, lightPos, 20); 


Dengan meminimalkan masalah ini, kami perhatikan hal berikut: bahkan di area yang tidak boleh diarsir, pelemahan masih terlihat di dekat dinding. Selain itu, kelembutan bayangan tampaknya sama untuk seluruh bayangan, dan tidak tajam di sebelah gambar dan lebih lembut ketika bergerak menjauh dari objek yang memancarkan bayangan.

Kami akan memperbaikinya dengan membagi jarak dalam adegan dengan perambatan balok. Berkat ini, kami akan membagi jarak menjadi angka yang sangat kecil di awal balok, yaitu, kami masih akan mendapatkan nilai tinggi dan bayangan jernih yang indah. Ketika kita menemukan titik terdekat dengan sinar pada titik berikutnya dalam sinar, titik terdekat dibagi dengan jumlah yang lebih besar, yang membuat bayangan lebih lembut. Karena ini tidak sepenuhnya terkait dengan jarak terpendek, kami akan mengganti nama variabel menjadi shadow .

Kami juga akan membuat satu perubahan kecil lagi: karena kami membaginya dengan rayProgress, Anda tidak boleh memulai dengan 0 (membaginya dengan nol hampir selalu merupakan ide yang buruk). Sebagai permulaan, Anda dapat memilih angka yang sangat kecil.

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + sceneDist; } return 0; } 


Berbagai Sumber Penerangan


Dalam implementasi single-core yang sederhana ini, cara termudah untuk mendapatkan banyak sumber cahaya adalah dengan menghitungnya secara individual dan menambahkan hasilnya.

 fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } 


Kode sumber


Pustaka SDF dua dimensi (tidak berubah, tetapi digunakan di sini)



Bayangan lembut dua dimensi



 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } #define STARTDISTANCE 0.00001 #define MINSTEPDIST 0.02 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float lightSceneDistance = scene(lightPosition) * 0.8; float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + max(sceneDist, 0.02); } return 0; } fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } ENDCG } } FallBack "Standard" } 

Ini hanyalah salah satu dari banyak contoh penggunaan bidang jarak yang ditandatangani. Sejauh ini mereka agak rumit, karena semua bentuk harus didaftarkan di shader atau melewati properti shader, tapi saya punya beberapa ide tentang cara membuatnya lebih nyaman untuk tutorial di masa depan.

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


All Articles