Bagaimana rendering The Witcher 3 diimplementasikan: petir, bakat witcher, dan efek lainnya

gambar

Bagian 1. Ritsleting


Pada bagian ini, kita akan melihat proses merender petir di Witcher 3: Wild Hunt.

Rendering petir dilakukan sedikit lebih lambat dari efek tirai hujan , tetapi masih terjadi di lulus rendering langsung. Petir dapat dilihat di video ini:


Mereka menghilang dengan sangat cepat, jadi lebih baik untuk menonton video di 0,25.

Anda dapat melihat bahwa ini bukan gambar statis; Seiring waktu, kecerahannya sedikit berubah.

Dari sudut pandang nuansa rendering, ada banyak kesamaan dengan menggambar tirai hujan di kejauhan, misalnya, kondisi pencampuran yang sama (pencampuran aditif) dan kedalaman (pengecekan diaktifkan, pencatatan kedalaman tidak dilakukan).


Adegan tanpa kilat


Adegan petir

Dalam hal geometri petir, The Witcher 3 adalah jaring seperti pohon. Contoh petir ini diwakili oleh jala berikut:


Ini memiliki koordinat UV dan vektor normal. Semua ini berguna pada tahap vertex shader.

Vertex shader


Mari kita lihat kode shader vertex rakitan:

vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret 

Ada banyak kesamaan dengan tirai hujan vertex shader, jadi saya tidak akan mengulanginya. Saya ingin menunjukkan kepada Anda perbedaan penting yang ada di baris 11-18:

  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 

Pertama, cb1 [8] .xyz adalah posisi kamera, dan r2.xyz adalah posisi di ruang dunia, yaitu, baris 11 menghitung vektor dari kamera ke posisi di dunia. Kemudian baris 12-15 menghitung panjang (worldPos - cameraPos) * 0,000001.

v2.xyz adalah vektor normal dari geometri yang masuk. Baris 16 meluas dari interval [0-1] ke interval [-1; 1].

Maka posisi akhir di dunia dihitung:

finalWorldPos = worldPos + panjang (worldPos - cameraPos) * 0.000001 * normalVector
Cuplikan kode HLSL untuk operasi ini akan menjadi seperti ini:

  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output; 

Operasi ini menghasilkan "ledakan" kecil dari mesh (ke arah vektor normal). Saya bereksperimen dengan mengganti 0,000001 dengan beberapa nilai lainnya. Inilah hasilnya:


0,000002


0,000005


0,00001


0,000025

Pixel shader


Nah, kami sudah menemukan vertex shader, sekarang saatnya untuk turun ke kode assembler untuk pixel shader!

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret 

Berita bagus: kodenya tidak begitu panjang.

Berita buruk:

  3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 

... tentang apa semua ini?

Jujur, ini bukan pertama kalinya saya melihat sepotong ... kode assembler di shader Witcher 3. Tetapi ketika saya bertemu dengannya untuk pertama kalinya, saya berpikir: "Apa-apaan ini?"

Hal serupa dapat ditemukan di beberapa shader TW3 lainnya. Saya tidak akan menggambarkan petualangan saya dengan fragmen ini, dan hanya mengatakan bahwa jawabannya terletak pada bunyi integer :

  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } 

Seperti yang Anda lihat, dalam pixel shader itu disebut dua kali. Dengan menggunakan panduan dari situs web ini, kita dapat memahami bagaimana kebisingan halus diterapkan dengan benar. Saya akan kembali ke ini sebentar lagi.

Lihat baris 0 - di sini kita menghidupkan berdasarkan rumus berikut:

animation = elapsedTime * animationSpeed ​​+ TextureUV.x
Nilai-nilai ini, setelah pembulatan ke sisi bawah ( lantai ) (instruksi round_ni ) di masa depan menjadi titik input untuk kebisingan bilangan bulat. Biasanya kami menghitung nilai kebisingan untuk dua bilangan bulat, dan kemudian kami menghitung nilai akhir yang diinterpolasi di antara mereka (lihat situs web libnoise untuk detailnya).

Ya, ini adalah bilangan bulat bilangan bulat , tetapi bagaimanapun juga, semua nilai yang disebutkan sebelumnya (juga dibulatkan ke bawah) mengambang!

Perhatikan bahwa tidak ada instruksi ftoi di sini . Saya berasumsi bahwa programmer dari CD Projekt Red telah menggunakan fungsi internal HLint asint di sini , yang melakukan konversi nilai-nilai floating point “reinterpret_cast” dan memperlakukannya sebagai pola integer.

Berat interpolasi untuk kedua nilai dihitung dalam baris 10-11.

interpolationWeight = 1.0 - frac (animasi);
Pendekatan ini memungkinkan kita untuk menginterpolasi antar nilai dari waktu ke waktu.

Untuk membuat noise halus, interpolator ini diteruskan ke fungsi SCurve :

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } 


Fungsi Smoothstep [libnoise.sourceforge.net]

Fitur ini dikenal sebagai "smoothstep." Tetapi seperti yang Anda lihat dari kode assembler, ini bukan fungsi smoothstep internal dari HLSL. Fungsi internal menerapkan batasan sehingga nilainya benar. Tetapi karena kita tahu bahwa bobot interpolasi akan selalu berada dalam kisaran [0-1], pemeriksaan ini dapat dilewati dengan aman.

Saat menghitung nilai akhir, beberapa operasi multiplikasi digunakan. Lihat bagaimana output alpha akhir dapat berubah tergantung pada nilai noise. Ini nyaman karena akan mempengaruhi opacity dari petir yang diberikan, seperti dalam kehidupan nyata.

