Rekayasa terbalik dari rendering The Witcher 3: berbagai efek langit

gambar

[Bagian sebelumnya dari analisis: pertama dan kedua dan ketiga .]

Bagian 1. Awan Cirrus


Ketika permainan berlangsung di ruang terbuka, salah satu faktor yang menentukan kredibilitas dunia adalah langit. Pikirkan tentang hal ini - sebagian besar waktu langit secara harfiah membutuhkan sekitar 40-50% dari seluruh layar. Langit lebih dari sekadar gradien yang indah. Ia memiliki bintang, matahari, bulan, dan akhirnya awan.

Meskipun tren saat ini tampaknya terdiri dalam rendering volumetrik awan menggunakan raymarching (lihat artikel ini ), awan di The Witcher 3 sepenuhnya berbasis tekstur. Saya sudah memeriksa mereka sebelumnya, tetapi ternyata dengan mereka semuanya lebih rumit dari yang saya harapkan. Jika Anda mengikuti serangkaian artikel saya, maka Anda tahu bahwa ada perbedaan antara Darah dan Anggur DLC dan sisa permainan. Dan, seperti yang Anda duga, ada beberapa perubahan dalam pekerjaan dengan cloud di DLC.

The Witcher 3 memiliki beberapa lapisan awan. Tergantung pada cuaca, itu hanya awan cirrus , awan kumulus tinggi , mungkin beberapa awan dari keluarga awan berlapis (misalnya, saat badai). Pada akhirnya, mungkin tidak ada awan sama sekali.

Beberapa lapisan berbeda dalam hal tekstur dan shader yang digunakan untuk membuatnya. Jelas, ini mempengaruhi kompleksitas dan panjang kode assembler untuk shader piksel.

Terlepas dari semua keragaman ini, ada beberapa pola umum yang dapat diamati ketika merender awan di Witcher 3. Pertama, mereka semua memberikan pass proaktif, dan ini adalah pilihan yang sempurna. Semuanya menggunakan pencampuran (lihat di bawah). Ini membuatnya lebih mudah untuk mengontrol bagaimana lapisan terpisah menutupi langit - ini dipengaruhi oleh nilai alpha dari pixel shader.


Lebih menarik lagi, beberapa layer dirender dua kali dengan parameter yang sama.

Setelah melihat kode, saya memilih shader terpendek untuk (1) kemungkinan melakukan full reverse engineering, (2) mencari tahu semua aspeknya.

Saya melihat lebih dekat pada awan cirrus dari Witcher 3: Blood and Wine.

Berikut ini contoh bingkai:


Sebelum rendering


Setelah render pass pertama


Setelah render pass kedua

Dalam bingkai khusus ini, awan cirrus adalah lapisan pertama dalam rendering. Seperti yang Anda lihat, itu diberikan dua kali, yang meningkatkan kecerahannya.

Geometris dan vertex shader


Sebelum pixel shader, kita akan membahas secara singkat tentang geometri dan vertex shaders yang digunakan. Jala untuk menampilkan awan sedikit seperti kubah langit biasa:


Semua simpul berada dalam interval [0-1], oleh karena itu, untuk memusatkan mesh pada titik (0,0,0), penskalaan dan deviasi digunakan sebelum mengkonversi ke worldViewProj (kita sudah tahu pola ini dari bagian seri sebelumnya). Dalam kasus awan, jala membentang kuat di sepanjang bidang XY (sumbu Z menunjuk ke atas) untuk menutupi lebih banyak ruang daripada piramida visibilitas. Hasilnya adalah sebagai berikut:


Selain itu, mesh memiliki vektor normal dan garis singgung. Vertex shader juga menghitung vektor bi-tangent oleh produk vektor - ketiganya ditampilkan dalam bentuk normal. Ada juga perhitungan atas kabut (warna dan kecerahannya).

Pixel shader


Kode rakitan pixel shader terlihat seperti ini:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb12[238], immediateIndexed  
    dcl_constantbuffer cb4[13], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.w  
    dcl_input_ps linear v3.xyzw  
    dcl_input_ps linear v4.xyz  
    dcl_input_ps linear v5.xyz  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  
   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx  
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx  
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   8: add r1.xyz, r1.xyzx, r1.xyzx  
   9: dp3 r0.w, r1.xyzx, r1.xyzx  
  10: rsq r0.w, r0.w  
  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  
  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x  
  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x  
  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x  
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x  
  53: mul r0.w, r0.w, r1.x  
  54: mul_sat r0.w, r0.w, cb4[6].x  
  55: mul o0.xyz, r0.wwww, r0.xyzx  
  56: mov o0.w, r0.w  
  57: ret 

Dua tekstur mulus adalah input. Salah satunya berisi peta normal (saluran xyz ) dan bentuk awan (saluran a ). Yang kedua adalah noise untuk mengubah bentuk.


Peta Normal, CD Projekt Red Property


Cloud Shape, Projekt Properti CD Merah


Tekstur kebisingan, properti CD Projekt Red

Buffer utama konstanta dengan parameter cloud adalah cb4. Untuk bingkai ini, ia memiliki arti sebagai berikut:


Selain itu, nilai-nilai lain dari cbuffer lain digunakan. Jangan khawatir, kami akan mempertimbangkannya juga.

Z-direction membalikkan sinar matahari


Hal pertama yang terjadi pada shader adalah perhitungan arah sinar matahari yang dinormalisasi terbalik di sepanjang sumbu Z:

   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  

   float3 invertedSunlightDir = normalize(lightDir * float3(1, 1, -1) );

Seperti disebutkan sebelumnya, sumbu Z diarahkan ke atas, dan cb0 [9] adalah arah sinar matahari. Vektor ini ditujukan untuk matahari - penting! Anda dapat memverifikasi ini dengan menulis shader komputasi sederhana yang menjalankan NdotL sederhana, dan memasukkannya ke dalam pass shader yang ditangguhkan.

Pengambilan Sampel Tekstur Awan


Langkah selanjutnya adalah menghitung texcoords untuk mencicipi tekstur cloud, membongkar vektor normal dan menormalkannya.

   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx   
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx   
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0   
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)   
   8: add r1.xyz, r1.xyzx, r1.xyzx   
   9: dp3 r0.w, r1.xyzx, r1.xyzx   
  10: rsq r0.w, r0.w   
   
   
   // Calc sampling coords  
   float2 cloudTextureUV = Texcoords * textureScale + elapsedTime * speedFactors;  
   
   // Sample texture and get data from it  
   float4 cloudTextureValue = texture0.Sample( sampler0, cloudTextureUV ).rgba;  
   float3 normalMap = cloudTextureValue.xyz;  
   float cloudShape = cloudTextureValue.a;  
   
   // Unpack normal and normalize it  
   float3 unpackedNormal = (normalMap - 0.5) * 2.0;  
   unpackedNormal = normalize(unpackedNormal);

Mari kita hadapi secara bertahap.

Untuk mendapatkan pergerakan awan, kita perlu waktu yang berlalu dalam detik ( cb [0] .x ) dikalikan dengan koefisien kecepatan, yang mempengaruhi seberapa cepat awan bergerak melintasi langit ( cb4 [5] .xy ).

Seperti yang saya katakan sebelumnya, UV direntangkan di sepanjang geometri kubah langit, dan kita juga membutuhkan faktor penskalaan tekstur yang memengaruhi ukuran awan ( cb4 [4] .xy ).

Formula terakhir adalah:

samplingUV = Input.TextureUV * textureScale + time * speedMultiplier;

Setelah mengambil sampel semua 4 saluran, kami memiliki peta normal (saluran rgb) dan bentuk awan (saluran a).

Untuk membuka kemasan peta normal dari interval [0; 1] dalam interval [-1; 1] kami menggunakan rumus berikut:

unpackedNormal = (packedNormal - 0.5) * 2.0;

Anda juga dapat menggunakan ini:

unpackedNormal = packedNormal * 2.0 - 1.0;

Akhirnya, kami menormalkan vektor normal yang sudah dibongkar.

Overlay normals


Dengan vektor normal, vektor tangen dan bi-tangen dari vertex shader, dan vektor normal dari peta normal, kami biasanya memetakan normalnya.

  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
    
   // Perform bump mapping  
   float3 SkyTangent = Input.Tangent;  
   float3 SkyNormal = (float3( Input.Texcoords.zw, Input.param3.w ));  
   float3 SkyBitangent = Input.param3.xyz;  
        
   float3x3 TBN = float3x3(SkyTangent, SkyBitangent, SkyNormal);  
   float3 finalNormal = (float3)mul( unpackedNormal, (TBN) );

