Rekayasa terbalik dari rendering The Witcher 3

Baru-baru ini, saya mulai berurusan dengan rendering The Witcher 3. Game ini memiliki teknik rendering yang luar biasa. Selain itu, ia luar biasa dalam hal plot / musik / gameplay.



Pada artikel ini saya akan berbicara tentang solusi yang digunakan untuk membuat The Witcher 3. Ini tidak akan komprehensif seperti analisis grafis GTA V oleh Adrian Correger, setidaknya untuk saat ini.

Kami akan mulai dengan rekayasa terbalik koreksi nada.

Bagian 1: koreksi nada


Di sebagian besar game AAA modern, salah satu langkah render adalah tentu koreksi nada.

Biarkan saya mengingatkan Anda bahwa dalam kehidupan nyata ada rentang kecerahan yang agak luas, sedangkan pada layar komputer sangat terbatas (8 bit per piksel, yang memberi kita 0-255). Di sinilah tonemapping datang untuk menyelamatkan, memungkinkan Anda untuk menyesuaikan rentang yang lebih luas ke dalam interval pencahayaan terbatas. Biasanya, ada dua sumber data dalam proses ini: gambar HDR dengan titik mengambang, nilai warna yang melebihi 1,0, dan pencahayaan rata-rata pemandangan (yang terakhir dapat dihitung dalam beberapa cara, bahkan dengan mempertimbangkan adaptasi mata untuk mensimulasikan perilaku mata manusia, tetapi itu tidak masalah di sini).

Langkah selanjutnya (dan terakhir) adalah untuk mendapatkan kecepatan rana, menghitung warna dengan kecepatan rana dan memprosesnya menggunakan kurva koreksi nada. Dan di sini semuanya menjadi sangat membingungkan, karena konsep baru muncul, seperti "titik putih" (titik putih) dan "abu-abu tengah" (abu-abu tengah). Setidaknya ada beberapa kurva populer, dan beberapa di antaranya tercakup dalam Matt Pettineo 's A Closer Look at Tone Mapping .

Jujur, saya selalu punya masalah dengan implementasi koreksi nada yang benar dalam kode saya sendiri. Setidaknya ada beberapa contoh online yang bermanfaat bagi saya ... sampai batas tertentu. Beberapa dari mereka memperhitungkan kecerahan HDR / titik putih / abu-abu sedang, yang lain tidak - karena itu mereka tidak benar-benar membantu. Saya ingin menemukan implementasi yang "teruji pertempuran".

Kami akan bekerja di RenderDoc dengan menangkap kerangka salah satu pencarian utama Novigrad ini. Semua pengaturan maksimal:


Setelah mencari sedikit, saya menemukan panggilan untuk koreksi nada! Seperti yang saya sebutkan di atas, ada penyangga warna HDR (nomor tekstur 0, resolusi penuh) dan kecerahan rata-rata adegan (nomor tekstur 1, 1x1, titik mengambang, dihitung sebelumnya oleh compute shader).


Mari kita lihat kode assembler untuk pixel shader:

ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret 

Ada beberapa poin yang perlu diperhatikan. Pertama, kecerahan yang dimuat tidak harus sama dengan yang digunakan, karena terbatas (panggilan maks / mnt) dalam nilai yang dipilih oleh artis (dari buffer konstan). Ini nyaman karena memungkinkan Anda untuk menghindari kecepatan rana terlalu tinggi atau rendah. Langkah ini sepertinya lumrah, tetapi saya belum pernah melakukannya sebelumnya. Kedua, seseorang yang terbiasa dengan kurva koreksi nada akan langsung mengenali nilai "11.2" ini, karena sebenarnya ini adalah nilai titik putih dari kurva koreksi nada John Hable's Uncharted2 .

Parameter AF diambil dari cbuffer.

Jadi, kami memiliki tiga parameter lagi: cb3_v16.x, cb3_v16.y, cb3_v16.z. Kita dapat memeriksa maknanya:


Firasat saya:

Saya percaya bahwa "x" adalah semacam "skala putih" atau abu-abu sedang, karena dikalikan dengan 11.2 (baris 4), dan setelah itu digunakan sebagai pembilang dalam menghitung pengaturan kecepatan rana (baris 10).

"Y" - Saya menyebutnya "faktor pembilang u2," dan segera Anda akan melihat alasannya.

"Z" adalah "parameter eksponensial", karena digunakan dalam triple log / mul / exp (pada kenyataannya, dalam eksponensial).

Tetapi perlakukan nama-nama variabel ini dengan tingkat skeptisisme!

Juga:

cb3_v4.yz - nilai min / maks dari kecerahan yang diizinkan,
cb3_v7.xyz - parameter AC dari kurva Uncharted2,
cb3_v8.xyz - Parameter DF pada kurva Uncharted2.

Sekarang mari kita turun ke bagian yang sulit - kita akan menulis shader HLSL yang akan memberi kita kode assembler yang persis sama.

Ini bisa sangat sulit, dan semakin lama shader, semakin sulit tugasnya. Untungnya, beberapa waktu lalu saya menulis alat untuk dengan cepat menelusuri hlsl-> asm.

Hadirin sekalian ... selamat datang D3DShaderDisassembler!


Setelah bereksperimen dengan kode, saya mendapat koreksi nada HLSL siap pakai The Witcher 3 :

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); } 

Tangkapan layar dari utilitas saya untuk mengonfirmasi ini:


Voila!

Saya percaya ini adalah implementasi koreksi nada TW3 yang cukup akurat, setidaknya dalam hal kode assembler. Saya sudah menerapkannya dalam kerangka kerja saya dan ini bekerja dengan sangat baik!

Saya mengatakan "cukup" karena saya tidak tahu mengapa penyebut di ToneMapU2Func menjadi maksimum pada nol. Saat membaginya dengan 0, Anda seharusnya tidak terdefinisi?

Ini bisa diselesaikan, tetapi hampir secara tidak sengaja saya menemukan dalam bingkai ini versi lain dari shader nada TW3, digunakan untuk matahari terbenam yang indah (menarik bahwa digunakan dengan pengaturan grafis minimal!)


Mari kita periksa. Pertama, kode assembler untuk shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret 

Pada awalnya, kode tersebut mungkin terlihat menakutkan, tetapi pada kenyataannya, tidak semuanya buruk. Setelah analisis singkat, Anda akan melihat bahwa ada dua panggilan ke fungsi Uncharted2 dengan set data input yang berbeda (AF, kecerahan minimum / min ...). Saya belum pernah melihat keputusan seperti itu sebelumnya.

Dan HLSL:

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); } 

Faktanya, kami memiliki dua set parameter kontrol, kami menghitung dua warna dengan koreksi nada, dan pada akhirnya kami menginterpolasi mereka. Keputusan cerdas!

Bagian 2: adaptasi mata


Bagian kedua akan jauh lebih sederhana.

Pada bagian pertama, saya menunjukkan bagaimana koreksi nada dilakukan di TW3. Menjelaskan latar belakang teoretis, saya secara singkat menyebutkan adaptasi mata. Dan tahukah Anda? Pada bagian ini saya akan berbicara tentang bagaimana adaptasi mata ini direalisasikan.

Tapi tunggu, apa itu adaptasi mata dan mengapa kita membutuhkannya? Wikipedia tahu segalanya tentang hal itu, tetapi saya akan menjelaskan: bayangkan Anda berada di ruangan gelap (ingat Hidup itu Aneh) atau di dalam gua, dan pergi ke luar di tempat yang terang. Misalnya, sumber utama penerangan mungkin matahari.

Dalam kegelapan, pupil mata kita melebar sehingga lebih banyak cahaya memasuki retina melalui mereka. Ketika menjadi ringan, pupil mata kita berkurang dan kadang-kadang kita menutup mata karena "sakit".