Shader piksel siap:

  cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); } 

Untuk meringkas


Pada bagian ini, saya menjelaskan cara membuat kilat di The Witcher 3.

Saya sangat senang bahwa kode assembler yang keluar dari shader saya benar-benar cocok dengan yang asli!


Bagian 2. Trik Langit Konyol


Bagian ini akan sedikit berbeda dari yang sebelumnya. Di dalamnya, saya ingin menunjukkan kepada Anda beberapa aspek sky shader Witcher 3.

Mengapa "trik konyol" dan bukan seluruh shader? Ada beberapa alasan. Pertama, Witcher 3 sky shader adalah binatang yang agak rumit. Pixel shader dari versi 2015 berisi 267 baris kode assembler, dan shader dari Blood and Wine DLC berisi 385 baris.

Selain itu, mereka menerima banyak input, yang tidak terlalu kondusif untuk merekayasa balik kode HLSL yang lengkap (dan dapat dibaca!).

Oleh karena itu, saya memutuskan untuk menunjukkan hanya sebagian dari trik dari shader ini. Jika saya menemukan sesuatu yang baru, saya akan melengkapi posting.

Perbedaan antara versi 2015 dan DLC (2016) sangat mencolok. Secara khusus, mereka memasukkan perbedaan dalam perhitungan bintang dan kerlipan mereka, pendekatan yang berbeda untuk membuat Matahari ... The Blood dan Wine shader bahkan menghitung Bima Sakti di malam hari.

Saya akan mulai dengan dasar-dasarnya dan kemudian berbicara tentang trik bodoh.

Dasar-dasarnya


Seperti kebanyakan game modern, Witcher 3 menggunakan skydome untuk memodelkan langit. Lihatlah belahan yang digunakan untuk ini dalam Witcher 3 (2015). Catatan: dalam hal ini, kotak pembatas dari mesh ini berada dalam kisaran dari [0,0,0] hingga [1,1,1] (Z adalah sumbu yang mengarah ke atas) dan telah mendistribusikan UV dengan lancar. Nanti kita menggunakannya.


Ide di balik skydome mirip dengan ide skybox (satu-satunya perbedaan adalah mesh yang digunakan). Pada tahap vertex shader, kita mengubah skydome relatif ke pengamat (biasanya sesuai dengan posisi kamera), yang menciptakan ilusi bahwa langit sebenarnya sangat jauh - kita tidak akan pernah sampai ke sana.

Jika Anda membaca bagian sebelumnya dari seri artikel ini, maka Anda tahu bahwa "The Witcher 3" menggunakan kedalaman terbalik, yaitu, pesawat jauh adalah 0,0f, dan terdekat adalah 1,0f. Agar output skydome sepenuhnya dieksekusi di pesawat jauh, dalam parameter jendela jelajahi kami menetapkan MinDepth ke nilai yang sama dengan MaxDepth :


Untuk mempelajari bagaimana bidang MinDepth dan MaxDepth digunakan selama konversi jendela penelusuran, klik di sini (docs.microsoft.com).

Vertex shader


Mari kita mulai dengan shader vertex. Dalam Witcher 3 (2015), kode shader assembler adalah sebagai berikut:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret 

Dalam hal ini, shader vertex hanya mentransfer texcoord dan posisi di ruang dunia ke output. Dalam Darah dan Anggur, ia juga menampilkan vektor normal yang dinormalisasi. Saya akan mempertimbangkan versi 2015 karena lebih sederhana.

Lihatlah buffer konstan yang ditunjuk sebagai cb2 :


Di sini kita memiliki matriks dunia (penskalaan seragam sebesar 100 dan transfer relatif ke posisi kamera). Tidak ada yang rumit. cb2_v4 dan cb2_v5 adalah koefisien skala / deviasi yang digunakan untuk mengubah posisi titik dari interval [0-1] ke interval [-1; 1]. Tetapi di sini, koefisien-koefisien ini “memampatkan” sumbu Z (ke atas).


Di bagian seri sebelumnya, kami memiliki vertex shaders yang serupa. Algoritma umum adalah untuk mentransfer texcoords lebih lanjut, kemudian Posisi dihitung dengan mempertimbangkan skala / koefisien deviasi, kemudian PositionW dihitung dalam ruang dunia, maka posisi akhir dari ruang kliping dihitung dengan mengalikan matWorld dan matViewProj -> produk mereka digunakan untuk menggandakan Posisi untuk mendapatkan SV_Position akhir. .

Oleh karena itu, HLSL dari vertex shader ini harus seperti ini:

  struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; } 

Perbandingan shader saya (kiri) dan asli (kanan):


Properti RenderDoc yang sangat baik adalah memungkinkan kami untuk menyuntikkan shader kami sendiri, bukan yang asli, dan perubahan ini akan memengaruhi pipa hingga akhir frame. Seperti yang Anda lihat dari kode HLSL, saya telah menyediakan beberapa opsi untuk memperbesar dan mengubah geometri akhir. Anda dapat bereksperimen dengan mereka dan mendapatkan hasil yang sangat lucu:


Optimalisasi Shader Vertex


Apakah Anda memperhatikan masalah shader vertex asli? Multiplikasi vertex dari sebuah matriks oleh sebuah matriks benar-benar redundan! Saya menemukan ini di setidaknya beberapa vertex shaders (misalnya, di shader tirai hujan di kejauhan ). Kita dapat mengoptimalkannya dengan segera mengalikan PositionW dengan matViewProj !