Kecerahan (1)


Pada langkah berikutnya, perhitungan NdotL diterapkan dan ini memengaruhi jumlah penerangan piksel tertentu.

Pertimbangkan kode assembler berikut:

  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  

Berikut ini visualisasi NdotL pada frame yang dimaksud:


Produk skalar ini (dengan saturasi) digunakan untuk menginterpolasi antara minIntensity dan maxIntensity. Berkat ini, bagian dari awan yang diterangi oleh sinar matahari akan menjadi lebih cerah.

   // Calculate cosine between normal and up-inv lightdir  
   float NdotL = saturate( dot(invertedSunlightDir, finalNormal) );  
   
   // Param 1, line 19, r0.x  
   float intensity1 = lerp( param1Min, param1Max, NdotL );

Kecerahan (2)


Ada faktor lain yang mempengaruhi kecerahan awan.

Awan yang terletak di bagian langit tempat matahari berada, harus lebih disorot. Untuk melakukan ini, kami menghitung gradien berdasarkan bidang XY.

Gradien ini digunakan untuk menghitung interpolasi linier antara nilai min / maks, mirip dengan apa yang terjadi pada bagian (1).

Artinya, secara teoritis, kita dapat meminta untuk menggelapkan awan yang terletak di sisi berlawanan dari matahari, tetapi ini tidak terjadi dalam kerangka khusus ini, karena param2Min dan param2Max ( cb4 [0] .x dan cb4 [1] .x ) diatur ke 1.0f.

  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x   
   
   
   // Calculate normalized -lightDir.xy (20-22)  
   float2 lightDirXY = normalize( -lightDir.xy );  
   
   // Calculate world to camera  
   float3 vWorldToCamera = ( CameraPos - WorldPos );  
   float worldToCamera_distance = length(vWorldToCamera);  
        
   // normalize vector  
   vWorldToCamera = normalize( vWorldToCamera );  
        
   
   float LdotV = saturate( dot(lightDirXY, vWorldToCamera.xy) );  
   float highlightedSkySection = saturate( 2*LdotV );  
   float intensity2 = lerp( param2Min, param2Max, highlightedSkySection );  
   
   float finalIntensity = pow( intensity2 *intensity1, 2.2);

Pada akhirnya, kami melipatgandakan kecerahan dan menaikkan hasilnya menjadi kekuatan 2.2.

Warna awan


Perhitungan warna awan dimulai dengan memperoleh dari konstanta buffer dua nilai yang menunjukkan warna awan di sebelah matahari dan awan di sisi yang berlawanan dari langit. Di antara mereka, interpolasi linier dilakukan berdasarkan highlightedSkySection .

Kemudian hasilnya dikalikan dengan finalIntensity .

Dan pada akhirnya, hasilnya dicampur dengan kabut (untuk alasan kinerja, dihitung oleh vertex shader).

  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
   
  float3 cloudsColor = lerp( cloudsColorBack, cloudsColorFront, highlightedSunSection );  
  cloudsColor *= finalIntensity;  
  cloudsColor = lerp( cloudsColor, FogColor, FogAmount );

Buat awan cirrus lebih terlihat di cakrawala


Ini tidak terlalu terlihat pada bingkai, tetapi sebenarnya lapisan ini lebih terlihat dekat cakrawala daripada di atas kepala Geralt. Begini cara melakukannya.

Anda dapat melihat bahwa ketika menghitung kecerahan kedua, kami menghitung panjang vektor worldToCamera :

  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w

Mari kita temukan kemunculan panjang ini dalam kode berikut:

  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  ...  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x

Wow, ada apa dengan kita?

cb [7] .x dan cb [8] .x memiliki nilai 2000.0 dan 7000.0.

Ternyata ini adalah hasil dari menggunakan fungsi linstep .

Dia menerima tiga parameter: min / maks - interval dan v - nilai.

Ini berfungsi sebagai berikut: jika v berada dalam interval [ min - max ], maka fungsi mengembalikan interpolasi linier dalam interval [0,0 - 1.0]. Di sisi lain, jika v berada di luar jangkauan, maka linstep mengembalikan 0,0 atau 1,0.

Contoh sederhana:

linstep( 1000.0, 2000.0, 999.0) = 0.0
linstep( 1000.0, 2000.0, 1500.0) = 0.5
linstep( 1000.0, 2000.0, 2000.0) = 1.0

Artinya, sangat mirip dengan langkah mulus dari HLSL, kecuali bahwa dalam kasus ini, alih-alih interpolasi Hermitian, linier dilakukan.

Linstep bukan fitur di HLSL, tetapi sangat berguna. Perlu memilikinya di toolkit Anda.

 // linstep:  
 //  
 // Returns a linear interpolation between 0 and 1 if t is in the range [min, max]   
 // if "v" is <= min, the output is 0  
 // if "v" i >= max, the output is 1  
   
 float linstep( float min, float max, float v )  
 {  
   return saturate( (v - min) / (max - min) );  
 } 

Mari kita kembali ke Witcher 3: setelah menghitung indikator ini, melaporkan seberapa jauh bagian tertentu langit dari Geralt, kita menggunakannya untuk melemahkan kecerahan awan:

  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
   
   float distanceAttenuation = linstep( fadeDistanceStart, fadeDistanceEnd, worldToCamera_distance );  
    
   float fadedCloudShape = closeCloudsHidingFactor * cloudShape;  
   cloudShape = lerp( fadedCloudShape, cloudShape, distanceAttenuation );

cloudShape adalah saluran .a dari tekstur pertama, dan closeCloudsHidingFactor adalah nilai buffer konstan yang mengontrol visibilitas awan di atas kepala Geralt. Di semua frame yang saya uji, itu sama dengan 0,0, yang setara dengan tidak adanya awan. Saat distanceAttenuation mendekati 1.0 (jarak dari kamera ke kubah langit meningkat), awan menjadi lebih terlihat.

Pengambilan sampel tekstur suara


Perhitungan koordinat sampling noise tekstur perhitungan yang sama untuk tekstur awan, kecuali bahwa Anda menggunakan serangkaian tekstur yang berbeda Skala dan kecepatan .

Tentu saja, sampler dengan mode pengalamatan bungkus diaktifkan digunakan untuk sampel semua tekstur ini .

  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
   
   // Calc sampling coords for noise  
   float2 noiseTextureUV = Texcoords * textureScaleNoise + elapsedTime * speedFactorsNoise;  
   
   // Sample texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;

Menyatukan Semuanya


Setelah menerima nilai noise, kita harus menggabungkannya dengan cloudShape.

Saya punya beberapa masalah memahami garis-garis ini, di mana ada param2.w (yang selalu 1.0) dan noiseMult (memiliki nilai 5.0, diambil dari buffer konstan).

Namun, hal yang paling penting di sini adalah nilai akhir dari generalCloudsVisibility , yang memengaruhi visibilitas awan.

Lihatlah juga nilai akhir dari kebisingan. Warna keluaran dari cloudColor dikalikan dengan derau akhir, yang juga merupakan keluaran ke saluran alfa.

  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x
  53: mul r0.w, r0.w, r1.x
  54: mul_sat r0.w, r0.w, cb4[6].x
  55: mul o0.xyz, r0.wwww, r0.xyzx
  56: mov o0.w, r0.w
  57: ret   

   // Sample noise texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;  
   noiseTextureValue = noiseTextureValue * noiseMult - noiseMult;  
     
   float noiseValue = saturate( noiseMult * Input.param2.w + noiseTextureValue);  
   noiseValue *= cloudShape;  
     
   float finalNoise = saturate( noiseValue * generalCloudsVisibility);  
   
   return float4( cloudsColor*finalNoise, finalNoise ); 

Total


Hasil akhir terlihat sangat dipercaya.

Anda bisa membandingkan. Gambar pertama adalah shader saya, yang kedua adalah game shader:


Jika Anda penasaran, shader tersedia di sini .

Bagian 2. Kabut


Kabut dapat diimplementasikan dengan berbagai cara. Namun, saat-saat ketika kita bisa menerapkan kabut tergantung jarak yang sederhana dan menyingkirkannya selamanya di masa lalu (kemungkinan besar). Hidup di dunia shader yang dapat diprogram telah membuka pintu bagi solusi baru yang gila, tetapi yang lebih penting, akurat secara fisik dan realistis secara visual.