Perubahan ini tidak terjadi secara instan. Mata harus beradaptasi dengan perubahan kecerahan. Itu sebabnya kami melakukan adaptasi mata dalam rendering waktu-nyata.

Contoh yang baik ketika kurangnya adaptasi mata terlihat adalah HDRToneMappingCS11 dari DirectX SDK. Perubahan tajam kecerahan sedang agak tidak menyenangkan dan tidak wajar.

Ayo mulai! Demi konsistensi, kami akan menganalisis kerangka yang sama dari Novigrad.


Sekarang kita akan masuk jauh ke dalam program frame capture RenderDoc. Adaptasi mata biasanya dilakukan tepat sebelum koreksi nada, dan The Witcher 3 tidak terkecuali.


Mari kita lihat keadaan pixel shader:


Kami memiliki dua sumber input - 2 tekstur, R32_FLOAT, 1x1 (satu piksel). tekstur0 berisi kecerahan rata-rata adegan dari bingkai sebelumnya. tekstur1 berisi kecerahan rata-rata adegan dari bingkai saat ini (dihitung segera sebelum penghitung komputasi ini - saya menandai ini dengan warna biru).

Diharapkan ada satu output - R32_FLOAT, 1x1. Mari kita lihat pixel shader.

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret 

Wow, sederhana sekali! Hanya 7 baris kode assembler. Apa yang sedang terjadi di sini? Saya akan menjelaskan setiap baris:

0) Dapatkan kecerahan rata-rata bingkai saat ini.
1) Dapatkan kecerahan rata-rata dari bingkai sebelumnya.
2) Lakukan pemeriksaan: apakah kecerahan saat ini kurang dari atau sama dengan kecerahan frame sebelumnya?
Jika ya, maka kecerahan berkurang, jika tidak, maka kecerahan meningkat.
3) Hitung perbedaannya: perbedaan = currentLum - previousLum.
4) Transfer bersyarat ini (movc) memberikan faktor kecepatan dari buffer konstan. Dua nilai berbeda dapat ditetapkan dari baris 2, tergantung pada hasil pemeriksaan. Ini adalah langkah cerdas, karena dengan cara ini Anda bisa mendapatkan kecepatan adaptasi yang berbeda untuk menurunkan dan meningkatkan kecerahan. Namun dalam kerangka yang dipelajari, kedua nilai tersebut sama dan bervariasi dari 0,11 hingga 0,3.
5) Perhitungan akhir kecerahan teradaptasi: adaptedLuminance = speedFactor * perbedaan + priorLuminance.
6) Akhir shader

Ini diimplementasikan dalam HLSL cukup sederhana:

  // The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; } 

Baris-baris ini memberi kita kode assembler yang sama. Saya hanya menyarankan mengganti tipe output dengan float4 dengan float . Tidak perlu membuang-buang bandwidth. Inilah cara Witcher 3 mengimplementasikan adaptasi mata. Cukup sederhana, bukan?

PS. Terima kasih banyak kepada Baldur Karlsson (Twitter: @baldurk ) untuk RenderDoc. Program ini sangat bagus.

Bagian 3: penyimpangan berwarna


Aberasi kromatik adalah efek terutama ditemukan di lensa murah. Ini terjadi karena lensa memiliki indeks bias yang berbeda untuk panjang cahaya tampak yang berbeda. Sebagai akibatnya, distorsi yang terlihat muncul. Namun, tidak semua orang menyukainya. Untungnya, dalam Witcher 3 efek ini sangat halus, dan karenanya tidak mengganggu dalam gameplay (setidaknya saya). Tapi Anda bisa mematikannya jika mau.

Mari kita perhatikan contoh adegan dengan aberasi kromatik dan tanpa itu:


Termasuk penyimpangan kromatik


Penyimpangan kromatik dinonaktifkan