Jadi, kita dapat mengganti kode ini dengan HLSL:

  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); 

sebagai berikut:

  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW ); 

Versi yang dioptimalkan memberi kita kode perakitan berikut:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret 

Seperti yang Anda lihat, kami mengurangi jumlah instruksi dari 26 menjadi 12 - perubahan yang cukup signifikan. Saya tidak tahu seberapa luas masalah ini dalam permainan, tetapi demi Tuhan, CD Projekt Red, mungkin melepaskan tambalan? :)

Dan aku tidak bercanda. Anda dapat memasukkan shader yang dioptimalkan bukan RenderDoc asli dan Anda akan melihat bahwa pengoptimalan ini tidak memengaruhi apa pun secara visual. Sejujurnya, saya tidak mengerti mengapa CD Projekt Red memutuskan untuk melakukan perkalian vertex dari sebuah matriks dengan sebuah matriks ...

Matahari


Dalam The Witcher 3 (2015), perhitungan hamburan atmosfer dan Matahari terdiri dari dua panggilan gambar yang terpisah:


Witcher 3 (2015) - Sampai


Witcher 3 (2015) - dengan langit


Witcher 3 (2015) - dengan langit + Matahari

Render Matahari dalam versi 2015 sangat mirip dengan rendering Bulan dalam hal geometri dan keadaan pencampuran / kedalaman.

Di sisi lain, dalam "Darah dan Anggur" langit dengan Matahari diterjemahkan dalam satu jalur:


The Witcher 3: Blood and Wine (2016) - To Heaven


The Witcher 3: Blood and Wine (2016) - bersama Heaven and the Sun

Tidak peduli bagaimana Anda membuat Matahari, pada tahap tertentu Anda masih membutuhkan arah (normalisasi) sinar matahari. Cara paling logis untuk mendapatkan vektor ini adalah dengan menggunakan koordinat bola . Faktanya, kita hanya membutuhkan dua nilai yang mengindikasikan dua sudut (dalam radian!): Phi dan theta . Setelah menerimanya, kita dapat mengasumsikan bahwa r = 1 , sehingga mengurangi itu. Kemudian untuk koordinat Cartesian dengan sumbu Y mengarah ke atas, Anda dapat menulis kode berikut dalam HLSL:

  float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir); 

Biasanya, arah sinar matahari dihitung dalam aplikasi, dan kemudian diteruskan ke buffer konstan untuk digunakan di masa depan.

Setelah menerima arah sinar matahari, kita dapat mempelajari lebih dalam kode assembler dari shader piksel "Darah dan Anggur" ...

  ... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ... 

Jadi, pertama, cb12 [0] .xyz adalah posisi kamera, dan di r0.xyz kami menyimpan posisi vertex (ini adalah output dari shader vertex). Oleh karena itu, baris 100 menghitung vektor worldToCamera . Tapi lihatlah garis 105-107. Kita dapat menuliskannya sebagai normalisasi (-worldToCamera) , yaitu kita menghitung vektor cameraToWorld yang dinormalisasi.

  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx 

Lalu kami menghitung produk skalar dari cameraToWorld dan vektor sunDirection ! Ingat bahwa mereka harus dinormalisasi. Kami juga menjenuhkan ekspresi penuh ini untuk membatasi ke interval [0-1].

Hebat! Produk skalar ini disimpan di r1.x. Mari kita lihat di mana itu berlaku selanjutnya ...

  152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x 

Trinitas “log, mul, exp” adalah eksponensial. Seperti yang Anda lihat, kami meningkatkan cosinus kami (produk skalar vektor dinormalisasi) sampai batas tertentu. Anda mungkin bertanya mengapa. Dengan cara ini, kita bisa membuat gradien yang meniru matahari. (Dan baris 155 memengaruhi opacity dari gradien ini, sehingga kita, misalnya, mengatur ulang untuk sepenuhnya menyembunyikan Matahari). Berikut ini beberapa contohnya:


eksponen = 54


eksponen = 2400

Memiliki gradien ini, kami menggunakannya untuk menginterpolasi antara skyColor dan sunColor ! Untuk menghindari artefak, Anda perlu menjenuhkan nilai pada baris 120.

Perlu dicatat bahwa trik ini dapat digunakan untuk mensimulasikan mahkota bulan (dengan nilai eksponen rendah). Untuk melakukan ini, kita memerlukan vektor moonDirection , yang dapat dengan mudah dihitung menggunakan koordinat bola.

Kode HLSL siap pakai mungkin terlihat seperti cuplikan berikut:

  float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient ); 

Gerak bintang


Jika Anda membuat selang waktu langit malam Witcher 3 yang cerah, Anda dapat melihat bahwa bintang-bintang itu tidak statis - mereka bergerak sedikit melintasi langit! Saya memperhatikan hal ini hampir secara tidak sengaja dan ingin tahu bagaimana penerapannya.

Mari kita mulai dengan fakta bahwa bintang-bintang di Witcher 3 disajikan sebagai peta kubik ukuran 1024x1024x6. Jika Anda memikirkannya, Anda dapat memahami bahwa ini adalah solusi yang sangat nyaman yang memungkinkan Anda untuk dengan mudah mengambil arah untuk mengambil sampel peta kubik.

Mari kita lihat kode assembler berikut:

  159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0 

Untuk menghitung vektor pengambilan sampel akhir (baris 173), kita mulai dengan menghitung vektor worldToCamera yang dinormalisasi (baris 159-162).

Kemudian kami menghitung dua produk vektor (163-164, 165-166) dengan moonDirection , dan kemudian kami menghitung tiga produk skalar untuk mendapatkan vektor sampel akhir. Kode HLSL:

  float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb; 