Tren saat ini dalam rendering kabut didasarkan pada bayangan komputasi (untuk detail, lihat presentasi ini oleh Bart Wronsky).

Terlepas dari kenyataan bahwa presentasi ini muncul pada 2014, dan The Witcher 3 dirilis pada 2015/2016, kabut di bagian terakhir dari petualangan Geralt sepenuhnya tergantung pada layar dan diimplementasikan sebagai tipikal pemrosesan pasca.

Sebelum kita memulai sesi reverse engineering berikutnya, saya harus mengatakan bahwa selama setahun terakhir saya mencoba mencari tahu kabut Witcher 3 setidaknya lima kali, dan setiap kali gagal. Kode assembler, seperti yang akan segera Anda lihat, cukup rumit, dan ini membuat proses pembuatan kabut kabut yang dapat dibaca di HLSL hampir tidak mungkin.

Namun, saya berhasil menemukan kabut shader di Internet yang segera menarik perhatian saya karena kemiripannya dengan kabut The Witcher 3 dalam hal nama variabel dan urutan instruksi umum. Shader ini tidak persis sama dengan di game, jadi saya harus mengolahnya sedikit. Saya ingin mengatakan ini bahwa bagian utama dari kode HLSL yang Anda lihat di sini adalah, dengan dua pengecualian, tidak dibuat / dianalisis oleh saya. Ingat ini.

Berikut adalah kode assembler untuk pixel fog shader - perlu dicatat bahwa itu sama untuk seluruh game (bagian utama tahun 2015 dan kedua DLC):

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[2], immediateIndexed  
    dcl_constantbuffer cb12[214], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_resource_texture2d (float,float,float,float) t2  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 7  
   0: ftou r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r1.x, r0.xyww, t0.xyzw  
   3: mad r1.y, r1.x, cb12[22].x, cb12[22].y  
   4: lt r1.y, r1.y, l(1.000000)  
   5: if_nz r1.y  
   6:  utof r1.yz, r0.xxyx  
   7:  mul r2.xyzw, r1.zzzz, cb12[211].xyzw  
   8:  mad r2.xyzw, cb12[210].xyzw, r1.yyyy, r2.xyzw  
   9:  mad r1.xyzw, cb12[212].xyzw, r1.xxxx, r2.xyzw  
  10:  add r1.xyzw, r1.xyzw, cb12[213].xyzw  
  11:  div r1.xyz, r1.xyzx, r1.wwww  
  12:  ld_indexable(texture2d)(float,float,float,float) r2.xyz, r0.xyww, t1.xyzw  
  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)  
  24:  div r0.yzw, r0.yyzw, r1.xxxx  
  25:  mad r1.y, r0.w, cb12[22].z, cb12[0].z  
  26:  add r1.x, r1.x, -cb12[22].z  
  27:  max r1.x, r1.x, l(0)  
  28:  min r1.x, r1.x, cb12[42].z  
  29:  mul r1.z, r0.w, r1.x  
  30:  mul r1.w, r1.x, cb12[43].x  
  31:  mul r1.zw, r1.zzzw, l(0.000000, 0.000000, 0.062500, 0.062500)  
  32:  dp3 r0.y, cb12[38].xyzx, r0.yzwy  
  33:  add r0.z, r0.y, cb12[42].x  
  34:  add r0.w, cb12[42].x, l(1.000000)  
  35:  div_sat r0.z, r0.z, r0.w  
  36:  add r0.w, -cb12[43].z, cb12[43].y  
  37:  mad r0.z, r0.z, r0.w, cb12[43].z  
  38:  mul r0.w, abs(r0.y), abs(r0.y)  
  39:  mad_sat r2.w, r1.x, l(0.002000), l(-0.300000)  
  40:  mul r0.w, r0.w, r2.w  
  41:  lt r0.y, l(0), r0.y  
  42:  movc r3.xyz, r0.yyyy, cb12[39].xyzx, cb12[41].xyzx  
  43:  add r3.xyz, r3.xyzx, -cb12[40].xyzx  
  44:  mad r3.xyz, r0.wwww, r3.xyzx, cb12[40].xyzx  
  45:  movc r4.xyz, r0.yyyy, cb12[45].xyzx, cb12[47].xyzx  
  46:  add r4.xyz, r4.xyzx, -cb12[46].xyzx  
  47:  mad r4.xyz, r0.wwww, r4.xyzx, cb12[46].xyzx  
  48:  ge r0.y, r1.x, cb12[48].y  
  49:  if_nz r0.y  
  50:   add r0.y, r1.y, cb12[42].y  
  51:   mul r0.w, r0.z, r0.y  
  52:   mul r1.y, r0.z, r1.z  
  53:   mad r5.xyzw, r1.yyyy, l(16.000000, 15.000000, 14.000000, 13.000000), r0.wwww  
  54:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  55:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  56:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  57:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  58:   mul r1.z, r5.y, r5.x  
  59:   mul r1.z, r5.z, r1.z  
  60:   mul r1.z, r5.w, r1.z  
  61:   mad r5.xyzw, r1.yyyy, l(12.000000, 11.000000, 10.000000, 9.000000), r0.wwww  
  62:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  63:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  64:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  65:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:   mul r1.z, r1.z, r5.x  
  67:   mul r1.z, r5.y, r1.z  
  68:   mul r1.z, r5.z, r1.z  
  69:   mul r1.z, r5.w, r1.z  
  70:   mad r5.xyzw, r1.yyyy, l(8.000000, 7.000000, 6.000000, 5.000000), r0.wwww  
  71:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  72:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  73:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  74:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  75:   mul r1.z, r1.z, r5.x  
  76:   mul r1.z, r5.y, r1.z  
  77:   mul r1.z, r5.z, r1.z  
  78:   mul r1.z, r5.w, r1.z  
  79:   mad r5.xy, r1.yyyy, l(4.000000, 3.000000, 0.000000, 0.000000), r0.wwww  
  80:   max r5.xy, r5.xyxx, l(0, 0, 0, 0)  
  81:   add r5.xy, r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  82:   div_sat r5.xy, r1.wwww, r5.xyxx  
  83:   add r5.xy, -r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  84:   mul r1.z, r1.z, r5.x  
  85:   mul r1.z, r5.y, r1.z  
  86:   mad r0.w, r1.y, l(2.000000), r0.w  
  87:   max r0.w, r0.w, l(0)  
  88:   add r0.w, r0.w, l(1.000000)  
  89:   div_sat r0.w, r1.w, r0.w  
  90:   add r0.w, -r0.w, l(1.000000)  
  91:   mul r0.w, r0.w, r1.z  
  92:   mad r0.y, r0.y, r0.z, r1.y  
  93:   max r0.y, r0.y, l(0)  
  94:   add r0.y, r0.y, l(1.000000)  
  95:   div_sat r0.y, r1.w, r0.y  
  96:   add r0.y, -r0.y, l(1.000000)  
  97:   mad r0.y, -r0.w, r0.y, l(1.000000)  
  98:   add r0.z, r1.x, -cb12[48].y  
  99:   mul_sat r0.z, r0.z, cb12[48].z  
  100:  else  
  101:   mov r0.yz, l(0.000000, 1.000000, 0.000000, 0.000000)  
  102:  endif  
  103:  log r0.y, r0.y  
  104:  mul r0.w, r0.y, cb12[42].w  
  105:  exp r0.w, r0.w  
  106:  mul r0.y, r0.y, cb12[48].x  
  107:  exp r0.y, r0.y  
  108:  mul r0.yw, r0.yyyw, r0.zzzz  
  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, cb12[188].w, l(-1.000000)  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.z, r0.w  
  115:  lt r0.z, l(0), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, cb12[190].w, l(-1.000000)  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.z, r0.w  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif  
  126:  mul r0.z, r0.x, r5.w  
  127:  mul r0.x, r0.x, r0.y  
  128:  dp3 r0.y, l(0.333000, 0.555000, 0.222000, 0.000000), r2.xyzx  
  129:  mad r1.xyz, r0.yyyy, r4.xyzx, -r2.xyzx  
  130:  mad r0.xyw, r0.xxxx, r1.xyxz, r2.xyxz  
  131:  add r1.xyz, -r0.xywx, r5.xyzx  
  132:  mad r0.xyz, r0.zzzz, r1.xyzx, r0.xywx  
  133: else  
  134:  mov r0.xyz, l(0, 0, 0, 0)  
  135: endif  
  136: mov o0.xyz, r0.xyzx  
  137: mov o0.w, l(1.000000)  
  138: ret 