Apakah Anda melihat ada perbedaan di dekat tepi? Aku juga. Mari kita coba adegan lain:


Penyimpangan kromatik disertakan. Perhatikan sedikit distorsi "merah" di area yang ditunjukkan.

Ya, jauh lebih baik! Di sini kontras antara area gelap dan terang lebih kuat, dan di sudut kita melihat sedikit distorsi. Seperti yang Anda lihat, efek ini sangat lemah. Namun, saya bertanya-tanya bagaimana penerapannya. Mari kita beralih ke bagian yang paling aneh: kode!

Implementasi

Hal pertama yang harus dilakukan adalah menemukan panggilan draw yang tepat dengan pixel shader. Faktanya, chromatic aberration adalah bagian dari pixel shader "post-processing" besar, yang terdiri dari chromatic aberration, vignetting, dan koreksi gamma. Semua ini ada di dalam shader piksel tunggal. Mari kita lihat lebih dekat kode assembler untuk pixel shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ... 

Dan untuk nilai cbuffer:


Jadi, mari kita coba memahami apa yang terjadi di sini. Bahkan, cb3_v17.xy adalah pusat penyimpangan kromatik, sehingga baris pertama menghitung vektor 2d dari koordinat texel (cb3_v17.zw = kebalikan dari ukuran viewport) ke "pusat penyimpangan kromatik" dan panjangnya, kemudian melakukan perhitungan, verifikasi, dan percabangan lainnya . Saat menerapkan aberasi kromatik, kami menghitung perpindahan menggunakan nilai-nilai tertentu dari buffer konstan dan mendistorsi saluran R dan G. Secara umum, semakin dekat ke tepi layar, semakin kuat efeknya. Baris 10 cukup menarik karena membuat piksel "bergerak lebih dekat", terutama ketika kita membesar-besarkan penyimpangan. Saya dengan senang hati akan berbagi dengan Anda realisasi saya tentang efeknya. Seperti biasa, ambillah nama variabel dengan bagian skeptisisme (solid). Dan perhatikan bahwa efeknya diterapkan sebelum koreksi gamma.

  void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } } 

Saya menambahkan "fChromaticAberrationIntensity" untuk meningkatkan ukuran offset, dan karenanya kekuatan efeknya, seperti namanya (TW3 = 1.0). Intensitas = 40:


Itu saja! Semoga Anda menikmati bagian ini.

Bagian 4: vignetting


Vignetting adalah salah satu efek post-processing yang paling umum digunakan dalam gim. Ia juga populer dalam fotografi. Sudut yang sedikit teduh dapat menciptakan efek yang indah. Ada beberapa jenis vignetting. Misalnya, Unreal Engine 4 menggunakan natural. Tetapi kembali ke The Witcher 3. Klik di sini untuk melihat perbandingan interaktif frame dengan dan tanpa sketsa. Perbandingan ini diambil dari panduan kinerja NVIDIA untuk The Witcher 3 .


Cuplikan layar dari "The Witcher 3" dengan vignetting dihidupkan.

Perhatikan bahwa sudut kiri atas (langit) tidak seteduh bagian gambar lainnya. Nanti kita akan kembali ke ini.

Detail Implementasi

Pertama, ada sedikit perbedaan antara sketsa yang digunakan dalam versi asli The Witcher 3 (yang dirilis pada 19 Mei 2015) dan dalam The Witcher 3: Blood and Wine. Dalam yang pertama, "inverse gradient" dihitung di dalam pixel shader, dan yang terakhir, itu pra-dihitung ke dalam tekstur 2D 256x256:


Tekstur 256x256, digunakan sebagai "gradien terbalik" dalam komplemen "Darah dan anggur."

Saya akan menggunakan shader dari "Blood and Wine" (permainan yang hebat, omong-omong). Seperti di sebagian besar gim lain, vignetting Witcher 3 dihitung dalam pixel shader dari pemrosesan akhir. Lihatlah kode assembler:

  ... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ... 