Catatan untuk diri saya sendiri: ini adalah kode yang dirancang dengan sangat baik, dan saya harus memeriksanya secara lebih rinci.

Catatan untuk pembaca: jika Anda tahu lebih banyak tentang operasi ini, maka beri tahu saya!

Bintang yang berkelap-kelip


Trik lain yang menarik yang ingin saya jelajahi lebih detail adalah kerlipan bintang.Misalnya, jika Anda berkeliaran di Novigrad dalam cuaca cerah, Anda akan melihat bahwa bintang-bintang berkelap-kelip.

Saya ingin tahu bagaimana ini diterapkan. Ternyata perbedaan antara versi 2015 dan "Darah dan Anggur" cukup besar. Untuk kesederhanaan, saya akan mempertimbangkan versi 2015.

Jadi, kita mulai tepat setelah mengambil sampel StarsColor dari bagian sebelumnya:

  174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx 

Hm Mari kita lihat bagian akhir dari kode assembly yang cukup panjang ini.

Setelah mengambil sampel StarsColor pada baris 173, kami menghitung semacam nilai ofset . Ini diimbangi digunakan untuk mendistorsi arah pertama Sampling (r2.xyz, baris 235) dan kemudian lagi sempliruem kubus bintang peta melakukan koreksi gamma dari dua nilai (237-242) dan kalikan mereka (243).

Sederhana bukan? Yah, tidak juga. Mari kita pikirkan tentang ini sedikit mengimbangi . Nilai ini harus berbeda di seluruh skydome - bintang yang berkedip-kedip sama akan terlihat sangat tidak realistis.

Untuk mengimbangiBeraneka ragam mungkin, kita akan mengambil keuntungan dari fakta bahwa UV direntangkan ke skydome (v0.xy) dan menerapkan waktu yang telah berlalu yang disimpan dalam buffer konstan (cb [0] .x).

Jika Anda tidak terbiasa dengan ishr / xor / dan ini, maka pada bagian tentang efek kilat, baca tentang kebisingan bilangan bulat.

Seperti yang Anda lihat, noise integer disebabkan empat kali di sini, tetapi berbeda dari yang digunakan untuk kilat. Untuk membuat hasil lebih acak, integer input untuk noise adalah jumlah ( iadd ) dan bit terbalik dengannya (internal function reversebits ; instruksi bfrev ).

Jadi, sekarang pelan-pelan. Mari kita mulai dari awal.

Kami memiliki 4 "iterasi" bilangan bulat bilangan bulat. Saya menganalisis kode assembler, perhitungan semua iterasi 4 terlihat seperti ini:

  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B ); 

Hasil akhir dari semua 4 iterasi (untuk menemukan mereka, pastikan petunjuk itof ):

Iterasi 1 - r5.x,

Iterasi 2 - r4.w,

Iterasi 3 - r1.w,

Iterasi 4 - r5.y

Setelah terakhir itof (baris 216 ) kami memiliki:

  217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 

Garis-garis ini menghitung nilai kurva-S untuk keseimbangan berdasarkan bagian pecahan dari UV, seperti dalam kasus petir. Jadi:

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY ); 

Seperti yang Anda duga, koefisien ini digunakan untuk menginterpolasi kebisingan dengan lancar dan menghasilkan offset akhir untuk koordinat sampel:

  229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb; 

Berikut ini adalah visualisasi kecil dari offset yang dihitung :


Setelah menghitung starsColorDisturbed, bagian tersulit selesai. Hore!

Langkah selanjutnya adalah melakukan koreksi gamma untuk starsColor dan starsColorDisturbed , setelah itu mereka dikalikan:

  starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed; 

Bintang - sentuhan akhir


Kami memiliki starsFinal di r1.xyz. Pada akhir pemrosesan bintang, hal berikut terjadi:

  256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000) 

Ini jauh lebih mudah dibandingkan dengan bintang yang berkelap-kelip dan bergerak.

Jadi, kita mulai dengan menaikkan starsFinal ke kekuatan 2,5 - ini memungkinkan kita untuk mengontrol kepadatan bintang. Cukup pintar. Kemudian kita membuat warna maksimum bintang sama dengan float3 (1, 1, 1).

cb0 [9] .w digunakan untuk mengontrol visibilitas keseluruhan bintang. Oleh karena itu, kita dapat berharap bahwa pada siang hari nilai ini adalah 1,0 (yang memberikan perkalian dengan nol), dan pada malam hari - 0,0.

Pada akhirnya, kami meningkatkan visibilitas bintang sebesar 10. Dan itu saja!

Bagian 3. The Witcher Flair (objek dan kecerahan peta)


Hampir semua efek dan teknik yang dijelaskan sebelumnya tidak benar-benar terkait dengan Witcher 3. Hal-hal seperti koreksi nada, vignetting, atau menghitung kecerahan rata-rata hadir di hampir setiap permainan modern. Bahkan efek keracunan cukup luas.

Itulah mengapa saya memutuskan untuk melihat lebih dekat pada mekanisme rendering dari "insting penyihir". Geralt adalah tukang sihir, dan karena itu perasaannya jauh lebih tajam daripada orang biasa. Akibatnya, ia dapat melihat dan mendengar lebih banyak daripada orang lain, yang sangat membantunya dalam penyelidikannya. Mekanika bakat penyihir memungkinkan pemain untuk memvisualisasikan jejak tersebut.

Berikut ini demonstrasi efeknya:


Dan satu lagi, dengan pencahayaan yang lebih baik:


Seperti yang Anda lihat, ada dua jenis objek: objek yang dapat berinteraksi dengan Geralt (garis kuning) dan jejak yang terkait dengan investigasi (garis merah). Setelah Geralt memeriksa jejak merah, itu bisa berubah menjadi kuning (video pertama). Perhatikan bahwa seluruh layar berubah menjadi abu-abu dan efek mata ikan (video kedua) ditambahkan.

Efek ini agak rumit, jadi saya memutuskan untuk membagi penelitiannya menjadi tiga bagian.

Dalam yang pertama saya akan berbicara tentang pemilihan objek, di yang kedua - tentang generasi kontur, dan yang ketiga - tentang penyatuan akhir dari semua ini menjadi satu kesatuan.

Pilih Objek


Seperti yang saya katakan, ada dua jenis objek, dan kita perlu membedakannya. Dalam Witcher 3, ini diimplementasikan menggunakan buffer stensil. Saat membuat jerat GBuffer yang harus ditandai sebagai "jejak" (merah), mereka diberikan dengan stensil = 8. Jaring yang ditandai dengan kuning sebagai objek "menarik" dirender dengan stensil = 4.

Misalnya, dua tekstur berikut menunjukkan contoh frame dengan insting witcher yang terlihat dan buffer stensil yang sesuai:



Penjelasan Ringkas Stensil


Buffer stensil sering digunakan dalam game untuk menandai jerat. Kategori jerat tertentu diberi ID yang sama.

Idenya adalah untuk menggunakan fungsi Selalu dengan operator Ganti jika tes stensil berhasil, dan dengan operator Keep dalam semua kasus lainnya.

Inilah cara penerapannya menggunakan D3D11:

  D3D11_DEPTH_STENCIL_DESC depthstencilState; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue ); 

Nilai stensil yang akan ditulis ke buffer dilewatkan sebagai StencilRef dalam panggilan API:

  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... ); 

Kecerahan render


Dalam bagian ini, dari sudut pandang implementasi, ada satu tekstur layar penuh dalam format R11G11B10_FLOAT, di mana objek dan jejak yang menarik disimpan di saluran R dan G.

Mengapa kita membutuhkan ini dalam hal kecerahan? Ternyata naluri Geralt memiliki radius terbatas, sehingga objek hanya mendapatkan garis besar ketika pemain cukup dekat dengan mereka.

Lihatlah aspek ini dalam tindakan:



Kami mulai dengan membersihkan tekstur kecerahan, mengisinya dengan warna hitam.

Kemudian dibuat dua panggilan gambar layar penuh: yang pertama untuk jejak, yang kedua untuk objek yang menarik:


Panggilan undian pertama dibuat untuk jejak - saluran hijau:


Panggilan kedua dilakukan untuk objek yang menarik - saluran merah:


Baiklah, tetapi bagaimana kita menentukan piksel mana yang harus dipertimbangkan? Kita harus menggunakan buffer stensil!

Untuk masing-masing panggilan ini, tes stensil dilakukan, dan hanya piksel yang sebelumnya ditandai sebagai "8" (panggilan draw pertama) atau "4" yang diterima.

Visualisasi uji stensil untuk jejak:


... dan untuk objek menarik:


Bagaimana tes dilakukan dalam kasus ini? Anda dapat mempelajari tentang dasar-dasar pengujian stensil dalam posting yang bagus . Secara umum, rumus uji stensil memiliki bentuk berikut:

  if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel 

di mana:
StencilRef adalah nilai yang diteruskan oleh panggilan API,

StencilReadMask adalah topeng yang digunakan untuk membaca nilai stensil (perhatikan bahwa ia ada di sisi kiri dan kanan),

OP adalah operator perbandingan, ditetapkan melalui API,

StencilValue adalah nilai buffer stensil dalam piksel saat ini sedang diproses.

Penting untuk dipahami bahwa kami menggunakan AND biner untuk menghitung operan.

Setelah berkenalan dengan dasar-dasar, mari kita lihat bagaimana parameter ini digunakan dalam panggilan draw ini:


Kondisi Stensil untuk Jejak


Keadaan stensil untuk objek menarik

Ha! Seperti yang bisa kita lihat, satu-satunya perbedaan adalah ReadMask. Mari kita periksa! Ganti nilai-nilai ini dalam persamaan uji stensil:

  Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE 

Secara cerdik. Seperti yang Anda lihat, dalam hal ini kami tidak membandingkan nilai stensil, tetapi periksa apakah sedikit buffer stensil telah disetel. Setiap piksel buffer stensil memiliki format uint8, sehingga interval nilainya adalah [0-255].

Catatan: semua panggilan DrawIndexed (36) terkait dengan rendering jejak kaki sebagai jejak, oleh karena itu dalam bingkai khusus ini peta kecerahan memiliki bentuk akhir berikut:


Tapi sebelum tes stensil ada pixel shader. Baik 28738 dan 28748 menggunakan pixel shader yang sama:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret 

Pixel shader ini hanya menulis untuk satu target render, jadi baris 24-27 berlebihan.

Hal pertama yang terjadi di sini adalah pengambilan sampel kedalaman (dengan sampler titik dengan batas nilai), baris 1. Nilai ini digunakan untuk membuat ulang posisi di dunia dengan mengalikannya dengan matriks khusus, diikuti dengan pembagian perspektif (garis 2-6).

Mengambil posisi Geralt (cb3 [7] .xyz - perhatikan bahwa ini bukan posisi kamera!), Kami menghitung jarak dari Geralt ke titik khusus ini (baris 7-9).