Jujur saja, shadernya cukup panjang. Mungkin terlalu lama untuk proses reverse engineering yang efektif.

Berikut adalah contoh pemandangan matahari terbenam dengan kabut:


Mari kita lihat input:

Adapun tekstur, kami memiliki buffer kedalaman, Oklusi Ambient, dan buffer warna HDR.


Buffer kedalaman masuk


Oklusi ambien masuk


Buffer warna HDR yang masuk

... dan hasil penerapan fog shader dalam adegan ini terlihat seperti ini:


Tekstur HDR setelah mengaplikasikan fog.

Depth buffer digunakan untuk menciptakan kembali posisi di dunia. Ini adalah pola standar untuk shitch Witcher 3.

Memiliki data oklusi ambien (jika diaktifkan) memungkinkan kita untuk mengaburkan kabut. Gagasan yang sangat cerdas, mungkin yang jelas, tetapi saya tidak pernah memikirkannya seperti itu. Saya akan kembali ke aspek ini nanti.

Shader dimulai dengan menentukan apakah suatu piksel ada di langit. Jika pixel terletak di langit (kedalaman == 1.0), shader mengembalikan warna hitam. Jika piksel ada di tempat kejadian (kedalaman <1.0), maka kami membuat ulang posisi di dunia menggunakan penyangga kedalaman (baris 7-11) dan terus menghitung kabut.

Bagian kabut terjadi segera setelah proses naungan tertunda. Anda mungkin memperhatikan bahwa beberapa elemen yang terkait dengan forward run belum tersedia. Dalam adegan khusus ini, volume pencahayaan yang ditangguhkan diterapkan, dan setelah itu kami memberikan rambut / wajah / mata Geralt.

Hal pertama yang perlu Anda ketahui tentang kabut di "The Witcher 3": terdiri dari dua bagian - "warna kabut" dan "warna atmosfer".

 struct FogResult  
 {  
    float4 paramsFog;     // RGB: color, A: influence  
    float4 paramsAerial;  // RGB: color, A: influence  
 };

Untuk setiap bagian ada tiga warna: depan, tengah dan belakang. Yaitu, di buffer konstan ada data seperti "FogColorFront", "FogColorMiddle", "AerialColorBack", dll. ... Mari kita lihat data yang masuk:


   // *** Inputs *** //  
   float3 FogSunDir = cb12_v38.xyz;  
   float3 FogColorFront = cb12_v39.xyz;  
   float3 FogColorMiddle = cb12_v40.xyz;  
   float3 FogColorBack = cb12_v41.xyz;  
     
   float4 FogBaseParams = cb12_v42;  
   float4 FogDensityParamsScene = cb12_v43;  
   float4 FogDensityParamsSky = cb12_v44;  
     
   float3 AerialColorFront = cb12_v45.xyz;  
   float3 AerialColorMiddle = cb12_v46.xyz;  
   float3 AerialColorBack = cb12_v47.xyz;  
   float4 AerialParams = cb12_v48;

Sebelum menghitung warna akhir, kita perlu menghitung vektor dan produk skalar. Shader memiliki akses ke posisi piksel di dunia, posisi kamera (cb12 [0] .xyz) dan arah kabut / pencahayaan (cb12 [38] .xyz). Ini memungkinkan kita untuk menghitung produk skalar dari vektor bentuk dan arah kabut.

   float3 frag_vec = fragPosWorldSpace.xyz - customCameraPos.xyz;  
   float frag_dist = length(frag_vec);  
     
   float3 frag_dir = frag_vec / frag_dist;  
   
   float dot_fragDirSunDir = dot(GlobalLightDirection.xyz, frag_dir);

Untuk menghitung gradien pencampuran, Anda perlu menggunakan kuadrat dari produk skalar absolut, dan kemudian kembali mengalikan hasilnya dengan beberapa parameter yang tergantung pada jarak:

   float3 curr_col_fog;  
   float3 curr_col_aerial;  
   {  
     float _dot = dot_fragDirSunDir;  
   
     float _dd = _dot;  
     {  
       const float _distOffset = -150;  
       const float _distRange = 500;  
       const float _mul = 1.0 / _distRange;  
       const float _bias = _distOffset * _mul;  
   
       _dd = abs(_dd);  
       _dd *= _dd;  
       _dd *= saturate( frag_dist * _mul + _bias );  
     }  
   
     curr_col_fog = lerp( FogColorMiddle.xyz, (_dot>0.0f ? FogColorFront.xyz : FogColorBack.xyz), _dd );  
     curr_col_aerial = lerp( AerialColorMiddle.xyz, (_dot>0.0f ? AerialColorFront.xyz : AerialColorBack.xyz), _dd );  
   }

Blok kode ini menjelaskan kepada kami dari mana 0,002 dan -0,300 ini berasal. Seperti yang dapat kita lihat, produk skalar antara vektor tampilan dan pencahayaan bertanggung jawab atas pilihan antara warna "depan" dan "belakang". Pintar!

Berikut adalah visualisasi dari gradien akhir yang dihasilkan (_dd).


Namun, menghitung efek atmosfer / kabut jauh lebih rumit. Seperti yang Anda lihat, kami memiliki lebih banyak pilihan daripada hanya warna rgb. Mereka termasuk, misalnya, kerapatan pemandangan. Kami menggunakan raymarching (16 langkah, dan inilah mengapa siklus dapat diperluas) untuk menentukan ukuran kabut dan faktor skala:

Memiliki vektor [kamera ---> dunia], kita dapat membagi semua komponennya menjadi 16 - ini akan menjadi satu langkah raymarching. Seperti yang kita lihat di bawah, hanya komponen .z (tinggi) ( curr_pos_z_step ) yang terlibat dalam perhitungan .

Baca lebih lanjut tentang kabut yang diterapkan oleh raymarching, misalnya, di sini .

   float fog_amount = 1;  
   float fog_amount_scale = 0;  
   [branch]  
   if ( frag_dist >= AerialParams.y )  
   {  
     float curr_pos_z_base = (customCameraPos.z + FogBaseParams.y) * density_factor;  
     float curr_pos_z_step = frag_step.z * density_factor;  
   
     [unroll]  
     for ( int i=16; i>0; --i )  
     {  
       fog_amount *= 1 - saturate( density_sample_scale / (1 + max( 0.0, curr_pos_z_base + (i) * curr_pos_z_step ) ) );  
     }  
   
     fog_amount = 1 - fog_amount;  
     fog_amount_scale = saturate( (frag_dist - AerialParams.y) * AerialParams.z );  
   }  
   
   FogResult ret;  
   
   ret.paramsFog = float4 ( curr_col_fog, fog_amount_scale * pow( abs(fog_amount), final_exp_fog ) );  
   ret.paramsAerial = float4 ( curr_col_aerial, fog_amount_scale * pow( abs(fog_amount), final_exp_aerial ) );

Jumlah kabut jelas tergantung pada ketinggian (komponen .z), pada akhirnya jumlah kabut dinaikkan ke tingkat kabut / atmosfer.

final_exp_fog dan final_exp_aerial diambil dari buffer konstan; mereka memungkinkan Anda untuk mengontrol bagaimana warna kabut dan atmosfer mempengaruhi dunia dengan meningkatnya ketinggian.

Fog override


Shader yang saya temukan tidak memiliki fragmen kode rakitan berikut:

  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, l(-1.000000), cb12[188].w  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.w, r0.z  
  115:  lt r0.z, l(0.000000), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, l(-1.000000), cb12[190].w  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.w, r0.z  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif

Menilai dari apa yang saya bisa mengerti, ini seperti mendefinisikan ulang warna dan efek kabut:

Sebagian besar waktu, hanya satu redefinisi yang dilakukan (cb12_v192.x adalah 0,0), tetapi dalam kasus khusus ini nilainya ~ 0,22, jadi kami melakukan override kedua.


 #ifdef OVERRIDE_FOG  
     
   // Override  
   float fog_influence = ret.paramsFog.w; // r0.w  
   
   float override1ColorScale = cb12_v189.x;  
   float override1ColorBias = cb12_v189.y;  
   float3 override1Color = cb12_v188.rgb;  
     
   float override1InfluenceScale = cb12_v189.z;  
   float override1InfluenceBias = cb12_v189.w;  
   float override1Influence = cb12_v188.w;  
     
   float override1ColorAmount = saturate(fog_influence * override1ColorScale + override1ColorBias);  
   float override1InfluenceAmount = saturate(fog_influence * override1InfluenceScale + override1InfluenceBias);    
     

   float4 paramsFogOverride;  
   paramsFogOverride.rgb = lerp(curr_col_fog, override1Color, override1ColorAmount ); // ***r5.xyz   
     
   float param1 = lerp(1.0, override1Influence, override1InfluenceAmount); // r0.x  
   paramsFogOverride.w = saturate(param1 * fog_influence ); // ** r5.w  
   
     
   const float extraFogOverride = cb12_v192.x;  
     
   [branch]   
   if (extraFogOverride > 0.0)  
   {  
     float override2ColorScale = cb12_v191.x;  
     float override2ColorBias = cb12_v191.y;  
     float3 override2Color = cb12_v190.rgb;  
     
     float override2InfluenceScale = cb12_v191.z;  
     float override2InfluenceBias = cb12_v191.w;  
     float override2Influence = cb12_v190.w;  
       
     float override2ColorAmount = saturate(fog_influence * override2ColorScale + override2ColorBias);  
     float override2InfluenceAmount = saturate(fog_influence * override2InfluenceScale + override2InfluenceBias);  
      

     float4 paramsFogOverride2;  
     paramsFogOverride2.rgb = lerp(curr_col_fog, override2Color, override2ColorAmount); // r3.xyz   
           
     float ov_param1 = lerp(1.0, override2Influence, override2InfluenceAmount); // r0.z  
     paramsFogOverride2.w = saturate(ov_param1 * fog_influence); // r3.w  
   
     paramsFogOverride = lerp(paramsFogOverride, paramsFogOverride2, extraFogOverride);  
   
   }  
   ret.paramsFog = paramsFogOverride;  
     
 #endif

Inilah harga akhir kami tanpa mendefinisikan ulang kabut (gambar pertama), dengan satu redefinisi (gambar kedua) dan redefinisi ganda (gambar ketiga, hasil akhir):




Regulasi oklusi ambien


Shader yang saya temukan juga tidak menggunakan oklusi ambient sama sekali. Mari kita lihat lagi tekstur AO dan kode yang menarik minat kita:


  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)

Mungkin pemandangan ini bukan contoh terbaik, karena kita tidak melihat detail di pulau yang jauh. Namun, mari kita lihat buffer konstan, yang digunakan untuk mengatur nilai oklusi ambien:


Kami mulai dengan memuat AO dari tekstur, lalu jalankan instruksi maks. Dalam adegan ini, cb3_v1.x sangat tinggi (0,96888), yang membuat AO sangat lemah.

Bagian selanjutnya dari kode menghitung jarak antara posisi kamera dan piksel di dunia.

Saya percaya bahwa kode kadang-kadang berbicara sendiri, jadi mari kita lihat HLSL, yang melakukan sebagian besar pengaturan ini:

 float AdjustAmbientOcclusion(in float inputAO, in float worldToCameraDistance)  
 {  
   // *** Inputs *** //  
   const float aoDistanceStart = cb3_v0.x;  
   const float aoDistanceEnd = cb3_v0.y;  
   const float aoStrengthStart = cb3_v0.z;  
   const float aoStrengthEnd = cb3_v0.w;  
      
   // * Adjust AO  
   float aoDistanceIntensity = linstep( aoDistanceStart, aoDistanceEnd, worldToCameraDistance );  
   float aoStrength = lerp(aoStrengthStart, aoStrengthEnd, aoDistanceIntensity);   
   float adjustedAO = lerp(1.0, inputAO, aoStrength);  
     
   return adjustedAO;   
 }

Jarak yang dihitung antara kamera dan dunia digunakan untuk fungsi garis depan. Kita sudah tahu fungsi ini, muncul di cloud cirrus shader.

Seperti yang Anda lihat, di buffer konstan kita memiliki nilai jarak mulai / akhir AO. Output dari linstep mempengaruhi kekuatan AO (dan juga dari cbuffer), dan kekuatan mempengaruhi output dari AO.

Contoh singkat: pixel jauh, misalnya, jaraknya 500.

linstep mengembalikan 1.0;
aoStrength sama dengan aoStrengthEnd;

Ini menghasilkan pengembalian AO, yang sekitar 77% (kekuatan akhir) dari nilai input.

AO yang masuk untuk fungsi ini sebelumnya dikenai operasi maks.

Menyatukan semuanya


Setelah menerima warna dan efek untuk warna kabut dan warna atmosfer, Anda akhirnya dapat menggabungkannya.

Kami mulai dengan menipiskan efek dengan AO yang dihasilkan:

   ...
   FogResult fog = CalculateFog( worldPos, CameraPosition, fogStart, ao, false );  
      
   // Apply AO to influence  
   fog.paramsFog.w *= ao;  
   fog.paramsAerial.w *= ao; 
      
   // Mix fog with scene color  
   outColor = ApplyFog(fog, colorHDR);

Semua keajaiban terjadi dalam fungsi ApplyFog :

 float3 ApplyFog(FogResult fog, float3 color)  
 {  
   const float3 LuminanceFactors = float3(0.333f, 0.555f, 0.222f);  
   
   float3 aerialColor = dot(LuminanceFactors, color) * fog.paramsAerial.xyz;  
   color = lerp(color, aerialColor, fog.paramsAerial.w);  
   color = lerp(color, fog.paramsFog.xyz, fog.paramsFog.w);  
    
   return color.xyz;  
 }

Pertama, kami menghitung luminositas piksel:


Lalu kita gandakan dengan warna atmosfer:


Lalu kami menggabungkan warna HDR dengan warna atmosfer:


Langkah terakhir adalah menggabungkan hasil antara dengan warna kabut:


Itu saja!

Beberapa tangkapan layar debugging



Efek atmosfer


Warna suasana


Efek kabut


Warna kabut


Adegan jadi tanpa kabut


Adegan siap pakai hanya dengan kabut


Adegan yang selesai hanyalah kabut utama


Adegan siap pakai lagi dengan semua kabut untuk kemudahan perbandingan

Total


Saya pikir Anda dapat memahami banyak hal di atas, jika Anda melihat shader, itu ada di sini .

Saya dapat mengatakan dengan senang hati bahwa shader ini persis sama dengan yang asli - itu membuat saya sangat bahagia.

Secara umum, hasil akhir sangat tergantung pada nilai yang diteruskan ke shader. Ini bukan solusi "magis" yang memberikan warna sempurna dalam output, itu membutuhkan banyak iterasi dan seniman untuk membuat hasil akhir terlihat layak. Saya pikir itu bisa menjadi proses yang panjang, tetapi setelah Anda menyelesaikannya, hasilnya akan sangat meyakinkan, seperti adegan matahari terbenam ini.

Witcher 3 Sky Shader juga menggunakan perhitungan kabut untuk membuat transisi warna yang halus di dekat cakrawala. Namun, set koefisien kepadatan yang berbeda dilewatkan ke shader langit.

Biarkan saya mengingatkan Anda - sebagian besar shader ini tidak dibuat / dianalisis oleh saya. Semua ucapan terima kasih harus dikirim ke CD PROJEKT RED. Mendukung mereka, mereka melakukan pekerjaan yang sangat baik.

Bagian 3. Bintang Menembak


Dalam The Witcher 3 ada detail kecil tapi penasaran - bintang jatuh. Menariknya, mereka tampaknya tidak berada di DLC Darah dan Anggur.

Dalam video Anda dapat melihat tampilannya:


Mari kita lihat bagaimana kita berhasil mendapatkan efek ini.

Seperti yang Anda lihat, tubuh bintang jatuh jauh lebih terang daripada ekornya. Ini adalah properti penting yang akan kita gunakan nanti.

Agenda kami cukup akrab: pertama saya akan menjelaskan properti umum, kemudian saya akan berbicara tentang topik yang berkaitan dengan geometri, dan pada akhirnya kita akan beralih ke pixel shader, di mana hal-hal paling menarik terjadi.

1. Gambaran umum