Menarik! Tampaknya gamma (baris 46) dan spasi linear (baris 51) digunakan untuk menghitung sketsa. Pada baris 48, kami mencicipi tekstur "gradien terbalik." cb3 [9] .xyz tidak terkait dengan vignetting. Dalam setiap frame yang diperiksa, ditetapkan nilai float3 (1.0, 1.0, 1.0), yaitu, mungkin merupakan filter akhir yang digunakan dalam efek fade-in / fade-out. Ada tiga parameter utama untuk vignetting di TW3:

  • Opacity (cb3 [6] .w) - memengaruhi kekuatan vignetting. 0 - tanpa vignetting, 1 - vignetting maksimal. Menurut pengamatan saya, di pangkalan The Witcher 3 itu sekitar 1,0, sementara di Darah dan Anggur berfluktuasi sekitar 0,15.
  • Warna (cb3 [7] .xyz) - fitur unggulan dari sketsa TW3 adalah kemampuan untuk mengubah warnanya. Tidak harus hitam, tetapi dalam praktiknya ... Biasanya memiliki nilai float3 (3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0) dan seterusnya - dalam kasus umum, ini adalah kelipatan 0,00392156 = 1.0 / 255.0
  • Bobot (cb3 [6] .xyz) adalah parameter yang sangat menarik. Saya selalu melihat sketsa "datar", misalnya seperti:



Topeng vignetting yang khas

Tetapi menggunakan bobot (baris 52), Anda bisa mendapatkan hasil yang sangat menarik:


Topeng vignetting TW3 dihitung menggunakan bobot

Bobotnya mendekati 1,0. Lihatlah data buffer konstanta untuk satu frame dari Darah dan Anggur (dunia magis dengan pelangi): inilah mengapa vignetting tidak memengaruhi piksel terang langit di atas.


Kode

Berikut ini adalah implementasi vignetting TW3 saya di HLSL.

GammaToLinear = pow (warna, 2.2)

  /* // The Witcher 3 vignette. // // Input color is in gamma space // Output color is in gamma space as well. */ float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; } 

Saya harap Anda menikmatinya. Anda juga dapat mencoba HLSLexplorer saya , yang banyak membantu saya dalam memahami kode assembler HLSL.

Seperti sebelumnya, ambil nama variabel dengan tingkat skeptisisme - TW3 shader diproses oleh D3DStripShader, jadi sebenarnya saya hampir tidak tahu apa-apa tentang mereka, saya hanya bisa menebak. Selain itu, saya tidak bertanggung jawab atas kerusakan yang disebabkan oleh peralatan Anda oleh shader ini;)

Bonus: menghitung gradien

Dalam The Witcher 3, dirilis pada 2015, gradien terbalik dihitung dalam pixel shader, daripada mengambil sampel tekstur yang sudah dihitung sebelumnya. Lihatlah kode assembler:

  35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000) 

Untungnya bagi kami, ini cukup sederhana. Pada HLSL, akan terlihat seperti ini:

  float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; } 

Artinya, kita cukup menghitung jarak dari pusat ke tekstil, lakukan sihir dengannya (perkalian, jenuh ...), dan kemudian ... hitung polinomialnya! Luar biasa.



Bagian 5: efek keracunan


Mari kita lihat bagaimana game "The Witcher 3: Wild Hunt" menerapkan efek mabuk. Jika Anda belum memainkannya, jatuhkan semuanya, beli dan mainkan, tonton video:

Sore:



Malam:


Pertama, kita melihat gambar ganda dan berputar-putar, sering muncul ketika Anda minum dalam kehidupan nyata. Semakin jauh piksel dari pusat gambar, semakin kuat efek rotasi. Saya sengaja memposting video kedua dengan malam itu, karena Anda dapat dengan jelas melihat rotasi ini pada bintang-bintang (lihat 8 poin terpisah?)