Input berikut ini penting dalam shader ini:

- cb3 [0] .rgb - warna keluaran. Ini dapat memiliki format float3 (0, 1, 0) (jejak) atau float3 (1, 0, 0) (objek menarik),
- cb3 [6] .y - faktor penskalaan jarak. Langsung mempengaruhi radius dan kecerahan dari hasil akhir.

Kemudian, kami memiliki rumus yang cukup rumit untuk menghitung kecerahan tergantung pada jarak antara Geralt dan objek. Saya dapat berasumsi bahwa semua koefisien dipilih secara eksperimental.

Hasil akhir adalah intensitas warna * . Kode HLSL akan terlihat seperti ini:



  struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; } 

Perbandingan kecil dari kode shader assembler asli (kiri) dan saya (kanan).


Ini adalah tahap pertama dari efek bakat penyihir . Faktanya, ini adalah yang paling sederhana.

Bagian 4. The Witcher Flair (peta garis besar)


Sekali lagi, lihat pemandangan yang sedang kami jelajahi:


Pada bagian pertama dari analisis efek naluri penyihir, saya menunjukkan bagaimana "peta kecerahan" dihasilkan.

Kami memiliki satu tekstur layar penuh format R11G11B10_FLOAT, yang mungkin terlihat seperti ini:


Saluran hijau berarti "jejak kaki", benda merah - menarik yang dengannya Geralt dapat berinteraksi.

Setelah menerima tekstur ini, kita dapat melanjutkan ke tahap berikutnya - saya menyebutnya "peta kontur".


Ini adalah tekstur yang agak aneh dari format R16G16_FLOAT 512x512. Adalah penting bahwa itu diimplementasikan dalam gaya "ping pong". Peta kontur dari bingkai sebelumnya adalah data input (bersama dengan peta kecerahan) untuk menghasilkan peta kontur baru dalam bingkai saat ini.

Buffer ping-pong dapat diimplementasikan dengan banyak cara, tetapi saya pribadi sangat menyukai yang berikut ini (pseudo-code):

  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; } 

Pendekatan ini, di mana input selalu [m_outlineIndex] , dan output selalu [! M_outlineIndex] , memberikan fleksibilitas untuk penggunaan efek pasca-lanjut.

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_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret 

Seperti yang Anda lihat, peta kontur keluaran dibagi menjadi empat kotak yang sama, dan ini adalah hal pertama yang perlu kita pelajari:

  0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 

Kita mulai dengan menghitung lantai (TextureUV * 2.0), yang memberi kita yang berikut:


Untuk menentukan kotak individu, fungsi kecil digunakan:

  float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; } 

Perhatikan bahwa fungsi mengembalikan 1,0 dengan input float2 (0,0, 0,0).

Kasing ini terjadi di sudut kiri atas. Untuk mendapatkan situasi yang sama di sudut kanan atas, kurangi float2 (1, 0) dari texcoords bundar, kurangi float2 (0, 1) untuk kotak hijau, dan float2 (1.0, 1.0) untuk kotak kuning.

Jadi:

  float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 ); 

Masing-masing komponen topeng adalah nol atau satu, dan bertanggung jawab untuk satu kuadrat dari tekstur. Misalnya, mask.r dan mask.w :


mask.r


mask.w

Kita punya topeng , mari kita lanjutkan. Baris 15 mengambil sampel peta luminance. Perhatikan bahwa tekstur kecerahan dalam format R11G11B10_FLOAT, meskipun kami mengambil sampel semua komponen rgba. Dalam situasi ini, diasumsikan bahwa .a adalah 1.0f.

Texcoords yang digunakan untuk operasi ini dapat dihitung sebagai frac (TextureUV * 2.0) . Oleh karena itu, hasil operasi ini dapat, misalnya, terlihat seperti ini:


Lihat kemiripannya?

Langkah selanjutnya sangat cerdas - produk skalar empat komponen (dp4) dilakukan:

  16: dp4 r1.x, r1.xyzw, r2.xyzw 

Jadi, hanya saluran merah (yaitu, hanya objek yang menarik) yang tersisa di sudut kiri atas, hanya saluran hijau di kanan atas (hanya jejak), dan semua yang ada di kanan bawah (karena komponen luminer. W secara tidak langsung diatur ke 1.0). Ide bagus Hasil dari produk skalar terlihat seperti ini:


Setelah menerima masterFilter ini , kami siap untuk menentukan kontur objek. Ini tidak sesulit kelihatannya. Algoritma ini sangat mirip dengan yang digunakan untuk mendapatkan ketajaman - kita perlu mendapatkan perbedaan nilai absolut maksimum.

Inilah yang terjadi: kami mencicipi empat texel di samping texel yang sedang diproses (penting: dalam hal ini, ukuran texel adalah 1.0 / 256.0!) Dan menghitung perbedaan absolut maksimum untuk saluran merah dan hijau:

  float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference); 

Sekarang jika kita kalikan filter dengan maxAbsDifference ...


Sangat sederhana dan efisien.

Setelah menerima kontur, kami mengambil sampel peta kontur dari bingkai sebelumnya.

Kemudian, untuk mendapatkan efek "hantu", kami mengambil bagian dari parameter yang dihitung pada pass saat ini dan nilai-nilai dari peta kontur.

Sampaikan salam pada teman lama kami - bilangan bulat integer. Dia hadir di sini. Parameter animasi (cb3 [0] .zw) diambil dari buffer konstan dan berubah seiring waktu.

  float2 outlines = masterFilter * maxAbsDifference; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y 