Jelaskan secara singkat apa yang terjadi.

Bintang jatuh diambil dalam lintasan proaktif, segera setelah kubah langit, langit dan bulan:



DrawIndexed (720) - kubah langit,
DrawIndexed (2160) - bola untuk langit / bulan,
DrawIndexed (36) - tidak relevan, terlihat seperti garis paralel dari oklusi matahari (?)
DrawIndexed (12) - bintang jatuh
DrawIndexedInstanced (1116, 1) - awan cirrus

Seperti awan cirrus , setiap bintang jatuh digambarkan dua kali berturut-turut.


Sebelum panggilan undian pertama


Hasil panggilan draw pertama


Hasil dari draw draw kedua

Sebagai tambahan, seperti pada banyak elemen dari preemptive pass dari game ini, keadaan pencampuran berikut digunakan:


2. Geometri


Dari sudut pandang geometri, hal pertama yang disebutkan adalah bahwa setiap bintang jatuh diwakili oleh segiempat tipis dengan texcoords: 4 simpul, 6 indeks. Ini adalah quad paling sederhana yang mungkin.


Perkiraan quad dari bintang jatuh.


Bahkan lebih dekat adalah perkiraan quad bintang jatuh. Anda dapat melihat tampilan kerangka gambar garis yang menunjukkan dua segitiga.

Tunggu sebentar , tapi ada DrawIndexed (12) ! Apakah ini berarti kita menggambar dua bintang jatuh secara bersamaan?

Iya.


Dalam bingkai ini, salah satu bintang jatuh sepenuhnya di luar piramida visibilitas.

Mari kita lihat kode assembler untuk shader vertex:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[193], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xyzw  
    dcl_input v2.xy  
    dcl_input v3.xy  
    dcl_input v4.xy  
    dcl_input v5.xyz  
    dcl_input v6.x  
    dcl_input v7.x  
    dcl_output o0.xyzw  
    dcl_output o1.xyzw  
    dcl_output o2.xy  
    dcl_output o3.xyzw  
    dcl_output_siv o4.xyzw, position  
    dcl_temps 5  
   0: mov r0.xyz, v0.xyzx  
   1: mov r0.w, l(1.000000)  
   2: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   3: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   4: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   5: add r0.x, v2.x, v2.y  
   6: add r0.y, -v2.y, v2.x  
   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx  
  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx  
  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx  
  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000)  
  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  
  30: add r0.xyz, r0.xyzx, -cb12[0].xyzx  
  31: dp3 r0.w, r0.xyzx, r0.xyzx  
  32: sqrt r0.w, r0.w  
  33: div r0.xyz, r0.xyzx, r0.wwww  
  34: add r0.w, r0.w, -cb12[22].z  
  35: max r0.w, r0.w, l(0)  
  36: min r0.w, r0.w, cb12[42].z  
  37: dp3 r0.x, cb12[38].xyzx, r0.xyzx  
  38: mul r0.y, abs(r0.x), abs(r0.x)  
  39: mad_sat r1.x, r0.w, l(0.002000), l(-0.300000)  
  40: mul r0.y, r0.y, r1.x  
  41: lt r1.x, l(0), r0.x  
  42: movc r1.yzw, r1.xxxx, cb12[39].xxyz, cb12[41].xxyz  
  43: add r1.yzw, r1.yyzw, -cb12[40].xxyz  
  44: mad r1.yzw, r0.yyyy, r1.yyzw, cb12[40].xxyz  
  45: movc r2.xyz, r1.xxxx, cb12[45].xyzx, cb12[47].xyzx  
  46: add r2.xyz, r2.xyzx, -cb12[46].xyzx  
  47: mad o0.xyz, r0.yyyy, r2.xyzx, cb12[46].xyzx  
  48: ge r0.y, r0.w, cb12[48].y  
  49: if_nz r0.y  
  50:  mad r0.y, r0.z, cb12[22].z, cb12[0].z  
  51:  mul r0.z, r0.w, r0.z  
  52:  mul r0.z, r0.z, l(0.062500)  
  53:  mul r1.x, r0.w, cb12[43].x  
  54:  mul r1.x, r1.x, l(0.062500)  
  55:  add r0.x, r0.x, cb12[42].x  
  56:  add r2.x, cb12[42].x, l(1.000000)  
  57:  div_sat r0.x, r0.x, r2.x  
  58:  add r2.x, -cb12[43].z, cb12[43].y  
  59:  mad r0.x, r0.x, r2.x, cb12[43].z  
  60:  add r0.y, r0.y, cb12[42].y  
  61:  mul r2.x, r0.x, r0.y  
  62:  mul r0.z, r0.x, r0.z  
  63:  mad r3.xyzw, r0.zzzz, l(16.000000, 15.000000, 14.000000, 13.000000), r2.xxxx  
  64:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  65:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  67:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  68:  mul r2.y, r3.y, r3.x  
  69:  mul r2.y, r3.z, r2.y  
  70:  mul r2.y, r3.w, r2.y  
  71:  mad r3.xyzw, r0.zzzz, l(12.000000, 11.000000, 10.000000, 9.000000), r2.xxxx  
  72:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  73:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  74:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  75:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  76:  mul r2.y, r2.y, r3.x  
  77:  mul r2.y, r3.y, r2.y  
  78:  mul r2.y, r3.z, r2.y  
  79:  mul r2.y, r3.w, r2.y  
  80:  mad r3.xyzw, r0.zzzz, l(8.000000, 7.000000, 6.000000, 5.000000), r2.xxxx  
  81:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  82:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  83:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  84:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  85:  mul r2.y, r2.y, r3.x  
  86:  mul r2.y, r3.y, r2.y  
  87:  mul r2.y, r3.z, r2.y  
  88:  mul r2.y, r3.w, r2.y  
  89:  mad r2.zw, r0.zzzz, l(0.000000, 0.000000, 4.000000, 3.000000), r2.xxxx  
  90:  max r2.zw, r2.zzzw, l(0, 0, 0, 0)  
  91:  add r2.zw, r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  92:  div_sat r2.zw, r1.xxxx, r2.zzzw  
  93:  add r2.zw, -r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  94:  mul r2.y, r2.z, r2.y  
  95:  mul r2.y, r2.w, r2.y  
  96:  mad r2.x, r0.z, l(2.000000), r2.x  
  97:  max r2.x, r2.x, l(0)  
  98:  add r2.x, r2.x, l(1.000000)  
  99:  div_sat r2.x, r1.x, r2.x  
  100:  add r2.x, -r2.x, l(1.000000)  
  101:  mul r2.x, r2.x, r2.y  
  102:  mad r0.x, r0.y, r0.x, r0.z  
  103:  max r0.x, r0.x, l(0)  
  104:  add r0.x, r0.x, l(1.000000)  
  105:  div_sat r0.x, r1.x, r0.x  
  106:  add r0.x, -r0.x, l(1.000000)  
  107:  mad r0.x, -r2.x, r0.x, l(1.000000)  
  108:  add r0.y, r0.w, -cb12[48].y  
  109:  mul_sat r0.y, r0.y, cb12[48].z  
  110: else  
  111:  mov r0.xy, l(1.000000, 0.000000, 0.000000, 0.000000)  
  112: endif  
  113: log r0.x, r0.x  
  114: mul r0.z, r0.x, cb12[42].w  
  115: exp r0.z, r0.z  
  116: mul r0.z, r0.z, r0.y  
  117: mul r0.x, r0.x, cb12[48].x  
  118: exp r0.x, r0.x  
  119: mul o0.w, r0.x, r0.y  
  120: mad_sat r0.xy, r0.zzzz, cb12[189].xzxx, cb12[189].ywyy  
  121: add r2.xyz, -r1.yzwy, cb12[188].xyzx  
  122: mad r2.xyz, r0.xxxx, r2.xyzx, r1.yzwy  
  123: add r0.x, cb12[188].w, l(-1.000000)  
  124: mad r0.x, r0.y, r0.x, l(1.000000)  
  125: mul_sat r2.w, r0.x, r0.z  
  126: lt r0.x, l(0), cb12[192].x  
  127: if_nz r0.x  
  128:  mad_sat r0.xy, r0.zzzz, cb12[191].xzxx, cb12[191].ywyy  
  129:  add r3.xyz, -r1.yzwy, cb12[190].xyzx  
  130:  mad r1.xyz, r0.xxxx, r3.xyzx, r1.yzwy  
  131:  add r0.x, cb12[190].w, l(-1.000000)  
  132:  mad r0.x, r0.y, r0.x, l(1.000000)  
  133:  mul_sat r1.w, r0.x, r0.z  
  134:  add r0.xyzw, -r2.xyzw, r1.xyzw  
  135:  mad o1.xyzw, cb12[192].xxxx, r0.xyzw, r2.xyzw  
  136: else  
  137:  mov o1.xyzw, r2.xyzw  
  138: endif  
  139: mov o3.xyzw, v1.xyzw  
  140: mov o2.xy, v4.yxyy  
  141: ret