Bagian kedua dari efek keracunan, yang mungkin tidak segera terlihat, adalah sedikit perubahan pada zoom. Itu terlihat dekat pusat.

Mungkin jelas bahwa efek ini adalah tipikal pemrosesan pasca (pixel shader). Namun, lokasinya di jalur render mungkin tidak begitu jelas. Ternyata efek keracunan diterapkan segera setelah koreksi tonal dan tepat sebelum blur (gambar "mabuk" adalah input untuk blur gerak).

Mari kita mulai gim dengan kode assembler:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Dua buffer konstan terpisah digunakan di sini. Mari kita periksa nilainya:



Kami tertarik pada beberapa di antaranya:

cb0_v0.x -> waktu yang berlalu (dalam detik)
cb0_v1.xyzw - ukuran viewport dan kebalikan dari ukuran viewport (alias "ukuran pixel")
cb3_v0.x - rotasi di sekitar pixel, selalu memiliki nilai 1,0.
cb3_v0.y - besarnya efek keracunan. Setelah dihidupkan, itu tidak bekerja dengan kekuatan penuh, tetapi secara bertahap meningkat dari 0,0 ke 1,0.
cv3_v1.xy - offset piksel (lebih lanjut tentang ini di bawah). Ini adalah pasangan sin / cos, sehingga Anda dapat menggunakan sincos (waktu) di shader jika Anda mau.
cb3_v2.xy adalah pusat dari efek, biasanya float2 (0,5, 0,5).
Di sini kami ingin fokus untuk memahami apa yang terjadi, dan tidak hanya menulis ulang shader secara membuta.

Kami akan mulai dengan baris pertama:

  ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 

Saya menyebut jalur 0 "rasio zoom" dan Anda akan segera melihat alasannya. Segera setelah itu (baris 1), kami menghitung "perpindahan rotasi". Ini hanya input dosa / cos pasangan data dikalikan dengan 0,05.

Baris 2-4: Pertama, kami menghitung vektor dari pusat efek ke koordinat UV tekstur. Kemudian kita menghitung kuadrat jarak (3) dan jarak sederhana (4) (dari pusat ke texel)

Koordinat tekstur zoom


Mari kita lihat kode assembler berikut:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 

Karena mereka dikemas dengan cara ini, kita hanya dapat menganalisis sepasang float.

Sebagai permulaan, r0.yz adalah "offset rotasi," r1.z adalah jarak dari pusat ke texel, r1.xy adalah vektor dari pusat ke texel, r0.x adalah "faktor zoom".

Untuk memahami ini, mari kita asumsikan untuk sekarang bahwa zoomFactor = 1.0, yaitu, Anda dapat menulis yang berikut:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center; 

Tapi zoomFactor = 1.0:

  r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter; 

Demikian pula untuk r3.xy:

  10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center 


Tapi zoomFactor = 1.0: Hebat. Artinya, saat ini kita pada dasarnya memiliki TextureUV (texel) ± rotasi offset saat ini, tetapi bagaimana dengan zoomFactor? Lihat baris 0. Faktanya, zoomFactor = 1.0 - 0.1 * drunkAmount. Untuk drunkAmount maksimum, nilai zoomFactor harus 0,9, dan koordinat tekstur dengan zoom sekarang dihitung sebagai berikut:

r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter



  baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter 

Mungkin penjelasan seperti itu akan lebih intuitif: itu hanya interpolasi linier oleh beberapa faktor antara koordinat tekstur normal dan pusat. Ini adalah gambar "zoom-in". Untuk memahami ini, yang terbaik adalah bereksperimen dengan nilai-nilai. Berikut ini tautan ke Shadertoy, tempat Anda dapat melihat efeknya dalam aksi.

Offset tekstur


Seluruh fragmen dalam kode assembler:

  2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset 