Catatan: jika Anda ingin menerapkan sendiri naluri penyihir, saya sarankan membatasi kebisingan integer ke interval [-1; 1] (seperti yang dikatakan di situs webnya). Tidak ada batasan dalam shader TW3 asli, tetapi tanpa itu saya mendapat artefak yang mengerikan dan seluruh peta garis besar tidak stabil.

Kemudian kita mengambil sampel peta kontur dengan cara yang sama seperti peta kecerahan sebelumnya (kali ini texel memiliki ukuran 1,0 / 512,0), dan menghitung nilai rata-rata komponen .x:

  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0; 

Kemudian, dilihat dari kode assembler, perbedaan antara rata-rata dan nilai piksel tertentu ini dihitung, setelah itu dilakukan distorsi oleh noise integer:

  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0; 

Langkah selanjutnya adalah mendistorsi nilai dari peta kontur "lama" menggunakan noise - ini adalah garis utama yang memberikan tekstur keluaran rasa gumpal.

Lalu ada perhitungan lain, setelah itu, pada akhirnya, "pelemahan" dihitung.

  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0); 

Berikut adalah video pendek yang menunjukkan peta kerangka sedang beraksi:


Jika Anda tertarik dengan shader piksel penuh, maka tersedia di sini . Shader kompatibel dengan RenderDoc.

Sangat menarik (dan, sejujurnya, sedikit menjengkelkan) bahwa meskipun identitas kode assembler dengan shader asli dari Witcher 3, penampilan akhir dari peta kontur di RenderDoc berubah!

Catatan: di pass terakhir (lihat bagian selanjutnya), Anda akan melihat bahwa hanya saluran .r dari peta kontur yang digunakan. Lalu mengapa kita membutuhkan saluran .g? Saya pikir ini adalah semacam buffer ping-pong dalam satu tekstur - perhatikan bahwa .r berisi saluran .g + beberapa nilai baru.

Bagian 5: The Witcher Flair (Fisheye dan hasil akhir)


Mari kita secara singkat mencantumkan apa yang sudah kita miliki: di bagian pertama, didedikasikan untuk naluri penyihir, dihasilkan peta kecerahan layar penuh yang memberi tahu seberapa besar pengaruhnya tergantung pada jarak. Pada bagian kedua, saya menjelajahi peta garis besar lebih detail, yang bertanggung jawab atas garis besar dan animasi efek selesai.

Kami telah sampai pada tahap terakhir. Semua ini perlu digabungkan! Lulus terakhir adalah quad layar penuh. Input: buffer warna, peta kontur, dan peta luminance.

Kepada:



Setelah:


Sekali lagi saya akan menunjukkan video dengan efek yang diterapkan:


Seperti yang Anda lihat, selain menerapkan kontur ke objek yang dapat dilihat atau didengar Geralt, efek mata ikan diterapkan ke seluruh layar, dan seluruh layar (terutama sudut) menjadi keabu-abuan untuk menyampaikan perasaan pemburu monster yang sebenarnya.

Kode shader piksel rakitan lengkap:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret 

82 baris - jadi kami memiliki banyak pekerjaan yang harus dilakukan!

Pertama, lihat pada data input:

  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x ); 

Nilai utama yang bertanggung jawab atas besarnya efek adalah fisheyeAmount . Saya pikir itu secara bertahap naik dari 0,0 ke 1,0 ketika Geralt mulai menggunakan instingnya. Sisa nilai hampir tidak berubah, tetapi saya menduga beberapa dari mereka akan berbeda jika pengguna telah menonaktifkan efek fisheye dalam opsi (saya tidak memeriksa ini).

Hal pertama yang terjadi di sini adalah shader menghitung topeng yang bertanggung jawab atas sudut abu-abu:

  0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 

Di HLSL, kita dapat menulis ini sebagai berikut:

  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); } 

Pertama, interval [-1; 1] UV dan nilai absolutnya. Lalu ada "pemerasan" yang rumit. Topeng jadi adalah sebagai berikut:


Saya akan kembali ke topeng ini nanti.

Sekarang saya akan dengan sengaja melewati beberapa baris kode dan dengan cermat mempelajari kode yang bertanggung jawab atas efek zoom.

  22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 

Pertama, koordinat tekstur "dua kali lipat" dihitung dan pengurangan float2 (1, 1) dilakukan:

  float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0); 

Texcoord tersebut dapat divisualisasikan sebagai berikut:


Kemudian titik produk skalar (uv4, uv4) dihitung , yang memberi kita topeng:


yang digunakan untuk mengalikan dengan texcoords di atas:


Penting: di sudut kiri atas (piksel hitam) nilainya negatif. Mereka ditampilkan dalam warna hitam (0,0) karena keakuratan format R11G11B10_FLOAT terbatas. Tidak memiliki bit tanda, jadi nilai negatif tidak dapat disimpan di dalamnya.

Kemudian koefisien atenuasi dihitung (seperti yang saya katakan di atas, fisheyeAmount bervariasi dari 0,0 hingga 1,0).

  float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation; 

Kemudian dilakukan pembatasan (maks / mnt) dan satu perkalian.

Dengan demikian, offset dihitung. Untuk menghitung akhir uv, yang akan digunakan untuk sampel tekstur warna, kami cukup melakukan pengurangan:

float2 colorUV = mainUv - offset;

Dengan mengambil sampel warna input , tekstur warna UV , kami mendapatkan gambar yang terdistorsi di dekat sudut:


Garis besar