Di sini, perhitungan kabut dapat langsung menarik perhatian (baris 30-138). Perhitungan kabut teratas masuk akal untuk alasan kinerja. Selain itu, kita tidak memerlukan ketepatan kabut seperti itu - meteoroid biasanya terbang di atas kepala Geralt dan tidak mencapai cakrawala.

Parameter atmosfer (rgb = warna, a = pengaruh) disimpan di o0.xyzw, dan parameter kabut di o1.xyzw.

o2.xy (baris 140) hanyalah texcoords.
o3.xyzw (baris 139) tidak relevan.

Sekarang mari kita ucapkan beberapa kata tentang menghitung posisi di dunia. Vertex shaders melakukan billboarding . Pertama-tama, data yang masuk untuk papan iklan berasal dari vertex buffer - mari kita melihatnya.

Data pertama adalah Posisi:


Seperti disebutkan di atas, di sini kita memiliki 2 quad-a: 8 simpul, 12 indeks.

Tetapi mengapa posisinya sama untuk setiap quad? Cukup sederhana - ini adalah posisi pusat quad.

Selanjutnya, setiap titik memiliki offset dari pusat ke tepi quad:


Ini berarti bahwa setiap bintang jatuh memiliki ukuran (400, 3) unit di ruang dunia. (pada bidang XY, dalam Witcher 3, sumbu Z diarahkan ke atas)

Elemen terakhir yang dimiliki masing-masing simpul adalah vektor arah satuan dalam ruang dunia yang mengontrol gerakan bintang jatuh:


Karena data berasal dari CPU, sulit untuk memahami bagaimana cara menghitungnya.

Sekarang mari kita beralih ke kode billboarding. Idenya cukup sederhana - pertama Anda mendapatkan vektor satuan dari pusat quad ke kamera:

   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx

Kemudian kita mendapatkan satu vektor garis singgung yang mengontrol gerakan bintang jatuh.

Mengingat bahwa vektor ini sudah dinormalisasi di sisi CPU, normalisasi ini berlebihan.

  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx

Jika ada dua vektor, produk vektor digunakan untuk menentukan vektor bi-tangent tegak lurus kedua vektor yang masuk.

  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx

Sekarang kita telah menormalisasi vektor tangen (r3.xyz) dan bitangent (r2.xyz).

Mari kita perkenalkan Xsize dan Ysize yang sesuai dengan elemen yang masuk TEXCOORD1, jadi misalnya (-200, 1,50).

Perhitungan akhir dari posisi di ruang dunia dilakukan sebagai berikut:

  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000) 

Mengingat bahwa r0.x, r0.y dan r0.z sama dengan 1.0, perhitungan akhir disederhanakan:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

Bagian terakhir adalah perkalian sederhana dari posisi di ruang dunia dengan matriks proyeksi-proyeksi untuk mendapatkan SV_Position:

  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  

3. Pixel Shader


Seperti yang dinyatakan di bagian Gambaran Umum, keadaan pencampuran berikut digunakan: di mana SrcColor dan SrcAlpha adalah komponen .rgb dan .a dari shader piksel, dan DestColor adalah warna .rgb yang saat ini berada dalam rendertarget. Indikator utama yang mengontrol transparansi adalah SrcAlpha . Banyak shader game proaktif menghitungnya sebagai opacity dan menerapkannya di akhir sebagai berikut: Bintang jatuh shader tidak terkecuali. Mengikuti pola ini, kami mempertimbangkan tiga kasus di mana opacity adalah 1,0, 0,1, dan 0,0.

FinalColor = SrcColor * One + DestColor * (1.0 - SrcAlpha) =
FinalColor = SrcColor + DestColor * (1.0 - SrcAlpha)






return float4( color * opacity, opacity )



a) opacity = 1.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = color = SrcColor



b) opacity = 0.1

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = 0.1 * color + 0.9 * DestColor



c) opacity = 0.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = DestColor



Gagasan yang mendasari shader ini adalah untuk memodelkan dan menggunakan opacity function opacity (x) , yang mengontrol opacity piksel sepanjang bintang jatuh. Persyaratan utama adalah bahwa opacity harus mencapai nilai maksimum di ujung bintang ("badan" -nya) dan dengan lancar memudar menjadi 0,0 (ke "ekornya").

Ketika kita mulai memahami kode assembler dari pixel shader, ini menjadi jelas:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[2], immediateIndexed  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.y  
    dcl_input_ps linear v3.w  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mov_sat r0.x, v2.y  
   1: ge r0.y, r0.x, l(0.052579)  
   2: ge r0.z, l(0.965679), r0.x  
   3: and r0.y, r0.z, r0.y  
   4: if_nz r0.y  
   5:  ge r0.y, l(0.878136), r0.x  
   6:  add r0.z, r0.x, l(-0.052579)  
   7:  mul r1.w, r0.z, l(1.211303)  
   8:  mov_sat r0.z, r1.w  
   9:  mad r0.w, r0.z, l(-2.000000), l(3.000000)  
  10:  mul r0.z, r0.z, r0.z  
  11:  mul r0.z, r0.z, r0.w  
  12:  mul r2.x, r0.z, l(0.084642)  
  13:  mov r1.yz, l(0.000000, 0.000000, 0.084642, 0.000000)  
  14:  movc r2.yzw, r0.yyyy, r1.yyzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  15:  not r0.z, r0.y  
  16:  if_z r0.y  
  17:   ge r0.y, l(0.924339), r0.x  
  18:   add r0.w, r0.x, l(-0.878136)  
  19:   mul r1.w, r0.w, l(21.643608)  
  20:   mov_sat r0.w, r1.w  
  21:   mad r3.x, r0.w, l(-2.000000), l(3.000000)  
  22:   mul r0.w, r0.w, r0.w  
  23:   mul r0.w, r0.w, r3.x  
  24:   mad r1.x, r0.w, l(0.889658), l(0.084642)  
  25:   mov r1.yz, l(0.000000, 0.084642, 0.974300, 0.000000)  
  26:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  27:  else  
  28:   mov r2.y, l(0)  
  29:   mov r0.y, l(-1)  
  30:  endif  
  31:  not r0.w, r0.y  
  32:  and r0.z, r0.w, r0.z  
  33:  if_nz r0.z  
  34:   ge r0.y, r0.x, l(0.924339)  
  35:   add r0.x, r0.x, l(-0.924339)  
  36:   mul r1.w, r0.x, l(24.189651)  
  37:   mov_sat r0.x, r1.w  
  38:   mad r0.z, r0.x, l(-2.000000), l(3.000000)  
  39:   mul r0.x, r0.x, r0.x  
  40:   mul r0.x, r0.x, r0.z  
  41:   mad r1.x, r0.x, l(-0.974300), l(0.974300)  
  42:   mov r1.yz, l(0.000000, 0.974300, 0.000000, 0.000000)  
  43:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  44:  endif  
  45: else  
  46:  mov r2.yzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  47:  mov r0.y, l(0)  
  48: endif  
  49: mov_sat r2.w, r2.w  
  50: mad r0.x, r2.w, l(-2.000000), l(3.000000)  
  51: mul r0.z, r2.w, r2.w  
  52: mul r0.x, r0.z, r0.x  
  53: add r0.z, -r2.y, r2.z  
  54: mad r0.x, r0.x, r0.z, r2.y  
  55: movc r0.x, r0.y, r2.x, r0.x  
  56: mad r0.y, cb4[1].x, -cb0[9].w, l(1.000000)  
  57: mul_sat r0.y, r0.y, v3.w  
  58: mul r0.x, r0.y, r0.x  
  59: mul r0.yzw, cb2[2].xxyz, cb4[0].xxxx  
  60: mul r0.x, r0.x, cb2[2].w  
  61: dp3 r1.x, l(0.333000, 0.555000, 0.222000, 0.000000), r0.yzwy  
  62: mad r1.xyz, r1.xxxx, v0.xyzx, -r0.yzwy  
  63: mad r0.yzw, v0.wwww, r1.xxyz, r0.yyzw  
  64: add r1.xyz, -r0.yzwy, v1.xyzx  
  65: mad r0.yzw, v1.wwww, r1.xxyz, r0.yyzw  
  66: mul o0.xyz, r0.xxxx, r0.yzwy  
  67: mov o0.w, r0.x  
  68: ret