menciptakan gradien tertentu, sebut saja "topeng intensitas perpindahan". Padahal, itu memberi dua makna. Yang pertama di r0.w (kita akan menggunakannya nanti) dan yang kedua adalah 5 kali lebih kuat di r0.x (baris 15). Yang terakhir sebenarnya berfungsi sebagai faktor ukuran texel, sehingga memengaruhi gaya bias.

Sampling-related Sampling

Selanjutnya, serangkaian sampling tekstur dilakukan. Bahkan, 2 seri 8 sampel digunakan, satu untuk setiap "sisi". Di HLSL, Anda dapat menulis ini sebagai berikut:

  static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB; 

Caranya adalah kita menambahkan ke baseTexcoordsA / B offset tambahan yang terletak pada lingkaran unit, dikalikan dengan "intensitas pergeseran koordinat tekstur" yang disebutkan sebelumnya. Semakin jauh dari pusat piksel, semakin besar jari-jari lingkaran di sekitar piksel - kita sampel 8 kali, yang terlihat jelas pada bintang-bintang. Nilai PointsAroundPixel (kelipatan 45 derajat):


Lingkaran tunggal

Pengambilan sampel terkait zoom

Bagian kedua dari efek mabuk di The Witcher 3 adalah zoom dengan zoom in dan zoom out. Mari kita lihat kode assembler yang melakukan tugas ini:

  56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 

Kita melihat bahwa ada tiga panggilan tekstur yang terpisah, yaitu, tiga koordinat tekstur yang berbeda. Mari kita menganalisis bagaimana koordinat tekstur dihitung darinya. Tetapi pertama-tama, kami menunjukkan input untuk bagian ini:

  float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60 

Beberapa kata tentang input. Kami menghitung offset dalam texels (misalnya, ukuran 8.0 * texel), yang kemudian ditambahkan ke koordinat dasar uv. Amplitudo hanya berkisar antara 0,98 dan 1,02 untuk memberikan rasa zoom, seperti halnya zoomFactor pada bagian yang melakukan rotasi.

Mari kita mulai dengan pasangan pertama - r1.xy (baris 61)

  r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude); 

Itu adalah:

 float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude); 

Mari kita periksa pasangan kedua - r3.xy (baris 62)

  r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV 

Itu adalah:

  float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates; 

Mari kita periksa pasangan ketiga - r0.xy (baris 63-64)

  r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV 

Itu adalah:

  float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates 

Ketiga kueri tekstur ditambahkan bersama-sama, dan hasilnya disimpan dalam register r1. Perlu dicatat bahwa pixel shader ini menggunakan sampler pengalamatan terbatas.

Menyatukan semuanya

Jadi, saat ini kami memiliki hasil rotasi dalam register r2 dan tiga permintaan zoom yang dilipat dalam register r1. Mari kita lihat baris terakhir kode assembler:

  70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Tentang input tambahan: r0.w diambil dari baris 7, ini adalah topeng intensitas kami, dan cb3 [0] .y adalah besarnya efek intoksikasi.

Mari kita lihat cara kerjanya. Pendekatan pertama saya adalah brute force:

  float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor; 

Tapi apa-apaan, tidak ada yang menulis shader seperti itu . Saya mengambil pensil dengan kertas dan menulis formula ini:

  finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart 


Di mana t = effectAmount * intensityMask

Jadi, kita mendapatkan:

  finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t ) 

Dan kita sampai pada hal berikut:

  finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity); 

Ya, bagian artikel ini ternyata sangat rinci, tetapi akhirnya kami selesai! Secara pribadi, saya belajar sesuatu dalam proses penulisan, saya harap Anda juga!

Jika Anda tertarik, sumber HLSL lengkap tersedia di sini . Saya mengujinya dengan HLSLexplorer saya , dan meskipun tidak ada korespondensi satu-ke-satu langsung dengan shader asli, perbedaannya sangat kecil (satu baris lebih sedikit) sehingga saya dapat mengatakan dengan keyakinan bahwa itu berfungsi. Terima kasih sudah membaca!

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


All Articles