Langkah selanjutnya adalah mengambil sampel peta kontur untuk menemukan kontur. Ini cukup sederhana, pertama kita menemukan texcoords untuk mencicipi kontur objek yang menarik, dan kemudian melakukan hal yang sama untuk trek:

  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y 


Objek menarik dari peta kontur


Jejak dari peta kontur

Perlu dicatat bahwa kami hanya mencicipi saluran .x dari peta kontur dan hanya memperhitungkan kotak atas.

Gerakan


Untuk menerapkan pergerakan trek, trik yang hampir sama digunakan seperti efek keracunan. Lingkaran berukuran satuan ditambahkan dan kami mencicipi 8 kali peta garis besar untuk objek dan jejak yang menarik, serta tekstur warna.

Perhatikan bahwa kami hanya membagi jalur yang ditemukan dengan 8.0.

Karena kita berada dalam ruang koordinat tekstur [0-1] 2 , keberadaan lingkaran jari-jari 1 untuk melingkari satu piksel akan membuat artefak yang tidak dapat diterima:


Karena itu, sebelum melanjutkan, mari kita cari tahu bagaimana jari-jari ini dihitung. Untuk melakukan ini, kita perlu kembali ke garis yang hilang 15-21. Masalah kecil dengan menghitung jari-jari ini adalah bahwa perhitungannya tersebar di sekitar shader (mungkin karena optimasi shader oleh kompiler). Oleh karena itu, inilah bagian pertama (15-21) dan yang kedua (41-42):

  15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 

Seperti yang Anda lihat, kami hanya mempertimbangkan texels dari [0,00 - 0,03] di sebelah setiap permukaan, merangkum nilainya, kalikan 20 dan jenuh. Seperti apa bentuknya setelah baris 15-21:


Dan inilah bagaimana setelah baris 41:


Pada baris 42, kita kalikan ini dengan 0,03, nilai ini adalah jari-jari lingkaran untuk seluruh layar. Seperti yang Anda lihat, semakin dekat ke tepi layar, jari-jari menjadi lebih kecil.

Sekarang kita bisa melihat kode assembler yang bertanggung jawab atas pergerakan:

  40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 

Mari kita tetap di sini sebentar. Pada baris 40 kita mendapatkan koefisien waktu - hanya elapsedTime * 0.1 . Pada baris 43 kami memiliki buffer untuk tekstur warna yang diperoleh di dalam loop.

r0.x (baris 41-42) adalah, seperti yang kita ketahui sekarang, jari-jari lingkaran. r4.x (baris 44) adalah garis besar objek yang menarik, r4.y (baris 45) adalah garis besar trek (sebelumnya dibagi 8!), dan r4.z (baris 46) adalah penghitung lingkaran.

Seperti yang Anda harapkan, loop memiliki 8 iterasi. Kita mulai dengan menghitung sudut dalam radian i * PI_4 , yang memberi kita 2 * PI - lingkaran penuh. Sudut terdistorsi dari waktu ke waktu.

Dengan menggunakan sincos, kami menentukan titik pengambilan sampel (satuan lingkaran) dan mengubah jari-jari menggunakan perkalian (baris 54).

Setelah itu, kita berkeliling pixel dalam lingkaran dan mencicipi kontur dan warna. Setelah siklus, kami mendapatkan nilai rata-rata (karena membaginya dengan 8) dari kontur dan warna.

  float timeParam = time * 0.1; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; } 

Pengambilan sampel warna akan dilakukan dengan cara yang hampir sama, tetapi kami akan menambahkan offset yang dikalikan dengan lingkaran "tunggal" ke colorUV dasar .

Kecerahan


Setelah siklus, kami mencicipi peta kecerahan dan mengubah nilai kecerahan akhir (karena peta kecerahan tidak tahu apa-apa tentang kontur):

  67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 

Kode HLSL:

  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces ); 

Sudut abu-abu dan penyatuan akhir segalanya


Warna abu-abu lebih dekat ke sudut dihitung menggunakan produk skalar (assembler line 69):

  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx; 


Kemudian dua interpolasi mengikuti. Yang pertama menggabungkan abu-abu dengan "warna dalam lingkaran" menggunakan topeng pertama yang saya jelaskan, sehingga sudut menjadi abu-abu. Selain itu, ada koefisien 0,6, yang mengurangi saturasi gambar akhir:


Yang kedua menggabungkan warna pertama dengan yang di atas menggunakan fisheyeAmount . Ini berarti bahwa layar secara bertahap menjadi lebih gelap (karena perkalian dengan 0,6) dan abu-abu di sudut-sudut! Cerdik.

HLSL:

  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount ); 

Sekarang kita bisa beralih ke menambahkan kontur objek.

Warna (merah dan kuning) diambil dari buffer konstan.

  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting; 


Fuh! Kita hampir sampai di garis finish!

Kami memiliki warna akhir, ada warna naluri penyihir ... tetap saja entah bagaimana menggabungkannya!

Dan untuk ini, penambahan sederhana tidak cocok. Pertama kami menghitung produk skalar:

  78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); 

yang terlihat seperti ini:


Dan nilai-nilai ini pada akhir digunakan untuk menginterpolasi antara warna dan bakat penyihir (jenuh):

  76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 ); 


Dan itu saja.

Shader lengkap tersedia di sini .

Perbandingan shader saya (kiri) dan asli (kanan):


Saya harap Anda menikmati artikel ini! Ada banyak ide cemerlang dalam mekanisme “naluri penyihir”, dan hasil akhirnya sangat masuk akal.

[Bagian sebelumnya dari analisis: pertama dan kedua .]

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


All Articles