Secara umum, shader agak rumit dan sulit bagi saya untuk mencari tahu apa yang terjadi di dalamnya. Misalnya, dari mana semua nilai seperti 1.211303, 21.643608 dan 24.189651 berasal?

Jika kita berbicara tentang fungsi opacity, maka kita memerlukan satu nilai input. Dengan ini, ini cukup sederhana - texcoord dalam kisaran dari [0,1] (baris 0) akan berguna di sini, sehingga kita dapat menerapkan fungsi tersebut ke seluruh panjang meteoroid.

Fungsi opacity memiliki tiga segmen / interval yang ditentukan oleh empat titik kontrol:

   // current status: no idea how these are generated  
   const float controlPoint0 = 0.052579;  
   const float controlPoint1 = 0.878136;  
   const float controlPoint2 = 0.924339;  
   const float controlPoint3 = 0.965679;

Saya tidak tahu bagaimana mereka dipilih / dihitung.

Seperti yang dapat kita lihat dari kode assembler, kondisi pertama hanya memeriksa apakah nilai input dalam kisaran [controlPoint0 - controlPoint3]. Jika tidak, maka opacity hanya 0,0.

   // Input for the opacity function
   float y = saturate(Input.Texcoords.y);  // r0.x
     
   // Value of opacity function.  
   // 0 - no change  
   // 1 - full color  
   float opacity = 0.0;  
     
   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
      ...

Dekripsi kode assembler di bawah ini diperlukan jika kita ingin memahami cara kerja fungsi opacity:

   6: add r0.z, r0.x, l(-0.052579)   
   7: mul r1.w, r0.z, l(1.211303)   
   8: mov_sat r0.z, r1.w   
   9: mad r0.w, r0.z, l(-2.000000), l(3.000000)   
  10: mul r0.z, r0.z, r0.z   
  11: mul r0.z, r0.z, r0.w   
  12: mul r2.x, r0.z, l(0.084642)

Jalur 9 memiliki koefisien '-2.0' dan '3.0', yang mengisyaratkan penggunaan fungsi smoothstep . Ya, ini dugaan yang bagus.

Fungsi smoothstep HLSL dengan prototipe: ret smoothstep (min, max, x) selalu membatasi x hingga [ min-max ]. Dari sudut pandang assembler, ini mengurangi min dari nilai input (mis., Dari r0.z pada baris 9), tetapi tidak ada yang seperti itu dalam kode. Untuk maks, ini berarti penggandaan nilai input, tetapi tidak ada yang seperti 'mul_sat' dalam kode. Sebaliknya, ada 'mov_sat'. Ini memberitahu kita bahwa fungsi min dan max smoothstep adalah 0 dan 1.

Sekarang kita tahu bahwa xharus dalam interval [0, 1]. Seperti yang dinyatakan di atas, ada tiga segmen dalam fungsi opacity. Ini jelas mengisyaratkan bahwa kode mencari di mana kita berada dalam interval [segmentStart-segmentEnd].

Jawabannya adalah fungsi Linstep!

 float linstep(float min, float max, float v)  
 {  
   return ( (v-min) / (max-min) );  
 }

Sebagai contoh, mari kita ambil segmen pertama: [0,052579 - 0,878136]. Pengurangan ada di baris 6. Jika kita mengganti pembagian dengan perkalian -> 1.0 / (0.878136 - 0.052579) = 1.0 / 0.825557 = ~ 1.211303.

Hasil smoothstep ada di kisaran [0, 1]. Perkalian pada baris 12 adalah bobot segmen. Setiap segmen memiliki bobotnya sendiri, memungkinkan Anda untuk mengontrol opacity maksimum dari segmen tertentu ini.

Ini berarti bahwa untuk segmen pertama [0,052579 - 0,878136], opacity berada dalam kisaran [0 - 0,084642].

Fungsi HLSL yang menghitung opacity untuk segmen arbitrer dapat ditulis sebagai berikut:

 float getOpacityFunctionValue(float x, float cpLeft, float cpRight, float weight)  
 {  
   float val = smoothstep( 0, 1, linstep(cpLeft, cpRight, x) );  
   return val * weight;  
 }

Jadi, intinya adalah memanggil fungsi ini untuk segmen yang sesuai.

Lihatlah bobotnya:

   const float weight0 = 0.084642;  
   const float weight1 = 0.889658;  
   const float weight2 = 0.974300; // note: weight0+weight1 = weight2

Menurut kode assembler, fungsi opacity (x) dihitung sebagai berikut:

   float opacity = 0.0;

   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
     // Range of v: [0, weight0]  
     float v = getOpacityFunctionValue(y, controlPoint0, controlPoint1, weight0);  
     opacity = v;  
     
     [branch]  
     if ( y >= controlPoint1 )  
     {  
       // Range of v: [0, weight1]  
       float v = getOpacityFunctionValue(y, controlPoint1, controlPoint2, weight1);  
       opacity = weight0 + v;  
   
       [branch]  
       if (y >= controlPoint2)  
       {  
         // Range of v: [0, weight2]  
         float v = getOpacityFunctionValue(y, controlPoint2, controlPoint3, weight2);
         opacity = weight2 - v;          
       }  
     }  
   }

Berikut adalah grafik dari fungsi opacity. Anda dapat dengan mudah melihat peningkatan tajam dalam opacity, menunjukkan awal dari tubuh bintang jatuh:


Fungsi opacity grafik.

Saluran merah - nilai opacity.
Saluran hijau - titik kontrol.
Saluran biru - bobot.


Setelah menghitung opacity, yang lainnya hanyalah sentuhan akhir. Lalu ada multiplikasi tambahan: kekaburan bintang-bintang, warna bintang jatuh dan pengaruh kabut. Seperti biasa di TW3 shaders, Anda juga dapat menemukan perkalian berlebihan dengan 1,0 di sini:

   // cb4_v1.x = 1.0  
   float starsOpacity = 1.0 - cb0_v9.w * cb4_v1.x;    
   opacity *= starsOpacity;  

   // Calculate color of a shooting star  
   // cb4_v0.x = 10.0
   // cb2_v2.rgb = (1.0, 1.0, 1.0)
   float3 color = cb2_v2.rgb * cb4_v0.x;
     
   // cb2_v2.w = 1  
   opacity *= cb2_v2.w;
     
   FogResult fr = { Input.FogParams, Input.AerialParams };  
   color = ApplyFog(fr, color);
     
   return float4( color*opacity, opacity);  
 }

4. Ringkasan


Kesulitan utama terletak pada bagian dengan fungsi opacity. Setelah memecahkan kode itu, semua yang lain cukup sederhana untuk dipahami.

Saya katakan di atas bahwa pixel shader agak terlalu rumit. Bahkan, kami hanya peduli tentang nilai fungsi opacity (x) , yang disimpan dalam r2.x (mulai dari baris 49). Namun, fungsi opacity dalam kode assembler menciptakan tiga variabel tambahan: minRange (r2.y), maxRange (r2.z) dan nilai (r2.w). Semuanya adalah parameter yang digunakan untuk menghitung opacity ketika opacity (x) tidak digunakan:

lerp( minRange, maxRange, smoothstep(0, 1, value) );

Pada kenyataannya, nilai opacity akhir diperoleh di cabang bersyarat pada baris 55 - jika nilai input xada dalam kisaran [controlPoint0 - controlPoint3], ini berarti bahwa fungsi opacity digunakan, jadi r2.x dipilih. Kalau tidak, ketika x berada di luar interval, opacity dihitung dari r0.x, yaitu, sesuai dengan persamaan di atas.

Saya men-debug beberapa piksel di luar interval [controlPoint0 - controlPoint3], dan opacity terakhir selalu menjadi nol.

Itu saja untuk hari ini. Dan, seperti biasa, terima kasih sudah membaca.

All Articles